@polotno/pdf-export 0.1.37 → 0.1.38

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.
@@ -0,0 +1,141 @@
1
+ import fs from 'fs';
2
+ function compareImages(original, converted, index) {
3
+ const differences = [];
4
+ // Compare position
5
+ const xDiff = Math.abs(original.x - converted.x);
6
+ const yDiff = Math.abs(original.y - converted.y);
7
+ if (xDiff > 1) {
8
+ differences.push(` X: ${original.x.toFixed(2)} → ${converted.x.toFixed(2)} (Δ ${(converted.x - original.x).toFixed(2)})`);
9
+ }
10
+ if (yDiff > 1) {
11
+ differences.push(` Y: ${original.y.toFixed(2)} → ${converted.y.toFixed(2)} (Δ ${(converted.y - original.y).toFixed(2)})`);
12
+ }
13
+ // Compare dimensions
14
+ const widthDiff = Math.abs(original.width - converted.width);
15
+ const heightDiff = Math.abs(original.height - converted.height);
16
+ if (widthDiff > 1) {
17
+ differences.push(` Width: ${original.width.toFixed(2)} → ${converted.width.toFixed(2)} (Δ ${(converted.width - original.width).toFixed(2)})`);
18
+ }
19
+ if (heightDiff > 1) {
20
+ differences.push(` Height: ${original.height.toFixed(2)} → ${converted.height.toFixed(2)} (Δ ${(converted.height - original.height).toFixed(2)})`);
21
+ }
22
+ // Compare rotation
23
+ const origRotation = original.rotation || 0;
24
+ const convRotation = converted.rotation || 0;
25
+ if (Math.abs(origRotation - convRotation) > 0.1) {
26
+ differences.push(` Rotation: ${origRotation} → ${convRotation}`);
27
+ }
28
+ return differences;
29
+ }
30
+ function compareJSON(originalPath, convertedPath) {
31
+ console.log('='.repeat(80));
32
+ console.log('PDF Import Comparison Tool');
33
+ console.log('='.repeat(80));
34
+ console.log();
35
+ // Read JSON files
36
+ const original = JSON.parse(fs.readFileSync(originalPath, 'utf-8'));
37
+ const converted = JSON.parse(fs.readFileSync(convertedPath, 'utf-8'));
38
+ // Compare document dimensions
39
+ console.log('📄 Document Dimensions:');
40
+ console.log(` Original: ${original.width.toFixed(2)} x ${original.height.toFixed(2)}`);
41
+ console.log(` Converted: ${converted.width.toFixed(2)} x ${converted.height.toFixed(2)}`);
42
+ const dimMatch = Math.abs(original.width - converted.width) < 1 &&
43
+ Math.abs(original.height - converted.height) < 1;
44
+ console.log(` Status: ${dimMatch ? '✅ Match' : '❌ Mismatch'}`);
45
+ console.log();
46
+ // Compare page count
47
+ console.log('📑 Page Count:');
48
+ console.log(` Original: ${original.pages.length}`);
49
+ console.log(` Converted: ${converted.pages.length}`);
50
+ console.log(` Status: ${original.pages.length === converted.pages.length ? '✅ Match' : '❌ Mismatch'}`);
51
+ console.log();
52
+ // Compare each page
53
+ const maxPages = Math.max(original.pages.length, converted.pages.length);
54
+ for (let pageIdx = 0; pageIdx < maxPages; pageIdx++) {
55
+ console.log(`${'─'.repeat(80)}`);
56
+ console.log(`Page ${pageIdx + 1}:`);
57
+ console.log(`${'─'.repeat(80)}`);
58
+ const origPage = original.pages[pageIdx];
59
+ const convPage = converted.pages[pageIdx];
60
+ if (!origPage || !convPage) {
61
+ console.log('❌ Page missing in one of the documents');
62
+ console.log();
63
+ continue;
64
+ }
65
+ // Filter to only image elements for comparison
66
+ const origImages = origPage.children.filter(el => el.type === 'image');
67
+ const convImages = convPage.children.filter(el => el.type === 'image');
68
+ console.log();
69
+ console.log(`🖼️ Image Elements:`);
70
+ console.log(` Original: ${origImages.length} images`);
71
+ console.log(` Converted: ${convImages.length} images`);
72
+ console.log();
73
+ // Compare each image
74
+ const maxImages = Math.max(origImages.length, convImages.length);
75
+ for (let imgIdx = 0; imgIdx < maxImages; imgIdx++) {
76
+ const origImg = origImages[imgIdx];
77
+ const convImg = convImages[imgIdx];
78
+ console.log(` Image ${imgIdx + 1}:`);
79
+ if (!origImg) {
80
+ console.log(' ❌ Missing in original');
81
+ console.log();
82
+ continue;
83
+ }
84
+ if (!convImg) {
85
+ console.log(' ❌ Missing in converted');
86
+ console.log();
87
+ continue;
88
+ }
89
+ // Show original values
90
+ console.log(` Original: x=${origImg.x.toFixed(2)}, y=${origImg.y.toFixed(2)}, w=${origImg.width.toFixed(2)}, h=${origImg.height.toFixed(2)}`);
91
+ console.log(` Converted: x=${convImg.x.toFixed(2)}, y=${convImg.y.toFixed(2)}, w=${convImg.width.toFixed(2)}, h=${convImg.height.toFixed(2)}`);
92
+ // Compare and show differences
93
+ const differences = compareImages(origImg, convImg, imgIdx);
94
+ if (differences.length === 0) {
95
+ console.log(' ✅ Perfect match');
96
+ }
97
+ else {
98
+ console.log(' ❌ Differences:');
99
+ differences.forEach(diff => console.log(` ${diff}`));
100
+ }
101
+ console.log();
102
+ }
103
+ // Compare text elements
104
+ const origTexts = origPage.children.filter(el => el.type === 'text');
105
+ const convTexts = convPage.children.filter(el => el.type === 'text');
106
+ if (origTexts.length > 0 || convTexts.length > 0) {
107
+ console.log(`📝 Text Elements:`);
108
+ console.log(` Original: ${origTexts.length} text elements`);
109
+ console.log(` Converted: ${convTexts.length} text elements`);
110
+ console.log();
111
+ }
112
+ // Show other element types
113
+ const origOther = origPage.children.filter(el => el.type !== 'image' && el.type !== 'text');
114
+ const convOther = convPage.children.filter(el => el.type !== 'image' && el.type !== 'text');
115
+ if (origOther.length > 0 || convOther.length > 0) {
116
+ console.log(`🔧 Other Elements:`);
117
+ console.log(` Original: ${origOther.length} elements`);
118
+ console.log(` Converted: ${convOther.length} elements`);
119
+ console.log();
120
+ }
121
+ }
122
+ console.log('='.repeat(80));
123
+ }
124
+ // CLI usage
125
+ const args = process.argv.slice(2);
126
+ if (args.length < 2) {
127
+ console.log('Usage: npm run compare-json <original.json> <converted.json>');
128
+ console.log('Example: npm run compare-json tests/files/pdf-img-design.json tests/files/pdf-img-converted.json');
129
+ process.exit(1);
130
+ }
131
+ const [originalPath, convertedPath] = args;
132
+ // Check files exist
133
+ if (!fs.existsSync(originalPath)) {
134
+ console.error(`Error: Original file not found: ${originalPath}`);
135
+ process.exit(1);
136
+ }
137
+ if (!fs.existsSync(convertedPath)) {
138
+ console.error(`Error: Converted file not found: ${convertedPath}`);
139
+ process.exit(1);
140
+ }
141
+ compareJSON(originalPath, convertedPath);
package/lib/text/fonts.js CHANGED
@@ -2,6 +2,35 @@ import { srcToBuffer } from '../utils.js';
2
2
  import getUrls from 'get-urls';
3
3
  import fetch from 'node-fetch';
4
4
  const fontUrlRegistry = {};
5
+ const patchedPrototypes = new WeakSet();
6
+ /**
7
+ * Patch fontkit's TTFGlyph._getCBox to handle zero-length glyphs.
8
+ * Some fonts (e.g. "Cute Font") have glyphs with equal start/end offsets
9
+ * in the loca table (no outline data), but fontkit still tries to read
10
+ * from the glyf table at that offset, causing a DataView out-of-bounds error.
11
+ */
12
+ function patchFontkitGlyphs(doc) {
13
+ const fkFont = doc._font?.font;
14
+ if (!fkFont || typeof fkFont.getGlyph !== 'function')
15
+ return;
16
+ const glyph = fkFont.getGlyph(0);
17
+ const proto = Object.getPrototypeOf(glyph);
18
+ if (!proto._getCBox || patchedPrototypes.has(proto))
19
+ return;
20
+ const origGetCBox = proto._getCBox;
21
+ proto._getCBox = function (internal) {
22
+ const loca = this._font.loca;
23
+ if (loca) {
24
+ const start = loca.offsets[this.id];
25
+ const end = loca.offsets[this.id + 1];
26
+ if (start === end) {
27
+ return Object.freeze({ minX: 0, minY: 0, maxX: 0, maxY: 0 });
28
+ }
29
+ }
30
+ return origGetCBox.call(this, internal);
31
+ };
32
+ patchedPrototypes.add(proto);
33
+ }
5
34
  export function registerFontUrl(fontFamily, url) {
6
35
  fontUrlRegistry[fontFamily] = url;
7
36
  }
@@ -53,6 +82,7 @@ export async function loadFontForSegment(doc, segment, element, fonts) {
53
82
  // Check if universal font is already defined
54
83
  if (fonts[fontFamily]) {
55
84
  doc.font(fontFamily);
85
+ patchFontkitGlyphs(doc);
56
86
  return fontFamily;
57
87
  }
58
88
  const fontKey = getFontKey(fontFamily, bold, italic, element.fontWeight);
@@ -74,6 +104,7 @@ export async function loadFontForSegment(doc, segment, element, fonts) {
74
104
  fonts[fontKey] = true;
75
105
  }
76
106
  doc.font(fontKey);
107
+ patchFontkitGlyphs(doc);
77
108
  return fontKey;
78
109
  }
79
110
  // Alias for backward compatibility
package/lib/text.d.ts CHANGED
@@ -31,19 +31,9 @@ export interface TextSegment {
31
31
  underline?: boolean;
32
32
  color?: string;
33
33
  }
34
- /**
35
- * Normalize rich text HTML by converting block-level line breaks into newline characters
36
- * while preserving inline formatting tags.
37
- */
38
- declare function normalizeRichText(text: string): string;
39
- /**
40
- * Parse HTML text into styled segments
41
- */
42
- declare function parseHTMLToSegments(html: string, baseElement: TextElement): TextSegment[];
43
34
  export declare function getGoogleFontPath(fontFamily: string, fontWeight?: string, italic?: boolean): Promise<string>;
44
35
  export declare function loadFontIfNeeded(doc: any, element: TextElement, fonts: Record<string, boolean>): Promise<string>;
45
36
  /**
46
37
  * Main text rendering function
47
38
  */
48
39
  export declare function renderText(doc: PDFKit.PDFDocument, element: TextElement, fonts: Record<string, boolean>, attrs?: RenderAttrs): Promise<void>;
49
- export { normalizeRichText as __normalizeRichTextForTests, parseHTMLToSegments as __parseHTMLToSegmentsForTests, };