@openpresentation/opf-render 0.18.1 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/font-subset.js +7 -1
- package/dist/fonts.d.ts +2 -1
- package/dist/player.js +2 -1
- package/dist/raster-text.js +69 -10
- package/dist/raster.js +6 -2
- package/dist/script-fonts.js +7 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -278,7 +278,7 @@ The default changed from raster to vector in the release that added this section
|
|
|
278
278
|
- `signal` (an `AbortSignal`, checked between pages) and `onProgress({ page, pages })` serve large decks; the same two options also work in the Node `toPdf`. The browser export yields to the page between slides.
|
|
279
279
|
|
|
280
280
|
Verified in Chromium by `test/export-browser.mjs` (`npm run test:export-browser`), which also checks the text pdf.js extracts, the eight JPEG orientations, offline operation and determinism. Safari and Firefox are not exercised by this repository's CI.
|
|
281
|
-
Complex scripts in raster output: resvg draws a HarfBuzz cluster with the advance of its widest glyph, so Indic, Thai, Lao, Khmer and Myanmar text lost the advance of vowel signs and ran together, and it ignores the SVG `lang` (Korean spacing). The raster path therefore rewrites its private copy of such text cluster by cluster at
|
|
281
|
+
Complex scripts in raster output: resvg draws a HarfBuzz cluster with the advance of its widest glyph, so Indic, Thai, Lao, Khmer and Myanmar text lost the advance of vowel signs and ran together, and it ignores the SVG `lang` (Korean spacing). The raster path therefore rewrites its private copy of such text cluster by cluster at HarfBuzz's positions when the fonts handle has its shaper (`fonts.outlines.shape`; the Node handle and the bundled faces always do), fontkit's otherwise (`src/raster-text.js`): Devanagari, Gujarati, Oriya, Tamil, Kannada and Sinhala as fontkit glyph outlines, the other scripts and Korean as single clusters resvg shapes in isolation. The emitted SVG is unchanged, text in other scripts is rasterized exactly as before, and `test/script-corpora-raster.mjs` holds every script to a HarfBuzz outline reference (the fixture's `rasterLimits` records what still differs: the KOR punctuation forms).
|
|
282
282
|
|
|
283
283
|
## Browser preview and fonts
|
|
284
284
|
|
package/dist/font-subset.js
CHANGED
|
@@ -16,6 +16,7 @@ import { fsTypeOf, subsettingAllowed } from "./font-fstype.js";
|
|
|
16
16
|
const SUBSET_LICENSES = new Set(["OFL-1.1", "Apache-2.0", "MIT", "UFL-1.0"]);
|
|
17
17
|
const HB_MEMORY_MODE_READONLY = 1;
|
|
18
18
|
const HB_SUBSET_SETS_LAYOUT_FEATURE_TAG = 6;
|
|
19
|
+
const HB_SUBSET_FLAGS_GLYPH_NAMES = 0x80;
|
|
19
20
|
const CACHE_LIMIT = 256;
|
|
20
21
|
|
|
21
22
|
/** The manifest faces that may be subset, by sha256: permissive license, no Reserved Font Name in the family or file name. */
|
|
@@ -56,7 +57,7 @@ export function createSubsetter(wasm) {
|
|
|
56
57
|
const exports = instance.exports;
|
|
57
58
|
const required = ["memory", "malloc", "free", "hb_blob_create", "hb_blob_destroy", "hb_blob_get_length", "hb_blob_get_data", "hb_face_create", "hb_face_destroy",
|
|
58
59
|
"hb_face_reference_blob", "hb_set_add", "hb_set_clear", "hb_set_invert", "hb_subset_input_create_or_fail", "hb_subset_input_destroy", "hb_subset_input_unicode_set",
|
|
59
|
-
"hb_subset_input_set", "hb_subset_or_fail"];
|
|
60
|
+
"hb_subset_input_set", "hb_subset_input_set_flags", "hb_subset_or_fail"];
|
|
60
61
|
for (const name of required) if (!(name in exports)) throw new Error(`harfbuzz-subset.wasm has no export ${name}.`);
|
|
61
62
|
return {
|
|
62
63
|
subset(data, codePoints) {
|
|
@@ -69,7 +70,12 @@ export function createSubsetter(wasm) {
|
|
|
69
70
|
let result = 0, output = null;
|
|
70
71
|
try {
|
|
71
72
|
if (!input) return null;
|
|
73
|
+
// macOS Chromium small-size rasterization changes when named X/o are absent, even when the text does not draw them.
|
|
74
|
+
// Keeping names alone or the outlines alone is insufficient: see the exact-pixel subset browser regression.
|
|
75
|
+
exports.hb_subset_input_set_flags(input, HB_SUBSET_FLAGS_GLYPH_NAMES);
|
|
72
76
|
const unicodes = exports.hb_subset_input_unicode_set(input);
|
|
77
|
+
exports.hb_set_add(unicodes, 0x58); // X
|
|
78
|
+
exports.hb_set_add(unicodes, 0x6f); // o
|
|
73
79
|
for (const codePoint of codePoints) exports.hb_set_add(unicodes, codePoint);
|
|
74
80
|
// Every layout feature, not only HarfBuzz's default list: the browser shapes the subset like the whole face whatever
|
|
75
81
|
// font-feature-settings the SVG asks for.
|
package/dist/fonts.d.ts
CHANGED
|
@@ -61,7 +61,8 @@ export interface TextOutlines {
|
|
|
61
61
|
outlineSlideText(content: string[], options: object): { content: string[]; defs: string };
|
|
62
62
|
/**
|
|
63
63
|
* HarfBuzz shaping of one run of a face (opf-render#188), present when the handle has the shaper (the Node handle always, a browser
|
|
64
|
-
* handle with `shapeWasm`). Read by `toPdf`, which then shapes the vector PDF's text as the browser shapes it
|
|
64
|
+
* handle with `shapeWasm`). Read by `toPdf`, which then shapes the vector PDF's text as the browser shapes it, and by `toPng` and
|
|
65
|
+
* the raster PDF, which pin complex-script clusters at its pens (opf-render#189); fontkit otherwise.
|
|
65
66
|
*/
|
|
66
67
|
shape?(face: { hash: string; data: Uint8Array }, text: string, options?: { direction?: "ltr" | "rtl"; lang?: string; features?: Record<string, boolean> }): { gid: number; codePoints: number[]; advance: number; xOffset: number; yOffset: number }[];
|
|
67
68
|
}
|
package/dist/player.js
CHANGED
|
@@ -11,7 +11,7 @@ import { loadPreviewFonts } from "./preview-fonts.js";
|
|
|
11
11
|
export { DeckError } from "./deck-runtime.js";
|
|
12
12
|
|
|
13
13
|
const AUDIENCE_CSS = `
|
|
14
|
-
:host { all: initial; }
|
|
14
|
+
:host { all: initial; color-scheme: inherit; }
|
|
15
15
|
* { box-sizing: border-box; }
|
|
16
16
|
[hidden] { display: none !important; }
|
|
17
17
|
.player { position: fixed; inset: 0; z-index: 2147483000; background: #000; color: #fff; font: 15px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; outline: none; overflow: hidden; touch-action: none; user-select: none; -webkit-user-select: none; }
|
|
@@ -37,6 +37,7 @@ const AUDIENCE_CSS = `
|
|
|
37
37
|
.msg { position: absolute; inset: 0; display: grid; place-items: center; padding: 24px; text-align: center; }
|
|
38
38
|
@media (forced-colors: active) {
|
|
39
39
|
.hud button { border-color: ButtonText; }
|
|
40
|
+
.hud button:focus-visible { outline-color: Highlight; }
|
|
40
41
|
.hud button[aria-disabled="true"] { opacity: 1; color: GrayText; border-color: GrayText; }
|
|
41
42
|
}
|
|
42
43
|
`;
|
package/dist/raster-text.js
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
// where the run's glyphs do not map back to its characters, or the run has a kinzi), measured with the face resvg will pick for the run
|
|
15
15
|
// (fontdb's family, style and weight matching over the same font files, with the same glyph fallback order), the OpenType language
|
|
16
16
|
// of the SVG's lang and the policy features, scaled to the run's textLength when it has one; text-anchor middle and end are
|
|
17
|
-
// resolved into absolute starts, baseline-shift into the y
|
|
17
|
+
// resolved into absolute starts, baseline-shift into the y. When the conversion has HarfBuzz (`shape`, the fonts handle's
|
|
18
|
+
// `outlines.shape`), the pens are HarfBuzz's, the browser's shaper (fontkit kerns after a decomposed Thai sara am differently);
|
|
19
|
+
// fontkit's otherwise;
|
|
18
20
|
// - a cluster of a script whose fontkit glyph runs equal HarfBuzz's in the corpus (Devanagari, Gujarati, Oriya, Tamil, Kannada,
|
|
19
21
|
// Sinhala) is drawn as fontkit's glyph outlines (<path> per glyph), so a conjunct under a pre-base vowel is the browser's;
|
|
20
22
|
// - any other cluster (Bengali, Gurmukhi, Telugu, Malayalam, Thai, Lao, Khmer and Myanmar, where fontkit orders marks differently
|
|
@@ -31,6 +33,7 @@ import { create } from "fontkit";
|
|
|
31
33
|
import { disabledFeaturesFor } from "./font-compatibility.js";
|
|
32
34
|
import { pinGlyphCodePoints } from "./font-registry.js";
|
|
33
35
|
import { openTypeLanguage } from "./script-fonts.js";
|
|
36
|
+
import { sha256Bytes } from "./sha256.js";
|
|
34
37
|
|
|
35
38
|
const AFFECTED = /[\p{scx=Devanagari}\p{scx=Bengali}\p{scx=Gurmukhi}\p{scx=Gujarati}\p{scx=Oriya}\p{scx=Tamil}\p{scx=Telugu}\p{scx=Kannada}\p{scx=Malayalam}\p{scx=Sinhala}\p{scx=Thai}\p{scx=Lao}\p{scx=Khmer}\p{scx=Myanmar}]/u;
|
|
36
39
|
// Scripts whose fontkit glyph runs equal HarfBuzz's for every corpus sample (glyph ids, order and advances, no retry): drawn as outlines.
|
|
@@ -56,8 +59,11 @@ const PAINT = new Set(["fill", "fill-opacity", "fill-rule", "opacity", "stroke",
|
|
|
56
59
|
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
57
60
|
const faceCache = new Map();
|
|
58
61
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Rewrites complex-script text of `svg` into per-cluster positioned pieces for resvg (see the file comment). `shape` is the fonts
|
|
64
|
+
* handle's HarfBuzz shaper (`outlines.shape`): the clusters are then pinned at HarfBuzz's pens, else at fontkit's.
|
|
65
|
+
*/
|
|
66
|
+
export async function pinScriptClusters(svg, { fontFiles = [], defaultFontFamily = "Roboto", sansSerifFamily, serifFamily, monospaceFamily, cursiveFamily, fantasyFamily, shape } = {}) {
|
|
61
67
|
if (!AFFECTED.test(svg) && !HANGUL.test(svg)) return svg;
|
|
62
68
|
// resvg-js names for the generic families (its own defaults where rasterizeSvg sets none) and for text without a font-family.
|
|
63
69
|
const generics = {
|
|
@@ -81,7 +87,7 @@ export async function pinScriptClusters(svg, { fontFiles = [], defaultFontFamily
|
|
|
81
87
|
const inner = svg.slice(tokens.lastIndex, closeIndex);
|
|
82
88
|
tokens.lastIndex = closeIndex + "</text>".length;
|
|
83
89
|
if (!needsRewrite(inner, props)) continue;
|
|
84
|
-
const rewritten = rewriteText({ attrs, attributes, inner, props }, await database(), generics);
|
|
90
|
+
const rewritten = rewriteText({ attrs, attributes, inner, props }, await database(), generics, typeof shape === "function" ? shape : undefined);
|
|
85
91
|
if (rewritten === null) continue;
|
|
86
92
|
output += svg.slice(end, match.index) + rewritten;
|
|
87
93
|
end = tokens.lastIndex;
|
|
@@ -218,7 +224,7 @@ export function clusterPieces(text, pinAll = false) {
|
|
|
218
224
|
return pieces;
|
|
219
225
|
}
|
|
220
226
|
|
|
221
|
-
function rewriteText(text, db, generics) {
|
|
227
|
+
function rewriteText(text, db, generics, shape) {
|
|
222
228
|
const root = parseInner(text.inner);
|
|
223
229
|
if (!root) return null;
|
|
224
230
|
const rootProps = text.props;
|
|
@@ -263,7 +269,7 @@ function rewriteText(text, db, generics) {
|
|
|
263
269
|
const base = selectFace(db, leaf.props, generics);
|
|
264
270
|
if (!base) return null;
|
|
265
271
|
leaf.runs = faceRuns(db, base, leaf.value);
|
|
266
|
-
const widths = measurePrefixes(leaf.runs, leaf.pieces.map(piece => piece.end), leaf.props);
|
|
272
|
+
const widths = measurePrefixes(leaf.runs, leaf.pieces.map(piece => piece.end), leaf.props, shape);
|
|
267
273
|
if (!widths) return null;
|
|
268
274
|
let previous = 0;
|
|
269
275
|
for (const [index, piece] of leaf.pieces.entries()) { piece.width = widths[index] - previous; previous = widths[index]; }
|
|
@@ -362,7 +368,7 @@ async function readFace(file) {
|
|
|
362
368
|
const names = Object.values(records.preferredFamily ?? {}).length ? Object.values(records.preferredFamily) : Object.values(records.fontFamily ?? {});
|
|
363
369
|
const os2 = font["OS/2"], selection = os2?.fsSelection;
|
|
364
370
|
return {
|
|
365
|
-
file, font, families: new Set(names.filter(name => typeof name === "string")), outlines: new Map(),
|
|
371
|
+
file, font, data, families: new Set(names.filter(name => typeof name === "string")), outlines: new Map(),
|
|
366
372
|
weight: os2?.usWeightClass ?? 400, stretch: os2?.usWidthClass ?? 5,
|
|
367
373
|
style: selection?.italic || (!os2 && font.head?.macStyle?.italic) ? "italic" : selection?.oblique ? "oblique" : "normal"
|
|
368
374
|
};
|
|
@@ -428,8 +434,8 @@ function shapeRun(face, value, props) {
|
|
|
428
434
|
catch { return { run: layout({ abvm: false, blwm: false, mark: false, mkmk: false }), retry: true }; }
|
|
429
435
|
}
|
|
430
436
|
|
|
431
|
-
/** Advance widths (px) of the prefixes of the runs' text ending at each offset of `ends
|
|
432
|
-
function measurePrefixes(runs, ends, props) {
|
|
437
|
+
/** Advance widths (px) of the prefixes of the runs' text ending at each offset of `ends`, at HarfBuzz's pens when `shape` is given. */
|
|
438
|
+
function measurePrefixes(runs, ends, props, shape) {
|
|
433
439
|
const width = (face, value) => {
|
|
434
440
|
if (!value) return 0;
|
|
435
441
|
// The Myanmar kinzi is a zero-advance mark in HarfBuzz; fontkit's universal shaper keeps the nga's advance, so it is not measured.
|
|
@@ -441,13 +447,23 @@ function measurePrefixes(runs, ends, props) {
|
|
|
441
447
|
const pens = new Map();
|
|
442
448
|
// opf-render#189: the pen at a cluster boundary inside a run is read from the run shaped as a whole, so an advance its context
|
|
443
449
|
// changes (a Thai leading vowel kerned to its consonant) is the one the browser draws; a prefix shaped alone ends without it.
|
|
450
|
+
// With the conversion's HarfBuzz (opf-render#189 follow-up), a run's pens are HarfBuzz's when it gives every boundary the run
|
|
451
|
+
// needs; a run it does not is measured with fontkit throughout, so no run mixes the two shapers.
|
|
452
|
+
const harfBuzz = new Map();
|
|
453
|
+
if (shape) for (const run of runs) {
|
|
454
|
+
const found = harfBuzzPens(run.face, run.text, props, shape);
|
|
455
|
+
if (found && ends.every(end => end <= run.start || end >= run.start + run.text.length || found.has(end - run.start))) harfBuzz.set(run, found);
|
|
456
|
+
}
|
|
444
457
|
const prefix = (run, length) => {
|
|
458
|
+
const shaped = harfBuzz.get(run)?.get(length);
|
|
459
|
+
if (shaped !== undefined) return shaped;
|
|
445
460
|
if (!pens.has(run)) pens.set(run, contextPens(run.face, run.text, props));
|
|
446
461
|
return pens.get(run)?.get(length) ?? width(run.face, run.text.slice(0, length));
|
|
447
462
|
};
|
|
463
|
+
const whole = run => harfBuzz.get(run)?.get(run.text.length) ?? width(run.face, run.text);
|
|
448
464
|
let runIndex = 0, before = 0;
|
|
449
465
|
for (const end of ends) {
|
|
450
|
-
while (runIndex < runs.length && runs[runIndex].start + runs[runIndex].text.length <= end) { before +=
|
|
466
|
+
while (runIndex < runs.length && runs[runIndex].start + runs[runIndex].text.length <= end) { before += whole(runs[runIndex]); runIndex++; }
|
|
451
467
|
const run = runs[runIndex];
|
|
452
468
|
widths.push(run && end > run.start ? before + prefix(run, end - run.start) : before);
|
|
453
469
|
}
|
|
@@ -474,6 +490,49 @@ function contextPens(face, value, props) {
|
|
|
474
490
|
return consumed === value.length ? pens : null;
|
|
475
491
|
}
|
|
476
492
|
|
|
493
|
+
/**
|
|
494
|
+
* HarfBuzz's pen (px) at each character offset of `value`, shaped as one run by `shape` (the fonts handle's `outlines.shape`), where
|
|
495
|
+
* the glyphs before it carry exactly the characters before it (a syllable boundary, also when HarfBuzz reorders inside a syllable),
|
|
496
|
+
* and at the end of the run. Null when HarfBuzz cannot shape the run or its glyphs do not carry its characters.
|
|
497
|
+
*/
|
|
498
|
+
function harfBuzzPens(face, value, props, shape) {
|
|
499
|
+
let glyphs;
|
|
500
|
+
try {
|
|
501
|
+
face.harfBuzz ??= { hash: hexDigest(sha256Bytes(face.data)), data: face.data };
|
|
502
|
+
const off = disabledFeaturesFor([...face.families][0]);
|
|
503
|
+
glyphs = shape(face.harfBuzz, value, { direction: "ltr", lang: props.lang, ...(off ? { features: Object.fromEntries(off.map(tag => [tag, false])) } : {}) });
|
|
504
|
+
} catch { return null; }
|
|
505
|
+
if (!Array.isArray(glyphs)) return null;
|
|
506
|
+
const pens = new Map(), scale = props.fontSize / face.font.unitsPerEm;
|
|
507
|
+
// How many more times each character was carried by the glyphs so far than it occurs in the text before `consumed`; `unmatched`
|
|
508
|
+
// counts the characters where that is not zero, so it is zero exactly where the glyphs so far carry the text before `consumed`.
|
|
509
|
+
const balance = new Map();
|
|
510
|
+
let consumed = 0, unmatched = 0, pen = 0;
|
|
511
|
+
const count = (character, step) => {
|
|
512
|
+
const before = balance.get(character) ?? 0, after = before + step;
|
|
513
|
+
if (!before) unmatched++;
|
|
514
|
+
if (!after) { unmatched--; balance.delete(character); } else balance.set(character, after);
|
|
515
|
+
};
|
|
516
|
+
for (const glyph of glyphs) {
|
|
517
|
+
if (glyph.codePoints?.length) {
|
|
518
|
+
if (!unmatched && !pens.has(consumed)) pens.set(consumed, pen * scale);
|
|
519
|
+
for (const point of glyph.codePoints) {
|
|
520
|
+
if (consumed >= value.length) return null;
|
|
521
|
+
const own = String.fromCodePoint(value.codePointAt(consumed));
|
|
522
|
+
count(String.fromCodePoint(point), 1);
|
|
523
|
+
count(own, -1);
|
|
524
|
+
consumed += own.length;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
pen += glyph.advance;
|
|
528
|
+
}
|
|
529
|
+
if (consumed !== value.length || unmatched) return null;
|
|
530
|
+
pens.set(value.length, pen * scale);
|
|
531
|
+
return pens;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const hexDigest = bytes => Array.from(bytes, byte => byte.toString(16).toUpperCase().padStart(2, "0")).join("");
|
|
535
|
+
|
|
477
536
|
/** The glyph outlines of one cluster, or null when fontkit needed the mark retry (its glyph placement is then not the browser's). */
|
|
478
537
|
function shapeOutlines(face, value, props) {
|
|
479
538
|
let shaped;
|
package/dist/raster.js
CHANGED
|
@@ -151,9 +151,13 @@ async function rasterizeSvg(svgInput, options) {
|
|
|
151
151
|
|
|
152
152
|
if (options.dpi !== undefined) renderOptions.dpi = positiveNumber(options.dpi, "dpi", 96);
|
|
153
153
|
|
|
154
|
+
// opf-render#189: complex-script clusters are pinned at HarfBuzz's pens (the browser's shaper) through the fonts handle's shaper,
|
|
155
|
+
// or that of the bundled handle the conversion draws with; at fontkit's when neither has one.
|
|
156
|
+
const shape = options.fonts?.outlines?.shape ?? (settings.useBundledFonts ? (await bundledFonts()).outlines?.shape : undefined);
|
|
157
|
+
const pinning = { ...font, shape };
|
|
154
158
|
// An SVG used as an image draws its text with these fonts: resvg gives the nested document none.
|
|
155
159
|
const nestedSvg = async text => {
|
|
156
|
-
const pinned = await pinScriptClusters(text,
|
|
160
|
+
const pinned = await pinScriptClusters(text, pinning);
|
|
157
161
|
const probe = new Resvg(pinned, { font, logLevel: "off" });
|
|
158
162
|
const zoom = Math.min(4, Math.max(1, 2048 / Math.max(probe.width, probe.height)));
|
|
159
163
|
return new Resvg(pinned, { ...renderOptions, fitTo: { mode: "zoom", value: zoom }, background: "rgba(0, 0, 0, 0)" }).render().asPng();
|
|
@@ -161,7 +165,7 @@ async function rasterizeSvg(svgInput, options) {
|
|
|
161
165
|
// FF-31: resvg ignores the SVG's ligature properties, so separate the letters a Gelasio ligature would join.
|
|
162
166
|
// FF-45: colour families (Noto Color Emoji) are drawn with their monochrome stand-in, which resvg can draw.
|
|
163
167
|
// FF-44: resvg loses the advance of a vowel sign or space inside a complex-script cluster, so pin each cluster (raster-text.js).
|
|
164
|
-
const svg = await pinScriptClusters(separateLigatures(monochromeColorFonts(await prepareRasterImages(normalizeSvgInput(svgInput), { nestedSvg }))),
|
|
168
|
+
const svg = await pinScriptClusters(separateLigatures(monochromeColorFonts(await prepareRasterImages(normalizeSvgInput(svgInput), { nestedSvg }))), pinning);
|
|
165
169
|
|
|
166
170
|
try {
|
|
167
171
|
const image = new Resvg(svg, renderOptions).render();
|
package/dist/script-fonts.js
CHANGED
|
@@ -615,6 +615,13 @@ export function createScriptFonts(profile = {}, measurement, options = {}) {
|
|
|
615
615
|
const push = (text, family, stack, sizeAdjust, ascent) => {
|
|
616
616
|
const own = family === style.fontFamily && (!stack || stack.length === 1);
|
|
617
617
|
const last = out.at(-1);
|
|
618
|
+
// opf#361: white space that follows a size-adjusted run (the space between two Hangul words of Malgun Gothic) stays in that run,
|
|
619
|
+
// as PowerPoint keeps it in the script font, so it is measured and drawn at the run's adjusted size. Left as its own run at the
|
|
620
|
+
// composed size between two adjusted runs, a browser drew it wider than its measured advance.
|
|
621
|
+
if (last?.sizeAdjust && !sizeAdjust) {
|
|
622
|
+
const space = /^\s+/u.exec(text)?.[0];
|
|
623
|
+
if (space) { last.text += space; text = text.slice(space.length); if (!text) return; }
|
|
624
|
+
}
|
|
618
625
|
if (last && last.family === family && last.sizeAdjust === sizeAdjust && last.lineAscent === ascent?.lineAscent && JSON.stringify(last.stack) === JSON.stringify(stack)) last.text += text;
|
|
619
626
|
else out.push({text, family, own, ...(stack && !own ? {stack} : {}), ...(sizeAdjust ? {sizeAdjust} : {}), ...(ascent ?? {})});
|
|
620
627
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-render",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"description": "Deterministic local OPF renderer for SVG, PNG, and PDF output.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"test:forced-colors-browser": "node test/forced-colors-browser.mjs artifacts/forced-colors"
|
|
143
143
|
},
|
|
144
144
|
"dependencies": {
|
|
145
|
-
"@openpresentation/opf": "^0.18.
|
|
145
|
+
"@openpresentation/opf": "^0.18.4",
|
|
146
146
|
"bidi-js": "1.1.0",
|
|
147
147
|
"fontkit": "^2.0.4",
|
|
148
148
|
"harfbuzzjs": "1.6.3",
|