pretext-pdf 1.1.1 → 1.6.0
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/CHANGELOG.md +659 -0
- package/README.md +82 -7
- package/dist/allowed-props.d.ts +76 -0
- package/dist/allowed-props.d.ts.map +1 -1
- package/dist/allowed-props.js.map +1 -1
- package/dist/assets/generators/barcode.d.ts +9 -0
- package/dist/assets/generators/barcode.d.ts.map +1 -0
- package/dist/assets/generators/barcode.js +24 -0
- package/dist/assets/generators/barcode.js.map +1 -0
- package/dist/assets/generators/chart.d.ts +13 -0
- package/dist/assets/generators/chart.d.ts.map +1 -0
- package/dist/assets/generators/chart.js +32 -0
- package/dist/assets/generators/chart.js.map +1 -0
- package/dist/assets/generators/qr.d.ts +9 -0
- package/dist/assets/generators/qr.d.ts.map +1 -0
- package/dist/assets/generators/qr.js +25 -0
- package/dist/assets/generators/qr.js.map +1 -0
- package/dist/assets/index.d.ts +19 -0
- package/dist/assets/index.d.ts.map +1 -0
- package/dist/assets/index.js +19 -0
- package/dist/assets/index.js.map +1 -0
- package/dist/assets/loaders/images.d.ts +20 -0
- package/dist/assets/loaders/images.d.ts.map +1 -0
- package/dist/assets/loaders/images.js +69 -0
- package/dist/assets/loaders/images.js.map +1 -0
- package/dist/assets/loaders/orchestrator.d.ts +24 -0
- package/dist/assets/loaders/orchestrator.d.ts.map +1 -0
- package/dist/assets/loaders/orchestrator.js +109 -0
- package/dist/assets/loaders/orchestrator.js.map +1 -0
- package/dist/assets/loaders/vectors.d.ts +25 -0
- package/dist/assets/loaders/vectors.d.ts.map +1 -0
- package/dist/assets/loaders/vectors.js +118 -0
- package/dist/assets/loaders/vectors.js.map +1 -0
- package/dist/assets/loaders/watermark.d.ts +12 -0
- package/dist/assets/loaders/watermark.d.ts.map +1 -0
- package/dist/assets/loaders/watermark.js +40 -0
- package/dist/assets/loaders/watermark.js.map +1 -0
- package/dist/assets/security/fetch.d.ts +14 -0
- package/dist/assets/security/fetch.d.ts.map +1 -0
- package/dist/assets/security/fetch.js +112 -0
- package/dist/assets/security/fetch.js.map +1 -0
- package/dist/assets/security/ipv4-normalize.d.ts +28 -0
- package/dist/assets/security/ipv4-normalize.d.ts.map +1 -0
- package/dist/assets/security/ipv4-normalize.js +116 -0
- package/dist/assets/security/ipv4-normalize.js.map +1 -0
- package/dist/assets/security/path-allowlist.d.ts +12 -0
- package/dist/assets/security/path-allowlist.d.ts.map +1 -0
- package/dist/assets/security/path-allowlist.js +26 -0
- package/dist/assets/security/path-allowlist.js.map +1 -0
- package/dist/assets/security/url-validation.d.ts +22 -0
- package/dist/assets/security/url-validation.d.ts.map +1 -0
- package/dist/assets/security/url-validation.js +164 -0
- package/dist/assets/security/url-validation.js.map +1 -0
- package/dist/assets/svg/dimensions.d.ts +19 -0
- package/dist/assets/svg/dimensions.d.ts.map +1 -0
- package/dist/assets/svg/dimensions.js +43 -0
- package/dist/assets/svg/dimensions.js.map +1 -0
- package/dist/assets/svg/rasterize.d.ts +6 -0
- package/dist/assets/svg/rasterize.d.ts.map +1 -0
- package/dist/assets/svg/rasterize.js +38 -0
- package/dist/assets/svg/rasterize.js.map +1 -0
- package/dist/assets/svg/resolve-content.d.ts +16 -0
- package/dist/assets/svg/resolve-content.d.ts.map +1 -0
- package/dist/assets/svg/resolve-content.js +38 -0
- package/dist/assets/svg/resolve-content.js.map +1 -0
- package/dist/assets/svg/sanitize.d.ts +22 -0
- package/dist/assets/svg/sanitize.d.ts.map +1 -0
- package/dist/assets/svg/sanitize.js +46 -0
- package/dist/assets/svg/sanitize.js.map +1 -0
- package/dist/assets/util/redact-path.d.ts +14 -0
- package/dist/assets/util/redact-path.d.ts.map +1 -0
- package/dist/assets/util/redact-path.js +16 -0
- package/dist/assets/util/redact-path.js.map +1 -0
- package/dist/assets.d.ts +10 -27
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +10 -549
- package/dist/assets.js.map +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +2 -1
- package/dist/builder.js.map +1 -1
- package/dist/cli.js +11 -1
- package/dist/cli.js.map +1 -1
- package/dist/compat.d.ts +63 -1
- package/dist/compat.d.ts.map +1 -1
- package/dist/compat.js +42 -5
- package/dist/compat.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/fonts.d.ts.map +1 -1
- package/dist/fonts.js +8 -10
- package/dist/fonts.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/layout-state.d.ts +1 -1
- package/dist/layout-state.d.ts.map +1 -1
- package/dist/layout-state.js +5 -0
- package/dist/layout-state.js.map +1 -1
- package/dist/measure-blocks/float-group.d.ts +9 -0
- package/dist/measure-blocks/float-group.d.ts.map +1 -0
- package/dist/measure-blocks/float-group.js +103 -0
- package/dist/measure-blocks/float-group.js.map +1 -0
- package/dist/measure-blocks/helpers.d.ts +44 -0
- package/dist/measure-blocks/helpers.d.ts.map +1 -0
- package/dist/measure-blocks/helpers.js +43 -0
- package/dist/measure-blocks/helpers.js.map +1 -0
- package/dist/measure-blocks/highlight.d.ts +26 -0
- package/dist/measure-blocks/highlight.d.ts.map +1 -0
- package/dist/measure-blocks/highlight.js +169 -0
- package/dist/measure-blocks/highlight.js.map +1 -0
- package/dist/measure-blocks/image.d.ts +9 -0
- package/dist/measure-blocks/image.d.ts.map +1 -0
- package/dist/measure-blocks/image.js +136 -0
- package/dist/measure-blocks/image.js.map +1 -0
- package/dist/measure-blocks/index.d.ts +24 -0
- package/dist/measure-blocks/index.d.ts.map +1 -0
- package/dist/measure-blocks/index.js +179 -0
- package/dist/measure-blocks/index.js.map +1 -0
- package/dist/measure-blocks/list.d.ts +8 -0
- package/dist/measure-blocks/list.d.ts.map +1 -0
- package/dist/measure-blocks/list.js +108 -0
- package/dist/measure-blocks/list.js.map +1 -0
- package/dist/measure-blocks/simple-blocks.d.ts +18 -0
- package/dist/measure-blocks/simple-blocks.d.ts.map +1 -0
- package/dist/measure-blocks/simple-blocks.js +121 -0
- package/dist/measure-blocks/simple-blocks.js.map +1 -0
- package/dist/measure-blocks/table/columns.d.ts +17 -0
- package/dist/measure-blocks/table/columns.d.ts.map +1 -0
- package/dist/measure-blocks/table/columns.js +83 -0
- package/dist/measure-blocks/table/columns.js.map +1 -0
- package/dist/measure-blocks/table/measure.d.ts +8 -0
- package/dist/measure-blocks/table/measure.d.ts.map +1 -0
- package/dist/measure-blocks/table/measure.js +231 -0
- package/dist/measure-blocks/table/measure.js.map +1 -0
- package/dist/measure-blocks/table/spans.d.ts +25 -0
- package/dist/measure-blocks/table/spans.d.ts.map +1 -0
- package/dist/measure-blocks/table/spans.js +55 -0
- package/dist/measure-blocks/table/spans.js.map +1 -0
- package/dist/measure-blocks/text-blocks.d.ts +17 -0
- package/dist/measure-blocks/text-blocks.d.ts.map +1 -0
- package/dist/measure-blocks/text-blocks.js +242 -0
- package/dist/measure-blocks/text-blocks.js.map +1 -0
- package/dist/measure-text.d.ts +21 -3
- package/dist/measure-text.d.ts.map +1 -1
- package/dist/measure-text.js +87 -36
- package/dist/measure-text.js.map +1 -1
- package/dist/measure.d.ts +1 -1
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +8 -6
- package/dist/measure.js.map +1 -1
- package/dist/node-polyfill.d.ts.map +1 -1
- package/dist/node-polyfill.js +9 -0
- package/dist/node-polyfill.js.map +1 -1
- package/dist/pipeline-footnotes.d.ts +1 -1
- package/dist/pipeline-footnotes.d.ts.map +1 -1
- package/dist/pipeline-toc.d.ts +1 -1
- package/dist/pipeline-toc.d.ts.map +1 -1
- package/dist/pipeline.d.ts +3 -3
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +4 -5
- package/dist/pipeline.js.map +1 -1
- package/dist/plugin-types.d.ts +1 -1
- package/dist/plugin-types.d.ts.map +1 -1
- package/dist/post-process.d.ts +2 -2
- package/dist/post-process.d.ts.map +1 -1
- package/dist/post-process.js +32 -9
- package/dist/post-process.js.map +1 -1
- package/dist/render-blocks/blockquote.d.ts +7 -0
- package/dist/render-blocks/blockquote.d.ts.map +1 -0
- package/dist/render-blocks/blockquote.js +87 -0
- package/dist/render-blocks/blockquote.js.map +1 -0
- package/dist/render-blocks/callout.d.ts +7 -0
- package/dist/render-blocks/callout.d.ts.map +1 -0
- package/dist/render-blocks/callout.js +84 -0
- package/dist/render-blocks/callout.js.map +1 -0
- package/dist/render-blocks/code.d.ts +7 -0
- package/dist/render-blocks/code.d.ts.map +1 -0
- package/dist/render-blocks/code.js +84 -0
- package/dist/render-blocks/code.js.map +1 -0
- package/dist/render-blocks/footnote.d.ts +11 -0
- package/dist/render-blocks/footnote.d.ts.map +1 -0
- package/dist/render-blocks/footnote.js +45 -0
- package/dist/render-blocks/footnote.js.map +1 -0
- package/dist/render-blocks/header-footer.d.ts +11 -0
- package/dist/render-blocks/header-footer.d.ts.map +1 -0
- package/dist/render-blocks/header-footer.js +56 -0
- package/dist/render-blocks/header-footer.js.map +1 -0
- package/dist/render-blocks/hr.d.ts +7 -0
- package/dist/render-blocks/hr.d.ts.map +1 -0
- package/dist/render-blocks/hr.js +24 -0
- package/dist/render-blocks/hr.js.map +1 -0
- package/dist/render-blocks/image.d.ts +9 -0
- package/dist/render-blocks/image.d.ts.map +1 -0
- package/dist/render-blocks/image.js +135 -0
- package/dist/render-blocks/image.js.map +1 -0
- package/dist/render-blocks/index.d.ts +17 -0
- package/dist/render-blocks/index.d.ts.map +1 -0
- package/dist/render-blocks/index.js +17 -0
- package/dist/render-blocks/index.js.map +1 -0
- package/dist/render-blocks/list-item.d.ts +7 -0
- package/dist/render-blocks/list-item.d.ts.map +1 -0
- package/dist/render-blocks/list-item.js +80 -0
- package/dist/render-blocks/list-item.js.map +1 -0
- package/dist/render-blocks/rich.d.ts +7 -0
- package/dist/render-blocks/rich.d.ts.map +1 -0
- package/dist/render-blocks/rich.js +160 -0
- package/dist/render-blocks/rich.js.map +1 -0
- package/dist/render-blocks/table.d.ts +7 -0
- package/dist/render-blocks/table.d.ts.map +1 -0
- package/dist/render-blocks/table.js +139 -0
- package/dist/render-blocks/table.js.map +1 -0
- package/dist/render-blocks/text.d.ts +7 -0
- package/dist/render-blocks/text.d.ts.map +1 -0
- package/dist/render-blocks/text.js +183 -0
- package/dist/render-blocks/text.js.map +1 -0
- package/dist/render-blocks/watermark.d.ts +8 -0
- package/dist/render-blocks/watermark.d.ts.map +1 -0
- package/dist/render-blocks/watermark.js +52 -0
- package/dist/render-blocks/watermark.js.map +1 -0
- package/dist/render-extras.d.ts.map +1 -1
- package/dist/render-extras.js +1 -2
- package/dist/render-extras.js.map +1 -1
- package/dist/render-utils.d.ts.map +1 -1
- package/dist/render-utils.js +10 -6
- package/dist/render-utils.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +9 -3
- package/dist/render.js.map +1 -1
- package/dist/rich-text.d.ts +2 -1
- package/dist/rich-text.d.ts.map +1 -1
- package/dist/rich-text.js +0 -1
- package/dist/rich-text.js.map +1 -1
- package/dist/types-internal.d.ts +19 -3
- package/dist/types-internal.d.ts.map +1 -1
- package/dist/types-public/document.d.ts +261 -0
- package/dist/types-public/document.d.ts.map +1 -0
- package/dist/types-public/document.js +2 -0
- package/dist/types-public/document.js.map +1 -0
- package/dist/types-public/elements-block.d.ts +246 -0
- package/dist/types-public/elements-block.d.ts.map +1 -0
- package/dist/types-public/elements-block.js +8 -0
- package/dist/types-public/elements-block.js.map +1 -0
- package/dist/types-public/elements-media.d.ts +199 -0
- package/dist/types-public/elements-media.d.ts.map +1 -0
- package/dist/types-public/elements-media.js +2 -0
- package/dist/types-public/elements-media.js.map +1 -0
- package/dist/types-public/elements-text.d.ts +327 -0
- package/dist/types-public/elements-text.d.ts.map +1 -0
- package/dist/types-public/elements-text.js +2 -0
- package/dist/types-public/elements-text.js.map +1 -0
- package/dist/types-public/index.d.ts +14 -0
- package/dist/types-public/index.d.ts.map +1 -0
- package/dist/types-public/index.js +2 -0
- package/dist/types-public/index.js.map +1 -0
- package/dist/types-public/render-options.d.ts +38 -0
- package/dist/types-public/render-options.d.ts.map +1 -0
- package/dist/types-public/render-options.js +2 -0
- package/dist/types-public/render-options.js.map +1 -0
- package/dist/types-public/union.d.ts +13 -0
- package/dist/types-public/union.d.ts.map +1 -0
- package/dist/types-public/union.js +2 -0
- package/dist/types-public/union.js.map +1 -0
- package/dist/types-public/validation.d.ts +64 -0
- package/dist/types-public/validation.d.ts.map +1 -0
- package/dist/types-public/validation.js +2 -0
- package/dist/types-public/validation.js.map +1 -0
- package/dist/types-public.d.ts +5 -1081
- package/dist/types-public.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/validate/document.d.ts +28 -0
- package/dist/validate/document.d.ts.map +1 -0
- package/dist/validate/document.js +295 -0
- package/dist/validate/document.js.map +1 -0
- package/dist/validate/elements/forms-floats.d.ts +19 -0
- package/dist/validate/elements/forms-floats.d.ts.map +1 -0
- package/dist/validate/elements/forms-floats.js +96 -0
- package/dist/validate/elements/forms-floats.js.map +1 -0
- package/dist/validate/elements/list.d.ts +10 -0
- package/dist/validate/elements/list.d.ts.map +1 -0
- package/dist/validate/elements/list.js +66 -0
- package/dist/validate/elements/list.js.map +1 -0
- package/dist/validate/elements/media.d.ts +23 -0
- package/dist/validate/elements/media.d.ts.map +1 -0
- package/dist/validate/elements/media.js +179 -0
- package/dist/validate/elements/media.js.map +1 -0
- package/dist/validate/elements/structural-simple.d.ts +21 -0
- package/dist/validate/elements/structural-simple.d.ts.map +1 -0
- package/dist/validate/elements/structural-simple.js +63 -0
- package/dist/validate/elements/structural-simple.js.map +1 -0
- package/dist/validate/elements/structural.d.ts +12 -0
- package/dist/validate/elements/structural.d.ts.map +1 -0
- package/dist/validate/elements/structural.js +12 -0
- package/dist/validate/elements/structural.js.map +1 -0
- package/dist/validate/elements/table.d.ts +10 -0
- package/dist/validate/elements/table.d.ts.map +1 -0
- package/dist/validate/elements/table.js +165 -0
- package/dist/validate/elements/table.js.map +1 -0
- package/dist/validate/elements/text.d.ts +26 -0
- package/dist/validate/elements/text.d.ts.map +1 -0
- package/dist/validate/elements/text.js +331 -0
- package/dist/validate/elements/text.js.map +1 -0
- package/dist/validate/errors.d.ts +9 -0
- package/dist/validate/errors.d.ts.map +1 -0
- package/dist/validate/errors.js +43 -0
- package/dist/validate/errors.js.map +1 -0
- package/dist/validate/fonts.d.ts +11 -0
- package/dist/validate/fonts.d.ts.map +1 -0
- package/dist/validate/fonts.js +118 -0
- package/dist/validate/fonts.js.map +1 -0
- package/dist/validate/helpers.d.ts +76 -0
- package/dist/validate/helpers.d.ts.map +1 -0
- package/dist/validate/helpers.js +169 -0
- package/dist/validate/helpers.js.map +1 -0
- package/dist/validate/index.d.ts +37 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +279 -0
- package/dist/validate/index.js.map +1 -0
- package/dist/validate.d.ts +6 -18
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +6 -1585
- package/dist/validate.js.map +1 -1
- package/dist/vendor/pretext/VERSION.d.ts +3 -0
- package/dist/vendor/pretext/VERSION.d.ts.map +1 -0
- package/dist/vendor/pretext/VERSION.js +12 -0
- package/dist/vendor/pretext/VERSION.js.map +1 -0
- package/dist/version-check.d.ts +47 -0
- package/dist/version-check.d.ts.map +1 -0
- package/dist/version-check.js +75 -0
- package/dist/version-check.js.map +1 -0
- package/package.json +26 -7
- package/dist/measure-blocks.d.ts +0 -26
- package/dist/measure-blocks.d.ts.map +0 -1
- package/dist/measure-blocks.js +0 -1317
- package/dist/measure-blocks.js.map +0 -1
- package/dist/render-blocks.d.ts +0 -28
- package/dist/render-blocks.d.ts.map +0 -1
- package/dist/render-blocks.js +0 -1059
- package/dist/render-blocks.js.map +0 -1
package/dist/render-blocks.js
DELETED
|
@@ -1,1059 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* render-blocks.ts — Element-level rendering functions
|
|
3
|
-
* All the specific renderer functions for different content types.
|
|
4
|
-
*/
|
|
5
|
-
import { rgb, degrees } from '@cantoo/pdf-lib';
|
|
6
|
-
import { PretextPdfError } from './errors.js';
|
|
7
|
-
import { drawJustifiedLine, addLinkAnnotation, addStickyNoteAnnotation, drawTextDecoration, toPdfY, resolveX, resolveTokens, hexToRgb, drawTabularText, LINE_HEIGHT_BODY, LINE_HEIGHT_COMPACT, } from './render-utils.js';
|
|
8
|
-
import { buildFontKey } from './measure.js';
|
|
9
|
-
// ─── Text block rendering (paragraph + heading) ───────────────────────────────
|
|
10
|
-
export function renderTextBlock(pdfPage, pagedBlock, geo, fontMap, pdfDoc) {
|
|
11
|
-
const { measuredBlock, startLine, endLine, yFromTop } = pagedBlock;
|
|
12
|
-
const { element } = measuredBlock;
|
|
13
|
-
const lines = measuredBlock.lines.slice(startLine, endLine);
|
|
14
|
-
if (lines.length === 0)
|
|
15
|
-
return;
|
|
16
|
-
const pdfFont = fontMap.get(measuredBlock.fontKey);
|
|
17
|
-
if (!pdfFont) {
|
|
18
|
-
throw new PretextPdfError('FONT_NOT_LOADED', `Font "${measuredBlock.fontKey}" not found in fontMap. This is a bug — font validation should have caught this.`);
|
|
19
|
-
}
|
|
20
|
-
const colorHex = (element.type === 'paragraph' || element.type === 'heading')
|
|
21
|
-
? (element.color ?? '#000000')
|
|
22
|
-
: '#000000';
|
|
23
|
-
const [r, g, b] = hexToRgb(colorHex);
|
|
24
|
-
const alignRaw = (element.type === 'paragraph' || element.type === 'heading')
|
|
25
|
-
? (element.align ?? (measuredBlock.isRTL ? 'right' : 'left'))
|
|
26
|
-
: 'left';
|
|
27
|
-
// For resolveX, treat 'justify' as 'left' (justify is handled by drawJustifiedLine)
|
|
28
|
-
const align = alignRaw === 'justify' ? 'left' : alignRaw;
|
|
29
|
-
const fontHeight = pdfFont.heightAtSize(measuredBlock.fontSize);
|
|
30
|
-
// Narrowed reference for paragraph/heading-only fields (smallCaps, tabularNumbers, letterSpacing, annotation)
|
|
31
|
-
const textElement = (element.type === 'paragraph' || element.type === 'heading') ? element : null;
|
|
32
|
-
// Draw background color for paragraph and heading (if set)
|
|
33
|
-
if ((element.type === 'paragraph' || element.type === 'heading') && element.bgColor) {
|
|
34
|
-
const columnData = measuredBlock.columnData;
|
|
35
|
-
const chunkHeight = columnData
|
|
36
|
-
? columnData.linesPerColumn * measuredBlock.lineHeight
|
|
37
|
-
: lines.length * measuredBlock.lineHeight;
|
|
38
|
-
const boxAbsY = yFromTop + geo.margins.top + geo.headerHeight;
|
|
39
|
-
const boxPdfY = toPdfY(boxAbsY, chunkHeight, geo.pageHeight);
|
|
40
|
-
const [bgR, bgG, bgB] = hexToRgb(element.bgColor);
|
|
41
|
-
pdfPage.drawRectangle({
|
|
42
|
-
x: geo.margins.left,
|
|
43
|
-
y: boxPdfY,
|
|
44
|
-
width: geo.contentWidth,
|
|
45
|
-
height: chunkHeight,
|
|
46
|
-
color: rgb(bgR, bgG, bgB),
|
|
47
|
-
borderWidth: 0,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
// Multi-column layout — mirrors single-column features (smallCaps, letterSpacing, justify, decoration)
|
|
51
|
-
const columnData = measuredBlock.columnData;
|
|
52
|
-
if (columnData) {
|
|
53
|
-
const { columnGap, columnWidth, linesPerColumn } = columnData;
|
|
54
|
-
const hasSmallCaps = textElement?.smallCaps === true;
|
|
55
|
-
const mcFontSize = hasSmallCaps ? measuredBlock.fontSize * 0.8 : measuredBlock.fontSize;
|
|
56
|
-
const hasTabular = textElement?.tabularNumbers === true;
|
|
57
|
-
const letterSpacing = (textElement?.letterSpacing ?? 0) > 0 ? textElement.letterSpacing : 0;
|
|
58
|
-
for (let i = 0; i < lines.length; i++) {
|
|
59
|
-
const line = lines[i];
|
|
60
|
-
if (line.text === '')
|
|
61
|
-
continue;
|
|
62
|
-
const colIdx = Math.floor(i / linesPerColumn);
|
|
63
|
-
const lineInCol = i % linesPerColumn;
|
|
64
|
-
const lineYFromTop = yFromTop + (lineInCol * measuredBlock.lineHeight);
|
|
65
|
-
const absoluteYFromTop = lineYFromTop + geo.margins.top + geo.headerHeight;
|
|
66
|
-
const pdfY = toPdfY(absoluteYFromTop, fontHeight, geo.pageHeight);
|
|
67
|
-
const colX = geo.margins.left + colIdx * (columnWidth + columnGap);
|
|
68
|
-
let trimmedText = line.text.trimEnd();
|
|
69
|
-
if (hasSmallCaps)
|
|
70
|
-
trimmedText = trimmedText.toUpperCase();
|
|
71
|
-
// Last line of each column should not be force-justified (left-align instead)
|
|
72
|
-
const isLastLineInCol = lineInCol === linesPerColumn - 1 || i === lines.length - 1;
|
|
73
|
-
let drawX;
|
|
74
|
-
if (alignRaw === 'justify' && letterSpacing === 0 && !hasTabular) {
|
|
75
|
-
drawJustifiedLine(pdfPage, trimmedText, isLastLineInCol, colX, pdfY, columnWidth, mcFontSize, pdfFont, rgb(r, g, b));
|
|
76
|
-
drawX = colX;
|
|
77
|
-
}
|
|
78
|
-
else if (letterSpacing > 0) {
|
|
79
|
-
const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize) + letterSpacing * (trimmedText.length - 1);
|
|
80
|
-
drawX = resolveX(align, colX, columnWidth, alignWidth);
|
|
81
|
-
let cx = drawX;
|
|
82
|
-
for (const ch of trimmedText) {
|
|
83
|
-
pdfPage.drawText(ch, { x: cx, y: pdfY, size: mcFontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
84
|
-
cx += pdfFont.widthOfTextAtSize(ch, mcFontSize) + letterSpacing;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
else if (hasTabular) {
|
|
88
|
-
const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize);
|
|
89
|
-
drawX = resolveX(align, colX, columnWidth, alignWidth);
|
|
90
|
-
drawTabularText(pdfPage, trimmedText, drawX, pdfY, mcFontSize, pdfFont, rgb(r, g, b));
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize);
|
|
94
|
-
drawX = resolveX(align, colX, columnWidth, alignWidth);
|
|
95
|
-
pdfPage.drawText(trimmedText, { x: drawX, y: pdfY, size: mcFontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
96
|
-
}
|
|
97
|
-
// Text decoration (underline, strikethrough)
|
|
98
|
-
if ((element.type === 'paragraph' || element.type === 'heading') && (element.underline || element.strikethrough)) {
|
|
99
|
-
const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize) + (letterSpacing > 0 ? letterSpacing * (trimmedText.length - 1) : 0);
|
|
100
|
-
drawTextDecoration(pdfPage, drawX, lineWidth, pdfY, mcFontSize, pdfFont, [r, g, b], { underline: element.underline ?? false, strikethrough: element.strikethrough ?? false });
|
|
101
|
-
}
|
|
102
|
-
// Hyperlink annotation
|
|
103
|
-
if ((element.type === 'paragraph' || element.type === 'heading') && element.url) {
|
|
104
|
-
const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, mcFontSize) + (letterSpacing > 0 ? letterSpacing * (trimmedText.length - 1) : 0);
|
|
105
|
-
addLinkAnnotation(pdfDoc, pdfPage, drawX, pdfY, lineWidth, mcFontSize, element.url);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// Sticky note annotation (once per block, not per line)
|
|
109
|
-
if (textElement?.annotation) {
|
|
110
|
-
const ann = textElement.annotation;
|
|
111
|
-
const absY = yFromTop + geo.margins.top + geo.headerHeight;
|
|
112
|
-
const annotPdfY = geo.pageHeight - absY;
|
|
113
|
-
addStickyNoteAnnotation(pdfDoc, pdfPage, geo.margins.left, annotPdfY, ann.contents, ann.author, ann.color, ann.open);
|
|
114
|
-
}
|
|
115
|
-
return; // skip standard single-column path
|
|
116
|
-
}
|
|
117
|
-
// Single-column layout (standard path)
|
|
118
|
-
for (let i = 0; i < lines.length; i++) {
|
|
119
|
-
const line = lines[i];
|
|
120
|
-
if (line.text === '')
|
|
121
|
-
continue; // empty lines from \n\n — occupy space, draw nothing
|
|
122
|
-
const lineYFromTop = yFromTop + (i * measuredBlock.lineHeight);
|
|
123
|
-
const absoluteYFromTop = lineYFromTop + geo.margins.top + geo.headerHeight;
|
|
124
|
-
const pdfY = toPdfY(absoluteYFromTop, fontHeight, geo.pageHeight);
|
|
125
|
-
let trimmedText = line.text.trimEnd();
|
|
126
|
-
const isLastLine = i === lines.length - 1;
|
|
127
|
-
// smallCaps — uppercase text at 80% font size
|
|
128
|
-
const hasSmallCaps = textElement?.smallCaps === true;
|
|
129
|
-
const effectiveFontSize = hasSmallCaps ? measuredBlock.fontSize * 0.8 : measuredBlock.fontSize;
|
|
130
|
-
if (hasSmallCaps)
|
|
131
|
-
trimmedText = trimmedText.toUpperCase();
|
|
132
|
-
const hasTabular = textElement?.tabularNumbers === true;
|
|
133
|
-
// letterSpacing — draw char by char
|
|
134
|
-
const letterSpacing = (textElement?.letterSpacing ?? 0) > 0
|
|
135
|
-
? textElement.letterSpacing
|
|
136
|
-
: 0;
|
|
137
|
-
let drawX;
|
|
138
|
-
if (alignRaw === 'justify' && letterSpacing === 0 && !hasTabular) {
|
|
139
|
-
drawJustifiedLine(pdfPage, trimmedText, isLastLine, geo.margins.left, pdfY, geo.contentWidth, effectiveFontSize, pdfFont, rgb(r, g, b));
|
|
140
|
-
drawX = geo.margins.left; // used for decoration below
|
|
141
|
-
}
|
|
142
|
-
else if (letterSpacing > 0) {
|
|
143
|
-
const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, effectiveFontSize) + letterSpacing * (trimmedText.length - 1);
|
|
144
|
-
drawX = resolveX(align, geo.margins.left, geo.contentWidth, alignWidth);
|
|
145
|
-
let cx = drawX;
|
|
146
|
-
for (const ch of trimmedText) {
|
|
147
|
-
pdfPage.drawText(ch, { x: cx, y: pdfY, size: effectiveFontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
148
|
-
cx += pdfFont.widthOfTextAtSize(ch, effectiveFontSize) + letterSpacing;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
else if (hasTabular) {
|
|
152
|
-
const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, effectiveFontSize);
|
|
153
|
-
drawX = resolveX(align, geo.margins.left, geo.contentWidth, alignWidth);
|
|
154
|
-
drawTabularText(pdfPage, trimmedText, drawX, pdfY, effectiveFontSize, pdfFont, rgb(r, g, b));
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
const alignWidth = pdfFont.widthOfTextAtSize(trimmedText, effectiveFontSize);
|
|
158
|
-
drawX = resolveX(align, geo.margins.left, geo.contentWidth, alignWidth);
|
|
159
|
-
pdfPage.drawText(trimmedText, {
|
|
160
|
-
x: drawX,
|
|
161
|
-
y: pdfY,
|
|
162
|
-
size: effectiveFontSize,
|
|
163
|
-
font: pdfFont,
|
|
164
|
-
color: rgb(r, g, b),
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
if ((element.type === 'paragraph' || element.type === 'heading') && (element.underline || element.strikethrough)) {
|
|
168
|
-
const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, effectiveFontSize) + (letterSpacing > 0 ? letterSpacing * (trimmedText.length - 1) : 0);
|
|
169
|
-
drawTextDecoration(pdfPage, drawX, lineWidth, pdfY, effectiveFontSize, pdfFont, [r, g, b], { underline: element.underline ?? false, strikethrough: element.strikethrough ?? false });
|
|
170
|
-
}
|
|
171
|
-
// Clickable link annotation on paragraph/heading
|
|
172
|
-
if ((element.type === 'paragraph' || element.type === 'heading') && element.url) {
|
|
173
|
-
const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, effectiveFontSize) + (letterSpacing > 0 ? letterSpacing * (trimmedText.length - 1) : 0);
|
|
174
|
-
addLinkAnnotation(pdfDoc, pdfPage, drawX, pdfY, lineWidth, effectiveFontSize, element.url);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
// Annotation on paragraph/heading — attach sticky note at top of block
|
|
178
|
-
if (textElement?.annotation) {
|
|
179
|
-
const ann = textElement.annotation;
|
|
180
|
-
const absY = yFromTop + geo.margins.top + geo.headerHeight;
|
|
181
|
-
const annotPdfY = geo.pageHeight - absY;
|
|
182
|
-
addStickyNoteAnnotation(pdfDoc, pdfPage, geo.margins.left, annotPdfY, ann.contents, ann.author, ann.color, ann.open);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
// ─── List item rendering ──────────────────────────────────────────────────────
|
|
186
|
-
export function renderListItem(pdfPage, pagedBlock, geo, fontMap) {
|
|
187
|
-
const { measuredBlock, startLine, endLine, yFromTop } = pagedBlock;
|
|
188
|
-
const listItemData = measuredBlock.listItemData;
|
|
189
|
-
const lines = measuredBlock.lines.slice(startLine, endLine);
|
|
190
|
-
if (lines.length === 0)
|
|
191
|
-
return;
|
|
192
|
-
const pdfFont = fontMap.get(measuredBlock.fontKey);
|
|
193
|
-
if (!pdfFont) {
|
|
194
|
-
throw new PretextPdfError('FONT_NOT_LOADED', `Font "${measuredBlock.fontKey}" not found in fontMap. This is a bug — font validation should have caught this.`);
|
|
195
|
-
}
|
|
196
|
-
const fontHeight = pdfFont.heightAtSize(measuredBlock.fontSize);
|
|
197
|
-
const [cr, cg, cb] = hexToRgb(listItemData.color);
|
|
198
|
-
// RTL support: mirror list layout if detected
|
|
199
|
-
const isRTL = measuredBlock.isRTL ?? false;
|
|
200
|
-
let textStartX;
|
|
201
|
-
let textAreaWidth;
|
|
202
|
-
if (isRTL) {
|
|
203
|
-
// RTL: marker on the right, text area on the left
|
|
204
|
-
textAreaWidth = geo.contentWidth - listItemData.indent - listItemData.markerWidth;
|
|
205
|
-
textStartX = geo.margins.left + listItemData.indent;
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
// LTR: marker on the left, text area on the right
|
|
209
|
-
textStartX = geo.margins.left + listItemData.indent + listItemData.markerWidth;
|
|
210
|
-
textAreaWidth = geo.contentWidth - listItemData.indent - listItemData.markerWidth;
|
|
211
|
-
}
|
|
212
|
-
// Draw marker on the first line of this item (only if startLine === 0)
|
|
213
|
-
// If startLine > 0, the item continued from a previous page — no marker
|
|
214
|
-
if (startLine === 0) {
|
|
215
|
-
const markerText = listItemData.marker;
|
|
216
|
-
const markerMeasuredWidth = pdfFont.widthOfTextAtSize(markerText, measuredBlock.fontSize);
|
|
217
|
-
let markerX;
|
|
218
|
-
if (isRTL) {
|
|
219
|
-
// RTL: marker on the right, right-aligned within marker column
|
|
220
|
-
markerX = geo.margins.left + geo.contentWidth - listItemData.indent - markerMeasuredWidth;
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
// LTR: marker on the left, right-aligned within marker column
|
|
224
|
-
markerX = geo.margins.left + listItemData.indent + listItemData.markerWidth - markerMeasuredWidth;
|
|
225
|
-
}
|
|
226
|
-
const firstLineAbsY = yFromTop + geo.margins.top + geo.headerHeight;
|
|
227
|
-
const markerPdfY = toPdfY(firstLineAbsY, fontHeight, geo.pageHeight);
|
|
228
|
-
pdfPage.drawText(markerText, {
|
|
229
|
-
x: markerX,
|
|
230
|
-
y: markerPdfY,
|
|
231
|
-
size: measuredBlock.fontSize,
|
|
232
|
-
font: pdfFont,
|
|
233
|
-
color: rgb(cr, cg, cb),
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
// Draw all text lines, indented to align with body text column
|
|
237
|
-
// RTL lists are right-aligned, LTR lists are left-aligned
|
|
238
|
-
const textAlign = isRTL ? 'right' : 'left';
|
|
239
|
-
for (let i = 0; i < lines.length; i++) {
|
|
240
|
-
const line = lines[i];
|
|
241
|
-
if (line.text === '')
|
|
242
|
-
continue;
|
|
243
|
-
const lineYFromTop = yFromTop + (i * measuredBlock.lineHeight);
|
|
244
|
-
const absoluteYFromTop = lineYFromTop + geo.margins.top + geo.headerHeight;
|
|
245
|
-
const pdfY = toPdfY(absoluteYFromTop, fontHeight, geo.pageHeight);
|
|
246
|
-
const trimmedText = line.text.trimEnd();
|
|
247
|
-
const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, measuredBlock.fontSize);
|
|
248
|
-
const x = resolveX(textAlign, textStartX, textAreaWidth, lineWidth);
|
|
249
|
-
pdfPage.drawText(trimmedText, {
|
|
250
|
-
x,
|
|
251
|
-
y: pdfY,
|
|
252
|
-
size: measuredBlock.fontSize,
|
|
253
|
-
font: pdfFont,
|
|
254
|
-
color: rgb(cr, cg, cb),
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
// ─── Table rendering ──────────────────────────────────────────────────────────
|
|
259
|
-
export function renderTable(pdfPage, pagedBlock, geo, fontMap) {
|
|
260
|
-
const { measuredBlock, yFromTop } = pagedBlock;
|
|
261
|
-
const tableData = measuredBlock.tableData;
|
|
262
|
-
const startRow = pagedBlock.startRow ?? 0;
|
|
263
|
-
const endRow = pagedBlock.endRow ?? tableData.rows.length - tableData.headerRowCount;
|
|
264
|
-
const { columnWidths, cellPaddingH, cellPaddingV, borderWidth, borderColor, headerBgColor } = tableData;
|
|
265
|
-
// Collect the rows to render for this chunk: headers (always) + body slice
|
|
266
|
-
const headerRows = tableData.rows.slice(0, tableData.headerRowCount);
|
|
267
|
-
const bodyRows = tableData.rows.slice(tableData.headerRowCount);
|
|
268
|
-
const chunkBodyRows = bodyRows.slice(startRow, endRow);
|
|
269
|
-
const chunkRows = [...headerRows, ...chunkBodyRows];
|
|
270
|
-
const chunkStartAbsY = yFromTop + geo.margins.top + geo.headerHeight;
|
|
271
|
-
const totalTableWidth = columnWidths.reduce((s, w) => s + w, 0);
|
|
272
|
-
const totalChunkHeight = chunkRows.reduce((s, r) => s + r.height, 0);
|
|
273
|
-
// ── Pass 1: Cell backgrounds ──────────────────────────────────────────────
|
|
274
|
-
let rowAbsY = chunkStartAbsY;
|
|
275
|
-
for (const row of chunkRows) {
|
|
276
|
-
let cellX = geo.margins.left;
|
|
277
|
-
for (const cell of row.cells) {
|
|
278
|
-
if (!cell.isSpanPlaceholder) {
|
|
279
|
-
const cellRenderHeight = cell.spanHeight ?? row.height;
|
|
280
|
-
const bgColorHex = cell.bgColor ?? (row.isHeader ? headerBgColor : undefined);
|
|
281
|
-
if (bgColorHex) {
|
|
282
|
-
const [r, g, b] = hexToRgb(bgColorHex);
|
|
283
|
-
pdfPage.drawRectangle({ x: cellX, y: toPdfY(rowAbsY, cellRenderHeight, geo.pageHeight), width: cell.mergedWidth, height: cellRenderHeight, color: rgb(r, g, b), borderWidth: 0 });
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
cellX += cell.mergedWidth;
|
|
287
|
-
}
|
|
288
|
-
rowAbsY += row.height;
|
|
289
|
-
}
|
|
290
|
-
// ── Pass 2: Grid (border-collapse model) ─────────────────────────────────
|
|
291
|
-
// Draw outer border + internal lines — single-thickness at every edge.
|
|
292
|
-
if (borderWidth > 0) {
|
|
293
|
-
const [br, bg, bb] = hexToRgb(borderColor);
|
|
294
|
-
const borderRgb = rgb(br, bg, bb);
|
|
295
|
-
const tableTopPdfY = toPdfY(chunkStartAbsY, totalChunkHeight, geo.pageHeight);
|
|
296
|
-
// Outer border rectangle (no fill)
|
|
297
|
-
pdfPage.drawRectangle({
|
|
298
|
-
x: geo.margins.left,
|
|
299
|
-
y: tableTopPdfY,
|
|
300
|
-
width: totalTableWidth,
|
|
301
|
-
height: totalChunkHeight,
|
|
302
|
-
borderColor: borderRgb,
|
|
303
|
-
borderWidth,
|
|
304
|
-
});
|
|
305
|
-
// Internal horizontal lines (row separators, between rows, not at edges)
|
|
306
|
-
// Suppressed after rows that have a spanning cell crossing into the next row
|
|
307
|
-
let lineAbsY = chunkStartAbsY;
|
|
308
|
-
for (let ri = 0; ri < chunkRows.length - 1; ri++) {
|
|
309
|
-
lineAbsY += chunkRows[ri].height;
|
|
310
|
-
if (!chunkRows[ri].hasRowspan) {
|
|
311
|
-
const linePdfY = geo.pageHeight - lineAbsY;
|
|
312
|
-
pdfPage.drawLine({
|
|
313
|
-
start: { x: geo.margins.left, y: linePdfY },
|
|
314
|
-
end: { x: geo.margins.left + totalTableWidth, y: linePdfY },
|
|
315
|
-
thickness: borderWidth,
|
|
316
|
-
color: borderRgb,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
// Internal vertical lines (column separators, between columns, not at edges)
|
|
321
|
-
// Draw per-row segments: a boundary absent from a row's activeBoundaries means a merged cell
|
|
322
|
-
// spans it — a full-chunk line would cut through it. Per-row drawing preserves colspan correctness.
|
|
323
|
-
// Pre-compute X positions once; convert each row's activeBoundaries to a Set for O(1) lookup.
|
|
324
|
-
const colBoundaryXPositions = [];
|
|
325
|
-
let bx = geo.margins.left;
|
|
326
|
-
for (let ci = 0; ci < columnWidths.length - 1; ci++) {
|
|
327
|
-
bx += columnWidths[ci];
|
|
328
|
-
colBoundaryXPositions.push(bx);
|
|
329
|
-
}
|
|
330
|
-
const rowBoundarySets = chunkRows.map(row => new Set(row.activeBoundaries));
|
|
331
|
-
let vertRowAbsY = chunkStartAbsY;
|
|
332
|
-
for (let ri = 0; ri < chunkRows.length; ri++) {
|
|
333
|
-
const row = chunkRows[ri];
|
|
334
|
-
const rowBoundarySet = rowBoundarySets[ri];
|
|
335
|
-
const rowTopPdfY = geo.pageHeight - vertRowAbsY;
|
|
336
|
-
const rowBottomPdfY = geo.pageHeight - (vertRowAbsY + row.height);
|
|
337
|
-
for (let ci = 0; ci < colBoundaryXPositions.length; ci++) {
|
|
338
|
-
if (rowBoundarySet.has(ci)) {
|
|
339
|
-
pdfPage.drawLine({
|
|
340
|
-
start: { x: colBoundaryXPositions[ci], y: rowTopPdfY },
|
|
341
|
-
end: { x: colBoundaryXPositions[ci], y: rowBottomPdfY },
|
|
342
|
-
thickness: borderWidth,
|
|
343
|
-
color: borderRgb,
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
vertRowAbsY += row.height;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
// ── Pass 3: Cell text ─────────────────────────────────────────────────────
|
|
351
|
-
rowAbsY = chunkStartAbsY;
|
|
352
|
-
for (const row of chunkRows) {
|
|
353
|
-
let cellX = geo.margins.left;
|
|
354
|
-
for (const cell of row.cells) {
|
|
355
|
-
if (!cell.isSpanPlaceholder && cell.lines.length > 0) {
|
|
356
|
-
const pdfFont = fontMap.get(cell.fontKey);
|
|
357
|
-
if (!pdfFont) {
|
|
358
|
-
throw new PretextPdfError('FONT_NOT_LOADED', `Table cell font "${cell.fontKey}" not found in fontMap. This is a bug — font validation should have caught this.`);
|
|
359
|
-
}
|
|
360
|
-
const fontHeight = pdfFont.heightAtSize(cell.fontSize);
|
|
361
|
-
const [r, g, b] = hexToRgb(cell.color);
|
|
362
|
-
const textAreaX = cellX + cellPaddingH;
|
|
363
|
-
const textAreaWidth = cell.mergedWidth - 2 * cellPaddingH;
|
|
364
|
-
// For rowspan cells, vertically center within the full spanHeight
|
|
365
|
-
const cellRenderHeight = cell.spanHeight ?? row.height;
|
|
366
|
-
const totalTextHeight = cell.lines.length * cell.lineHeight;
|
|
367
|
-
const verticalOffset = Math.max(0, (cellRenderHeight - totalTextHeight - 2 * cellPaddingV) / 2);
|
|
368
|
-
for (let li = 0; li < cell.lines.length; li++) {
|
|
369
|
-
const line = cell.lines[li];
|
|
370
|
-
if (line.text === '')
|
|
371
|
-
continue;
|
|
372
|
-
const lineYFromPageTop = rowAbsY + cellPaddingV + verticalOffset + li * cell.lineHeight;
|
|
373
|
-
const pdfY = toPdfY(lineYFromPageTop, fontHeight, geo.pageHeight);
|
|
374
|
-
const trimmedText = line.text.trimEnd();
|
|
375
|
-
const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, cell.fontSize);
|
|
376
|
-
const x = resolveX(cell.align, textAreaX, textAreaWidth, lineWidth);
|
|
377
|
-
if (cell.tabularNumbers) {
|
|
378
|
-
drawTabularText(pdfPage, trimmedText, x, pdfY, cell.fontSize, pdfFont, rgb(r, g, b));
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
pdfPage.drawText(trimmedText, { x, y: pdfY, size: cell.fontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
cellX += cell.mergedWidth;
|
|
386
|
-
}
|
|
387
|
-
rowAbsY += row.height;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
// ─── Image rendering ──────────────────────────────────────────────────────────
|
|
391
|
-
export function renderImage(pdfPage, pagedBlock, geo, imageMap) {
|
|
392
|
-
const { measuredBlock, yFromTop } = pagedBlock;
|
|
393
|
-
const imageData = measuredBlock.imageData;
|
|
394
|
-
const pdfImage = imageMap.get(imageData.imageKey);
|
|
395
|
-
if (!pdfImage) {
|
|
396
|
-
throw new PretextPdfError('IMAGE_LOAD_FAILED', `Image "${imageData.imageKey}" not found in imageMap. This is a bug — image loading should have caught this.`);
|
|
397
|
-
}
|
|
398
|
-
const absoluteYFromTop = yFromTop + geo.margins.top + geo.headerHeight;
|
|
399
|
-
// drawImage places the BOTTOM-LEFT corner at (x, y) — use toPdfY with renderHeight
|
|
400
|
-
const pdfY = toPdfY(absoluteYFromTop, imageData.renderHeight, geo.pageHeight);
|
|
401
|
-
const x = resolveX(imageData.align, geo.margins.left, geo.contentWidth, imageData.renderWidth);
|
|
402
|
-
pdfPage.drawImage(pdfImage, {
|
|
403
|
-
x,
|
|
404
|
-
y: pdfY,
|
|
405
|
-
width: imageData.renderWidth,
|
|
406
|
-
height: imageData.renderHeight,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
// ─── Float image block rendering ─────────────────────────────────────────────
|
|
410
|
-
export function renderFloatBlock(pdfPage, pagedBlock, geo, fontMap, imageMap, pdfDoc) {
|
|
411
|
-
const { measuredBlock, yFromTop } = pagedBlock;
|
|
412
|
-
const fd = measuredBlock.floatData;
|
|
413
|
-
const baseAbsY = yFromTop + geo.margins.top + geo.headerHeight;
|
|
414
|
-
// Draw image
|
|
415
|
-
const pdfImage = imageMap.get(fd.imageKey);
|
|
416
|
-
if (!pdfImage)
|
|
417
|
-
throw new PretextPdfError('IMAGE_LOAD_FAILED', `Float image key "${fd.imageKey}" not found in imageMap. This is a bug — image loading should have caught this.`);
|
|
418
|
-
const imgX = geo.margins.left + fd.imageColX;
|
|
419
|
-
const imgPdfY = toPdfY(baseAbsY, fd.imageRenderHeight, geo.pageHeight);
|
|
420
|
-
pdfPage.drawImage(pdfImage, {
|
|
421
|
-
x: imgX,
|
|
422
|
-
y: imgPdfY,
|
|
423
|
-
width: fd.imageRenderWidth,
|
|
424
|
-
height: fd.imageRenderHeight,
|
|
425
|
-
});
|
|
426
|
-
// Draw text lines (rich or plain)
|
|
427
|
-
const textBaseX = geo.margins.left + fd.textColX;
|
|
428
|
-
if (fd.richFloatLines && fd.richFloatLines.length > 0) {
|
|
429
|
-
let cumY = 0;
|
|
430
|
-
for (const richLine of fd.richFloatLines) {
|
|
431
|
-
const lineAbsY = baseAbsY + cumY;
|
|
432
|
-
for (const fragment of richLine.fragments) {
|
|
433
|
-
if (!fragment.text || fragment.text.trim() === '')
|
|
434
|
-
continue;
|
|
435
|
-
const pdfFont = fontMap.get(fragment.fontKey);
|
|
436
|
-
if (!pdfFont)
|
|
437
|
-
throw new PretextPdfError('FONT_NOT_LOADED', `Float rich text font "${fragment.fontKey}" not found in fontMap.`);
|
|
438
|
-
const fontHeight = pdfFont.heightAtSize(fragment.fontSize);
|
|
439
|
-
const [r, g, b] = hexToRgb(fragment.color);
|
|
440
|
-
const drawX = textBaseX + fragment.x;
|
|
441
|
-
const pdfY = toPdfY(lineAbsY, fontHeight, geo.pageHeight) + (fragment.yOffset ?? 0);
|
|
442
|
-
const drawText = fragment.text.trimEnd();
|
|
443
|
-
if (fragment.letterSpacing && fragment.letterSpacing > 0) {
|
|
444
|
-
let cx = drawX;
|
|
445
|
-
for (const ch of drawText) {
|
|
446
|
-
pdfPage.drawText(ch, { x: cx, y: pdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
447
|
-
cx += pdfFont.widthOfTextAtSize(ch, fragment.fontSize) + fragment.letterSpacing;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
else {
|
|
451
|
-
pdfPage.drawText(drawText, { x: drawX, y: pdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
452
|
-
}
|
|
453
|
-
const fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
|
|
454
|
-
drawTextDecoration(pdfPage, drawX, fragWidth, pdfY, fragment.fontSize, pdfFont, [r, g, b], { underline: fragment.underline ?? false, strikethrough: fragment.strikethrough ?? false });
|
|
455
|
-
if (fragment.url)
|
|
456
|
-
addLinkAnnotation(pdfDoc, pdfPage, drawX, pdfY, fragWidth, fragment.fontSize, fragment.url);
|
|
457
|
-
}
|
|
458
|
-
cumY += richLine.lineHeight;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
const pdfFont = fontMap.get(fd.textFontKey);
|
|
463
|
-
if (!pdfFont)
|
|
464
|
-
throw new PretextPdfError('FONT_NOT_LOADED', `Float text font key "${fd.textFontKey}" not found in fontMap. This is a bug — font loading should have caught this.`);
|
|
465
|
-
const fontHeight = pdfFont.heightAtSize(fd.textFontSize);
|
|
466
|
-
const [r, g, b] = hexToRgb(fd.textColor);
|
|
467
|
-
for (let i = 0; i < fd.textLines.length; i++) {
|
|
468
|
-
const line = fd.textLines[i];
|
|
469
|
-
if (line.text === '')
|
|
470
|
-
continue;
|
|
471
|
-
const lineAbsY = baseAbsY + (i * fd.textLineHeight);
|
|
472
|
-
const pdfY = toPdfY(lineAbsY, fontHeight, geo.pageHeight);
|
|
473
|
-
pdfPage.drawText(line.text.trimEnd(), { x: textBaseX, y: pdfY, size: fd.textFontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
// ─── Float group block rendering ──────────────────────────────────────────────
|
|
478
|
-
export function renderFloatGroup(pdfPage, pagedBlock, geo, fontMap, imageMap, pdfDoc) {
|
|
479
|
-
const { measuredBlock, yFromTop } = pagedBlock;
|
|
480
|
-
const fd = measuredBlock.floatGroupData;
|
|
481
|
-
const baseAbsY = yFromTop + geo.margins.top + geo.headerHeight;
|
|
482
|
-
// Draw image
|
|
483
|
-
const pdfImage = imageMap.get(fd.imageKey);
|
|
484
|
-
if (!pdfImage)
|
|
485
|
-
throw new PretextPdfError('IMAGE_LOAD_FAILED', `Float group image key "${fd.imageKey}" not found in imageMap. This is a bug — image loading should have caught this.`);
|
|
486
|
-
const imgX = geo.margins.left + fd.imageColX;
|
|
487
|
-
const imgPdfY = toPdfY(baseAbsY, fd.imageRenderHeight, geo.pageHeight);
|
|
488
|
-
pdfPage.drawImage(pdfImage, {
|
|
489
|
-
x: imgX,
|
|
490
|
-
y: imgPdfY,
|
|
491
|
-
width: fd.imageRenderWidth,
|
|
492
|
-
height: fd.imageRenderHeight,
|
|
493
|
-
});
|
|
494
|
-
// Draw text items
|
|
495
|
-
const textBaseX = geo.margins.left + fd.textColX;
|
|
496
|
-
for (const textItem of fd.textItems) {
|
|
497
|
-
const pdfFont = fontMap.get(textItem.fontKey);
|
|
498
|
-
if (!pdfFont)
|
|
499
|
-
throw new PretextPdfError('FONT_NOT_LOADED', `Float group font key "${textItem.fontKey}" not found in fontMap. This is a bug — font loading should have caught this.`);
|
|
500
|
-
const fontHeight = pdfFont.heightAtSize(textItem.fontSize);
|
|
501
|
-
// Draw plain lines (plain-text fallback for rich-paragraphs)
|
|
502
|
-
for (let i = 0; i < textItem.lines.length; i++) {
|
|
503
|
-
const line = textItem.lines[i];
|
|
504
|
-
if (line.text === '')
|
|
505
|
-
continue;
|
|
506
|
-
const lineAbsY = baseAbsY + textItem.yOffsetFromTop + (i * textItem.lineHeight);
|
|
507
|
-
const pdfY = toPdfY(lineAbsY, fontHeight, geo.pageHeight);
|
|
508
|
-
pdfPage.drawText(line.text.trimEnd(), {
|
|
509
|
-
x: textBaseX,
|
|
510
|
-
y: pdfY,
|
|
511
|
-
size: textItem.fontSize,
|
|
512
|
-
font: pdfFont,
|
|
513
|
-
color: rgb(0, 0, 0),
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
// ─── Horizontal rule rendering ────────────────────────────────────────────────
|
|
519
|
-
export function renderHR(pdfPage, pagedBlock, geo) {
|
|
520
|
-
const { measuredBlock, yFromTop } = pagedBlock;
|
|
521
|
-
const element = measuredBlock.element;
|
|
522
|
-
const spaceAbove = element.spaceAbove ?? 12;
|
|
523
|
-
const thickness = element.thickness ?? 0.5;
|
|
524
|
-
const colorHex = element.color ?? '#cccccc';
|
|
525
|
-
// Line sits at the middle of the HR element (after spaceAbove, before spaceBelow)
|
|
526
|
-
const lineYFromTop = yFromTop + spaceAbove + geo.margins.top + geo.headerHeight;
|
|
527
|
-
const pdfY = toPdfY(lineYFromTop, thickness / 2, geo.pageHeight);
|
|
528
|
-
const [r, g, b] = hexToRgb(colorHex);
|
|
529
|
-
pdfPage.drawLine({
|
|
530
|
-
start: { x: geo.margins.left, y: pdfY },
|
|
531
|
-
end: { x: geo.margins.left + geo.contentWidth, y: pdfY },
|
|
532
|
-
thickness,
|
|
533
|
-
color: rgb(r, g, b),
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
// ─── Code block rendering ─────────────────────────────────────────────────────
|
|
537
|
-
export function renderCodeBlock(pdfPage, pagedBlock, geo, fontMap) {
|
|
538
|
-
const { measuredBlock, startLine, endLine, yFromTop } = pagedBlock;
|
|
539
|
-
const element = measuredBlock.element;
|
|
540
|
-
const padding = measuredBlock.codePadding ?? 8;
|
|
541
|
-
const bgColorHex = element.bgColor ?? '#f6f8fa';
|
|
542
|
-
const textColorHex = element.color ?? '#24292f';
|
|
543
|
-
// Slice the lines being rendered on this page chunk
|
|
544
|
-
const lines = measuredBlock.lines.slice(startLine, endLine);
|
|
545
|
-
const lineHeight = measuredBlock.lineHeight;
|
|
546
|
-
const fontSize = measuredBlock.fontSize;
|
|
547
|
-
// Compute per-chunk padding (only apply padding at the edge of the code block)
|
|
548
|
-
const isFirstChunk = startLine === 0;
|
|
549
|
-
const isLastChunk = endLine === measuredBlock.lines.length;
|
|
550
|
-
const paddingTop = isFirstChunk ? padding : 0;
|
|
551
|
-
const paddingBottom = isLastChunk ? padding : 0;
|
|
552
|
-
const visibleHeight = lines.length * lineHeight + paddingTop + paddingBottom;
|
|
553
|
-
// ── Background box ──────────────────────────────────────────────────────────
|
|
554
|
-
const boxAbsY = yFromTop + geo.margins.top + geo.headerHeight;
|
|
555
|
-
const boxPdfY = toPdfY(boxAbsY, visibleHeight, geo.pageHeight);
|
|
556
|
-
const [bgR, bgG, bgB] = hexToRgb(bgColorHex);
|
|
557
|
-
pdfPage.drawRectangle({
|
|
558
|
-
x: geo.margins.left,
|
|
559
|
-
y: boxPdfY,
|
|
560
|
-
width: geo.contentWidth,
|
|
561
|
-
height: visibleHeight,
|
|
562
|
-
color: rgb(bgR, bgG, bgB),
|
|
563
|
-
borderWidth: 0,
|
|
564
|
-
});
|
|
565
|
-
// ── Text lines ──────────────────────────────────────────────────────────────
|
|
566
|
-
const pdfFont = fontMap.get(measuredBlock.fontKey);
|
|
567
|
-
if (!pdfFont || lines.length === 0)
|
|
568
|
-
return;
|
|
569
|
-
const fontHeight = pdfFont.heightAtSize(fontSize);
|
|
570
|
-
const [r, g, b] = hexToRgb(textColorHex);
|
|
571
|
-
const textX = geo.margins.left + padding;
|
|
572
|
-
// Syntax highlighting: tokenize if language is set and highlight.js is available
|
|
573
|
-
const highlightTokens = measuredBlock.codeHighlightTokens;
|
|
574
|
-
if (highlightTokens && highlightTokens.length > 0) {
|
|
575
|
-
// Render with per-token colors
|
|
576
|
-
for (let i = 0; i < lines.length; i++) {
|
|
577
|
-
const lineYFromPageTop = boxAbsY + paddingTop + i * lineHeight;
|
|
578
|
-
const pdfY = toPdfY(lineYFromPageTop, fontHeight, geo.pageHeight);
|
|
579
|
-
const lineTokens = highlightTokens[startLine + i];
|
|
580
|
-
if (!lineTokens)
|
|
581
|
-
continue;
|
|
582
|
-
let curX = textX;
|
|
583
|
-
for (const token of lineTokens) {
|
|
584
|
-
if (!token.text)
|
|
585
|
-
continue;
|
|
586
|
-
const [tr, tg, tb] = hexToRgb(token.color);
|
|
587
|
-
pdfPage.drawText(token.text, {
|
|
588
|
-
x: curX,
|
|
589
|
-
y: pdfY,
|
|
590
|
-
size: fontSize,
|
|
591
|
-
font: pdfFont,
|
|
592
|
-
color: rgb(tr, tg, tb),
|
|
593
|
-
});
|
|
594
|
-
curX += pdfFont.widthOfTextAtSize(token.text, fontSize);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
// Plain text rendering (no highlighting)
|
|
600
|
-
for (let i = 0; i < lines.length; i++) {
|
|
601
|
-
const line = lines[i];
|
|
602
|
-
const lineYFromPageTop = boxAbsY + paddingTop + i * lineHeight;
|
|
603
|
-
const pdfY = toPdfY(lineYFromPageTop, fontHeight, geo.pageHeight);
|
|
604
|
-
pdfPage.drawText(line.text.trimEnd(), {
|
|
605
|
-
x: textX,
|
|
606
|
-
y: pdfY,
|
|
607
|
-
size: fontSize,
|
|
608
|
-
font: pdfFont,
|
|
609
|
-
color: rgb(r, g, b),
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
// ─── Blockquote rendering ─────────────────────────────────────────────────────
|
|
615
|
-
export function renderBlockquote(pdfPage, pagedBlock, geo, fontMap) {
|
|
616
|
-
const { measuredBlock, startLine, endLine, yFromTop } = pagedBlock;
|
|
617
|
-
const element = measuredBlock.element;
|
|
618
|
-
const paddingV = measuredBlock.blockquotePaddingV ?? 10;
|
|
619
|
-
const paddingH = measuredBlock.blockquotePaddingH ?? 16;
|
|
620
|
-
const borderWidth = measuredBlock.blockquoteBorderWidth ?? 3;
|
|
621
|
-
const bgColorHex = element.bgColor ?? '#f8f9fa';
|
|
622
|
-
const borderColorHex = element.borderColor ?? '#0070f3';
|
|
623
|
-
const textColorHex = element.color ?? '#333333';
|
|
624
|
-
const alignRaw = element.align ?? (measuredBlock.isRTL ? 'right' : 'left');
|
|
625
|
-
const align = alignRaw === 'justify' ? 'left' : alignRaw;
|
|
626
|
-
const lines = measuredBlock.lines.slice(startLine, endLine);
|
|
627
|
-
const lineHeight = measuredBlock.lineHeight;
|
|
628
|
-
const fontSize = measuredBlock.fontSize;
|
|
629
|
-
// Compute per-chunk padding (only at the edge of the block, like code)
|
|
630
|
-
const isFirstChunk = startLine === 0;
|
|
631
|
-
const isLastChunk = endLine === measuredBlock.lines.length;
|
|
632
|
-
const paddingTop = isFirstChunk ? paddingV : 0;
|
|
633
|
-
const paddingBottom = isLastChunk ? paddingV : 0;
|
|
634
|
-
const visibleHeight = lines.length * lineHeight + paddingTop + paddingBottom;
|
|
635
|
-
const boxAbsY = yFromTop + geo.margins.top + geo.headerHeight;
|
|
636
|
-
const boxPdfY = toPdfY(boxAbsY, visibleHeight, geo.pageHeight);
|
|
637
|
-
// ── Background box ──────────────────────────────────────────────────────────
|
|
638
|
-
const [bgR, bgG, bgB] = hexToRgb(bgColorHex);
|
|
639
|
-
pdfPage.drawRectangle({
|
|
640
|
-
x: geo.margins.left,
|
|
641
|
-
y: boxPdfY,
|
|
642
|
-
width: geo.contentWidth,
|
|
643
|
-
height: visibleHeight,
|
|
644
|
-
color: rgb(bgR, bgG, bgB),
|
|
645
|
-
borderWidth: 0,
|
|
646
|
-
});
|
|
647
|
-
// ── Left border stripe ──────────────────────────────────────────────────────
|
|
648
|
-
const [bdR, bdG, bdB] = hexToRgb(borderColorHex);
|
|
649
|
-
pdfPage.drawRectangle({
|
|
650
|
-
x: geo.margins.left,
|
|
651
|
-
y: boxPdfY,
|
|
652
|
-
width: borderWidth,
|
|
653
|
-
height: visibleHeight,
|
|
654
|
-
color: rgb(bdR, bdG, bdB),
|
|
655
|
-
borderWidth: 0,
|
|
656
|
-
});
|
|
657
|
-
// ── Text lines ──────────────────────────────────────────────────────────────
|
|
658
|
-
const pdfFont = fontMap.get(measuredBlock.fontKey);
|
|
659
|
-
if (!pdfFont || lines.length === 0)
|
|
660
|
-
return;
|
|
661
|
-
const fontHeight = pdfFont.heightAtSize(fontSize);
|
|
662
|
-
const [r, g, b] = hexToRgb(textColorHex);
|
|
663
|
-
const textStartX = geo.margins.left + borderWidth + paddingH;
|
|
664
|
-
const textAreaWidth = geo.contentWidth - borderWidth - 2 * paddingH;
|
|
665
|
-
for (let i = 0; i < lines.length; i++) {
|
|
666
|
-
const line = lines[i];
|
|
667
|
-
if (line.text === '')
|
|
668
|
-
continue;
|
|
669
|
-
const lineYFromPageTop = boxAbsY + paddingTop + i * lineHeight;
|
|
670
|
-
const pdfY = toPdfY(lineYFromPageTop, fontHeight, geo.pageHeight);
|
|
671
|
-
const trimmedText = line.text.trimEnd();
|
|
672
|
-
const isLastLine = i === lines.length - 1;
|
|
673
|
-
let drawX;
|
|
674
|
-
if (alignRaw === 'justify') {
|
|
675
|
-
drawJustifiedLine(pdfPage, trimmedText, isLastLine, textStartX, pdfY, textAreaWidth, fontSize, pdfFont, rgb(r, g, b));
|
|
676
|
-
drawX = textStartX;
|
|
677
|
-
}
|
|
678
|
-
else {
|
|
679
|
-
const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, fontSize);
|
|
680
|
-
drawX = resolveX(align, textStartX, textAreaWidth, lineWidth);
|
|
681
|
-
pdfPage.drawText(trimmedText, {
|
|
682
|
-
x: drawX,
|
|
683
|
-
y: pdfY,
|
|
684
|
-
size: fontSize,
|
|
685
|
-
font: pdfFont,
|
|
686
|
-
color: rgb(r, g, b),
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
if (element.underline || element.strikethrough) {
|
|
690
|
-
const lineWidth = pdfFont.widthOfTextAtSize(trimmedText, fontSize);
|
|
691
|
-
drawTextDecoration(pdfPage, drawX, lineWidth, pdfY, fontSize, pdfFont, [r, g, b], { underline: element.underline ?? false, strikethrough: element.strikethrough ?? false });
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
// ─── Callout rendering ────────────────────────────────────────────
|
|
696
|
-
export function renderCallout(pdfPage, pagedBlock, geo, fontMap) {
|
|
697
|
-
const { measuredBlock, startLine, endLine, yFromTop } = pagedBlock;
|
|
698
|
-
const el = measuredBlock.element;
|
|
699
|
-
const cd = measuredBlock.calloutData;
|
|
700
|
-
if (!cd)
|
|
701
|
-
return;
|
|
702
|
-
const { paddingH, paddingV, borderColor, backgroundColor, titleColor, color, titleText } = cd;
|
|
703
|
-
const isFirstChunk = startLine === 0;
|
|
704
|
-
const isLastChunk = endLine === measuredBlock.lines.length;
|
|
705
|
-
const lines = measuredBlock.lines.slice(startLine, endLine);
|
|
706
|
-
const fs = measuredBlock.fontSize;
|
|
707
|
-
const lh = measuredBlock.lineHeight;
|
|
708
|
-
const font = fontMap.get(measuredBlock.fontKey) ?? [...fontMap.values()][0];
|
|
709
|
-
if (!font)
|
|
710
|
-
return;
|
|
711
|
-
const titleH = isFirstChunk && titleText ? cd.titleHeight : 0;
|
|
712
|
-
const topPad = isFirstChunk ? paddingV : 0;
|
|
713
|
-
const bottomPad = isLastChunk ? paddingV : 0;
|
|
714
|
-
const chunkHeight = topPad + titleH + lines.length * lh + bottomPad;
|
|
715
|
-
const boxAbsY = yFromTop + geo.margins.top + geo.headerHeight;
|
|
716
|
-
const boxPdfY = toPdfY(boxAbsY, chunkHeight, geo.pageHeight);
|
|
717
|
-
// Background
|
|
718
|
-
const [bgR, bgG, bgB] = hexToRgb(backgroundColor);
|
|
719
|
-
pdfPage.drawRectangle({
|
|
720
|
-
x: geo.margins.left,
|
|
721
|
-
y: boxPdfY,
|
|
722
|
-
width: geo.contentWidth,
|
|
723
|
-
height: chunkHeight,
|
|
724
|
-
color: rgb(bgR, bgG, bgB),
|
|
725
|
-
borderWidth: 0,
|
|
726
|
-
});
|
|
727
|
-
// Left border stripe (3pt wide)
|
|
728
|
-
const [bdR, bdG, bdB] = hexToRgb(borderColor);
|
|
729
|
-
pdfPage.drawRectangle({
|
|
730
|
-
x: geo.margins.left,
|
|
731
|
-
y: boxPdfY,
|
|
732
|
-
width: 3,
|
|
733
|
-
height: chunkHeight,
|
|
734
|
-
color: rgb(bdR, bdG, bdB),
|
|
735
|
-
borderWidth: 0,
|
|
736
|
-
});
|
|
737
|
-
const fontHeight = font.heightAtSize(fs);
|
|
738
|
-
let currentAbsY = boxAbsY + topPad;
|
|
739
|
-
// Draw title if first chunk
|
|
740
|
-
if (isFirstChunk && titleText) {
|
|
741
|
-
// Try to get bold font variant by modifying the fontKey
|
|
742
|
-
const boldFontKey = measuredBlock.fontKey.replace(/-400-/, '-700-');
|
|
743
|
-
const titleFont = fontMap.get(boldFontKey) ?? font;
|
|
744
|
-
const [tR, tG, tB] = hexToRgb(titleColor);
|
|
745
|
-
const titlePdfY = toPdfY(currentAbsY + (fs * LINE_HEIGHT_COMPACT - fs) / 2, fontHeight, geo.pageHeight);
|
|
746
|
-
pdfPage.drawText(titleText, {
|
|
747
|
-
x: geo.margins.left + paddingH,
|
|
748
|
-
y: titlePdfY,
|
|
749
|
-
size: fs,
|
|
750
|
-
font: titleFont,
|
|
751
|
-
color: rgb(tR, tG, tB),
|
|
752
|
-
});
|
|
753
|
-
currentAbsY += titleH;
|
|
754
|
-
}
|
|
755
|
-
// Draw content lines
|
|
756
|
-
const [tR, tG, tB] = hexToRgb(color);
|
|
757
|
-
for (let i = 0; i < lines.length; i++) {
|
|
758
|
-
const line = lines[i];
|
|
759
|
-
if (line.text === '') {
|
|
760
|
-
currentAbsY += lh;
|
|
761
|
-
continue;
|
|
762
|
-
}
|
|
763
|
-
const linePdfY = toPdfY(currentAbsY + (lh - fs) / 2, fontHeight, geo.pageHeight);
|
|
764
|
-
pdfPage.drawText(line.text.trimEnd(), {
|
|
765
|
-
x: geo.margins.left + paddingH,
|
|
766
|
-
y: linePdfY,
|
|
767
|
-
size: fs,
|
|
768
|
-
font,
|
|
769
|
-
color: rgb(tR, tG, tB),
|
|
770
|
-
});
|
|
771
|
-
currentAbsY += lh;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
// ─── Rich paragraph rendering ─────────────────────────────────────────────────
|
|
775
|
-
export function renderRichParagraph(pdfPage, pagedBlock, geo, fontMap, pdfDoc, footnoteNumbering) {
|
|
776
|
-
const { measuredBlock, startLine, endLine, yFromTop } = pagedBlock;
|
|
777
|
-
const { element, richLines, lineHeight, fontSize } = measuredBlock;
|
|
778
|
-
const tabularNumbers = element.type === 'rich-paragraph' && element.tabularNumbers === true;
|
|
779
|
-
if (!richLines || richLines.length === 0)
|
|
780
|
-
return;
|
|
781
|
-
// Only render the lines on this page chunk
|
|
782
|
-
const visibleLines = richLines.slice(startLine, endLine);
|
|
783
|
-
// Draw background color if set
|
|
784
|
-
const columnData = measuredBlock.columnData;
|
|
785
|
-
if (element.type === 'rich-paragraph' && element.bgColor) {
|
|
786
|
-
// Use sum of per-line heights (may vary with per-span fontSize)
|
|
787
|
-
const chunkHeight = columnData
|
|
788
|
-
? visibleLines.slice(0, columnData.linesPerColumn).reduce((sum, rl) => sum + rl.lineHeight, 0)
|
|
789
|
-
: visibleLines.reduce((sum, rl) => sum + rl.lineHeight, 0);
|
|
790
|
-
const boxAbsY = yFromTop + geo.margins.top + geo.headerHeight;
|
|
791
|
-
const boxPdfY = toPdfY(boxAbsY, chunkHeight, geo.pageHeight);
|
|
792
|
-
const [bgR, bgG, bgB] = hexToRgb(element.bgColor);
|
|
793
|
-
pdfPage.drawRectangle({
|
|
794
|
-
x: geo.margins.left,
|
|
795
|
-
y: boxPdfY,
|
|
796
|
-
width: geo.contentWidth,
|
|
797
|
-
height: chunkHeight,
|
|
798
|
-
color: rgb(bgR, bgG, bgB),
|
|
799
|
-
borderWidth: 0,
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
// Multi-column layout
|
|
803
|
-
if (columnData) {
|
|
804
|
-
const { columnCount, columnGap, columnWidth, linesPerColumn } = columnData;
|
|
805
|
-
// Track cumulative Y per column (per-line heights may vary)
|
|
806
|
-
const colCumY = new Array(columnCount).fill(0);
|
|
807
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
808
|
-
const richLine = visibleLines[i];
|
|
809
|
-
const colIdx = Math.floor(i / linesPerColumn);
|
|
810
|
-
const colOffsetX = colIdx * (columnWidth + columnGap);
|
|
811
|
-
const lineYFromTop = yFromTop + colCumY[colIdx] + geo.margins.top + geo.headerHeight;
|
|
812
|
-
for (const fragment of richLine.fragments) {
|
|
813
|
-
if (!fragment.text || fragment.text.trim() === '')
|
|
814
|
-
continue;
|
|
815
|
-
const pdfFont = fontMap.get(fragment.fontKey);
|
|
816
|
-
if (!pdfFont) {
|
|
817
|
-
throw new PretextPdfError('FONT_NOT_LOADED', `Rich text fragment font "${fragment.fontKey}" not found in fontMap. This is a bug — font validation should have caught this.`);
|
|
818
|
-
}
|
|
819
|
-
const fontHeight = pdfFont.heightAtSize(fragment.fontSize);
|
|
820
|
-
const basePdfY = toPdfY(lineYFromTop, fontHeight, geo.pageHeight);
|
|
821
|
-
const [r, g, b] = hexToRgb(fragment.color);
|
|
822
|
-
const drawX = geo.margins.left + colOffsetX + fragment.x;
|
|
823
|
-
// Footnote ref spans render as superscript number, replacing the original text
|
|
824
|
-
if (fragment.footnoteRef) {
|
|
825
|
-
const num = footnoteNumbering?.get(fragment.footnoteRef) ?? '?';
|
|
826
|
-
const superText = String(num);
|
|
827
|
-
const superSize = fragment.fontSize * 0.65;
|
|
828
|
-
const superYOffset = fragment.fontSize * 0.4;
|
|
829
|
-
const superPdfY = basePdfY + superYOffset;
|
|
830
|
-
pdfPage.drawText(superText, {
|
|
831
|
-
x: drawX,
|
|
832
|
-
y: superPdfY,
|
|
833
|
-
size: superSize,
|
|
834
|
-
font: pdfFont,
|
|
835
|
-
color: rgb(r, g, b),
|
|
836
|
-
});
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
|
-
const fragmentPdfY = basePdfY + (fragment.yOffset ?? 0);
|
|
840
|
-
const drawText = fragment.text.trimEnd();
|
|
841
|
-
let fragWidth;
|
|
842
|
-
if (fragment.letterSpacing && fragment.letterSpacing > 0) {
|
|
843
|
-
let cx = drawX;
|
|
844
|
-
for (const ch of drawText) {
|
|
845
|
-
pdfPage.drawText(ch, { x: cx, y: fragmentPdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
846
|
-
cx += pdfFont.widthOfTextAtSize(ch, fragment.fontSize) + fragment.letterSpacing;
|
|
847
|
-
}
|
|
848
|
-
fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize) + fragment.letterSpacing * (drawText.length - 1);
|
|
849
|
-
}
|
|
850
|
-
else if (tabularNumbers) {
|
|
851
|
-
drawTabularText(pdfPage, drawText, drawX, fragmentPdfY, fragment.fontSize, pdfFont, rgb(r, g, b));
|
|
852
|
-
fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
|
|
853
|
-
}
|
|
854
|
-
else {
|
|
855
|
-
pdfPage.drawText(drawText, { x: drawX, y: fragmentPdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
856
|
-
fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
|
|
857
|
-
}
|
|
858
|
-
drawTextDecoration(pdfPage, drawX, fragWidth, fragmentPdfY, fragment.fontSize, pdfFont, [r, g, b], { underline: fragment.underline ?? false, strikethrough: fragment.strikethrough ?? false });
|
|
859
|
-
if (fragment.url) {
|
|
860
|
-
addLinkAnnotation(pdfDoc, pdfPage, drawX, fragmentPdfY, fragWidth, fragment.fontSize, fragment.url);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
colCumY[colIdx] += richLine.lineHeight;
|
|
864
|
-
}
|
|
865
|
-
return; // skip standard single-column path
|
|
866
|
-
}
|
|
867
|
-
// Single-column layout (standard path)
|
|
868
|
-
// Track cumulative Y (per-line heights may vary due to per-span fontSize)
|
|
869
|
-
let cumY = 0;
|
|
870
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
871
|
-
const richLine = visibleLines[i];
|
|
872
|
-
const lineYFromTop = yFromTop + cumY + geo.margins.top + geo.headerHeight;
|
|
873
|
-
for (const fragment of richLine.fragments) {
|
|
874
|
-
if (!fragment.text || fragment.text.trim() === '')
|
|
875
|
-
continue;
|
|
876
|
-
const pdfFont = fontMap.get(fragment.fontKey);
|
|
877
|
-
if (!pdfFont) {
|
|
878
|
-
throw new PretextPdfError('FONT_NOT_LOADED', `Rich text fragment font "${fragment.fontKey}" not found in fontMap. This is a bug — font validation should have caught this.`);
|
|
879
|
-
}
|
|
880
|
-
const fontHeight = pdfFont.heightAtSize(fragment.fontSize);
|
|
881
|
-
const basePdfY = toPdfY(lineYFromTop, fontHeight, geo.pageHeight);
|
|
882
|
-
const [r, g, b] = hexToRgb(fragment.color);
|
|
883
|
-
const drawX = geo.margins.left + fragment.x;
|
|
884
|
-
// Footnote ref spans render as superscript number, replacing the original text
|
|
885
|
-
if (fragment.footnoteRef) {
|
|
886
|
-
const num = footnoteNumbering?.get(fragment.footnoteRef) ?? '?';
|
|
887
|
-
const superText = String(num);
|
|
888
|
-
const superSize = fragment.fontSize * 0.65;
|
|
889
|
-
const superYOffset = fragment.fontSize * 0.4;
|
|
890
|
-
const superPdfY = basePdfY + superYOffset;
|
|
891
|
-
pdfPage.drawText(superText, {
|
|
892
|
-
x: drawX,
|
|
893
|
-
y: superPdfY,
|
|
894
|
-
size: superSize,
|
|
895
|
-
font: pdfFont,
|
|
896
|
-
color: rgb(r, g, b),
|
|
897
|
-
});
|
|
898
|
-
continue;
|
|
899
|
-
}
|
|
900
|
-
const fragmentPdfY = basePdfY + (fragment.yOffset ?? 0);
|
|
901
|
-
const drawText = fragment.text.trimEnd();
|
|
902
|
-
let fragWidth;
|
|
903
|
-
if (fragment.letterSpacing && fragment.letterSpacing > 0) {
|
|
904
|
-
let cx = drawX;
|
|
905
|
-
for (const ch of drawText) {
|
|
906
|
-
pdfPage.drawText(ch, { x: cx, y: fragmentPdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
907
|
-
cx += pdfFont.widthOfTextAtSize(ch, fragment.fontSize) + fragment.letterSpacing;
|
|
908
|
-
}
|
|
909
|
-
fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize) + fragment.letterSpacing * (drawText.length - 1);
|
|
910
|
-
}
|
|
911
|
-
else if (tabularNumbers) {
|
|
912
|
-
drawTabularText(pdfPage, drawText, drawX, fragmentPdfY, fragment.fontSize, pdfFont, rgb(r, g, b));
|
|
913
|
-
fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
|
|
914
|
-
}
|
|
915
|
-
else {
|
|
916
|
-
pdfPage.drawText(drawText, { x: drawX, y: fragmentPdfY, size: fragment.fontSize, font: pdfFont, color: rgb(r, g, b) });
|
|
917
|
-
fragWidth = pdfFont.widthOfTextAtSize(drawText, fragment.fontSize);
|
|
918
|
-
}
|
|
919
|
-
drawTextDecoration(pdfPage, drawX, fragWidth, fragmentPdfY, fragment.fontSize, pdfFont, [r, g, b], { underline: fragment.underline ?? false, strikethrough: fragment.strikethrough ?? false });
|
|
920
|
-
if (fragment.url) {
|
|
921
|
-
addLinkAnnotation(pdfDoc, pdfPage, drawX, fragmentPdfY, fragWidth, fragment.fontSize, fragment.url);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
cumY += richLine.lineHeight;
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
// ─── Footnote zone rendering ──────────────────────────────────────────────────
|
|
928
|
-
export function renderFootnoteZone(pdfPage, footnoteItems, zoneHeight, fontMap, doc, geo) {
|
|
929
|
-
const { pageHeight, margins, footerHeight, contentWidth } = geo;
|
|
930
|
-
const SEPARATOR_PADDING = 6; // pt above and below the separator line
|
|
931
|
-
// Zone top in PDF coords (Y=0 at bottom of page)
|
|
932
|
-
const zoneTopPdfY = margins.bottom + footerHeight + zoneHeight;
|
|
933
|
-
const separatorY = zoneTopPdfY - SEPARATOR_PADDING;
|
|
934
|
-
// Draw separator line: 1/3 content width, max 120pt
|
|
935
|
-
const lineLength = Math.min(contentWidth * 0.33, 120);
|
|
936
|
-
pdfPage.drawLine({
|
|
937
|
-
start: { x: margins.left, y: separatorY },
|
|
938
|
-
end: { x: margins.left + lineLength, y: separatorY },
|
|
939
|
-
thickness: 0.5,
|
|
940
|
-
color: rgb(0.5, 0.5, 0.5),
|
|
941
|
-
});
|
|
942
|
-
const defaultFontSize = doc.defaultFontSize ?? 12;
|
|
943
|
-
let currentPdfY = separatorY - SEPARATOR_PADDING;
|
|
944
|
-
for (const { def, number } of footnoteItems) {
|
|
945
|
-
const fontSize = def.fontSize ?? Math.max(8, defaultFontSize - 2);
|
|
946
|
-
const lineHeight = fontSize * LINE_HEIGHT_BODY;
|
|
947
|
-
const fontFamily = def.fontFamily ?? doc.defaultFont ?? 'Inter';
|
|
948
|
-
const fontKey = buildFontKey(fontFamily, 400, 'normal');
|
|
949
|
-
const pdfFont = fontMap.get(fontKey);
|
|
950
|
-
if (!pdfFont)
|
|
951
|
-
continue;
|
|
952
|
-
currentPdfY -= lineHeight;
|
|
953
|
-
const prefix = `${number}. `;
|
|
954
|
-
const fullText = prefix + def.text;
|
|
955
|
-
pdfPage.drawText(fullText, {
|
|
956
|
-
x: margins.left,
|
|
957
|
-
y: currentPdfY,
|
|
958
|
-
size: fontSize,
|
|
959
|
-
font: pdfFont,
|
|
960
|
-
color: rgb(0.2, 0.2, 0.2),
|
|
961
|
-
});
|
|
962
|
-
currentPdfY -= (def.spaceAfter ?? 4);
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
// ─── Header / Footer rendering ────────────────────────────────────────────────
|
|
966
|
-
export function renderHeaderFooter(pdfPage, spec, pageNumber, totalPages, geo, fontMap, position, extra) {
|
|
967
|
-
const text = resolveTokens(spec.text, pageNumber, totalPages, extra);
|
|
968
|
-
const fontSize = spec.fontSize ?? 10;
|
|
969
|
-
const align = spec.align ?? 'center';
|
|
970
|
-
const fontKey = `${spec.fontFamily ?? 'Inter'}-${spec.fontWeight ?? 400}-normal`;
|
|
971
|
-
const pdfFont = fontMap.get(fontKey);
|
|
972
|
-
if (!pdfFont) {
|
|
973
|
-
throw new PretextPdfError('FONT_NOT_LOADED', `${position} font "${fontKey}" not found in fontMap. This is a bug — font validation should have caught this.`);
|
|
974
|
-
}
|
|
975
|
-
const fontHeight = pdfFont.heightAtSize(fontSize);
|
|
976
|
-
let yFromTop;
|
|
977
|
-
if (position === 'header') {
|
|
978
|
-
yFromTop = (geo.margins.top - fontHeight) / 2;
|
|
979
|
-
}
|
|
980
|
-
else {
|
|
981
|
-
yFromTop = geo.pageHeight - geo.margins.bottom + (geo.margins.bottom - fontHeight) / 2;
|
|
982
|
-
}
|
|
983
|
-
const pdfY = toPdfY(yFromTop, fontHeight, geo.pageHeight);
|
|
984
|
-
const textWidth = pdfFont.widthOfTextAtSize(text, fontSize);
|
|
985
|
-
const x = resolveX(align, geo.margins.left, geo.contentWidth, textWidth);
|
|
986
|
-
const [textR, textG, textB] = hexToRgb(spec.color ?? '#666666');
|
|
987
|
-
pdfPage.drawText(text, {
|
|
988
|
-
x,
|
|
989
|
-
y: pdfY,
|
|
990
|
-
size: fontSize,
|
|
991
|
-
font: pdfFont,
|
|
992
|
-
color: rgb(textR, textG, textB),
|
|
993
|
-
});
|
|
994
|
-
// Separator line
|
|
995
|
-
if (position === 'header') {
|
|
996
|
-
const lineY = toPdfY(geo.margins.top - 4, 1, geo.pageHeight);
|
|
997
|
-
pdfPage.drawLine({
|
|
998
|
-
start: { x: geo.margins.left, y: lineY },
|
|
999
|
-
end: { x: geo.margins.left + geo.contentWidth, y: lineY },
|
|
1000
|
-
thickness: 0.5,
|
|
1001
|
-
color: rgb(0.8, 0.8, 0.8),
|
|
1002
|
-
});
|
|
1003
|
-
}
|
|
1004
|
-
else {
|
|
1005
|
-
const lineY = toPdfY(geo.pageHeight - geo.margins.bottom + 4, 1, geo.pageHeight);
|
|
1006
|
-
pdfPage.drawLine({
|
|
1007
|
-
start: { x: geo.margins.left, y: lineY },
|
|
1008
|
-
end: { x: geo.margins.left + geo.contentWidth, y: lineY },
|
|
1009
|
-
thickness: 0.5,
|
|
1010
|
-
color: rgb(0.8, 0.8, 0.8),
|
|
1011
|
-
});
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
// ─── Watermark rendering ──────────────────────────────────────────────────
|
|
1015
|
-
export function renderWatermark(pdfPage, doc, fontMap, imageMap, geo) {
|
|
1016
|
-
const wm = doc.watermark;
|
|
1017
|
-
if (!wm)
|
|
1018
|
-
return;
|
|
1019
|
-
const opacity = wm.opacity ?? 0.3;
|
|
1020
|
-
const rotation = wm.rotation ?? -45;
|
|
1021
|
-
const { pageWidth, pageHeight } = geo;
|
|
1022
|
-
if (wm.text) {
|
|
1023
|
-
const fontKey = `${wm.fontFamily ?? doc.defaultFont ?? 'Inter'}-${wm.fontWeight ?? 400}-normal`;
|
|
1024
|
-
const pdfFont = fontMap.get(fontKey);
|
|
1025
|
-
if (!pdfFont) {
|
|
1026
|
-
throw new PretextPdfError('FONT_NOT_LOADED', `Watermark font "${fontKey}" not found in fontMap. This is a bug.`);
|
|
1027
|
-
}
|
|
1028
|
-
// Auto-compute font size to span ~60% of page diagonal
|
|
1029
|
-
const fontSize = wm.fontSize ?? (() => {
|
|
1030
|
-
const diagonal = Math.sqrt(pageWidth ** 2 + pageHeight ** 2);
|
|
1031
|
-
const widthAt100 = pdfFont.widthOfTextAtSize(wm.text, 100);
|
|
1032
|
-
return Math.min(120, (diagonal * 0.6 / widthAt100) * 100);
|
|
1033
|
-
})();
|
|
1034
|
-
const [r, g, b] = hexToRgb(wm.color ?? '#CCCCCC');
|
|
1035
|
-
pdfPage.drawText(wm.text, {
|
|
1036
|
-
x: pageWidth / 2,
|
|
1037
|
-
y: pageHeight / 2,
|
|
1038
|
-
size: fontSize,
|
|
1039
|
-
font: pdfFont,
|
|
1040
|
-
color: rgb(r, g, b),
|
|
1041
|
-
rotate: degrees(rotation),
|
|
1042
|
-
opacity,
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
if (wm.image) {
|
|
1046
|
-
const pdfImage = imageMap.get('watermark');
|
|
1047
|
-
if (!pdfImage)
|
|
1048
|
-
return;
|
|
1049
|
-
const margin = 40;
|
|
1050
|
-
pdfPage.drawImage(pdfImage, {
|
|
1051
|
-
x: margin,
|
|
1052
|
-
y: margin,
|
|
1053
|
-
width: pageWidth - margin * 2,
|
|
1054
|
-
height: pageHeight - margin * 2,
|
|
1055
|
-
opacity,
|
|
1056
|
-
});
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
//# sourceMappingURL=render-blocks.js.map
|