From f23e462cb15e483dfe174e50abd2f5890b77f668 Mon Sep 17 00:00:00 2001 From: Daniel Petri Rocha Date: Wed, 1 Jun 2022 23:22:08 +0200 Subject: [PATCH] Use probe-image-size for dimensions; embed SVGs in place of PNGs --- export-annotations/package-lock.json | 187 +++++++++++++++++++----- export-annotations/package.json | 2 +- export-annotations/workers/collector.js | 64 ++------ export-annotations/workers/process.js | 32 ++-- 4 files changed, 186 insertions(+), 99 deletions(-) diff --git a/export-annotations/package-lock.json b/export-annotations/package-lock.json index f5b4dff8d7..6508301e11 100644 --- a/export-annotations/package-lock.json +++ b/export-annotations/package-lock.json @@ -10,8 +10,8 @@ "dependencies": { "axios": "^0.26.0", "form-data": "^4.0.0", - "image-size": "^1.0.1", "perfect-freehand": "^1.0.16", + "probe-image-size": "^7.2.3", "redis": "^4.0.3", "sanitize-filename": "^1.6.3", "twemoji": "^14.0.2", @@ -153,6 +153,14 @@ "node": ">= 0.8" } }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -239,25 +247,17 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, - "node_modules/image-size": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.1.tgz", - "integrity": "sha512-VAwkvNSNGClRw9mDHhc5Efax8PLlsOGcUTh0T/LIriC8vPA3U5PdqXWqkz406MoYHMKW8Uf9gWr05T/rYB44kQ==", + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" + "safer-buffer": ">= 2.1.2 < 3" }, "engines": { - "node": ">=12.0.0" + "node": ">=0.10.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, "node_modules/jsonfile": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz", @@ -269,6 +269,11 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "node_modules/mime-db": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", @@ -288,17 +293,40 @@ "node": ">= 0.6" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/needle": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", + "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, "node_modules/perfect-freehand": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.0.16.tgz", "integrity": "sha512-D4+avUeR8CHSl2vaPbPYX/dNpSMRYO3VOFp7qSSc+LRkSgzQbLATVnXosy7VxtsSHEh1C5t8K8sfmo0zCVnfWQ==" }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "node_modules/probe-image-size": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/probe-image-size/-/probe-image-size-7.2.3.tgz", + "integrity": "sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==", "dependencies": { - "inherits": "~2.0.3" + "lodash.merge": "^4.6.2", + "needle": "^2.5.2", + "stream-parser": "~0.3.1" } }, "node_modules/redis": { @@ -333,6 +361,11 @@ "node": ">=4" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/sanitize-filename": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", @@ -341,11 +374,37 @@ "truncate-utf8-bytes": "^1.0.0" } }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "node_modules/stream-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz", + "integrity": "sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=", + "dependencies": { + "debug": "2" + } + }, + "node_modules/stream-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/stream-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -529,6 +588,14 @@ "delayed-stream": "~1.0.0" } }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -584,19 +651,14 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, - "image-size": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.1.tgz", - "integrity": "sha512-VAwkvNSNGClRw9mDHhc5Efax8PLlsOGcUTh0T/LIriC8vPA3U5PdqXWqkz406MoYHMKW8Uf9gWr05T/rYB44kQ==", + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "requires": { - "queue": "6.0.2" + "safer-buffer": ">= 2.1.2 < 3" } }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, "jsonfile": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz", @@ -606,6 +668,11 @@ "universalify": "^0.1.2" } }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "mime-db": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", @@ -619,17 +686,34 @@ "mime-db": "1.51.0" } }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "needle": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", + "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, "perfect-freehand": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.0.16.tgz", "integrity": "sha512-D4+avUeR8CHSl2vaPbPYX/dNpSMRYO3VOFp7qSSc+LRkSgzQbLATVnXosy7VxtsSHEh1C5t8K8sfmo0zCVnfWQ==" }, - "queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "probe-image-size": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/probe-image-size/-/probe-image-size-7.2.3.tgz", + "integrity": "sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==", "requires": { - "inherits": "~2.0.3" + "lodash.merge": "^4.6.2", + "needle": "^2.5.2", + "stream-parser": "~0.3.1" } }, "redis": { @@ -658,6 +742,11 @@ "redis-errors": "^1.0.0" } }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "sanitize-filename": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", @@ -666,11 +755,39 @@ "truncate-utf8-bytes": "^1.0.0" } }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "stream-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz", + "integrity": "sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=", + "requires": { + "debug": "2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, "truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", diff --git a/export-annotations/package.json b/export-annotations/package.json index 2d8dccdae9..0cc3a363e8 100644 --- a/export-annotations/package.json +++ b/export-annotations/package.json @@ -8,8 +8,8 @@ "dependencies": { "axios": "^0.26.0", "form-data": "^4.0.0", - "image-size": "^1.0.1", "perfect-freehand": "^1.0.16", + "probe-image-size": "^7.2.3", "redis": "^4.0.3", "sanitize-filename": "^1.6.3", "twemoji": "^14.0.2", diff --git a/export-annotations/workers/collector.js b/export-annotations/workers/collector.js index 8cf604ba41..8e8a5fa9f7 100644 --- a/export-annotations/workers/collector.js +++ b/export-annotations/workers/collector.js @@ -2,8 +2,7 @@ const Logger = require('../lib/utils/logger'); const config = require('../config'); const fs = require('fs'); const redis = require('redis'); -const { execSync } = require("child_process"); -const { Worker, workerData, parentPort } = require('worker_threads') +const { Worker, workerData, parentPort } = require('worker_threads'); const path = require('path'); const jobId = workerData; @@ -46,6 +45,8 @@ let exportJob = JSON.parse(job); // Remove annotations from Redis await client.DEL(jobId); + client.disconnect(); + let annotations = JSON.stringify(presAnn); let whiteboard = JSON.parse(annotations); @@ -56,60 +57,28 @@ let exportJob = JSON.parse(job); }); // Collect the Presentation Page files from the presentation directory + + // PDF / PNG / JPEG file let presentationFile = path.join(exportJob.presLocation, exportJob.presId); - let pdfFileExists = fs.existsSync(`${presentationFile}.pdf`); + // Use the SVG files as shown in the browser in order to avoid incorrect dimensions + // Tldraw uses absolute coordinates + for (let p of pages) { let pageNumber = p.page; + let svgFile = path.join(exportJob.presLocation, 'svgs', `slide${pageNumber}.svg`) let outputFile = path.join(dropbox, `slide${pageNumber}`); + let svgFileExists = fs.existsSync(svgFile); + + // CairoSVG doesn't handle transparent SVG embeds properly, e.g., for transparent pictures. + // In these cases we reference the PNG image - if(pdfFileExists) { - let extactSlideAsPDFCommands = [ - 'pdftocairo', - '-png', - '-scale-to', '1920', - '-f', pageNumber, - '-l', pageNumber, - '-singlefile', - `${presentationFile}.pdf`, - outputFile - ].join(' '); - - execSync(extactSlideAsPDFCommands, (error, stderr) => { - if (error) { - return logger.error(`PDFtoCairo failed with error: ${error.message}`); - } - - if (stderr) { - return logger.error(`PDFtoCairo failed with stderr: ${stderr}`); - } - }) - } - - else if (fs.existsSync(`${presentationFile}.png`)) { + if (fs.existsSync(`${presentationFile}.png`)) { fs.copyFileSync(`${presentationFile}.png`, `${outputFile}.png`); } - - else if (fs.existsSync(`${presentationFile}.jpeg`)) { - let convertImageToPngCommands = [ - 'convert', - `${presentationFile}.jpeg`, - '-background', 'white', - '-resize', '1920x1920', - '-auto-orient', - '-flatten', - `${outputFile}.png` - ].join(' '); - execSync(convertImageToPngCommands, (error, stderr) => { - if (error) { - return logger.error(`Image conversion to PNG failed with error: ${error.message}`); - } - - if (stderr) { - return logger.error(`Image conversion to PNG failed with stderr: ${stderr}`); - } - }) + else if (svgFileExists) { + fs.copyFileSync(svgFile, `${outputFile}.svg`); } else { @@ -118,7 +87,6 @@ let exportJob = JSON.parse(job); } kickOffProcessWorker(exportJob.jobId); - client.disconnect(); })() parentPort.postMessage({ message: workerData }) diff --git a/export-annotations/workers/process.js b/export-annotations/workers/process.js index 9076ce8357..869d6a125d 100644 --- a/export-annotations/workers/process.js +++ b/export-annotations/workers/process.js @@ -1,7 +1,6 @@ const Logger = require('../lib/utils/logger'); const config = require('../config'); const fs = require('fs'); -const sizeOf = require('image-size'); const { create } = require('xmlbuilder2', { encoding: 'utf-8' }); const { execSync } = require("child_process"); const { Worker, workerData, parentPort } = require('worker_threads'); @@ -9,6 +8,7 @@ const path = require('path'); const sanitize = require("sanitize-filename"); const twemoji = require("twemoji"); const { getStroke, getStrokePoints } = require('perfect-freehand'); +const probe = require('probe-image-size'); const jobId = workerData; @@ -395,20 +395,14 @@ function overlay_text(svg, annotation) { } } -// function sortByKey(array, key, pos) { -// return array.sort(function(a, b) { -// let [x, y] = [a[key][pos], b[key][pos]] -// return ((x < y) ? -1 : ((x > y) ? 1 : 0)); -// }); -// } - function overlay_annotations(svg, currentSlideAnnotations, w, h) { - - // currentSlideAnnotations = sortByKey(currentSlideAnnotations, 'annotationInfo', 'childIndex'); - for (let annotation of currentSlideAnnotations) { + switch (annotation.annotationInfo.type) { + // case 'arrow': + // overlay_arrow(svg, annotation.annotationInfo); + // break; case 'draw': overlay_draw(svg, annotation.annotationInfo); break; @@ -448,9 +442,17 @@ let ghostScriptInput = "" // 3. Convert annotations to SVG for (let currentSlide of pages) { - var dimensions = sizeOf(path.join(dropbox, `slide${currentSlide.page}.png`)); - var slideWidth = dimensions.width; - var slideHeight = dimensions.height; + + let backgroundImagePath = path.join(dropbox, `slide${currentSlide.page}`); + let backgroundFormat = fs.existsSync(`${backgroundImagePath}.png`) ? 'png' : 'svg' + + // Output dimensions in pixels even if stated otherwise (pt) + // CairoSVG didn't like attempts to read the dimensions from a stream + // to prevent loading file in memory + let dimensions = probe.sync(fs.readFileSync(`${backgroundImagePath}.${backgroundFormat}`)); + + let slideWidth = dimensions.width; + let slideHeight = dimensions.height; // Create the SVG slide with the background image let svg = create({ version: '1.0', encoding: 'UTF-8' }) @@ -465,7 +467,7 @@ for (let currentSlide of pages) { sysID: 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' }) .ele('image', { - 'xlink:href': `file://${dropbox}/slide${currentSlide.page}.png`, + 'xlink:href': `file://${dropbox}/slide${currentSlide.page}.${backgroundFormat}`, width: slideWidth, height: slideHeight, })