bigbluebutton-Github/export-annotations/workers/process.js

819 lines
26 KiB
JavaScript
Raw Normal View History

2022-02-13 04:03:07 +08:00
const Logger = require('../lib/utils/logger');
const config = require('../config');
const fs = require('fs');
const { create } = require('xmlbuilder2', { encoding: 'utf-8' });
2022-02-23 00:02:54 +08:00
const { execSync } = require("child_process");
const { Worker, workerData, parentPort } = require('worker_threads');
const path = require('path');
const sanitize = require('sanitize-filename');
2022-05-26 00:35:43 +08:00
const { getStroke, getStrokePoints } = require('perfect-freehand');
const probe = require('probe-image-size');
2022-02-13 04:03:07 +08:00
const jobId = workerData;
const logger = new Logger('presAnn Process Worker');
logger.info("Processing PDF for job " + jobId);
2022-05-01 05:28:11 +08:00
const kickOffNotifierWorker = (jobType, filename) => {
return new Promise((resolve, reject) => {
2022-05-01 05:28:11 +08:00
const worker = new Worker('./workers/notifier.js', { workerData: [jobType, jobId, filename] });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0)
reject(new Error(`PresAnn Notifier Worker stopped with exit code ${code}`));
})
})
2022-02-23 00:02:54 +08:00
}
2022-06-09 02:58:38 +08:00
// General utilities for rendering SVGs resembling Tldraw as much as possible
function align_to_pango(alignment) {
2022-05-26 00:35:43 +08:00
switch (alignment) {
case 'start': return 'left'
case 'middle': return 'center'
case 'end': return 'right'
case 'justify': return 'justify'
default: return 'left'
2022-05-26 00:35:43 +08:00
}
}
2022-06-01 01:23:24 +08:00
function color_to_hex(color, isStickyNote = false, isFilled = false) {
2022-05-26 00:35:43 +08:00
if (isStickyNote) { color = `sticky-${color}` }
2022-06-01 01:23:24 +08:00
if (isFilled) { color = `fill-${color}` }
2022-05-25 00:35:08 +08:00
2022-05-26 00:35:43 +08:00
switch (color) {
case 'white': return '#1d1d1d'
2022-06-01 01:23:24 +08:00
case 'fill-white': return '#fefefe'
2022-05-25 00:35:08 +08:00
case 'sticky-white': return '#fddf8e'
case 'lightGray': return '#c6cbd1'
2022-06-01 01:23:24 +08:00
case 'fill-lightGray': return '#f1f2f3'
2022-05-25 00:35:08 +08:00
case 'sticky-lightGray': return '#dde0e3'
case 'gray': return '#788492'
2022-06-01 01:23:24 +08:00
case 'fill-gray': return '#e3e5e7'
2022-05-25 00:35:08 +08:00
case 'sticky-gray': return '#b3b9c1'
case 'black': return '#1d1d1d'
2022-06-01 01:23:24 +08:00
case 'fill-black': return '#d2d2d2'
2022-05-25 00:35:08 +08:00
case 'sticky-black': return '#fddf8e'
case 'green': return '#36b24d'
2022-06-01 01:23:24 +08:00
case 'fill-green': return '#d7eddb'
2022-05-25 00:35:08 +08:00
case 'sticky-green': return '#8ed29b'
case 'cyan': return '#0e98ad'
2022-06-01 01:23:24 +08:00
case 'fill-cyan': return '#d0e8ec'
2022-05-25 00:35:08 +08:00
case 'sticky-cyan': return '#78c4d0'
case 'blue': return '#1c7ed6'
2022-06-01 01:23:24 +08:00
case 'fill-blue': return '#d2e4f4'
2022-05-25 00:35:08 +08:00
case 'sticky-blue': return '#80b6e6'
case 'indigo': return '#4263eb'
2022-06-01 01:23:24 +08:00
case 'fill-indigo': return '#d9dff7'
2022-05-25 00:35:08 +08:00
case 'sticky-indigo': return '#95a7f2'
case 'violet': return '#7746f1'
2022-06-01 01:23:24 +08:00
case 'fill-violet': return '#e2daf8'
2022-05-25 00:35:08 +08:00
case 'sticky-violet': return '#b297f5'
case 'red': return '#ff2133'
2022-06-01 01:23:24 +08:00
case 'fill-red': return '#fbd3d6'
2022-05-25 00:35:08 +08:00
case 'sticky-red': return '#fd838d'
case 'orange': return '#ff9433'
2022-06-01 01:23:24 +08:00
case 'fill-orange': return '#fbe8d6'
2022-05-25 00:35:08 +08:00
case 'sticky-orange': return '#fdc28d'
case 'yellow': return '#ffc936'
2022-06-01 01:23:24 +08:00
case 'fill-yellow': return '#fbf1d7'
2022-05-25 00:35:08 +08:00
case 'sticky-yellow': return '#fddf8e'
default: return '#0d0d0d'
}
}
2022-05-26 00:35:43 +08:00
function determine_dasharray(dash, gap = 0) {
2022-05-25 00:35:08 +08:00
2022-05-26 00:35:43 +08:00
switch (dash) {
case 'dashed': return `stroke-linecap:butt;stroke-dasharray:${gap};`
case 'dotted': return `stroke-linecap:round;stroke-dasharray:${gap};`
2022-05-26 00:35:43 +08:00
default: return 'stroke-linejoin:round;stroke-linecap:round;'
}
}
2022-02-23 00:02:54 +08:00
function determine_font_from_family(family) {
2022-05-26 00:35:43 +08:00
switch (family) {
case 'script': return 'Caveat Brush'
2022-06-01 01:23:24 +08:00
case 'sans': return 'Source Sans Pro'
case 'serif': return 'Crimson Pro'
// Temporary workaround due to typo in messages
case 'erif': return 'Crimson Pro'
2022-06-01 01:23:24 +08:00
case 'mono': return 'Source Code Pro'
default: return family
}
}
2022-05-26 00:35:43 +08:00
function rad_to_degree(angle) {
2022-06-01 01:23:24 +08:00
return angle * (180 / Math.PI);
2022-02-15 23:48:58 +08:00
}
function render_textbox(textColor, font, fontSize, textAlign, text, id, textBoxWidth = null) {
// Convert pixels to points
fontSize = (fontSize / config.process.pixelsPerInch) * config.process.pointsPerInch;
// Sticky notes need automatic line wrapping: take width into account
let size = textBoxWidth ? `-size ${textBoxWidth}x` : ''
let pangoText = `pango:"<span font_family='${font}' font='${fontSize}' color='${textColor}'>${text}</span>"`
let justify = textAlign === 'justify'
textAlign = justify ? 'left' : textAlign
2022-05-25 00:35:08 +08:00
let commands = [
'convert',
'-encoding', `${config.process.whiteboardTextEncoding}`,
'-density', config.process.pixelsPerInch,
'-background', 'transparent',
size,
'-define', `pango:align=${textAlign}`,
'-define', `pango:justify=${justify}`,
'-define', 'pango:wrap=word-char',
pangoText,
path.join(dropbox, `text${id}.png`)
].join(' ')
execSync(commands);
2022-05-25 00:35:08 +08:00
}
2022-05-26 00:35:43 +08:00
function get_gap(dash, size) {
switch (dash) {
2022-05-31 17:09:38 +08:00
case 'dashed':
2022-05-26 00:35:43 +08:00
if (size == 'small') { return '8 8' }
else if (size == 'medium') { return '14 14' }
else { return '20 20' }
case 'dotted':
if (size == 'small') { return '0.1 8' }
else if (size == 'medium') { return '0.1 14' }
else { return '0.1 20' }
default: return '0'
}
}
function get_stroke_width(dash, size) {
switch (size) {
case 'small': if (dash === 'draw') { return 1 } else { return 4 };
case 'medium': if (dash === 'draw') { return 1.75 } else { return 6.25 };
case 'large': if (dash === 'draw') { return 2.5 } else { return 8.5 }
default: return 1;
}
}
2022-06-09 02:58:38 +08:00
function sortByKey(array, key, value) {
return array.sort(function (a, b) {
let [x, y] = [a[key][value], b[key][value]];
return x - y;
});
}
2022-06-01 01:23:24 +08:00
function text_size_to_px(size, scale = 1, isStickyNote = false) {
2022-05-26 00:35:43 +08:00
if (isStickyNote) { size = `sticky-${size}` }
switch (size) {
case 'sticky-small': return 24
2022-06-01 01:23:24 +08:00
case 'small': return 28 * scale
2022-05-26 00:35:43 +08:00
case 'sticky-medium': return 36
2022-06-01 01:23:24 +08:00
case 'medium': return 48 * scale
2022-05-26 00:35:43 +08:00
case 'sticky-large': return 48
2022-06-01 01:23:24 +08:00
case 'large': return 96 * scale
2022-05-26 00:35:43 +08:00
2022-06-01 01:23:24 +08:00
default: return 28 * scale
2022-05-26 00:35:43 +08:00
}
}
2022-06-15 23:49:36 +08:00
2022-06-09 02:58:38 +08:00
// Methods based on tldraw's utilities
2022-05-31 17:09:38 +08:00
function getPath(annotationPoints) {
// Gets inner path of a stroke outline
// For solid, dashed, and dotted types
let stroke = getStrokePoints(annotationPoints).map((strokePoint) => strokePoint.point);
2022-05-26 00:35:43 +08:00
2022-05-31 17:09:38 +08:00
let [max_x, max_y] = [0, 0];
let path = stroke.reduce(
(acc, [x0, y0], i, arr) => {
2022-06-01 01:23:24 +08:00
if (!arr[i + 1]) return acc
let [x1, y1] = arr[i + 1]
if (x1 >= max_x) { max_x = x1 }
if (y1 >= max_y) { max_y = y1 }
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2)
return acc
2022-05-31 17:09:38 +08:00
},
2022-02-16 19:06:12 +08:00
2022-05-31 17:09:38 +08:00
['M', ...stroke[0], 'Q']
2022-06-01 01:23:24 +08:00
)
path.join(' ');
return [path, max_x, max_y];
2022-02-16 19:06:12 +08:00
}
2022-05-31 17:09:38 +08:00
function getOutlinePath(annotationPoints) {
// Gets outline of a hand-drawn input, with pressure
let stroke = getStroke(annotationPoints, {
simulatePressure: true,
size: 8,
});
2022-05-26 00:35:43 +08:00
2022-05-31 17:09:38 +08:00
let [max_x, max_y] = [0, 0];
let path = stroke.reduce(
(acc, [x0, y0], i, arr) => {
let [x1, y1] = arr[(i + 1) % arr.length]
if (x1 >= max_x) { max_x = x1 }
if (y1 >= max_y) { max_y = y1 }
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2)
return acc;
},
2022-02-16 21:49:41 +08:00
2022-05-31 17:09:38 +08:00
['M', ...stroke[0], 'Q']
);
2022-02-16 21:49:41 +08:00
2022-05-31 17:09:38 +08:00
path.push('Z');
path.join(' ');
2022-02-16 21:49:41 +08:00
2022-05-31 17:09:38 +08:00
return [path, max_x, max_y];
2022-02-16 21:49:41 +08:00
}
2022-06-08 19:02:05 +08:00
function circleFromThreePoints(A, B, C) {
let [x1, y1] = A
let [x2, y2] = B
let [x3, y3] = C
let a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2
let b =
(x1 * x1 + y1 * y1) * (y3 - y2) +
(x2 * x2 + y2 * y2) * (y1 - y3) +
(x3 * x3 + y3 * y3) * (y2 - y1)
let c =
(x1 * x1 + y1 * y1) * (x2 - x3) +
(x2 * x2 + y2 * y2) * (x3 - x1) +
(x3 * x3 + y3 * y3) * (x1 - x2)
let x = -b / (2 * a)
let y = -c / (2 * a)
return [x, y, Math.hypot(x - x1, y - y1)]
}
2022-06-15 23:49:36 +08:00
function distance(x1, y1, x2, y2) {
return Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((y2 - y1), 2));
}
2022-06-08 19:02:05 +08:00
function getArcLength(C, r, A, B) {
let sweep = getSweep(C, A, B);
return r * (2 * Math.PI) * (sweep / (2 * Math.PI));
}
function getSweep(C, A, B) {
// Get angle between two vectors in radians
2022-06-14 18:51:40 +08:00
let a0 = Math.atan2(A[1] - C[1], A[0] - C[0]);
let a1 = Math.atan2(B[1] - C[1], B[0] - C[0]);
2022-06-08 19:02:05 +08:00
// Short distance between two angles
let max = Math.PI * 2
let da = (a1 - a0) % max
return ((2 * da) % max) - da
}
function intersectCircleCircle(c1, r1, c2, r2) {
2022-06-09 02:58:38 +08:00
2022-06-08 19:02:05 +08:00
let dx = c2[0] - c1[0];
let dy = c2[1] - c1[1];
2022-06-09 02:58:38 +08:00
2022-06-08 19:02:05 +08:00
let d = Math.sqrt(dx * dx + dy * dy);
let x = (d * d - r2 * r2 + r1 * r1) / (2 * d);
let y = Math.sqrt(r1 * r1 - x * x);
2022-06-09 02:58:38 +08:00
2022-06-08 19:02:05 +08:00
dx /= d
dy /= d
2022-06-09 02:58:38 +08:00
return [[c1[0] + dx * x - dy * y, c1[1] + dy * x + dx * y],
[c1[0] + dx * x + dy * y, c1[1] + dy * x - dx * y]]
2022-06-08 19:02:05 +08:00
}
function rotWith(A, C, r = 0) {
2022-06-09 02:58:38 +08:00
// Rotate a vector A around another vector C by r radians
if (r === 0) return A
2022-06-08 19:02:05 +08:00
2022-06-09 02:58:38 +08:00
let s = Math.sin(r)
let c = Math.cos(r)
2022-06-08 19:02:05 +08:00
2022-06-09 02:58:38 +08:00
let px = A[0] - C[0]
let py = A[1] - C[1]
2022-06-08 19:02:05 +08:00
2022-06-09 02:58:38 +08:00
let nx = px * c - py * s
let ny = px * s + py * c
2022-06-08 19:02:05 +08:00
2022-06-09 02:58:38 +08:00
return [nx + C[0], ny + C[1]]
2022-06-08 19:02:05 +08:00
}
function nudge(A, B, d) {
2022-06-09 02:58:38 +08:00
// Pushes a point A towards a point B by a given distance
2022-06-08 19:02:05 +08:00
if (A[0] === B[0] && A[1] === B[1]) return A
// B - A
2022-06-14 18:51:40 +08:00
let sub = [B[0] - A[0], B[1] - A[1]];
2022-06-08 19:02:05 +08:00
// Vector length
let len = Math.hypot(sub[0], sub[1]);
// Get unit vector
let unit = [sub[0] / len, sub[1] / len];
// Multiply by distance
let mul = [unit[0] * d, unit[1] * d];
return [A[0] + mul[0], A[1] + mul[1]]
}
function getCurvedArrowHeadPath(A, r1, C, r2, sweep) {
const phi = (1 + Math.sqrt(5)) / 2;
// Determine intersections between two circles
let ints = intersectCircleCircle(A, r1 * (phi - 1), C, r2)
2022-06-09 02:58:38 +08:00
2022-06-08 19:02:05 +08:00
if (!ints) {
logger.info('Could not find an intersection for the arrow head.')
return { left: A, right: A }
}
let int = sweep ? ints[0] : ints[1]
let left = int ? nudge(rotWith(int, A, Math.PI / 6), A, r1 * -0.382) : A
let right = int ? nudge(rotWith(int, A, -Math.PI / 6), A, r1 * -0.382) : A
2022-06-09 02:58:38 +08:00
2022-06-08 19:02:05 +08:00
return `M ${left} L ${A} ${right}`
}
2022-06-09 02:58:38 +08:00
// Methods to convert Akka message contents into SVG
2022-06-08 00:18:32 +08:00
function overlay_arrow(svg, annotation) {
let [x, y] = annotation.point;
let bend = annotation.bend;
let decorations = annotation.decorations;
let dash = annotation.style.dash;
dash = (dash == 'draw') ? 'solid' : dash // Use 'solid' thickness
let shapeColor = color_to_hex(annotation.style.color);
let sw = get_stroke_width(dash, annotation.style.size);
let gap = get_gap(dash, annotation.style.size);
let stroke_dasharray = determine_dasharray(dash, gap);
let [start_x, start_y] = annotation.handles.start.point;
let [end_x, end_y] = annotation.handles.end.point;
let [bend_x, bend_y] = annotation.handles.bend.point;
let line = [];
let arrowHead = [];
2022-06-15 23:49:36 +08:00
let arrowDistance = distance(start_x, start_y, end_x, end_y);
2022-06-08 00:18:32 +08:00
let arrowHeadLength = Math.min(arrowDistance / 3, 8 * sw);
let isStraightLine = parseFloat(bend).toFixed(3) == 0;
let angle = Math.atan2(end_y - start_y, end_x - start_x)
if (isStraightLine) {
2022-06-08 19:02:05 +08:00
// Draws a straight line / arrow
2022-06-08 00:18:32 +08:00
line.push(`M ${start_x} ${start_y} L ${end_x} ${end_y}`);
2022-06-08 19:02:05 +08:00
2022-06-08 00:18:32 +08:00
if (decorations.start || decorations.end) {
arrowHead.push(`M ${end_x} ${end_y}`);
2022-06-08 19:02:05 +08:00
arrowHead.push(`L ${end_x + arrowHeadLength * Math.cos(angle + (7 / 6) * Math.PI)} ${end_y + arrowHeadLength * Math.sin(angle + (7 / 6) * Math.PI)}`);
2022-06-08 00:18:32 +08:00
arrowHead.push(`M ${end_x} ${end_y}`);
2022-06-08 19:02:05 +08:00
arrowHead.push(`L ${end_x + arrowHeadLength * Math.cos(angle + (5 / 6) * Math.PI)} ${end_y + arrowHeadLength * Math.sin(angle + (5 / 6) * Math.PI)}`);
2022-06-08 00:18:32 +08:00
}
} else {
2022-06-08 19:02:05 +08:00
// Curved lines and arrows
let circle = circleFromThreePoints([start_x, start_y], [bend_x, bend_y], [end_x, end_y]);
let center = [circle[0], circle[1]]
let radius = circle[2]
let length = getArcLength(center, radius, [start_x, start_y], [end_x, end_y]);
line.push(`M ${start_x} ${start_y} A ${radius} ${radius} 0 0 ${length > 0 ? '1' : '0'} ${end_x} ${end_y}`);
if (decorations.start)
arrowHead.push(getCurvedArrowHeadPath([start_x, start_y], arrowHeadLength, center, radius, length < 0));
else if (decorations.end) {
arrowHead.push(getCurvedArrowHeadPath([end_x, end_y], arrowHeadLength, center, radius, length >= 0));
}
2022-06-08 00:18:32 +08:00
}
// The arrowhead is purposely not styled (e.g., dashed / dotted)
svg.ele('g', {
style: `stroke:${shapeColor};stroke-width:${sw};fill:none;`,
transform: `translate(${x} ${y})`
}).ele('path', {
'style': stroke_dasharray,
d: line.join(' '),
}).up()
2022-06-08 19:02:05 +08:00
.ele('path', {
d: arrowHead.join(' '),
}).up();
2022-06-08 00:18:32 +08:00
}
2022-05-26 00:35:43 +08:00
function overlay_draw(svg, annotation) {
let dash = annotation.style.dash;
2022-05-25 00:35:08 +08:00
2022-05-31 17:09:38 +08:00
let [path, max_x, max_y] = (dash == 'draw') ? getOutlinePath(annotation.points) : getPath(annotation.points);
2022-05-26 00:35:43 +08:00
2022-05-31 17:09:38 +08:00
if (!path.length) return;
2022-05-26 00:35:43 +08:00
let shapeColor = color_to_hex(annotation.style.color);
let rotation = rad_to_degree(annotation.rotation);
2022-05-26 00:35:43 +08:00
let thickness = get_stroke_width(dash, annotation.style.size);
let gap = get_gap(dash, annotation.style.size);
2022-05-26 00:35:43 +08:00
let [x, y] = annotation.point;
2022-05-26 00:35:43 +08:00
let stroke_dasharray = determine_dasharray(dash, gap);
2022-05-31 17:09:38 +08:00
let fill = (dash === 'draw') ? shapeColor : 'none';
2022-05-26 00:35:43 +08:00
2022-06-15 23:49:36 +08:00
let shapeFillColor = color_to_hex(`fill-${annotation.style.color}`)
2022-06-21 18:20:27 +08:00
let shapeTransform = `translate(${x} ${y}), rotate(${rotation} ${max_x / 2} ${max_y / 2})`
2022-06-15 23:49:36 +08:00
2022-06-21 18:20:27 +08:00
// Fill assuming solid, small pencil used when path start- and end points overlap
let shapeIsFilled =
2022-06-21 18:20:27 +08:00
annotation.style.isFilled &&
annotation.points.length > 3
2022-06-15 23:49:36 +08:00
&& Math.round(distance(
annotation.points[0][0],
annotation.points[0][1],
annotation.points[annotation.points.length - 1][0],
annotation.points[annotation.points.length - 1][1]
2022-06-21 18:20:27 +08:00
)) <= 2 * get_stroke_width('solid', 'small');
2022-06-15 23:49:36 +08:00
2022-06-21 18:20:27 +08:00
if (shapeIsFilled) {
svg.ele('path', {
style: `fill:${shapeFillColor};`,
d: getPath(annotation.points)[0] + 'Z',
transform: shapeTransform
2022-06-15 23:49:36 +08:00
}).up()
2022-06-21 18:20:27 +08:00
}
2022-06-21 18:20:27 +08:00
svg.ele('path', {
style: `stroke:${shapeColor};stroke-width:${thickness};fill:${fill};${stroke_dasharray}`,
d: path,
transform: shapeTransform
})
2022-02-17 02:42:46 +08:00
}
2022-06-01 20:03:40 +08:00
function overlay_ellipse(svg, annotation) {
let dash = annotation.style.dash;
2022-06-14 17:19:43 +08:00
dash = (dash == 'draw') ? 'solid' : dash // Use 'solid' thickness for draw type
2022-06-01 20:03:40 +08:00
let [x, y] = annotation.point; // Ellipse center coordinates
let [rx, ry] = annotation.radius;
let isFilled = annotation.style.isFilled;
let shapeColor = color_to_hex(annotation.style.color);
let fillColor = isFilled ? color_to_hex(annotation.style.color, false, isFilled) : 'none';
let rotation = rad_to_degree(annotation.rotation);
let sw = get_stroke_width(dash, annotation.style.size);
let gap = get_gap(dash, annotation.style.size);
let stroke_dasharray = determine_dasharray(dash, gap);
svg.ele('g', {
style: `stroke:${shapeColor};stroke-width:${sw};fill:${fillColor};${stroke_dasharray}`,
}).ele('ellipse', {
'cx': x + rx,
'cy': y + ry,
'rx': rx,
'ry': ry,
transform: `rotate(${rotation} ${x + rx} ${y + ry})`
}).up()
2022-06-14 21:33:03 +08:00
if (annotation.label) { overlay_shape_label(svg, annotation) }
2022-06-01 20:03:40 +08:00
}
2022-06-01 01:23:24 +08:00
function overlay_rectangle(svg, annotation) {
let dash = annotation.style.dash;
let rect_dash = (dash == 'draw') ? 'solid' : dash // Use 'solid' thickness for draw type
let [x, y] = annotation.point;
let [w, h] = annotation.size;
let isFilled = annotation.style.isFilled;
let shapeColor = color_to_hex(annotation.style.color);
let fillColor = isFilled ? color_to_hex(annotation.style.color, false, isFilled) : 'none';
let rotation = rad_to_degree(annotation.rotation);
let sw = get_stroke_width(rect_dash, annotation.style.size);
let gap = get_gap(dash, annotation.style.size);
let stroke_dasharray = determine_dasharray(dash, gap);
let rx = (dash == 'draw') ? Math.min(w / 4, sw * 2) : 0;
let ry = (dash == 'draw') ? Math.min(h / 4, sw * 2) : 0;
svg.ele('g', {
style: `stroke:${shapeColor};stroke-width:${sw};fill:${fillColor};${stroke_dasharray}`,
}).ele('rect', {
width: w,
height: h,
'rx': rx,
'ry': ry,
2022-06-01 20:03:40 +08:00
transform: `translate(${x} ${y}), rotate(${rotation} ${w / 2} ${h / 2})`
2022-06-01 01:23:24 +08:00
}).up()
2022-06-14 21:33:03 +08:00
if (annotation.label) { overlay_shape_label(svg, annotation) }
}
function overlay_shape_label(svg, annotation) {
let fontColor = color_to_hex(annotation.style.color);
let font = determine_font_from_family(annotation.style.font);
let fontSize = text_size_to_px(annotation.style.size, annotation.style.scale);
let textAlign = 'center';
let text = annotation.label;
let id = annotation.id;
let rotation = rad_to_degree(annotation.rotation);
let [shape_width, shape_height] = annotation.size
let [shape_x, shape_y] = annotation.point;
let x_offset = annotation.labelPoint[0]
let y_offset = annotation.labelPoint[1]
let label_center_x = shape_x + shape_width * x_offset
let label_center_y = shape_y + shape_height * y_offset
render_textbox(fontColor, font, fontSize, textAlign, text, id);
2022-06-14 21:33:03 +08:00
let dimensions = probe.sync(fs.readFileSync(path.join(dropbox, `text${id}.png`)));
let labelWidth = dimensions.width;
let labelHeight = dimensions.height;
svg.ele('g', {
transform: `rotate(${rotation} ${label_center_x} ${label_center_y})`
}).ele('image', {
x: label_center_x - (labelWidth * x_offset),
y: label_center_y - (labelHeight * y_offset),
width: labelWidth,
height: labelHeight,
'xlink:href': `file://${dropbox}/text${id}.png`,
}).up();
2022-06-01 01:23:24 +08:00
}
2022-05-25 00:35:08 +08:00
function overlay_sticky(svg, annotation) {
let backgroundColor = color_to_hex(annotation.style.color, true);
2022-06-01 01:23:24 +08:00
let fontSize = text_size_to_px(annotation.style.size, annotation.style.scale, true);
2022-05-25 00:35:08 +08:00
let rotation = rad_to_degree(annotation.rotation);
let font = determine_font_from_family(annotation.style.font);
let textAlign = align_to_pango(annotation.style.textAlign);
2022-05-25 00:35:08 +08:00
let [textBoxWidth, textBoxHeight] = annotation.size;
let [textBox_x, textBox_y] = annotation.point;
2022-06-15 23:49:36 +08:00
let textColor = "#0d0d0d" // For sticky notes
let text = annotation.text
let id = annotation.id;
2022-05-25 00:35:08 +08:00
render_textbox(textColor, font, fontSize, textAlign, text, id, textBoxWidth);
2022-05-25 00:35:08 +08:00
// Overlay transparent text image over empty sticky note
svg.ele('g', {
transform: `rotate(${rotation}, ${textBox_x + (textBoxWidth / 2)}, ${textBox_y + (textBoxHeight / 2)})`
}).ele('rect', {
x: textBox_x,
y: textBox_y,
width: textBoxWidth,
height: textBoxHeight,
fill: backgroundColor,
}).up()
.ele('image', {
2022-05-25 00:35:08 +08:00
x: textBox_x,
y: textBox_y,
2022-05-25 00:35:08 +08:00
width: textBoxWidth,
height: textBoxHeight,
'xlink:href': `file://${dropbox}/text${id}.png`,
2022-05-25 00:35:08 +08:00
}).up();
}
2022-06-01 20:34:15 +08:00
function overlay_triangle(svg, annotation) {
let dash = annotation.style.dash;
2022-06-09 02:58:38 +08:00
dash = (dash == 'draw') ? 'solid' : dash
2022-06-01 20:34:15 +08:00
let [x, y] = annotation.point;
let [w, h] = annotation.size;
let isFilled = annotation.style.isFilled;
let shapeColor = color_to_hex(annotation.style.color);
let fillColor = isFilled ? color_to_hex(annotation.style.color, false, isFilled) : 'none';
let rotation = rad_to_degree(annotation.rotation);
let sw = get_stroke_width(dash, annotation.style.size);
let gap = get_gap(dash, annotation.style.size);
let stroke_dasharray = determine_dasharray(dash, gap);
let points = `${w / 2} 0, ${w} ${h}, 0 ${h}, ${w / 2} 0`
svg.ele('g', {
style: `stroke:${shapeColor};stroke-width:${sw};fill:${fillColor};${stroke_dasharray}`,
}).ele('polygon', {
'points': points,
transform: `translate(${x}, ${y}), rotate(${rotation} ${w / 2} ${h / 2})`
}).up()
2022-06-14 21:33:03 +08:00
if (annotation.label) { overlay_shape_label(svg, annotation) }
2022-06-01 20:34:15 +08:00
}
2022-05-26 00:35:43 +08:00
function overlay_text(svg, annotation) {
let [textBoxWidth, textBoxHeight] = annotation.size;
2022-05-26 00:35:43 +08:00
let fontColor = color_to_hex(annotation.style.color);
let font = determine_font_from_family(annotation.style.font);
2022-06-01 01:23:24 +08:00
let fontSize = text_size_to_px(annotation.style.size, annotation.style.scale);
let textAlign = align_to_pango(annotation.style.textAlign);
let text = annotation.text
let id = annotation.id;
2022-06-07 19:45:54 +08:00
let rotation = rad_to_degree(annotation.rotation);
2022-05-26 00:35:43 +08:00
let [textBox_x, textBox_y] = annotation.point;
2022-06-07 19:45:54 +08:00
render_textbox(fontColor, font, fontSize, textAlign, text, id);
2022-05-26 00:35:43 +08:00
let rotation_x = textBox_x + (textBoxWidth / 2)
let rotation_y = textBox_y + (textBoxHeight / 2)
svg.ele('g', {
transform: `rotate(${rotation} ${rotation_x} ${rotation_y})`
}).ele('image', {
x: textBox_x,
y: textBox_y,
width: textBoxWidth,
height: textBoxHeight,
'xlink:href': `file://${dropbox}/text${id}.png`,
}).up();
2022-05-26 00:35:43 +08:00
}
2022-06-09 02:58:38 +08:00
function overlay_annotation(svg, currentAnnotation) {
2022-05-25 00:35:08 +08:00
2022-06-09 02:58:38 +08:00
if (currentAnnotation.childIndex >= 1) {
switch (currentAnnotation.type) {
2022-06-08 00:18:32 +08:00
case 'arrow':
2022-06-09 02:58:38 +08:00
overlay_arrow(svg, currentAnnotation);
2022-06-08 00:18:32 +08:00
break;
2022-05-26 00:35:43 +08:00
case 'draw':
2022-06-09 02:58:38 +08:00
overlay_draw(svg, currentAnnotation);
2022-05-26 00:35:43 +08:00
break;
2022-06-01 20:03:40 +08:00
case 'ellipse':
2022-06-09 02:58:38 +08:00
overlay_ellipse(svg, currentAnnotation);
2022-06-01 20:03:40 +08:00
break;
2022-06-01 01:23:24 +08:00
case 'rectangle':
2022-06-09 02:58:38 +08:00
overlay_rectangle(svg, currentAnnotation);
2022-06-01 01:23:24 +08:00
break;
2022-05-25 00:35:08 +08:00
case 'sticky':
2022-06-09 02:58:38 +08:00
overlay_sticky(svg, currentAnnotation);
2022-05-25 00:35:08 +08:00
break;
2022-06-01 20:34:15 +08:00
case 'triangle':
2022-06-09 02:58:38 +08:00
overlay_triangle(svg, currentAnnotation);
2022-06-01 20:34:15 +08:00
break;
2022-02-16 21:28:35 +08:00
case 'text':
2022-06-09 02:58:38 +08:00
overlay_text(svg, currentAnnotation);
2022-02-16 21:28:35 +08:00
break;
2022-02-15 23:48:58 +08:00
default:
2022-06-09 02:58:38 +08:00
logger.info(`Unknown annotation type ${currentAnnotation.type}.`);
}
}
}
function overlay_annotations(svg, currentSlideAnnotations) {
// Sort annotations by lowest child index
currentSlideAnnotations = sortByKey(currentSlideAnnotations, 'annotationInfo', 'childIndex');
for (let annotation of currentSlideAnnotations) {
switch (annotation.annotationInfo.type) {
case 'group':
// Get annotations that have this group as parent
let children = annotation.annotationInfo.children;
for (let childId of children) {
let childAnnotation = currentSlideAnnotations.find(ann => ann.id == childId);
overlay_annotation(svg, childAnnotation.annotationInfo);
}
break;
default:
// Add individual annotations if they don't belong to a group
2022-06-14 17:19:43 +08:00
if (annotation.annotationInfo.parentId % 1 === 0) {
2022-06-09 02:58:38 +08:00
overlay_annotation(svg, annotation.annotationInfo);
}
2022-02-15 23:48:58 +08:00
}
}
}
2022-02-13 04:03:07 +08:00
// Process the presentation pages and annotations into a PDF file
// 1. Get the job
const dropbox = path.join(config.shared.presAnnDropboxDir, jobId);
let job = fs.readFileSync(path.join(dropbox, 'job'));
let exportJob = JSON.parse(job);
2022-02-13 04:03:07 +08:00
// 2. Get the annotations
let annotations = fs.readFileSync(path.join(dropbox, 'whiteboard'));
let whiteboard = JSON.parse(annotations);
let pages = JSON.parse(whiteboard.pages);
let ghostScriptInput = ""
2022-02-13 04:03:07 +08:00
// 3. Convert annotations to SVG
2022-04-05 21:01:26 +08:00
for (let currentSlide of pages) {
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
2022-06-14 17:19:43 +08:00
// Ideally, use dimensions provided by tldraw's background image asset instead
let dimensions = probe.sync(fs.readFileSync(`${backgroundImagePath}.${backgroundFormat}`));
let slideWidth = dimensions.width;
let slideHeight = dimensions.height;
// Create the SVG slide with the background image
2022-02-15 20:40:08 +08:00
let svg = create({ version: '1.0', encoding: 'UTF-8' })
2022-05-26 00:35:43 +08:00
.ele('svg', {
xmlns: 'http://www.w3.org/2000/svg',
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
width: slideWidth,
height: slideHeight,
})
.dtd({
pubID: '-//W3C//DTD SVG 1.1//EN',
sysID: 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'
})
.ele('image', {
'xlink:href': `file://${dropbox}/slide${currentSlide.page}.${backgroundFormat}`,
2022-05-26 00:35:43 +08:00
width: slideWidth,
height: slideHeight,
})
.up()
.ele('g', {
class: 'canvas'
});
2022-02-15 23:48:58 +08:00
// 4. Overlay annotations onto slides
2022-06-09 02:58:38 +08:00
overlay_annotations(svg, currentSlide.annotations)
2022-02-15 23:48:58 +08:00
2022-02-15 20:40:08 +08:00
svg = svg.end({ prettyPrint: true });
// Write annotated SVG file
let SVGfile = path.join(dropbox, `annotated-slide${currentSlide.page}.svg`)
let PDFfile = path.join(dropbox, `annotated-slide${currentSlide.page}.pdf`)
2022-05-26 00:35:43 +08:00
fs.writeFileSync(SVGfile, svg, function (err) {
if (err) { return logger.error(err); }
});
2022-02-16 01:11:13 +08:00
let convertAnnotatedSlide = [
'cairosvg',
SVGfile,
'-o', PDFfile
].join(' ');
2022-05-31 17:09:38 +08:00
execSync(convertAnnotatedSlide);
ghostScriptInput += `${PDFfile} `
}
// Create PDF output directory if it doesn't exist
let output_dir = path.join(exportJob.presLocation, 'pdfs', jobId);
if (!fs.existsSync(output_dir)) { fs.mkdirSync(output_dir, { recursive: true }); }
2022-05-01 05:28:11 +08:00
let filename = sanitize(exportJob.filename.replace(/\s/g, '_'));
let mergePDFs = [
'gs',
'-dNOPAUSE',
'-sDEVICE=pdfwrite',
`-sOUTPUTFILE="${path.join(output_dir, `${filename}.pdf`)}"`,
`-dBATCH`,
ghostScriptInput,
2022-05-26 00:35:43 +08:00
].join(' ');
2022-02-13 04:03:07 +08:00
// Resulting PDF file is stored in the presentation dir
2022-05-31 17:09:38 +08:00
execSync(mergePDFs);
2022-02-13 04:03:07 +08:00
// Launch Notifier Worker depending on job type
logger.info(`Saved PDF at ${output_dir}/${jobId}/${filename}.pdf`);
kickOffNotifierWorker(exportJob.jobType, filename);
2022-02-13 04:03:07 +08:00
2022-05-31 17:09:38 +08:00
parentPort.postMessage({ message: workerData });