-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdemos.test.ts
More file actions
412 lines (374 loc) · 17.4 KB
/
demos.test.ts
File metadata and controls
412 lines (374 loc) · 17.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
/**
* Demo conversion tests.
* Loads each demo HTML file in a real browser, extracts the IR,
* and converts to both DXF and PDF output files.
*/
import { test, expect } from "@playwright/test";
import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { setupPage } from "../helpers.js";
import { DXFWriter } from "../../src/writers/dxf-writer.js";
import { PDFWriter } from "../../src/writers/pdf-writer.js";
import { SVGWriter } from "../../src/writers/svg-writer.js";
import { HTMLWriter } from "../../src/writers/html-writer.js";
import { EMFWriter } from "../../src/writers/emf-writer.js";
import { EMFPlusWriter } from "../../src/writers/emfplus-writer.js";
import { DWGWriter } from "../../src/writers/acad-writer.js";
import { AcadDXFWriter } from "../../src/writers/acad-writer.js";
import { renderIR } from "../../src/pipeline.js";
import type { IRNode } from "../../src/types.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const demosDir = path.resolve(__dirname, "..", "demos");
const outputDir = path.resolve(__dirname, "..", "output");
// Ensure output directory exists
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Discover all demo HTML files
const demoFiles = fs
.readdirSync(demosDir)
.filter((f) => f.endsWith(".html"))
.sort();
for (const demoFile of demoFiles) {
const name = path.basename(demoFile, ".html");
const convertFormControls = name === "form-controls" || name === "form2" || name === "github" || name === "google";
test(`convert demo: ${name}`, async ({ page, browserName }) => {
// Complex pages (e.g. github.html with external CSS) need more time,
// especially on Firefox which loads resources differently.
test.setTimeout(120_000);
const projectOutputDir = browserName === "chromium" ? outputDir : path.join(outputDir, browserName);
if (!fs.existsSync(projectOutputDir)) {
fs.mkdirSync(projectOutputDir, { recursive: true });
}
// Load demo HTML
const htmlContent = fs.readFileSync(
path.join(demosDir, demoFile),
"utf-8"
);
// Use goto with file URL so relative paths (e.g. img src) resolve correctly
const fileUrl = pathToFileURL(path.join(demosDir, demoFile)).href;
await page.goto(fileUrl, { waitUntil: "load" });
// Copy HTML to output dir (and any referenced subdirectories)
fs.writeFileSync(path.join(projectOutputDir, demoFile), htmlContent, "utf-8");
const svgsDir = path.join(demosDir, "svgs");
const svgsOutDir = path.join(projectOutputDir, "svgs");
if (fs.existsSync(svgsDir) && !fs.existsSync(svgsOutDir)) {
fs.cpSync(svgsDir, svgsOutDir, { recursive: true });
}
// Copy PNG files referenced by demo HTML files
for (const file of fs.readdirSync(demosDir)) {
if (file.endsWith(".png")) {
const src = path.join(demosDir, file);
const dest = path.join(projectOutputDir, file);
if (!fs.existsSync(dest)) {
fs.copyFileSync(src, dest);
}
}
}
// Inject polyfill + library via helpers
const { injectBoxQuadsPolyfill, injectLibrary } = await import(
"../helpers.js"
);
await injectBoxQuadsPolyfill(page);
await injectLibrary(page);
const walkIframes = await page.evaluate(() => document.querySelector("iframe") !== null);
// Pre-convert file:// URLs to data URLs (file:// taints canvas and blocks XHR in Chromium)
// Collect all image src and background-image URLs from the page (including shadow DOM)
const fileUrls: string[] = await page.evaluate(() => {
const urls: string[] = [];
function walk(root: Document | ShadowRoot | Element) {
const els = root.querySelectorAll("*");
for (const el of Array.from(els)) {
if (el.tagName === "IMG") {
const src = (el as HTMLImageElement).src;
if (src && !src.startsWith("data:")) urls.push(src);
}
const bg = getComputedStyle(el).backgroundImage;
if (bg && bg !== "none") {
const m = bg.match(/url\(["']?([^"')]+)["']?\)/);
if (m && m[1] && !m[1].startsWith("data:")) urls.push(m[1]);
}
if (el.shadowRoot) walk(el.shadowRoot);
}
}
walk(document);
return [...new Set(urls)];
});
const dataUrlMap: Record<string, string> = {};
for (const src of fileUrls) {
try {
const filePath = src.startsWith("file:///") ? fileURLToPath(src) : src;
if (fs.existsSync(filePath)) {
const buf = fs.readFileSync(filePath);
const ext = path.extname(filePath).toLowerCase();
const mime = ext === ".svg" ? "image/svg+xml"
: ext === ".jpg" || ext === ".jpeg" ? "image/jpeg"
: ext === ".gif" ? "image/gif" : "image/png";
dataUrlMap[src] = `data:${mime};base64,${buf.toString("base64")}`;
}
} catch { /* skip */ }
}
if (Object.keys(dataUrlMap).length > 0) {
await page.evaluate((map) => {
function walk(root: Document | ShadowRoot | Element) {
for (const el of Array.from(root.querySelectorAll("*"))) {
if (el.tagName === "IMG") {
const img = el as HTMLImageElement;
if (map[img.src]) img.src = map[img.src];
}
const bg = getComputedStyle(el).backgroundImage;
if (bg && bg !== "none") {
const m = bg.match(/url\(["']?([^"')]+)["']?\)/);
if (m && m[1] && map[m[1]]) {
(el as HTMLElement).style.backgroundImage = `url("${map[m[1]]}")`;
}
}
if (el.shadowRoot) walk(el.shadowRoot);
}
}
walk(document);
}, dataUrlMap);
}
// Extract IR in the browser
const ir: IRNode[] = await page.evaluate(({ shouldConvertFormControls, shouldWalkIframes }: { shouldConvertFormControls: boolean; shouldWalkIframes: boolean }) => {
const root = document.getElementById("root") ?? document.body;
return (window as any).__HC.extractIR(root, {
boxType: "border",
includeText: true,
includeImages: true,
convertFormControls: shouldConvertFormControls,
walkIframes: shouldWalkIframes,
textMeasurement: "auto",
});
}, { shouldConvertFormControls: convertFormControls, shouldWalkIframes: walkIframes });
expect(ir.length).toBeGreaterThan(0);
// Dump IR for specific demos
if (name === "comprehensive" || name === "images" || name === "test4" || name === "github") {
const irPath = path.join(projectOutputDir, `${name}-ir.json`);
fs.writeFileSync(irPath, JSON.stringify(ir, null, 2), "utf-8");
}
const viewport = await page.evaluate(() => {
const root = document.getElementById("root") ?? document.body;
const rootElement = root as Element & {
getBoxQuads?: (options?: { box?: "border" | "content" }) => DOMQuad[];
};
const quads = rootElement.getBoxQuads?.({ box: "border" }) ?? [];
if (quads.length > 0) {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const quad of quads) {
for (const point of [quad.p1, quad.p2, quad.p3, quad.p4]) {
if (point.x < minX) minX = point.x;
if (point.y < minY) minY = point.y;
if (point.x > maxX) maxX = point.x;
if (point.y > maxY) maxY = point.y;
}
}
const width = Math.ceil(Math.max(maxX - minX, root.scrollWidth, root.clientWidth));
const height = Math.ceil(Math.max(maxY - minY, root.scrollHeight, root.clientHeight));
return {
width: width || 1,
height: height || 1,
};
}
const rect = root.getBoundingClientRect();
return {
width: Math.ceil(Math.max(rect.width, root.scrollWidth, root.clientWidth)) || 1,
height: Math.ceil(Math.max(rect.height, root.scrollHeight, root.clientHeight)) || 1,
};
});
// Fallback to the IR extent if the root box was unexpectedly empty.
let maxX = 0, maxY = 0;
for (const node of ir) {
const pts: Array<{ x: number; y: number }> =
node.type === "polygon" || node.type === "polyline" ? node.points
: node.type === "text" || node.type === "image" ? node.quad
: [];
for (const p of pts) {
if (p.x > maxX) maxX = p.x;
if (p.y > maxY) maxY = p.y;
}
}
if (viewport.width <= 1 && maxX > 0) viewport.width = Math.ceil(maxX);
if (viewport.height <= 1 && maxY > 0) viewport.height = Math.ceil(maxY);
// --- DXF output ---
const dxfWriter = new DXFWriter(viewport.height);
const dxfContent = await renderIR(ir, dxfWriter);
expect(dxfContent).toBeTruthy();
expect(dxfContent.length).toBeGreaterThan(100);
const dxfPath = path.join(projectOutputDir, `${name}.dxf`);
fs.writeFileSync(dxfPath, dxfContent, "utf-8");
// Save DXF image files alongside the DXF (referenced by IMAGE entities)
for (const [relPath, imageDataUrl] of dxfWriter.imageFiles) {
const imgPath = path.join(projectOutputDir, relPath);
const imgDir = path.dirname(imgPath);
if (!fs.existsSync(imgDir)) fs.mkdirSync(imgDir, { recursive: true });
const base64Match = imageDataUrl.match(/^data:[^;]+;base64,(.+)$/);
if (base64Match) {
fs.writeFileSync(imgPath, Buffer.from(base64Match[1], "base64"));
}
}
// --- PDF output ---
// Convert viewport px to mm (1px ≈ 0.2646mm)
// Load custom TTF font files from the demos directory
const customFonts = new Map<string, Uint8Array>();
for (const file of fs.readdirSync(demosDir)) {
if (file.endsWith(".ttf") || file.endsWith(".otf")) {
const fontFamily = path.basename(file, path.extname(file));
const fontData = fs.readFileSync(path.join(demosDir, file));
customFonts.set(fontFamily, new Uint8Array(fontData));
// Also copy font file to output dir
const dest = path.join(projectOutputDir, file);
if (!fs.existsSync(dest)) fs.copyFileSync(path.join(demosDir, file), dest);
}
}
// Load a default Unicode-capable font for full character support in PDF
let defaultFont: Uint8Array | undefined;
const defaultFontPaths = [
"C:\\Windows\\Fonts\\segoeui.ttf",
"C:\\Windows\\Fonts\\arial.ttf",
"C:\\Windows\\Fonts\\wingding.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/System/Library/Fonts/Helvetica.ttc",
];
for (const fp of defaultFontPaths) {
if (fs.existsSync(fp)) {
defaultFont = new Uint8Array(fs.readFileSync(fp));
break;
}
}
// Load symbol/fallback fonts for characters not in the default font (e.g. ⚖ U+2696)
const symbolFontPaths = [
"C:\\Windows\\Fonts\\seguisym.ttf", // Segoe UI Symbol
"C:\\Windows\\Fonts\\symbol.ttf", // Symbol
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
];
// Load Wingdings font for symbolic characters (å → arrow etc.)
const wingdingsPaths = [
"C:\\Windows\\Fonts\\wingding.ttf",
"C:\\Windows\\Fonts\\WINGDING.TTF",
];
for (const fp of wingdingsPaths) {
if (fs.existsSync(fp) && !customFonts.has("wingdings")) {
customFonts.set("wingdings", new Uint8Array(fs.readFileSync(fp)));
break;
}
}
for (const fp of symbolFontPaths) {
if (fs.existsSync(fp)) {
const fontFamily = path.basename(fp, path.extname(fp));
if (!customFonts.has(fontFamily)) {
customFonts.set(fontFamily, new Uint8Array(fs.readFileSync(fp)));
}
}
}
const pdfWriter = new PDFWriter(viewport.width * 0.2646, viewport.height * 0.2646, customFonts.size > 0 ? customFonts : undefined, defaultFont);
const pdfDoc = await renderIR(ir, pdfWriter);
expect(pdfDoc).toBeTruthy();
await pdfDoc.finalize();
const pdfBuffer = pdfDoc.toBytes();
const pdfPath = path.join(projectOutputDir, `${name}.pdf`);
fs.writeFileSync(pdfPath, pdfBuffer);
// --- PNG output ---
// PNG writer needs Canvas API so it runs in the browser
// Firefox blocks toDataURL on file:// origins (security restriction), so
// we wrap this step in try/catch and skip PNG output when it fails.
let pngStat: fs.Stats | null = null;
const pngPath = path.join(projectOutputDir, `${name}.png`);
try {
const pngDataUrl: string = await page.evaluate(async (irNodes) => {
let maxX = 0, maxY = 0;
for (const node of irNodes) {
const pts: Array<{ x: number; y: number }> =
node.type === "polygon" || node.type === "polyline" ? node.points
: node.type === "text" || node.type === "image" ? node.quad
: [];
for (const p of pts) {
if (p.x > maxX) maxX = p.x;
if (p.y > maxY) maxY = p.y;
}
}
const vp = { width: Math.ceil(maxX) || 1, height: Math.ceil(maxY) || 1 };
const writer = new (window as any).__HC.PNGWriter(vp.width, vp.height);
const pngResult = await (window as any).__HC.renderIR(irNodes, writer);
await pngResult.finalize();
return pngResult.toDataURL();
}, ir);
expect(pngDataUrl).toMatch(/^data:image\/png;base64,/);
const pngBase64 = pngDataUrl.split(",")[1];
const pngBuffer = Buffer.from(pngBase64, "base64");
fs.writeFileSync(pngPath, pngBuffer);
pngStat = fs.statSync(pngPath);
} catch {
console.log(` ⚠ ${name}: PNG output skipped (canvas security restriction)`);
}
// --- SVG output ---
const svgWriter = new SVGWriter(viewport.width, viewport.height);
const svgContent = await renderIR(ir, svgWriter);
expect(svgContent).toBeTruthy();
expect(svgContent.length).toBeGreaterThan(100);
const svgPath = path.join(projectOutputDir, `${name}.svg`);
fs.writeFileSync(svgPath, svgContent, "utf-8");
// --- HTML output ---
const htmlWriter = new HTMLWriter(viewport.width, viewport.height);
const htmlContent2 = await renderIR(ir, htmlWriter);
expect(htmlContent2).toBeTruthy();
expect(htmlContent2.length).toBeGreaterThan(100);
const htmlOutPath = path.join(projectOutputDir, `${name}-ir.html`);
fs.writeFileSync(htmlOutPath, htmlContent2, "utf-8");
// --- EMF output ---
const emfWriter = new EMFWriter({ width: viewport.width, height: viewport.height });
const emfBytes = await renderIR(ir, emfWriter);
expect(emfBytes).toBeInstanceOf(Uint8Array);
expect(emfBytes.length).toBeGreaterThan(80);
const emfPath = path.join(projectOutputDir, `${name}.emf`);
fs.writeFileSync(emfPath, emfBytes);
// --- EMF+ output ---
const emfPlusWriter = new EMFPlusWriter({ width: viewport.width, height: viewport.height });
const emfPlusBytes = await renderIR(ir, emfPlusWriter);
expect(emfPlusBytes).toBeInstanceOf(Uint8Array);
expect(emfPlusBytes.length).toBeGreaterThan(80);
const emfPlusPath = path.join(projectOutputDir, `${name}-emfplus.emf`);
fs.writeFileSync(emfPlusPath, emfPlusBytes);
// --- DWG output ---
const dwgWriter = new DWGWriter({ maxY: viewport.height });
const dwgBytes = await renderIR(ir, dwgWriter);
expect(dwgBytes).toBeInstanceOf(Uint8Array);
expect(dwgBytes.length).toBeGreaterThan(100);
const dwgPath = path.join(projectOutputDir, `${name}.dwg`);
fs.writeFileSync(dwgPath, dwgBytes);
// --- Acad DXF output ---
const acadDxfWriter = new AcadDXFWriter({ maxY: viewport.height });
const acadDxfBytes = await renderIR(ir, acadDxfWriter);
expect(acadDxfBytes).toBeInstanceOf(Uint8Array);
expect(acadDxfBytes.length).toBeGreaterThan(100);
const acadDxfPath = path.join(projectOutputDir, `${name}-acad.dxf`);
fs.writeFileSync(acadDxfPath, acadDxfBytes);
// Verify files are non-empty
const dxfStat = fs.statSync(dxfPath);
const pdfStat = fs.statSync(pdfPath);
if (!pngStat && fs.existsSync(pngPath)) pngStat = fs.statSync(pngPath);
const svgStat = fs.statSync(svgPath);
const htmlStat = fs.statSync(htmlOutPath);
const emfStat = fs.statSync(emfPath);
const emfPlusStat = fs.statSync(emfPlusPath);
const dwgStat = fs.statSync(dwgPath);
const acadDxfStat = fs.statSync(acadDxfPath);
expect(dxfStat.size).toBeGreaterThan(0);
expect(pdfStat.size).toBeGreaterThan(0);
if (pngStat) expect(pngStat.size).toBeGreaterThan(0);
expect(svgStat.size).toBeGreaterThan(0);
expect(htmlStat.size).toBeGreaterThan(0);
expect(emfStat.size).toBeGreaterThan(0);
expect(emfPlusStat.size).toBeGreaterThan(0);
expect(dwgStat.size).toBeGreaterThan(0);
expect(acadDxfStat.size).toBeGreaterThan(0);
console.log(
` \u2713 ${name}: ${ir.length} IR nodes \u2192 DXF (${dxfStat.size} bytes), PDF (${pdfStat.size} bytes), PNG (${pngStat ? pngStat.size + " bytes" : "skipped"}), SVG (${svgStat.size} bytes), HTML (${htmlStat.size} bytes), EMF (${emfStat.size} bytes), EMF+ (${emfPlusStat.size} bytes), DWG (${dwgStat.size} bytes), AcadDXF (${acadDxfStat.size} bytes)`
);
});
}