emf-converter 4.2.0 → 4.2.1

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 CHANGED
@@ -214,7 +214,7 @@ Everything below is verified against output painted by Windows itself; `src/gdi-
214
214
 
215
215
  Everything is measured against output painted by Windows itself; `src/gdi-parity.fixture.test.ts` holds the exact per-fixture bounds.
216
216
 
217
- - **Colour adjustment and image effects**: `EMR_SETCOLORADJUSTMENT` is read but not yet applied, and the `HALFTONE` stretch mode is not bit-exact. EMF+ image effects (`SerializableObject`) are applied to the image's pixels before it is drawn. Colour matrix and lookup table effects follow GDI+'s definitions; MS-EMFPLUS does not specify the algorithms of the others (blur, sharpen, brightness/contrast, levels, colour balance, colour curves, hue/saturation/lightness, tint, red-eye), so those use documented approximations and have not been checked against GDI+ output. A blur with `expandEdge` keeps the image's size instead of growing it, and an effect is skipped when the image's pixels are not available (a PNG or JPEG in SVG output without an image decoder).
217
+ - **Colour adjustment and image effects**: `EMR_SETCOLORADJUSTMENT` is read but not yet applied, and the `HALFTONE` stretch mode is not bit-exact. EMF+ image effects (`SerializableObject`) are applied to the image's pixels before it is drawn. Colour matrix and lookup table effects follow GDI+'s definitions; MS-EMFPLUS does not specify the algorithms of the others (blur, sharpen, brightness/contrast, levels, colour balance, colour curves, hue/saturation/lightness, tint, red-eye), so those use documented approximations and have not been checked against GDI+ output. An effect is applied to the draw's source rectangle only, and a blur with `expandEdge` grows it by the blur radius, as GDI+ does. When the source rectangle is not in pixels, the effect is applied to the whole image without that growth. An effect is skipped when the image cannot be decoded to pixels: PNG and BMP always can be, while JPEG, GIF, TIFF and similar formats need a canvas backend (a browser or `@napi-rs/canvas`).
218
218
  - **Pen transforms**: nonuniform and skewed EMF+ pen transforms are drawn with the transformed (elliptical or sheared) nib, but unlike uniform ones they are not yet verified against Windows output, and a singular pen transform is ignored.
219
219
  - **Text**: ANSI, multi-string and small-text records now render; the Windows text fixtures retain a few glyph-edge differences and a `PolyTextOut` C1 control-glyph difference (under 0.1% of pixels; bounds in `src/emf-text-records.fixture.test.ts`). ANSI decoding uses the host's `TextDecoder` for common Windows code pages; unsupported encodings (including Johab and OEM on standard runtimes) fall back to Windows-1252. Vertical `ETO_PDY` advances require the `fonts` engine. Without `fonts`, SVG text measurements use deterministic estimates, so precise justification requires supplying the matching fonts.
220
220
  - **Wide pens and paths**: flat-capped GDI pens 7 px and wider can differ by a few pixels at round joins, and dashed wide Bezier curves follow `WidenPath` (which Windows' direct drawing does not quite match); at most 0.2% of pixels on the fixtures. `EMR_WIDENPATH` does not reproduce the extra inner join triangles GDI's own `WidenPath` emits (visible only when the widened outline is itself stroked). EMF+ 1-pixel antialiased lines can differ by one antialiasing sample at their ends, some closed widened outlines by one sample along an edge, and Inset or compound pens on closed figures are approximate.
package/dist/index.js CHANGED
@@ -19177,7 +19177,7 @@ function paintBrushThroughMask(rCtx, sampler, box, drawCoverage, mode = "canvas"
19177
19177
  const device = plusWorldMatrix(rCtx);
19178
19178
  const m = mask.ctx;
19179
19179
  const shift = aliased ? aliasedSampleShift(rCtx) : geometry ? plusCanvasShift(rCtx) : 0;
19180
- m.setTransform(device[0], device[1], device[2], device[3], device[4] - box.x + shift + Number(process.env.HDX ?? 0), device[5] - box.y + shift + Number(process.env.HDY ?? 0));
19180
+ m.setTransform(device[0], device[1], device[2], device[3], device[4] - box.x + shift, device[5] - box.y + shift);
19181
19181
  m.fillStyle = "#000";
19182
19182
  m.strokeStyle = "#000";
19183
19183
  drawCoverage(rasterMode !== "canvas" ? flatteningContext(m, device) : m);
@@ -23940,6 +23940,50 @@ function applyImageEffect(rgba, width, height, effect) {
23940
23940
  return applyTint(rgba, effect.hue, effect.amount);
23941
23941
  }
23942
23942
  }
23943
+ function applyImageEffectToRect(rgba, width, height, effect, src) {
23944
+ if (width <= 0 || height <= 0 || rgba.length !== width * height * 4) {
23945
+ return null;
23946
+ }
23947
+ let x0 = 0;
23948
+ let y0 = 0;
23949
+ let x1 = width;
23950
+ let y1 = height;
23951
+ if (src) {
23952
+ x0 = clamp(Math.floor(src.x), 0, width);
23953
+ y0 = clamp(Math.floor(src.y), 0, height);
23954
+ x1 = clamp(Math.ceil(src.x + src.w), 0, width);
23955
+ y1 = clamp(Math.ceil(src.y + src.h), 0, height);
23956
+ if (x1 <= x0 || y1 <= y0) {
23957
+ return null;
23958
+ }
23959
+ }
23960
+ const pad = src && effect.kind === "blur" && effect.expandEdge ? Math.ceil(effect.radius) : 0;
23961
+ const cw = x1 - x0;
23962
+ const ch = y1 - y0;
23963
+ const w = cw + 2 * pad;
23964
+ const h = ch + 2 * pad;
23965
+ let region = rgba;
23966
+ if (w !== width || h !== height || x0 !== 0 || y0 !== 0) {
23967
+ region = new Uint8ClampedArray(w * h * 4);
23968
+ for (let y = 0; y < ch; y++) {
23969
+ const from = ((y0 + y) * width + x0) * 4;
23970
+ region.set(rgba.subarray(from, from + cw * 4), ((pad + y) * w + pad) * 4);
23971
+ }
23972
+ }
23973
+ const ox = x0 - pad;
23974
+ const oy = y0 - pad;
23975
+ const local = effect.kind === "redEyeCorrection" ? {
23976
+ kind: effect.kind,
23977
+ areas: effect.areas.map((a) => ({
23978
+ left: a.left - ox,
23979
+ top: a.top - oy,
23980
+ right: a.right - ox,
23981
+ bottom: a.bottom - oy
23982
+ }))
23983
+ } : effect;
23984
+ const out = applyImageEffect(region, w, h, local);
23985
+ return out ? { rgba: out, x: ox, y: oy, width: w, height: h, pad } : null;
23986
+ }
23943
23987
 
23944
23988
  // src/emf-plus-draw-image.ts
23945
23989
  function surfaceSize4(ctx) {
@@ -24076,7 +24120,7 @@ function drawNestedMetafile(rCtx, img, cached, source) {
24076
24120
  rCtx.deferredImages.push(...deferred);
24077
24121
  return true;
24078
24122
  }
24079
- function effectedBitmap(img, bitmap, effect) {
24123
+ function effectedDraw(img, bitmap, effect, draw, source) {
24080
24124
  let src = bitmap;
24081
24125
  if (!src && img.data) {
24082
24126
  const bmp = decodeBmpFile(img.data.slice(0));
@@ -24087,21 +24131,61 @@ function effectedBitmap(img, bitmap, effect) {
24087
24131
  if (!src) {
24088
24132
  return null;
24089
24133
  }
24090
- const rgba = applyImageEffect(src.rgba, src.width, src.height, effect);
24091
- return rgba ? { kind: "bitmap", width: src.width, height: src.height, rgba } : null;
24134
+ const rect = source && source.unit === UNIT_PIXEL && source.srcW > 0 && source.srcH > 0 && [source.srcX, source.srcY, source.srcW, source.srcH].every(Number.isFinite) ? source : null;
24135
+ const region = applyImageEffectToRect(
24136
+ src.rgba,
24137
+ src.width,
24138
+ src.height,
24139
+ effect,
24140
+ rect && { x: rect.srcX, y: rect.srcY, w: rect.srcW, h: rect.srcH }
24141
+ );
24142
+ if (!region) {
24143
+ return null;
24144
+ }
24145
+ const out = { kind: "bitmap", width: region.width, height: region.height, rgba: region.rgba };
24146
+ if (!rect) {
24147
+ return { bitmap: out, draw };
24148
+ }
24149
+ const grow = region.pad;
24150
+ const srcX = rect.srcX - grow - region.x;
24151
+ const srcY = rect.srcY - grow - region.y;
24152
+ const srcW = rect.srcW + 2 * grow;
24153
+ const srcH = rect.srcH + 2 * grow;
24154
+ const shift = [1, 0, 0, 1, region.x, region.y];
24155
+ const toWorld = mulMatrix(rect.toWorld(rect.srcX, rect.srcY, rect.srcW, rect.srcH), shift);
24156
+ return {
24157
+ bitmap: out,
24158
+ draw: {
24159
+ ...draw,
24160
+ // The Canvas fallback scales the whole effected bitmap.
24161
+ transform: mulMatrix(draw.transform, toWorld),
24162
+ dx: 0,
24163
+ dy: 0,
24164
+ dw: region.width,
24165
+ dh: region.height,
24166
+ resample: draw.resample && {
24167
+ ...draw.resample,
24168
+ srcX,
24169
+ srcY,
24170
+ srcW,
24171
+ srcH,
24172
+ toDevice: mulMatrix(draw.resample.toDevice, shift)
24173
+ }
24174
+ }
24175
+ };
24092
24176
  }
24093
24177
  function drawEmfPlusImageNow(rCtx, img, draw, source, effect = null) {
24094
24178
  const cached = img.cacheKey !== void 0 ? rCtx.imageCache?.get(img.cacheKey) : void 0;
24095
24179
  if (img.type === 2) {
24096
24180
  return drawNestedMetafile(rCtx, img, cached, source);
24097
24181
  }
24098
- let bitmap = cached && cached.kind === "bitmap" ? cached : null;
24099
- const effected = effect ? effectedBitmap(img, bitmap, effect) : null;
24182
+ const bitmap = cached && cached.kind === "bitmap" ? cached : null;
24183
+ const effected = effect ? effectedDraw(img, bitmap, effect, draw, source) : null;
24100
24184
  if (effected) {
24101
- bitmap = effected;
24185
+ return isSvgContext(rCtx.ctx) ? paintBitmapSvg(rCtx.ctx, img, effected.bitmap, effected.draw, true) : paintBitmapRaster(rCtx.ctx, effected.bitmap, effected.draw);
24102
24186
  }
24103
24187
  if (isSvgContext(rCtx.ctx)) {
24104
- return paintBitmapSvg(rCtx.ctx, img, bitmap, draw, effected !== null);
24188
+ return paintBitmapSvg(rCtx.ctx, img, bitmap, draw);
24105
24189
  }
24106
24190
  return bitmap ? paintBitmapRaster(rCtx.ctx, bitmap, draw) : false;
24107
24191
  }
@@ -24393,28 +24477,32 @@ function strokePlusGeometry(rCtx, pen, buildPath, points, closedFigure = false)
24393
24477
  if (mode !== "canvas" && size && strokeGdiplus(rCtx, pen, nib, sampler, buildPath, device, size, mode, closedFigure)) {
24394
24478
  return;
24395
24479
  }
24396
- const miter = Math.max(1, ctx.miterLimit || 10);
24397
- const margin = pen.width * scale * miter / 2 + 2;
24398
- const box = size ? deviceBounds(points, device, size, margin) : null;
24480
+ const outline = hasCustomCap(pen) ? penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure) : null;
24481
+ let box = null;
24482
+ if (size && outline) {
24483
+ box = deviceBounds(outline.flat(), device, size, 2);
24484
+ } else if (size) {
24485
+ const miter = Math.max(1, ctx.miterLimit || 10);
24486
+ const margin = pen.width * scale * miter / 2 + 2;
24487
+ box = deviceBounds(points, device, size, margin);
24488
+ }
24399
24489
  if (size && !box) {
24400
24490
  return;
24401
24491
  }
24402
- if (box && paintBrushThroughMask(
24403
- rCtx,
24404
- sampler,
24405
- box,
24406
- (c) => {
24407
- applyPlusPenStyle(c, pen, widthScale);
24408
- stroke(c);
24409
- c.beginPath();
24410
- buildPath(c);
24411
- if (nib) {
24412
- c.transform(nib[0], nib[1], nib[2], nib[3], 0, 0);
24413
- }
24414
- },
24415
- mode,
24416
- (c, x, y) => c.isPointInStroke(x, y) && (!inset || c.isPointInPath(x, y))
24417
- )) {
24492
+ const drawMask = outline ? (c) => {
24493
+ traceOutline(c, outline);
24494
+ c.fill("nonzero");
24495
+ } : (c) => {
24496
+ applyPlusPenStyle(c, pen, widthScale);
24497
+ stroke(c);
24498
+ c.beginPath();
24499
+ buildPath(c);
24500
+ if (nib) {
24501
+ c.transform(nib[0], nib[1], nib[2], nib[3], 0, 0);
24502
+ }
24503
+ };
24504
+ const hitTest = outline ? (c, x, y) => c.isPointInPath(x, y, "nonzero") : (c, x, y) => c.isPointInStroke(x, y) && (!inset || c.isPointInPath(x, y));
24505
+ if (box && paintBrushThroughMask(rCtx, sampler, box, drawMask, mode, hitTest)) {
24418
24506
  return;
24419
24507
  }
24420
24508
  }
@@ -24579,18 +24667,18 @@ function customCapOutline(figures, width, devicePen, pen) {
24579
24667
  outline.push(...geo.polygons);
24580
24668
  return outline;
24581
24669
  }
24582
- function fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure) {
24670
+ function penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure) {
24583
24671
  const device = plusWorldMatrix(rCtx);
24584
24672
  const frame2 = penFrame(device, nib);
24585
24673
  const deviceInv = invertAffine(device);
24586
24674
  if (!frame2 || !deviceInv || !(frame2.unit > 0)) {
24587
- return false;
24675
+ return null;
24588
24676
  }
24589
24677
  let figures;
24590
24678
  try {
24591
24679
  figures = recordDeviceFigures(buildPath, frame2.record);
24592
24680
  } catch {
24593
- return false;
24681
+ return null;
24594
24682
  }
24595
24683
  if (closedFigure) {
24596
24684
  figures = figures.map((f) => ({ ...f, closed: true }));
@@ -24598,25 +24686,33 @@ function fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure) {
24598
24686
  const width = (pen.width || 1) * frame2.unit;
24599
24687
  const devicePen = framePen(pen, frame2.unit);
24600
24688
  const outline = hasCustomCap(pen) ? customCapOutline(figures, width, devicePen, pen) : widenFigures(figures, devicePen);
24601
- const toWorld = mulMatrix(deviceInv, frame2.toDevice);
24689
+ const m = mulMatrix(deviceInv, frame2.toDevice);
24690
+ return outline.map((poly) => poly.map((p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] })));
24691
+ }
24692
+ function traceOutline(c, outline) {
24693
+ c.beginPath();
24694
+ for (const poly of outline) {
24695
+ poly.forEach((p, i) => {
24696
+ if (i === 0) {
24697
+ c.moveTo(p.x, p.y);
24698
+ } else {
24699
+ c.lineTo(p.x, p.y);
24700
+ }
24701
+ });
24702
+ c.closePath();
24703
+ }
24704
+ }
24705
+ function fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure) {
24706
+ const outline = penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure);
24707
+ if (!outline) {
24708
+ return false;
24709
+ }
24602
24710
  const { ctx } = rCtx;
24603
24711
  ctx.save();
24604
24712
  try {
24605
24713
  ctx.fillStyle = paint;
24606
24714
  applyPlusWorldTransform(rCtx);
24607
- ctx.beginPath();
24608
- for (const poly of outline) {
24609
- poly.forEach((p, i) => {
24610
- const x = toWorld[0] * p.x + toWorld[2] * p.y + toWorld[4];
24611
- const y = toWorld[1] * p.x + toWorld[3] * p.y + toWorld[5];
24612
- if (i === 0) {
24613
- ctx.moveTo(x, y);
24614
- } else {
24615
- ctx.lineTo(x, y);
24616
- }
24617
- });
24618
- ctx.closePath();
24619
- }
24715
+ traceOutline(ctx, outline);
24620
24716
  ctx.fill("nonzero");
24621
24717
  } finally {
24622
24718
  ctx.restore();
package/dist/index.mjs CHANGED
@@ -19175,7 +19175,7 @@ function paintBrushThroughMask(rCtx, sampler, box, drawCoverage, mode = "canvas"
19175
19175
  const device = plusWorldMatrix(rCtx);
19176
19176
  const m = mask.ctx;
19177
19177
  const shift = aliased ? aliasedSampleShift(rCtx) : geometry ? plusCanvasShift(rCtx) : 0;
19178
- m.setTransform(device[0], device[1], device[2], device[3], device[4] - box.x + shift + Number(process.env.HDX ?? 0), device[5] - box.y + shift + Number(process.env.HDY ?? 0));
19178
+ m.setTransform(device[0], device[1], device[2], device[3], device[4] - box.x + shift, device[5] - box.y + shift);
19179
19179
  m.fillStyle = "#000";
19180
19180
  m.strokeStyle = "#000";
19181
19181
  drawCoverage(rasterMode !== "canvas" ? flatteningContext(m, device) : m);
@@ -23938,6 +23938,50 @@ function applyImageEffect(rgba, width, height, effect) {
23938
23938
  return applyTint(rgba, effect.hue, effect.amount);
23939
23939
  }
23940
23940
  }
23941
+ function applyImageEffectToRect(rgba, width, height, effect, src) {
23942
+ if (width <= 0 || height <= 0 || rgba.length !== width * height * 4) {
23943
+ return null;
23944
+ }
23945
+ let x0 = 0;
23946
+ let y0 = 0;
23947
+ let x1 = width;
23948
+ let y1 = height;
23949
+ if (src) {
23950
+ x0 = clamp(Math.floor(src.x), 0, width);
23951
+ y0 = clamp(Math.floor(src.y), 0, height);
23952
+ x1 = clamp(Math.ceil(src.x + src.w), 0, width);
23953
+ y1 = clamp(Math.ceil(src.y + src.h), 0, height);
23954
+ if (x1 <= x0 || y1 <= y0) {
23955
+ return null;
23956
+ }
23957
+ }
23958
+ const pad = src && effect.kind === "blur" && effect.expandEdge ? Math.ceil(effect.radius) : 0;
23959
+ const cw = x1 - x0;
23960
+ const ch = y1 - y0;
23961
+ const w = cw + 2 * pad;
23962
+ const h = ch + 2 * pad;
23963
+ let region = rgba;
23964
+ if (w !== width || h !== height || x0 !== 0 || y0 !== 0) {
23965
+ region = new Uint8ClampedArray(w * h * 4);
23966
+ for (let y = 0; y < ch; y++) {
23967
+ const from = ((y0 + y) * width + x0) * 4;
23968
+ region.set(rgba.subarray(from, from + cw * 4), ((pad + y) * w + pad) * 4);
23969
+ }
23970
+ }
23971
+ const ox = x0 - pad;
23972
+ const oy = y0 - pad;
23973
+ const local = effect.kind === "redEyeCorrection" ? {
23974
+ kind: effect.kind,
23975
+ areas: effect.areas.map((a) => ({
23976
+ left: a.left - ox,
23977
+ top: a.top - oy,
23978
+ right: a.right - ox,
23979
+ bottom: a.bottom - oy
23980
+ }))
23981
+ } : effect;
23982
+ const out = applyImageEffect(region, w, h, local);
23983
+ return out ? { rgba: out, x: ox, y: oy, width: w, height: h, pad } : null;
23984
+ }
23941
23985
 
23942
23986
  // src/emf-plus-draw-image.ts
23943
23987
  function surfaceSize4(ctx) {
@@ -24074,7 +24118,7 @@ function drawNestedMetafile(rCtx, img, cached, source) {
24074
24118
  rCtx.deferredImages.push(...deferred);
24075
24119
  return true;
24076
24120
  }
24077
- function effectedBitmap(img, bitmap, effect) {
24121
+ function effectedDraw(img, bitmap, effect, draw, source) {
24078
24122
  let src = bitmap;
24079
24123
  if (!src && img.data) {
24080
24124
  const bmp = decodeBmpFile(img.data.slice(0));
@@ -24085,21 +24129,61 @@ function effectedBitmap(img, bitmap, effect) {
24085
24129
  if (!src) {
24086
24130
  return null;
24087
24131
  }
24088
- const rgba = applyImageEffect(src.rgba, src.width, src.height, effect);
24089
- return rgba ? { kind: "bitmap", width: src.width, height: src.height, rgba } : null;
24132
+ const rect = source && source.unit === UNIT_PIXEL && source.srcW > 0 && source.srcH > 0 && [source.srcX, source.srcY, source.srcW, source.srcH].every(Number.isFinite) ? source : null;
24133
+ const region = applyImageEffectToRect(
24134
+ src.rgba,
24135
+ src.width,
24136
+ src.height,
24137
+ effect,
24138
+ rect && { x: rect.srcX, y: rect.srcY, w: rect.srcW, h: rect.srcH }
24139
+ );
24140
+ if (!region) {
24141
+ return null;
24142
+ }
24143
+ const out = { kind: "bitmap", width: region.width, height: region.height, rgba: region.rgba };
24144
+ if (!rect) {
24145
+ return { bitmap: out, draw };
24146
+ }
24147
+ const grow = region.pad;
24148
+ const srcX = rect.srcX - grow - region.x;
24149
+ const srcY = rect.srcY - grow - region.y;
24150
+ const srcW = rect.srcW + 2 * grow;
24151
+ const srcH = rect.srcH + 2 * grow;
24152
+ const shift = [1, 0, 0, 1, region.x, region.y];
24153
+ const toWorld = mulMatrix(rect.toWorld(rect.srcX, rect.srcY, rect.srcW, rect.srcH), shift);
24154
+ return {
24155
+ bitmap: out,
24156
+ draw: {
24157
+ ...draw,
24158
+ // The Canvas fallback scales the whole effected bitmap.
24159
+ transform: mulMatrix(draw.transform, toWorld),
24160
+ dx: 0,
24161
+ dy: 0,
24162
+ dw: region.width,
24163
+ dh: region.height,
24164
+ resample: draw.resample && {
24165
+ ...draw.resample,
24166
+ srcX,
24167
+ srcY,
24168
+ srcW,
24169
+ srcH,
24170
+ toDevice: mulMatrix(draw.resample.toDevice, shift)
24171
+ }
24172
+ }
24173
+ };
24090
24174
  }
24091
24175
  function drawEmfPlusImageNow(rCtx, img, draw, source, effect = null) {
24092
24176
  const cached = img.cacheKey !== void 0 ? rCtx.imageCache?.get(img.cacheKey) : void 0;
24093
24177
  if (img.type === 2) {
24094
24178
  return drawNestedMetafile(rCtx, img, cached, source);
24095
24179
  }
24096
- let bitmap = cached && cached.kind === "bitmap" ? cached : null;
24097
- const effected = effect ? effectedBitmap(img, bitmap, effect) : null;
24180
+ const bitmap = cached && cached.kind === "bitmap" ? cached : null;
24181
+ const effected = effect ? effectedDraw(img, bitmap, effect, draw, source) : null;
24098
24182
  if (effected) {
24099
- bitmap = effected;
24183
+ return isSvgContext(rCtx.ctx) ? paintBitmapSvg(rCtx.ctx, img, effected.bitmap, effected.draw, true) : paintBitmapRaster(rCtx.ctx, effected.bitmap, effected.draw);
24100
24184
  }
24101
24185
  if (isSvgContext(rCtx.ctx)) {
24102
- return paintBitmapSvg(rCtx.ctx, img, bitmap, draw, effected !== null);
24186
+ return paintBitmapSvg(rCtx.ctx, img, bitmap, draw);
24103
24187
  }
24104
24188
  return bitmap ? paintBitmapRaster(rCtx.ctx, bitmap, draw) : false;
24105
24189
  }
@@ -24391,28 +24475,32 @@ function strokePlusGeometry(rCtx, pen, buildPath, points, closedFigure = false)
24391
24475
  if (mode !== "canvas" && size && strokeGdiplus(rCtx, pen, nib, sampler, buildPath, device, size, mode, closedFigure)) {
24392
24476
  return;
24393
24477
  }
24394
- const miter = Math.max(1, ctx.miterLimit || 10);
24395
- const margin = pen.width * scale * miter / 2 + 2;
24396
- const box = size ? deviceBounds(points, device, size, margin) : null;
24478
+ const outline = hasCustomCap(pen) ? penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure) : null;
24479
+ let box = null;
24480
+ if (size && outline) {
24481
+ box = deviceBounds(outline.flat(), device, size, 2);
24482
+ } else if (size) {
24483
+ const miter = Math.max(1, ctx.miterLimit || 10);
24484
+ const margin = pen.width * scale * miter / 2 + 2;
24485
+ box = deviceBounds(points, device, size, margin);
24486
+ }
24397
24487
  if (size && !box) {
24398
24488
  return;
24399
24489
  }
24400
- if (box && paintBrushThroughMask(
24401
- rCtx,
24402
- sampler,
24403
- box,
24404
- (c) => {
24405
- applyPlusPenStyle(c, pen, widthScale);
24406
- stroke(c);
24407
- c.beginPath();
24408
- buildPath(c);
24409
- if (nib) {
24410
- c.transform(nib[0], nib[1], nib[2], nib[3], 0, 0);
24411
- }
24412
- },
24413
- mode,
24414
- (c, x, y) => c.isPointInStroke(x, y) && (!inset || c.isPointInPath(x, y))
24415
- )) {
24490
+ const drawMask = outline ? (c) => {
24491
+ traceOutline(c, outline);
24492
+ c.fill("nonzero");
24493
+ } : (c) => {
24494
+ applyPlusPenStyle(c, pen, widthScale);
24495
+ stroke(c);
24496
+ c.beginPath();
24497
+ buildPath(c);
24498
+ if (nib) {
24499
+ c.transform(nib[0], nib[1], nib[2], nib[3], 0, 0);
24500
+ }
24501
+ };
24502
+ const hitTest = outline ? (c, x, y) => c.isPointInPath(x, y, "nonzero") : (c, x, y) => c.isPointInStroke(x, y) && (!inset || c.isPointInPath(x, y));
24503
+ if (box && paintBrushThroughMask(rCtx, sampler, box, drawMask, mode, hitTest)) {
24416
24504
  return;
24417
24505
  }
24418
24506
  }
@@ -24577,18 +24665,18 @@ function customCapOutline(figures, width, devicePen, pen) {
24577
24665
  outline.push(...geo.polygons);
24578
24666
  return outline;
24579
24667
  }
24580
- function fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure) {
24668
+ function penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure) {
24581
24669
  const device = plusWorldMatrix(rCtx);
24582
24670
  const frame2 = penFrame(device, nib);
24583
24671
  const deviceInv = invertAffine(device);
24584
24672
  if (!frame2 || !deviceInv || !(frame2.unit > 0)) {
24585
- return false;
24673
+ return null;
24586
24674
  }
24587
24675
  let figures;
24588
24676
  try {
24589
24677
  figures = recordDeviceFigures(buildPath, frame2.record);
24590
24678
  } catch {
24591
- return false;
24679
+ return null;
24592
24680
  }
24593
24681
  if (closedFigure) {
24594
24682
  figures = figures.map((f) => ({ ...f, closed: true }));
@@ -24596,25 +24684,33 @@ function fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure) {
24596
24684
  const width = (pen.width || 1) * frame2.unit;
24597
24685
  const devicePen = framePen(pen, frame2.unit);
24598
24686
  const outline = hasCustomCap(pen) ? customCapOutline(figures, width, devicePen, pen) : widenFigures(figures, devicePen);
24599
- const toWorld = mulMatrix(deviceInv, frame2.toDevice);
24687
+ const m = mulMatrix(deviceInv, frame2.toDevice);
24688
+ return outline.map((poly) => poly.map((p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] })));
24689
+ }
24690
+ function traceOutline(c, outline) {
24691
+ c.beginPath();
24692
+ for (const poly of outline) {
24693
+ poly.forEach((p, i) => {
24694
+ if (i === 0) {
24695
+ c.moveTo(p.x, p.y);
24696
+ } else {
24697
+ c.lineTo(p.x, p.y);
24698
+ }
24699
+ });
24700
+ c.closePath();
24701
+ }
24702
+ }
24703
+ function fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure) {
24704
+ const outline = penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure);
24705
+ if (!outline) {
24706
+ return false;
24707
+ }
24600
24708
  const { ctx } = rCtx;
24601
24709
  ctx.save();
24602
24710
  try {
24603
24711
  ctx.fillStyle = paint;
24604
24712
  applyPlusWorldTransform(rCtx);
24605
- ctx.beginPath();
24606
- for (const poly of outline) {
24607
- poly.forEach((p, i) => {
24608
- const x = toWorld[0] * p.x + toWorld[2] * p.y + toWorld[4];
24609
- const y = toWorld[1] * p.x + toWorld[3] * p.y + toWorld[5];
24610
- if (i === 0) {
24611
- ctx.moveTo(x, y);
24612
- } else {
24613
- ctx.lineTo(x, y);
24614
- }
24615
- });
24616
- ctx.closePath();
24617
- }
24713
+ traceOutline(ctx, outline);
24618
24714
  ctx.fill("nonzero");
24619
24715
  } finally {
24620
24716
  ctx.restore();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emf-converter",
3
- "version": "4.2.0",
3
+ "version": "4.2.1",
4
4
  "description": "Convert EMF/WMF metafile binaries to PNG data URLs or SVG (markup, data URL, React/JSX)",
5
5
  "keywords": [
6
6
  "canvas",
@@ -45,6 +45,7 @@
45
45
  "test": "vitest run",
46
46
  "test:watch": "vitest",
47
47
  "pack": "bun run build && bun pm pack",
48
+ "docs:typecheck": "vue-tsc --noEmit -p docs/tsconfig.json",
48
49
  "docs:dev": "vitepress dev docs",
49
50
  "docs:build": "vitepress build docs",
50
51
  "docs:preview": "vitepress preview docs"
@@ -59,7 +60,8 @@
59
60
  "tsup": "^8.5.1",
60
61
  "typescript": "^6.0.3",
61
62
  "vitepress": "^1.6.4",
62
- "vitest": "^5.0.0"
63
+ "vitest": "^5.0.0",
64
+ "vue-tsc": "^3.3.11"
63
65
  },
64
66
  "peerDependencies": {
65
67
  "@napi-rs/canvas": "^1.0.9"