web-doc 0.9.0 → 0.10.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.
@@ -0,0 +1,163 @@
1
+ /*
2
+ * Glyph names and the code points they stand for: the Adobe Glyph List For
3
+ * New Fonts 1.7 (aglfn.txt), and from the Adobe Glyph List 2.0
4
+ * (glyphlist.txt) the ligatures, superior figures and no-break space that
5
+ * font subsets commonly name, from https://github.com/adobe-type-tools/agl-aglfn.
6
+ * Each is listed as a name and its code point in hexadecimal.
7
+ *
8
+ * Copyright 2002-2019 Adobe (http://www.adobe.com/).
9
+ *
10
+ * Redistribution and use in source and binary forms, with or
11
+ * without modification, are permitted provided that the
12
+ * following conditions are met:
13
+ *
14
+ * Redistributions of source code must retain the above
15
+ * copyright notice, this list of conditions and the following
16
+ * disclaimer.
17
+ *
18
+ * Redistributions in binary form must reproduce the above
19
+ * copyright notice, this list of conditions and the following
20
+ * disclaimer in the documentation and/or other materials
21
+ * provided with the distribution.
22
+ *
23
+ * Neither the name of Adobe nor the names of its contributors
24
+ * may be used to endorse or promote products derived from this
25
+ * software without specific prior written permission.
26
+ *
27
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
28
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
29
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
30
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
31
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
32
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
33
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
34
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
35
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
36
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
37
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
38
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
39
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40
+ */
41
+ const AGLFN = `
42
+ A 0041 AE 00C6 AEacute 01FC Aacute 00C1 Abreve 0102 Acircumflex 00C2
43
+ Adieresis 00C4 Agrave 00C0 Alpha 0391 Alphatonos 0386 Amacron 0100
44
+ Aogonek 0104 Aring 00C5 Aringacute 01FA Atilde 00C3 B 0042 Beta 0392 C 0043
45
+ Cacute 0106 Ccaron 010C Ccedilla 00C7 Ccircumflex 0108 Cdotaccent 010A
46
+ Chi 03A7 D 0044 Dcaron 010E Dcroat 0110 Delta 2206 E 0045 Eacute 00C9
47
+ Ebreve 0114 Ecaron 011A Ecircumflex 00CA Edieresis 00CB Edotaccent 0116
48
+ Egrave 00C8 Emacron 0112 Eng 014A Eogonek 0118 Epsilon 0395
49
+ Epsilontonos 0388 Eta 0397 Etatonos 0389 Eth 00D0 Euro 20AC F 0046 G 0047
50
+ Gamma 0393 Gbreve 011E Gcaron 01E6 Gcircumflex 011C Gdotaccent 0120 H 0048
51
+ H18533 25CF H18543 25AA H18551 25AB H22073 25A1 Hbar 0126 Hcircumflex 0124
52
+ I 0049 IJ 0132 Iacute 00CD Ibreve 012C Icircumflex 00CE Idieresis 00CF
53
+ Idotaccent 0130 Ifraktur 2111 Igrave 00CC Imacron 012A Iogonek 012E
54
+ Iota 0399 Iotadieresis 03AA Iotatonos 038A Itilde 0128 J 004A
55
+ Jcircumflex 0134 K 004B Kappa 039A L 004C Lacute 0139 Lambda 039B
56
+ Lcaron 013D Ldot 013F Lslash 0141 M 004D Mu 039C N 004E Nacute 0143
57
+ Ncaron 0147 Ntilde 00D1 Nu 039D O 004F OE 0152 Oacute 00D3 Obreve 014E
58
+ Ocircumflex 00D4 Odieresis 00D6 Ograve 00D2 Ohorn 01A0 Ohungarumlaut 0150
59
+ Omacron 014C Omega 2126 Omegatonos 038F Omicron 039F Omicrontonos 038C
60
+ Oslash 00D8 Oslashacute 01FE Otilde 00D5 P 0050 Phi 03A6 Pi 03A0 Psi 03A8
61
+ Q 0051 R 0052 Racute 0154 Rcaron 0158 Rfraktur 211C Rho 03A1 S 0053
62
+ SF010000 250C SF020000 2514 SF030000 2510 SF040000 2518 SF050000 253C
63
+ SF060000 252C SF070000 2534 SF080000 251C SF090000 2524 SF100000 2500
64
+ SF110000 2502 SF190000 2561 SF200000 2562 SF210000 2556 SF220000 2555
65
+ SF230000 2563 SF240000 2551 SF250000 2557 SF260000 255D SF270000 255C
66
+ SF280000 255B SF360000 255E SF370000 255F SF380000 255A SF390000 2554
67
+ SF400000 2569 SF410000 2566 SF420000 2560 SF430000 2550 SF440000 256C
68
+ SF450000 2567 SF460000 2568 SF470000 2564 SF480000 2565 SF490000 2559
69
+ SF500000 2558 SF510000 2552 SF520000 2553 SF530000 256B SF540000 256A
70
+ Sacute 015A Scaron 0160 Scedilla 015E Scircumflex 015C Sigma 03A3 T 0054
71
+ Tau 03A4 Tbar 0166 Tcaron 0164 Theta 0398 Thorn 00DE U 0055 Uacute 00DA
72
+ Ubreve 016C Ucircumflex 00DB Udieresis 00DC Ugrave 00D9 Uhorn 01AF
73
+ Uhungarumlaut 0170 Umacron 016A Uogonek 0172 Upsilon 03A5 Upsilon1 03D2
74
+ Upsilondieresis 03AB Upsilontonos 038E Uring 016E Utilde 0168 V 0056 W 0057
75
+ Wacute 1E82 Wcircumflex 0174 Wdieresis 1E84 Wgrave 1E80 X 0058 Xi 039E
76
+ Y 0059 Yacute 00DD Ycircumflex 0176 Ydieresis 0178 Ygrave 1EF2 Z 005A
77
+ Zacute 0179 Zcaron 017D Zdotaccent 017B Zeta 0396 a 0061 aacute 00E1
78
+ abreve 0103 acircumflex 00E2 acute 00B4 acutecomb 0301 adieresis 00E4
79
+ ae 00E6 aeacute 01FD agrave 00E0 aleph 2135 alpha 03B1 alphatonos 03AC
80
+ amacron 0101 ampersand 0026 angle 2220 angleleft 2329 angleright 232A
81
+ anoteleia 0387 aogonek 0105 approxequal 2248 aring 00E5 aringacute 01FB
82
+ arrowboth 2194 arrowdblboth 21D4 arrowdbldown 21D3 arrowdblleft 21D0
83
+ arrowdblright 21D2 arrowdblup 21D1 arrowdown 2193 arrowleft 2190
84
+ arrowright 2192 arrowup 2191 arrowupdn 2195 arrowupdnbse 21A8
85
+ asciicircum 005E asciitilde 007E asterisk 002A asteriskmath 2217 at 0040
86
+ atilde 00E3 b 0062 backslash 005C bar 007C beta 03B2 block 2588
87
+ braceleft 007B braceright 007D bracketleft 005B bracketright 005D breve 02D8
88
+ brokenbar 00A6 bullet 2022 c 0063 cacute 0107 caron 02C7 carriagereturn 21B5
89
+ ccaron 010D ccedilla 00E7 ccircumflex 0109 cdotaccent 010B cedilla 00B8
90
+ cent 00A2 chi 03C7 circle 25CB circlemultiply 2297 circleplus 2295
91
+ circumflex 02C6 club 2663 colon 003A colonmonetary 20A1 comma 002C
92
+ congruent 2245 copyright 00A9 currency 00A4 d 0064 dagger 2020
93
+ daggerdbl 2021 dcaron 010F dcroat 0111 degree 00B0 delta 03B4 diamond 2666
94
+ dieresis 00A8 dieresistonos 0385 divide 00F7 dkshade 2593 dnblock 2584
95
+ dollar 0024 dong 20AB dotaccent 02D9 dotbelowcomb 0323 dotlessi 0131
96
+ dotmath 22C5 e 0065 eacute 00E9 ebreve 0115 ecaron 011B ecircumflex 00EA
97
+ edieresis 00EB edotaccent 0117 egrave 00E8 eight 0038 element 2208
98
+ ellipsis 2026 emacron 0113 emdash 2014 emptyset 2205 endash 2013 eng 014B
99
+ eogonek 0119 epsilon 03B5 epsilontonos 03AD equal 003D equivalence 2261
100
+ estimated 212E eta 03B7 etatonos 03AE eth 00F0 exclam 0021 exclamdbl 203C
101
+ exclamdown 00A1 existential 2203 f 0066 female 2640 figuredash 2012
102
+ filledbox 25A0 filledrect 25AC five 0035 fiveeighths 215D florin 0192
103
+ four 0034 fraction 2044 franc 20A3 g 0067 gamma 03B3 gbreve 011F gcaron 01E7
104
+ gcircumflex 011D gdotaccent 0121 germandbls 00DF gradient 2207 grave 0060
105
+ gravecomb 0300 greater 003E greaterequal 2265 guillemotleft 00AB
106
+ guillemotright 00BB guilsinglleft 2039 guilsinglright 203A h 0068 hbar 0127
107
+ hcircumflex 0125 heart 2665 hookabovecomb 0309 house 2302 hungarumlaut 02DD
108
+ hyphen 002D i 0069 iacute 00ED ibreve 012D icircumflex 00EE idieresis 00EF
109
+ igrave 00EC ij 0133 imacron 012B infinity 221E integral 222B integralbt 2321
110
+ integraltp 2320 intersection 2229 invbullet 25D8 invcircle 25D9
111
+ invsmileface 263B iogonek 012F iota 03B9 iotadieresis 03CA
112
+ iotadieresistonos 0390 iotatonos 03AF itilde 0129 j 006A jcircumflex 0135
113
+ k 006B kappa 03BA kgreenlandic 0138 l 006C lacute 013A lambda 03BB
114
+ lcaron 013E ldot 0140 less 003C lessequal 2264 lfblock 258C lira 20A4
115
+ logicaland 2227 logicalnot 00AC logicalor 2228 longs 017F lozenge 25CA
116
+ lslash 0142 ltshade 2591 m 006D macron 00AF male 2642 minus 2212 minute 2032
117
+ mu 00B5 multiply 00D7 musicalnote 266A musicalnotedbl 266B n 006E
118
+ nacute 0144 napostrophe 0149 ncaron 0148 nine 0039 notelement 2209
119
+ notequal 2260 notsubset 2284 ntilde 00F1 nu 03BD numbersign 0023 o 006F
120
+ oacute 00F3 obreve 014F ocircumflex 00F4 odieresis 00F6 oe 0153 ogonek 02DB
121
+ ograve 00F2 ohorn 01A1 ohungarumlaut 0151 omacron 014D omega 03C9
122
+ omega1 03D6 omegatonos 03CE omicron 03BF omicrontonos 03CC one 0031
123
+ onedotenleader 2024 oneeighth 215B onehalf 00BD onequarter 00BC
124
+ onethird 2153 openbullet 25E6 ordfeminine 00AA ordmasculine 00BA
125
+ orthogonal 221F oslash 00F8 oslashacute 01FF otilde 00F5 p 0070
126
+ paragraph 00B6 parenleft 0028 parenright 0029 partialdiff 2202 percent 0025
127
+ period 002E periodcentered 00B7 perpendicular 22A5 perthousand 2030
128
+ peseta 20A7 phi 03C6 phi1 03D5 pi 03C0 plus 002B plusminus 00B1
129
+ prescription 211E product 220F propersubset 2282 propersuperset 2283
130
+ proportional 221D psi 03C8 q 0071 question 003F questiondown 00BF
131
+ quotedbl 0022 quotedblbase 201E quotedblleft 201C quotedblright 201D
132
+ quoteleft 2018 quotereversed 201B quoteright 2019 quotesinglbase 201A
133
+ quotesingle 0027 r 0072 racute 0155 radical 221A rcaron 0159
134
+ reflexsubset 2286 reflexsuperset 2287 registered 00AE revlogicalnot 2310
135
+ rho 03C1 ring 02DA rtblock 2590 s 0073 sacute 015B scaron 0161 scedilla 015F
136
+ scircumflex 015D second 2033 section 00A7 semicolon 003B seven 0037
137
+ seveneighths 215E shade 2592 sigma 03C3 sigma1 03C2 similar 223C six 0036
138
+ slash 002F smileface 263A space 0020 spade 2660 sterling 00A3 suchthat 220B
139
+ summation 2211 sun 263C t 0074 tau 03C4 tbar 0167 tcaron 0165 therefore 2234
140
+ theta 03B8 theta1 03D1 thorn 00FE three 0033 threeeighths 215C
141
+ threequarters 00BE tilde 02DC tildecomb 0303 tonos 0384 trademark 2122
142
+ triagdn 25BC triaglf 25C4 triagrt 25BA triagup 25B2 two 0032
143
+ twodotenleader 2025 twothirds 2154 u 0075 uacute 00FA ubreve 016D
144
+ ucircumflex 00FB udieresis 00FC ugrave 00F9 uhorn 01B0 uhungarumlaut 0171
145
+ umacron 016B underscore 005F underscoredbl 2017 union 222A universal 2200
146
+ uogonek 0173 upblock 2580 upsilon 03C5 upsilondieresis 03CB
147
+ upsilondieresistonos 03B0 upsilontonos 03CD uring 016F utilde 0169 v 0076
148
+ w 0077 wacute 1E83 wcircumflex 0175 wdieresis 1E85 weierstrass 2118
149
+ wgrave 1E81 x 0078 xi 03BE y 0079 yacute 00FD ycircumflex 0177
150
+ ydieresis 00FF yen 00A5 ygrave 1EF3 z 007A zacute 017A zcaron 017E
151
+ zdotaccent 017C zero 0030 zeta 03B6
152
+ `;
153
+ const AGL_EXTRAS = `
154
+ fi FB01 fl FB02 ff FB00 ffi FB03 ffl FB04 onesuperior 00B9 twosuperior 00B2
155
+ threesuperior 00B3 nbspace 00A0
156
+ `;
157
+ /** Code points by glyph name. */
158
+ export const GLYPH_UNICODE = new Map(`${AGLFN} ${AGL_EXTRAS}`
159
+ .trim()
160
+ .split(/\s+/)
161
+ .flatMap((word, index, words) => index % 2 === 0
162
+ ? [[word, Number.parseInt(words[index + 1], 16)]]
163
+ : []));
@@ -94,6 +94,8 @@ export function createPdfEditHandler(host) {
94
94
  const { pageIndex, elementIds, scale } = payload;
95
95
  return engine().renderPageWithout(pageIndex, elementIds, scale);
96
96
  }
97
+ case "edit-text-font":
98
+ return engine().textFont(payload.id);
97
99
  case "edit-page-layout":
98
100
  return engine().pageLayout(payload.pageIndex);
99
101
  case "edit-dispose":
@@ -0,0 +1,14 @@
1
+ import { type CffFont } from "./cff.js";
2
+ /**
3
+ * The OpenType file, or `undefined` when no glyph has a code point or the
4
+ * program's name cannot be made one a browser accepts.
5
+ */
6
+ export declare function cffOpenType(program: Uint8Array, font: CffFont, unicode: ReadonlyMap<number, number>): Uint8Array | undefined;
7
+ /**
8
+ * An embedded TrueType or OpenType program with the OS/2, name and post
9
+ * tables it lacks, laid out on four-byte boundaries; `undefined` when its
10
+ * table directory or core tables are broken.
11
+ */
12
+ export declare function completeSfnt(program: Uint8Array, fontName: string): Uint8Array | undefined;
13
+ /** The sfnt file maps Unicode: a cmap subtable for platform 0, or for Windows Unicode BMP or full repertoire. */
14
+ export declare function hasUnicodeCmap(program: Uint8Array): boolean;
@@ -0,0 +1,317 @@
1
+ import { cffWithSafeName } from "./cff.js";
2
+ /*
3
+ * Fonts a browser loads with `FontFace`. A PDF's CFF program gets a minimal
4
+ * OpenType font around it: the CFF table as the PDF holds it, a Unicode cmap,
5
+ * and the tables a font loader requires (head, hhea, hmtx, maxp, name, OS/2,
6
+ * post), filled from what the CFF says. A PDF's TrueType program keeps its
7
+ * tables; subsetting tools leave out the OS/2, name and post tables and pack
8
+ * tables off four-byte boundaries, which browsers refuse, so those tables are
9
+ * added and the file laid out again. Nothing in a program's tables changes.
10
+ */
11
+ const SFNT_TRUETYPE = 0x00010000;
12
+ const SFNT_CFF = 0x4f54544f; // "OTTO"
13
+ /**
14
+ * The OpenType file, or `undefined` when no glyph has a code point or the
15
+ * program's name cannot be made one a browser accepts.
16
+ */
17
+ export function cffOpenType(program, font, unicode) {
18
+ const safe = cffWithSafeName(program);
19
+ if (unicode.size === 0 || !safe)
20
+ return undefined;
21
+ const em = font.unitsPerEm;
22
+ const [xMin, yMin, xMax, yMax] = fontBox(font.bbox, em);
23
+ const glyphCount = font.widths.length;
24
+ const widths = font.widths.map((width) => Math.max(0, Math.min(0xffff, Math.round(width))));
25
+ const codes = [...unicode.keys()].sort((a, b) => a - b);
26
+ const tables = new Map([
27
+ ["CFF ", safe],
28
+ ["OS/2", os2(em, widths, codes, yMin, yMax)],
29
+ ["cmap", cmap(unicode, codes)],
30
+ ["head", head(em, xMin, yMin, xMax, yMax)],
31
+ ["hhea", hhea(widths, glyphCount, xMin, yMin, xMax, yMax)],
32
+ ["hmtx", concat(widths.map((width) => bytes(u16(width), i16(0))))],
33
+ ["maxp", bytes(u32(0x00005000), u16(glyphCount))],
34
+ ["name", names(postScriptName(font.name))],
35
+ ["post", post(em)],
36
+ ]);
37
+ return assemble(tables, SFNT_CFF);
38
+ }
39
+ /**
40
+ * The font box in int16 font units, in order; a font without one, which CFF
41
+ * allows, gets a box of the usual proportions so its line has a height.
42
+ */
43
+ function fontBox(box, em) {
44
+ const clamp = (value) => Math.max(-0x8000, Math.min(0x7fff, Math.round(value)));
45
+ const [x0 = 0, y0 = 0, x1 = 0, y1 = 0] = box.map(clamp);
46
+ if (y1 <= y0)
47
+ return [0, clamp(-em * 0.2), clamp(em), clamp(em * 0.8)];
48
+ return [Math.min(x0, x1), y0, Math.max(x0, x1), y1];
49
+ }
50
+ /**
51
+ * An embedded TrueType or OpenType program with the OS/2, name and post
52
+ * tables it lacks, laid out on four-byte boundaries; `undefined` when its
53
+ * table directory or core tables are broken.
54
+ */
55
+ export function completeSfnt(program, fontName) {
56
+ const tables = sfntTables(program);
57
+ const headTable = tables?.get("head");
58
+ const hheaTable = tables?.get("hhea");
59
+ const maxpTable = tables?.get("maxp");
60
+ const hmtxTable = tables?.get("hmtx");
61
+ if (!tables ||
62
+ !tables.has("cmap") ||
63
+ !headTable ||
64
+ headTable.length < 54 ||
65
+ !hheaTable ||
66
+ hheaTable.length < 36 ||
67
+ !maxpTable ||
68
+ maxpTable.length < 6 ||
69
+ !hmtxTable)
70
+ return undefined;
71
+ const view = (table) => new DataView(table.buffer, table.byteOffset, table.byteLength);
72
+ const headView = view(headTable);
73
+ const em = headView.getUint16(18);
74
+ const yMin = headView.getInt16(38);
75
+ const yMax = headView.getInt16(42);
76
+ // The adjustment is the file's, so it is worked out again.
77
+ const ownHead = headTable.slice();
78
+ ownHead.fill(0, 8, 12);
79
+ tables.set("head", ownHead);
80
+ if (!tables.has("OS/2")) {
81
+ const metrics = view(hheaTable).getUint16(34);
82
+ const widths = Array.from({ length: Math.min(metrics, hmtxTable.length >> 2) }, (_, glyph) => view(hmtxTable).getUint16(glyph * 4));
83
+ tables.set("OS/2", os2(em, widths, [], yMin, yMax));
84
+ }
85
+ if (!tables.has("name"))
86
+ tables.set("name", names(postScriptName(fontName)));
87
+ if (!tables.has("post"))
88
+ tables.set("post", post(em));
89
+ const version = view(program).getUint32(0);
90
+ return assemble(tables, version === SFNT_CFF ? SFNT_CFF : SFNT_TRUETYPE);
91
+ }
92
+ /** The sfnt file maps Unicode: a cmap subtable for platform 0, or for Windows Unicode BMP or full repertoire. */
93
+ export function hasUnicodeCmap(program) {
94
+ const table = sfntTables(program)?.get("cmap");
95
+ if (!table || table.length < 4)
96
+ return false;
97
+ const view = new DataView(table.buffer, table.byteOffset, table.byteLength);
98
+ const count = Math.min(view.getUint16(2), (table.length - 4) >> 3);
99
+ for (let index = 0; index < count; index += 1) {
100
+ const platform = view.getUint16(4 + index * 8);
101
+ const encoding = view.getUint16(6 + index * 8);
102
+ if (platform === 0 ||
103
+ (platform === 3 && (encoding === 1 || encoding === 10)))
104
+ return true;
105
+ }
106
+ return false;
107
+ }
108
+ /** The tables of an sfnt file by tag, or `undefined` when its directory is broken. */
109
+ function sfntTables(program) {
110
+ if (program.length < 12)
111
+ return undefined;
112
+ const view = new DataView(program.buffer, program.byteOffset, program.byteLength);
113
+ const count = view.getUint16(4);
114
+ if (count === 0 || 12 + count * 16 > program.length)
115
+ return undefined;
116
+ const tables = new Map();
117
+ for (let index = 0; index < count; index += 1) {
118
+ const record = 12 + index * 16;
119
+ const tag = String.fromCharCode(...program.subarray(record, record + 4));
120
+ const offset = view.getUint32(record + 8);
121
+ const length = view.getUint32(record + 12);
122
+ if (tables.has(tag) || offset + length > program.length)
123
+ return undefined;
124
+ tables.set(tag, program.subarray(offset, offset + length));
125
+ }
126
+ return tables;
127
+ }
128
+ /** The PostScript name without a subset tag, in the characters a name table takes. */
129
+ function postScriptName(name) {
130
+ return (name
131
+ .replace(/^[A-Z]{6}\+/, "")
132
+ .replace(/[^\x21-\x7e]|[[\](){}<>/%]/g, "")
133
+ .slice(0, 63) || "Embedded");
134
+ }
135
+ function post(em) {
136
+ // Version 3: no glyph names.
137
+ return bytes(u32(0x00030000), u32(0), i16(-Math.round(em / 10)), i16(Math.round(em / 20)), u32(0), u32(0), u32(0), u32(0), u32(0));
138
+ }
139
+ function head(em, xMin, yMin, xMax, yMax) {
140
+ return bytes(u32(0x00010000), u32(0x00010000), u32(0), // checkSumAdjustment, set once the font is assembled
141
+ u32(0x5f0f3cf5), u16(0x000b), u16(em), u32(0), u32(0), u32(0), u32(0), i16(xMin), i16(yMin), i16(xMax), i16(yMax), u16(0), u16(8), i16(2), i16(0), i16(0));
142
+ }
143
+ function hhea(widths, glyphCount, xMin, yMin, xMax, yMax) {
144
+ return bytes(u32(0x00010000), i16(yMax), i16(yMin), i16(0), u16(widths.reduce((most, width) => Math.max(most, width), 0)), i16(xMin), i16(0), i16(xMax), i16(1), i16(0), i16(0), i16(0), i16(0), i16(0), i16(0), i16(0), u16(glyphCount));
145
+ }
146
+ function os2(em, widths, codes, yMin, yMax) {
147
+ const scaled = (fraction) => Math.round(em * fraction);
148
+ const drawn = widths.filter((width) => width > 0);
149
+ const average = drawn.length
150
+ ? Math.round(drawn.reduce((sum, width) => sum + width, 0) / drawn.length)
151
+ : 0;
152
+ return bytes(u16(4), i16(average), u16(400), u16(5), u16(0), i16(scaled(0.65)), i16(scaled(0.6)), i16(0), i16(scaled(0.075)), i16(scaled(0.65)), i16(scaled(0.6)), i16(0), i16(scaled(0.35)), i16(scaled(0.05)), i16(scaled(0.25)), i16(0), new Array(10).fill(0), u32(0), u32(0), u32(0), u32(0), [0x57, 0x44, 0x4f, 0x43], // "WDOC"
153
+ u16(0x0040), // regular
154
+ u16(Math.min(codes[0] ?? 0x20, 0xffff)), u16(Math.min(codes.at(-1) ?? 0xffff, 0xffff)), i16(yMax), i16(yMin), i16(0), u16(Math.max(0, yMax)), u16(Math.max(0, -yMin)), u32(1), u32(0), i16(scaled(0.5)), i16(scaled(0.7)), u16(0), u16(32), u16(0));
155
+ }
156
+ /**
157
+ * Format 4 for the BMP, and format 12 as well when a code point lies beyond
158
+ * it or the BMP takes more segments than format 4 holds. Records are in
159
+ * platform and encoding order, as the specification requires.
160
+ */
161
+ function cmap(unicode, codes) {
162
+ const basic = codes.filter((code) => code <= 0xffff);
163
+ const format4 = cmapFormat4(unicode, basic);
164
+ const format12 = !format4 || codes.length > basic.length
165
+ ? cmapFormat12(unicode, codes)
166
+ : undefined;
167
+ const subtables = [
168
+ ...(format4 ? [{ platform: 0, encoding: 3, data: format4 }] : []),
169
+ ...(format12 ? [{ platform: 0, encoding: 4, data: format12 }] : []),
170
+ ...(format4 ? [{ platform: 3, encoding: 1, data: format4 }] : []),
171
+ ...(format12 ? [{ platform: 3, encoding: 10, data: format12 }] : []),
172
+ ];
173
+ // Subtables shared by two records are written once.
174
+ const unique = [...new Set(subtables.map((subtable) => subtable.data))];
175
+ const headerSize = 4 + subtables.length * 8;
176
+ const offsets = new Map();
177
+ let offset = headerSize;
178
+ for (const data of unique) {
179
+ offsets.set(data, offset);
180
+ offset += data.length;
181
+ }
182
+ return concat([
183
+ bytes(u16(0), u16(subtables.length)),
184
+ ...subtables.map(({ platform, encoding, data }) => bytes(u16(platform), u16(encoding), u32(offsets.get(data)))),
185
+ ...unique,
186
+ ]);
187
+ }
188
+ /** `undefined` when the segments overflow the subtable's 16-bit length. */
189
+ function cmapFormat4(unicode, codes) {
190
+ // Runs of consecutive code points on consecutive glyphs share one delta.
191
+ const segments = [];
192
+ for (const code of codes) {
193
+ const glyph = unicode.get(code);
194
+ const last = segments.at(-1);
195
+ if (last &&
196
+ code === last.end + 1 &&
197
+ glyph === last.glyph + (code - last.start))
198
+ last.end = code;
199
+ else
200
+ segments.push({ start: code, end: code, glyph });
201
+ }
202
+ segments.push({ start: 0xffff, end: 0xffff, glyph: 0 });
203
+ const count = segments.length;
204
+ if (16 + count * 8 > 0xffff)
205
+ return undefined;
206
+ const searchRange = 2 * 2 ** Math.floor(Math.log2(count));
207
+ const delta = (segment) => segment.start === 0xffff ? 1 : (segment.glyph - segment.start) & 0xffff;
208
+ return bytes(u16(4), u16(16 + count * 8), u16(0), u16(count * 2), u16(searchRange), u16(Math.floor(Math.log2(count))), u16(count * 2 - searchRange), segments.flatMap((segment) => u16(segment.end)), u16(0), segments.flatMap((segment) => u16(segment.start)), segments.flatMap((segment) => u16(delta(segment))), segments.flatMap(() => u16(0)));
209
+ }
210
+ function cmapFormat12(unicode, codes) {
211
+ const groups = [];
212
+ for (const code of codes) {
213
+ const glyph = unicode.get(code);
214
+ const last = groups.at(-1);
215
+ if (last &&
216
+ code === last.end + 1 &&
217
+ glyph === last.glyph + (code - last.start))
218
+ last.end = code;
219
+ else
220
+ groups.push({ start: code, end: code, glyph });
221
+ }
222
+ return bytes(u16(12), u16(0), u32(16 + groups.length * 12), u32(0), u32(groups.length), groups.flatMap((group) => [
223
+ ...u32(group.start),
224
+ ...u32(group.end),
225
+ ...u32(group.glyph),
226
+ ]));
227
+ }
228
+ function names(postScript) {
229
+ const records = [
230
+ [1, postScript],
231
+ [2, "Regular"],
232
+ [3, `${postScript}-web-doc`],
233
+ [4, postScript],
234
+ [5, "Version 1.0"],
235
+ [6, postScript],
236
+ ];
237
+ const strings = records.map(([, text]) => [...text].flatMap((character) => u16(character.charCodeAt(0))));
238
+ let offset = 0;
239
+ const entries = records.map(([id], index) => {
240
+ const entry = [
241
+ ...u16(3),
242
+ ...u16(1),
243
+ ...u16(0x0409),
244
+ ...u16(id),
245
+ ...u16(strings[index].length),
246
+ ...u16(offset),
247
+ ];
248
+ offset += strings[index].length;
249
+ return entry;
250
+ });
251
+ return bytes(u16(0), u16(records.length), u16(6 + records.length * 12), entries.flat(), strings.flat());
252
+ }
253
+ /** The table directory, the tables padded to four bytes, and the head checksum adjustment. */
254
+ function assemble(tables, version) {
255
+ const tags = [...tables.keys()].sort();
256
+ const count = tags.length;
257
+ const selector = Math.floor(Math.log2(count));
258
+ const headerSize = 12 + count * 16;
259
+ let offset = headerSize;
260
+ const placed = tags.map((tag) => {
261
+ const data = tables.get(tag);
262
+ const at = offset;
263
+ offset += (data.length + 3) & ~3;
264
+ return { tag, data, at };
265
+ });
266
+ const font = new Uint8Array(offset);
267
+ font.set(bytes(u32(version), u16(count), u16(2 ** selector * 16), u16(selector), u16(count * 16 - 2 ** selector * 16), placed.flatMap(({ tag, data, at }) => [
268
+ ...[...tag].map((character) => character.charCodeAt(0)),
269
+ ...u32(checksum(data)),
270
+ ...u32(at),
271
+ ...u32(data.length),
272
+ ])));
273
+ for (const { data, at } of placed)
274
+ font.set(data, at);
275
+ const head = placed.find((table) => table.tag === "head");
276
+ const adjustment = (0xb1b0afba - checksum(font)) >>> 0;
277
+ font.set(u32(adjustment), head.at + 8);
278
+ return font;
279
+ }
280
+ function checksum(data) {
281
+ let sum = 0;
282
+ for (let offset = 0; offset < data.length; offset += 4)
283
+ sum =
284
+ (sum +
285
+ (((data[offset] ?? 0) << 24) |
286
+ ((data[offset + 1] ?? 0) << 16) |
287
+ ((data[offset + 2] ?? 0) << 8) |
288
+ (data[offset + 3] ?? 0))) >>>
289
+ 0;
290
+ return sum;
291
+ }
292
+ function u16(value) {
293
+ return [(value >> 8) & 0xff, value & 0xff];
294
+ }
295
+ function i16(value) {
296
+ return u16(value < 0 ? value + 0x10000 : value);
297
+ }
298
+ function u32(value) {
299
+ return [
300
+ (value >>> 24) & 0xff,
301
+ (value >>> 16) & 0xff,
302
+ (value >>> 8) & 0xff,
303
+ value & 0xff,
304
+ ];
305
+ }
306
+ function bytes(...parts) {
307
+ return concat(parts.map((part) => Uint8Array.from(part)));
308
+ }
309
+ function concat(parts) {
310
+ const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
311
+ let offset = 0;
312
+ for (const part of parts) {
313
+ out.set(part, offset);
314
+ offset += part.length;
315
+ }
316
+ return out;
317
+ }
@@ -0,0 +1,4 @@
1
+ import type { TextFont } from "../types.js";
2
+ import type { Pdfium } from "./pdfium.js";
3
+ export type TextFace = Omit<TextFont, "elementId">;
4
+ export declare function textFaceOf(pdfium: Pdfium, object: number, cache: Map<string, TextFace>): TextFace;
@@ -0,0 +1,81 @@
1
+ import { glyphUnicode, readCff } from "./cff.js";
2
+ import { textStyle } from "./elements.js";
3
+ import { cffOpenType, completeSfnt, hasUnicodeCmap } from "./opentype.js";
4
+ export function textFaceOf(pdfium, object, cache) {
5
+ const { lib } = pdfium;
6
+ const font = lib.FPDFTextObj_GetFont(object);
7
+ const family = textStyle(pdfium, object).fontFamily;
8
+ const baseName = pdfium.readUtf8String((buffer, bytes) => lib.FPDFFont_GetBaseFontName(font, buffer, bytes));
9
+ if (!lib.FPDFFont_GetIsEmbedded(font))
10
+ return { key: `name:${baseName}`, family, missing: "not-embedded" };
11
+ // A Type 3 font is embedded as drawings, with no program to read.
12
+ const program = fontProgram(pdfium, font);
13
+ if (!program)
14
+ return { key: `embedded:${baseName}`, family, missing: "unreadable" };
15
+ const key = `${baseName}#${fnv1a(program).toString(16)}`;
16
+ const known = cache.get(key);
17
+ if (known)
18
+ return { ...known, family };
19
+ const face = { key, family, ...faceOf(program, baseName) };
20
+ cache.set(key, face);
21
+ return face;
22
+ }
23
+ function faceOf(program, baseName) {
24
+ const tag = String.fromCharCode(...program.subarray(0, 4));
25
+ const truetype = tag === "true" ||
26
+ (program[0] === 0 &&
27
+ program[1] === 1 &&
28
+ program[2] === 0 &&
29
+ program[3] === 0);
30
+ if (truetype || tag === "OTTO") {
31
+ if (!hasUnicodeCmap(program))
32
+ return { missing: "no-unicode" };
33
+ const data = completeSfnt(program, baseName);
34
+ return data
35
+ ? { face: { data, format: truetype ? "truetype" : "opentype" } }
36
+ : { missing: "unreadable" };
37
+ }
38
+ // Type 1 programs start with "%!" in clear text, or with a PFB segment.
39
+ if (tag.startsWith("%!") || program[0] === 0x80)
40
+ return { missing: "type1" };
41
+ const cff = readCff(program);
42
+ if (!cff)
43
+ return { missing: "unreadable" };
44
+ if (cff.cid)
45
+ return { missing: "cid-keyed" };
46
+ const unicode = new Map();
47
+ cff.glyphNames.forEach((name, glyph) => {
48
+ const code = glyphUnicode(name);
49
+ if (code !== undefined && !unicode.has(code))
50
+ unicode.set(code, glyph);
51
+ });
52
+ const data = cffOpenType(program, cff, unicode);
53
+ return data
54
+ ? { face: { data, format: "opentype" } }
55
+ : { missing: "no-unicode" };
56
+ }
57
+ /** The font program a PDF font object embeds, or `undefined` for a font it does not embed. */
58
+ function fontProgram(pdfium, font) {
59
+ const { lib } = pdfium;
60
+ const size = pdfium.readNumbers(1, "i32", ([out]) => lib.FPDFFont_GetFontData(font, 0, 0, out))?.[0];
61
+ if (!size)
62
+ return undefined;
63
+ const buffer = pdfium.malloc(size);
64
+ const out = pdfium.malloc(4);
65
+ try {
66
+ return lib.FPDFFont_GetFontData(font, buffer, size, out)
67
+ ? pdfium.readBytes(buffer, size)
68
+ : undefined;
69
+ }
70
+ finally {
71
+ pdfium.free(out);
72
+ pdfium.free(buffer);
73
+ }
74
+ }
75
+ /** FNV-1a over the program: a cheap, stable key for one font. */
76
+ function fnv1a(bytes) {
77
+ let hash = 0x811c9dc5;
78
+ for (const byte of bytes)
79
+ hash = Math.imul(hash ^ byte, 0x01000193);
80
+ return hash >>> 0;
81
+ }
@@ -2,10 +2,11 @@ import { type WorkerLike } from "../../worker-client.js";
2
2
  import type { EditEngineContext } from "../engine.js";
3
3
  import type { PagePoint, PageRect, TextPosition, TextRange } from "../types.js";
4
4
  import { WorkerEngineClient } from "../worker-engine.js";
5
- import type { PageBitmap, PageLayout, TextLayout } from "./types.js";
5
+ import type { PageBitmap, PageLayout, TextFont, TextLayout } from "./types.js";
6
6
  /** The reads behind the overlay primitives, beyond the core engine interface. */
7
7
  export interface PdfEngineReads {
8
8
  textLayout(id: string, signal: AbortSignal): Promise<TextLayout | undefined>;
9
+ textFont(id: string, signal: AbortSignal): Promise<TextFont | undefined>;
9
10
  positionAt(pageIndex: number, point: PagePoint, signal: AbortSignal): Promise<TextPosition | undefined>;
10
11
  rangeRects(range: TextRange, signal: AbortSignal): Promise<readonly PageRect[]>;
11
12
  renderWithout(pageIndex: number, elementIds: readonly string[], scale: number, signal: AbortSignal): Promise<PageBitmap>;
@@ -28,6 +29,7 @@ export declare class PdfEditEngineClient extends WorkerEngineClient implements P
28
29
  readonly schemas: import("../types.js").OperationSchemaSet;
29
30
  start(original: Uint8Array, options: PdfEditProviderOptions): Promise<void>;
30
31
  textLayout(id: string, signal: AbortSignal): Promise<TextLayout | undefined>;
32
+ textFont(id: string, signal: AbortSignal): Promise<TextFont | undefined>;
31
33
  positionAt(pageIndex: number, point: PagePoint, signal: AbortSignal): Promise<TextPosition | undefined>;
32
34
  rangeRects(range: TextRange, signal: AbortSignal): Promise<readonly PageRect[]>;
33
35
  renderWithout(pageIndex: number, elementIds: readonly string[], scale: number, signal: AbortSignal): Promise<PageBitmap>;
@@ -53,6 +53,9 @@ export class PdfEditEngineClient extends WorkerEngineClient {
53
53
  textLayout(id, signal) {
54
54
  return this.request("edit-text-layout", { id }, signal);
55
55
  }
56
+ textFont(id, signal) {
57
+ return this.request("edit-text-font", { id }, signal);
58
+ }
56
59
  positionAt(pageIndex, point, signal) {
57
60
  return this.request("edit-position-at", { pageIndex, point }, signal);
58
61
  }
@@ -2,7 +2,7 @@ import type { TextSelection } from "../../contracts.js";
2
2
  import type { DescribeOptions, DocumentDescription, EditCheckpoint, OutlineOptions, OutlineResult, TargetCandidate, TargetQuery, ToolCall, ToolCallOptions, ToolResult, ToolSet } from "../ai/types.js";
3
3
  import type { EditSessionCore } from "../engine.js";
4
4
  import type { ApplyOptions, AssetOptions, EditFindOptions, EditOperation, EditReceipt, EditState, ElementQuery, HistoryOptions, OperationSchemaSet, PagePoint, PageRect, ReadItem, ReadOptions, ReadResult, SavedDocument, TextPosition, TextRange, TextTarget } from "../types.js";
5
- import type { DeleteElementOperation, DeletePageOperation, Fields, InsertImageOperation, InsertPageOperation, InsertShapeOperation, InsertTableOperation, InsertTextBoxOperation, MoveElementOperation, MovePageOperation, PdfEditSession, PdfElement, PdfOperation, PdfSaveOptions, ReplaceTextOperation, ResizeElementOperation, RotatePageOperation, SetShapeStyleOperation, SetTableCellOperation, PageBitmap, PageLayout, RenderOptions, SetTextStyleOperation, TextLayout } from "./types.js";
5
+ import type { DeleteElementOperation, DeletePageOperation, Fields, InsertImageOperation, InsertPageOperation, InsertShapeOperation, InsertTableOperation, InsertTextBoxOperation, MoveElementOperation, MovePageOperation, PdfEditSession, PdfElement, PdfOperation, PdfSaveOptions, ReplaceTextOperation, ResizeElementOperation, RotatePageOperation, SetShapeStyleOperation, SetTableCellOperation, PageBitmap, PageLayout, RenderOptions, SetTextStyleOperation, TextFont, TextLayout } from "./types.js";
6
6
  /**
7
7
  * The PDF session: the core session narrowed to PDF operations and elements.
8
8
  * Typed methods are added here as their operations ship; each is `apply()`
@@ -39,6 +39,7 @@ export declare class PdfSession implements PdfEditSession {
39
39
  get tools(): ToolSet;
40
40
  callTool(call: ToolCall, options?: ToolCallOptions): Promise<ToolResult>;
41
41
  getTextLayout(elementId: string, options?: ReadOptions): Promise<ReadItem<TextLayout>>;
42
+ getTextFont(elementId: string, options?: ReadOptions): Promise<ReadItem<TextFont>>;
42
43
  positionAt(pageIndex: number, point: PagePoint, options?: ReadOptions): Promise<ReadItem<TextPosition>>;
43
44
  rangeRects(range: TextRange, options?: ReadOptions): Promise<ReadResult<PageRect>>;
44
45
  getPageLayout(pageIndex: number, options?: ReadOptions): Promise<ReadItem<PageLayout>>;
@@ -198,6 +198,9 @@ export class PdfSession {
198
198
  getTextLayout(elementId, options) {
199
199
  return this.#core.readItem(options, (engine, signal) => pdfReads(engine).textLayout(elementId, signal));
200
200
  }
201
+ getTextFont(elementId, options) {
202
+ return this.#core.readItem(options, (engine, signal) => pdfReads(engine).textFont(elementId, signal));
203
+ }
201
204
  positionAt(pageIndex, point, options) {
202
205
  return this.#core.readItem(options, (engine, signal) => pdfReads(engine).positionAt(pageIndex, point, signal));
203
206
  }