@weasel-js/pdf 1.11.1 → 1.12.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/README.md +18 -9
- package/dist/index.d.ts +8 -1
- package/dist/index.js +235 -43
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -36,8 +36,10 @@ masks for transparency), strokes (native, or outlined where PDF cannot express
|
|
|
36
36
|
them) with their markers, images, sprites, tiling patterns, text as Type0 fonts
|
|
37
37
|
with a ToUnicode map, color matrices, and isolated groups.
|
|
38
38
|
|
|
39
|
-
Handed to the `rasterize` hook: effects groups, shader commands,
|
|
40
|
-
|
|
39
|
+
Handed to the `rasterize` hook: effects groups, shader commands, fills in a
|
|
40
|
+
registered paint kind, and groups with a Porter-Duff `composite` other than
|
|
41
|
+
`source-over`, which PDF has no operator for. Without a hook the first three are
|
|
42
|
+
skipped and a composite is drawn source-over.
|
|
41
43
|
|
|
42
44
|
```ts
|
|
43
45
|
rasterize(commands, rect, scale, view) => Promise<{ width, height, data }>
|
|
@@ -45,9 +47,18 @@ rasterize(commands, rect, scale, view) => Promise<{ width, height, data }>
|
|
|
45
47
|
|
|
46
48
|
`rect` is the region in page points, `scale` the pixels per point, and `view`
|
|
47
49
|
the page's `view`, which `world`-unit paint in `commands` resolves through.
|
|
48
|
-
`data` is straight RGBA, top-down.
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
`data` is straight RGBA, top-down. The hook draws `commands` in order onto a
|
|
51
|
+
clear raster.
|
|
52
|
+
|
|
53
|
+
For a composite group, `commands` are the page up to and including that group,
|
|
54
|
+
so the raster is the group composited onto what it was drawn over. It is placed
|
|
55
|
+
on white paper over the group's bounds, or over the whole page for an operator
|
|
56
|
+
that clears what the group leaves empty (`source-in`, `source-out`,
|
|
57
|
+
`destination-in`, `destination-atop`, `copy`). An isolated group holding a
|
|
58
|
+
composite is rasterized whole.
|
|
59
|
+
|
|
60
|
+
Each TrueType face is embedded as a subset of the glyphs the document draws;
|
|
61
|
+
`subsetFonts: false` embeds it whole. A face that is CFF-only, restricted (`fsType` 0x0002), bitmap-only (`fsType`
|
|
51
62
|
0x0200), registered from a WOFF file, or unreadable is drawn as glyph outlines
|
|
52
63
|
instead, so its text is not selectable.
|
|
53
64
|
|
|
@@ -55,16 +66,14 @@ instead, so its text is not selectable.
|
|
|
55
66
|
|
|
56
67
|
Each entry is `{ kind: 'glyph' | 'command', detail }`. It reports:
|
|
57
68
|
|
|
58
|
-
- an effects group, shader command or registered-paint fill with no `rasterize` hook, or
|
|
69
|
+
- an effects group, shader command or registered-paint fill with no `rasterize` hook, a composite group with none (drawn source-over), or a raster whose hook threw or returned the wrong number of pixels;
|
|
59
70
|
- an image that could not be read, and a texture that cannot be drawn;
|
|
60
71
|
- a glyph nothing can draw: a color glyph, a slot the layout left empty, or one with no outline in the outline fallback;
|
|
61
72
|
- a command holding NaN or an infinity, which is left out whole;
|
|
62
|
-
- the approximations below, except the stroke width
|
|
73
|
+
- the approximations below, except the stroke width.
|
|
63
74
|
|
|
64
75
|
## Known approximations
|
|
65
76
|
|
|
66
77
|
- A `{ px }` stroke width under an anisotropic view uses the mean scale.
|
|
67
78
|
- A `vertexColors` stroke uses a single paint, and a `vertexColors` fill its solid fill.
|
|
68
|
-
- `screen`-unit paint inside a rasterized group is placed in the raster's pixels, not the page's points.
|
|
69
|
-
- A Porter-Duff composite is drawn source-over.
|
|
70
79
|
- An isolated group's color matrix alpha offset is not drawn.
|
package/dist/index.d.ts
CHANGED
|
@@ -35,11 +35,18 @@ interface RasterPixels {
|
|
|
35
35
|
/** Settings and host callbacks for `writePdf`; only `fonts` is required. */
|
|
36
36
|
interface WritePdfOptions {
|
|
37
37
|
fonts: PdfFontSource;
|
|
38
|
+
/** Default true: each face carries only the glyphs the document draws. False embeds each face whole. */
|
|
39
|
+
subsetFonts?: boolean;
|
|
38
40
|
title?: string;
|
|
39
41
|
/** Default true; unit tests pass false because jsdom realms lack `CompressionStream`. */
|
|
40
42
|
compress?: boolean;
|
|
41
43
|
readPixels?: (image: ImageBitmap | HTMLImageElement) => RasterPixels;
|
|
42
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Draws `commands`, which are in the page's frame, in order onto a clear raster of `rect` at `scale`
|
|
46
|
+
* pixels per point. `view` is the page's `PdfPage.view`: what a `world`-unit paint in `commands`
|
|
47
|
+
* resolves through. For a group with its own `composite`, `commands` are the page up to and including
|
|
48
|
+
* that group, so the raster is the group composited onto what it was drawn over.
|
|
49
|
+
*/
|
|
43
50
|
rasterize?: (commands: readonly DrawCommand[], rect: Rect, scale: number, view: GlMat3 | undefined) => Promise<RasterPixels>;
|
|
44
51
|
}
|
|
45
52
|
/** A glyph or command the writer left out of the file, with the reason. */
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { glyphOutline, fontEmbedding } from '@weasel-js/font';
|
|
2
2
|
import { mat3, fillsOpenSubpaths, transformPath, pathFromD, resolveCorners, forEachSegment, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, boundsOfPath } from '@weasel-js/geom';
|
|
3
|
-
import { fillHasInk, strokeHasInk, isGradientPaint, isSolidPaint, parseColor, resolveGradientStops, sampleResolvedStops, gradientGeometry } from '@weasel-js/paint';
|
|
4
|
-
import { isOwnComposite, SPRITE_STRIDE, markerDrawCommands, commandsBounds, composeColorMatrix, resolveStrokeLengths, textureRegistry, applyColorMatrix } from '@weasel-js/renderer';
|
|
5
|
-
import { strokeToPath, strokeInsets } from '@weasel-js/stroke';
|
|
3
|
+
import { fillHasInk, strokeHasInk, isGradientPaint, isSolidPaint, dashPattern, parseColor, resolveGradientStops, sampleResolvedStops, gradientGeometry } from '@weasel-js/paint';
|
|
4
|
+
import { isOwnComposite, readsBackdrop, SPRITE_STRIDE, markerDrawCommands, leavesUncovered, commandsBounds, composeColorMatrix, resolveStrokeLengths, textureRegistry, applyColorMatrix } from '@weasel-js/renderer';
|
|
5
|
+
import { strokeToPath, strokeInsets, dashLength } from '@weasel-js/stroke';
|
|
6
6
|
import { resolveTextStyle, cachedLayoutRuns, verticalAlignOffset, cachedPaintRuns, syntheticBoldOffset, emboldenGlyphD, SYNTHETIC_ITALIC_RADIANS } from '@weasel-js/text';
|
|
7
7
|
|
|
8
8
|
// src/fonts/fontSource.ts
|
|
@@ -692,7 +692,7 @@ function setClip(ctx, clip) {
|
|
|
692
692
|
function compositeName(op) {
|
|
693
693
|
return typeof op === "string" ? op : `${op.source}/${op.destination}`;
|
|
694
694
|
}
|
|
695
|
-
async function drawRaster(ctx, commands, box, state, reason, unhooked = reason) {
|
|
695
|
+
async function drawRaster(ctx, commands, box, state, reason, unhooked = reason, paper = false) {
|
|
696
696
|
const hook = ctx.opts.rasterize;
|
|
697
697
|
if (!hook) return skip(ctx, unhooked);
|
|
698
698
|
if (box.width <= 0 || box.height <= 0) return;
|
|
@@ -708,6 +708,11 @@ async function drawRaster(ctx, commands, box, state, reason, unhooked = reason)
|
|
|
708
708
|
const { out } = ctx;
|
|
709
709
|
out.op("q");
|
|
710
710
|
out.op("cm", ...mat3.toAffine(inverse));
|
|
711
|
+
if (paper) {
|
|
712
|
+
out.op("rg", 1, 1, 1);
|
|
713
|
+
out.op("re", box.x, box.y, box.width, box.height);
|
|
714
|
+
out.op("f");
|
|
715
|
+
}
|
|
711
716
|
writeRaster(ctx, { ...px, data: foldPixels(px.data, state) }, box, state.alpha);
|
|
712
717
|
out.op("Q");
|
|
713
718
|
}
|
|
@@ -728,16 +733,13 @@ function rasterFill(ctx, cmd, state, kind) {
|
|
|
728
733
|
const box = commandsBounds([wrapper], mat3.identity(), ctx);
|
|
729
734
|
return drawRaster(ctx, [wrapper], box, state, `paint kind ${kind}`, `paint kind ${kind} needs a raster`);
|
|
730
735
|
}
|
|
731
|
-
|
|
736
|
+
function inPageFrame(cmd, state) {
|
|
732
737
|
const full = cmd.transform ? mat3.multiply(Float64Array.from(state.transform), cmd.transform) : state.transform;
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
children: cmd.children
|
|
739
|
-
};
|
|
740
|
-
const box = commandsBounds([wrapper], mat3.identity(), ctx);
|
|
738
|
+
return { kind: "group", transform: full, clip: cmd.clip, effects: cmd.effects, children: cmd.children };
|
|
739
|
+
}
|
|
740
|
+
async function rasterGroup(ctx, cmd, state, reason) {
|
|
741
|
+
const wrapper = inPageFrame(cmd, state);
|
|
742
|
+
const box = commandsBounds([wrapper], mat3.identity(), ctx, RASTER_SCALE);
|
|
741
743
|
const grouped = {
|
|
742
744
|
transform: state.transform,
|
|
743
745
|
alpha: state.alpha * (cmd.alpha ?? 1),
|
|
@@ -745,6 +747,13 @@ async function rasterGroup(ctx, cmd, state, reason) {
|
|
|
745
747
|
};
|
|
746
748
|
await drawRaster(ctx, [wrapper], box, grouped, reason);
|
|
747
749
|
}
|
|
750
|
+
function compositeGroup(ctx, cmd, state) {
|
|
751
|
+
const composite = cmd.composite;
|
|
752
|
+
const box = leavesUncovered(composite) ? commandsBounds([inPageFrame(cmd, state)], mat3.identity(), ctx, RASTER_SCALE) : { x: 0, y: 0, width: ctx.width, height: ctx.height };
|
|
753
|
+
const reason = `composite ${compositeName(composite)}`;
|
|
754
|
+
const placed = { transform: state.transform, alpha: 1, colorMatrix: null };
|
|
755
|
+
return drawRaster(ctx, pageThrough(ctx.trail), box, placed, reason, reason, true);
|
|
756
|
+
}
|
|
748
757
|
async function isolatedGroup(ctx, cmd, state, inner) {
|
|
749
758
|
if (cmd.colorMatrix?.[19]) skip(ctx, "group color matrix alpha offset is not drawn");
|
|
750
759
|
const flip = pageFlip(ctx);
|
|
@@ -754,7 +763,7 @@ async function isolatedGroup(ctx, cmd, state, inner) {
|
|
|
754
763
|
const sub = { ...ctx, out: new ContentStream(), resources: new PageResources() };
|
|
755
764
|
sub.out.op("cm", ...mat3.toAffine(mat3.multiply(flip, Float64Array.from(inner.transform))));
|
|
756
765
|
if (cmd.clip) setClip(sub, cmd.clip);
|
|
757
|
-
await writeCommands(sub, cmd.children, { ...inner, alpha: 1 });
|
|
766
|
+
await writeCommands(sub, cmd.children, { ...inner, alpha: 1 }, cmd);
|
|
758
767
|
const form = ctx.objects.alloc();
|
|
759
768
|
ctx.objects.streamDeflated(
|
|
760
769
|
form,
|
|
@@ -775,10 +784,13 @@ async function isolatedGroup(ctx, cmd, state, inner) {
|
|
|
775
784
|
out.op("Q");
|
|
776
785
|
}
|
|
777
786
|
async function writeGroup(ctx, cmd, state) {
|
|
787
|
+
const hooked = ctx.opts.rasterize !== void 0;
|
|
778
788
|
if (cmd.composite && isOwnComposite(cmd.composite)) {
|
|
789
|
+
if (hooked) return compositeGroup(ctx, cmd, state);
|
|
779
790
|
skip(ctx, `composite ${compositeName(cmd.composite)} drawn as source-over`);
|
|
780
791
|
}
|
|
781
792
|
if (cmd.effects?.length) return rasterGroup(ctx, cmd, state, "effects");
|
|
793
|
+
if (cmd.isolate && hooked && readsBackdrop(cmd.children)) return rasterGroup(ctx, cmd, state, "composite");
|
|
782
794
|
const m = cmd.transform;
|
|
783
795
|
const inner = {
|
|
784
796
|
transform: m ? mat3.multiply(Float64Array.from(state.transform), m) : state.transform,
|
|
@@ -790,7 +802,7 @@ async function writeGroup(ctx, cmd, state) {
|
|
|
790
802
|
out.op("q");
|
|
791
803
|
if (m) out.op("cm", ...mat3.toAffine(m));
|
|
792
804
|
if (cmd.clip) setClip(ctx, cmd.clip);
|
|
793
|
-
await writeCommands(ctx, cmd.children, inner);
|
|
805
|
+
await writeCommands(ctx, cmd.children, inner, cmd);
|
|
794
806
|
out.op("Q");
|
|
795
807
|
}
|
|
796
808
|
var CAPS = { butt: 0, round: 1, square: 2 };
|
|
@@ -801,14 +813,15 @@ function isNative(stroke, width) {
|
|
|
801
813
|
const { start, end } = strokeInsets(stroke, width);
|
|
802
814
|
return start === 0 && end === 0;
|
|
803
815
|
}
|
|
804
|
-
function strokeStyleOps(ctx, stroke, width) {
|
|
816
|
+
function strokeStyleOps(ctx, stroke, width, length) {
|
|
805
817
|
const { out } = ctx;
|
|
806
818
|
out.op("w", width);
|
|
807
819
|
out.op("J", CAPS[stroke.cap ?? "butt"]);
|
|
808
820
|
out.op("j", JOINS[stroke.join ?? "miter"]);
|
|
809
821
|
out.op("M", stroke.miterLimit ?? 4);
|
|
810
|
-
const dash = stroke.dash
|
|
811
|
-
if (dash
|
|
822
|
+
const dash = dashPattern(stroke.dash, length);
|
|
823
|
+
if (dash === "non-finite") throw new NonFiniteNumber(`pdf: cannot write non-finite dash ${stroke.dash}`);
|
|
824
|
+
if (dash !== "solid") out.op("d", `[${dash.map(num).join(" ")}]`, 0);
|
|
812
825
|
}
|
|
813
826
|
function resolveStroke(path, raw, state) {
|
|
814
827
|
const { cmd } = resolveStrokeLengths(state.transform, { kind: "path", path, stroke: raw }, { quantize: false });
|
|
@@ -822,7 +835,7 @@ function writeNative(ctx, path, stroke, state, width) {
|
|
|
822
835
|
out.op("q");
|
|
823
836
|
out.op("gs", `/${ctx.resources.extGState(alpha, alpha)}`);
|
|
824
837
|
out.op("RG", ...rgb);
|
|
825
|
-
strokeStyleOps(ctx, stroke, width);
|
|
838
|
+
strokeStyleOps(ctx, stroke, width, stroke.dash ? dashLength(path) : void 0);
|
|
826
839
|
pathOps(out, path, "all");
|
|
827
840
|
out.op("S");
|
|
828
841
|
out.op("Q");
|
|
@@ -918,6 +931,14 @@ function textObject(ctx, seg, mode, dx, dy) {
|
|
|
918
931
|
out.op("TJ", `[${parts.join(" ")}]`);
|
|
919
932
|
out.op("ET");
|
|
920
933
|
}
|
|
934
|
+
function longestOutline(ctx, seg, dx, dy) {
|
|
935
|
+
let length = 0;
|
|
936
|
+
for (const { g } of seg.glyphs) {
|
|
937
|
+
const outline = isSpace(g.cp) ? null : placedOutline(ctx, g, dx, dy);
|
|
938
|
+
if (outline) length = Math.max(length, dashLength(outline));
|
|
939
|
+
}
|
|
940
|
+
return length;
|
|
941
|
+
}
|
|
921
942
|
async function paintOutlines(ctx, seg, cmd, dx, dy, state, paint) {
|
|
922
943
|
for (const { g } of seg.glyphs) {
|
|
923
944
|
if (isSpace(g.cp)) continue;
|
|
@@ -962,7 +983,8 @@ async function writeSegment(ctx, seg, cmd, dx, dy, state) {
|
|
|
962
983
|
if (paintsFill) out.op("rg", ...fillColor.rgb);
|
|
963
984
|
if (stroke && strokeColor) {
|
|
964
985
|
out.op("RG", ...strokeColor.rgb);
|
|
965
|
-
|
|
986
|
+
const length = stroke.stroke.dash ? longestOutline(ctx, seg, dx, dy) : void 0;
|
|
987
|
+
strokeStyleOps(ctx, stroke.stroke, stroke.width, length);
|
|
966
988
|
} else if (bold && fillColor) {
|
|
967
989
|
out.op("RG", ...fillColor.rgb);
|
|
968
990
|
out.op("w", 2 * bold.growth * first.fontSize);
|
|
@@ -1034,6 +1056,15 @@ async function writeText2(ctx, cmd, state) {
|
|
|
1034
1056
|
}
|
|
1035
1057
|
|
|
1036
1058
|
// src/drawCommands.ts
|
|
1059
|
+
function pageThrough(trail) {
|
|
1060
|
+
let tail = [];
|
|
1061
|
+
for (let i = trail.length - 1; i >= 0; i--) {
|
|
1062
|
+
const { cmds, at, group } = trail[i];
|
|
1063
|
+
const list = [...cmds.slice(0, at), ...tail.length ? tail : [cmds[at]]];
|
|
1064
|
+
tail = group ? [{ ...group, children: list }] : list;
|
|
1065
|
+
}
|
|
1066
|
+
return tail;
|
|
1067
|
+
}
|
|
1037
1068
|
function skip(ctx, detail) {
|
|
1038
1069
|
ctx.skipped.push({ kind: "command", detail });
|
|
1039
1070
|
}
|
|
@@ -1094,23 +1125,170 @@ async function writeCommand(ctx, cmd, state) {
|
|
|
1094
1125
|
else if (cmd.kind === "text") await writeText2(ctx, cmd, state);
|
|
1095
1126
|
else await writeShader(ctx, cmd, state);
|
|
1096
1127
|
}
|
|
1097
|
-
async function writeCommands(ctx, cmds, state) {
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1128
|
+
async function writeCommands(ctx, cmds, state, group) {
|
|
1129
|
+
const step = { cmds, at: 0, group };
|
|
1130
|
+
ctx.trail.push(step);
|
|
1131
|
+
try {
|
|
1132
|
+
for (; step.at < cmds.length; step.at++) await writeOne(ctx, cmds[step.at], state);
|
|
1133
|
+
} finally {
|
|
1134
|
+
ctx.trail.pop();
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
async function writeOne(ctx, cmd, state) {
|
|
1138
|
+
const line = ctx.out.mark();
|
|
1139
|
+
const object = ctx.objects.mark();
|
|
1140
|
+
const skips = ctx.skipped.length;
|
|
1141
|
+
try {
|
|
1142
|
+
await writeCommand(ctx, cmd, state);
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
if (!(err instanceof NonFiniteNumber)) throw err;
|
|
1145
|
+
ctx.out.rollback(line);
|
|
1146
|
+
ctx.objects.nullUnset(object);
|
|
1147
|
+
ctx.skipped.length = skips;
|
|
1148
|
+
skip(ctx, `${cmd.kind} has a non-finite coordinate`);
|
|
1111
1149
|
}
|
|
1112
1150
|
}
|
|
1113
1151
|
|
|
1152
|
+
// src/fonts/subset.ts
|
|
1153
|
+
var KEPT = ["cvt ", "fpgm", "glyf", "head", "hhea", "hmtx", "loca", "maxp", "prep"];
|
|
1154
|
+
var REQUIRED = ["glyf", "head", "hhea", "hmtx", "loca", "maxp"];
|
|
1155
|
+
var ARG_1_AND_2_ARE_WORDS = 1;
|
|
1156
|
+
var WE_HAVE_A_SCALE = 8;
|
|
1157
|
+
var MORE_COMPONENTS = 32;
|
|
1158
|
+
var WE_HAVE_AN_X_AND_Y_SCALE = 64;
|
|
1159
|
+
var WE_HAVE_A_TWO_BY_TWO = 128;
|
|
1160
|
+
function readTables(sfnt) {
|
|
1161
|
+
const view = new DataView(sfnt);
|
|
1162
|
+
const out = /* @__PURE__ */ new Map();
|
|
1163
|
+
for (let i = 0; i < view.getUint16(4); i++) {
|
|
1164
|
+
const at = 12 + i * 16;
|
|
1165
|
+
const tag = String.fromCharCode(...new Uint8Array(sfnt, at, 4));
|
|
1166
|
+
out.set(tag, new Uint8Array(sfnt, view.getUint32(at + 8), view.getUint32(at + 12)));
|
|
1167
|
+
}
|
|
1168
|
+
for (const tag of REQUIRED) if (!out.has(tag)) throw new Error(`pdf: a TrueType face has no "${tag}" table`);
|
|
1169
|
+
return out;
|
|
1170
|
+
}
|
|
1171
|
+
var viewOf = (b) => new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
1172
|
+
function glyphRanges(t, numGlyphs) {
|
|
1173
|
+
const loca = viewOf(t.get("loca"));
|
|
1174
|
+
const long = viewOf(t.get("head")).getInt16(50) === 1;
|
|
1175
|
+
const at = (i) => long ? loca.getUint32(i * 4) : loca.getUint16(i * 2) * 2;
|
|
1176
|
+
return Array.from({ length: numGlyphs }, (_, i) => [at(i), at(i + 1)]);
|
|
1177
|
+
}
|
|
1178
|
+
function componentsOf(glyph) {
|
|
1179
|
+
if (glyph.byteLength < 10) return [];
|
|
1180
|
+
const v = viewOf(glyph);
|
|
1181
|
+
if (v.getInt16(0) >= 0) return [];
|
|
1182
|
+
const out = [];
|
|
1183
|
+
let at = 10;
|
|
1184
|
+
for (; ; ) {
|
|
1185
|
+
const flags = v.getUint16(at);
|
|
1186
|
+
out.push(v.getUint16(at + 2));
|
|
1187
|
+
at += 4 + (flags & ARG_1_AND_2_ARE_WORDS ? 4 : 2);
|
|
1188
|
+
if (flags & WE_HAVE_A_SCALE) at += 2;
|
|
1189
|
+
else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) at += 4;
|
|
1190
|
+
else if (flags & WE_HAVE_A_TWO_BY_TWO) at += 8;
|
|
1191
|
+
if (!(flags & MORE_COMPONENTS)) return out;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
function closure(used, glyf, ranges) {
|
|
1195
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1196
|
+
const queue = [0, ...used];
|
|
1197
|
+
while (queue.length > 0) {
|
|
1198
|
+
const gid = queue.pop();
|
|
1199
|
+
if (keep.has(gid) || gid < 0 || gid >= ranges.length) continue;
|
|
1200
|
+
keep.add(gid);
|
|
1201
|
+
const [start, end] = ranges[gid];
|
|
1202
|
+
queue.push(...componentsOf(glyf.subarray(start, end)));
|
|
1203
|
+
}
|
|
1204
|
+
return keep;
|
|
1205
|
+
}
|
|
1206
|
+
function checksum(b) {
|
|
1207
|
+
const v = viewOf(b);
|
|
1208
|
+
let sum = 0;
|
|
1209
|
+
for (let i = 0; i < b.byteLength; i += 4) sum = sum + v.getUint32(i) >>> 0;
|
|
1210
|
+
return sum;
|
|
1211
|
+
}
|
|
1212
|
+
var pad4 = (n) => n + 3 & -4;
|
|
1213
|
+
function writeSfnt(tables) {
|
|
1214
|
+
const tags = [...tables.keys()].sort();
|
|
1215
|
+
const dirBytes = 12 + tags.length * 16;
|
|
1216
|
+
let total = dirBytes;
|
|
1217
|
+
for (const tag of tags) total += pad4(tables.get(tag).byteLength);
|
|
1218
|
+
const out = new Uint8Array(total);
|
|
1219
|
+
const v = viewOf(out);
|
|
1220
|
+
v.setUint32(0, 65536);
|
|
1221
|
+
v.setUint16(4, tags.length);
|
|
1222
|
+
const entrySelector = Math.floor(Math.log2(tags.length));
|
|
1223
|
+
const searchRange = 2 ** entrySelector * 16;
|
|
1224
|
+
v.setUint16(6, searchRange);
|
|
1225
|
+
v.setUint16(8, entrySelector);
|
|
1226
|
+
v.setUint16(10, tags.length * 16 - searchRange);
|
|
1227
|
+
let cursor = dirBytes;
|
|
1228
|
+
let headAt = 0;
|
|
1229
|
+
tags.forEach((tag, i) => {
|
|
1230
|
+
const data = tables.get(tag);
|
|
1231
|
+
const rec = 12 + i * 16;
|
|
1232
|
+
for (let c = 0; c < 4; c++) out[rec + c] = tag.charCodeAt(c);
|
|
1233
|
+
out.set(data, cursor);
|
|
1234
|
+
v.setUint32(rec + 4, checksum(out.subarray(cursor, cursor + pad4(data.byteLength))));
|
|
1235
|
+
v.setUint32(rec + 8, cursor);
|
|
1236
|
+
v.setUint32(rec + 12, data.byteLength);
|
|
1237
|
+
if (tag === "head") headAt = cursor;
|
|
1238
|
+
cursor += pad4(data.byteLength);
|
|
1239
|
+
});
|
|
1240
|
+
v.setUint32(headAt + 8, 2981146554 - checksum(out) >>> 0);
|
|
1241
|
+
return out;
|
|
1242
|
+
}
|
|
1243
|
+
function subsetTrueType(sfnt, used) {
|
|
1244
|
+
const t = readTables(sfnt);
|
|
1245
|
+
const numGlyphs = viewOf(t.get("maxp")).getUint16(4);
|
|
1246
|
+
const glyf = t.get("glyf");
|
|
1247
|
+
const ranges = glyphRanges(t, numGlyphs);
|
|
1248
|
+
const keep = closure(used, glyf, ranges);
|
|
1249
|
+
const count = Math.max(...keep) + 1;
|
|
1250
|
+
let glyfBytes = 0;
|
|
1251
|
+
for (const gid of keep) glyfBytes += pad4(ranges[gid][1] - ranges[gid][0]);
|
|
1252
|
+
const newGlyf = new Uint8Array(glyfBytes);
|
|
1253
|
+
const newLoca = new Uint8Array((count + 1) * 4);
|
|
1254
|
+
const loca = viewOf(newLoca);
|
|
1255
|
+
let cursor = 0;
|
|
1256
|
+
for (let gid = 0; gid < count; gid++) {
|
|
1257
|
+
loca.setUint32(gid * 4, cursor);
|
|
1258
|
+
if (!keep.has(gid)) continue;
|
|
1259
|
+
const [start, end] = ranges[gid];
|
|
1260
|
+
newGlyf.set(glyf.subarray(start, end), cursor);
|
|
1261
|
+
cursor += pad4(end - start);
|
|
1262
|
+
}
|
|
1263
|
+
loca.setUint32(count * 4, cursor);
|
|
1264
|
+
const hhea = t.get("hhea").slice();
|
|
1265
|
+
const hmtxIn = viewOf(t.get("hmtx"));
|
|
1266
|
+
const longMetrics = viewOf(hhea).getUint16(34);
|
|
1267
|
+
const hmtx = new Uint8Array(count * 4);
|
|
1268
|
+
for (let gid = 0; gid < count; gid++) {
|
|
1269
|
+
const advance = hmtxIn.getUint16(Math.min(gid, longMetrics - 1) * 4);
|
|
1270
|
+
const lsb = gid < longMetrics ? hmtxIn.getInt16(gid * 4 + 2) : hmtxIn.getInt16(longMetrics * 4 + (gid - longMetrics) * 2);
|
|
1271
|
+
viewOf(hmtx).setUint16(gid * 4, advance);
|
|
1272
|
+
viewOf(hmtx).setInt16(gid * 4 + 2, lsb);
|
|
1273
|
+
}
|
|
1274
|
+
viewOf(hhea).setUint16(34, count);
|
|
1275
|
+
const head = t.get("head").slice();
|
|
1276
|
+
viewOf(head).setUint32(8, 0);
|
|
1277
|
+
viewOf(head).setInt16(50, 1);
|
|
1278
|
+
const maxp = t.get("maxp").slice();
|
|
1279
|
+
viewOf(maxp).setUint16(4, count);
|
|
1280
|
+
const out = /* @__PURE__ */ new Map([
|
|
1281
|
+
["glyf", newGlyf],
|
|
1282
|
+
["head", head],
|
|
1283
|
+
["hhea", hhea],
|
|
1284
|
+
["hmtx", hmtx],
|
|
1285
|
+
["loca", newLoca],
|
|
1286
|
+
["maxp", maxp]
|
|
1287
|
+
]);
|
|
1288
|
+
for (const tag of KEPT) if (!out.has(tag) && t.has(tag)) out.set(tag, t.get(tag));
|
|
1289
|
+
return writeSfnt(out);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1114
1292
|
// src/fonts/toUnicode.ts
|
|
1115
1293
|
var MAX_PER_BLOCK = 100;
|
|
1116
1294
|
var hex4 = (n) => n.toString(16).toUpperCase().padStart(4, "0");
|
|
@@ -1148,15 +1326,24 @@ function baseFontName(e, family, weight, style) {
|
|
|
1148
1326
|
const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "");
|
|
1149
1327
|
return safe(e.postScriptName) || safe(`${family}-${weight}-${style}`);
|
|
1150
1328
|
}
|
|
1329
|
+
function subsetTag(gids) {
|
|
1330
|
+
let h = 2166136261;
|
|
1331
|
+
for (const gid of [...gids].sort((a, b) => a - b)) h = Math.imul(h ^ gid, 16777619) >>> 0;
|
|
1332
|
+
let tag = "";
|
|
1333
|
+
for (let i = 0; i < 6; i++, h = Math.floor(h / 26)) tag += String.fromCharCode(65 + h % 26);
|
|
1334
|
+
return tag;
|
|
1335
|
+
}
|
|
1151
1336
|
var Face = class {
|
|
1152
|
-
constructor(objects, emb, baseFont) {
|
|
1337
|
+
constructor(objects, emb, baseFont, subset) {
|
|
1153
1338
|
this.objects = objects;
|
|
1154
1339
|
this.emb = emb;
|
|
1155
1340
|
this.baseFont = baseFont;
|
|
1341
|
+
this.subset = subset;
|
|
1156
1342
|
}
|
|
1157
1343
|
objects;
|
|
1158
1344
|
emb;
|
|
1159
1345
|
baseFont;
|
|
1346
|
+
subset;
|
|
1160
1347
|
/** Glyph id to the first codepoint that asked for it. */
|
|
1161
1348
|
used = /* @__PURE__ */ new Map();
|
|
1162
1349
|
fontRef;
|
|
@@ -1184,11 +1371,13 @@ var Face = class {
|
|
|
1184
1371
|
const descriptor = objects.alloc();
|
|
1185
1372
|
const cid = objects.alloc();
|
|
1186
1373
|
const toUnicode = objects.alloc();
|
|
1187
|
-
|
|
1374
|
+
const program = this.subset ? subsetTrueType(emb.sfnt, this.used.keys()) : new Uint8Array(emb.sfnt);
|
|
1375
|
+
const fontName = this.subset ? `${subsetTag(this.used.keys())}+${this.baseFont}` : this.baseFont;
|
|
1376
|
+
objects.streamDeflated(file, { Length1: program.byteLength }, program);
|
|
1188
1377
|
objects.streamDeflated(toUnicode, {}, encoder.encode(toUnicodeCMap(this.used)));
|
|
1189
1378
|
objects.set(descriptor, {
|
|
1190
1379
|
Type: name("FontDescriptor"),
|
|
1191
|
-
FontName: name(
|
|
1380
|
+
FontName: name(fontName),
|
|
1192
1381
|
Flags: 4,
|
|
1193
1382
|
FontBBox: emb.bbox.map((v, i) => i < 2 ? Math.floor(v * k) : Math.ceil(v * k)),
|
|
1194
1383
|
ItalicAngle: emb.italicAngle,
|
|
@@ -1204,7 +1393,7 @@ var Face = class {
|
|
|
1204
1393
|
objects.set(cid, {
|
|
1205
1394
|
Type: name("Font"),
|
|
1206
1395
|
Subtype: name("CIDFontType2"),
|
|
1207
|
-
BaseFont: name(
|
|
1396
|
+
BaseFont: name(fontName),
|
|
1208
1397
|
CIDSystemInfo: { Registry: text("Adobe"), Ordering: text("Identity"), Supplement: 0 },
|
|
1209
1398
|
FontDescriptor: descriptor,
|
|
1210
1399
|
CIDToGIDMap: name("Identity"),
|
|
@@ -1213,7 +1402,7 @@ var Face = class {
|
|
|
1213
1402
|
objects.set(this.fontRef, {
|
|
1214
1403
|
Type: name("Font"),
|
|
1215
1404
|
Subtype: name("Type0"),
|
|
1216
|
-
BaseFont: name(
|
|
1405
|
+
BaseFont: name(fontName),
|
|
1217
1406
|
Encoding: name("Identity-H"),
|
|
1218
1407
|
DescendantFonts: [cid],
|
|
1219
1408
|
ToUnicode: toUnicode
|
|
@@ -1221,12 +1410,14 @@ var Face = class {
|
|
|
1221
1410
|
}
|
|
1222
1411
|
};
|
|
1223
1412
|
var DocumentFonts = class {
|
|
1224
|
-
constructor(objects, source) {
|
|
1413
|
+
constructor(objects, source, subset = true) {
|
|
1225
1414
|
this.objects = objects;
|
|
1226
1415
|
this.source = source;
|
|
1416
|
+
this.subset = subset;
|
|
1227
1417
|
}
|
|
1228
1418
|
objects;
|
|
1229
1419
|
source;
|
|
1420
|
+
subset;
|
|
1230
1421
|
faces = /* @__PURE__ */ new Map();
|
|
1231
1422
|
/** The embedded font for a face, or null to draw its glyphs as outlines. */
|
|
1232
1423
|
face(family, weight, style) {
|
|
@@ -1246,7 +1437,7 @@ var DocumentFonts = class {
|
|
|
1246
1437
|
return null;
|
|
1247
1438
|
}
|
|
1248
1439
|
if (!emb || emb.outlines !== "truetype" || !emb.embeddable) return null;
|
|
1249
|
-
return new Face(this.objects, emb, baseFontName(emb, family, weight, style));
|
|
1440
|
+
return new Face(this.objects, emb, baseFontName(emb, family, weight, style), this.subset);
|
|
1250
1441
|
}
|
|
1251
1442
|
/** Writes every face's objects; call before `PdfObjects.settle`. */
|
|
1252
1443
|
async finish() {
|
|
@@ -1266,7 +1457,7 @@ async function writePdfWith(pages, opts, deflate2) {
|
|
|
1266
1457
|
const info = objects.alloc();
|
|
1267
1458
|
const skipped = [];
|
|
1268
1459
|
const kids = [];
|
|
1269
|
-
const fonts = new DocumentFonts(objects, opts.fonts);
|
|
1460
|
+
const fonts = new DocumentFonts(objects, opts.fonts, opts.subsetFonts !== false);
|
|
1270
1461
|
for (const page of pages) {
|
|
1271
1462
|
const pageRef = objects.alloc();
|
|
1272
1463
|
const contentRef = objects.alloc();
|
|
@@ -1279,7 +1470,8 @@ async function writePdfWith(pages, opts, deflate2) {
|
|
|
1279
1470
|
skipped,
|
|
1280
1471
|
view: page.view,
|
|
1281
1472
|
width: page.width,
|
|
1282
|
-
height: page.height
|
|
1473
|
+
height: page.height,
|
|
1474
|
+
trail: []
|
|
1283
1475
|
};
|
|
1284
1476
|
ctx.out.op("cm", ...mat3.toAffine(pageFlip(page)));
|
|
1285
1477
|
await writeCommands(ctx, page.commands, { transform: mat3.identity(), alpha: 1, colorMatrix: null });
|