@boxpdf/html-writer 0.1.21 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +80 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +80 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -348,17 +348,20 @@ function number(value) {
|
|
|
348
348
|
// src/visual-font.ts
|
|
349
349
|
function visualFontAliases(pageNumber, fonts) {
|
|
350
350
|
return new Map(
|
|
351
|
-
fonts.filter(
|
|
351
|
+
fonts.filter(
|
|
352
|
+
(font) => (font.format === "truetype" || font.format === "opentype") && !/(?:courier|^TTE)/i.test(font.family ?? "")
|
|
353
|
+
).map((font) => [font.id, `boxpdf-${pageNumber}-${font.id}`])
|
|
352
354
|
);
|
|
353
355
|
}
|
|
354
356
|
function visualFontFace(font, aliases) {
|
|
355
|
-
if (font.format !== "truetype") return "";
|
|
357
|
+
if (font.format !== "truetype" && font.format !== "opentype") return "";
|
|
356
358
|
const alias = aliases.get(font.id);
|
|
357
359
|
if (!alias) return "";
|
|
358
360
|
const styles2 = visualFontStyles(font.family, alias).filter(
|
|
359
361
|
(style) => !style.startsWith("font-family:")
|
|
360
362
|
);
|
|
361
|
-
|
|
363
|
+
const mime = font.format === "opentype" ? "font/otf" : "font/ttf";
|
|
364
|
+
return `@font-face{font-family:${alias};src:url(data:${mime};base64,${base64(font.data)}) format("${font.format}");${styles2.join(";")}}`;
|
|
362
365
|
}
|
|
363
366
|
function visualFontStyles(fontFamily, alias) {
|
|
364
367
|
const normalized = fontFamily?.toLowerCase() ?? "";
|
|
@@ -440,7 +443,9 @@ function vectorMedia(page, imageOptions) {
|
|
|
440
443
|
);
|
|
441
444
|
const aliases = visualFontAliases(page.number, page.fonts ?? []);
|
|
442
445
|
const visualCodeFonts = new Set(
|
|
443
|
-
(page.fonts ?? []).filter(
|
|
446
|
+
(page.fonts ?? []).filter(
|
|
447
|
+
(font) => (font.format === "truetype" || font.format === "opentype") && font.visualCodeMapping
|
|
448
|
+
).map((font) => font.id)
|
|
444
449
|
);
|
|
445
450
|
return components.map((component, componentIndex) => {
|
|
446
451
|
const bounds2 = component.bounds;
|
|
@@ -1293,7 +1298,8 @@ async function writeHtmlDocument(pages, write, options = {}) {
|
|
|
1293
1298
|
);
|
|
1294
1299
|
options.onSemanticStats?.(stats);
|
|
1295
1300
|
} else {
|
|
1296
|
-
|
|
1301
|
+
const documentFonts = { entries: [] };
|
|
1302
|
+
for await (const page of pages) await writePositionedPage(page, write, options, documentFonts);
|
|
1297
1303
|
}
|
|
1298
1304
|
await write("</main>");
|
|
1299
1305
|
if (includeDocument) await write("</body></html>");
|
|
@@ -1336,7 +1342,7 @@ async function pageToHtml(page, options = {}) {
|
|
|
1336
1342
|
);
|
|
1337
1343
|
return output;
|
|
1338
1344
|
}
|
|
1339
|
-
async function writePositionedPage(page, write, options) {
|
|
1345
|
+
async function writePositionedPage(page, write, options, documentFonts) {
|
|
1340
1346
|
const imageOptions = resolveImageOptions("visual", options);
|
|
1341
1347
|
const visualImages = await prepareVisualImages(page, imageOptions, options.onImage);
|
|
1342
1348
|
const visualSpans = coalesceVisualSpans(page.visualSpans ?? page.spans);
|
|
@@ -1347,14 +1353,18 @@ async function writePositionedPage(page, write, options) {
|
|
|
1347
1353
|
await write(
|
|
1348
1354
|
`<section class="pdf-page pdf-page--visual pdf-page--positioned" data-page="${page.number}" data-rotate="${page.rotate}" style="width:${number3(displayWidth)}pt;height:${number3(displayHeight)}pt">`
|
|
1349
1355
|
);
|
|
1350
|
-
const
|
|
1356
|
+
const { aliases: fontAliases, fontsToEmit } = visualPageFonts(
|
|
1357
|
+
page.number,
|
|
1358
|
+
page.fonts ?? [],
|
|
1359
|
+
documentFonts
|
|
1360
|
+
);
|
|
1351
1361
|
const type3Fonts = new Map(
|
|
1352
1362
|
(page.fonts ?? []).filter((font) => font.format === "type3").map((font) => [font.id, font])
|
|
1353
1363
|
);
|
|
1354
1364
|
const textClasses = options.includeStyles ?? true ? visualTextClasses(page.number, visualSpans, fontAliases) : void 0;
|
|
1355
|
-
if ((options.includeStyles ?? true) &&
|
|
1365
|
+
if ((options.includeStyles ?? true) && fontsToEmit.length) {
|
|
1356
1366
|
await write(
|
|
1357
|
-
`<style>${
|
|
1367
|
+
`<style>${fontsToEmit.map((font) => visualFontFace(font, fontAliases)).join("")}</style>`
|
|
1358
1368
|
);
|
|
1359
1369
|
}
|
|
1360
1370
|
if (textClasses?.css) await write(`<style>${textClasses.css}</style>`);
|
|
@@ -1421,6 +1431,30 @@ async function writePositionedPage(page, write, options) {
|
|
|
1421
1431
|
}
|
|
1422
1432
|
await write("</div></section>");
|
|
1423
1433
|
}
|
|
1434
|
+
function visualPageFonts(pageNumber, fonts, documentFonts) {
|
|
1435
|
+
const aliases = visualFontAliases(pageNumber, fonts);
|
|
1436
|
+
if (!documentFonts) return { aliases, fontsToEmit: fonts };
|
|
1437
|
+
const fontsToEmit = [];
|
|
1438
|
+
for (const font of fonts) {
|
|
1439
|
+
if (!aliases.has(font.id) || font.format === "type3") continue;
|
|
1440
|
+
const existing = documentFonts.entries.find(
|
|
1441
|
+
(entry) => entry.format === font.format && equalBytes(entry.data, font.data)
|
|
1442
|
+
);
|
|
1443
|
+
if (existing) {
|
|
1444
|
+
aliases.set(font.id, existing.alias);
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
const alias = `boxpdf-document-font-${documentFonts.entries.length + 1}`;
|
|
1448
|
+
aliases.set(font.id, alias);
|
|
1449
|
+
documentFonts.entries.push({ alias, data: font.data, format: font.format });
|
|
1450
|
+
fontsToEmit.push(font);
|
|
1451
|
+
}
|
|
1452
|
+
return { aliases, fontsToEmit };
|
|
1453
|
+
}
|
|
1454
|
+
function equalBytes(left, right) {
|
|
1455
|
+
if (left.length !== right.length) return false;
|
|
1456
|
+
return left.every((value, index) => value === right[index]);
|
|
1457
|
+
}
|
|
1424
1458
|
function coalesceVisualSpans(spans) {
|
|
1425
1459
|
const output = [];
|
|
1426
1460
|
for (const span of spans) {
|
|
@@ -1446,11 +1480,14 @@ function canCoalesceVisualSpans(left, right) {
|
|
|
1446
1480
|
if (left.direction !== "ltr" || right.direction !== "ltr") return false;
|
|
1447
1481
|
if (/guardian/i.test(left.fontFamily ?? "")) return false;
|
|
1448
1482
|
if (left.glyphCodes || right.glyphCodes) return false;
|
|
1483
|
+
if (left.fontAssetId || right.fontAssetId) return false;
|
|
1449
1484
|
if (!sameVisualTextState(left, right)) return false;
|
|
1450
1485
|
const tolerance = Math.max(0.02, left.fontSize * 0.015);
|
|
1451
1486
|
if (Math.abs(left.bounds.y - right.bounds.y) > tolerance) return false;
|
|
1452
1487
|
const gap = right.bounds.x - (left.bounds.x + left.bounds.width);
|
|
1453
|
-
|
|
1488
|
+
if (right.hasLeadingSpace) return false;
|
|
1489
|
+
if (gap >= -tolerance && gap <= tolerance) return true;
|
|
1490
|
+
return right.textAdjustmentBefore !== void 0 && right.textAdjustmentBefore < 0 && Math.abs(right.textAdjustmentBefore - gap) <= 1e-3;
|
|
1454
1491
|
}
|
|
1455
1492
|
function sameVisualTextState(left, right) {
|
|
1456
1493
|
return Math.abs(left.fontSize - right.fontSize) <= 1e-3 && left.fontName === right.fontName && left.fontFamily === right.fontFamily && left.fontAssetId === right.fontAssetId && left.color === right.color && left.fillOpacity === right.fillOpacity && left.strokeColor === right.strokeColor && left.strokeWidth === right.strokeWidth && left.strokeOpacity === right.strokeOpacity && left.renderingMode === right.renderingMode && sameTransform(left.transform, right.transform);
|
|
@@ -1491,6 +1528,13 @@ function visualTextLine(spans, startIndex, styleClasses, pageHeight, fontAliases
|
|
|
1491
1528
|
if (endIndex === startIndex) return void 0;
|
|
1492
1529
|
const baseline = pageHeight - first.bounds.y;
|
|
1493
1530
|
const lineSpans = spans.slice(startIndex, endIndex + 1);
|
|
1531
|
+
const dxRun = visualDxTextRun(lineSpans, fontAliases);
|
|
1532
|
+
if (dxRun) {
|
|
1533
|
+
return {
|
|
1534
|
+
html: `<text class="${className}" x="${number3(first.bounds.x)}" y="${number3(baseline)}" dx="${dxRun.offsets.map(number3).join(" ")}">${escapeHtml4(dxRun.text)}</text>`,
|
|
1535
|
+
endIndex
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1494
1538
|
const content = lineSpans.map(
|
|
1495
1539
|
(span, index) => visualTextTspan(span, index > 0 ? textSpanGap(lineSpans[index - 1], span) : void 0)
|
|
1496
1540
|
).join("");
|
|
@@ -1499,6 +1543,32 @@ function visualTextLine(spans, startIndex, styleClasses, pageHeight, fontAliases
|
|
|
1499
1543
|
endIndex
|
|
1500
1544
|
};
|
|
1501
1545
|
}
|
|
1546
|
+
function visualDxTextRun(spans, fontAliases) {
|
|
1547
|
+
const first = spans[0];
|
|
1548
|
+
if (!first?.fontAssetId || !fontAliases.has(first.fontAssetId) || spans.length < 2)
|
|
1549
|
+
return void 0;
|
|
1550
|
+
let text = first.text;
|
|
1551
|
+
const offsets = characterOffsets(first, 0);
|
|
1552
|
+
for (let index = 1; index < spans.length; index += 1) {
|
|
1553
|
+
const previous = spans[index - 1];
|
|
1554
|
+
const current = spans[index];
|
|
1555
|
+
if (!previous || !current || current.fontAssetId !== first.fontAssetId) return void 0;
|
|
1556
|
+
const characters = [...current.text];
|
|
1557
|
+
if (characters.length === 0 || previous.naturalWidth === void 0) return void 0;
|
|
1558
|
+
const gap = current.bounds.x - (previous.bounds.x + previous.bounds.width);
|
|
1559
|
+
const trailingSpacing = (previous.characterSpacing ?? 0) + (previous.text.endsWith(" ") ? previous.wordSpacing ?? 0 : 0);
|
|
1560
|
+
offsets.push(...characterOffsets(current, gap + trailingSpacing));
|
|
1561
|
+
text += current.text;
|
|
1562
|
+
}
|
|
1563
|
+
while (offsets.at(-1) === 0) offsets.pop();
|
|
1564
|
+
return offsets.length > 0 ? { text, offsets } : void 0;
|
|
1565
|
+
}
|
|
1566
|
+
function characterOffsets(span, first) {
|
|
1567
|
+
const characters = [...span.text];
|
|
1568
|
+
return characters.map(
|
|
1569
|
+
(_, index) => index === 0 ? first : (span.characterSpacing ?? 0) + (characters[index - 1] === " " ? span.wordSpacing ?? 0 : 0)
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1502
1572
|
function visualTextTspan(span, dx) {
|
|
1503
1573
|
const extent = span.bounds.width;
|
|
1504
1574
|
const offset = dx === void 0 || number3(dx) === "0" ? "" : ` dx="${number3(dx)}"`;
|