rapier-markdown-kit 1.1.78 → 1.1.80
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/kit/render-markdown.mjs +489 -12
- package/dist/kit/render-print.mjs +264 -3
- package/dist/kit/render-sanitize.mjs +252 -13
- package/dist/kit/render-styles.mjs +5 -5
- package/dist/kit/render.mjs +33 -11
- package/dist/spec/md-assets.mjs +3 -2
- package/package.json +1 -1
- package/style.css +73 -187
|
@@ -61,6 +61,9 @@ function _rapierPrintWillFont(markers) {
|
|
|
61
61
|
u16(segments.length * 2 - power * 2), segments.map(s => u16(s[1])), u16(0), segments.map(s => u16(s[0])),
|
|
62
62
|
segments.map(s => u16(s[2])), segments.map(() => u16(0)));
|
|
63
63
|
const full = cat(u16(12), u16(0), u32(16 + wide.length * 12), u32(0), u32(wide.length), wide.map(g => cat(u32(g.start), u32(g.end), u32(g.glyph))));
|
|
64
|
+
const cmap = narrow.length <= 0xffff
|
|
65
|
+
? cat(u16(0), u16(2), u16(3), u16(1), u32(20), u16(3), u16(10), u32(20 + narrow.length), narrow, full)
|
|
66
|
+
: cat(u16(0), u16(1), u16(3), u16(10), u32(12), full);
|
|
64
67
|
const outline = cat(u16(1), u16(0), u16(-2), u16(10), u16(2), u16(2), u16(0), [1, 1, 1], u16(0), u16(10), u16(-5), u16(-2), u16(4), u16(-2), [0]);
|
|
65
68
|
const utf16 = text => Array.from(text, ch => u16(ch.charCodeAt(0)));
|
|
66
69
|
const names = [[1, 'RapierWill'], [2, 'Regular'], [4, 'RapierWill Regular'], [6, 'RapierWill-Regular']];
|
|
@@ -70,7 +73,7 @@ function _rapierPrintWillFont(markers) {
|
|
|
70
73
|
u16(50), u16(300), u16(0), new Array(10).fill(0), u32(0), u32(0), u32(0), u32(0), [78, 79, 78, 69], u16(0x40),
|
|
71
74
|
u16(order.find(code => code < 0xffff) || 0x20), u16(order.filter(code => code < 0xffff).pop() || 0x20), u16(500), u16(-500), u16(0),
|
|
72
75
|
u16(500), u16(500), u32(1), u32(0), u16(500), u16(700), u16(0), u16(32), u16(0)),
|
|
73
|
-
cmap
|
|
76
|
+
cmap,
|
|
74
77
|
glyf: Array.from({ length: count }, () => outline).flat(),
|
|
75
78
|
head: cat(u32(0x00010000), u32(0x00010000), u32(0), u32(0x5f0f3cf5), u16(1), u16(1000), new Array(16).fill(0), u16(0), u16(-2), u16(10), u16(2),
|
|
76
79
|
u16(0), u16(8), u16(2), u16(1), u16(0)),
|
|
@@ -93,7 +96,7 @@ function _rapierPrintWillFont(markers) {
|
|
|
93
96
|
const file = cat(u32(0x00010000), u16(tags.length), u16((1 << tableSelect) * 16), u16(tableSelect), u16(tags.length * 16 - (1 << tableSelect) * 16), directory, tags.map(tag => padded(tables[tag])));
|
|
94
97
|
const headAt = 12 + tags.length * 16 + tags.slice(0, tags.indexOf('head')).reduce((total, tag) => total + padded(tables[tag]).length, 0);
|
|
95
98
|
file.splice(headAt + 8, 4, ...u32((0xb1b0afba - sum(file)) >>> 0));
|
|
96
|
-
return { family: 'RapierWillCarrier', bytes: Uint8Array.from(file), size };
|
|
99
|
+
return { family: 'RapierWillCarrier', bytes: Uint8Array.from(file), size, codes: order };
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
function _rapierPrintWillMetrics(markers) {
|
|
@@ -160,6 +163,264 @@ function _rapierPrintWillPageCss() {
|
|
|
160
163
|
+ '}';
|
|
161
164
|
}
|
|
162
165
|
|
|
163
|
-
|
|
166
|
+
// The capture's own limits (agent/visual.mjs VISUAL_LIMITS): the longest edge and the most pixels one capture returns.
|
|
167
|
+
const CAPTURE_EDGE = 4096, CAPTURE_PIXELS = 4 * 1024 * 1024;
|
|
168
|
+
|
|
169
|
+
async function _rapierWritePrintPdf({root, css, canonical, filename, title, settings}, {capture, signal, current, maxBytes = 8 * 1024 * 1024} = {}) {
|
|
170
|
+
const refuse = code => { throw Object.assign(new Error(code), {code}); };
|
|
171
|
+
const check = () => {
|
|
172
|
+
if (signal?.aborted) refuse('cancelled');
|
|
173
|
+
if (current?.() === false) refuse('document_changed');
|
|
174
|
+
};
|
|
175
|
+
if (!root?.ownerDocument || typeof capture !== 'function') refuse('pdf_render_unavailable');
|
|
176
|
+
// The native printer chooses its default paper. A file made without a printer uses A4,
|
|
177
|
+
// with the same explicit document settings and default margins as the print artifact.
|
|
178
|
+
const paper = settings?.papersize || {width: 11906, height: 16838};
|
|
179
|
+
const width = paper.width / 20, height = paper.height / 20;
|
|
180
|
+
const margin = settings?.geometry ? settings.geometry.twips / 20 : null;
|
|
181
|
+
const margins = {top: margin ?? 12 * 72 / 25.4, right: margin ?? 14 * 72 / 25.4,
|
|
182
|
+
bottom: margin ?? 14 * 72 / 25.4, left: margin ?? 14 * 72 / 25.4};
|
|
183
|
+
const bodyWidth = Math.floor((width - margins.left - margins.right) * 4 / 3);
|
|
184
|
+
const bodyHeight = Math.floor((height - margins.top - margins.bottom) * 4 / 3);
|
|
185
|
+
if (bodyWidth < 1 || bodyHeight < 1 || bodyWidth * bodyHeight > 4 * 1024 * 1024) refuse('pdf_page_geometry_invalid');
|
|
186
|
+
const frame = document.createElement('iframe');
|
|
187
|
+
let rejectAbort;
|
|
188
|
+
const aborted = new Promise((_, reject) => { rejectAbort = reject; });
|
|
189
|
+
aborted.catch(() => {});
|
|
190
|
+
const abort = () => rejectAbort(Object.assign(new Error('cancelled'), {code: 'cancelled'}));
|
|
191
|
+
signal?.addEventListener('abort', abort, {once: true});
|
|
192
|
+
const step = async work => {
|
|
193
|
+
check();
|
|
194
|
+
let timer;
|
|
195
|
+
try {
|
|
196
|
+
const timeout = new Promise((_, reject) => {
|
|
197
|
+
timer = setTimeout(() => reject(Object.assign(new Error('pdf_render_timeout'), {code: 'pdf_render_timeout'})), 15000);
|
|
198
|
+
});
|
|
199
|
+
const value = await Promise.race([work, aborted, timeout]); check(); return value;
|
|
200
|
+
} finally { clearTimeout(timer); }
|
|
201
|
+
};
|
|
202
|
+
frame.setAttribute('aria-hidden', 'true');
|
|
203
|
+
frame.style.cssText = `position:fixed;left:-100000px;top:0;width:${bodyWidth}px;height:${bodyHeight}px;border:0;pointer-events:none`;
|
|
204
|
+
document.body.append(frame);
|
|
205
|
+
try {
|
|
206
|
+
check();
|
|
207
|
+
const into = frame.contentDocument;
|
|
208
|
+
if (!into?.body) refuse('pdf_render_unavailable');
|
|
209
|
+
into.documentElement.setAttribute('data-rapier-theme', 'light');
|
|
210
|
+
into.documentElement.setAttribute('data-highlights', document.documentElement.getAttribute('data-highlights') || 'standard');
|
|
211
|
+
into.body.className = 'light';
|
|
212
|
+
const sheet = into.createElement('style'); sheet.textContent = String(css || ''); into.head.append(sheet);
|
|
213
|
+
const printRules = [];
|
|
214
|
+
const collect = rules => {
|
|
215
|
+
for (const rule of rules) {
|
|
216
|
+
if (rule.media?.mediaText === 'print') printRules.push(...Array.from(rule.cssRules, item => item.cssText));
|
|
217
|
+
else if (rule.cssRules && !rule.media) collect(rule.cssRules);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
collect(sheet.sheet.cssRules);
|
|
221
|
+
const printed = into.createElement('style');
|
|
222
|
+
printed.textContent = printRules.join('\n') + `\nhtml,body{width:${bodyWidth}px!important;height:${bodyHeight}px!important;margin:0!important;padding:0!important;overflow:visible!important}`;
|
|
223
|
+
into.head.append(printed);
|
|
224
|
+
const surface = into.createElement('div'), strip = into.createElement('div');
|
|
225
|
+
surface.style.cssText = `width:${bodyWidth}px;height:${bodyHeight}px;overflow:hidden;position:relative;background:#fff`;
|
|
226
|
+
strip.style.cssText = `width:${bodyWidth}px;height:${bodyHeight}px;position:relative`;
|
|
227
|
+
strip.append(root); surface.append(strip); into.body.append(surface);
|
|
228
|
+
const main = root.querySelector('main');
|
|
229
|
+
if (!main) refuse('pdf_render_unavailable');
|
|
230
|
+
// A file for paper is light whatever the person's own scheme: the page's text takes its colour scheme from this attribute.
|
|
231
|
+
main.setAttribute('data-md-theme', 'light');
|
|
232
|
+
for (const details of main.querySelectorAll('details')) details.open = true;
|
|
233
|
+
main.getBoundingClientRect();
|
|
234
|
+
await step(into.fonts.ready);
|
|
235
|
+
for (const image of main.querySelectorAll('img')) { image.loading = 'eager'; await step(image.decode()); }
|
|
236
|
+
check();
|
|
237
|
+
// The print sheet owns the words and styles. CSS fragmentation supplies the file's
|
|
238
|
+
// pages without a print dialog; its break constraints become column constraints here.
|
|
239
|
+
for (const node of [main, ...main.querySelectorAll('*')]) {
|
|
240
|
+
const style = into.defaultView.getComputedStyle(node);
|
|
241
|
+
for (const name of ['break-before', 'break-after', 'break-inside']) {
|
|
242
|
+
const value = style.getPropertyValue(name);
|
|
243
|
+
if (value.includes('page')) node.style.setProperty(name, value.replace('page', 'column'), 'important');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const [name, value] of Object.entries({width: bodyWidth + 'px', height: bodyHeight + 'px',
|
|
247
|
+
'max-width': 'none', 'min-height': '0', padding: '0', margin: '0', overflow: 'visible',
|
|
248
|
+
'column-width': bodyWidth + 'px', 'column-gap': '0', 'column-count': 'auto', 'column-fill': 'auto'}))
|
|
249
|
+
main.style.setProperty(name, value, 'important');
|
|
250
|
+
const count = Math.max(1, Math.ceil((main.scrollWidth - 1) / bodyWidth));
|
|
251
|
+
if (!Number.isSafeInteger(count)) refuse('pdf_page_geometry_invalid');
|
|
252
|
+
const textPages = await _rapierPrintTextPages(main, bodyWidth, count, check);
|
|
253
|
+
const text = textPages.flatMap(page => page.map(run => run.text));
|
|
254
|
+
const glyphs = new Set();
|
|
255
|
+
for (const line of text) for (const character of line) {
|
|
256
|
+
glyphs.add(character);
|
|
257
|
+
if (glyphs.size > 65000) refuse('pdf_text_glyph_limit');
|
|
258
|
+
}
|
|
259
|
+
const font = _rapierPrintWillFont(text);
|
|
260
|
+
strip.style.width = count * bodyWidth + 'px';
|
|
261
|
+
// A capture costs the whole document, however little of it the clip takes, so the pages that carry no picture share captures: as many to one
|
|
262
|
+
// as the capture's own limits allow, fewer when the capture refuses its size. A page with a picture is captured alone, so the picture keeps
|
|
263
|
+
// the capture's byte limit to itself.
|
|
264
|
+
let share = main.querySelector('img,svg,canvas,video,picture') ? 1
|
|
265
|
+
: Math.max(1, Math.min(Math.floor(CAPTURE_EDGE / bodyWidth), Math.floor(CAPTURE_PIXELS / (bodyWidth * bodyHeight))));
|
|
266
|
+
const pages = [];
|
|
267
|
+
let imageBytes = 0;
|
|
268
|
+
for (let first = 0; first < count;) {
|
|
269
|
+
check();
|
|
270
|
+
const span = Math.min(share, count - first);
|
|
271
|
+
surface.style.width = span * bodyWidth + 'px';
|
|
272
|
+
strip.style.transform = `translateX(${-first * bodyWidth}px)`;
|
|
273
|
+
let captured;
|
|
274
|
+
try { captured = await capture({root: surface, clip: {x: 0, y: 0, width: span * bodyWidth, height: bodyHeight}, signal, current}); }
|
|
275
|
+
catch (error) {
|
|
276
|
+
if (error?.code === 'visual_too_large' && span > 1) { share = Math.ceil(span / 2); continue; }
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
check();
|
|
280
|
+
const image = new into.defaultView.Image();
|
|
281
|
+
image.src = 'data:' + captured.mimeType + ';base64,' + captured.data;
|
|
282
|
+
try {
|
|
283
|
+
await step(image.decode());
|
|
284
|
+
for (let offset = 0; offset < span; offset++) {
|
|
285
|
+
const canvas = into.createElement('canvas'); canvas.width = bodyWidth; canvas.height = bodyHeight;
|
|
286
|
+
try {
|
|
287
|
+
const context = canvas.getContext('2d', {alpha: false});
|
|
288
|
+
if (!context) refuse('pdf_render_unavailable');
|
|
289
|
+
context.fillStyle = '#fff'; context.fillRect(0, 0, bodyWidth, bodyHeight);
|
|
290
|
+
context.drawImage(image, offset * bodyWidth, 0, bodyWidth, bodyHeight, 0, 0, bodyWidth, bodyHeight);
|
|
291
|
+
const blob = await step(new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.95)));
|
|
292
|
+
if (!blob) refuse('pdf_render_unavailable');
|
|
293
|
+
imageBytes += blob.size;
|
|
294
|
+
if (imageBytes > maxBytes) refuse('export_too_large');
|
|
295
|
+
pages.push(new Uint8Array(await blob.arrayBuffer()));
|
|
296
|
+
} finally { canvas.width = canvas.height = 0; }
|
|
297
|
+
}
|
|
298
|
+
} finally { image.src = ''; }
|
|
299
|
+
first += span;
|
|
300
|
+
}
|
|
301
|
+
check();
|
|
302
|
+
const bytes = await writeRasterPdf({pages, textPages, canonical, filename, title, font, width, height, margins,
|
|
303
|
+
pixelWidth: bodyWidth, pixelHeight: bodyHeight, pageNumbers: settings?.pagestyle === 'plain', maxBytes, check});
|
|
304
|
+
check();
|
|
305
|
+
return {bytes, pages: count};
|
|
306
|
+
} finally { signal?.removeEventListener('abort', abort); frame.remove(); }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Ranges locate the actual text fragments after column layout. Splitting a text node at
|
|
310
|
+
// its line and page boundaries keeps a PDF search on the page that carries those words.
|
|
311
|
+
async function _rapierPrintTextPages(main, pageWidth, count, check) {
|
|
312
|
+
const into = main.ownerDocument, origin = main.getBoundingClientRect();
|
|
313
|
+
const pages = Array.from({length: count}, () => []), walker = into.createTreeWalker(main, 4);
|
|
314
|
+
const range = into.createRange();
|
|
315
|
+
let nodes = 0, operations = 0;
|
|
316
|
+
for (let node; (node = walker.nextNode());) {
|
|
317
|
+
if (++nodes % 128 === 0) { await new Promise(resolve => setTimeout(resolve, 0)); check(); }
|
|
318
|
+
const style = into.defaultView.getComputedStyle(node.parentElement);
|
|
319
|
+
if (style.visibility === 'hidden' || style.visibility === 'collapse') continue;
|
|
320
|
+
const preserve = /pre|break-spaces/.test(style.whiteSpace);
|
|
321
|
+
const block = node.parentElement.closest('p,h1,h2,h3,h4,h5,h6,li,pre,td,th,figcaption,blockquote,.rapier-will-carrier') || node.parentElement;
|
|
322
|
+
const carrier = node.parentElement.closest('.rapier-will-carrier');
|
|
323
|
+
const value = node.data;
|
|
324
|
+
const pageOf = x => Math.max(0, Math.min(count - 1, Math.floor((x - origin.left + .01) / pageWidth)));
|
|
325
|
+
const visit = (start, end) => {
|
|
326
|
+
if (++operations % 128 === 0) check();
|
|
327
|
+
range.setStart(node, start); range.setEnd(node, end);
|
|
328
|
+
const rects = Array.from(range.getClientRects()).filter(rect => rect.width > 0 && rect.height > 0);
|
|
329
|
+
if (!rects.length) return;
|
|
330
|
+
const first = rects[0], page = pageOf(first.left);
|
|
331
|
+
const oneLine = rects.every(rect => pageOf(rect.left) === page && pageOf(rect.right - .02) === page && Math.abs(rect.top - first.top) < .5);
|
|
332
|
+
let middle = Math.floor((start + end) / 2);
|
|
333
|
+
if (middle > start && /[\uD800-\uDBFF]/.test(value[middle - 1]) && /[\uDC00-\uDFFF]/.test(value[middle])) middle--;
|
|
334
|
+
if (!oneLine && middle > start && middle < end) { visit(start, middle); visit(middle, end); return; }
|
|
335
|
+
let text = value.slice(start, end);
|
|
336
|
+
text = preserve ? text.replace(/\r\n?|\n/g, '') : text.replace(/[ \t\r\n\f]+/g, ' ');
|
|
337
|
+
if (!text) return;
|
|
338
|
+
const x = Math.min(...rects.map(rect => rect.left)) - origin.left - page * pageWidth;
|
|
339
|
+
const y = Math.min(...rects.map(rect => rect.top)) - origin.top;
|
|
340
|
+
const right = Math.max(...rects.map(rect => rect.right)) - origin.left - page * pageWidth;
|
|
341
|
+
const bottom = Math.max(...rects.map(rect => rect.bottom)) - origin.top;
|
|
342
|
+
const last = pages[page].at(-1);
|
|
343
|
+
if (last && last.block === block && last.carrier === carrier && Math.abs(last.y - y) < Math.max(2, Math.min(last.height, bottom - y) * .2)) {
|
|
344
|
+
last.text = preserve ? last.text + text : (last.text + text).replace(/[ \t\r\n\f]+/g, ' ');
|
|
345
|
+
last.width = Math.max(last.x + last.width, right) - Math.min(last.x, x);
|
|
346
|
+
last.height = Math.max(last.y + last.height, bottom) - Math.min(last.y, y);
|
|
347
|
+
last.x = Math.min(last.x, x); last.y = Math.min(last.y, y);
|
|
348
|
+
} else pages[page].push({text, x, y, width: right - x, height: bottom - y, block, carrier});
|
|
349
|
+
};
|
|
350
|
+
visit(0, value.length);
|
|
351
|
+
}
|
|
352
|
+
check();
|
|
353
|
+
return pages.map(page => page.map(({block, carrier, ...run}) => run));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {_rapierWillSentinelPlan, _rapierPrintWillAdmit, _rapierPrintWillFont, _rapierPrintWillMetrics, _rapierPrintWillPlace, _rapierPrintWillLost, _rapierPrintWillPageCss, _rapierWritePrintPdf};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// A standard PDF page tree, image streams, Unicode text and an associated source file.
|
|
360
|
+
// It accepts already-rendered pages, so there is no second document grammar or layout engine.
|
|
361
|
+
async function writeRasterPdf({pages, textPages, canonical, filename, title, font, width, height, margins, pixelWidth, pixelHeight, pageNumbers, maxBytes, check}) {
|
|
362
|
+
const encoder = new TextEncoder(), encode = value => encoder.encode(value);
|
|
363
|
+
const fail = code => { throw Object.assign(new Error(code), {code}); };
|
|
364
|
+
const concat = parts => {
|
|
365
|
+
const length = parts.reduce((sum, part) => sum + part.length, 0), bytes = new Uint8Array(length);
|
|
366
|
+
let at = 0; for (const part of parts) {bytes.set(part, at); at += part.length;} return bytes;
|
|
367
|
+
};
|
|
368
|
+
const compressed = async bytes => {
|
|
369
|
+
check();
|
|
370
|
+
const pipe = new Blob([bytes]).stream().pipeThrough(new CompressionStream('deflate'));
|
|
371
|
+
const result = new Uint8Array(await new Response(pipe).arrayBuffer()); check(); return result;
|
|
372
|
+
};
|
|
373
|
+
const objects = [null, null, null];
|
|
374
|
+
let used = 0;
|
|
375
|
+
const put = (id, bytes) => {
|
|
376
|
+
bytes = typeof bytes === 'string' ? encode(bytes) : bytes;
|
|
377
|
+
used += bytes.length + 32; if (used > maxBytes) fail('export_too_large'); objects[id] = bytes; return id;
|
|
378
|
+
};
|
|
379
|
+
const add = bytes => {objects.push(null); return put(objects.length - 1, bytes);};
|
|
380
|
+
const stream = (dict, bytes) => add(concat([encode('<<' + dict + ' /Length ' + bytes.length + '>>\nstream\n'), bytes, encode('\nendstream')]));
|
|
381
|
+
const hex = value => Array.from({length: value.length}, (_, index) => value.charCodeAt(index).toString(16).padStart(4, '0')).join('');
|
|
382
|
+
const literal = value => '(' + String(value).replace(/[\\()\r\n]/g, character => ({'\\': '\\\\', '(': '\\(', ')': '\\)', '\r': '\\r', '\n': '\\n'}[character])) + ')';
|
|
383
|
+
const source = encoder.encode(canonical);
|
|
384
|
+
const embedded = stream('/Type /EmbeddedFile /Subtype /text#2Fplain /Params <</Size ' + source.length + '>> /Filter /FlateDecode', await compressed(source));
|
|
385
|
+
const file = add('<< /Type /Filespec /F ' + literal(String(filename).replace(/[^\x20-\x7e]/g, '_')) + ' /UF <feff' + hex(filename) + '> /AFRelationship /Source /EF << /F ' + embedded + ' 0 R /UF ' + embedded + ' 0 R >> >>');
|
|
386
|
+
const codes = new Map(font.codes.map((code, index) => [code, index + 1]));
|
|
387
|
+
const rows = font.codes.map((code, index) => '<' + (index + 1).toString(16).padStart(4, '0') + '> <' + hex(String.fromCodePoint(code)) + '>');
|
|
388
|
+
let cmap = '/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n/CMapName /RapierText def\n/CMapType 2 def\n1 begincodespacerange\n<0000> <ffff>\nendcodespacerange\n';
|
|
389
|
+
for (let at = 0; at < rows.length; at += 100) cmap += Math.min(100, rows.length - at) + ' beginbfchar\n' + rows.slice(at, at + 100).join('\n') + '\nendbfchar\n';
|
|
390
|
+
cmap += 'endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend';
|
|
391
|
+
const mapping = stream('/Filter /FlateDecode', await compressed(encode(cmap)));
|
|
392
|
+
const fontFile = stream('/Length1 ' + font.bytes.length + ' /Filter /FlateDecode', await compressed(font.bytes));
|
|
393
|
+
const descriptor = add('<< /Type /FontDescriptor /FontName /RapierText /Flags 4 /FontBBox [0 -2 10 2] /ItalicAngle 0 /Ascent 500 /Descent -500 /CapHeight 500 /StemV 80 /FontFile2 ' + fontFile + ' 0 R >>');
|
|
394
|
+
const descendant = add('<< /Type /Font /Subtype /CIDFontType2 /BaseFont /RapierText /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ' + descriptor + ' 0 R /DW 500 /CIDToGIDMap /Identity >>');
|
|
395
|
+
const type = add('<< /Type /Font /Subtype /Type0 /BaseFont /RapierText /Encoding /Identity-H /DescendantFonts [' + descendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
|
|
396
|
+
const numberFont = pageNumbers ? add('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>') : null;
|
|
397
|
+
const kids = [], decimal = value => Number(value.toFixed(4));
|
|
398
|
+
for (let index = 0; index < pages.length; index++) {
|
|
399
|
+
check();
|
|
400
|
+
const picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode', pages[index]);
|
|
401
|
+
let commands = 'q\n' + [decimal(pixelWidth * .75), 0, 0, decimal(pixelHeight * .75), decimal(margins.left), decimal(height - margins.top - pixelHeight * .75)].join(' ') + ' cm\n/Im0 Do\nQ\n';
|
|
402
|
+
for (const run of textPages[index]) {
|
|
403
|
+
const characters = Array.from(run.text);
|
|
404
|
+
const value = characters.map(character => codes.get(character.codePointAt(0)).toString(16).padStart(4, '0')).join('');
|
|
405
|
+
const size = Math.max(.1, run.height * .75);
|
|
406
|
+
const scale = run.width * .75 / (characters.length * size * .5) * 100;
|
|
407
|
+
commands += `BT\n/F1 ${decimal(size)} Tf\n${decimal(scale)} Tz\n3 Tr\n1 0 0 1 ${decimal(margins.left + run.x * .75)} ${decimal(height - margins.top - (run.y + run.height * .8) * .75)} Tm\n<${value}> Tj\nET\n`;
|
|
408
|
+
}
|
|
409
|
+
if (numberFont) commands += `BT\n/F2 9 Tf\n0 Tr\n1 0 0 1 ${decimal(width / 2 - String(index + 1).length * 2.5)} ${decimal(margins.bottom / 2)} Tm\n(${index + 1}) Tj\nET\n`;
|
|
410
|
+
const content = stream('/Filter /FlateDecode', await compressed(encode(commands)));
|
|
411
|
+
kids.push(add('<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ' + decimal(width) + ' ' + decimal(height) + '] /Resources << /XObject << /Im0 ' + picture + ' 0 R >> /Font << /F1 ' + type + ' 0 R' + (numberFont ? ' /F2 ' + numberFont + ' 0 R' : '') + ' >> >> /Contents ' + content + ' 0 R >>'));
|
|
412
|
+
}
|
|
413
|
+
put(2, '<< /Type /Pages /Count ' + kids.length + ' /Kids [' + kids.map(id => id + ' 0 R').join(' ') + '] >>');
|
|
414
|
+
put(1, '<< /Type /Catalog /Pages 2 0 R /Names << /EmbeddedFiles << /Names [<feff' + hex(filename) + '> ' + file + ' 0 R] >> >> /AF [' + file + ' 0 R] >>');
|
|
415
|
+
const info = add('<< /Producer (Rapier) /Title <feff' + hex(title || filename.replace(/\.[^.]*$/, '')) + '> >>');
|
|
416
|
+
const parts = [encode('%PDF-1.7\n%\u00e2\u00e3\u00cf\u00d3\n')], offsets = [0];
|
|
417
|
+
let length = parts[0].length;
|
|
418
|
+
for (let id = 1; id < objects.length; id++) {
|
|
419
|
+
offsets.push(length);
|
|
420
|
+
const body = concat([encode(id + ' 0 obj\n'), objects[id], encode('\nendobj\n')]); parts.push(body); length += body.length;
|
|
421
|
+
}
|
|
422
|
+
const xref = length;
|
|
423
|
+
parts.push(encode('xref\n0 ' + objects.length + '\n0000000000 65535 f \n' + offsets.slice(1).map(at => String(at).padStart(10, '0') + ' 00000 n \n').join('') + 'trailer\n<< /Size ' + objects.length + ' /Root 1 0 R /Info ' + info + ' 0 R >>\nstartxref\n' + xref + '\n%%EOF\n'));
|
|
424
|
+
const bytes = concat(parts); if (bytes.length > maxBytes) fail('export_too_large'); return bytes;
|
|
164
425
|
}
|
|
165
426
|
export {createPrintRenderer, RAPIER_WILL_SENTINEL_MARK as sentinel};
|