510 lines
21 KiB
JavaScript
510 lines
21 KiB
JavaScript
// Slide thumbnail capture helpers for player-sourced screenshots.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const vm = require('vm');
|
|
const {
|
|
escapeHtml,
|
|
mediaKind,
|
|
renderEditorJsContent,
|
|
sanitizeFontFamily,
|
|
sanitizeFontSize,
|
|
sanitizeTextColor
|
|
} = require('#src/player/render-helpers');
|
|
const { createRequestAuthHeaders, createPageAuthToken } = require('#src/request-auth');
|
|
|
|
const placeholderUtils = (() => {
|
|
const sandbox = { window: {} };
|
|
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '..', '..', 'public', 'js', 'shared', 'placeholder-utils.js'), 'utf8'), sandbox);
|
|
return sandbox.window.placeholderUtils || {};
|
|
})();
|
|
|
|
const SYSTEM_CHROMIUM_PATHS = [
|
|
process.env.PUPPETEER_EXECUTABLE_PATH,
|
|
process.env.CHROMIUM_PATH,
|
|
'/usr/bin/chromium',
|
|
'/usr/bin/chromium-browser',
|
|
'/usr/local/bin/chromium',
|
|
'/snap/bin/chromium'
|
|
].filter(Boolean);
|
|
const PLAYER_VIEWPORT = {
|
|
width: 1920,
|
|
height: 1080,
|
|
deviceScaleFactor: 1
|
|
};
|
|
const THUMBNAIL_MAX_SIZE = {
|
|
width: 480,
|
|
height: 270
|
|
};
|
|
|
|
function normalizeBaseUrl(baseUrl) {
|
|
return String(baseUrl || '').trim().replace(/\/$/, '');
|
|
}
|
|
|
|
function resolveAssetUrl(baseUrl, value) {
|
|
const raw = String(value || '').trim();
|
|
if (!raw) {
|
|
return '';
|
|
}
|
|
if (/^(?:https?:)?\/\//i.test(raw) || raw.startsWith('data:')) {
|
|
return raw;
|
|
}
|
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
if (!normalizedBaseUrl) {
|
|
return raw;
|
|
}
|
|
if (raw.startsWith('/')) {
|
|
return normalizedBaseUrl + raw;
|
|
}
|
|
return normalizedBaseUrl + '/' + raw.replace(/^\/+/, '');
|
|
}
|
|
|
|
function normalizeRenderableValue(value) {
|
|
if (value && typeof value === 'object') {
|
|
if (value.value !== undefined) {
|
|
return normalizeRenderableValue(value.value);
|
|
}
|
|
if (value.text !== undefined) {
|
|
return normalizeRenderableValue(value.text);
|
|
}
|
|
if (value.html !== undefined) {
|
|
return normalizeRenderableValue(value.html);
|
|
}
|
|
if (value.url !== undefined) {
|
|
return normalizeRenderableValue(value.url);
|
|
}
|
|
if (value.href !== undefined) {
|
|
return normalizeRenderableValue(value.href);
|
|
}
|
|
if (value.src !== undefined) {
|
|
return normalizeRenderableValue(value.src);
|
|
}
|
|
if (value.content !== undefined) {
|
|
return normalizeRenderableValue(value.content);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
return String(value === undefined || value === null ? '' : value);
|
|
}
|
|
|
|
function getCanvasSize(slide) {
|
|
const template = slide && slide.template ? slide.template : null;
|
|
return {
|
|
width: Math.max(1, Number(template && template.canvas_size_width ? template.canvas_size_width : 1920)),
|
|
height: Math.max(1, Number(template && template.canvas_size_height ? template.canvas_size_height : 1080))
|
|
};
|
|
}
|
|
|
|
function getRegionContent(slide, region) {
|
|
const content = slide && slide.content && slide.content[region.region_key] ? slide.content[region.region_key] : {};
|
|
return content && typeof content === 'object' ? content : { value: content };
|
|
}
|
|
|
|
function getThumbnailCanvasSize(slide) {
|
|
const template = slide && slide.template ? slide.template : null;
|
|
return {
|
|
width: Math.max(1, Number(template && template.canvas_size_width ? template.canvas_size_width : 1920)),
|
|
height: Math.max(1, Number(template && template.canvas_size_height ? template.canvas_size_height : 1080))
|
|
};
|
|
}
|
|
|
|
function buildThumbnailRegionStyle(region, canvasWidth, canvasHeight) {
|
|
const left = Number.isFinite(Number(region && region.x)) && canvasWidth > 0 ? (Number(region.x) / canvasWidth) * 100 : 0;
|
|
const top = Number.isFinite(Number(region && region.y)) && canvasHeight > 0 ? (Number(region.y) / canvasHeight) * 100 : 0;
|
|
const width = Number.isFinite(Number(region && region.width)) && canvasWidth > 0 ? (Number(region.width) / canvasWidth) * 100 : 0;
|
|
const height = Number.isFinite(Number(region && region.height)) && canvasHeight > 0 ? (Number(region.height) / canvasHeight) * 100 : 0;
|
|
|
|
return 'left:' + left + '%;top:' + top + '%;width:' + width + '%;height:' + height + '%;z-index:' + Number(region && region.z_index || 0) + ';';
|
|
}
|
|
|
|
function hasVisibleContent(html) {
|
|
var raw = String(html || '').trim();
|
|
if (!raw) {
|
|
return false;
|
|
}
|
|
|
|
if (/<img\b/i.test(raw)) {
|
|
return true;
|
|
}
|
|
|
|
return Boolean(raw.replace(/<[^>]+>/g, '').trim());
|
|
}
|
|
|
|
function resolvePlaceholderPath(value, expression) {
|
|
const pathValue = String(expression || '').replace(/\.image\s*\([^)]*\)\s*$/i, '').trim();
|
|
return pathValue.split('.').reduce(function (current, segment) {
|
|
return current === undefined || current === null ? '' : current[segment];
|
|
}, value);
|
|
}
|
|
|
|
function getImagePlaceholderConfig(expression) {
|
|
const match = String(expression || '').match(/\.image\s*\(\s*([0-9]+)?\s*,?\s*([0-9]+)?\s*\)/i);
|
|
return {
|
|
width: match && match[1] ? Number(match[1]) : 0,
|
|
height: match && match[2] ? Number(match[2]) : 0
|
|
};
|
|
}
|
|
|
|
function substitutePlaceholders(html, regionContent, options) {
|
|
const source = String(html || '');
|
|
const type = String(regionContent && regionContent.type || '').trim().toLowerCase();
|
|
const item = options && typeof options.getItem === 'function' ? options.getItem(type, regionContent) : null;
|
|
if (!item) return source;
|
|
return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/gi, function (_match, expression) {
|
|
const resolved = typeof placeholderUtils.resolvePlaceholderExpression === 'function'
|
|
? placeholderUtils.resolvePlaceholderExpression(item, expression)
|
|
: resolvePlaceholderPath(item, expression);
|
|
const value = typeof placeholderUtils.formatPlaceholderValue === 'function' ? placeholderUtils.formatPlaceholderValue(resolved) : String(resolved || '');
|
|
if (typeof placeholderUtils.isImagePlaceholderExpression !== 'function' || !placeholderUtils.isImagePlaceholderExpression(expression)) {
|
|
return escapeHtml(value);
|
|
}
|
|
const remoteUrl = String(value || '').trim();
|
|
if (!/^https?:\/\//i.test(remoteUrl)) return '';
|
|
const imageConfig = typeof placeholderUtils.getImagePlaceholderConfig === 'function' ? placeholderUtils.getImagePlaceholderConfig(expression) : getImagePlaceholderConfig(expression);
|
|
const cacheFile = options && typeof options.getCachedImagePath === 'function' ? options.getCachedImagePath(remoteUrl) : '';
|
|
const imageUrl = cacheFile || remoteUrl;
|
|
const style = imageConfig.width && imageConfig.height
|
|
? 'display:block;width:' + imageConfig.width + 'px;height:' + imageConfig.height + 'px;object-fit:contain;'
|
|
: 'display:block;max-width:' + (imageConfig.width || 100) + 'px;max-height:' + (imageConfig.height || 100) + 'px;width:auto;height:auto;';
|
|
return '<img src="' + escapeHtml(resolveAssetUrl(options && options.baseUrl, imageUrl)) + '" alt="" style="' + style + '" />';
|
|
});
|
|
}
|
|
|
|
function buildTextRegionMarkup(region, regionContent, options) {
|
|
const fontFamily = sanitizeFontFamily(regionContent.font_family || region.font_family);
|
|
const fontSize = sanitizeFontSize(regionContent.font_size || region.font_size);
|
|
const fontColor = sanitizeTextColor(regionContent.font_color || region.font_color);
|
|
const renderedBody = renderEditorJsContent(substitutePlaceholders(regionContent.value || '', regionContent, options));
|
|
if (!hasVisibleContent(renderedBody)) {
|
|
return '';
|
|
}
|
|
|
|
return '<div class="slide-preview-region slide-preview-text-region" style="' + region.baseStyle + '"><div class="slide-preview-text-content" style="width:' + region.pixelWidth + 'px;height:' + region.pixelHeight + 'px;overflow:hidden;' + (fontFamily ? 'font-family:' + escapeHtml(fontFamily) + ';' : '') + 'font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor || '#000000') + ';">' + renderedBody + '</div></div>';
|
|
}
|
|
|
|
function buildRegionInnerHtml(region, regionContent, baseUrl, options) {
|
|
const regionType = String(regionContent.type || region.region_type || 'text').trim().toLowerCase();
|
|
const rawValue = normalizeRenderableValue(regionContent && regionContent.value !== undefined ? regionContent.value : '');
|
|
|
|
if (regionType === 'image') {
|
|
const src = resolveAssetUrl(baseUrl, rawValue);
|
|
return src
|
|
? '<div class="slide-preview-region slide-preview-image-region" style="' + region.baseStyle + '"><img class="slide-preview-image" src="' + escapeHtml(src) + '" alt="' + escapeHtml(region.label || region.region_key || 'image') + '" /></div>'
|
|
: '';
|
|
}
|
|
|
|
if (regionType === 'video') {
|
|
const src = resolveAssetUrl(baseUrl, rawValue);
|
|
return src
|
|
? '<div class="slide-preview-region slide-preview-video-region" style="' + region.baseStyle + '"><video class="slide-preview-video" src="' + escapeHtml(src) + '" title="' + escapeHtml(region.label || region.region_key || 'video') + '" muted playsinline preload="metadata"></video></div>'
|
|
: '';
|
|
}
|
|
|
|
if (regionType === 'webpage') {
|
|
const src = resolveAssetUrl(baseUrl, rawValue);
|
|
return src
|
|
? '<div class="slide-preview-region slide-preview-webpage-region" style="' + region.baseStyle + '"><iframe src="' + escapeHtml(src) + '" title="' + escapeHtml(region.label || region.region_key || 'webpage') + '" loading="eager" referrerpolicy="no-referrer" scrolling="no" style="width:100%;height:100%;border:0;display:block;background:#fff;overflow:hidden;"></iframe></div>'
|
|
: '';
|
|
}
|
|
|
|
if (regionType === 'qr-code') {
|
|
const src = String(regionContent.qr_preview || '').trim();
|
|
const borderRadius = Math.max(0, Math.round(Number(regionContent.qr_border_radius || 0)));
|
|
const radiusStyle = borderRadius > 0 ? ' style="border-radius:' + borderRadius + 'px;overflow:hidden;"' : '';
|
|
return src
|
|
? '<div class="slide-preview-region slide-preview-qr-code-region" style="' + region.baseStyle + radiusStyle + '"><img class="slide-preview-image" src="' + escapeHtml(src) + '" alt="' + escapeHtml(region.label || region.region_key || 'qr code') + '" /></div>'
|
|
: '';
|
|
}
|
|
|
|
if (regionType === 'html') {
|
|
const html = String(rawValue || '').trim();
|
|
return html
|
|
? '<div class="slide-preview-region slide-preview-html-region" style="' + region.baseStyle + '"><iframe sandbox="" allowtransparency="true" srcdoc="' + escapeHtml('<!doctype html><html><head><style>html,body{margin:0;width:100%;height:100%;overflow:hidden;background:transparent;}</style></head><body>' + html + '</body></html>') + '" title="' + escapeHtml(region.label || region.region_key || 'html') + '" loading="eager" scrolling="no" style="width:100%;height:100%;border:0;display:block;background:transparent;overflow:hidden;"></iframe></div>'
|
|
: '';
|
|
}
|
|
|
|
if (regionType === 'rtmp') {
|
|
const label = String(rawValue || '').trim() || 'RTMP source';
|
|
return '<div class="slide-preview-region slide-preview-rtmp-region" style="' + region.baseStyle + '"><div class="slide-preview-rtmp-placeholder">' + escapeHtml(label) + '</div></div>';
|
|
}
|
|
|
|
return buildTextRegionMarkup(region, regionContent, Object.assign({}, options, { baseUrl: baseUrl }));
|
|
}
|
|
|
|
async function loadChromium() {
|
|
const chromiumModule = await import('@sparticuz/chromium');
|
|
const resolved = chromiumModule && chromiumModule.default ? chromiumModule.default : chromiumModule;
|
|
return resolved;
|
|
}
|
|
|
|
async function loadPuppeteer() {
|
|
return require('puppeteer-core');
|
|
}
|
|
|
|
function loadSharp() {
|
|
return require('sharp');
|
|
}
|
|
|
|
function buildThumbnailPreviewMarkup(slide, baseUrl, options) {
|
|
const template = slide && slide.template ? slide.template : null;
|
|
if (!template) {
|
|
return '';
|
|
}
|
|
|
|
const canvasSize = getThumbnailCanvasSize(slide);
|
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
return (Array.isArray(template.regions) ? template.regions : []).map(function (region) {
|
|
const regionContent = getRegionContent(slide, region);
|
|
const previewRegion = Object.assign({}, region, {
|
|
baseStyle: buildThumbnailRegionStyle(region, canvasSize.width, canvasSize.height),
|
|
pixelWidth: Math.max(1, Math.round(Number(region && region.width || 0) || 1)),
|
|
pixelHeight: Math.max(1, Math.round(Number(region && region.height || 0) || 1))
|
|
});
|
|
return buildRegionInnerHtml(previewRegion, regionContent, normalizedBaseUrl, options);
|
|
}).join('');
|
|
}
|
|
|
|
function buildThumbnailPreviewPayload(slide, options) {
|
|
const template = slide && slide.template ? slide.template : null;
|
|
const canvasSize = getThumbnailCanvasSize(slide);
|
|
return {
|
|
thumbnailPreview: true,
|
|
canvasWidth: canvasSize.width,
|
|
canvasHeight: canvasSize.height,
|
|
backgroundColor: template && template.background_color ? String(template.background_color) : '#111111',
|
|
backgroundImagePath: template && template.background_image_path ? String(template.background_image_path) : '',
|
|
fontStylesheetHref: String(options && options.fontStylesheetHref || '').trim(),
|
|
html: buildThumbnailPreviewMarkup(slide, options && options.baseUrl, options)
|
|
};
|
|
}
|
|
|
|
async function launchBrowser() {
|
|
const puppeteer = await loadPuppeteer();
|
|
const chromium = await loadChromium();
|
|
let executablePath = SYSTEM_CHROMIUM_PATHS.find(function (candidate) {
|
|
return fs.existsSync(candidate);
|
|
}) || '';
|
|
const usingSystemChromium = Boolean(executablePath);
|
|
|
|
if (!executablePath && chromium && typeof chromium.executablePath === 'function') {
|
|
executablePath = await chromium.executablePath();
|
|
}
|
|
|
|
if (!executablePath || !fs.existsSync(executablePath)) {
|
|
throw new Error('Chromium executable was not found.');
|
|
}
|
|
|
|
if (usingSystemChromium) {
|
|
return puppeteer.launch({
|
|
args: [
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
'--disable-dev-shm-usage',
|
|
'--disable-gpu'
|
|
],
|
|
defaultViewport: { width: 1920, height: 1080, deviceScaleFactor: 1 },
|
|
executablePath: executablePath,
|
|
headless: true
|
|
});
|
|
}
|
|
|
|
return puppeteer.launch({
|
|
args: puppeteer.defaultArgs({
|
|
args: chromium && chromium.args ? chromium.args : [],
|
|
headless: 'shell'
|
|
}),
|
|
defaultViewport: chromium && chromium.defaultViewport ? chromium.defaultViewport : null,
|
|
executablePath: executablePath,
|
|
headless: 'shell'
|
|
});
|
|
}
|
|
|
|
async function captureSlideThumbnail(options) {
|
|
const sharp = loadSharp();
|
|
const pool = options && options.pool;
|
|
const common = options && options.common;
|
|
const mediaDir = String(options && options.mediaDir || '').trim();
|
|
const baseUrl = normalizeBaseUrl(options && options.baseUrl);
|
|
const slideId = Number(options && options.slideId || 0);
|
|
const previousThumbnailPath = String(options && options.previousThumbnailPath || '').trim();
|
|
|
|
if (!pool || !common || !mediaDir || !Number.isFinite(slideId) || slideId <= 0) {
|
|
throw new Error('captureSlideThumbnail requires pool, common, mediaDir, and slideId.');
|
|
}
|
|
|
|
const slide = await common.fetchSlideById(pool, slideId);
|
|
if (!slide) {
|
|
throw new Error('Slide not found.');
|
|
}
|
|
|
|
const rssFeedsData = typeof common.fetchRssFeedsData === 'function' ? await common.fetchRssFeedsData(pool) : { rssFeeds: [] };
|
|
const rssFeeds = await Promise.all((rssFeedsData.rssFeeds || []).map(async function (feed) {
|
|
const items = typeof common.fetchRssFeedItemsByFeedId === 'function' ? await common.fetchRssFeedItemsByFeedId(pool, feed.id) : [];
|
|
return Object.assign({}, feed, { items: items });
|
|
}));
|
|
const apiSourcesData = typeof common.fetchApiSourcesData === 'function' ? await common.fetchApiSourcesData(pool) : { apiSources: [] };
|
|
const apiSources = (apiSourcesData.apiSources || []).map(function (source) {
|
|
return Object.assign({}, source, { responseJson: common.parseJsonSafe ? common.parseJsonSafe(source.last_response_json) : null });
|
|
});
|
|
|
|
function getApiItems(source) {
|
|
const response = source && source.responseJson;
|
|
const itemsPath = String(source && source.items_path || '').trim();
|
|
if (itemsPath) {
|
|
const selected = itemsPath.split('.').reduce(function (current, segment) {
|
|
return current === undefined || current === null ? '' : current[segment];
|
|
}, response);
|
|
return Array.isArray(selected) ? selected : [];
|
|
}
|
|
if (Array.isArray(response)) return response;
|
|
if (response && Array.isArray(response.items)) return response.items;
|
|
if (response && Array.isArray(response.results)) return response.results;
|
|
if (response && Array.isArray(response.data)) return response.data;
|
|
return response ? [response] : [];
|
|
}
|
|
|
|
function getThumbnailItem(type, regionContent) {
|
|
const index = Math.max(0, Math.max(1, Number(regionContent && regionContent.item_number || 1)) - 1);
|
|
if (type === 'api') {
|
|
const source = apiSources.find(function (entry) { return Number(entry.id) === Number(regionContent.source_id); });
|
|
return getApiItems(source)[index] || null;
|
|
}
|
|
if (type === 'rss') {
|
|
const feed = rssFeeds.find(function (entry) { return Number(entry.id) === Number(regionContent.feed_id); });
|
|
return feed && Array.isArray(feed.items) ? feed.items[index] || null : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getCachedImagePath(remoteUrl) {
|
|
const hash = crypto.createHash('sha256').update(String(remoteUrl || '')).digest('hex');
|
|
const cacheDir = path.join(mediaDir, 'player-cache', 'remote-images');
|
|
const candidates = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'];
|
|
const candidate = candidates.map(function (extension) { return path.join(cacheDir, hash + extension); }).find(function (filePath) { return fs.existsSync(filePath); });
|
|
return candidate ? '/media/player-cache/remote-images/' + path.basename(candidate) : '';
|
|
}
|
|
|
|
const thumbnailDir = path.join(mediaDir, 'thumbnails');
|
|
const thumbnailRelativePath = String(slide.thumbnail_path || '').trim() || '/media/thumbnails/slides/slide-' + slide.id + '.png';
|
|
const filePath = path.join(mediaDir, thumbnailRelativePath.replace(/^\/+media\//, ''));
|
|
const fullSizePath = filePath.replace(/\.png$/i, '.full.png');
|
|
const thumbnailTempPath = filePath.replace(/\.png$/i, '.tmp.png');
|
|
const thumbnailPath = thumbnailRelativePath;
|
|
|
|
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
|
|
async function waitForThumbnailRender(page) {
|
|
await page.waitForFunction(function () {
|
|
return document.readyState === 'complete' && Boolean(document.querySelector('#popup-preview-canvas'));
|
|
}, { timeout: 30000 });
|
|
|
|
await page.waitForFunction(function () {
|
|
var canvas = document.querySelector('#popup-preview-canvas');
|
|
if (!canvas) {
|
|
return false;
|
|
}
|
|
|
|
var images = Array.prototype.slice.call(canvas.querySelectorAll('img'));
|
|
return images.every(function (image) {
|
|
return image.complete && typeof image.naturalWidth === 'number';
|
|
});
|
|
}, { timeout: 30000 });
|
|
|
|
await page.evaluate(async function () {
|
|
if (document.fonts && document.fonts.ready) {
|
|
try {
|
|
await document.fonts.ready;
|
|
} catch (_error) {
|
|
// Ignore font readiness failures and fall back to the rendered frame.
|
|
}
|
|
}
|
|
});
|
|
|
|
await page.evaluate(function () {
|
|
return new Promise(function (resolve) {
|
|
window.requestAnimationFrame(function () {
|
|
window.requestAnimationFrame(resolve);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
const browser = await launchBrowser();
|
|
try {
|
|
const page = await browser.newPage();
|
|
try {
|
|
const previewPayload = buildThumbnailPreviewPayload(slide, {
|
|
baseUrl: baseUrl,
|
|
fontStylesheetHref: options && options.fontStylesheetHref ? options.fontStylesheetHref : '',
|
|
getItem: getThumbnailItem,
|
|
getCachedImagePath: getCachedImagePath
|
|
});
|
|
const previewPath = '/slides/popup-preview';
|
|
const previewUrl = baseUrl + previewPath + '#' + encodeURIComponent(JSON.stringify(previewPayload));
|
|
await page.setViewport({
|
|
width: Math.max(1, Number(previewPayload.canvasWidth || PLAYER_VIEWPORT.width)),
|
|
height: Math.max(1, Number(previewPayload.canvasHeight || PLAYER_VIEWPORT.height)),
|
|
deviceScaleFactor: 1
|
|
});
|
|
await page.setExtraHTTPHeaders(Object.assign({}, createRequestAuthHeaders({
|
|
method: 'GET',
|
|
pathname: previewPath
|
|
}), {
|
|
'x-pulse-page-auth': createPageAuthToken({ scope: 'thumbnail-preview', slideId: slide.id })
|
|
}));
|
|
const previewResponse = await page.goto(previewUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
if (!previewResponse || previewResponse.status() >= 400) {
|
|
throw new Error('Popup preview returned HTTP ' + (previewResponse ? previewResponse.status() : 'no response') + '.');
|
|
}
|
|
await waitForThumbnailRender(page);
|
|
const canvas = await page.$('#popup-preview-canvas');
|
|
if (!canvas) {
|
|
throw new Error('Popup preview did not produce a slide canvas.');
|
|
}
|
|
await canvas.screenshot({ path: fullSizePath });
|
|
} finally {
|
|
await page.close().catch(function () {
|
|
return null;
|
|
});
|
|
}
|
|
} finally {
|
|
await browser.close().catch(function () {
|
|
return null;
|
|
});
|
|
}
|
|
|
|
await sharp(fullSizePath)
|
|
.resize({
|
|
width: THUMBNAIL_MAX_SIZE.width,
|
|
height: THUMBNAIL_MAX_SIZE.height,
|
|
fit: 'inside',
|
|
withoutEnlargement: true
|
|
})
|
|
.png()
|
|
.toFile(thumbnailTempPath);
|
|
|
|
await fs.promises.rm(filePath, { force: true });
|
|
await fs.promises.rename(thumbnailTempPath, filePath);
|
|
await fs.promises.unlink(fullSizePath).catch(function (error) {
|
|
if (!error || error.code !== 'ENOENT') {
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
await pool.query('UPDATE c_slides SET thumbnail_path = ? WHERE id = ?', [thumbnailPath, slide.id]);
|
|
return {
|
|
slideId: slide.id,
|
|
thumbnailPath: thumbnailPath,
|
|
filePath: filePath,
|
|
fullSizePath: fullSizePath,
|
|
mediaKind: mediaKind('')
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
captureSlideThumbnail: captureSlideThumbnail,
|
|
buildThumbnailPreviewPayload: buildThumbnailPreviewPayload,
|
|
buildThumbnailPreviewMarkup: buildThumbnailPreviewMarkup
|
|
}; |