emf-converter 4.8.20 → 4.9.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 +4 -33
- package/dist/browser.js +160 -41
- package/dist/browser.mjs +160 -41
- package/dist/index.js +160 -41
- package/dist/index.mjs +160 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,15 +18,6 @@ Windows metafiles are recorded GDI and GDI+ drawing calls, commonly embedded in
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
## What's new
|
|
22
|
-
|
|
23
|
-
- **SVG output**: `convertMetafileToSvg`, `convertMetafileToSvgDataUrl`, `convertMetafileToSvgTree` + `svgTreeToReact` / `svgTreeToJsx` for JSX/TSX.
|
|
24
|
-
- **Windows-exact PNG by default**: GDI shapes are drawn by a rasteriser fitted to Windows GDI (28.4 fixed-point geometry, GDI's fill rule, line algorithm, ellipse and Bezier construction, wide pens, dash styles), and EMF+ drawing follows the file's recorded GDI+ `SmoothingMode` with GDI+'s own rasteriser. **Breaking:** default PNG output is no longer Canvas-antialiased; pass `gdiAntialias: true` for the previous smooth edges.
|
|
25
|
-
- **Exact text with the `fonts` option**: a built-in TrueType engine (hinting interpreter, dropout control, GDI font mapping and metrics, grayscale and ClearType) plus Windows raster `.fon` fonts. `loadSystemFonts()` reads the installed fonts in Node.js.
|
|
26
|
-
- **No canvas required**: a built-in pure-JavaScript rasteriser renders SVG anywhere and PNG for drawings without text; `@napi-rs/canvas` is only needed for PNG output with text in plain Node.js.
|
|
27
|
-
- **Complete WMF playback**: bitmaps, clipping, regions, mapping modes, palettes, flood fills, and embedded EMF comments, played as Windows' `PlayMetaFile` plays them.
|
|
28
|
-
- Many correctness fixes found by the new fixtures (see the changelog).
|
|
29
|
-
|
|
30
21
|
## Documentation and demo
|
|
31
22
|
|
|
32
23
|
The documentation site at **https://christophervr.github.io/emf-converter/** includes a live demo: drop an `.emf` or `.wmf` file to see the PNG or SVG output, download it, or copy it as a TSX component.
|
|
@@ -192,35 +183,15 @@ Node.js only (returns `[]` elsewhere; the package stays browser-safe). Reads the
|
|
|
192
183
|
|
|
193
184
|
## How it works
|
|
194
185
|
|
|
195
|
-
A three-phase pipeline: **parse → replay → export**.
|
|
196
|
-
|
|
197
|
-
Everything below is verified against output painted by Windows itself; `src/gdi-parity.fixture.test.ts` holds the per-fixture bounds.
|
|
198
|
-
|
|
199
|
-
- **GDI shapes** (`gdi-raster.ts`, `gdi-raster-widen.ts`): 28.4 fixed-point geometry; GDI's fill rule (ALTERNATE/WINDING); one-pixel lines by GDI's diamond rule with its tie-breaks; GDI's own Bezier flattener, ellipse, rounded-rectangle, arc (GDI's trigonometry table and `SetArcDirection`), chord and pie construction; cosmetic dash styles (dash 18/6, dot 3/3, ...) and geometric dashes; wide pens widened from GDI's own pen polygons with every cap and join and the miter limit; rotated and skewed world transforms. Pixel-exact on the shape fixtures. A path bracket's GDI geometry has the points, point types, figure starts and direction that Windows' `GetPath` reports (checked against the Windows data in Wine's `gdi32` path tests).
|
|
200
|
-
- **Raster operations**: all 256 ROP3 codes for `BitBlt`/`StretchBlt`/`StretchDIBits`/`PatBlt`, exact per bit against the destination, brush and source, with GDI's stretch modes, mirrored rectangles and rotated destinations (each device pixel mapped back to one source texel). Every `SetROP2` mode, including the bitwise AND/OR/XOR family, for shapes, paths and pattern-brush fills.
|
|
201
|
-
- **Brushes**: hatch, monochrome and DIB pattern brushes anchored to the brush origin, with the background mode; GDI+ solid, hatch, texture (bilinear, WrapMode-aware, as GDI+ samples them), linear gradients (GDI+'s own interpolation table: preset colours, blend shapes, gamma correction, every WrapMode) and path gradients (true boundary-shaped falloff, every WrapMode).
|
|
202
|
-
- **Clipping**: every GDI and GDI+ region combine mode exact for every clip (vector where possible, otherwise scan-converted to pixel regions, which is how GDI stores them), path clips with their fill mode, and region offsets.
|
|
203
|
-
- **EMF+**: fills, pens and clips follow the recorded `SmoothingMode` with GDI+'s own fill rasteriser (8 x 4-sample antialiasing, blend arithmetic) and pen widener (joins, caps, dash caps, compound lines, inset alignment); `DrawImage` with every InterpolationMode/PixelOffsetMode kernel, ImageAttributes wrap modes, drawn in record order under the live clip; embedded metafiles replayed as vectors; continuation records reassembled; compressed textures and images decoded before replay.
|
|
204
|
-
- **Text**: see [Exact text](#exact-text).
|
|
205
|
-
- **WMF**: `PS_INSIDEFRAME` half-pixel width sweeps and mirrored line fixtures match Windows exactly; scaled shapes retain a small residual. Metric map modes default to a 96 dpi reference device; `wmfReferenceDpi` accepts a scalar or `{ x, y }` to reproduce another device.
|
|
206
|
-
|
|
207
|
-
### Supported records
|
|
186
|
+
A three-phase pipeline: **parse → replay → export**. Records are replayed by the GDI, EMF+ or WMF handlers onto a Canvas (or the built-in pure-JavaScript rasteriser) for PNG, or onto a recording context for SVG. GDI and GDI+ shapes, raster operations, brushes, clipping, text and WMF playback are all fitted to output painted by Windows itself.
|
|
208
187
|
|
|
209
|
-
|
|
210
|
-
- **EMF+**: every record in MS-EMFPLUS (Beziers, cardinal curves, regions, containers, save/restore, compositing mode, rendering origin, text contrast, `StrokeFillPath`, the terminal-server `SetTSGraphics` / `SetTSClip`, `MultiFormat*` played as GDI+ plays them), with 32-bit, compressed 16-bit and relative point data, and every object type (solid, hatch, texture and gradient brushes; pens with caps, joins, dash styles, dash caps, compound lines and custom line caps; paths, regions, bitmap and metafile images, fonts, string formats, image attributes). Several encodings follow what GDI+ actually does where it differs from MS-EMFPLUS (relative points, `StrokeFillPath`, `SetTSGraphics`, `SetTSClip`).
|
|
211
|
-
- **WMF**: META_ANIMATEPALETTE, META_ARC, META_BITBLT, META_CHORD, META_CREATEBITMAP, META_CREATEBITMAPINDIRECT, META_CREATEBRUSH, META_CREATEBRUSHINDIRECT, META_CREATEFONTINDIRECT, META_CREATEPALETTE, META_CREATEPATTERNBRUSH, META_CREATEPENINDIRECT, META_CREATEREGION, META_DELETEOBJECT, META_DIBBITBLT, META_DIBCREATEPATTERNBRUSH, META_DIBSTRETCHBLT, META_ELLIPSE, META_EOF, META_ESCAPE, META_EXCLUDECLIPRECT, META_EXTFLOODFILL, META_EXTTEXTOUT, META_FILLREGION, META_FLOODFILL, META_FRAMEREGION, META_INTERSECTCLIPRECT, META_INVERTREGION, META_LINETO, META_MOVETO, META_OFFSETCLIPRGN, META_OFFSETVIEWPORTORG, META_OFFSETWINDOWORG, META_PAINTREGION, META_PATBLT, META_PIE, META_POLYGON, META_POLYLINE, META_POLYPOLYGON, META_REALIZEPALETTE, META_RECTANGLE, META_RESIZEPALETTE, META_RESTOREDC, META_ROUNDRECT, META_SAVEDC, META_SCALEVIEWPORTEXT, META_SCALEWINDOWEXT, META_SELECTCLIPREGION, META_SELECTOBJECT, META_SELECTPALETTE, META_SETBKCOLOR, META_SETBKMODE, META_SETDIBTODEV, META_SETLAYOUT, META_SETMAPMODE, META_SETMAPPERFLAGS, META_SETPALENTRIES, META_SETPIXEL, META_SETPOLYFILLMODE, META_SETRELABS, META_SETROP2, META_SETSTRETCHBLTMODE, META_SETTEXTALIGN, META_SETTEXTCHAREXTRA, META_SETTEXTCOLOR, META_SETTEXTJUSTIFICATION, META_SETVIEWPORTEXT, META_SETVIEWPORTORG, META_SETWINDOWEXT, META_SETWINDOWORG, META_STRETCHBLT, META_STRETCHDIB, META_TEXTOUT. Where Windows no longer plays a record the way MS-WMF describes it (Win16 device bitmaps in META_BITBLT / META_STRETCHBLT, META_CREATEPATTERNBRUSH, banded META_SETDIBTODEV), the converter follows Windows.
|
|
188
|
+
See [How it works](docs/how-it-works.md) for the pipeline, the Windows-parity details and the full list of supported records.
|
|
212
189
|
|
|
213
190
|
## Limitations
|
|
214
191
|
|
|
215
|
-
Everything is measured against output painted by Windows itself
|
|
192
|
+
Everything is measured against output painted by Windows itself, and a few areas still differ from it: some image effects, text with ClearType and EMF+ antialiasing, nonuniform wide-pen nibs, and rotated bicubic `DrawImage` sampling. Metric WMF map modes assume a 96 dpi device unless you pass `wmfReferenceDpi`.
|
|
216
193
|
|
|
217
|
-
|
|
218
|
-
- **Pen transforms**: EMF+ pen transforms (uniform, nonuniform and skewed) match GDI+ exactly on the 17 pen-transform fixtures, including dashes, which GDI+ lays out along the path in world space at the untransformed pen width. Native fixtures verify dashes under both uniform and non-uniform scales. GDI+ rejects singular pen transforms; the converter ignores such malformed transform data. Square, round, diamond and arrow anchor caps use their native shapes; the new centered-cap fixtures match every pixel with and without antialiasing.
|
|
219
|
-
- **Text**: ANSI, multi-string and small-text records render with residuals under 0.1% on their record fixtures. The broader font-engine fixtures still have larger ClearType, EMF+ antialiasing and TextContrast differences. ANSI decoding uses the host's `TextDecoder` for common Windows code pages; Johab and OEM CP437 use bundled Windows mappings. Vertical `ETO_PDY` advances work with and without the `fonts` engine. Without `fonts`, SVG text measurements use deterministic estimates, so precise justification requires supplying the matching fonts.
|
|
220
|
-
- **Wide pens and paths**: the 2,136 native wide-path fills and widened ellipse outlines, including their inner triangles, match exactly. Square-capped curves, line-to-curve joins, fractional transformed inside-frame geometry and nonuniform GDI pen nibs remain open. EMF+ 1-pixel antialiased lines can differ by one sample at their ends; additional closed-figure Inset and compound-pen captures are still needed.
|
|
221
|
-
- **GM_COMPATIBLE recordings**: EMF files do not record the graphics mode, and Windows plays RoundRect, Arc, Chord, Pie and null-pen Ellipse records back differently from how a GM_COMPATIBLE application drew them on screen; the converter follows Windows' playback.
|
|
222
|
-
- **EMF+ sampling**: rotated `HighQualityBicubic` `DrawImage` retains 0.013% of pixels beyond eight channel levels, with more widespread small sampling differences in interpolation and path gradients. Linear `Blend` tables and the nested-metafile scale/clip fixtures now match exactly.
|
|
223
|
-
- **WMF**: inside-frame half-pixel width sweeps and mirrored `LAYOUT_RTL` line fixtures match exactly; scaled shapes retain seven pixels in the current fixture. Metric map modes default to a 96 dpi reference device; supply `wmfReferenceDpi` when the original resolution is known.
|
|
194
|
+
See [Limitations](docs/limitations.md) for the details and [Outstanding work](docs/outstanding-work.md) for what is still open.
|
|
224
195
|
|
|
225
196
|
## License
|
|
226
197
|
|
package/dist/browser.js
CHANGED
|
@@ -11661,11 +11661,20 @@ function ellipseBeziersBox(box, clockwise = false) {
|
|
|
11661
11661
|
if (!clockwise) {
|
|
11662
11662
|
return ellipsePoints(box, false);
|
|
11663
11663
|
}
|
|
11664
|
-
const
|
|
11664
|
+
const { halfX, ...plain } = box;
|
|
11665
|
+
const e = clockwiseEllipseBeziersBox(plain);
|
|
11665
11666
|
const out = [];
|
|
11666
11667
|
for (let i = e.length - 2; i >= 0; i -= 2) {
|
|
11667
11668
|
out.push(e[i], e[i + 1]);
|
|
11668
11669
|
}
|
|
11670
|
+
const odd = Math.abs(plain.exx) % 2;
|
|
11671
|
+
out[18] += odd;
|
|
11672
|
+
out[6] -= odd;
|
|
11673
|
+
if (halfX) {
|
|
11674
|
+
for (const k of [0, 1, 2, 3, 4, 5, 12]) {
|
|
11675
|
+
out[2 * k]++;
|
|
11676
|
+
}
|
|
11677
|
+
}
|
|
11669
11678
|
return out;
|
|
11670
11679
|
}
|
|
11671
11680
|
function clockwiseEllipseBeziersBox(box) {
|
|
@@ -11681,10 +11690,10 @@ function ellipsePoints(box, cwControls) {
|
|
|
11681
11690
|
const vMidHi = (v) => half(v, true);
|
|
11682
11691
|
const vCtl = (v) => Math.floor(v / 2) - Math.floor(KAPPA * Math.floor(v / 2));
|
|
11683
11692
|
const vCtlR = (v) => v - vCtl(v);
|
|
11684
|
-
const vDist = (v) => Math.ceil(KAPPA * Math.floor(v / 2));
|
|
11685
|
-
const rightUpper = cwControls ? (v) =>
|
|
11693
|
+
const vDist = (v) => v % 2 === 0 ? Math.ceil(KAPPA * Math.floor(v / 2)) : Math.floor(KAPPA * Math.ceil(v / 2));
|
|
11694
|
+
const rightUpper = cwControls ? (v) => vMidLo(v) - vDist(v) : vCtl;
|
|
11686
11695
|
const leftUpper = cwControls ? (v) => vMidLo(v) - vDist(v) : vCtl;
|
|
11687
|
-
const leftLower = cwControls ? (v) =>
|
|
11696
|
+
const leftLower = cwControls ? (v) => vMidHi(v) + vDist(v) : vCtlR;
|
|
11688
11697
|
const rightLower = cwControls ? (v) => vMidHi(v) + vDist(v) : vCtlR;
|
|
11689
11698
|
const zero = () => 0;
|
|
11690
11699
|
const full = (v) => v;
|
|
@@ -11926,7 +11935,7 @@ function unitPiece(from, to, small) {
|
|
|
11926
11935
|
}
|
|
11927
11936
|
return { start: p0, ctrl };
|
|
11928
11937
|
}
|
|
11929
|
-
function arcBeziers(l, t, r, b, xs, ys, xe, ye, clockwise) {
|
|
11938
|
+
function arcBeziers(l, t, r, b, xs, ys, xe, ye, clockwise, halfX = false, framed = false) {
|
|
11930
11939
|
const w = r - l;
|
|
11931
11940
|
const h = b - t;
|
|
11932
11941
|
const Q = Math.PI / 2;
|
|
@@ -11946,11 +11955,25 @@ function arcBeziers(l, t, r, b, xs, ys, xe, ye, clockwise) {
|
|
|
11946
11955
|
} else {
|
|
11947
11956
|
while (a1 >= a0) a1 -= 2 * Math.PI;
|
|
11948
11957
|
}
|
|
11949
|
-
const px = (u) => Math.round(cx + rx * u);
|
|
11958
|
+
const px = (u, v) => clockwise && halfX ? Math.ceil(cx + rx * u + 0.5 - v) : Math.round(cx + rx * u + (halfX ? 1 + v : 0));
|
|
11950
11959
|
const py = (v) => Math.round(cy - ry * v);
|
|
11951
|
-
const
|
|
11960
|
+
const eb = axisBox(l, t, r, b);
|
|
11961
|
+
if (halfX) eb.halfX = true;
|
|
11962
|
+
const E = ellipseBeziersBox(eb);
|
|
11963
|
+
if (clockwise) {
|
|
11964
|
+
const cwPts = ellipseBeziersBox(eb, true);
|
|
11965
|
+
for (let i = 0; i < cwPts.length; i += 2) {
|
|
11966
|
+
E[cwPts.length - 2 - i] = cwPts[i];
|
|
11967
|
+
E[cwPts.length - 1 - i] = cwPts[i + 1];
|
|
11968
|
+
}
|
|
11969
|
+
}
|
|
11952
11970
|
const startUnit = small ? [Math.cos(a0), Math.sin(a0)] : polygonTrig(a0);
|
|
11953
|
-
const out = [px(startUnit[0]), py(startUnit[1])];
|
|
11971
|
+
const out = [px(startUnit[0], startUnit[1]), py(startUnit[1])];
|
|
11972
|
+
const onAxis = (angle2) => Math.abs(angle2 / Q - Math.round(angle2 / Q)) < 1e-9;
|
|
11973
|
+
const axisQuadrant = (angle2) => (Math.round(angle2 / Q) % 4 + 4) % 4;
|
|
11974
|
+
if (framed && onAxis(a0) && (s > 0 ? axisQuadrant(a0) === 1 || axisQuadrant(a0) === 2 : axisQuadrant(a0) === 2 || axisQuadrant(a0) === 3)) {
|
|
11975
|
+
out.push(out[0], out[1], out[0], out[1], out[0], out[1]);
|
|
11976
|
+
}
|
|
11954
11977
|
let a = a0;
|
|
11955
11978
|
for (let guard = 0; guard < 8 && (s > 0 ? a < a1 - 1e-12 : a > a1 + 1e-12); guard++) {
|
|
11956
11979
|
const next = s > 0 ? Math.floor(a / Q + 1e-9) * Q + Q : Math.ceil(a / Q - 1e-9) * Q - Q;
|
|
@@ -11972,10 +11995,16 @@ function arcBeziers(l, t, r, b, xs, ys, xe, ye, clockwise) {
|
|
|
11972
11995
|
const e = s > 0 ? Math.min(next, a1) : Math.max(next, a1);
|
|
11973
11996
|
const piece = unitPiece(a, e, small);
|
|
11974
11997
|
for (let i = 0; i < 6; i += 2) {
|
|
11975
|
-
out.push(px(piece.ctrl[i]), py(piece.ctrl[i + 1]));
|
|
11998
|
+
out.push(px(piece.ctrl[i], piece.ctrl[i + 1]), py(piece.ctrl[i + 1]));
|
|
11976
11999
|
}
|
|
11977
12000
|
a = e;
|
|
11978
12001
|
}
|
|
12002
|
+
if (framed && onAxis(a1) && (s > 0 ? axisQuadrant(a1) === 3 || axisQuadrant(a1) === 0 : axisQuadrant(a1) === 1 || axisQuadrant(a1) === 0)) {
|
|
12003
|
+
const endUnit = polygonTrig(a1);
|
|
12004
|
+
const ex = px(endUnit[0], endUnit[1]);
|
|
12005
|
+
const ey = py(endUnit[1]);
|
|
12006
|
+
out.push(ex, ey, ex, ey, ex, ey);
|
|
12007
|
+
}
|
|
11979
12008
|
return out;
|
|
11980
12009
|
}
|
|
11981
12010
|
var GdiRasterPath = class {
|
|
@@ -29614,7 +29643,7 @@ var Outliner = class {
|
|
|
29614
29643
|
this.flush();
|
|
29615
29644
|
}
|
|
29616
29645
|
};
|
|
29617
|
-
function dashPieces(P, tangents, curveFlags, pattern, shorten, wholePixelVectors) {
|
|
29646
|
+
function dashPieces(P, tangents, curveFlags, pattern, shorten, wholePixelVectors, metric) {
|
|
29618
29647
|
const out = [];
|
|
29619
29648
|
const dashes = [];
|
|
29620
29649
|
for (let i = 0; i < pattern.length; i += 2) {
|
|
@@ -29634,8 +29663,8 @@ function dashPieces(P, tangents, curveFlags, pattern, shorten, wholePixelVectors
|
|
|
29634
29663
|
const [x0, y0] = P[i];
|
|
29635
29664
|
const [x1, y1] = P[i + 1];
|
|
29636
29665
|
const dir = [x1 - x0, y1 - y0];
|
|
29637
|
-
const real = Math.hypot(dir[0], dir[1]);
|
|
29638
|
-
const len = wholePixelVectors ? Math.hypot(Math.floor(dir[0] / 16), Math.floor(dir[1] / 16)) * 16 : Math.hypot(dir[0], dir[1]);
|
|
29666
|
+
const real = metric ? Math.hypot(dir[0] / metric[0], dir[1] / metric[1]) : Math.hypot(dir[0], dir[1]);
|
|
29667
|
+
const len = metric ? real : wholePixelVectors ? Math.hypot(Math.floor(dir[0] / 16), Math.floor(dir[1] / 16)) * 16 : Math.hypot(dir[0], dir[1]);
|
|
29639
29668
|
if (len === 0) {
|
|
29640
29669
|
if (on && cur) {
|
|
29641
29670
|
cur.pts.push(P[i + 1]);
|
|
@@ -29708,10 +29737,10 @@ function widenPath(path, opts) {
|
|
|
29708
29737
|
}
|
|
29709
29738
|
if (dashed) {
|
|
29710
29739
|
const run = closed ? [...P, P[0]] : P;
|
|
29711
|
-
const shorten = opts.cap === "flat" || opts.shortenDashes === false ? 0 : opts.width;
|
|
29740
|
+
const shorten = opts.cap === "flat" || opts.shortenDashes === false ? 0 : opts.dashMetric ? opts.width / opts.dashMetric[0] : opts.width;
|
|
29712
29741
|
const runTangents = closed ? [...dirs, void 0] : dirs;
|
|
29713
29742
|
const runCurves = closed ? [...curves, !!fig.roundWiden] : curves;
|
|
29714
|
-
for (const piece of dashPieces(run, runTangents, runCurves, opts.dashes, shorten, !!opts.wholePixelDashVectors)) {
|
|
29743
|
+
for (const piece of dashPieces(run, runTangents, runCurves, opts.dashes, shorten, !!opts.wholePixelDashVectors, opts.dashMetric)) {
|
|
29715
29744
|
const pts = [piece.pts[0]];
|
|
29716
29745
|
const pdirs = [];
|
|
29717
29746
|
const pdraws = [];
|
|
@@ -29793,6 +29822,10 @@ function flipBox(box) {
|
|
|
29793
29822
|
}
|
|
29794
29823
|
function rectRasterPath(box, clockwise = false) {
|
|
29795
29824
|
const c = boxCorners(clockwise ? flipBox(box) : box);
|
|
29825
|
+
if (box.halfXRect) {
|
|
29826
|
+
c[4]--;
|
|
29827
|
+
c[6]--;
|
|
29828
|
+
}
|
|
29796
29829
|
const path = new GdiRasterPath();
|
|
29797
29830
|
path.moveTo(c[0], c[1]);
|
|
29798
29831
|
path.lineTo(c[2], c[3]);
|
|
@@ -29839,8 +29872,49 @@ function roundRectDeviceBeziers(box, cw, ch, clockwise = false, compatible = fal
|
|
|
29839
29872
|
pts.push(...map(q[i], clockwise ? f.t + f.b - q[i + 1] : q[i + 1]));
|
|
29840
29873
|
}
|
|
29841
29874
|
if (box.halfX) {
|
|
29842
|
-
|
|
29843
|
-
|
|
29875
|
+
if (box.halfXCompat || !clockwise) {
|
|
29876
|
+
const cy = (f.t + f.b) / 2;
|
|
29877
|
+
const ry = (f.b - f.t) / 2 || 1;
|
|
29878
|
+
for (let k = 0; k < 16; k++) {
|
|
29879
|
+
const v = (cy - pts[2 * k + 1]) / ry;
|
|
29880
|
+
pts[2 * k] += k >= 8 ? Math.round(1 + v) : Math.ceil(v - 0.5);
|
|
29881
|
+
}
|
|
29882
|
+
} else {
|
|
29883
|
+
const tall = box.cornerExact !== void 0 && box.cornerExact[1] * 2 >= Math.abs(box.eyy) - 1e-9;
|
|
29884
|
+
for (const k of tall ? [1, 2, 3, 4, 5, 6, 8, 15] : [0, 1, 2, 3, 4, 5, 6, 7]) {
|
|
29885
|
+
pts[2 * k]++;
|
|
29886
|
+
}
|
|
29887
|
+
}
|
|
29888
|
+
}
|
|
29889
|
+
if (box.cornerExact && isAxisBox(box)) {
|
|
29890
|
+
const w = f.r - f.l;
|
|
29891
|
+
const h = f.b - f.t;
|
|
29892
|
+
if (w >= 0 && h >= 0) {
|
|
29893
|
+
const hx = w === 0 ? 0 : Math.round(box.cornerExact[0] * (w + w % 2) / w / 2);
|
|
29894
|
+
const hy = h === 0 ? 0 : Math.round(box.cornerExact[1] * (clockwise ? h + h % 2 : h - h % 2) / h / 2);
|
|
29895
|
+
const hc = Math.ceil(KAPPA * hx);
|
|
29896
|
+
const vc = clockwise ? Math.ceil(KAPPA * hy) : Math.floor(KAPPA * hy);
|
|
29897
|
+
const set = (k, x, y) => {
|
|
29898
|
+
if (x !== null) pts[2 * k] = x;
|
|
29899
|
+
pts[2 * k + 1] = clockwise ? f.t + f.b - y : y;
|
|
29900
|
+
};
|
|
29901
|
+
const a = box.halfX ? 1 : 0;
|
|
29902
|
+
set(0, null, f.t + hy);
|
|
29903
|
+
set(1, null, f.t + hy - vc);
|
|
29904
|
+
set(2, f.r + a - hx + hc, f.t);
|
|
29905
|
+
set(3, f.r + a - hx, f.t);
|
|
29906
|
+
set(4, f.l + a + hx, f.t);
|
|
29907
|
+
set(5, f.l + a + hx - hc, f.t);
|
|
29908
|
+
set(6, null, f.t + hy - vc);
|
|
29909
|
+
set(7, null, f.t + hy);
|
|
29910
|
+
set(8, null, f.b - hy);
|
|
29911
|
+
set(9, null, f.b - hy + vc);
|
|
29912
|
+
set(10, f.l + hx - hc, f.b);
|
|
29913
|
+
set(11, f.l + hx, f.b);
|
|
29914
|
+
set(12, f.r - hx, f.b);
|
|
29915
|
+
set(13, f.r - hx + hc, f.b);
|
|
29916
|
+
set(14, null, f.b - hy + vc);
|
|
29917
|
+
set(15, null, f.b - hy);
|
|
29844
29918
|
}
|
|
29845
29919
|
}
|
|
29846
29920
|
return pts;
|
|
@@ -29883,7 +29957,7 @@ function arcRasterPath(box, s, e, clockwise, kind, from, path = new GdiRasterPat
|
|
|
29883
29957
|
cw = !cw;
|
|
29884
29958
|
}
|
|
29885
29959
|
}
|
|
29886
|
-
const bz = arcBeziers(f.l, f.t, f.r, f.b, sx, sy, ex, ey, cw);
|
|
29960
|
+
const bz = arcBeziers(f.l, f.t, f.r, f.b, sx, sy, ex, ey, cw, !!box.halfX, !!box.framed);
|
|
29887
29961
|
const pts = [];
|
|
29888
29962
|
for (let i = 0; i < bz.length; i += 2) {
|
|
29889
29963
|
pts.push(...map(bz[i], bz[i + 1]));
|
|
@@ -29897,7 +29971,7 @@ function arcRasterPath(box, s, e, clockwise, kind, from, path = new GdiRasterPat
|
|
|
29897
29971
|
if (kind === "pie") {
|
|
29898
29972
|
const w = f.r - f.l;
|
|
29899
29973
|
const h = f.b - f.t;
|
|
29900
|
-
const [cx, cy] = map(f.l + Math.ceil(w / 2), f.t + Math.ceil(h / 2));
|
|
29974
|
+
const [cx, cy] = map(f.l + Math.ceil(w / 2) + (box.halfX ? 1 : 0), f.t + Math.ceil(h / 2));
|
|
29901
29975
|
path.lineTo(cx, cy);
|
|
29902
29976
|
}
|
|
29903
29977
|
if (kind === "pie" || kind === "chord") {
|
|
@@ -29933,8 +30007,9 @@ function penWidenOptions(rCtx, opts = {}) {
|
|
|
29933
30007
|
const widthPx = rCtx.wholeDevicePixels && (flags & 15) === 6 ? state.penWidth * Math.sqrt(Math.abs(matrix[0] * matrix[3] - matrix[1] * matrix[2])) : penDeviceWidth(rCtx);
|
|
29934
30008
|
const capBits = flags & 3840;
|
|
29935
30009
|
const joinBits = flags & 61440;
|
|
29936
|
-
const
|
|
29937
|
-
const
|
|
30010
|
+
const ellipse = !rCtx.wholeDevicePixels && state.penWidth * Math.abs(matrix[0]) * 16 >= 1 && state.penWidth * Math.abs(matrix[3]) * 16 >= 1 && Math.abs(matrix[1]) < 1e-6 && Math.abs(matrix[2]) < 1e-6 && Math.abs(Math.abs(matrix[0]) - Math.abs(matrix[3])) > 1e-6;
|
|
30011
|
+
const dashes = state.penExtended ? ellipse ? geometricStyle(flags, state.penWidth, state.penUserStyle, 1) : geometricStyle(flags, widthPx, state.penUserStyle, widthPx / (state.penWidth || 1)) : null;
|
|
30012
|
+
const dashMetric = ellipse && dashes ? [Math.abs(matrix[0]), Math.abs(matrix[3])] : void 0;
|
|
29938
30013
|
return {
|
|
29939
30014
|
width: Math.round((ellipse ? state.penWidth * Math.abs(matrix[0]) : widthPx) * 16),
|
|
29940
30015
|
...ellipse ? { height: Math.round(state.penWidth * Math.abs(matrix[3]) * 16) } : {},
|
|
@@ -29942,6 +30017,7 @@ function penWidenOptions(rCtx, opts = {}) {
|
|
|
29942
30017
|
join: opts.roundPen ? "round" : opts.rectangle && !state.penExtended ? "miter" : !state.penExtended || joinBits === 0 ? "round" : joinBits === 4096 ? "bevel" : "miter",
|
|
29943
30018
|
miterLimit: state.miterLimit ?? 10,
|
|
29944
30019
|
dashes: dashes ? dashes.map((v) => v * 16) : null,
|
|
30020
|
+
...dashMetric ? { dashMetric } : {},
|
|
29945
30021
|
shortenDashes: (flags & 15) !== 7,
|
|
29946
30022
|
// One logical unit per device pixel, unrotated: GDI measures dash
|
|
29947
30023
|
// segments in whole pixels.
|
|
@@ -30954,9 +31030,24 @@ function insideFrameInset(rCtx, box, rectangle = false, clockwiseRoundRect = fal
|
|
|
30954
31030
|
right: Math.round((rectangle ? lo : hi) * px),
|
|
30955
31031
|
top: y,
|
|
30956
31032
|
bottom: y,
|
|
30957
|
-
half: wx % 2 === 1 && !rectangle
|
|
31033
|
+
half: wx % 2 === 1 && !rectangle,
|
|
31034
|
+
halfRect: wx % 2 === 1 && rectangle
|
|
30958
31035
|
};
|
|
30959
31036
|
}
|
|
31037
|
+
function insideFramePenTooWide(rCtx, box) {
|
|
31038
|
+
if ((rCtx.state.penStyle & 15) !== PS_INSIDEFRAME || !isAxisBox(box) || hasWorldRotation(rCtx)) {
|
|
31039
|
+
return false;
|
|
31040
|
+
}
|
|
31041
|
+
const m = gdiDeviceMatrix(rCtx);
|
|
31042
|
+
const px = gdiDevicePixelX(rCtx);
|
|
31043
|
+
const py = gdiDevicePixelY(rCtx);
|
|
31044
|
+
const widthX = Math.abs(rCtx.state.penWidth * m[0]) / px * 16;
|
|
31045
|
+
const widthY = Math.abs(rCtx.state.penWidth * m[3]) / py * 16;
|
|
31046
|
+
if (Math.max(widthX, widthY) < 24) {
|
|
31047
|
+
return false;
|
|
31048
|
+
}
|
|
31049
|
+
return Math.abs(box.exx) / px < Math.round(widthX) || Math.abs(box.eyy) / py < Math.round(widthY);
|
|
31050
|
+
}
|
|
30960
31051
|
function insetFixBox(box, inset) {
|
|
30961
31052
|
const sx = box.exx < 0 ? -1 : 1;
|
|
30962
31053
|
const sy = box.eyy < 0 ? -1 : 1;
|
|
@@ -30966,11 +31057,15 @@ function insetFixBox(box, inset) {
|
|
|
30966
31057
|
exx: box.exx - sx * (inset.left + inset.right),
|
|
30967
31058
|
exy: 0,
|
|
30968
31059
|
eyx: 0,
|
|
30969
|
-
eyy: box.eyy - sy * (inset.top + inset.bottom)
|
|
31060
|
+
eyy: box.eyy - sy * (inset.top + inset.bottom),
|
|
31061
|
+
framed: true
|
|
30970
31062
|
};
|
|
30971
31063
|
if (inset.half && sx > 0) {
|
|
30972
31064
|
framed.halfX = true;
|
|
30973
31065
|
}
|
|
31066
|
+
if (inset.halfRect && sx > 0) {
|
|
31067
|
+
framed.halfXRect = true;
|
|
31068
|
+
}
|
|
30974
31069
|
return framed;
|
|
30975
31070
|
}
|
|
30976
31071
|
function sameEllipseRay(l, t, r, b, sx, sy, ex, ey) {
|
|
@@ -31053,7 +31148,7 @@ function handleRectangle(rCtx, dataOff, recSize) {
|
|
|
31053
31148
|
const r = view.getInt32(dataOff + 8, true);
|
|
31054
31149
|
const b = view.getInt32(dataOff + 12, true);
|
|
31055
31150
|
const unframed = uprightFixBox(rCtx, l, t, r, b);
|
|
31056
|
-
const inset = insideFrameInset(rCtx, unframed, true);
|
|
31151
|
+
const inset = insideFrameInset(rCtx, unframed, true, rCtx.state.arcDirection === 2);
|
|
31057
31152
|
const box = inset ? insetFixBox(unframed, inset) : unframed;
|
|
31058
31153
|
const clockwise = rCtx.state.arcDirection === 2;
|
|
31059
31154
|
if (!inPath) {
|
|
@@ -31239,6 +31334,13 @@ function handleRoundRect(rCtx, dataOff, recSize) {
|
|
|
31239
31334
|
gmh(rCtx, cornerH) / 2,
|
|
31240
31335
|
null
|
|
31241
31336
|
);
|
|
31337
|
+
if (inset) {
|
|
31338
|
+
const fullW = Math.abs((r - l) * devM[0]) * 16;
|
|
31339
|
+
const fullH = Math.abs((b - t) * devM[3]) * 16;
|
|
31340
|
+
if (fullW > 0 && fullH > 0) {
|
|
31341
|
+
framed.cornerExact = [Math.min(cornerFixW, fullW) * Math.abs(framed.exx) / fullW, Math.min(cornerFixH, fullH) * Math.abs(framed.eyy) / fullH];
|
|
31342
|
+
}
|
|
31343
|
+
}
|
|
31242
31344
|
const raster = (box = framed) => roundRectRasterPath(box, cw, ch, clockwise, cornerW === 0 || cornerH === 0);
|
|
31243
31345
|
if (inPath) {
|
|
31244
31346
|
const box = framed;
|
|
@@ -31292,7 +31394,7 @@ function handleEllipse(rCtx, dataOff, recSize) {
|
|
|
31292
31394
|
const b = view.getInt32(dataOff + 12, true);
|
|
31293
31395
|
const clockwise = rCtx.state.arcDirection === 2;
|
|
31294
31396
|
const unframed = uprightFixBox(rCtx, l, t, r, b);
|
|
31295
|
-
const inset = insideFrameInset(rCtx, unframed);
|
|
31397
|
+
const inset = insideFrameInset(rCtx, unframed, false, rCtx.state.arcDirection === 2);
|
|
31296
31398
|
const framed = inset ? insetFixBox(unframed, inset) : unframed;
|
|
31297
31399
|
const params = inset ? { cx: (framed.ax + framed.exx / 2) / 16, cy: (framed.ay + framed.eyy / 2) / 16, rx: Math.abs(framed.exx) / 32, ry: Math.abs(framed.eyy) / 32, rotation: 0 } : hasWorldRotation(rCtx) ? gdiEllipseParams(rCtx, (l + r) / 2, (t + b) / 2, Math.abs(r - l) / 2, Math.abs(b - t) / 2) : {
|
|
31298
31400
|
cx: gmx(rCtx, (l + r) / 2),
|
|
@@ -31342,7 +31444,10 @@ function handleArcFamily(rCtx, recType, dataOff, recSize) {
|
|
|
31342
31444
|
const endAngle = fullEllipse ? startAngle + (clockwise ? 2 : -2) * Math.PI : Math.atan2((endY - cyA) / (ry || 1), (endX - cxA) / (rx || 1));
|
|
31343
31445
|
const rotated = hasWorldRotation(rCtx);
|
|
31344
31446
|
const unframed = fixBox(rCtx, l, t, r, b);
|
|
31345
|
-
|
|
31447
|
+
if (recType !== EMR_ARCTO && insideFramePenTooWide(rCtx, unframed)) {
|
|
31448
|
+
return true;
|
|
31449
|
+
}
|
|
31450
|
+
const inset = insideFrameInset(rCtx, unframed, false, rCtx.state.arcDirection === 2);
|
|
31346
31451
|
const framed = inset ? insetFixBox(unframed, inset) : unframed;
|
|
31347
31452
|
const params = inset ? { cx: (framed.ax + framed.exx / 2) / 16, cy: (framed.ay + framed.eyy / 2) / 16, rx: Math.abs(framed.exx) / 32, ry: Math.abs(framed.eyy) / 32, rotation: 0 } : rotated ? gdiEllipseParams(rCtx, cxA, cyA, rx, ry) : {
|
|
31348
31453
|
cx: gmx(rCtx, cxA),
|
|
@@ -31373,20 +31478,25 @@ function handleArcFamily(rCtx, recType, dataOff, recSize) {
|
|
|
31373
31478
|
}
|
|
31374
31479
|
};
|
|
31375
31480
|
const kind = isArcTo ? "arcto" : recType === EMR_PIE ? "pie" : recType === EMR_CHORD ? "chord" : "arc";
|
|
31376
|
-
const
|
|
31481
|
+
const devM = gdiDeviceMatrix(rCtx);
|
|
31482
|
+
const rawFix = (x, y) => [(devM[0] * x + devM[2] * y + devM[4]) * 16, (devM[1] * x + devM[3] * y + devM[5]) * 16];
|
|
31483
|
+
const radialOnBox = (p, raw) => {
|
|
31377
31484
|
if (!inset) {
|
|
31378
31485
|
return p;
|
|
31379
31486
|
}
|
|
31380
|
-
const
|
|
31381
|
-
const
|
|
31382
|
-
|
|
31487
|
+
const [rl, rt] = rawFix(l, t);
|
|
31488
|
+
const [rr, rb] = rawFix(r, b);
|
|
31489
|
+
const kx = rr !== rl ? framed.exx / (rr - rl) : 1;
|
|
31490
|
+
const ky = rb !== rt ? framed.eyy / (rb - rt) : 1;
|
|
31491
|
+
const snap = (v) => Math.abs(v) < 1e-6 ? 0 : v;
|
|
31492
|
+
return [framed.ax + framed.exx / 2 + snap(raw[0] - (rl + rr) / 2) * kx, framed.ay + framed.eyy / 2 + snap(raw[1] - (rt + rb) / 2) * ky];
|
|
31383
31493
|
};
|
|
31384
31494
|
const rasterArgs = (immediate = false) => ({
|
|
31385
31495
|
box: inset ? framed : immediate && needsFill ? curvedFixBox(rCtx, l, t, r, b) : unframed,
|
|
31386
|
-
s: radialOnBox(fixPoint(rCtx, startX, startY)),
|
|
31496
|
+
s: radialOnBox(fixPoint(rCtx, startX, startY), rawFix(startX, startY)),
|
|
31387
31497
|
// Device FIX rounding can separate distinct radial points on the
|
|
31388
31498
|
// same logical ray. Preserve their proven endpoint identity.
|
|
31389
|
-
e: fullEllipse ? radialOnBox(fixPoint(rCtx, startX, startY)) : radialOnBox(fixPoint(rCtx, endX, endY)),
|
|
31499
|
+
e: fullEllipse ? radialOnBox(fixPoint(rCtx, startX, startY), rawFix(startX, startY)) : radialOnBox(fixPoint(rCtx, endX, endY), rawFix(endX, endY)),
|
|
31390
31500
|
from: currentFix(rCtx)
|
|
31391
31501
|
});
|
|
31392
31502
|
if (inPath) {
|
|
@@ -39683,6 +39793,7 @@ function compatBox(p, l, t, r, b, opts = {}) {
|
|
|
39683
39793
|
if ((p.rCtx.state.penStyle & 15) === PS_INSIDEFRAME2) {
|
|
39684
39794
|
const w = Math.round(penDeviceWidth(p.rCtx) / p.kx);
|
|
39685
39795
|
if (w > 1) {
|
|
39796
|
+
const whole = { ...box };
|
|
39686
39797
|
const matrix = gdiDeviceMatrix(p.rCtx);
|
|
39687
39798
|
const wx = Math.round(Math.abs(p.rCtx.state.penWidth * matrix[0]) / p.kx * 16);
|
|
39688
39799
|
const wy = Math.round(Math.abs(p.rCtx.state.penWidth * matrix[3]) / p.ky * 16);
|
|
@@ -39696,14 +39807,21 @@ function compatBox(p, l, t, r, b, opts = {}) {
|
|
|
39696
39807
|
if (half) {
|
|
39697
39808
|
box.halfX = true;
|
|
39698
39809
|
}
|
|
39810
|
+
if (opts.clampEmpty && (box.x1 < box.x0 || box.y1 < box.y0)) {
|
|
39811
|
+
return { x0: whole.x0, y0: whole.y0, x1: whole.x1 + Math.round(ux), y1: whole.y1 + Math.round(uy) };
|
|
39812
|
+
}
|
|
39699
39813
|
}
|
|
39700
39814
|
}
|
|
39701
39815
|
return box;
|
|
39702
39816
|
}
|
|
39703
|
-
function fixBoxOf(box) {
|
|
39704
|
-
const fix = axisBox(box.x0, box.y0, Math.max(box.x0, box.x1), Math.max(box.y0, box.y1));
|
|
39817
|
+
function fixBoxOf(box, cornerExact, keepInverted = false) {
|
|
39818
|
+
const fix = keepInverted ? axisBox(box.x0, box.y0, box.x1, box.y1) : axisBox(box.x0, box.y0, Math.max(box.x0, box.x1), Math.max(box.y0, box.y1));
|
|
39705
39819
|
if (box.halfX) {
|
|
39706
39820
|
fix.halfX = true;
|
|
39821
|
+
fix.halfXCompat = true;
|
|
39822
|
+
}
|
|
39823
|
+
if (cornerExact) {
|
|
39824
|
+
fix.cornerExact = cornerExact;
|
|
39707
39825
|
}
|
|
39708
39826
|
return fix;
|
|
39709
39827
|
}
|
|
@@ -39767,7 +39885,8 @@ function compatCorner(p, w, h, box, logical) {
|
|
|
39767
39885
|
const width2 = box.x1 - box.x0, height2 = box.y1 - box.y0;
|
|
39768
39886
|
const fullW2 = Math.abs((logical[2] - logical[0]) * m[0]) * 16 || width2;
|
|
39769
39887
|
const fullH2 = Math.abs((logical[3] - logical[1]) * m[3]) * 16 || height2;
|
|
39770
|
-
|
|
39888
|
+
const cornerW = Math.min(Math.abs(w * m[0]) * 16, fullW2) * width2 / fullW2;
|
|
39889
|
+
return [Math.floor(cornerW), Math.floor(Math.min(Math.abs(h * m[3]) * 16, fullH2) * height2 / fullH2), [cornerW, Math.min(Math.abs(h * m[3]) * 16, fullH2) * height2 / fullH2]];
|
|
39771
39890
|
}
|
|
39772
39891
|
const width = box.x1 - box.x0, height = box.y1 - box.y0;
|
|
39773
39892
|
const fullW = Math.abs((logical[2] - logical[0]) * m[0]) * 16 || width + ux;
|
|
@@ -39776,11 +39895,11 @@ function compatCorner(p, w, h, box, logical) {
|
|
|
39776
39895
|
return penIsCosmetic(p.rCtx) && !penIsNull(p) ? scaled2 : [Math.floor(scaled2[0]), Math.floor(scaled2[1])];
|
|
39777
39896
|
}
|
|
39778
39897
|
function wmfRoundRect(p, l, t, r, b, w, h) {
|
|
39779
|
-
const box = compatBox(p, l, t, r, b, { curved: true, half: true });
|
|
39898
|
+
const box = compatBox(p, l, t, r, b, { curved: true, half: true, clampEmpty: true });
|
|
39780
39899
|
if (box.x1 < box.x0 || box.y1 < box.y0) {
|
|
39781
39900
|
return;
|
|
39782
39901
|
}
|
|
39783
|
-
const [cw, ch] = compatCorner(p, w, h, box, [l, t, r, b]);
|
|
39902
|
+
const [cw, ch, half] = compatCorner(p, w, h, box, [l, t, r, b]);
|
|
39784
39903
|
const cr = canvasRect(box);
|
|
39785
39904
|
paintGdiShape(p.rCtx, {
|
|
39786
39905
|
build: (c) => {
|
|
@@ -39805,14 +39924,14 @@ function wmfRoundRect(p, l, t, r, b, w, h) {
|
|
|
39805
39924
|
c.ellipse(cr.x + ex, cr.y + ey, ex, ey, 0, 2 * q, 3 * q);
|
|
39806
39925
|
c.closePath();
|
|
39807
39926
|
},
|
|
39808
|
-
raster: () => roundRectRasterPath(fixBoxOf(box), cw, ch, clockwiseOf(p), w === 0 || h === 0, !penIsNull(p) && penIsCosmetic(p.rCtx)),
|
|
39927
|
+
raster: () => roundRectRasterPath(fixBoxOf(box, half), cw, ch, clockwiseOf(p), w === 0 || h === 0, !penIsNull(p) && penIsCosmetic(p.rCtx)),
|
|
39809
39928
|
roundPen: true,
|
|
39810
39929
|
fill: true,
|
|
39811
39930
|
stroke: true
|
|
39812
39931
|
});
|
|
39813
39932
|
}
|
|
39814
39933
|
function wmfEllipse(p, l, t, r, b) {
|
|
39815
|
-
const box = compatBox(p, l, t, r, b, { curved: true, half: true });
|
|
39934
|
+
const box = compatBox(p, l, t, r, b, { curved: true, half: true, clampEmpty: true });
|
|
39816
39935
|
if (box.x1 < box.x0 || box.y1 < box.y0) {
|
|
39817
39936
|
return;
|
|
39818
39937
|
}
|
|
@@ -39828,9 +39947,9 @@ function wmfEllipse(p, l, t, r, b) {
|
|
|
39828
39947
|
stroke: true
|
|
39829
39948
|
});
|
|
39830
39949
|
}
|
|
39831
|
-
function compatArc(p, kind, l, t, r, b, xs, ys, xe, ye) {
|
|
39832
|
-
const box = compatBox(p, l, t, r, b, { curved: kind === "chord" || kind === "pie", roundFrame: true });
|
|
39833
|
-
if (box.
|
|
39950
|
+
function compatArc(p, kind, l, t, r, b, xs, ys, xe, ye, keepInverted = false) {
|
|
39951
|
+
const box = compatBox(p, l, t, r, b, { curved: kind === "chord" || kind === "pie", roundFrame: true, half: true });
|
|
39952
|
+
if (box.y1 < box.y0 || !keepInverted && box.x1 < box.x0) {
|
|
39834
39953
|
return null;
|
|
39835
39954
|
}
|
|
39836
39955
|
const m = gdiDeviceMatrix(p.rCtx);
|