emf-converter 4.8.19 → 4.8.21
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 +151 -34
- package/dist/browser.mjs +151 -34
- package/dist/index.js +151 -34
- package/dist/index.mjs +151 -34
- package/package.json +5 -5
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 {
|
|
@@ -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") {
|
|
@@ -30954,9 +31028,24 @@ function insideFrameInset(rCtx, box, rectangle = false, clockwiseRoundRect = fal
|
|
|
30954
31028
|
right: Math.round((rectangle ? lo : hi) * px),
|
|
30955
31029
|
top: y,
|
|
30956
31030
|
bottom: y,
|
|
30957
|
-
half: wx % 2 === 1 && !rectangle
|
|
31031
|
+
half: wx % 2 === 1 && !rectangle,
|
|
31032
|
+
halfRect: wx % 2 === 1 && rectangle
|
|
30958
31033
|
};
|
|
30959
31034
|
}
|
|
31035
|
+
function insideFramePenTooWide(rCtx, box) {
|
|
31036
|
+
if ((rCtx.state.penStyle & 15) !== PS_INSIDEFRAME || !isAxisBox(box) || hasWorldRotation(rCtx)) {
|
|
31037
|
+
return false;
|
|
31038
|
+
}
|
|
31039
|
+
const m = gdiDeviceMatrix(rCtx);
|
|
31040
|
+
const px = gdiDevicePixelX(rCtx);
|
|
31041
|
+
const py = gdiDevicePixelY(rCtx);
|
|
31042
|
+
const widthX = Math.abs(rCtx.state.penWidth * m[0]) / px * 16;
|
|
31043
|
+
const widthY = Math.abs(rCtx.state.penWidth * m[3]) / py * 16;
|
|
31044
|
+
if (Math.max(widthX, widthY) < 24) {
|
|
31045
|
+
return false;
|
|
31046
|
+
}
|
|
31047
|
+
return Math.abs(box.exx) / px < Math.round(widthX) || Math.abs(box.eyy) / py < Math.round(widthY);
|
|
31048
|
+
}
|
|
30960
31049
|
function insetFixBox(box, inset) {
|
|
30961
31050
|
const sx = box.exx < 0 ? -1 : 1;
|
|
30962
31051
|
const sy = box.eyy < 0 ? -1 : 1;
|
|
@@ -30966,11 +31055,15 @@ function insetFixBox(box, inset) {
|
|
|
30966
31055
|
exx: box.exx - sx * (inset.left + inset.right),
|
|
30967
31056
|
exy: 0,
|
|
30968
31057
|
eyx: 0,
|
|
30969
|
-
eyy: box.eyy - sy * (inset.top + inset.bottom)
|
|
31058
|
+
eyy: box.eyy - sy * (inset.top + inset.bottom),
|
|
31059
|
+
framed: true
|
|
30970
31060
|
};
|
|
30971
31061
|
if (inset.half && sx > 0) {
|
|
30972
31062
|
framed.halfX = true;
|
|
30973
31063
|
}
|
|
31064
|
+
if (inset.halfRect && sx > 0) {
|
|
31065
|
+
framed.halfXRect = true;
|
|
31066
|
+
}
|
|
30974
31067
|
return framed;
|
|
30975
31068
|
}
|
|
30976
31069
|
function sameEllipseRay(l, t, r, b, sx, sy, ex, ey) {
|
|
@@ -31053,7 +31146,7 @@ function handleRectangle(rCtx, dataOff, recSize) {
|
|
|
31053
31146
|
const r = view.getInt32(dataOff + 8, true);
|
|
31054
31147
|
const b = view.getInt32(dataOff + 12, true);
|
|
31055
31148
|
const unframed = uprightFixBox(rCtx, l, t, r, b);
|
|
31056
|
-
const inset = insideFrameInset(rCtx, unframed, true);
|
|
31149
|
+
const inset = insideFrameInset(rCtx, unframed, true, rCtx.state.arcDirection === 2);
|
|
31057
31150
|
const box = inset ? insetFixBox(unframed, inset) : unframed;
|
|
31058
31151
|
const clockwise = rCtx.state.arcDirection === 2;
|
|
31059
31152
|
if (!inPath) {
|
|
@@ -31239,6 +31332,13 @@ function handleRoundRect(rCtx, dataOff, recSize) {
|
|
|
31239
31332
|
gmh(rCtx, cornerH) / 2,
|
|
31240
31333
|
null
|
|
31241
31334
|
);
|
|
31335
|
+
if (inset) {
|
|
31336
|
+
const fullW = Math.abs((r - l) * devM[0]) * 16;
|
|
31337
|
+
const fullH = Math.abs((b - t) * devM[3]) * 16;
|
|
31338
|
+
if (fullW > 0 && fullH > 0) {
|
|
31339
|
+
framed.cornerExact = [Math.min(cornerFixW, fullW) * Math.abs(framed.exx) / fullW, Math.min(cornerFixH, fullH) * Math.abs(framed.eyy) / fullH];
|
|
31340
|
+
}
|
|
31341
|
+
}
|
|
31242
31342
|
const raster = (box = framed) => roundRectRasterPath(box, cw, ch, clockwise, cornerW === 0 || cornerH === 0);
|
|
31243
31343
|
if (inPath) {
|
|
31244
31344
|
const box = framed;
|
|
@@ -31292,7 +31392,7 @@ function handleEllipse(rCtx, dataOff, recSize) {
|
|
|
31292
31392
|
const b = view.getInt32(dataOff + 12, true);
|
|
31293
31393
|
const clockwise = rCtx.state.arcDirection === 2;
|
|
31294
31394
|
const unframed = uprightFixBox(rCtx, l, t, r, b);
|
|
31295
|
-
const inset = insideFrameInset(rCtx, unframed);
|
|
31395
|
+
const inset = insideFrameInset(rCtx, unframed, false, rCtx.state.arcDirection === 2);
|
|
31296
31396
|
const framed = inset ? insetFixBox(unframed, inset) : unframed;
|
|
31297
31397
|
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
31398
|
cx: gmx(rCtx, (l + r) / 2),
|
|
@@ -31342,7 +31442,10 @@ function handleArcFamily(rCtx, recType, dataOff, recSize) {
|
|
|
31342
31442
|
const endAngle = fullEllipse ? startAngle + (clockwise ? 2 : -2) * Math.PI : Math.atan2((endY - cyA) / (ry || 1), (endX - cxA) / (rx || 1));
|
|
31343
31443
|
const rotated = hasWorldRotation(rCtx);
|
|
31344
31444
|
const unframed = fixBox(rCtx, l, t, r, b);
|
|
31345
|
-
|
|
31445
|
+
if (recType !== EMR_ARCTO && insideFramePenTooWide(rCtx, unframed)) {
|
|
31446
|
+
return true;
|
|
31447
|
+
}
|
|
31448
|
+
const inset = insideFrameInset(rCtx, unframed, false, rCtx.state.arcDirection === 2);
|
|
31346
31449
|
const framed = inset ? insetFixBox(unframed, inset) : unframed;
|
|
31347
31450
|
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
31451
|
cx: gmx(rCtx, cxA),
|
|
@@ -31373,20 +31476,25 @@ function handleArcFamily(rCtx, recType, dataOff, recSize) {
|
|
|
31373
31476
|
}
|
|
31374
31477
|
};
|
|
31375
31478
|
const kind = isArcTo ? "arcto" : recType === EMR_PIE ? "pie" : recType === EMR_CHORD ? "chord" : "arc";
|
|
31376
|
-
const
|
|
31479
|
+
const devM = gdiDeviceMatrix(rCtx);
|
|
31480
|
+
const rawFix = (x, y) => [(devM[0] * x + devM[2] * y + devM[4]) * 16, (devM[1] * x + devM[3] * y + devM[5]) * 16];
|
|
31481
|
+
const radialOnBox = (p, raw) => {
|
|
31377
31482
|
if (!inset) {
|
|
31378
31483
|
return p;
|
|
31379
31484
|
}
|
|
31380
|
-
const
|
|
31381
|
-
const
|
|
31382
|
-
|
|
31485
|
+
const [rl, rt] = rawFix(l, t);
|
|
31486
|
+
const [rr, rb] = rawFix(r, b);
|
|
31487
|
+
const kx = rr !== rl ? framed.exx / (rr - rl) : 1;
|
|
31488
|
+
const ky = rb !== rt ? framed.eyy / (rb - rt) : 1;
|
|
31489
|
+
const snap = (v) => Math.abs(v) < 1e-6 ? 0 : v;
|
|
31490
|
+
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
31491
|
};
|
|
31384
31492
|
const rasterArgs = (immediate = false) => ({
|
|
31385
31493
|
box: inset ? framed : immediate && needsFill ? curvedFixBox(rCtx, l, t, r, b) : unframed,
|
|
31386
|
-
s: radialOnBox(fixPoint(rCtx, startX, startY)),
|
|
31494
|
+
s: radialOnBox(fixPoint(rCtx, startX, startY), rawFix(startX, startY)),
|
|
31387
31495
|
// Device FIX rounding can separate distinct radial points on the
|
|
31388
31496
|
// same logical ray. Preserve their proven endpoint identity.
|
|
31389
|
-
e: fullEllipse ? radialOnBox(fixPoint(rCtx, startX, startY)) : radialOnBox(fixPoint(rCtx, endX, endY)),
|
|
31497
|
+
e: fullEllipse ? radialOnBox(fixPoint(rCtx, startX, startY), rawFix(startX, startY)) : radialOnBox(fixPoint(rCtx, endX, endY), rawFix(endX, endY)),
|
|
31390
31498
|
from: currentFix(rCtx)
|
|
31391
31499
|
});
|
|
31392
31500
|
if (inPath) {
|
|
@@ -39683,6 +39791,7 @@ function compatBox(p, l, t, r, b, opts = {}) {
|
|
|
39683
39791
|
if ((p.rCtx.state.penStyle & 15) === PS_INSIDEFRAME2) {
|
|
39684
39792
|
const w = Math.round(penDeviceWidth(p.rCtx) / p.kx);
|
|
39685
39793
|
if (w > 1) {
|
|
39794
|
+
const whole = { ...box };
|
|
39686
39795
|
const matrix = gdiDeviceMatrix(p.rCtx);
|
|
39687
39796
|
const wx = Math.round(Math.abs(p.rCtx.state.penWidth * matrix[0]) / p.kx * 16);
|
|
39688
39797
|
const wy = Math.round(Math.abs(p.rCtx.state.penWidth * matrix[3]) / p.ky * 16);
|
|
@@ -39696,14 +39805,21 @@ function compatBox(p, l, t, r, b, opts = {}) {
|
|
|
39696
39805
|
if (half) {
|
|
39697
39806
|
box.halfX = true;
|
|
39698
39807
|
}
|
|
39808
|
+
if (opts.clampEmpty && (box.x1 < box.x0 || box.y1 < box.y0)) {
|
|
39809
|
+
return { x0: whole.x0, y0: whole.y0, x1: whole.x1 + Math.round(ux), y1: whole.y1 + Math.round(uy) };
|
|
39810
|
+
}
|
|
39699
39811
|
}
|
|
39700
39812
|
}
|
|
39701
39813
|
return box;
|
|
39702
39814
|
}
|
|
39703
|
-
function fixBoxOf(box) {
|
|
39704
|
-
const fix = axisBox(box.x0, box.y0, Math.max(box.x0, box.x1), Math.max(box.y0, box.y1));
|
|
39815
|
+
function fixBoxOf(box, cornerExact, keepInverted = false) {
|
|
39816
|
+
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
39817
|
if (box.halfX) {
|
|
39706
39818
|
fix.halfX = true;
|
|
39819
|
+
fix.halfXCompat = true;
|
|
39820
|
+
}
|
|
39821
|
+
if (cornerExact) {
|
|
39822
|
+
fix.cornerExact = cornerExact;
|
|
39707
39823
|
}
|
|
39708
39824
|
return fix;
|
|
39709
39825
|
}
|
|
@@ -39767,7 +39883,8 @@ function compatCorner(p, w, h, box, logical) {
|
|
|
39767
39883
|
const width2 = box.x1 - box.x0, height2 = box.y1 - box.y0;
|
|
39768
39884
|
const fullW2 = Math.abs((logical[2] - logical[0]) * m[0]) * 16 || width2;
|
|
39769
39885
|
const fullH2 = Math.abs((logical[3] - logical[1]) * m[3]) * 16 || height2;
|
|
39770
|
-
|
|
39886
|
+
const cornerW = Math.min(Math.abs(w * m[0]) * 16, fullW2) * width2 / fullW2;
|
|
39887
|
+
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
39888
|
}
|
|
39772
39889
|
const width = box.x1 - box.x0, height = box.y1 - box.y0;
|
|
39773
39890
|
const fullW = Math.abs((logical[2] - logical[0]) * m[0]) * 16 || width + ux;
|
|
@@ -39776,11 +39893,11 @@ function compatCorner(p, w, h, box, logical) {
|
|
|
39776
39893
|
return penIsCosmetic(p.rCtx) && !penIsNull(p) ? scaled2 : [Math.floor(scaled2[0]), Math.floor(scaled2[1])];
|
|
39777
39894
|
}
|
|
39778
39895
|
function wmfRoundRect(p, l, t, r, b, w, h) {
|
|
39779
|
-
const box = compatBox(p, l, t, r, b, { curved: true, half: true });
|
|
39896
|
+
const box = compatBox(p, l, t, r, b, { curved: true, half: true, clampEmpty: true });
|
|
39780
39897
|
if (box.x1 < box.x0 || box.y1 < box.y0) {
|
|
39781
39898
|
return;
|
|
39782
39899
|
}
|
|
39783
|
-
const [cw, ch] = compatCorner(p, w, h, box, [l, t, r, b]);
|
|
39900
|
+
const [cw, ch, half] = compatCorner(p, w, h, box, [l, t, r, b]);
|
|
39784
39901
|
const cr = canvasRect(box);
|
|
39785
39902
|
paintGdiShape(p.rCtx, {
|
|
39786
39903
|
build: (c) => {
|
|
@@ -39805,14 +39922,14 @@ function wmfRoundRect(p, l, t, r, b, w, h) {
|
|
|
39805
39922
|
c.ellipse(cr.x + ex, cr.y + ey, ex, ey, 0, 2 * q, 3 * q);
|
|
39806
39923
|
c.closePath();
|
|
39807
39924
|
},
|
|
39808
|
-
raster: () => roundRectRasterPath(fixBoxOf(box), cw, ch, clockwiseOf(p), w === 0 || h === 0, !penIsNull(p) && penIsCosmetic(p.rCtx)),
|
|
39925
|
+
raster: () => roundRectRasterPath(fixBoxOf(box, half), cw, ch, clockwiseOf(p), w === 0 || h === 0, !penIsNull(p) && penIsCosmetic(p.rCtx)),
|
|
39809
39926
|
roundPen: true,
|
|
39810
39927
|
fill: true,
|
|
39811
39928
|
stroke: true
|
|
39812
39929
|
});
|
|
39813
39930
|
}
|
|
39814
39931
|
function wmfEllipse(p, l, t, r, b) {
|
|
39815
|
-
const box = compatBox(p, l, t, r, b, { curved: true, half: true });
|
|
39932
|
+
const box = compatBox(p, l, t, r, b, { curved: true, half: true, clampEmpty: true });
|
|
39816
39933
|
if (box.x1 < box.x0 || box.y1 < box.y0) {
|
|
39817
39934
|
return;
|
|
39818
39935
|
}
|
|
@@ -39828,9 +39945,9 @@ function wmfEllipse(p, l, t, r, b) {
|
|
|
39828
39945
|
stroke: true
|
|
39829
39946
|
});
|
|
39830
39947
|
}
|
|
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.
|
|
39948
|
+
function compatArc(p, kind, l, t, r, b, xs, ys, xe, ye, keepInverted = false) {
|
|
39949
|
+
const box = compatBox(p, l, t, r, b, { curved: kind === "chord" || kind === "pie", roundFrame: true, half: true });
|
|
39950
|
+
if (box.y1 < box.y0 || !keepInverted && box.x1 < box.x0) {
|
|
39834
39951
|
return null;
|
|
39835
39952
|
}
|
|
39836
39953
|
const m = gdiDeviceMatrix(p.rCtx);
|