@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 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, and fills in
40
- a registered paint kind. Without a hook they are skipped.
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
- A face that is CFF-only, restricted (`fsType` 0x0002), bitmap-only (`fsType`
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 one whose hook threw or returned the wrong number of pixels;
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 and `screen`-unit paint.
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
- /** `view` is the page's `PdfPage.view`: what a `world`-unit paint in `commands` resolves through. */
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
- async function rasterGroup(ctx, cmd, state, reason) {
736
+ function inPageFrame(cmd, state) {
732
737
  const full = cmd.transform ? mat3.multiply(Float64Array.from(state.transform), cmd.transform) : state.transform;
733
- const wrapper = {
734
- kind: "group",
735
- transform: full,
736
- clip: cmd.clip,
737
- effects: cmd.effects,
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.reduce((a, b) => a + b, 0) > 0) out.op("d", `[${dash.map(num).join(" ")}]`, 0);
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
- strokeStyleOps(ctx, stroke.stroke, stroke.width);
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
- for (const cmd of cmds) {
1099
- const line = ctx.out.mark();
1100
- const object = ctx.objects.mark();
1101
- const skips = ctx.skipped.length;
1102
- try {
1103
- await writeCommand(ctx, cmd, state);
1104
- } catch (err) {
1105
- if (!(err instanceof NonFiniteNumber)) throw err;
1106
- ctx.out.rollback(line);
1107
- ctx.objects.nullUnset(object);
1108
- ctx.skipped.length = skips;
1109
- skip(ctx, `${cmd.kind} has a non-finite coordinate`);
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
- objects.streamDeflated(file, { Length1: emb.sfnt.byteLength }, new Uint8Array(emb.sfnt));
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(this.baseFont),
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(this.baseFont),
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(this.baseFont),
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 });