From 88014dacaa04d3767140396a5b14f5824e3f5ac9 Mon Sep 17 00:00:00 2001 From: Jean-Yves NOLEN Date: Tue, 13 Aug 2024 15:12:50 +0200 Subject: [PATCH] feat: add image resolution --- .gitignore | 1 + diagrams.net/src/index.js | 7 +++-- diagrams.net/src/task.js | 3 +- diagrams.net/src/worker.js | 64 ++++++++++++++++++++++++-------------- 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 657e25835..0c2454fbb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ target/ /node_modules/ dependency-reduced-pom.xml .idea +**/.vscode \ No newline at end of file diff --git a/diagrams.net/src/index.js b/diagrams.net/src/index.js index 673d6b778..81c9791c2 100644 --- a/diagrams.net/src/index.js +++ b/diagrams.net/src/index.js @@ -1,7 +1,7 @@ // must be declared first import { logger } from './logger.js' import http from 'node:http' -import {TimeoutError as PuppeteerTimeoutError} from 'puppeteer' +import { TimeoutError as PuppeteerTimeoutError } from 'puppeteer' import micro from 'micro' import Task from './task.js' import { create } from './browser-instance.js' @@ -23,7 +23,8 @@ import { SyntaxError, TimeoutError, Worker } from './worker.js' if (diagramSource) { try { const isPng = outputType === 'png' - const output = await worker.convert(new Task(diagramSource, isPng)) + const krokiUnsafe = (process.env.KROKI_SAFE_MODE ?? 'secure').toLowerCase() === 'unsafe' + const output = await worker.convert(new Task(diagramSource, isPng, krokiUnsafe)) res.setHeader('Content-Type', isPng ? 'image/png' : 'image/svg+xml') return micro.send(res, 200, output) } catch (err) { @@ -72,7 +73,7 @@ import { SyntaxError, TimeoutError, Worker } from './worker.js' }) }) ) - server.listen(8005) + server.listen(9000) })().catch(err => { logger.error({ err }, 'Unable to start the service') process.exit(1) diff --git a/diagrams.net/src/task.js b/diagrams.net/src/task.js index 17305316f..cc2c4805c 100644 --- a/diagrams.net/src/task.js +++ b/diagrams.net/src/task.js @@ -1,6 +1,7 @@ export default class Task { - constructor (source, isPng = false) { + constructor (source, isPng = false, isUnsafe = false) { this.source = source this.isPng = isPng + this.isUnsafe = isUnsafe } } diff --git a/diagrams.net/src/worker.js b/diagrams.net/src/worker.js index a080fb52e..cc56a4402 100644 --- a/diagrams.net/src/worker.js +++ b/diagrams.net/src/worker.js @@ -2,7 +2,6 @@ import path from 'node:path' import { URL, fileURLToPath } from 'node:url' import puppeteer from 'puppeteer' - import { logger } from './logger.js' const __dirname = fileURLToPath(new URL('.', import.meta.url)) @@ -15,8 +14,10 @@ export class TimeoutError extends Error { export class SyntaxError extends Error { constructor (err) { - logger.error({ err }) - super(`Syntax error in graph: ${JSON.stringify(err)}`) + super('Syntax error in graph', { cause: err }) + logger.error(this) + this.name = 'SyntaxError' + this.message = err.message } } @@ -29,6 +30,36 @@ export class Worker { /** * + * @param {string} source + * @param {boolean} performResolveImage + * @returns {Promise} + */ + async browserRender (source, performResolveImage) { + const resolveImage = async function (svg) { + for (const img of await svg.querySelectorAll('image')) { + if (img.attributes['xlink:href'].value.startsWith('data:')) { + continue + } + const imgb64 = await fetch(img.attributes['xlink:href'].value).then(async (value) => { + const mimeType = value.headers.get('content-type') + const b64img = btoa(String.fromCharCode(...new Uint8Array(await value.arrayBuffer()))) + return `data:${mimeType};base64,${b64img}` + }) + img.setAttribute('xlink:href', imgb64) + img.removeAttribute('pointer-events') + } + return svg + } + const s = new XMLSerializer() + let svgRoot = render({ // eslint-disable-line no-undef + xml: source, + format: 'svg' + }).getSvg() + svgRoot = performResolveImage ? await resolveImage(svgRoot) : svgRoot + return s.serializeToString(svgRoot) + } + + /** * @param task * @returns {Promise} */ @@ -38,36 +69,21 @@ export class Worker { ignoreHTTPSErrors: true }) const page = await browser.newPage() + page.on('console', msg => { + console.log(msg.text()) + }) try { await page.setViewport({ height: 800, width: 600 }) await page.goto(this.pageUrl) const evalResult = await Promise.race([ - page.evaluate((source) => { - /* global render */ - try { - const svgRoot = render({ - xml: source, - format: 'svg' - }).getSvg() - const s = new XMLSerializer() - return { svg: s.serializeToString(svgRoot), error: null } - } catch (err) { - logger.log({ err }) - return { svg: null, error: err } - } - }, task.source), + page.evaluate(this.browserRender, task.source, task.isUnsafe).catch((err) => { throw new SyntaxError(err) }), new Promise((resolve, reject) => setTimeout(() => reject(new TimeoutError(this.convertTimeout)), this.convertTimeout)) ]) - if (evalResult && evalResult.error) { - throw new SyntaxError(evalResult.error) - } - // const bounds = await page.mainFrame().$eval('#LoadingComplete', div => div.getAttribute('bounds')) // const pageId = await page.mainFrame().$eval('#LoadingComplete', div => div.getAttribute('page-id')) // const scale = await page.mainFrame().$eval('#LoadingComplete', div => div.getAttribute('scale')) // const pageCount = parseInt(await page.mainFrame().$eval('#LoadingComplete', div => div.getAttribute('pageCount'))) - if (task.isPng) { await page.setContent(` @@ -76,7 +92,7 @@ export class Worker { -${evalResult.svg} +${evalResult} `) const container = await page.$('svg') @@ -85,7 +101,7 @@ ${evalResult.svg} omitBackground: true })) } else { - return evalResult.svg + return evalResult } } finally { try {