documonster 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README_zh.md +1 -1
- package/dist/esm/modules/draw/index.js +20 -0
- package/dist/esm/modules/draw/raster/canvas.js +285 -21
- package/dist/esm/modules/draw/raster/glyph-outline.js +357 -0
- package/dist/esm/modules/draw/raster/glyph-rasterizer.js +10 -506
- package/dist/esm/modules/draw/raster/surface.js +6 -1
- package/dist/esm/modules/draw/raster/system-raster-font.browser.js +28 -10
- package/dist/esm/modules/draw/raster/system-raster-font.js +374 -35
- package/dist/esm/modules/pdf/builder/document-builder.js +7 -4
- package/dist/esm/modules/pdf/builder/pdf-editor.js +8 -2
- package/dist/esm/modules/pdf/font/font-embedder.js +27 -85
- package/dist/esm/modules/pdf/font/font-manager.js +230 -9
- package/dist/esm/modules/pdf/font/font-plan.js +5 -3
- package/dist/esm/modules/pdf/font/system-fonts.js +29 -1190
- package/dist/esm/modules/pdf/font/text-features.js +29 -78
- package/dist/esm/modules/pdf/font/ttf-parser.js +38 -708
- package/dist/esm/modules/pdf/render/pdf-exporter.js +1 -1
- package/dist/esm/modules/pdf/word-bridge.js +1 -1
- package/dist/esm/modules/word/convert/markdown/markdown-import.js +42 -2
- package/dist/esm/modules/word/font/font-embed.js +74 -212
- package/dist/esm/modules/word/font/text-shaping.js +9 -630
- package/dist/esm/utils/cjk.js +28 -1
- package/dist/esm/utils/complex-text.js +245 -0
- package/dist/esm/utils/errors.js +19 -0
- package/dist/esm/{modules/pdf/font/system-fonts.browser.js → utils/font-discovery.browser.js} +7 -6
- package/dist/esm/utils/font-discovery.js +1264 -0
- package/dist/esm/utils/font-metrics.js +38 -0
- package/dist/esm/utils/font-ttf.js +894 -0
- package/dist/esm/utils/text-measure.js +20 -0
- package/dist/esm/utils/text-shaping.js +711 -0
- package/dist/iife/documonster.archive.iife.min.js +1 -1
- package/dist/iife/documonster.csv.iife.min.js +1 -1
- package/dist/iife/documonster.draw.iife.min.js +2 -2
- package/dist/iife/documonster.excel.iife.min.js +50 -50
- package/dist/iife/documonster.formula.iife.min.js +1 -1
- package/dist/iife/documonster.markdown.iife.min.js +1 -1
- package/dist/iife/documonster.mermaid.iife.min.js +3 -3
- package/dist/iife/documonster.pdf.iife.min.js +33 -33
- package/dist/iife/documonster.stream.iife.min.js +1 -1
- package/dist/iife/documonster.word.iife.min.js +60 -60
- package/dist/iife/documonster.xml.iife.min.js +1 -1
- package/dist/types/modules/draw/index.d.ts +36 -3
- package/dist/types/modules/draw/raster/canvas.d.ts +84 -5
- package/dist/types/modules/draw/raster/glyph-outline.d.ts +131 -0
- package/dist/types/modules/draw/raster/glyph-rasterizer.d.ts +10 -42
- package/dist/types/modules/draw/raster/surface.d.ts +68 -1
- package/dist/types/modules/draw/raster/system-raster-font.browser.d.ts +26 -10
- package/dist/types/modules/draw/raster/system-raster-font.d.ts +60 -14
- package/dist/types/modules/draw/types.d.ts +13 -3
- package/dist/types/modules/pdf/builder/document-builder.d.ts +5 -2
- package/dist/types/modules/pdf/builder/pdf-editor.d.ts +7 -1
- package/dist/types/modules/pdf/font/font-embedder.d.ts +19 -0
- package/dist/types/modules/pdf/font/font-manager.d.ts +75 -0
- package/dist/types/modules/pdf/font/font-plan.d.ts +5 -3
- package/dist/types/modules/pdf/font/system-fonts.d.ts +25 -155
- package/dist/types/modules/pdf/font/text-features.d.ts +25 -17
- package/dist/types/modules/pdf/font/ttf-parser.d.ts +24 -89
- package/dist/types/modules/pdf/types.d.ts +15 -0
- package/dist/types/modules/word/font/text-shaping.d.ts +10 -53
- package/dist/types/utils/cjk.d.ts +22 -0
- package/dist/types/utils/complex-text.d.ts +145 -0
- package/dist/types/utils/errors.d.ts +16 -0
- package/dist/types/{modules/pdf/font/system-fonts.browser.d.ts → utils/font-discovery.browser.d.ts} +22 -8
- package/dist/types/utils/font-discovery.d.ts +237 -0
- package/dist/types/utils/font-metrics.d.ts +29 -0
- package/dist/types/utils/font-ttf.d.ts +191 -0
- package/dist/types/utils/text-shaping.d.ts +111 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -211,7 +211,7 @@ const buffer = await Workbook.toBuffer(wb);
|
|
|
211
211
|
|
|
212
212
|
```html
|
|
213
213
|
<!-- Script tag (no bundler) — one IIFE per module, each under the shared `Documonster` global -->
|
|
214
|
-
<script src="https://unpkg.com/documonster@0.
|
|
214
|
+
<script src="https://unpkg.com/documonster@0.16.0/dist/iife/documonster.excel.iife.min.js"></script>
|
|
215
215
|
<script>
|
|
216
216
|
const { Workbook, Cell } = Documonster.Excel;
|
|
217
217
|
const wb = Workbook.create();
|
package/README_zh.md
CHANGED
|
@@ -198,7 +198,7 @@ const buffer = await Workbook.toBuffer(wb);
|
|
|
198
198
|
|
|
199
199
|
```html
|
|
200
200
|
<!-- Script 标签(无需打包工具)— 每个模块一个 IIFE,共享同一个 `Documonster` 全局 -->
|
|
201
|
-
<script src="https://unpkg.com/documonster@0.
|
|
201
|
+
<script src="https://unpkg.com/documonster@0.16.0/dist/iife/documonster.excel.iife.min.js"></script>
|
|
202
202
|
<script>
|
|
203
203
|
const { Workbook, Cell } = Documonster.Excel;
|
|
204
204
|
const wb = Workbook.create();
|
|
@@ -40,5 +40,25 @@ export { POINTS_PER_PIXEL, measureText, widestText, wrapText } from "./text.js";
|
|
|
40
40
|
export { renderDrawList, renderNode } from "./render.js";
|
|
41
41
|
export { SvgSurface, toSvg } from "./svg.js";
|
|
42
42
|
export { createRasterSurface, downsample, normalizeSamples, rasterizeToRgba } from "./raster/surface.js";
|
|
43
|
+
/**
|
|
44
|
+
* Ask, before rendering, whether text can be laid out one glyph per code point.
|
|
45
|
+
*
|
|
46
|
+
* `RasterizedImage.textWarnings` reports this after the fact; these answer beforehand,
|
|
47
|
+
* so a caller can route Arabic or Devanagari to `toSvg` — which is correct for them —
|
|
48
|
+
* instead of producing pixels it will have to discard.
|
|
49
|
+
*/
|
|
50
|
+
export { isSimpleText, textFeaturesOf } from "../../utils/complex-text.js";
|
|
43
51
|
export { BasicRasterCanvas } from "./raster/canvas.js";
|
|
52
|
+
/**
|
|
53
|
+
* Parse font bytes once, to hand the result to several renders.
|
|
54
|
+
*
|
|
55
|
+
* `RasterizeOptions.fonts` accepts bytes directly, but re-parses them on every call —
|
|
56
|
+
* for a CJK face that means rebuilding a 43,000-entry `cmap` each time, and a fresh
|
|
57
|
+
* object also misses the glyph cache, which is keyed on outline identity. It is also
|
|
58
|
+
* the only way to choose a face inside a `.ttc`.
|
|
59
|
+
*
|
|
60
|
+
* `fontHasGlyph` answers coverage for any `RasterFont`, including one a consumer
|
|
61
|
+
* implemented themselves without the optional `hasGlyph` member.
|
|
62
|
+
*/
|
|
63
|
+
export { fontHasGlyph, parseRasterFont } from "./raster/glyph-outline.js";
|
|
44
64
|
export { DEFAULT_TEXT_FAMILY, IDENTITY, apply, arcToCubics, multiply, rotate, flattenPath, rectNode, roundedRectToPath, rotationOf, scale, sectorToPath, translate, uniformScale } from "./types.js";
|
|
@@ -12,12 +12,48 @@
|
|
|
12
12
|
* inside `setPixel` so every primitive — including glyph coverage — is bounded by
|
|
13
13
|
* it without each one needing its own check.
|
|
14
14
|
*/
|
|
15
|
+
import { fontHasGlyph, parseInjectedFonts } from "./glyph-outline.js";
|
|
15
16
|
import { rasterizeGlyph } from "./glyph-rasterizer.js";
|
|
16
17
|
import { STROKE_FONT } from "./stroke-font.js";
|
|
17
|
-
import {
|
|
18
|
+
import { resolveFontChain } from "#platform/modules/draw/raster/system-raster-font";
|
|
18
19
|
import { measureText } from "../text.js";
|
|
19
20
|
import { DEFAULT_TEXT_FAMILY } from "../types.js";
|
|
21
|
+
import { TextFeatureTally, isSimpleText, isWellMeasuredText } from "../../../utils/complex-text.js";
|
|
22
|
+
import { isFullWidthCodePoint, isNonPrintingControl, isZeroWidthCodePoint } from "../../../utils/font-metrics.js";
|
|
20
23
|
import { parseCssColor } from "../../../utils/svg-lex.js";
|
|
24
|
+
import { shapeTextForFace } from "../../../utils/text-shaping.js";
|
|
25
|
+
/**
|
|
26
|
+
* Tab: the one control character that is width without ink.
|
|
27
|
+
*
|
|
28
|
+
* It has no glyph in practically any font, so it is neither drawn nor reported as
|
|
29
|
+
* undrawable — but it does advance the pen, by the same half em `@utils/text-measure`
|
|
30
|
+
* charges it. Treating it as a formatting control would collapse it to nothing and
|
|
31
|
+
* pull the rest of the line left; treating it as an ordinary missing glyph would draw
|
|
32
|
+
* `?` and tell the caller to install a font.
|
|
33
|
+
*/
|
|
34
|
+
const TAB = 0x09;
|
|
35
|
+
/** Tab's advance, in em. Matches the default advance the measurer uses for it. */
|
|
36
|
+
const TAB_EM_WIDTH = 0.5;
|
|
37
|
+
/**
|
|
38
|
+
* Shape `text`, taking a contextual form only when some face in `fonts` can draw it.
|
|
39
|
+
*
|
|
40
|
+
* Substituting unconditionally is what a viewer does, and it is wrong here for the same
|
|
41
|
+
* reason it is wrong in the PDF writer: many faces publish the base letters and none of the
|
|
42
|
+
* presentation forms. Measured on one macOS host, eleven of the fifty Arabic-capable system
|
|
43
|
+
* faces are like that.
|
|
44
|
+
*
|
|
45
|
+
* And the failure was worse here than a `.notdef` box. This rasteriser has no notdef to
|
|
46
|
+
* fall back on, so a form nothing could draw painted **nothing at all** — `مرحبا` came out
|
|
47
|
+
* as an empty line — and the uncovered-code-point report then asked the caller to install a
|
|
48
|
+
* font for U+FE8E, which is not a thing anyone has. Falling back to the base letters gives
|
|
49
|
+
* up the joining and keeps the word.
|
|
50
|
+
*/
|
|
51
|
+
function shapeAgainstChain(text, fonts) {
|
|
52
|
+
const drawable = (codePoint) => fonts.some(font => fontHasGlyph(font, codePoint));
|
|
53
|
+
return shapeTextForFace(text, drawable)
|
|
54
|
+
.map(cluster => cluster.visual)
|
|
55
|
+
.join("");
|
|
56
|
+
}
|
|
21
57
|
/**
|
|
22
58
|
* Cache rasterised glyphs so repeated text at one size is rasterised once. Keyed
|
|
23
59
|
* by the outline reference (stable per font and code point) and the font size.
|
|
@@ -73,6 +109,37 @@ export class BasicRasterCanvas {
|
|
|
73
109
|
* per-primitive bounds check would miss.
|
|
74
110
|
*/
|
|
75
111
|
this.clips = [];
|
|
112
|
+
/**
|
|
113
|
+
* Every code point this canvas could not draw in any available face, accumulated
|
|
114
|
+
* across all {@link drawText} calls and never cleared.
|
|
115
|
+
*
|
|
116
|
+
* Accumulated rather than per-call because the useful question is about the
|
|
117
|
+
* finished picture, not the last label in it: `rasterizeToRgba` builds one canvas
|
|
118
|
+
* per render and reports this as `RgbaImage.uncoveredCodePoints`.
|
|
119
|
+
*
|
|
120
|
+
* A missing glyph is otherwise invisible — the pen advances and nothing is painted,
|
|
121
|
+
* which is exactly how a page of Chinese came out blank with no error raised. This
|
|
122
|
+
* lets a caller act on the gap (supply a font) instead of shipping a picture with
|
|
123
|
+
* holes in it.
|
|
124
|
+
*/
|
|
125
|
+
this.uncoveredCodePoints = new Set();
|
|
126
|
+
/**
|
|
127
|
+
* Scripts and directionality this canvas cannot lay out, accumulated per canvas.
|
|
128
|
+
*
|
|
129
|
+
* A missing glyph is one kind of wrong output; text that needs shaping is another,
|
|
130
|
+
* and a worse one, because every glyph *is* drawn — just in the wrong shape or the
|
|
131
|
+
* wrong order. Arabic comes out as disconnected isolated letters, Devanagari with
|
|
132
|
+
* its vowel signs on the wrong side of the consonant. Nothing about the image looks
|
|
133
|
+
* broken to a reader who cannot read the script.
|
|
134
|
+
*
|
|
135
|
+
* `rasterizeToRgba` surfaces this as `RasterizedImage.textWarnings`. The detection
|
|
136
|
+
* is shared with the PDF writer, which has the same limitation for the same reason
|
|
137
|
+
* — see `@utils/complex-text`.
|
|
138
|
+
*/
|
|
139
|
+
this.textFeatures = new TextFeatureTally();
|
|
140
|
+
/** Faces this canvas draws with, ahead of anything discovered on the host. */
|
|
141
|
+
this.ownFonts = [];
|
|
142
|
+
this.ownUseSystemFonts = true;
|
|
76
143
|
this.data = new Uint8Array(width * height * 4);
|
|
77
144
|
}
|
|
78
145
|
/**
|
|
@@ -611,13 +678,21 @@ export class BasicRasterCanvas {
|
|
|
611
678
|
if (!text) {
|
|
612
679
|
return;
|
|
613
680
|
}
|
|
614
|
-
//
|
|
681
|
+
// Recorded here, before either drawing path is chosen, so the finding does not
|
|
682
|
+
// depend on which one runs. Text drawn at zero alpha is therefore reported too:
|
|
683
|
+
// the statement is about whether the text *can* be laid out, which stays true when
|
|
684
|
+
// it happens to be invisible, and moving this past the paint check would mean
|
|
685
|
+
// calling it from both paths — one more place to forget.
|
|
686
|
+
this.textFeatures.note(text);
|
|
687
|
+
// Measure with the style the caller asked for. The width decides where centred and
|
|
688
|
+
// right-anchored text starts, so measuring bold text as regular shifted every such
|
|
689
|
+
// label left by the difference.
|
|
615
690
|
//
|
|
616
|
-
// The glyphs
|
|
617
|
-
//
|
|
618
|
-
// the
|
|
619
|
-
//
|
|
620
|
-
//
|
|
691
|
+
// The glyphs are chosen for the same style — `fontChain` passes it to font
|
|
692
|
+
// discovery — but the *width* still comes from the static advance tables rather
|
|
693
|
+
// than the chosen face. That is deliberate: layout has to be identical on every
|
|
694
|
+
// machine, and it cannot be if it depends on which fonts are installed. The
|
|
695
|
+
// per-glyph advances are then normalised to this width.
|
|
621
696
|
const measured = measureText(text, {
|
|
622
697
|
size: fontSize,
|
|
623
698
|
family: style.family ?? DEFAULT_TEXT_FAMILY,
|
|
@@ -626,30 +701,177 @@ export class BasicRasterCanvas {
|
|
|
626
701
|
});
|
|
627
702
|
const textWidth = measured > 0 ? measured : Math.max(1, fontSize * 0.5) * text.length;
|
|
628
703
|
const startX = anchor === "middle" ? x - textWidth / 2 : anchor === "end" ? x - textWidth : x;
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
704
|
+
// A chain, not a font: a face that covers `混合` may not cover `Mixed`, and the
|
|
705
|
+
// rasteriser has nothing to fall back to for a missing glyph except another
|
|
706
|
+
// face. The chain is ordered here; `drawTextWithFonts` picks per character.
|
|
707
|
+
// Complex scripts are shaped first: Arabic letters take their contextual forms and
|
|
708
|
+
// right-to-left runs are put in visual order, so the glyphs drawn below are the ones
|
|
709
|
+
// a reader expects rather than a row of isolated letters. `isSimpleText` gates it so
|
|
710
|
+
// that ordinary text — the overwhelming majority — takes exactly the path it did
|
|
711
|
+
// before, at exactly the same cost.
|
|
712
|
+
//
|
|
713
|
+
// `visualText` is what gets drawn; `text` is what was measured. Two independent
|
|
714
|
+
// decisions follow, and they are gated on different predicates because they answer
|
|
715
|
+
// different questions — conflating them broke a line in each direction.
|
|
716
|
+
//
|
|
717
|
+
// Shaping runs when the text might need it. Layout mode depends instead on whether
|
|
718
|
+
// the measurer's static tables describe the text: for a complex script they do not
|
|
719
|
+
// (Tamil measures at roughly half its real width), so normalising the glyph advances
|
|
720
|
+
// to that measurement squeezes the line into an overlapping mess, and the font's own
|
|
721
|
+
// advances have to be used. Latin carrying a stray bidi control is the converse case
|
|
722
|
+
// — it needs reordering but *is* measured correctly, and putting it on natural widths
|
|
723
|
+
// shifted every glyph by a rounding step, which is how inserting a zero-width control
|
|
724
|
+
// came to move visible text.
|
|
725
|
+
// The chain is resolved from the *original* text, before shaping. That order matters
|
|
726
|
+
// twice over. Font discovery should search for what the text actually is — nobody has a
|
|
727
|
+
// font installed "for U+FEE3" — and shaping needs the chain's answer, because a
|
|
728
|
+
// contextual form may only be used if something in the chain can draw it.
|
|
729
|
+
const fonts = this.fontChain(text, style);
|
|
730
|
+
const visualText = isSimpleText(text) || fonts.length === 0 ? text : shapeAgainstChain(text, fonts);
|
|
731
|
+
const useNaturalWidth = !isWellMeasuredText(text);
|
|
732
|
+
if (fonts.length > 0) {
|
|
733
|
+
// Text on natural widths is anchored and spaced by the font's own advances; the
|
|
734
|
+
// rest keeps the measured width, so centring stays identical to what it was.
|
|
735
|
+
const width = useNaturalWidth
|
|
736
|
+
? this.naturalWidth(fonts, visualText, fontSize, new Map())
|
|
737
|
+
: textWidth;
|
|
738
|
+
const left = anchor === "middle" ? x - width / 2 : anchor === "end" ? x - width : x;
|
|
739
|
+
this.drawTextWithFonts(fonts, left, y, visualText, fontSize, useNaturalWidth ? undefined : textWidth, color, rotation);
|
|
633
740
|
return;
|
|
634
741
|
}
|
|
635
|
-
// Fallback: stroke font
|
|
636
|
-
this.drawTextStroke(startX, y,
|
|
742
|
+
// Fallback: stroke font. ASCII 32–126 only — everything else becomes `?`.
|
|
743
|
+
this.drawTextStroke(startX, y, visualText, fontSize, textWidth, color, rotation);
|
|
637
744
|
}
|
|
638
|
-
|
|
745
|
+
/** Warnings about text this canvas drew but could not lay out correctly. */
|
|
746
|
+
textWarnings() {
|
|
747
|
+
return this.textFeatures.warnings("This rasteriser", {
|
|
748
|
+
contextualForms: true,
|
|
749
|
+
visualOrder: true
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Use these fonts for this canvas, instead of the process-wide registry.
|
|
754
|
+
*
|
|
755
|
+
* Scoped to the instance on purpose. This used to go through a module-level
|
|
756
|
+
* registry, which meant one render's fonts stayed in place for the next render that
|
|
757
|
+
* passed none — so two rasterisations in a process could not use different faces,
|
|
758
|
+
* and a caller who supplied a font once silently changed every later render.
|
|
759
|
+
*
|
|
760
|
+
* With `useSystemFonts: false` the chain is exactly `fonts`, which is what a
|
|
761
|
+
* reproducible render needs: font discovery cannot promise the same pixels on two
|
|
762
|
+
* machines.
|
|
763
|
+
*/
|
|
764
|
+
setFonts(fonts, options = {}) {
|
|
765
|
+
this.ownFonts = parseInjectedFonts(fonts);
|
|
766
|
+
this.ownUseSystemFonts = options.useSystemFonts ?? true;
|
|
767
|
+
}
|
|
768
|
+
/** The ordered faces to try for `text`, in the style it asked for. */
|
|
769
|
+
fontChain(text, style) {
|
|
770
|
+
return resolveFontChain(text, this.ownFonts, this.ownUseSystemFonts, style);
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Resolve one code point against the chain, returning the face that can draw it.
|
|
774
|
+
*
|
|
775
|
+
* `hasGlyph` is asked before `getOutline` because a `.notdef` gid must not count
|
|
776
|
+
* as coverage: it draws a box, so a chain has to keep looking rather than stop at
|
|
777
|
+
* the first face that technically answers.
|
|
778
|
+
*/
|
|
779
|
+
resolveGlyph(fonts, codePoint, memo) {
|
|
780
|
+
// Both passes below — measuring the advances, then drawing — ask about the same
|
|
781
|
+
// characters, and a chain lookup is a walk over every face. Memoising within the
|
|
782
|
+
// call halves it, and repeated characters cost one lookup instead of one each.
|
|
783
|
+
if (memo.has(codePoint)) {
|
|
784
|
+
return memo.get(codePoint);
|
|
785
|
+
}
|
|
786
|
+
let found;
|
|
787
|
+
for (const font of fonts) {
|
|
788
|
+
if (!fontHasGlyph(font, codePoint)) {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const outline = font.getOutline(codePoint);
|
|
792
|
+
if (outline) {
|
|
793
|
+
found = { font, outline };
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
memo.set(codePoint, found);
|
|
798
|
+
return found;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* The advance to assume for a character no face can draw.
|
|
802
|
+
*
|
|
803
|
+
* It has to agree with what `measureText` assumed for the same character, or the
|
|
804
|
+
* `hScale` correction below redistributes the difference across the rest of the
|
|
805
|
+
* line. This was a flat `fontSize * 0.4` while `measureText` gives an ideograph a
|
|
806
|
+
* full em (`@utils/text-measure`'s `wideAdvance`), so a label mixing scripts had
|
|
807
|
+
* its *surviving* Latin letters stretched apart to fill the measured width — the
|
|
808
|
+
* missing CJK was invisible, but the visible text was visibly wrong too.
|
|
809
|
+
*
|
|
810
|
+
* A zero-width character takes nothing even when the face has no glyph for it: a
|
|
811
|
+
* combining acute the font cannot draw must not push the rest of the line along.
|
|
812
|
+
*/
|
|
813
|
+
missingAdvance(codePoint, fontSize) {
|
|
814
|
+
if (isZeroWidthCodePoint(codePoint)) {
|
|
815
|
+
return 0;
|
|
816
|
+
}
|
|
817
|
+
return isFullWidthCodePoint(codePoint) ? fontSize : fontSize * 0.5;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Width of `text` as the chain's own advances give it, in user units.
|
|
821
|
+
*
|
|
822
|
+
* Used for shaped text, where the static advance tables cannot answer: they hold no
|
|
823
|
+
* entry for a presentation form, so `measureText` would report the width of the
|
|
824
|
+
* unshaped letters instead. Anchoring and normalisation then have to agree, and the
|
|
825
|
+
* only figure both can use is the one the faces actually being drawn provide.
|
|
826
|
+
*/
|
|
827
|
+
naturalWidth(fonts, text, fontSize, memo) {
|
|
828
|
+
let total = 0;
|
|
829
|
+
for (const ch of text) {
|
|
830
|
+
const code = ch.codePointAt(0);
|
|
831
|
+
if (isNonPrintingControl(code)) {
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const found = this.resolveGlyph(fonts, code, memo);
|
|
835
|
+
total += found
|
|
836
|
+
? found.outline.advanceWidth * (fontSize / found.font.unitsPerEm)
|
|
837
|
+
: this.missingAdvance(code, fontSize);
|
|
838
|
+
}
|
|
839
|
+
return total;
|
|
840
|
+
}
|
|
841
|
+
drawTextWithFonts(fonts, startX, y, text, fontSize,
|
|
842
|
+
/**
|
|
843
|
+
* Width to stretch the glyph advances onto, or `undefined` to use the font's own.
|
|
844
|
+
*
|
|
845
|
+
* Normalising is right when the width came from the static advance tables for the
|
|
846
|
+
* *same* characters — it absorbs the difference between the table's estimate and the
|
|
847
|
+
* face actually used. It is wrong after shaping: the glyphs drawn are presentation
|
|
848
|
+
* forms whose advances are deliberately narrower than the isolated letters the table
|
|
849
|
+
* measured, so stretching them to the unshaped width pulled a joined Arabic word
|
|
850
|
+
* apart into evenly-spaced letters that merely *looked* connected.
|
|
851
|
+
*/
|
|
852
|
+
normalizeTo, color, rotation) {
|
|
639
853
|
const rgba = parseSvgColor(color);
|
|
640
854
|
if (!rgba) {
|
|
641
855
|
return;
|
|
642
856
|
}
|
|
643
|
-
const scale = fontSize / font.unitsPerEm;
|
|
644
857
|
// Compute total advance from font metrics, then scale to match measured width.
|
|
645
858
|
// Iterate by code point (not UTF-16 code unit) so surrogate pairs for
|
|
646
859
|
// non-BMP characters resolve to a single glyph lookup.
|
|
860
|
+
const memo = new Map();
|
|
647
861
|
let totalAdvance = 0;
|
|
648
862
|
for (const ch of text) {
|
|
649
|
-
const
|
|
650
|
-
|
|
863
|
+
const code = ch.codePointAt(0);
|
|
864
|
+
// A formatting control is an instruction, not a character: it must not be
|
|
865
|
+
// looked up, charged an advance, or reported as undrawable.
|
|
866
|
+
if (isNonPrintingControl(code)) {
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const found = this.resolveGlyph(fonts, code, memo);
|
|
870
|
+
totalAdvance += found
|
|
871
|
+
? found.outline.advanceWidth * (fontSize / found.font.unitsPerEm)
|
|
872
|
+
: this.missingAdvance(code, fontSize);
|
|
651
873
|
}
|
|
652
|
-
const hScale = totalAdvance > 0 ?
|
|
874
|
+
const hScale = normalizeTo !== undefined && totalAdvance > 0 ? normalizeTo / totalAdvance : 1;
|
|
653
875
|
const theta = rotation && rotation.angle !== 0 ? (rotation.angle * Math.PI) / 180 : 0;
|
|
654
876
|
const cos = Math.cos(theta);
|
|
655
877
|
const sin = Math.sin(theta);
|
|
@@ -658,11 +880,23 @@ export class BasicRasterCanvas {
|
|
|
658
880
|
let curX = startX;
|
|
659
881
|
for (const ch of text) {
|
|
660
882
|
const code = ch.codePointAt(0);
|
|
661
|
-
|
|
662
|
-
if (!outline) {
|
|
663
|
-
curX += fontSize * 0.4 * hScale;
|
|
883
|
+
if (isNonPrintingControl(code)) {
|
|
664
884
|
continue;
|
|
665
885
|
}
|
|
886
|
+
const found = this.resolveGlyph(fonts, code, memo);
|
|
887
|
+
if (!found) {
|
|
888
|
+
// Nothing on this machine can draw it. Record it so the gap is reportable
|
|
889
|
+
// rather than a silent hole in the picture — but tab is not a gap: it is
|
|
890
|
+
// width without ink, and practically no font carries a glyph for it, so
|
|
891
|
+
// reporting it would tell the caller to install a font that cannot help.
|
|
892
|
+
if (code !== TAB) {
|
|
893
|
+
this.uncoveredCodePoints.add(code);
|
|
894
|
+
}
|
|
895
|
+
curX += this.missingAdvance(code, fontSize) * hScale;
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
const { font, outline } = found;
|
|
899
|
+
const scale = fontSize / font.unitsPerEm;
|
|
666
900
|
const glyph = cachedRasterizeGlyph(outline, fontSize, font.unitsPerEm);
|
|
667
901
|
if (glyph.pixels.length === 0) {
|
|
668
902
|
curX += outline.advanceWidth * scale * hScale;
|
|
@@ -705,6 +939,22 @@ export class BasicRasterCanvas {
|
|
|
705
939
|
let totalGlyphW = 0;
|
|
706
940
|
for (const ch of text) {
|
|
707
941
|
const code = ch.codePointAt(0);
|
|
942
|
+
// A formatting control has no glyph in any font, so substituting `?` for it
|
|
943
|
+
// would invent a character the text does not contain.
|
|
944
|
+
if (isNonPrintingControl(code)) {
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
// The stroke font covers ASCII 32-126 and nothing else, so anything outside
|
|
948
|
+
// it is drawn as `?`. That is a substitution, not a rendering: record it so a
|
|
949
|
+
// caller is told their CJK became punctuation rather than discovering it in
|
|
950
|
+
// the image. Tab is exempt for the same reason as above.
|
|
951
|
+
if (STROKE_FONT[code] === undefined && code !== TAB) {
|
|
952
|
+
this.uncoveredCodePoints.add(code);
|
|
953
|
+
}
|
|
954
|
+
if (code === TAB) {
|
|
955
|
+
totalGlyphW += TAB_EM_WIDTH; // width, no ink
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
708
958
|
const glyph = STROKE_FONT[code] ?? STROKE_FONT[63];
|
|
709
959
|
totalGlyphW += glyph ? glyph.w : 0.4;
|
|
710
960
|
}
|
|
@@ -713,6 +963,13 @@ export class BasicRasterCanvas {
|
|
|
713
963
|
let cx = startX;
|
|
714
964
|
for (const ch of text) {
|
|
715
965
|
const code = ch.codePointAt(0);
|
|
966
|
+
if (isNonPrintingControl(code)) {
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
if (code === TAB) {
|
|
970
|
+
cx += TAB_EM_WIDTH * fontSize * scale;
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
716
973
|
const glyph = STROKE_FONT[code] ?? STROKE_FONT[63];
|
|
717
974
|
if (glyph) {
|
|
718
975
|
for (const stroke of glyph.d) {
|
|
@@ -742,6 +999,13 @@ export class BasicRasterCanvas {
|
|
|
742
999
|
let cx = startX;
|
|
743
1000
|
for (const ch of text) {
|
|
744
1001
|
const code = ch.codePointAt(0);
|
|
1002
|
+
if (isNonPrintingControl(code)) {
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
if (code === TAB) {
|
|
1006
|
+
cx += TAB_EM_WIDTH * fontSize * scale;
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
745
1009
|
const glyph = STROKE_FONT[code] ?? STROKE_FONT[63];
|
|
746
1010
|
if (glyph) {
|
|
747
1011
|
for (const stroke of glyph.d) {
|