Merge pull request #15326 from danielpetri1/tldrawpdfexport

This commit is contained in:
Gustavo Trott 2022-07-26 17:48:29 -03:00 committed by GitHub
commit b64032b954
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 32 additions and 19 deletions

View File

@ -7,11 +7,11 @@
"presAnnDropboxDir": "/tmp/pres-ann-dropbox"
},
"collector": {
"backgroundSlideDPI": 300,
"backgroundSlidePPI": 200
"pngWidthRasterizedSlides": 2560
},
"process": {
"whiteboardTextEncoding": "utf-8",
"textScaleFactor": 4,
"pointsPerInch": 72,
"pixelsPerInch": 96
},

View File

@ -4,7 +4,6 @@ const fs = require('fs');
const redis = require('redis');
const { Worker, workerData, parentPort } = require('worker_threads');
const path = require('path');
const probe = require('probe-image-size');
const { execSync } = require("child_process");
const jobId = workerData;
@ -66,27 +65,25 @@ let exportJob = JSON.parse(job);
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}`);
// CairoSVG doesn't handle transparent SVG and PNG embeds properly, e.g., in rasterized text.
// So textboxes may get a black background when downloading/exporting repeatedly.
// To avoid that, we take slides from the uploaded file, but later probe the dimensions from the SVG
// so it matches what was shown in the browser -- Tldraw unfortunately uses absolute coordinates.
// so it matches what was shown in the browser.
let extract_png_from_pdf = [
'pdftocairo',
'-png',
'-f', pageNumber,
'-l', pageNumber,
'-r', config.collector.backgroundSlidePPI,
'-scale-to', config.collector.pngWidthRasterizedSlides,
'-singlefile',
'-cropbox',
pdfFile, outputFile,
].join(' ')
execSync(extract_png_from_pdf);
fs.copyFileSync(svgFile, path.join(dropbox, `slide${pageNumber}.svg`));
}
}

View File

@ -77,4 +77,7 @@ if (jobType == 'PresentationWithAnnotationDownloadJob') {
logger.error(`Notifier received unknown job type ${jobType}`);
}
// Delete temporary files
fs.rm(dropbox, { recursive: true }, (err) => { if (err) { throw err; } });
parentPort.postMessage({ message: workerData })

View File

@ -120,12 +120,26 @@ function to_px(pt) {
return (pt / config.process.pointsPerInch) * config.process.pixelsPerInch
}
// Escape shell metacharacters based on MDN's page on regular expressions,
// the escape-string-regexp npm package, and Pango markup.
function escapeText(string) {
return string
.replace(/&/g, '\\&')
.replace(/'/g, '\\'')
.replace(/>/g, '\\>')
.replace(/</g, '\\&lt;')
.replace(/[~`!".*+?%^${}()|[\]\\\/]/g, '\\$&')
.replace(/-/g, '\\&#x2D;');
}
function render_textbox(textColor, font, fontSize, textAlign, text, id, textBoxWidth = null) {
fontSize = to_pt(fontSize);
fontSize = to_pt(fontSize) * config.process.textScaleFactor
text = escapeText(text);
// Sticky notes need automatic line wrapping: take width into account
let size = textBoxWidth ? `-size ${textBoxWidth}x` : ''
// Texbox scaled by a constant factor to improve resolution at small scales
let size = textBoxWidth ? `-size ${textBoxWidth * config.process.textScaleFactor}x` : ''
let pangoText = `pango:"<span font_family='${font}' font='${fontSize}' color='${textColor}'>${text}</span>"`
@ -560,8 +574,8 @@ function overlay_shape_label(svg, annotation) {
render_textbox(fontColor, font, fontSize, textAlign, text, id);
let dimensions = probe.sync(fs.readFileSync(path.join(dropbox, `text${id}.png`)));
let labelWidth = dimensions.width;
let labelHeight = dimensions.height;
let labelWidth = dimensions.width / config.process.textScaleFactor;
let labelHeight = dimensions.height / config.process.textScaleFactor;
svg.ele('g', {
transform: `rotate(${rotation} ${label_center_x} ${label_center_y})`
@ -744,7 +758,8 @@ let ghostScriptInput = ""
for (let currentSlide of pages) {
let backgroundImagePath = path.join(dropbox, `slide${currentSlide.page}`);
let svgFileExists = fs.existsSync(`${backgroundImagePath}.svg`)
let svgBackgroundSlide = path.join(exportJob.presLocation, 'svgs', `slide${currentSlide.page}.svg`);
let svgBackgroundExists = fs.existsSync(svgBackgroundSlide);
let backgroundFormat = fs.existsSync(`${backgroundImagePath}.png`) ? 'png' : 'jpeg'
// Output dimensions in pixels even if stated otherwise (pt)
@ -752,8 +767,8 @@ for (let currentSlide of pages) {
// that would prevent loading file in memory
// Ideally, use dimensions provided by tldraw's background image asset
// (this is not yet always provided)
let dimensions = svgFileExists ? 
probe.sync(fs.readFileSync(`${backgroundImagePath}.svg`)) :
let dimensions = svgBackgroundExists ? 
probe.sync(fs.readFileSync(svgBackgroundSlide)) :
probe.sync(fs.readFileSync(`${backgroundImagePath}.${backgroundFormat}`));
let slideWidth = parseInt(dimensions.width, 10);
@ -794,14 +809,12 @@ for (let currentSlide of pages) {
if (err) { return logger.error(err); }
});
// Dimensions converted back to a pixel size which,
// Dimensions converted to a pixel size which,
// when converted to points, will yield the desired
// dimension in pixels when read without conversion
// e.g. say Tldraw's canvas is 1920x1080 px.
// The background SVG dimensions are set to 1920x1080 pt (incorrect unit).
// So we read it in ignoring the unit as 1920x1080 px, making the position of the drawings match.
// Now we assume we had 1920x1080pt and resize to 2560x1440 px so that the SVG generates with the original "wrong" size.
// e.g. say the background SVG dimensions are set to 1920x1080 pt
// Resize output to 2560x1440 px so that the SVG generates with the original size in pt.
let convertAnnotatedSlide = [
'cairosvg',