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.
- package/dist/agent/vendor/hyphen-en-us/SOURCE.json +3 -3
- package/dist/agent/vendor/hyphen-en-us/patterns.js +4 -3
- package/dist/kit/export-fonts.mjs +7 -0
- package/dist/kit/ledger/authorship.mjs +10 -19
- package/dist/kit/ledger/format.mjs +1 -1
- package/dist/kit/ledger/history.mjs +12 -3
- package/dist/kit/ledger/inverse-source.mjs +9 -126
- package/dist/kit/ledger/journal-records.mjs +2 -17
- package/dist/kit/ledger/source.mjs +323 -0
- package/dist/kit/pdf-glyphs.mjs +388 -96
- package/dist/kit/pdf-vectors.mjs +7 -6
- package/dist/kit/render-markdown.mjs +15 -7
- package/dist/kit/render-print.mjs +193 -51
- package/dist/kit/render-sanitize.mjs +7 -7
- package/dist/kit/render.mjs +16 -1
- package/dist/layout/preparation.mjs +1 -1
- package/dist/spec/document-settings.mjs +5 -2
- package/dist/spec/md-assets.mjs +99 -8
- package/dist/spec/md-layout.mjs +55 -4
- package/package.json +1 -1
- package/style.css +35 -7
package/dist/kit/pdf-glyphs.mjs
CHANGED
|
@@ -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)
|
|
55
|
+
if (![4, 7].includes(u16(lookup)) || u16(lookup + 2) & 1) continue;
|
|
55
56
|
for (let k = 0; k < u16(lookup + 4); k++) {
|
|
56
|
-
|
|
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,
|
|
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,
|
|
76
|
-
const result = [];
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
103
|
-
const
|
|
104
|
-
const
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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'
|
|
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'
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/kit/pdf-vectors.mjs
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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,
|
|
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;}
|