rapier-markdown-kit 1.2.11 → 1.2.13

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.
@@ -6,9 +6,10 @@ const familyName = value => value.trim().replace(/^['"]|['"]$/g, '');
6
6
  function readFont(asset, fontCache) {
7
7
  if (fontCache.has(asset)) return fontCache.get(asset);
8
8
  const bytes = typeof asset.read === 'function' ? asset.read() : Uint8Array.from(atob(asset.data), character => character.charCodeAt(0));
9
- const view = new DataView(bytes.buffer), u16 = at => view.getUint16(at), i16 = at => view.getInt16(at), u32 = at => view.getUint32(at);
9
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength), u16 = at => view.getUint16(at), i16 = at => view.getInt16(at), u32 = at => view.getUint32(at);
10
10
  const tag = at => String.fromCharCode(...bytes.subarray(at, at + 4)), tables = new Map();
11
11
  for (let at = 12, end = 12 + u16(4) * 16; at < end; at += 16) tables.set(tag(at), {at: u32(at + 8), length: u32(at + 12)});
12
+ if (tables.has('CFF2') || ['COLR', 'SVG ', 'sbix', 'CBDT', 'EBDT'].some(tag => tables.has(tag))) throw new Error('The PDF font needs a color or CFF2 renderer.');
12
13
  const head = tables.get('head').at, hhea = tables.get('hhea').at, hmtx = tables.get('hmtx').at, cmap = tables.get('cmap').at;
13
14
  const units = u16(head + 18), metricCount = u16(hhea + 34), glyphCount = u16(tables.get('maxp').at + 4);
14
15
  let mapping = null;
@@ -51,9 +52,11 @@ function readFont(asset, fontCache) {
51
52
  const feature = features + u16(record + 4);
52
53
  for (let j = 0; j < u16(feature + 2); j++) {
53
54
  const lookup = lookups + u16(lookups + 2 + u16(feature + 4 + j * 2) * 2), rows = new Map();
54
- if (u16(lookup) !== 4 || (u16(lookup + 2) & ~8)) throw new Error('Unsupported embedded font ligature lookup.');
55
+ if (![4, 7].includes(u16(lookup)) || u16(lookup + 2) & 1) continue;
55
56
  for (let k = 0; k < u16(lookup + 4); k++) {
56
- const sub = lookup + u16(lookup + 6 + k * 2); if (u16(sub) !== 1) throw new Error('Unsupported embedded font ligature table.');
57
+ let sub = lookup + u16(lookup + 6 + k * 2), type = u16(lookup);
58
+ if (type === 7) {type = u16(sub + 2); sub += u32(sub + 4);}
59
+ if (type !== 4 || u16(sub) !== 1) continue;
57
60
  const covered = coverage(sub + u16(sub + 2));
58
61
  for (let n = 0; n < u16(sub + 4); n++) {
59
62
  const set = sub + u16(sub + 6 + n * 2), values = [];
@@ -65,127 +68,416 @@ function readFont(asset, fontCache) {
65
68
  }
66
69
  }
67
70
  }
71
+ const tabular = new Map();
72
+ if (gsub) {
73
+ const features = gsub + u16(gsub + 6), lookups = gsub + u16(gsub + 8);
74
+ for (let i = 0; i < u16(features); i++) {
75
+ const record = features + 2 + i * 6;
76
+ if (tag(record) !== 'tnum') continue;
77
+ const feature = features + u16(record + 4);
78
+ for (let j = 0; j < u16(feature + 2); j++) {
79
+ const lookup = lookups + u16(lookups + 2 + u16(feature + 4 + j * 2) * 2);
80
+ for (let k = 0; k < u16(lookup + 4); k++) {
81
+ let sub = lookup + u16(lookup + 6 + k * 2), type = u16(lookup);
82
+ if (type === 7) {type = u16(sub + 2); sub += u32(sub + 4);}
83
+ if (type !== 1) continue;
84
+ coverage(sub + u16(sub + 2)).forEach((id, n) => tabular.set(id, u16(sub) === 1 ? (id + i16(sub + 4)) & 65535 : u16(sub + 6 + n * 2)));
85
+ }
86
+ }
87
+ }
88
+ }
68
89
  const os2 = tables.get('OS/2');
69
- const font = {asset, bytes, units, glyph, ligatures, glyphCount, advance: id => u16(hmtx + Math.min(id, metricCount - 1) * 4),
90
+ const font = {asset, bytes, units, glyph, ligatures, tabular, glyphCount, cids: asset.cids,
91
+ weight: os2 ? u16(os2.at + 4) : 400, italic: !!(u16(head + 44) & 2 || os2 && u16(os2.at + 62) & 513),
92
+ italicAngle: tables.has('post') ? view.getInt32(tables.get('post').at + 4) / 65536 : 0,
93
+ outline: tables.has('glyf') ? 'truetype' : 'cff', advance: id => u16(hmtx + Math.min(id, metricCount - 1) * 4),
70
94
  bbox: [36, 38, 40, 42].map(offset => i16(head + offset)), ascent: i16(hhea + 4), descent: i16(hhea + 6),
71
95
  capHeight: os2 && os2.length >= 90 && u16(os2.at) >= 2 ? i16(os2.at + 88) : i16(hhea + 4)};
72
96
  fontCache.set(asset, font); return font;
73
97
  }
74
98
 
75
- function shape(value, font, preserve, ligatures) {
76
- const result = [];
77
- for (let at = 0; at < value.length;) {
78
- const code = value.codePointAt(at), character = String.fromCodePoint(code), start = at;
79
- if (code === 0x00ad) {
80
- at += character.length;
81
- const own = font.glyph(code), hyphen = font.glyph(0x2d), glyph = own || hyphen;
82
- if (!glyph || glyph >= font.glyphCount) return null;
83
- result.push({start, end: at, text: own ? character : '-', glyph, soft: true});
84
- continue;
85
- }
86
- if (/[\p{Mark}\p{Format}]/u.test(character) || (/[\p{Letter}]/u.test(character) && !/\p{Script=Latin}/u.test(character)) || code === 0xfb01 || code === 0xfb02) return null;
87
- at += character.length;
88
- if (!preserve && /[ \t\r\n\f]/.test(character)) {while (at < value.length && /[ \t\r\n\f]/.test(value[at])) at++;}
89
- const text = preserve ? character.replace(/[\r\n]/g, '') : /[ \t\r\n\f]/.test(character) ? ' ' : character;
90
- const glyph = font.glyph(/[\t\r\n\f]/.test(character) ? 32 : code);
91
- if (!glyph || glyph >= font.glyphCount) return null;
92
- result.push({start, end: at, text, glyph});
93
- }
94
- if (ligatures) for (const lookup of font.ligatures) for (let i = 0; i < result.length; i++) {
95
- const found = lookup.get(result[i].glyph)?.find(row => row.components.every((id, offset) => result[i + offset + 1]?.glyph === id));
96
- if (found) {const held = result.splice(i, found.components.length + 1); result.splice(i, 0, {start: held[0].start, end: held.at(-1).end, text: held.map(row => row.text).join(''), glyph: found.glyph});}
99
+ function shape(value, font, style) {
100
+ const result = [], preserve = /pre|break-spaces/.test(style.whiteSpace);
101
+ const segmenter = new Intl.Segmenter(undefined, {granularity: 'grapheme'});
102
+ for (const {segment, index: start} of segmenter.segment(value)) {
103
+ const end = start + segment.length, soft = segment === '\u00ad';
104
+ const text = soft ? '\u00ad' : !preserve && /^[ \t\r\n\f]+$/.test(segment) ? ' ' : segment;
105
+ const normalized = (soft ? '-' : text).normalize('NFC');
106
+ let reason = '';
107
+ if (/[\p{Mark}\p{Format}]/u.test(normalized) || /\p{Letter}/u.test(normalized) &&
108
+ !/^[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}\p{Script=Hebrew}\p{Script=Armenian}\p{Script=Georgian}\p{Script=Ethiopic}\p{Script=Cherokee}\p{Script=Canadian_Aboriginal}\p{Script=Runic}\p{Script=Ogham}\p{Script=Tifinagh}\p{Script=Vai}\p{Script=Lisu}\p{Script=Bopomofo}\p{Script=Yi}\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+$/u.test(normalized)) reason = 'shaping';
109
+ let glyph = [...normalized].length === 1 ? font.glyph(normalized.codePointAt(0)) : 0;
110
+ if (style.fontVariantNumeric === 'tabular-nums') glyph = font.tabular.get(glyph) || glyph;
111
+ if (!reason && (!glyph || glyph >= font.glyphCount)) reason = 'missing-glyph';
112
+ result.push({start, end, text, glyph, soft, reason});
97
113
  }
114
+ if (style.fontVariantLigatures === 'normal' && !(Number.parseFloat(style.letterSpacing) || 0))
115
+ for (const lookup of font.ligatures) for (let i = 0; i < result.length; i++) {
116
+ if (result[i].reason) continue;
117
+ const found = lookup.get(result[i].glyph)?.find(row => row.components.every((id, offset) => !result[i + offset + 1]?.reason && result[i + offset + 1]?.glyph === id));
118
+ if (found) {const held = result.splice(i, found.components.length + 1); result.splice(i, 0, {start: held[0].start, end: held.at(-1).end, text: held.map(row => row.text).join(''), glyph: found.glyph});}
119
+ }
98
120
  return result;
99
121
  }
100
122
 
101
- export async function collectPdfGlyphs(main, {width, check, faces = []}) {
102
- const fontCache = new Map();
103
- const document = main.ownerDocument, window = document.defaultView, origin = main.getBoundingClientRect();
104
- const nodes = new Map(), hidden = [], canvas = document.createElement('canvas'); canvas.width = canvas.height = 1;
123
+ export async function collectPdfGlyphs(main, {width, check, faces = [], prepareFont, systemFonts, maskFont, origin = main.getBoundingClientRect()}) {
124
+ const document = main.ownerDocument, window = document.defaultView;
125
+ const nodes = new Map(), markers = new Map(), painted = [], restoreSteps = [], pending = [], fontCache = new Map(), prepared = new Map();
126
+ const canvas = document.createElement('canvas'); canvas.width = canvas.height = 1;
105
127
  const context = canvas.getContext('2d', {willReadFrequently: true});
106
- const restore = () => {
107
- for (const row of hidden) {
108
- if (row.wrap) {row.wrap.replaceWith(...row.wrap.childNodes); continue;}
109
- if (row.value) row.node.style.setProperty('-webkit-text-fill-color', row.value, row.priority); else row.node.style.removeProperty('-webkit-text-fill-color');
128
+ const restore = () => {for (const undo of restoreSteps.reverse()) undo(); restoreSteps.length = 0; canvas.width = canvas.height = 0;};
129
+ if (!context) return {nodes, markers, painted, restore};
130
+ const colorCache = new Map(), styles = new Map();
131
+ const styleOf = node => {if (!styles.has(node)) styles.set(node, window.getComputedStyle(node)); return styles.get(node);};
132
+ const color = value => {
133
+ if (!colorCache.has(value)) {context.clearRect(0, 0, 1, 1); context.fillStyle = value; context.fillRect(0, 0, 1, 1); colorCache.set(value, Array.from(context.getImageData(0, 0, 1, 1).data, item => item / 255));}
134
+ return colorCache.get(value);
135
+ };
136
+ const svgScale = node => {
137
+ const svg = node.namespaceURI === 'http://www.w3.org/2000/svg' ? node : node.closest('foreignObject');
138
+ if (!svg) return 1;
139
+ const matrix = svg.getScreenCTM();
140
+ return matrix && matrix.a > 0 && Math.abs(matrix.b) < .00001 && Math.abs(matrix.c) < .00001 && Math.abs(matrix.a - matrix.d) < .00001 ? matrix.a : 0;
141
+ };
142
+ const stretches = new Map();
143
+ const svgStretch = element => {
144
+ const holder = element.closest('text[textLength],tspan[textLength]');
145
+ if (!holder || holder.getAttribute('lengthAdjust') !== 'spacingAndGlyphs') return 1;
146
+ if (!stretches.has(holder)) {
147
+ const value = holder.getAttribute('textLength'), probe = holder.cloneNode(true), style = styleOf(holder);
148
+ probe.removeAttribute('textLength');
149
+ for (const property of style) probe.style.setProperty(property, style.getPropertyValue(property));
150
+ probe.style.setProperty('visibility', 'hidden', 'important'); holder.parentNode.append(probe);
151
+ let natural;
152
+ try {natural = probe.getComputedTextLength();} finally {probe.remove();}
153
+ stretches.set(holder, Number(value) > 0 && natural > 0 ? Number(value) / natural : 1);
110
154
  }
111
- canvas.width = canvas.height = 0;
155
+ return stretches.get(holder);
112
156
  };
113
- if (!context) return {nodes, restore};
114
- const colorCache = new Map(), styles = new Map(), styleOf = node => {if (!styles.has(node)) styles.set(node, window.getComputedStyle(node)); return styles.get(node);};
115
- const color = value => {if (!colorCache.has(value)) {context.clearRect(0, 0, 1, 1); context.fillStyle = value; context.fillRect(0, 0, 1, 1); colorCache.set(value, Array.from(context.getImageData(0, 0, 1, 1).data, item => item / 255));} return colorCache.get(value);};
116
157
  const safe = node => {
117
158
  for (let parent = node; parent; parent = parent.parentElement) {
118
159
  const style = styleOf(parent);
119
- if (style.visibility !== 'visible' || style.display === 'none' || style.transform !== 'none' || style.filter !== 'none' || style.mixBlendMode !== 'normal'
120
- || ['absolute', 'fixed', 'sticky'].includes(style.position) || style.zIndex !== 'auto'
160
+ if (style.visibility !== 'visible' || style.display === 'none' || style.transform !== 'none' && !(parent.namespaceURI === 'http://www.w3.org/2000/svg' && svgScale(parent)) || style.filter !== 'none' || style.mixBlendMode !== 'normal'
161
+ || ['absolute', 'fixed'].includes(style.position) || style.zIndex !== 'auto' && style.position !== 'sticky'
121
162
  || Number(style.opacity) !== 1 || style.clip !== 'auto' || style.clipPath !== 'none' || style.maskImage !== 'none'
122
- || (parent !== main && (style.overflowX !== 'visible' || style.overflowY !== 'visible'))) return false;
163
+ || (parent !== main && ((style.overflowX !== 'visible' && parent.scrollWidth > parent.clientWidth + 1)
164
+ || (style.overflowY !== 'visible' && parent.scrollHeight > parent.clientHeight + 1)))) return false;
123
165
  if (parent === main) break;
124
166
  }
125
167
  return true;
126
168
  };
169
+ let localFonts;
170
+ const prepare = async (asset, style) => {
171
+ const key = faces.indexOf(asset) + ':' + (asset.system ? asset.postscriptName : '') +
172
+ (String(asset.weight).includes(' ') || asset.system ? ':' + style.fontWeight + ':' + style.fontStyle + ':' + style.fontSize : '');
173
+ if (!prepared.has(key)) prepared.set(key, (async () => {
174
+ try {
175
+ const raw = typeof asset.read === 'function' ? await asset.read() : Uint8Array.from(atob(asset.data), c => c.charCodeAt(0));
176
+ const prepared = prepareFont ? await prepareFont(raw, main.textContent + '0123456789.- ', style, asset) : raw;
177
+ if (!prepared) return null;
178
+ const bytes = prepared.bytes || prepared;
179
+ return readFont({...asset, cids: prepared.cids, postscriptName: prepared.postscriptName || asset.postscriptName, subset: prepared.subset, read: () => bytes}, fontCache);
180
+ } catch (_) {return null;}
181
+ })());
182
+ return prepared.get(key);
183
+ };
184
+ const fontFor = async style => {
185
+ const family = familyName(style.fontFamily.split(',')[0]), weight = Number(style.fontWeight), italic = style.fontStyle !== 'normal';
186
+ const candidates = faces.filter(face => face.family.toLowerCase() === family.toLowerCase());
187
+ const styled = candidates.filter(face => !!(face.italic || /italic|oblique/i.test(face.style)) === italic);
188
+ const rank = face => {
189
+ const range = String(face.weight).split(/\s+/).map(Number), chosen = Math.max(range[0], Math.min(range[1] || range[0], weight));
190
+ return weight >= 400 && weight <= 500 ? chosen >= weight && chosen <= 500 ? chosen - weight : chosen < weight ? 1000 + weight - chosen : 2000 + chosen - weight :
191
+ (weight < 400 ? chosen <= weight : chosen >= weight) ? Math.abs(chosen - weight) : 1000 + Math.abs(chosen - weight);
192
+ };
193
+ const asset = (styled.length ? styled : candidates).sort((left, right) => rank(left) - rank(right))[0];
194
+ if (!asset) return null;
195
+ if (weight >= 600 && Number(String(asset.weight).split(/\s+/).at(-1)) < 600) return null;
196
+ return prepare(asset, style);
197
+ };
198
+
199
+ const matches = new Map(), aliases = new Map(), systemChoices = new Map(), proofCanvas = document.createElement('canvas');
200
+ proofCanvas.width = 512; proofCanvas.height = 128;
201
+ const proofContext = proofCanvas.getContext('2d', {willReadFrequently: true});
202
+ restoreSteps.push(() => {proofCanvas.width = proofCanvas.height = 0;});
203
+ const pixels = (text, style, family, expectedWidth) => {
204
+ proofContext.clearRect(0, 0, 512, 128);
205
+ proofContext.font = [style.fontStyle, style.fontWeight, style.fontSize, family].join(' ');
206
+ const width = proofContext.measureText(text).width;
207
+ if (expectedWidth != null && Math.abs(width - expectedWidth) > .01) return null;
208
+ proofContext.fillText(text, 16, 96);
209
+ return {width, data: proofContext.getImageData(0, 0, Math.min(512, Math.ceil(width) + 48), 128).data};
210
+ };
211
+ const fallbackFor = async (text, style) => {
212
+ const key = [text, style.fontFamily, style.fontStyle, style.fontWeight, style.fontSize].join('\n');
213
+ if (matches.has(key)) return matches.get(key);
214
+ const actual = pixels(text, style, style.fontFamily), families = style.fontFamily.split(',').map(familyName);
215
+ localFonts ??= await systemFonts?.() || [];
216
+ for (const asset of [...faces.filter(face => families.includes(face.family)), ...localFonts]) {
217
+ const candidate = pixels(text, style, '"' + asset.family.replace(/["\\]/g, '\\$&') + '"', actual.width);
218
+ if (!candidate || !actual.data.every((value, index) => value === candidate.data[index])) continue;
219
+ const font = await prepare(asset, style);
220
+ if (!font || shape(text, font, style).some(part => part.reason)) continue;
221
+ if (asset.system) {
222
+ const key = [asset.family, style.fontStyle, style.fontWeight, style.fontSize].join(':');
223
+ if (!systemChoices.has(key)) {
224
+ const fonts = (await Promise.all(localFonts.filter(face => face.family === asset.family).map(face => prepare(face, style)))).filter(Boolean);
225
+ const italic = style.fontStyle !== 'normal', styled = fonts.filter(font => font.italic === italic), weight = Number(style.fontWeight);
226
+ const rank = font => weight >= 400 && weight <= 500 ? font.weight >= weight && font.weight <= 500 ? font.weight - weight : font.weight < weight ? 1000 + weight - font.weight : 2000 + font.weight - weight :
227
+ (weight < 400 ? font.weight <= weight : font.weight >= weight) ? Math.abs(font.weight - weight) : 1000 + Math.abs(font.weight - weight);
228
+ systemChoices.set(key, (styled.length ? styled : fonts).sort((a, b) => rank(a) - rank(b))[0]);
229
+ }
230
+ if (systemChoices.get(key) !== font || Number(style.fontWeight) >= 600 && font.weight < 600) continue;
231
+ }
232
+ if (!aliases.has(font)) {
233
+ const alias = 'rapier-pdf-face-' + aliases.size;
234
+ const face = new window.FontFace(alias, font.bytes, {weight: style.fontWeight, style: font.italic ? 'italic' : 'normal'});
235
+ try {await face.load(); document.fonts.add(face);} catch (_) {continue;}
236
+ aliases.set(font, alias); restoreSteps.push(() => document.fonts.delete(face));
237
+ }
238
+ const held = pixels(text, style, aliases.get(font), actual.width);
239
+ if (!held || !asset.system && !actual.data.every((value, index) => value === held.data[index])) continue;
240
+ matches.set(key, font); return font;
241
+ }
242
+ matches.set(key, null); return null;
243
+ };
244
+ const admit = style => {
245
+ if (!/^(normal|italic|oblique(?: -?[\d.]+deg)?)$/.test(style.fontStyle) || !['ltr', 'rtl'].includes(style.direction) || style.writingMode !== 'horizontal-tb') return 'direction-or-style';
246
+ if (style.textTransform !== 'none' || style.textShadow !== 'none' || style.hyphens === 'auto' || Number.parseFloat(style.webkitTextStrokeWidth) > 0) return 'text-effect';
247
+ if (['fontFeatureSettings', 'fontVariationSettings', 'fontVariantAlternates', 'fontVariantCaps', 'fontVariantEastAsian', 'fontVariantPosition'].some(name => style[name] && style[name] !== 'normal') || !['normal', 'tabular-nums'].includes(style.fontVariantNumeric)) return 'font-feature';
248
+ if ((style.fontSizeAdjust && style.fontSizeAdjust !== 'none') || !['normal', '100%'].includes(style.fontStretch) || style.textRendering === 'optimizeSpeed'
249
+ || !['normal', 'none', 'no-common-ligatures'].includes(style.fontVariantLigatures)) return 'font-feature';
250
+ return '';
251
+ };
252
+ const measure = async (node, style, font) => {
253
+ const scale = svgScale(node.parentElement), size = Number.parseFloat(style.fontSize) * scale, parts = [], range = document.createRange();
254
+ const svg = node.parentElement.namespaceURI === 'http://www.w3.org/2000/svg' ? node.parentElement : null, scaleX = svg ? svgStretch(node.parentElement) : 1;
255
+ context.font = [style.fontStyle, style.fontWeight, style.fontSize, style.fontFamily].join(' ');
256
+ const ascent = context.measureText('Hg').fontBoundingBoxAscent * scale, rgba = color(svg ? style.fill : style.webkitTextFillColor || style.color).slice();
257
+ if (svg) rgba[3] *= Number(style.fillOpacity);
258
+ if (!(size > 0) || !Number.isFinite(ascent)) return parts;
259
+ let svgOffset = 0;
260
+ if (svg) {
261
+ const walker = document.createTreeWalker(svg, 4);
262
+ for (let text; (text = walker.nextNode()) && text !== node;) svgOffset += text.data.length;
263
+ }
264
+ const shaped = shape(node.data, font || {glyph: () => 0, ligatures: [], tabular: new Map()}, style);
265
+ for (let index = 0; index < shaped.length; index++) {
266
+ let item = shaped[index], face = font;
267
+ if (item.reason === 'missing-glyph') {
268
+ face = await fallbackFor(item.text, style);
269
+ if (face) item = {...item, ...shape(item.text, face, style)[0], start: item.start, end: item.end};
270
+ else item.reason = font ? 'fallback-font-bytes-unavailable' : 'font-bytes-unavailable';
271
+ }
272
+ const italic = style.fontStyle !== 'normal' && !face?.italic;
273
+ const skew = italic ? style.fontStyle.startsWith('oblique ') ? Math.tan(parseFloat(style.fontStyle.slice(8)) * Math.PI / 180) : .25 : 0;
274
+ range.setStart(node, item.start); range.setEnd(node, item.end);
275
+ const rects = Array.from(range.getClientRects()).filter(rect => rect.height > 0 && rect.width > .001);
276
+ const first = shaped[index - 1]?.soft && rects.length > 1 ? rects.at(-1) : rects[0];
277
+ if (!first) continue;
278
+ const page = Math.max(0, Math.floor((first.left - origin.left + .01) / width));
279
+ const split = rects.some(rect => Math.abs(rect.top - first.top) > .25) && !item.soft && !shaped[index - 1]?.soft && item.text.trim();
280
+ let advance = first.width, x = first.left - origin.left - page * width, baseline = first.top - origin.top + ascent;
281
+ if (svg && !item.reason) {
282
+ const point = svg.getStartPositionOfChar(svgOffset + item.start).matrixTransform(svg.getScreenCTM());
283
+ const end = svg.getEndPositionOfChar(svgOffset + item.end - 1).matrixTransform(svg.getScreenCTM());
284
+ advance = Math.abs(end.x - point.x);
285
+ x = point.x - origin.left - page * width; baseline = point.y - origin.top;
286
+ }
287
+ parts.push({...item, reason: split ? 'fragmented-cluster' : item.reason, font: item.reason || split ? null : face, size, rgba, skew, scaleX, page,
288
+ x, y: first.top - origin.top, width: advance, height: first.height, baseline});
289
+ }
290
+ return parts;
291
+ };
292
+ const record = (text, reason, style, part) => {
293
+ if (!text.trim()) return;
294
+ const last = painted.at(-1), family = style.fontFamily;
295
+ if (last && last.reason === reason && last.family === family && last.page === part?.page && last.node === part?.node && last.end === part?.start) {last.text += text; last.end = part?.end;}
296
+ else painted.push({text, reason, family, page: part?.page, node: part?.node, end: part?.end});
297
+ };
127
298
  try {
128
299
  let visited = 0;
129
- for (const element of main.querySelectorAll('*')) {
300
+ for (const element of [main, ...main.querySelectorAll('*')]) {
130
301
  if (++visited % 128 === 0) {await new Promise(resolve => setTimeout(resolve, 0)); check();}
131
- if (element.namespaceURI !== 'http://www.w3.org/1999/xhtml' || element.closest('svg,pre,.rapier-will-carrier')) continue;
132
- const textNodes = Array.from(element.childNodes).filter(node => node.nodeType === 3 && node.data.trim());
133
- if (!textNodes.length || !safe(element)) continue;
134
- const language = (element.closest('[lang]')?.getAttribute('lang') || document.documentElement.lang || '').toLowerCase();
135
- if (language && !/^en(?:-|$)/.test(language)) continue;
136
- const style = styleOf(element), family = familyName(style.fontFamily.split(',')[0]), weight = Number(style.fontWeight), size = Number.parseFloat(style.fontSize);
137
- if (!['Geist', 'Geist Mono'].includes(family) || style.fontStyle !== 'normal' || style.direction !== 'ltr' || style.writingMode !== 'horizontal-tb'
138
- || style.textTransform !== 'none' || style.textShadow !== 'none' || style.hyphens === 'auto' || Number.parseFloat(style.webkitTextStrokeWidth) > 0
139
- || ['fontFeatureSettings', 'fontVariationSettings', 'fontVariantAlternates', 'fontVariantCaps', 'fontVariantNumeric', 'fontVariantEastAsian', 'fontVariantPosition'].some(name => style[name] && style[name] !== 'normal')
140
- || (style.fontSizeAdjust && style.fontSizeAdjust !== 'none') || !['normal', '100%'].includes(style.fontStretch) || style.textRendering === 'optimizeSpeed'
141
- || !['normal', 'none', 'no-common-ligatures'].includes(style.fontVariantLigatures) || !(size > 0)) continue;
142
- const generated = ['::before', '::after'].some(pseudo => !['none', 'normal', '""', "''"].includes(window.getComputedStyle(element, pseudo).content));
143
- const altered = ['::first-letter', '::first-line'].some(pseudo => {const held = window.getComputedStyle(element, pseudo); return ['fontFamily', 'fontWeight', 'fontSize', 'fontStyle', 'fontVariantCaps', 'color'].some(name => held[name] !== style[name]);});
144
- if (generated || altered || !Array.from(document.fonts).some(face => familyName(face.family) === family && face.status === 'loaded')) continue;
145
- const asset = faces.find(face => face.family === family && face.weight === weight);
146
- if (!asset) continue;
147
- const font = readFont(asset, fontCache), preserve = /pre|break-spaces/.test(style.whiteSpace);
148
- context.font = [style.fontStyle, style.fontWeight, style.fontSize, style.fontFamily].join(' ');
149
- const metrics = context.measureText('Hg'), ascent = metrics.fontBoundingBoxAscent;
150
- if (!Number.isFinite(ascent)) continue;
151
- const rgba = color(style.webkitTextFillColor || style.color), range = document.createRange();
302
+ const svg = element.namespaceURI === 'http://www.w3.org/2000/svg';
303
+ if (svg && !['text','tspan'].includes(element.localName) || element.closest('style,script,defs,title,desc,.rapier-will-carrier,.math-rendered')) continue;
304
+ const textNodes = Array.from(element.childNodes).filter(node => node.nodeType === 3 && node.data.length);
305
+ if (!textNodes.length) continue;
306
+ const style = styleOf(element), reason = element.namespaceURI === 'http://www.w3.org/1998/Math/MathML' ? 'math-layout' : !safe(element) ? 'compositing' : !svgScale(element) ? 'svg-transform' :
307
+ svg && style.stroke !== 'none' && parseFloat(style.strokeWidth) > 0 ? 'svg-stroke' :
308
+ svg && element.closest('textPath') ? 'svg-text-path' :
309
+ svg && element.getNumberOfChars() !== element.textContent.length ? 'svg-character-layout' :
310
+ svg && style.fill.startsWith('url(') ? 'svg-paint' : admit(style);
311
+ const altered = !svg && ['::first-letter', '::first-line'].some(pseudo => {const held = window.getComputedStyle(element, pseudo); return ['fontFamily', 'fontWeight', 'fontSize', 'fontStyle', 'fontVariantCaps', 'color'].some(name => held[name] !== style[name]);});
312
+ const font = !reason && !altered ? await fontFor(style) : null;
152
313
  for (const node of textNodes) {
153
- const shaped = shape(node.data, font, preserve, style.fontVariantLigatures === 'normal' && !(Number.parseFloat(style.letterSpacing) || 0));
154
- if (!shaped) continue;
155
- let valid = true;
156
- const placed = [];
157
- for (const item of shaped) {
158
- range.setStart(node, item.start); range.setEnd(node, item.end);
159
- const ink = Array.from(range.getClientRects()).filter(rect => rect.height > 0);
160
- const lines = [];
161
- for (const rect of ink) {
162
- const line = lines.find(row => Math.abs(row[0].top - rect.top) <= .25);
163
- if (line) line.push(rect); else lines.push([rect]);
164
- }
165
- lines.sort((a, b) => b.reduce((sum, rect) => sum + rect.width, 0) - a.reduce((sum, rect) => sum + rect.width, 0));
166
- const previous = shaped[shaped.indexOf(item) - 1];
167
- if (lines.length > 1 && !item.soft && !previous?.soft) {valid = false; break;}
168
- const rects = lines[0] || [];
169
- const first = (item.soft ? rects.find(rect => rect.width > 0) : null) || rects[0];
170
- if (item.soft && !(first?.width > 0)) continue;
171
- if (!first) {valid = false; break;}
172
- const page = Math.max(0, Math.floor((first.left - origin.left + .01) / width));
173
- Object.assign(item, {font, size, rgba, x: first.left - origin.left - page * width, baseline: first.top - origin.top + ascent});
174
- placed.push(item);
175
- }
176
- if (!valid || !placed.length) continue;
177
- nodes.set(node, placed);
178
- if (element.localName === 'li' || element.children.length || textNodes.length > 1) {
179
- const wrap = document.createElement('span');
180
- node.replaceWith(wrap); wrap.append(node);
181
- wrap.style.setProperty('-webkit-text-fill-color', 'transparent', 'important');
182
- hidden.push({wrap});
314
+ if (reason || altered) {
315
+ const range = document.createRange(); range.selectNodeContents(node);
316
+ const rect = Array.from(range.getClientRects()).find(rect => rect.width > 0 && rect.height > 0);
317
+ if (rect && style.visibility === 'visible') record(node.data, reason || 'first-line-or-letter', style,
318
+ {node, page: Math.max(0, Math.floor((rect.left - origin.left + .01) / width)), start: 0, end: node.data.length});
183
319
  continue;
184
320
  }
185
- hidden.push({node: element, value: element.style.getPropertyValue('-webkit-text-fill-color'), priority: element.style.getPropertyPriority('-webkit-text-fill-color')});
186
- element.style.setProperty('-webkit-text-fill-color', 'transparent', 'important');
321
+ const parts = await measure(node, style, font);
322
+ for (const part of parts) if (!part.font) record(part.text, part.reason, style, {...part, node});
323
+ if (parts.some(part => part.font)) pending.push({node, parts});
324
+ }
325
+ }
326
+ const markerRules = [], sheet = document.createElement('style');
327
+ for (const list of main.querySelectorAll('ol')) {
328
+ let value = Number(/(?:^|\s)rapier-ol\s+(-?\d+)/.exec(styleOf(list).counterReset)?.[1] || 0);
329
+ for (const item of list.children) {
330
+ if (item.localName !== 'li') continue;
331
+ value += Number(/(?:^|\s)rapier-ol(?:\s+(-?\d+))?/.exec(styleOf(item).counterIncrement)?.[1] || 1);
332
+ const style = window.getComputedStyle(item, '::before');
333
+ if (style.content !== 'counter(rapier-ol)' || !safe(item) || admit(style)) continue;
334
+ const font = await fontFor(style); if (!font) {record(String(value), 'marker-font-unavailable', style); continue;}
335
+ const probe = document.createElement('span');
336
+ for (const property of style) probe.style.setProperty(property, style.getPropertyValue(property));
337
+ probe.style.setProperty('visibility', 'hidden', 'important'); probe.textContent = String(value); item.prepend(probe);
338
+ const parts = await measure(probe.firstChild, style, font); probe.remove();
339
+ if (!parts.length || parts.some(part => !part.font)) continue;
340
+ parts.at(-1).text += ' ';
341
+ markers.set(item, {text: String(value) + ' ', parts, ...Object.fromEntries(['x','y','width','height','page'].map(key => [key, parts[0][key]]))});
342
+ const id = 'm' + markers.size, old = item.getAttribute('data-rapier-pdf-marker'); item.setAttribute('data-rapier-pdf-marker', id);
343
+ restoreSteps.push(() => old == null ? item.removeAttribute('data-rapier-pdf-marker') : item.setAttribute('data-rapier-pdf-marker', old));
344
+ markerRules.push('[data-rapier-pdf-marker="' + id + '"]::before{-webkit-text-fill-color:transparent!important}');
345
+ }
346
+ }
347
+ const bulletFonts = new Map();
348
+ for (const item of main.querySelectorAll('ul>li')) {
349
+ const style = window.getComputedStyle(item, '::before');
350
+ if (style.content !== '""' && style.content !== "''" || !safe(item) || style.transform !== 'none') continue;
351
+ const probe = document.createElement('span');
352
+ for (const property of style) probe.style.setProperty(property, style.getPropertyValue(property));
353
+ probe.style.setProperty('visibility', 'hidden', 'important'); item.prepend(probe);
354
+ const box = probe.getBoundingClientRect(); probe.remove();
355
+ if (!(box.width > 0 && box.height > 0)) continue;
356
+ const fill = color(style.backgroundColor), stroke = color(style.borderTopColor), border = parseFloat(style.borderTopWidth) || 0;
357
+ const radius = parseFloat(style.borderTopLeftRadius), round = radius > 0;
358
+ if (border && style.borderTopStyle !== 'solid' || !fill[3] && !border) continue;
359
+ const key = [box.width / box.height, border / box.height, round, ...fill, ...stroke].join(':');
360
+ let font = bulletFonts.get(key);
361
+ if (!font) {
362
+ const w = box.width / box.height * 1000, inset = border / box.height * 500;
363
+ const path = round ? (() => {
364
+ const x = w / 2, y = 500, rx = x - inset, ry = y - inset, k = .552284749831;
365
+ return `${x + rx} ${y} m ${x + rx} ${y + k * ry} ${x + k * rx} ${y + ry} ${x} ${y + ry} c ` +
366
+ `${x - k * rx} ${y + ry} ${x - rx} ${y + k * ry} ${x - rx} ${y} c ` +
367
+ `${x - rx} ${y - k * ry} ${x - k * rx} ${y - ry} ${x} ${y - ry} c ` +
368
+ `${x + k * rx} ${y - ry} ${x + rx} ${y - k * ry} ${x + rx} ${y} c h`;
369
+ })() : `${inset} ${inset} ${w - 2 * inset} ${1000 - 2 * inset} re`;
370
+ font = {type3: true, asset: {family: 'RapierListMarker', style: 'normal'}, units: 1000, bbox: [0, 0, w, 1000],
371
+ commands: fill.slice(0, 3).join(' ') + ' rg\n' + stroke.slice(0, 3).join(' ') + ' RG\n' + border / box.height * 1000 + ' w\n' + path + (border ? fill[3] ? ' B\n' : ' S\n' : ' f\n')};
372
+ bulletFonts.set(key, font);
373
+ }
374
+ const page = Math.max(0, Math.floor((box.left - origin.left + .01) / width));
375
+ const part = {font, glyph: 1, text: (round ? border ? '◦' : '•' : '▪') + ' ', size: box.height, rgba: [0, 0, 0, 1], skew: 0, page,
376
+ x: box.left - origin.left - page * width, y: box.top - origin.top, width: box.width, height: box.height, baseline: box.bottom - origin.top};
377
+ markers.set(item, {text: part.text, parts: [part], ...Object.fromEntries(['x','y','width','height','page'].map(key => [key, part[key]]))});
378
+ const id = 'm' + markers.size, old = item.getAttribute('data-rapier-pdf-marker'); item.setAttribute('data-rapier-pdf-marker', id);
379
+ restoreSteps.push(() => old == null ? item.removeAttribute('data-rapier-pdf-marker') : item.setAttribute('data-rapier-pdf-marker', old));
380
+ markerRules.push('[data-rapier-pdf-marker="' + id + '"]::before{visibility:hidden!important}');
381
+ }
382
+ for (const list of main.querySelectorAll('ol,ul')) {
383
+ let value = list.hasAttribute('start') ? Number(list.getAttribute('start')) : list.hasAttribute('reversed') ? list.children.length : 1;
384
+ for (const item of list.children) {
385
+ if (item.localName !== 'li') continue;
386
+ if (item.hasAttribute('value')) value = Number(item.getAttribute('value'));
387
+ const ordinal = value; value += list.hasAttribute('reversed') ? -1 : 1;
388
+ if (markers.has(item) || !safe(item)) continue;
389
+ const style = window.getComputedStyle(item, '::marker'), itemStyle = styleOf(item);
390
+ if (style.content !== 'normal' || itemStyle.listStyleType === 'none' || itemStyle.listStyleImage !== 'none' || admit(style)) continue;
391
+ const type = itemStyle.listStyleType;
392
+ const text = type === 'decimal' ? ordinal + '. ' : type === 'decimal-leading-zero' ? String(ordinal).padStart(2, '0') + '. ' :
393
+ type === 'disc' ? '• ' : type === 'circle' ? '◦ ' : type === 'square' ? '▪ ' : null;
394
+ if (!text) {record('[' + type + ' marker ' + ordinal + ']', 'marker-style', style, {node: item}); continue;}
395
+ const first = document.createTreeWalker(item, 4).nextNode();
396
+ if (!first) continue;
397
+ const range = document.createRange(); range.selectNodeContents(first);
398
+ const line = Array.from(range.getClientRects()).find(rect => rect.height > 0 && rect.width > 0); if (!line) continue;
399
+ const probe = document.createElement('span');
400
+ for (const property of style) probe.style.setProperty(property, style.getPropertyValue(property));
401
+ probe.style.cssText += ';position:absolute;left:0;top:0;white-space:pre;visibility:hidden';
402
+ probe.textContent = text; main.append(probe);
403
+ const box = probe.getBoundingClientRect(), parts = await measure(probe.firstChild, style, await fontFor(style)); probe.remove();
404
+ if (!parts.length || parts.some(part => !part.font)) {record(text, 'marker-font-unavailable', style, {node: item}); continue;}
405
+ const edge = itemStyle.listStylePosition === 'inside' ? line.left : item.getBoundingClientRect().left + parseFloat(itemStyle.borderLeftWidth) + parseFloat(itemStyle.paddingLeft);
406
+ const left = edge - box.width, page = Math.max(0, Math.floor((edge - origin.left + .01) / width));
407
+ for (const part of parts) {
408
+ const dx = left - box.left + (part.page - page) * width, dy = line.top - box.top;
409
+ part.x += dx; part.y += dy; part.baseline += dy; part.page = page;
410
+ }
411
+ markers.set(item, {text, parts, ...Object.fromEntries(['x','y','width','height','page'].map(key => [key, parts[0][key]]))});
412
+ const id = 'm' + markers.size, old = item.getAttribute('data-rapier-pdf-marker'); item.setAttribute('data-rapier-pdf-marker', id);
413
+ restoreSteps.push(() => old == null ? item.removeAttribute('data-rapier-pdf-marker') : item.setAttribute('data-rapier-pdf-marker', old));
414
+ markerRules.push('[data-rapier-pdf-marker="' + id + '"]::marker{color:transparent!important;-webkit-text-fill-color:transparent!important}');
415
+ }
416
+ }
417
+ const pin = (element, property, value) => {
418
+ const old = element.getAttribute('style');
419
+ element.style.setProperty(property, value, 'important');
420
+ restoreSteps.push(() => old == null ? element.removeAttribute('style') : element.setAttribute('style', old));
421
+ };
422
+ const elements = [main, ...main.querySelectorAll('*')].filter(element => element.namespaceURI === 'http://www.w3.org/1999/xhtml' || ['text','tspan'].includes(element.localName));
423
+ const originals = elements.map(element => ({element, family: styleOf(element).fontFamily, fill: styleOf(element).webkitTextFillColor, svgFill: styleOf(element).fill,
424
+ pseudos: ['::before', '::after', '::marker'].map(pseudo => ({pseudo, style: window.getComputedStyle(element, pseudo), fill: window.getComputedStyle(element, pseudo).webkitTextFillColor}))}));
425
+ const inheritedRules = [];
426
+ for (const [index, {element, family, fill, svgFill, pseudos}] of originals.entries()) {
427
+ pin(element, 'font-family', family); pin(element, '-webkit-text-fill-color', fill);
428
+ if (element.namespaceURI === 'http://www.w3.org/2000/svg') pin(element, 'fill', svgFill);
429
+ const old = element.getAttribute('data-rapier-pdf-element'); element.setAttribute('data-rapier-pdf-element', index);
430
+ restoreSteps.push(() => old == null ? element.removeAttribute('data-rapier-pdf-element') : element.setAttribute('data-rapier-pdf-element', old));
431
+ for (const {pseudo, style, fill} of pseudos) if (style.content && style.content !== 'none' && (style.content !== 'normal' || pseudo === '::marker'))
432
+ inheritedRules.push('[data-rapier-pdf-element="' + index + '"]' + pseudo + '{-webkit-text-fill-color:' + fill + '!important}');
433
+ }
434
+ const byElement = new Map();
435
+ for (const row of pending) {const element = row.node.parentElement; if (!byElement.has(element)) byElement.set(element, []); byElement.get(element).push(row);}
436
+ let maskSequence = 0; const maskRules = [];
437
+ for (const [element, rows] of byElement) {
438
+ const all = rows.every(row => row.parts.every(part => part.font));
439
+ const direct = Array.from(element.childNodes).filter(node => node.nodeType === 3 && node.data.trim());
440
+ if (all && direct.every(node => rows.some(row => row.node === node))) {
441
+ pin(element, element.namespaceURI === 'http://www.w3.org/2000/svg' ? 'fill' : '-webkit-text-fill-color', 'transparent');
442
+ for (const {node, parts} of rows) nodes.set(node, parts);
443
+ continue;
444
+ }
445
+ if (!maskFont) {for (const {node, parts} of rows) for (const part of parts) if (part.font) record(part.text, 'font-mask-unavailable', styleOf(element), {...part,node}); continue;}
446
+ const fonts = new Map(), forbidden = new Set();
447
+ for (const {node, parts} of rows) for (let at = 0; at < parts.length; at++) if (parts[at].reason === 'shaping') {
448
+ let start = at, end = at + 1;
449
+ while (start && !/\s/u.test(parts[start - 1].text)) start--;
450
+ while (end < parts.length && !/\s/u.test(parts[end].text)) end++;
451
+ for (let index = start; index < end; index++) if (parts[index].font) {
452
+ record(parts[index].text, 'shaping-context', styleOf(element), {...parts[index],node}); parts[index].font = null;
453
+ }
454
+ }
455
+ for (const {parts} of rows) for (const part of parts) if (!part.font) for (const character of part.text) forbidden.add(character.codePointAt(0));
456
+ for (const {node, parts} of rows) for (const part of parts) if (part.font) {
457
+ if ([...part.text].some(character => forbidden.has(character.codePointAt(0)))) {
458
+ record(part.text, 'shaping-context', styleOf(element), {...part,node}); part.font = null; continue;
459
+ }
460
+ if (!fonts.has(part.font)) fonts.set(part.font, {glyphs: new Set(), codes: new Set()});
461
+ const entry = fonts.get(part.font); entry.glyphs.add(part.glyph);
462
+ for (const character of part.text) entry.codes.add(character.codePointAt(0));
463
+ }
464
+ const names = [], style = styleOf(element);
465
+ for (const [font, {glyphs, codes}] of fonts) {
466
+ const name = 'rapier-pdf-mask-' + maskSequence++;
467
+ const bytes = await maskFont(font.bytes, [...glyphs]), weight = style.fontWeight, faceStyle = font.italic ? 'italic' : 'normal';
468
+ const primary = font.asset.family.toLowerCase() === familyName(style.fontFamily.split(',')[0]).toLowerCase();
469
+ const unicodeRange = primary ? 'U+0-10FFFF' : [...codes].map(code => 'U+' + code.toString(16)).join(',');
470
+ const face = new window.FontFace(name, bytes, {weight, style: faceStyle, unicodeRange});
471
+ await face.load(); document.fonts.add(face); restoreSteps.push(() => document.fonts.delete(face)); names.push('"' + name + '"');
472
+ let binary = ''; for (let at = 0; at < bytes.length; at += 16384) binary += String.fromCharCode(...bytes.subarray(at, at + 16384));
473
+ maskRules.push('@font-face{font-family:"' + name + '";font-weight:' + weight + ';font-style:' + faceStyle + ';unicode-range:' + unicodeRange + ';src:url(data:font/ttf;base64,' + btoa(binary) + ')}');
187
474
  }
475
+ if (names.length) pin(element, 'font-family', names.join(',') + ',' + style.fontFamily);
476
+ for (const {node, parts} of rows) nodes.set(node, parts);
188
477
  }
189
- check(); return {nodes, restore};
478
+ sheet.textContent = [...maskRules, ...inheritedRules, ...markerRules].join('\n');
479
+ document.head.append(sheet); restoreSteps.push(() => sheet.remove());
480
+ main.getBoundingClientRect(); await document.fonts.ready;
481
+ check(); return {nodes, markers, painted: painted.map(({node, end, ...row}) => row), restore};
190
482
  } catch (error) {restore(); throw error;}
191
483
  }
@@ -92,9 +92,9 @@ function geometry(node) {
92
92
  unsupported();
93
93
  }
94
94
 
95
- export async function collectPdfVectors(main, {width, height, count, capture, signal, current, check, maxBytes, sanitizeSvg}) {
95
+ export async function collectPdfVectors(main, {width, height, count, capture, signal, current, check, maxBytes, sanitizeSvg, collectText}) {
96
96
  const document = main.ownerDocument, window = document.defaultView, origin = main.getBoundingClientRect();
97
- const pages = Array.from({length: count}, () => []), hidden = [], temporary = [], textRoots = [];
97
+ const pages = Array.from({length: count}, () => []), hidden = [], temporary = [], textRoots = [], textRestores = [];
98
98
  const colorCanvas = document.createElement('canvas'); colorCanvas.width = colorCanvas.height = 1;
99
99
  const colorContext = colorCanvas.getContext('2d', {willReadFrequently: true});
100
100
  if (!colorContext) return {pages, textRoots, restore() {}};
@@ -112,7 +112,7 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
112
112
  if (!colors.has(text)) { colorContext.clearRect(0, 0, 1, 1); colorContext.fillStyle = text; colorContext.fillRect(0, 0, 1, 1); colors.set(text, Array.from(colorContext.getImageData(0, 0, 1, 1).data, n => n / 255)); }
113
113
  return colors.get(text);
114
114
  };
115
- const restore = () => { for (const {node, value, priority} of hidden) {if (value) node.style.setProperty('visibility', value, priority); else node.style.removeProperty('visibility');} for (const node of temporary) node.remove(); colorCanvas.width = colorCanvas.height = 0; };
115
+ const restore = () => { for (const undo of textRestores.reverse()) undo(); textRestores.length = 0; for (const {node, style} of hidden) {if (style == null) node.removeAttribute('style'); else node.setAttribute('style', style);} for (const node of temporary) node.remove(); colorCanvas.width = colorCanvas.height = 0; };
116
116
  const roots = Array.from(main.querySelectorAll('svg,img')).filter(node => node.localName === 'img' || !node.ownerSVGElement);
117
117
  try {
118
118
  for (const original of roots) {
@@ -132,7 +132,8 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
132
132
  root.style.setProperty('width', originalBox.width + 'px', 'important'); root.style.setProperty('height', originalBox.height + 'px', 'important');
133
133
  mount = document.createElement('div'); mount.style.cssText = `position:fixed;left:${originalBox.left}px;top:${originalBox.top}px;width:${originalBox.width}px;height:${originalBox.height}px;pointer-events:none`;
134
134
  mount.attachShadow({mode: 'open'}).append(root); document.body.append(mount); temporary.push(mount); await document.fonts.ready;
135
- textRoots.push(root);
135
+ const glyphs = await collectText?.(root); if (glyphs) textRestores.push(glyphs.restore);
136
+ textRoots.push({root, original, text: glyphs});
136
137
  }
137
138
  const box = root.getBoundingClientRect(), nodes = [root, ...root.querySelectorAll('*')], sequence = [];
138
139
  const raster = async node => {
@@ -193,12 +194,12 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
193
194
  sequence.push({commands, fillAlpha, strokeAlpha}); geometries++;
194
195
  };
195
196
  await walk(root);
196
- if (!geometries) continue;
197
+ if (!geometries && !textRoots.find(entry => entry.root === root)?.text?.nodes.size) continue;
197
198
  const painted = [];
198
199
  for (const piece of sequence) {const held = piece.raster ? await raster(piece.raster) : piece; if (held) painted.push(held);}
199
200
  const first = Math.max(0, Math.floor((box.left - origin.left + .01) / width)), last = Math.min(count - 1, Math.floor((box.right - origin.left - .01) / width));
200
201
  for (let page = first; page <= last; page++) pages[page].push({sequence: painted, x: -page * width, clip: {x: box.left - origin.left, y: box.top - origin.top, width: box.width, height: box.height}});
201
- hidden.push({node: original, value: original.style.getPropertyValue('visibility'), priority: original.style.getPropertyPriority('visibility')}); original.style.setProperty('visibility', 'hidden', 'important');
202
+ hidden.push({node: original, style: original.getAttribute('style')}); original.style.setProperty('visibility', 'hidden', 'important');
202
203
  }
203
204
  return {pages, textRoots, restore};
204
205
  } catch (error) {restore(); throw error;}