Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"celaria-formats": "^1.0.2",
"chess.js": "^1.4.0",
"confbox": "^0.2.4",
"dom-to-svg": "^0.12.2",
"imagetracer": "^0.2.2",
"js-synthesizer": "^1.11.0",
"json5": "^2.2.3",
Expand Down
166 changes: 166 additions & 0 deletions src/handlers/htmlToSvg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { elementToSVG, inlineResources } from "dom-to-svg";
import CommonFormats, { Category } from "src/CommonFormats.ts";
import type { FileData, FileFormat, FormatHandler } from "../FormatHandler.ts";

function nextPaint(): Promise<void> {
return new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => resolve());
});
});
}

async function waitForRenderableAssets(root: ParentNode): Promise<void> {
const pendingImages = Array.from(root.querySelectorAll("img"))
.filter(image => !image.complete)
.map(image => new Promise<void>(resolve => {
image.addEventListener("load", () => resolve(), { once: true });
image.addEventListener("error", () => resolve(), { once: true });
}));

const pendingVideos = Array.from(root.querySelectorAll("video"))
.filter(video => video.readyState < 2)
.map(video => new Promise<void>(resolve => {
video.addEventListener("loadeddata", () => resolve(), { once: true });
video.addEventListener("error", () => resolve(), { once: true });
}));

await Promise.all([...pendingImages, ...pendingVideos]);
await nextPaint();
}

type HtmlToSvgOptions = {
width?: number;
height?: number;
backgroundColor?: string;
};

function measureRenderedElement(
element: Element,
options: HtmlToSvgOptions,
): { width: number; height: number } {
const rect = element.getBoundingClientRect();
const widthCandidate = element instanceof HTMLElement || element instanceof SVGElement
? Math.max(rect.width, element.scrollWidth || 0, element.clientWidth || 0)
: rect.width;
const heightCandidate = element instanceof HTMLElement || element instanceof SVGElement
? Math.max(rect.height, element.scrollHeight || 0, element.clientHeight || 0)
: rect.height;

return {
width: Math.max(1, Math.ceil(options.width ?? widthCandidate)),
height: Math.max(1, Math.ceil(options.height ?? heightCandidate)),
};
}

async function renderRootToSvgString(
root: HTMLElement,
options: HtmlToSvgOptions,
): Promise<string> {
await waitForRenderableAssets(root);

const { width, height } = measureRenderedElement(root, options);
const existingStyle = root.getAttribute("style") || "";
const bg = options.backgroundColor ? `background-color:${options.backgroundColor};` : "";
root.setAttribute(
"style",
`${existingStyle}${bg}width:${width}px;height:${height}px;box-sizing:border-box;`,
);

await nextPaint();

const bounds = root.getBoundingClientRect();
const svgDocument = elementToSVG(root, { captureArea: bounds });
await inlineResources(svgDocument.documentElement);
return new XMLSerializer().serializeToString(svgDocument);
}

async function htmlContentToSvgString(
htmlContent: string,
options: HtmlToSvgOptions = {},
): Promise<string> {
const parsed = new DOMParser().parseFromString(htmlContent, "text/html");
const host = document.createElement("div");
host.style.all = "initial";
host.style.position = "fixed";
host.style.left = "-20000px";
host.style.top = "0";
host.style.pointerEvents = "none";
host.style.background = "transparent";
document.body.appendChild(host);

try {
const shadow = host.attachShadow({ mode: "closed" });

for (const styleElement of Array.from(parsed.querySelectorAll("style"))) {
shadow.appendChild(styleElement.cloneNode(true));
}

const root = document.createElement("div");
const bodyStyle = parsed.body.getAttribute("style");
if (bodyStyle) root.setAttribute("style", bodyStyle);

const sourceNodes = parsed.body.childNodes.length > 0
? Array.from(parsed.body.childNodes)
: Array.from(parsed.documentElement.childNodes);
for (const childNode of sourceNodes) {
root.appendChild(childNode.cloneNode(true));
}

shadow.appendChild(root);

return await renderRootToSvgString(root, options);
} finally {
host.remove();
}
}

class HtmlToSvgHandler implements FormatHandler {

public name: string = "dom-to-svg";

public supportedFormats: FileFormat[] = [
CommonFormats.HTML.supported("html", true, false),
CommonFormats.SVG.supported("svg", false, true, false, {
category: [Category.IMAGE, Category.VECTOR],
})
];

public ready: boolean = true;

async init () {
this.ready = true;
}

async doConvert (
inputFiles: FileData[],
inputFormat: FileFormat,
outputFormat: FileFormat,
): Promise<FileData[]> {

if (inputFormat.internal !== "html") throw "Invalid input format.";
if (outputFormat.internal !== "svg") throw "Invalid output format.";

const outputFiles: FileData[] = [];

const encoder = new TextEncoder();
const decoder = new TextDecoder();

for (const inputFile of inputFiles) {
const { name, bytes } = inputFile;
const htmlStr = decoder.decode(bytes);
const svgStr = await htmlContentToSvgString(htmlStr);
const newName = (name.endsWith(".html") ? name.slice(0, -5) : name) + ".svg";
outputFiles.push({
name: newName,
bytes: encoder.encode(svgStr),
});
}

return outputFiles;

}

}

export default HtmlToSvgHandler;
4 changes: 2 additions & 2 deletions src/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import svgTraceHandler from "./svgTrace.ts";
import { renameZipHandler, renameTxtHandler, renameJsonHandler } from "./rename.ts";
import envelopeHandler from "./envelope.ts";
import pandocHandler from "./pandoc.ts";
import svgForeignObjectHandler from "./svgForeignObject.ts";
import htmlToSvgHandler from "./htmlToSvg.ts";
import qoiFuHandler from "./qoi-fu.ts";
import sppdHandler from "./sppd.ts";
import threejsHandler from "./threejs.ts";
Expand Down Expand Up @@ -88,7 +88,7 @@ try { handlers.push(renameZipHandler) } catch (_) { };
try { handlers.push(renameTxtHandler) } catch (_) { };
try { handlers.push(renameJsonHandler) } catch (_) { };
try { handlers.push(new envelopeHandler()) } catch (_) { };
try { handlers.push(new svgForeignObjectHandler()) } catch (_) { };
try { handlers.push(new htmlToSvgHandler()) } catch (_) { };
try { handlers.push(new qoiFuHandler()) } catch (_) { };
try { handlers.push(new sppdHandler()) } catch (_) { };
try { handlers.push(new threejsHandler()) } catch (_) { };
Expand Down
106 changes: 0 additions & 106 deletions src/handlers/svgForeignObject.ts

This file was deleted.

Loading
Loading