pagyra-js 0.0.13 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/pagyra.min.js +32 -32
- package/dist/browser/pagyra.min.js.map +4 -4
- package/dist/src/pdf/font/binary-writer.d.ts +18 -0
- package/dist/src/pdf/font/binary-writer.js +67 -0
- package/dist/src/pdf/font/builtin-fonts.browser.js +1 -1
- package/dist/src/pdf/font/embedder.d.ts +1 -0
- package/dist/src/pdf/font/embedder.js +20 -23
- package/dist/src/pdf/font/font-registry.js +4 -25
- package/dist/src/pdf/font/font-subset.d.ts +7 -4
- package/dist/src/pdf/font/font-subset.js +329 -124
- package/dist/src/pdf/font/managers/subset-resource-manager.js +14 -2
- package/dist/src/pdf/font/ttf-lite.js +2 -1
- package/dist/src/pdf/font/ttf-table-parser.d.ts +1 -0
- package/dist/src/pdf/font/ttf-table-parser.js +7 -0
- package/dist/src/pdf/renderers/text-renderer.js +17 -16
- package/dist/src/pdf/renderers/text-shadow-renderer.d.ts +4 -0
- package/dist/src/pdf/renderers/text-shadow-renderer.js +34 -74
- package/dist/src/types/fonts.d.ts +9 -1
- package/dist/src/types/fonts.js +6 -1
- package/dist/tests/verify-subset-multi.spec.d.ts +1 -0
- package/dist/tests/verify-subset-multi.spec.js +36 -0
- package/dist/tests/verify-subset.spec.d.ts +1 -0
- package/dist/tests/verify-subset.spec.js +35 -0
- package/package.json +1 -1
|
@@ -12,7 +12,7 @@ export class TextShadowRenderer {
|
|
|
12
12
|
}
|
|
13
13
|
async render(context) {
|
|
14
14
|
const commands = [];
|
|
15
|
-
const { run, font, encoded, Tm, fontSizePt, fontSizePx, wordSpacingPt, appliedWordSpacing, fontResourceName } = context;
|
|
15
|
+
const { run, font, encoded, Tm, fontSizePt, fontSizePx, wordSpacingPt, appliedWordSpacing, fontResourceName, subset, subsetAlias } = context;
|
|
16
16
|
if (!run.textShadows || run.textShadows.length === 0) {
|
|
17
17
|
return commands;
|
|
18
18
|
}
|
|
@@ -23,17 +23,17 @@ export class TextShadowRenderer {
|
|
|
23
23
|
const faceMetrics = glyphMetrics ?? (embedder ? embedder.getMetrics(font.baseFont) : null);
|
|
24
24
|
const wordSpacingCmd = appliedWordSpacing ? `${formatNumber(wordSpacingPt)} Tw` : undefined;
|
|
25
25
|
const resetWordSpacingCmd = appliedWordSpacing ? "0 Tw" : undefined;
|
|
26
|
-
|
|
26
|
+
// If we have a subset, we use its alias and re-encode the text to match its CIDs
|
|
27
|
+
const fontName = subsetAlias ?? fontResourceName ?? font.resourceName;
|
|
28
|
+
const finalEncoded = subset ? encodeSubsetText(run, subset) : encoded;
|
|
27
29
|
if (this.imageRenderer && needsRaster) {
|
|
28
30
|
if (run.glyphs && faceMetrics) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
catch {
|
|
36
|
-
// ignore
|
|
31
|
+
// ... (rasterization logic stays the same)
|
|
32
|
+
// Note: rasterization uses faceMetrics which doesn't trigger font realization
|
|
33
|
+
// unless font.ref is accessed, but here we use font.baseFont or glyphs already in memory.
|
|
34
|
+
const pages = (await import("../font/glyph-atlas.js")).globalGlyphAtlas.getPages();
|
|
35
|
+
if (pages && pages.length > 0) {
|
|
36
|
+
this.imageRenderer.registerAtlasPages(pages);
|
|
37
37
|
}
|
|
38
38
|
const supersample = 4;
|
|
39
39
|
const glyphMasks = [];
|
|
@@ -70,7 +70,6 @@ export class TextShadowRenderer {
|
|
|
70
70
|
if (gy1 > maxY)
|
|
71
71
|
maxY = gy1;
|
|
72
72
|
}
|
|
73
|
-
// Pad the bounding box to leave room for blur bleed.
|
|
74
73
|
const bleedPad = Math.ceil(maxBlurPx * 2);
|
|
75
74
|
minX = Math.floor(minX - bleedPad);
|
|
76
75
|
minY = Math.floor(minY - bleedPad);
|
|
@@ -104,9 +103,8 @@ export class TextShadowRenderer {
|
|
|
104
103
|
}
|
|
105
104
|
}
|
|
106
105
|
for (const sh of run.textShadows) {
|
|
107
|
-
if (!sh || !sh.color)
|
|
106
|
+
if (!sh || !sh.color)
|
|
108
107
|
continue;
|
|
109
|
-
}
|
|
110
108
|
const blurPx = Math.max(0, sh.blur ?? 0);
|
|
111
109
|
const clampedCombined = combinedAlpha instanceof Uint8ClampedArray ? combinedAlpha : new Uint8ClampedArray(combinedAlpha);
|
|
112
110
|
const rawAlphaBuf = blurPx > 0 ? blurAlpha(clampedCombined, combinedW, combinedH, blurPx) : clampedCombined;
|
|
@@ -155,79 +153,24 @@ export class TextShadowRenderer {
|
|
|
155
153
|
}
|
|
156
154
|
}
|
|
157
155
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
this.appendVectorShadowLayers(commands, run, font, encoded, Tm, fontSizePt, fontName, wordSpacingCmd, resetWordSpacingCmd);
|
|
156
|
+
else {
|
|
157
|
+
this.appendVectorShadowLayers(commands, run, font, finalEncoded, Tm, fontSizePt, fontName, wordSpacingCmd, resetWordSpacingCmd);
|
|
161
158
|
}
|
|
162
159
|
}
|
|
163
160
|
else {
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
for (const sh of run.textShadows) {
|
|
167
|
-
if (!sh || !sh.color)
|
|
168
|
-
continue;
|
|
169
|
-
const offsetX = sh.offsetX ?? 0;
|
|
170
|
-
const offsetY = sh.offsetY ?? 0;
|
|
171
|
-
const blurPx = Math.max(0, sh.blur ?? 0);
|
|
172
|
-
const baseAlpha = sh.color.a ?? 1;
|
|
173
|
-
const samples = [];
|
|
174
|
-
if (blurPx <= 1) {
|
|
175
|
-
samples.push({ dx: 0, dy: 0, weight: 1 });
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
const centerW = 0.38;
|
|
179
|
-
const orthoW = 0.12;
|
|
180
|
-
const diagW = (1 - (centerW + 4 * orthoW)) / 4;
|
|
181
|
-
const radius = blurPx / 2;
|
|
182
|
-
samples.push({ dx: 0, dy: 0, weight: centerW });
|
|
183
|
-
samples.push({ dx: -radius, dy: 0, weight: orthoW });
|
|
184
|
-
samples.push({ dx: radius, dy: 0, weight: orthoW });
|
|
185
|
-
samples.push({ dx: 0, dy: -radius, weight: orthoW });
|
|
186
|
-
samples.push({ dx: 0, dy: radius, weight: orthoW });
|
|
187
|
-
samples.push({ dx: -radius, dy: -radius, weight: diagW });
|
|
188
|
-
samples.push({ dx: radius, dy: -radius, weight: diagW });
|
|
189
|
-
samples.push({ dx: -radius, dy: radius, weight: diagW });
|
|
190
|
-
samples.push({ dx: radius, dy: radius, weight: diagW });
|
|
191
|
-
}
|
|
192
|
-
for (const s of samples) {
|
|
193
|
-
const sx = s.dx;
|
|
194
|
-
const sy = s.dy;
|
|
195
|
-
const sampleAlpha = baseAlpha * s.weight;
|
|
196
|
-
const sampleColor = { r: sh.color.r, g: sh.color.g, b: sh.color.b, a: sampleAlpha };
|
|
197
|
-
const shadowX = this.coordinateTransformer.convertPxToPt(Tm.e + offsetX + sx);
|
|
198
|
-
const shadowLocalBaseline = Tm.f - this.coordinateTransformer.pageOffsetPx + offsetY + sy;
|
|
199
|
-
const shadowYPt = this.coordinateTransformer.pageHeightPt - this.coordinateTransformer.convertPxToPt(shadowLocalBaseline);
|
|
200
|
-
const shadowSequence = ["q", fillColorCommand(sampleColor, this.graphicsStateManager), "BT"];
|
|
201
|
-
if (wordSpacingCmd)
|
|
202
|
-
shadowSequence.push(wordSpacingCmd);
|
|
203
|
-
shadowSequence.push(`/${fontName} ${formatNumber(fontSizePt)} Tf`, `${formatNumber(Tm.a)} ${formatNumber(Tm.b)} ${formatNumber(Tm.c)} ${formatNumber(Tm.d)} ${formatNumber(shadowX)} ${formatNumber(shadowYPt)} Tm`, `(${encoded}) Tj`);
|
|
204
|
-
if (resetWordSpacingCmd)
|
|
205
|
-
shadowSequence.push(resetWordSpacingCmd);
|
|
206
|
-
shadowSequence.push("ET", "Q");
|
|
207
|
-
commands.push(...shadowSequence);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
this.appendVectorShadowLayers(commands, run, font, encoded, Tm, fontSizePt, fontName, wordSpacingCmd, resetWordSpacingCmd);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
else {
|
|
216
|
-
this.appendVectorShadowLayers(commands, run, font, encoded, Tm, fontSizePt, fontName, wordSpacingCmd, resetWordSpacingCmd);
|
|
217
|
-
}
|
|
161
|
+
this.appendVectorShadowLayers(commands, run, font, finalEncoded, Tm, fontSizePt, fontName, wordSpacingCmd, resetWordSpacingCmd);
|
|
218
162
|
}
|
|
219
163
|
}
|
|
220
164
|
else {
|
|
221
|
-
this.appendVectorShadowLayers(commands, run, font,
|
|
165
|
+
this.appendVectorShadowLayers(commands, run, font, finalEncoded, Tm, fontSizePt, fontName, wordSpacingCmd, resetWordSpacingCmd);
|
|
222
166
|
}
|
|
223
167
|
return commands;
|
|
224
168
|
}
|
|
225
|
-
appendVectorShadowLayers(commands, run,
|
|
169
|
+
appendVectorShadowLayers(commands, run, _font, encoded, Tm, fontSizePt, fontName, wordSpacingCmd, resetWordSpacingCmd) {
|
|
226
170
|
const shadows = run.textShadows ?? [];
|
|
227
171
|
for (const sh of shadows) {
|
|
228
|
-
if (!sh || !sh.color)
|
|
172
|
+
if (!sh || !sh.color)
|
|
229
173
|
continue;
|
|
230
|
-
}
|
|
231
174
|
const shadowX = this.coordinateTransformer.convertPxToPt(Tm.e + (sh.offsetX ?? 0));
|
|
232
175
|
const shadowLocalBaseline = Tm.f - this.coordinateTransformer.pageOffsetPx + (sh.offsetY ?? 0);
|
|
233
176
|
const shadowYPt = this.coordinateTransformer.pageHeightPt - this.coordinateTransformer.convertPxToPt(shadowLocalBaseline);
|
|
@@ -242,6 +185,23 @@ export class TextShadowRenderer {
|
|
|
242
185
|
}
|
|
243
186
|
}
|
|
244
187
|
}
|
|
188
|
+
function encodeSubsetText(run, subset) {
|
|
189
|
+
const glyphRun = run.glyphs;
|
|
190
|
+
if (!glyphRun)
|
|
191
|
+
return "";
|
|
192
|
+
let encoded = "";
|
|
193
|
+
for (const gid of glyphRun.glyphIds) {
|
|
194
|
+
const subsetGid = subset.gidMap.get(gid) ?? 0;
|
|
195
|
+
// Sequential subset CIDs are 2-bytes big-endian
|
|
196
|
+
encoded += String.fromCharCode((subsetGid >> 8) & 0xff, subsetGid & 0xff);
|
|
197
|
+
}
|
|
198
|
+
// We need escapePdfLiteral but it's not imported here yet.
|
|
199
|
+
// We can use a local simple version or import it.
|
|
200
|
+
return escapePdfLiteral(encoded);
|
|
201
|
+
}
|
|
202
|
+
function escapePdfLiteral(text) {
|
|
203
|
+
return text.replace(/([()\\])/g, "\\$1");
|
|
204
|
+
}
|
|
245
205
|
// Merge UnifiedFont metrics with its outline provider so glyph rasterization can work.
|
|
246
206
|
function mergeMetricsWithOutline(font) {
|
|
247
207
|
if (!font?.metrics || !font.program?.getGlyphOutline) {
|
|
@@ -68,6 +68,10 @@ export declare class TtfFontMetrics {
|
|
|
68
68
|
* Optional kerning map (left GID -> right GID -> adjustment in font units).
|
|
69
69
|
*/
|
|
70
70
|
readonly kerning?: KerningMap | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Optional hook that returns raw table data by its 4-character tag.
|
|
73
|
+
*/
|
|
74
|
+
readonly getRawTableData?: ((tag: string) => Uint8Array | null) | undefined;
|
|
71
75
|
constructor(metrics: TtfMetrics, glyphMetrics: Map<number, GlyphMetrics>, cmap: CmapData, headBBox?: readonly [number, number, number, number] | undefined,
|
|
72
76
|
/**
|
|
73
77
|
* Optional hook that returns a glyph's outline command sequence.
|
|
@@ -78,5 +82,9 @@ export declare class TtfFontMetrics {
|
|
|
78
82
|
/**
|
|
79
83
|
* Optional kerning map (left GID -> right GID -> adjustment in font units).
|
|
80
84
|
*/
|
|
81
|
-
kerning?: KerningMap | undefined
|
|
85
|
+
kerning?: KerningMap | undefined,
|
|
86
|
+
/**
|
|
87
|
+
* Optional hook that returns raw table data by its 4-character tag.
|
|
88
|
+
*/
|
|
89
|
+
getRawTableData?: ((tag: string) => Uint8Array | null) | undefined);
|
|
82
90
|
}
|
package/dist/src/types/fonts.js
CHANGED
|
@@ -11,12 +11,17 @@ export class TtfFontMetrics {
|
|
|
11
11
|
/**
|
|
12
12
|
* Optional kerning map (left GID -> right GID -> adjustment in font units).
|
|
13
13
|
*/
|
|
14
|
-
kerning
|
|
14
|
+
kerning,
|
|
15
|
+
/**
|
|
16
|
+
* Optional hook that returns raw table data by its 4-character tag.
|
|
17
|
+
*/
|
|
18
|
+
getRawTableData) {
|
|
15
19
|
this.metrics = metrics;
|
|
16
20
|
this.glyphMetrics = glyphMetrics;
|
|
17
21
|
this.cmap = cmap;
|
|
18
22
|
this.headBBox = headBBox;
|
|
19
23
|
this.getGlyphOutline = getGlyphOutline;
|
|
20
24
|
this.kerning = kerning;
|
|
25
|
+
this.getRawTableData = getRawTableData;
|
|
21
26
|
}
|
|
22
27
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderHtmlToPdf } from '../src/html-to-pdf.js';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
describe('Multi-Font Subsetting Verification', () => {
|
|
6
|
+
it('should generate a small PDF with Tinos and Arimo fonts', async () => {
|
|
7
|
+
process.env.PAGYRA_FONTS_DIR = path.resolve(process.cwd(), 'assets/fonts');
|
|
8
|
+
const html = `
|
|
9
|
+
<!DOCTYPE html>
|
|
10
|
+
<html>
|
|
11
|
+
<head>
|
|
12
|
+
<style>
|
|
13
|
+
.serif { font-family: 'Tinos', serif; font-size: 24px; }
|
|
14
|
+
.sans { font-family: 'Arimo', sans-serif; font-size: 24px; }
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<div class="serif">Serif Text: Hello World with Tinos! - ação, acentuação, coração</div>
|
|
19
|
+
<div class="sans">Sans-Serif Text: Hello World with Arimo! - ação, acentuação, coração</div>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
22
|
+
`;
|
|
23
|
+
const pdfBuffer = await renderHtmlToPdf({
|
|
24
|
+
html,
|
|
25
|
+
pageWidth: 600,
|
|
26
|
+
pageHeight: 800,
|
|
27
|
+
assetRootDir: path.resolve(process.cwd(), 'assets')
|
|
28
|
+
});
|
|
29
|
+
const size = pdfBuffer.byteLength;
|
|
30
|
+
console.log(`Generated Multi-Font PDF size: ${size} bytes`);
|
|
31
|
+
// Log if any fallback occurred (I'd need to intercept logs, but let's just check size)
|
|
32
|
+
fs.writeFileSync('test-subset-multi.pdf', pdfBuffer);
|
|
33
|
+
// Increased limit for multi-font Latin characters if they have high GIDs
|
|
34
|
+
expect(size).toBeLessThan(200 * 1024);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderHtmlToPdf } from '../src/html-to-pdf.js';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
describe('Font Subsetting Verification', () => {
|
|
6
|
+
it('should generate a small PDF with Tinos font', async () => {
|
|
7
|
+
// Point to assets relative to CWD (project root)
|
|
8
|
+
process.env.PAGYRA_FONTS_DIR = path.resolve(process.cwd(), 'assets/fonts');
|
|
9
|
+
const html = `
|
|
10
|
+
<!DOCTYPE html>
|
|
11
|
+
<html>
|
|
12
|
+
<head>
|
|
13
|
+
<style>
|
|
14
|
+
body { font-family: 'Tinos', serif; font-size: 24px; }
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
Hello World with Tinos!
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
21
|
+
`;
|
|
22
|
+
const pdfBuffer = await renderHtmlToPdf({
|
|
23
|
+
html,
|
|
24
|
+
pageWidth: 600,
|
|
25
|
+
pageHeight: 800,
|
|
26
|
+
assetRootDir: path.resolve(process.cwd(), 'assets')
|
|
27
|
+
});
|
|
28
|
+
const size = pdfBuffer.byteLength;
|
|
29
|
+
console.log(`Generated PDF size: ${size} bytes`);
|
|
30
|
+
fs.writeFileSync('test-subset-tinos.pdf', pdfBuffer);
|
|
31
|
+
// Subsetting "Hello World..." should be very small (< 50KB)
|
|
32
|
+
// Full font is ~2MB
|
|
33
|
+
expect(size).toBeLessThan(50 * 1024);
|
|
34
|
+
});
|
|
35
|
+
});
|