@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.
- package/lib/pdf-import/coordinate-transform.d.ts +51 -0
- package/lib/pdf-import/coordinate-transform.js +99 -0
- package/lib/pdf-import/element-builder.d.ts +21 -0
- package/lib/pdf-import/element-builder.js +163 -0
- package/lib/pdf-import/font-mapper.d.ts +17 -0
- package/lib/pdf-import/font-mapper.js +142 -0
- package/lib/pdf-import/index.d.ts +35 -0
- package/lib/pdf-import/index.js +105 -0
- package/lib/pdf-import/parser.d.ts +29 -0
- package/lib/pdf-import/parser.js +285 -0
- package/lib/pdf-import/text-analysis.d.ts +17 -0
- package/lib/pdf-import/text-analysis.js +186 -0
- package/lib/pdf-import/types.d.ts +101 -0
- package/lib/scripts/compare-json.d.ts +1 -0
- package/lib/scripts/compare-json.js +141 -0
- package/lib/text/fonts.js +31 -0
- package/lib/text.d.ts +0 -10
- package/lib/text.js +161 -862
- package/package.json +1 -1
- package/lib/browser-entry.d.ts +0 -7
- package/lib/browser-entry.js +0 -11
- package/lib/core/index.d.ts +0 -26
- package/lib/core/index.js +0 -87
- package/lib/platform/adapter.d.ts +0 -37
- package/lib/platform/adapter.js +0 -13
- package/lib/platform/browser-polyfill.js +0 -5
- package/lib/platform/browser.d.ts +0 -7
- package/lib/platform/browser.js +0 -145
- package/lib/platform/node.d.ts +0 -7
- package/lib/platform/node.js +0 -142
- /package/lib/{platform/browser-polyfill.d.ts → pdf-import/types.js} +0 -0
|
@@ -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, };
|