emf-converter 4.2.0 → 4.3.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 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**: The `HALFTONE` stretch mode resamples with an area-averaging box filter. Integer enlargements match Windows exactly; reductions do not, because Windows' halftone engine uses a narrower kernel and its own colour mapping (1.05% of pixels differ on the fixture). `EMR_SETCOLORADJUSTMENT` is applied to the source of `HALFTONE` `StretchBlt` and `StretchDIBits` calls, as in GDI. Windows does not publish its adjustment formulas, so gamma, reference black/white, contrast, brightness, colourfulness, red-green tint, negative and log filter use documented approximations, and Windows' dithering of the adjusted colours is not reproduced (6.6% of pixels differ by more than 24 on the fixture). The illuminant is ignored. 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);
@@ -19982,6 +19982,97 @@ function handleEmfGdiShapeRecord(rCtx, recType, dataOff, recSize) {
19982
19982
  }
19983
19983
  }
19984
19984
 
19985
+ // src/emf-gdi-color-adjust.ts
19986
+ var CA_NEGATIVE = 1;
19987
+ var CA_LOG_FILTER = 2;
19988
+ var COLOR_ADJUSTMENT_SIZE = 24;
19989
+ function readColorAdjustment(view, off) {
19990
+ const ca = {
19991
+ flags: view.getUint16(off + 2, true),
19992
+ illuminant: view.getUint16(off + 4, true),
19993
+ redGamma: view.getUint16(off + 6, true),
19994
+ greenGamma: view.getUint16(off + 8, true),
19995
+ blueGamma: view.getUint16(off + 10, true),
19996
+ referenceBlack: view.getUint16(off + 12, true),
19997
+ referenceWhite: view.getUint16(off + 14, true),
19998
+ contrast: view.getInt16(off + 16, true),
19999
+ brightness: view.getInt16(off + 18, true),
20000
+ colorfulness: view.getInt16(off + 20, true),
20001
+ redGreenTint: view.getInt16(off + 22, true)
20002
+ };
20003
+ const within = (v, lo, hi) => v >= lo && v <= hi;
20004
+ const valid = (ca.flags & -4) === 0 && within(ca.illuminant, 0, 8) && within(ca.redGamma, 2500, 65e3) && within(ca.greenGamma, 2500, 65e3) && within(ca.blueGamma, 2500, 65e3) && within(ca.referenceBlack, 0, 4e3) && within(ca.referenceWhite, 6e3, 1e4) && within(ca.contrast, -100, 100) && within(ca.brightness, -100, 100) && within(ca.colorfulness, -100, 100) && within(ca.redGreenTint, -100, 100);
20005
+ return valid ? ca : null;
20006
+ }
20007
+ function isIdentityColorAdjustment(ca) {
20008
+ return !ca || ca.flags === 0 && ca.redGamma === 1e4 && ca.greenGamma === 1e4 && ca.blueGamma === 1e4 && ca.referenceBlack === 0 && ca.referenceWhite === 1e4 && ca.contrast === 0 && ca.brightness === 0 && ca.colorfulness === 0 && ca.redGreenTint === 0;
20009
+ }
20010
+ function colorfulnessScale(c) {
20011
+ return c > 0 ? 1 + 2 * c / 100 : 1 + c / 100;
20012
+ }
20013
+ var clamp012 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
20014
+ function colorAdjustmentMapper(ca) {
20015
+ const black = ca.referenceBlack / 1e4;
20016
+ const span = Math.max(1e-6, ca.referenceWhite / 1e4 - black);
20017
+ const gammas = [ca.redGamma / 1e4, ca.greenGamma / 1e4, ca.blueGamma / 1e4];
20018
+ const curves = gammas.map((gamma) => {
20019
+ const table = new Float64Array(256);
20020
+ for (let i = 0; i < 256; i++) {
20021
+ let v = clamp012((i / 255 - black) / span);
20022
+ v = Math.pow(v, gamma);
20023
+ table[i] = ca.flags & CA_NEGATIVE ? 1 - v : v;
20024
+ }
20025
+ return table;
20026
+ });
20027
+ const chroma = colorfulnessScale(ca.colorfulness);
20028
+ const contrast = 1 + ca.contrast / 100;
20029
+ const brightness = 0.8 * ca.brightness / 100;
20030
+ const tint = 0.3 * ca.redGreenTint / 100;
20031
+ const log = (ca.flags & CA_LOG_FILTER) !== 0;
20032
+ const out = (v) => {
20033
+ v = clamp012(v);
20034
+ if (log) {
20035
+ v = Math.log10(1 + 9 * v);
20036
+ }
20037
+ return Math.round(v * 255);
20038
+ };
20039
+ return (rgb2) => {
20040
+ const r = curves[0][rgb2 >> 16 & 255];
20041
+ const g = curves[1][rgb2 >> 8 & 255];
20042
+ const b = curves[2][rgb2 & 255];
20043
+ let y = 0.299 * r + 0.587 * g + 0.114 * b;
20044
+ let cr = (r - y) * chroma;
20045
+ const cb = (b - y) * chroma;
20046
+ y = (y - 0.5) * contrast + 0.5 + brightness;
20047
+ cr += tint;
20048
+ const r2 = y + cr;
20049
+ const b2 = y + cb;
20050
+ const g2 = (y - 0.299 * r2 - 0.114 * b2) / 0.587;
20051
+ return out(r2) << 16 | out(g2) << 8 | out(b2);
20052
+ };
20053
+ }
20054
+ function applyColorAdjustment(pixels, ca) {
20055
+ if (!ca || isIdentityColorAdjustment(ca)) {
20056
+ return;
20057
+ }
20058
+ const map = colorAdjustmentMapper(ca);
20059
+ const d = pixels.data;
20060
+ const cache = /* @__PURE__ */ new Map();
20061
+ for (let i = 0; i + 3 < d.length; i += 4) {
20062
+ const rgb2 = d[i] << 16 | d[i + 1] << 8 | d[i + 2];
20063
+ let v = cache.get(rgb2);
20064
+ if (v === void 0) {
20065
+ v = map(rgb2);
20066
+ if (cache.size < 65536) {
20067
+ cache.set(rgb2, v);
20068
+ }
20069
+ }
20070
+ d[i] = v >> 16 & 255;
20071
+ d[i + 1] = v >> 8 & 255;
20072
+ d[i + 2] = v & 255;
20073
+ }
20074
+ }
20075
+
19985
20076
  // src/emf-gdi-stretch.ts
19986
20077
  var BLACKONWHITE = 1;
19987
20078
  var WHITEONBLACK = 2;
@@ -20054,6 +20145,66 @@ function stretchGdi(src, sx, sy, sw, sh, dw, dh, mode) {
20054
20145
  }
20055
20146
  return { width: W, height: H, data };
20056
20147
  }
20148
+ function axisBoxWeights(srcStart, srcLen, dstLen, reverse) {
20149
+ const out = [];
20150
+ if (dstLen <= 0 || srcLen <= 0) {
20151
+ return out;
20152
+ }
20153
+ const at = (k) => reverse ? srcStart + srcLen - 1 - k : srcStart + k;
20154
+ for (let i = 0; i < dstLen; i++) {
20155
+ const a = i * srcLen;
20156
+ const b = (i + 1) * srcLen;
20157
+ const taps = [];
20158
+ for (let k = Math.floor(a / dstLen); k * dstLen < b; k++) {
20159
+ const overlap = Math.min(b, (k + 1) * dstLen) - Math.max(a, k * dstLen);
20160
+ if (overlap > 0) {
20161
+ taps.push([at(k), overlap / srcLen]);
20162
+ }
20163
+ }
20164
+ out.push(taps);
20165
+ }
20166
+ return out;
20167
+ }
20168
+ function stretchHalftone(src, sx, sy, sw, sh, dw, dh) {
20169
+ const W = Math.max(0, Math.round(Math.abs(dw)));
20170
+ const H = Math.max(0, Math.round(Math.abs(dh)));
20171
+ const flipX = dw < 0 !== sw < 0;
20172
+ const flipY = dh < 0 !== sh < 0;
20173
+ const cols = axisBoxWeights(Math.round(Math.min(sx, sx + sw)), Math.round(Math.abs(sw)), W, flipX);
20174
+ const rows = axisBoxWeights(Math.round(Math.min(sy, sy + sh)), Math.round(Math.abs(sh)), H, flipY);
20175
+ const data = new Uint8ClampedArray(W * H * 4);
20176
+ const s = src.data;
20177
+ for (let y = 0; y < H; y++) {
20178
+ const ys = rows[y] ?? [];
20179
+ for (let x = 0; x < W; x++) {
20180
+ const xs = cols[x] ?? [];
20181
+ let r = 0;
20182
+ let g = 0;
20183
+ let b = 0;
20184
+ for (const [yy, wy] of ys) {
20185
+ if (yy < 0 || yy >= src.height) {
20186
+ continue;
20187
+ }
20188
+ for (const [xx, wx] of xs) {
20189
+ if (xx < 0 || xx >= src.width) {
20190
+ continue;
20191
+ }
20192
+ const w = wx * wy;
20193
+ const i = (yy * src.width + xx) * 4;
20194
+ r += s[i] * w;
20195
+ g += s[i + 1] * w;
20196
+ b += s[i + 2] * w;
20197
+ }
20198
+ }
20199
+ const o = (y * W + x) * 4;
20200
+ data[o] = Math.floor(r + 1e-6);
20201
+ data[o + 1] = Math.floor(g + 1e-6);
20202
+ data[o + 2] = Math.floor(b + 1e-6);
20203
+ data[o + 3] = 255;
20204
+ }
20205
+ }
20206
+ return { width: W, height: H, data };
20207
+ }
20057
20208
 
20058
20209
  // src/emf-gdi-draw-bitmap.ts
20059
20210
  function patternOperand(rCtx, brush) {
@@ -20077,46 +20228,18 @@ function patternOperand(rCtx, brush) {
20077
20228
  function drawSourceMapped(target, decoded, req, offsetX, offsetY, mode) {
20078
20229
  const dLeft = Math.min(req.dx, req.dx + req.dw) - offsetX;
20079
20230
  const dTop = Math.min(req.dy, req.dy + req.dh) - offsetY;
20080
- if (mode !== HALFTONE) {
20081
- const px = stretchGdi(decoded.pixels, req.sx, req.sy, req.sw, req.sh, req.dw, req.dh, mode);
20082
- const tile = createTempCanvas(px.width, px.height);
20083
- if (!tile) {
20084
- return;
20085
- }
20086
- canvasPutImageData(tile.ctx, createImageDataCompat(px.data, px.width, px.height), 0, 0);
20087
- target.save();
20088
- target.globalCompositeOperation = "source-over";
20089
- canvasDrawImage(target, tile.canvas, Math.round(dLeft), Math.round(dTop), px.width, px.height);
20090
- target.restore();
20231
+ const px = mode === HALFTONE ? stretchHalftone(decoded.pixels, req.sx, req.sy, req.sw, req.sh, req.dw, req.dh) : stretchGdi(decoded.pixels, req.sx, req.sy, req.sw, req.sh, req.dw, req.dh, mode);
20232
+ if (px.width === 0 || px.height === 0) {
20091
20233
  return;
20092
20234
  }
20093
- const flipX = req.dw < 0 !== req.sw < 0;
20094
- const flipY = req.dh < 0 !== req.sh < 0;
20095
- const adw = Math.abs(req.dw);
20096
- const adh = Math.abs(req.dh);
20235
+ const tile = createTempCanvas(px.width, px.height);
20236
+ if (!tile) {
20237
+ return;
20238
+ }
20239
+ canvasPutImageData(tile.ctx, createImageDataCompat(px.data, px.width, px.height), 0, 0);
20097
20240
  target.save();
20098
- target.imageSmoothingEnabled = true;
20099
- target.transform(
20100
- flipX ? -1 : 1,
20101
- 0,
20102
- 0,
20103
- flipY ? -1 : 1,
20104
- flipX ? dLeft * 2 + adw : 0,
20105
- flipY ? dTop * 2 + adh : 0
20106
- );
20107
- const draw = target.drawImage;
20108
- draw.call(
20109
- target,
20110
- decoded.canvas,
20111
- Math.min(req.sx, req.sx + req.sw),
20112
- Math.min(req.sy, req.sy + req.sh),
20113
- Math.abs(req.sw),
20114
- Math.abs(req.sh),
20115
- dLeft,
20116
- dTop,
20117
- adw,
20118
- adh
20119
- );
20241
+ target.globalCompositeOperation = "source-over";
20242
+ canvasDrawImage(target, tile.canvas, Math.round(dLeft), Math.round(dTop), px.width, px.height);
20120
20243
  target.restore();
20121
20244
  }
20122
20245
  function decodeSource(rCtx, req) {
@@ -20133,17 +20256,15 @@ function decodeSource(rCtx, req) {
20133
20256
  if (!image) {
20134
20257
  return null;
20135
20258
  }
20136
- const temp = createTempCanvas(image.width, image.height);
20137
- if (!temp) {
20138
- return null;
20259
+ if (req.stretch && rCtx.state.stretchBltMode === HALFTONE) {
20260
+ applyColorAdjustment(image, rCtx.state.colorAdjustment);
20139
20261
  }
20140
- canvasPutImageData(temp.ctx, image, 0, 0);
20141
20262
  let { sy } = req;
20142
20263
  if (req.dibOrigin === "bottom-left" && rCtx.view.getInt32(req.source.bmi + 8, true) > 0) {
20143
20264
  sy = image.height - sy - req.sh;
20144
20265
  }
20145
20266
  return {
20146
- decoded: { pixels: image, canvas: temp.canvas },
20267
+ decoded: { pixels: image },
20147
20268
  req: { ...req, sy }
20148
20269
  };
20149
20270
  }
@@ -20256,7 +20377,7 @@ function nudgedFloor(num, den, dx, dy) {
20256
20377
  const q = Math.floor(num / den);
20257
20378
  return num === q * den && nudgedSign(0, dx, dy) < 0 ? q - 1 : q;
20258
20379
  }
20259
- function executeRotatedBlit(rCtx, logDx, logDy, logDw, logDh, rop, source, sx, sy, sw, sh, dibOrigin) {
20380
+ function executeRotatedBlit(rCtx, logDx, logDy, logDw, logDh, rop, source, sx, sy, sw, sh, dibOrigin, stretch) {
20260
20381
  const { ctx } = rCtx;
20261
20382
  const index = rop3Index(rop);
20262
20383
  if (index === 170) {
@@ -20292,7 +20413,8 @@ function executeRotatedBlit(rCtx, logDx, logDy, logDw, logDh, rop, source, sx, s
20292
20413
  sy,
20293
20414
  sw,
20294
20415
  sh,
20295
- dibOrigin
20416
+ dibOrigin,
20417
+ stretch
20296
20418
  });
20297
20419
  if (!decoded) {
20298
20420
  return;
@@ -20460,7 +20582,8 @@ function handleBlt(rCtx, offset, dataOff, recSize, stretch) {
20460
20582
  srcRect.sy,
20461
20583
  srcRect.sw,
20462
20584
  srcRect.sh,
20463
- "top-left"
20585
+ "top-left",
20586
+ stretch
20464
20587
  );
20465
20588
  return true;
20466
20589
  }
@@ -20472,7 +20595,8 @@ function handleBlt(rCtx, offset, dataOff, recSize, stretch) {
20472
20595
  dh: gmh(rCtx, cyDest),
20473
20596
  source,
20474
20597
  ...srcRect,
20475
- dibOrigin: "top-left"
20598
+ dibOrigin: "top-left",
20599
+ stretch
20476
20600
  });
20477
20601
  return true;
20478
20602
  }
@@ -20499,7 +20623,7 @@ function handleStretchDibits(rCtx, offset, dataOff, recSize) {
20499
20623
  const sw = view.getInt32(dataOff + 32, true);
20500
20624
  const sh = view.getInt32(dataOff + 36, true);
20501
20625
  if (hasWorldRotation(rCtx)) {
20502
- executeRotatedBlit(rCtx, xDest, yDest, cxDest, cyDest, rop, source, sx, sy, sw, sh, "bottom-left");
20626
+ executeRotatedBlit(rCtx, xDest, yDest, cxDest, cyDest, rop, source, sx, sy, sw, sh, "bottom-left", true);
20503
20627
  return true;
20504
20628
  }
20505
20629
  executeBlit(rCtx, {
@@ -20513,7 +20637,8 @@ function handleStretchDibits(rCtx, offset, dataOff, recSize) {
20513
20637
  sy,
20514
20638
  sw,
20515
20639
  sh,
20516
- dibOrigin: "bottom-left"
20640
+ dibOrigin: "bottom-left",
20641
+ stretch: true
20517
20642
  });
20518
20643
  return true;
20519
20644
  }
@@ -23029,20 +23154,9 @@ function handleEmfGdiMiscRecord(rCtx, recType, dataOff, recSize) {
23029
23154
  return true;
23030
23155
  }
23031
23156
  case EMR_SETCOLORADJUSTMENT: {
23032
- if (recSize >= 32) {
23033
- state.colorAdjustment = {
23034
- flags: view.getUint16(dataOff + 2, true),
23035
- illuminant: view.getUint16(dataOff + 4, true),
23036
- redGamma: view.getUint16(dataOff + 6, true),
23037
- greenGamma: view.getUint16(dataOff + 8, true),
23038
- blueGamma: view.getUint16(dataOff + 10, true),
23039
- referenceBlack: view.getUint16(dataOff + 12, true),
23040
- referenceWhite: view.getUint16(dataOff + 14, true),
23041
- contrast: view.getInt16(dataOff + 16, true),
23042
- brightness: view.getInt16(dataOff + 18, true),
23043
- colorfulness: view.getInt16(dataOff + 20, true),
23044
- redGreenTint: view.getInt16(dataOff + 22, true)
23045
- };
23157
+ const ca = recSize >= 8 + COLOR_ADJUSTMENT_SIZE ? readColorAdjustment(view, dataOff) : null;
23158
+ if (ca) {
23159
+ state.colorAdjustment = ca;
23046
23160
  }
23047
23161
  return true;
23048
23162
  }
@@ -23940,6 +24054,50 @@ function applyImageEffect(rgba, width, height, effect) {
23940
24054
  return applyTint(rgba, effect.hue, effect.amount);
23941
24055
  }
23942
24056
  }
24057
+ function applyImageEffectToRect(rgba, width, height, effect, src) {
24058
+ if (width <= 0 || height <= 0 || rgba.length !== width * height * 4) {
24059
+ return null;
24060
+ }
24061
+ let x0 = 0;
24062
+ let y0 = 0;
24063
+ let x1 = width;
24064
+ let y1 = height;
24065
+ if (src) {
24066
+ x0 = clamp(Math.floor(src.x), 0, width);
24067
+ y0 = clamp(Math.floor(src.y), 0, height);
24068
+ x1 = clamp(Math.ceil(src.x + src.w), 0, width);
24069
+ y1 = clamp(Math.ceil(src.y + src.h), 0, height);
24070
+ if (x1 <= x0 || y1 <= y0) {
24071
+ return null;
24072
+ }
24073
+ }
24074
+ const pad = src && effect.kind === "blur" && effect.expandEdge ? Math.ceil(effect.radius) : 0;
24075
+ const cw = x1 - x0;
24076
+ const ch = y1 - y0;
24077
+ const w = cw + 2 * pad;
24078
+ const h = ch + 2 * pad;
24079
+ let region = rgba;
24080
+ if (w !== width || h !== height || x0 !== 0 || y0 !== 0) {
24081
+ region = new Uint8ClampedArray(w * h * 4);
24082
+ for (let y = 0; y < ch; y++) {
24083
+ const from = ((y0 + y) * width + x0) * 4;
24084
+ region.set(rgba.subarray(from, from + cw * 4), ((pad + y) * w + pad) * 4);
24085
+ }
24086
+ }
24087
+ const ox = x0 - pad;
24088
+ const oy = y0 - pad;
24089
+ const local = effect.kind === "redEyeCorrection" ? {
24090
+ kind: effect.kind,
24091
+ areas: effect.areas.map((a) => ({
24092
+ left: a.left - ox,
24093
+ top: a.top - oy,
24094
+ right: a.right - ox,
24095
+ bottom: a.bottom - oy
24096
+ }))
24097
+ } : effect;
24098
+ const out = applyImageEffect(region, w, h, local);
24099
+ return out ? { rgba: out, x: ox, y: oy, width: w, height: h, pad } : null;
24100
+ }
23943
24101
 
23944
24102
  // src/emf-plus-draw-image.ts
23945
24103
  function surfaceSize4(ctx) {
@@ -24076,7 +24234,7 @@ function drawNestedMetafile(rCtx, img, cached, source) {
24076
24234
  rCtx.deferredImages.push(...deferred);
24077
24235
  return true;
24078
24236
  }
24079
- function effectedBitmap(img, bitmap, effect) {
24237
+ function effectedDraw(img, bitmap, effect, draw, source) {
24080
24238
  let src = bitmap;
24081
24239
  if (!src && img.data) {
24082
24240
  const bmp = decodeBmpFile(img.data.slice(0));
@@ -24087,21 +24245,61 @@ function effectedBitmap(img, bitmap, effect) {
24087
24245
  if (!src) {
24088
24246
  return null;
24089
24247
  }
24090
- const rgba = applyImageEffect(src.rgba, src.width, src.height, effect);
24091
- return rgba ? { kind: "bitmap", width: src.width, height: src.height, rgba } : null;
24248
+ 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;
24249
+ const region = applyImageEffectToRect(
24250
+ src.rgba,
24251
+ src.width,
24252
+ src.height,
24253
+ effect,
24254
+ rect && { x: rect.srcX, y: rect.srcY, w: rect.srcW, h: rect.srcH }
24255
+ );
24256
+ if (!region) {
24257
+ return null;
24258
+ }
24259
+ const out = { kind: "bitmap", width: region.width, height: region.height, rgba: region.rgba };
24260
+ if (!rect) {
24261
+ return { bitmap: out, draw };
24262
+ }
24263
+ const grow = region.pad;
24264
+ const srcX = rect.srcX - grow - region.x;
24265
+ const srcY = rect.srcY - grow - region.y;
24266
+ const srcW = rect.srcW + 2 * grow;
24267
+ const srcH = rect.srcH + 2 * grow;
24268
+ const shift = [1, 0, 0, 1, region.x, region.y];
24269
+ const toWorld = mulMatrix(rect.toWorld(rect.srcX, rect.srcY, rect.srcW, rect.srcH), shift);
24270
+ return {
24271
+ bitmap: out,
24272
+ draw: {
24273
+ ...draw,
24274
+ // The Canvas fallback scales the whole effected bitmap.
24275
+ transform: mulMatrix(draw.transform, toWorld),
24276
+ dx: 0,
24277
+ dy: 0,
24278
+ dw: region.width,
24279
+ dh: region.height,
24280
+ resample: draw.resample && {
24281
+ ...draw.resample,
24282
+ srcX,
24283
+ srcY,
24284
+ srcW,
24285
+ srcH,
24286
+ toDevice: mulMatrix(draw.resample.toDevice, shift)
24287
+ }
24288
+ }
24289
+ };
24092
24290
  }
24093
24291
  function drawEmfPlusImageNow(rCtx, img, draw, source, effect = null) {
24094
24292
  const cached = img.cacheKey !== void 0 ? rCtx.imageCache?.get(img.cacheKey) : void 0;
24095
24293
  if (img.type === 2) {
24096
24294
  return drawNestedMetafile(rCtx, img, cached, source);
24097
24295
  }
24098
- let bitmap = cached && cached.kind === "bitmap" ? cached : null;
24099
- const effected = effect ? effectedBitmap(img, bitmap, effect) : null;
24296
+ const bitmap = cached && cached.kind === "bitmap" ? cached : null;
24297
+ const effected = effect ? effectedDraw(img, bitmap, effect, draw, source) : null;
24100
24298
  if (effected) {
24101
- bitmap = effected;
24299
+ return isSvgContext(rCtx.ctx) ? paintBitmapSvg(rCtx.ctx, img, effected.bitmap, effected.draw, true) : paintBitmapRaster(rCtx.ctx, effected.bitmap, effected.draw);
24102
24300
  }
24103
24301
  if (isSvgContext(rCtx.ctx)) {
24104
- return paintBitmapSvg(rCtx.ctx, img, bitmap, draw, effected !== null);
24302
+ return paintBitmapSvg(rCtx.ctx, img, bitmap, draw);
24105
24303
  }
24106
24304
  return bitmap ? paintBitmapRaster(rCtx.ctx, bitmap, draw) : false;
24107
24305
  }
@@ -24393,28 +24591,32 @@ function strokePlusGeometry(rCtx, pen, buildPath, points, closedFigure = false)
24393
24591
  if (mode !== "canvas" && size && strokeGdiplus(rCtx, pen, nib, sampler, buildPath, device, size, mode, closedFigure)) {
24394
24592
  return;
24395
24593
  }
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;
24594
+ const outline = hasCustomCap(pen) ? penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure) : null;
24595
+ let box = null;
24596
+ if (size && outline) {
24597
+ box = deviceBounds(outline.flat(), device, size, 2);
24598
+ } else if (size) {
24599
+ const miter = Math.max(1, ctx.miterLimit || 10);
24600
+ const margin = pen.width * scale * miter / 2 + 2;
24601
+ box = deviceBounds(points, device, size, margin);
24602
+ }
24399
24603
  if (size && !box) {
24400
24604
  return;
24401
24605
  }
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
- )) {
24606
+ const drawMask = outline ? (c) => {
24607
+ traceOutline(c, outline);
24608
+ c.fill("nonzero");
24609
+ } : (c) => {
24610
+ applyPlusPenStyle(c, pen, widthScale);
24611
+ stroke(c);
24612
+ c.beginPath();
24613
+ buildPath(c);
24614
+ if (nib) {
24615
+ c.transform(nib[0], nib[1], nib[2], nib[3], 0, 0);
24616
+ }
24617
+ };
24618
+ const hitTest = outline ? (c, x, y) => c.isPointInPath(x, y, "nonzero") : (c, x, y) => c.isPointInStroke(x, y) && (!inset || c.isPointInPath(x, y));
24619
+ if (box && paintBrushThroughMask(rCtx, sampler, box, drawMask, mode, hitTest)) {
24418
24620
  return;
24419
24621
  }
24420
24622
  }
@@ -24579,18 +24781,18 @@ function customCapOutline(figures, width, devicePen, pen) {
24579
24781
  outline.push(...geo.polygons);
24580
24782
  return outline;
24581
24783
  }
24582
- function fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure) {
24784
+ function penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure) {
24583
24785
  const device = plusWorldMatrix(rCtx);
24584
24786
  const frame2 = penFrame(device, nib);
24585
24787
  const deviceInv = invertAffine(device);
24586
24788
  if (!frame2 || !deviceInv || !(frame2.unit > 0)) {
24587
- return false;
24789
+ return null;
24588
24790
  }
24589
24791
  let figures;
24590
24792
  try {
24591
24793
  figures = recordDeviceFigures(buildPath, frame2.record);
24592
24794
  } catch {
24593
- return false;
24795
+ return null;
24594
24796
  }
24595
24797
  if (closedFigure) {
24596
24798
  figures = figures.map((f) => ({ ...f, closed: true }));
@@ -24598,25 +24800,33 @@ function fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure) {
24598
24800
  const width = (pen.width || 1) * frame2.unit;
24599
24801
  const devicePen = framePen(pen, frame2.unit);
24600
24802
  const outline = hasCustomCap(pen) ? customCapOutline(figures, width, devicePen, pen) : widenFigures(figures, devicePen);
24601
- const toWorld = mulMatrix(deviceInv, frame2.toDevice);
24803
+ const m = mulMatrix(deviceInv, frame2.toDevice);
24804
+ 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] })));
24805
+ }
24806
+ function traceOutline(c, outline) {
24807
+ c.beginPath();
24808
+ for (const poly of outline) {
24809
+ poly.forEach((p, i) => {
24810
+ if (i === 0) {
24811
+ c.moveTo(p.x, p.y);
24812
+ } else {
24813
+ c.lineTo(p.x, p.y);
24814
+ }
24815
+ });
24816
+ c.closePath();
24817
+ }
24818
+ }
24819
+ function fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure) {
24820
+ const outline = penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure);
24821
+ if (!outline) {
24822
+ return false;
24823
+ }
24602
24824
  const { ctx } = rCtx;
24603
24825
  ctx.save();
24604
24826
  try {
24605
24827
  ctx.fillStyle = paint;
24606
24828
  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
- }
24829
+ traceOutline(ctx, outline);
24620
24830
  ctx.fill("nonzero");
24621
24831
  } finally {
24622
24832
  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);
@@ -19980,6 +19980,97 @@ function handleEmfGdiShapeRecord(rCtx, recType, dataOff, recSize) {
19980
19980
  }
19981
19981
  }
19982
19982
 
19983
+ // src/emf-gdi-color-adjust.ts
19984
+ var CA_NEGATIVE = 1;
19985
+ var CA_LOG_FILTER = 2;
19986
+ var COLOR_ADJUSTMENT_SIZE = 24;
19987
+ function readColorAdjustment(view, off) {
19988
+ const ca = {
19989
+ flags: view.getUint16(off + 2, true),
19990
+ illuminant: view.getUint16(off + 4, true),
19991
+ redGamma: view.getUint16(off + 6, true),
19992
+ greenGamma: view.getUint16(off + 8, true),
19993
+ blueGamma: view.getUint16(off + 10, true),
19994
+ referenceBlack: view.getUint16(off + 12, true),
19995
+ referenceWhite: view.getUint16(off + 14, true),
19996
+ contrast: view.getInt16(off + 16, true),
19997
+ brightness: view.getInt16(off + 18, true),
19998
+ colorfulness: view.getInt16(off + 20, true),
19999
+ redGreenTint: view.getInt16(off + 22, true)
20000
+ };
20001
+ const within = (v, lo, hi) => v >= lo && v <= hi;
20002
+ const valid = (ca.flags & -4) === 0 && within(ca.illuminant, 0, 8) && within(ca.redGamma, 2500, 65e3) && within(ca.greenGamma, 2500, 65e3) && within(ca.blueGamma, 2500, 65e3) && within(ca.referenceBlack, 0, 4e3) && within(ca.referenceWhite, 6e3, 1e4) && within(ca.contrast, -100, 100) && within(ca.brightness, -100, 100) && within(ca.colorfulness, -100, 100) && within(ca.redGreenTint, -100, 100);
20003
+ return valid ? ca : null;
20004
+ }
20005
+ function isIdentityColorAdjustment(ca) {
20006
+ return !ca || ca.flags === 0 && ca.redGamma === 1e4 && ca.greenGamma === 1e4 && ca.blueGamma === 1e4 && ca.referenceBlack === 0 && ca.referenceWhite === 1e4 && ca.contrast === 0 && ca.brightness === 0 && ca.colorfulness === 0 && ca.redGreenTint === 0;
20007
+ }
20008
+ function colorfulnessScale(c) {
20009
+ return c > 0 ? 1 + 2 * c / 100 : 1 + c / 100;
20010
+ }
20011
+ var clamp012 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
20012
+ function colorAdjustmentMapper(ca) {
20013
+ const black = ca.referenceBlack / 1e4;
20014
+ const span = Math.max(1e-6, ca.referenceWhite / 1e4 - black);
20015
+ const gammas = [ca.redGamma / 1e4, ca.greenGamma / 1e4, ca.blueGamma / 1e4];
20016
+ const curves = gammas.map((gamma) => {
20017
+ const table = new Float64Array(256);
20018
+ for (let i = 0; i < 256; i++) {
20019
+ let v = clamp012((i / 255 - black) / span);
20020
+ v = Math.pow(v, gamma);
20021
+ table[i] = ca.flags & CA_NEGATIVE ? 1 - v : v;
20022
+ }
20023
+ return table;
20024
+ });
20025
+ const chroma = colorfulnessScale(ca.colorfulness);
20026
+ const contrast = 1 + ca.contrast / 100;
20027
+ const brightness = 0.8 * ca.brightness / 100;
20028
+ const tint = 0.3 * ca.redGreenTint / 100;
20029
+ const log = (ca.flags & CA_LOG_FILTER) !== 0;
20030
+ const out = (v) => {
20031
+ v = clamp012(v);
20032
+ if (log) {
20033
+ v = Math.log10(1 + 9 * v);
20034
+ }
20035
+ return Math.round(v * 255);
20036
+ };
20037
+ return (rgb2) => {
20038
+ const r = curves[0][rgb2 >> 16 & 255];
20039
+ const g = curves[1][rgb2 >> 8 & 255];
20040
+ const b = curves[2][rgb2 & 255];
20041
+ let y = 0.299 * r + 0.587 * g + 0.114 * b;
20042
+ let cr = (r - y) * chroma;
20043
+ const cb = (b - y) * chroma;
20044
+ y = (y - 0.5) * contrast + 0.5 + brightness;
20045
+ cr += tint;
20046
+ const r2 = y + cr;
20047
+ const b2 = y + cb;
20048
+ const g2 = (y - 0.299 * r2 - 0.114 * b2) / 0.587;
20049
+ return out(r2) << 16 | out(g2) << 8 | out(b2);
20050
+ };
20051
+ }
20052
+ function applyColorAdjustment(pixels, ca) {
20053
+ if (!ca || isIdentityColorAdjustment(ca)) {
20054
+ return;
20055
+ }
20056
+ const map = colorAdjustmentMapper(ca);
20057
+ const d = pixels.data;
20058
+ const cache = /* @__PURE__ */ new Map();
20059
+ for (let i = 0; i + 3 < d.length; i += 4) {
20060
+ const rgb2 = d[i] << 16 | d[i + 1] << 8 | d[i + 2];
20061
+ let v = cache.get(rgb2);
20062
+ if (v === void 0) {
20063
+ v = map(rgb2);
20064
+ if (cache.size < 65536) {
20065
+ cache.set(rgb2, v);
20066
+ }
20067
+ }
20068
+ d[i] = v >> 16 & 255;
20069
+ d[i + 1] = v >> 8 & 255;
20070
+ d[i + 2] = v & 255;
20071
+ }
20072
+ }
20073
+
19983
20074
  // src/emf-gdi-stretch.ts
19984
20075
  var BLACKONWHITE = 1;
19985
20076
  var WHITEONBLACK = 2;
@@ -20052,6 +20143,66 @@ function stretchGdi(src, sx, sy, sw, sh, dw, dh, mode) {
20052
20143
  }
20053
20144
  return { width: W, height: H, data };
20054
20145
  }
20146
+ function axisBoxWeights(srcStart, srcLen, dstLen, reverse) {
20147
+ const out = [];
20148
+ if (dstLen <= 0 || srcLen <= 0) {
20149
+ return out;
20150
+ }
20151
+ const at = (k) => reverse ? srcStart + srcLen - 1 - k : srcStart + k;
20152
+ for (let i = 0; i < dstLen; i++) {
20153
+ const a = i * srcLen;
20154
+ const b = (i + 1) * srcLen;
20155
+ const taps = [];
20156
+ for (let k = Math.floor(a / dstLen); k * dstLen < b; k++) {
20157
+ const overlap = Math.min(b, (k + 1) * dstLen) - Math.max(a, k * dstLen);
20158
+ if (overlap > 0) {
20159
+ taps.push([at(k), overlap / srcLen]);
20160
+ }
20161
+ }
20162
+ out.push(taps);
20163
+ }
20164
+ return out;
20165
+ }
20166
+ function stretchHalftone(src, sx, sy, sw, sh, dw, dh) {
20167
+ const W = Math.max(0, Math.round(Math.abs(dw)));
20168
+ const H = Math.max(0, Math.round(Math.abs(dh)));
20169
+ const flipX = dw < 0 !== sw < 0;
20170
+ const flipY = dh < 0 !== sh < 0;
20171
+ const cols = axisBoxWeights(Math.round(Math.min(sx, sx + sw)), Math.round(Math.abs(sw)), W, flipX);
20172
+ const rows = axisBoxWeights(Math.round(Math.min(sy, sy + sh)), Math.round(Math.abs(sh)), H, flipY);
20173
+ const data = new Uint8ClampedArray(W * H * 4);
20174
+ const s = src.data;
20175
+ for (let y = 0; y < H; y++) {
20176
+ const ys = rows[y] ?? [];
20177
+ for (let x = 0; x < W; x++) {
20178
+ const xs = cols[x] ?? [];
20179
+ let r = 0;
20180
+ let g = 0;
20181
+ let b = 0;
20182
+ for (const [yy, wy] of ys) {
20183
+ if (yy < 0 || yy >= src.height) {
20184
+ continue;
20185
+ }
20186
+ for (const [xx, wx] of xs) {
20187
+ if (xx < 0 || xx >= src.width) {
20188
+ continue;
20189
+ }
20190
+ const w = wx * wy;
20191
+ const i = (yy * src.width + xx) * 4;
20192
+ r += s[i] * w;
20193
+ g += s[i + 1] * w;
20194
+ b += s[i + 2] * w;
20195
+ }
20196
+ }
20197
+ const o = (y * W + x) * 4;
20198
+ data[o] = Math.floor(r + 1e-6);
20199
+ data[o + 1] = Math.floor(g + 1e-6);
20200
+ data[o + 2] = Math.floor(b + 1e-6);
20201
+ data[o + 3] = 255;
20202
+ }
20203
+ }
20204
+ return { width: W, height: H, data };
20205
+ }
20055
20206
 
20056
20207
  // src/emf-gdi-draw-bitmap.ts
20057
20208
  function patternOperand(rCtx, brush) {
@@ -20075,46 +20226,18 @@ function patternOperand(rCtx, brush) {
20075
20226
  function drawSourceMapped(target, decoded, req, offsetX, offsetY, mode) {
20076
20227
  const dLeft = Math.min(req.dx, req.dx + req.dw) - offsetX;
20077
20228
  const dTop = Math.min(req.dy, req.dy + req.dh) - offsetY;
20078
- if (mode !== HALFTONE) {
20079
- const px = stretchGdi(decoded.pixels, req.sx, req.sy, req.sw, req.sh, req.dw, req.dh, mode);
20080
- const tile = createTempCanvas(px.width, px.height);
20081
- if (!tile) {
20082
- return;
20083
- }
20084
- canvasPutImageData(tile.ctx, createImageDataCompat(px.data, px.width, px.height), 0, 0);
20085
- target.save();
20086
- target.globalCompositeOperation = "source-over";
20087
- canvasDrawImage(target, tile.canvas, Math.round(dLeft), Math.round(dTop), px.width, px.height);
20088
- target.restore();
20229
+ const px = mode === HALFTONE ? stretchHalftone(decoded.pixels, req.sx, req.sy, req.sw, req.sh, req.dw, req.dh) : stretchGdi(decoded.pixels, req.sx, req.sy, req.sw, req.sh, req.dw, req.dh, mode);
20230
+ if (px.width === 0 || px.height === 0) {
20089
20231
  return;
20090
20232
  }
20091
- const flipX = req.dw < 0 !== req.sw < 0;
20092
- const flipY = req.dh < 0 !== req.sh < 0;
20093
- const adw = Math.abs(req.dw);
20094
- const adh = Math.abs(req.dh);
20233
+ const tile = createTempCanvas(px.width, px.height);
20234
+ if (!tile) {
20235
+ return;
20236
+ }
20237
+ canvasPutImageData(tile.ctx, createImageDataCompat(px.data, px.width, px.height), 0, 0);
20095
20238
  target.save();
20096
- target.imageSmoothingEnabled = true;
20097
- target.transform(
20098
- flipX ? -1 : 1,
20099
- 0,
20100
- 0,
20101
- flipY ? -1 : 1,
20102
- flipX ? dLeft * 2 + adw : 0,
20103
- flipY ? dTop * 2 + adh : 0
20104
- );
20105
- const draw = target.drawImage;
20106
- draw.call(
20107
- target,
20108
- decoded.canvas,
20109
- Math.min(req.sx, req.sx + req.sw),
20110
- Math.min(req.sy, req.sy + req.sh),
20111
- Math.abs(req.sw),
20112
- Math.abs(req.sh),
20113
- dLeft,
20114
- dTop,
20115
- adw,
20116
- adh
20117
- );
20239
+ target.globalCompositeOperation = "source-over";
20240
+ canvasDrawImage(target, tile.canvas, Math.round(dLeft), Math.round(dTop), px.width, px.height);
20118
20241
  target.restore();
20119
20242
  }
20120
20243
  function decodeSource(rCtx, req) {
@@ -20131,17 +20254,15 @@ function decodeSource(rCtx, req) {
20131
20254
  if (!image) {
20132
20255
  return null;
20133
20256
  }
20134
- const temp = createTempCanvas(image.width, image.height);
20135
- if (!temp) {
20136
- return null;
20257
+ if (req.stretch && rCtx.state.stretchBltMode === HALFTONE) {
20258
+ applyColorAdjustment(image, rCtx.state.colorAdjustment);
20137
20259
  }
20138
- canvasPutImageData(temp.ctx, image, 0, 0);
20139
20260
  let { sy } = req;
20140
20261
  if (req.dibOrigin === "bottom-left" && rCtx.view.getInt32(req.source.bmi + 8, true) > 0) {
20141
20262
  sy = image.height - sy - req.sh;
20142
20263
  }
20143
20264
  return {
20144
- decoded: { pixels: image, canvas: temp.canvas },
20265
+ decoded: { pixels: image },
20145
20266
  req: { ...req, sy }
20146
20267
  };
20147
20268
  }
@@ -20254,7 +20375,7 @@ function nudgedFloor(num, den, dx, dy) {
20254
20375
  const q = Math.floor(num / den);
20255
20376
  return num === q * den && nudgedSign(0, dx, dy) < 0 ? q - 1 : q;
20256
20377
  }
20257
- function executeRotatedBlit(rCtx, logDx, logDy, logDw, logDh, rop, source, sx, sy, sw, sh, dibOrigin) {
20378
+ function executeRotatedBlit(rCtx, logDx, logDy, logDw, logDh, rop, source, sx, sy, sw, sh, dibOrigin, stretch) {
20258
20379
  const { ctx } = rCtx;
20259
20380
  const index = rop3Index(rop);
20260
20381
  if (index === 170) {
@@ -20290,7 +20411,8 @@ function executeRotatedBlit(rCtx, logDx, logDy, logDw, logDh, rop, source, sx, s
20290
20411
  sy,
20291
20412
  sw,
20292
20413
  sh,
20293
- dibOrigin
20414
+ dibOrigin,
20415
+ stretch
20294
20416
  });
20295
20417
  if (!decoded) {
20296
20418
  return;
@@ -20458,7 +20580,8 @@ function handleBlt(rCtx, offset, dataOff, recSize, stretch) {
20458
20580
  srcRect.sy,
20459
20581
  srcRect.sw,
20460
20582
  srcRect.sh,
20461
- "top-left"
20583
+ "top-left",
20584
+ stretch
20462
20585
  );
20463
20586
  return true;
20464
20587
  }
@@ -20470,7 +20593,8 @@ function handleBlt(rCtx, offset, dataOff, recSize, stretch) {
20470
20593
  dh: gmh(rCtx, cyDest),
20471
20594
  source,
20472
20595
  ...srcRect,
20473
- dibOrigin: "top-left"
20596
+ dibOrigin: "top-left",
20597
+ stretch
20474
20598
  });
20475
20599
  return true;
20476
20600
  }
@@ -20497,7 +20621,7 @@ function handleStretchDibits(rCtx, offset, dataOff, recSize) {
20497
20621
  const sw = view.getInt32(dataOff + 32, true);
20498
20622
  const sh = view.getInt32(dataOff + 36, true);
20499
20623
  if (hasWorldRotation(rCtx)) {
20500
- executeRotatedBlit(rCtx, xDest, yDest, cxDest, cyDest, rop, source, sx, sy, sw, sh, "bottom-left");
20624
+ executeRotatedBlit(rCtx, xDest, yDest, cxDest, cyDest, rop, source, sx, sy, sw, sh, "bottom-left", true);
20501
20625
  return true;
20502
20626
  }
20503
20627
  executeBlit(rCtx, {
@@ -20511,7 +20635,8 @@ function handleStretchDibits(rCtx, offset, dataOff, recSize) {
20511
20635
  sy,
20512
20636
  sw,
20513
20637
  sh,
20514
- dibOrigin: "bottom-left"
20638
+ dibOrigin: "bottom-left",
20639
+ stretch: true
20515
20640
  });
20516
20641
  return true;
20517
20642
  }
@@ -23027,20 +23152,9 @@ function handleEmfGdiMiscRecord(rCtx, recType, dataOff, recSize) {
23027
23152
  return true;
23028
23153
  }
23029
23154
  case EMR_SETCOLORADJUSTMENT: {
23030
- if (recSize >= 32) {
23031
- state.colorAdjustment = {
23032
- flags: view.getUint16(dataOff + 2, true),
23033
- illuminant: view.getUint16(dataOff + 4, true),
23034
- redGamma: view.getUint16(dataOff + 6, true),
23035
- greenGamma: view.getUint16(dataOff + 8, true),
23036
- blueGamma: view.getUint16(dataOff + 10, true),
23037
- referenceBlack: view.getUint16(dataOff + 12, true),
23038
- referenceWhite: view.getUint16(dataOff + 14, true),
23039
- contrast: view.getInt16(dataOff + 16, true),
23040
- brightness: view.getInt16(dataOff + 18, true),
23041
- colorfulness: view.getInt16(dataOff + 20, true),
23042
- redGreenTint: view.getInt16(dataOff + 22, true)
23043
- };
23155
+ const ca = recSize >= 8 + COLOR_ADJUSTMENT_SIZE ? readColorAdjustment(view, dataOff) : null;
23156
+ if (ca) {
23157
+ state.colorAdjustment = ca;
23044
23158
  }
23045
23159
  return true;
23046
23160
  }
@@ -23938,6 +24052,50 @@ function applyImageEffect(rgba, width, height, effect) {
23938
24052
  return applyTint(rgba, effect.hue, effect.amount);
23939
24053
  }
23940
24054
  }
24055
+ function applyImageEffectToRect(rgba, width, height, effect, src) {
24056
+ if (width <= 0 || height <= 0 || rgba.length !== width * height * 4) {
24057
+ return null;
24058
+ }
24059
+ let x0 = 0;
24060
+ let y0 = 0;
24061
+ let x1 = width;
24062
+ let y1 = height;
24063
+ if (src) {
24064
+ x0 = clamp(Math.floor(src.x), 0, width);
24065
+ y0 = clamp(Math.floor(src.y), 0, height);
24066
+ x1 = clamp(Math.ceil(src.x + src.w), 0, width);
24067
+ y1 = clamp(Math.ceil(src.y + src.h), 0, height);
24068
+ if (x1 <= x0 || y1 <= y0) {
24069
+ return null;
24070
+ }
24071
+ }
24072
+ const pad = src && effect.kind === "blur" && effect.expandEdge ? Math.ceil(effect.radius) : 0;
24073
+ const cw = x1 - x0;
24074
+ const ch = y1 - y0;
24075
+ const w = cw + 2 * pad;
24076
+ const h = ch + 2 * pad;
24077
+ let region = rgba;
24078
+ if (w !== width || h !== height || x0 !== 0 || y0 !== 0) {
24079
+ region = new Uint8ClampedArray(w * h * 4);
24080
+ for (let y = 0; y < ch; y++) {
24081
+ const from = ((y0 + y) * width + x0) * 4;
24082
+ region.set(rgba.subarray(from, from + cw * 4), ((pad + y) * w + pad) * 4);
24083
+ }
24084
+ }
24085
+ const ox = x0 - pad;
24086
+ const oy = y0 - pad;
24087
+ const local = effect.kind === "redEyeCorrection" ? {
24088
+ kind: effect.kind,
24089
+ areas: effect.areas.map((a) => ({
24090
+ left: a.left - ox,
24091
+ top: a.top - oy,
24092
+ right: a.right - ox,
24093
+ bottom: a.bottom - oy
24094
+ }))
24095
+ } : effect;
24096
+ const out = applyImageEffect(region, w, h, local);
24097
+ return out ? { rgba: out, x: ox, y: oy, width: w, height: h, pad } : null;
24098
+ }
23941
24099
 
23942
24100
  // src/emf-plus-draw-image.ts
23943
24101
  function surfaceSize4(ctx) {
@@ -24074,7 +24232,7 @@ function drawNestedMetafile(rCtx, img, cached, source) {
24074
24232
  rCtx.deferredImages.push(...deferred);
24075
24233
  return true;
24076
24234
  }
24077
- function effectedBitmap(img, bitmap, effect) {
24235
+ function effectedDraw(img, bitmap, effect, draw, source) {
24078
24236
  let src = bitmap;
24079
24237
  if (!src && img.data) {
24080
24238
  const bmp = decodeBmpFile(img.data.slice(0));
@@ -24085,21 +24243,61 @@ function effectedBitmap(img, bitmap, effect) {
24085
24243
  if (!src) {
24086
24244
  return null;
24087
24245
  }
24088
- const rgba = applyImageEffect(src.rgba, src.width, src.height, effect);
24089
- return rgba ? { kind: "bitmap", width: src.width, height: src.height, rgba } : null;
24246
+ 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;
24247
+ const region = applyImageEffectToRect(
24248
+ src.rgba,
24249
+ src.width,
24250
+ src.height,
24251
+ effect,
24252
+ rect && { x: rect.srcX, y: rect.srcY, w: rect.srcW, h: rect.srcH }
24253
+ );
24254
+ if (!region) {
24255
+ return null;
24256
+ }
24257
+ const out = { kind: "bitmap", width: region.width, height: region.height, rgba: region.rgba };
24258
+ if (!rect) {
24259
+ return { bitmap: out, draw };
24260
+ }
24261
+ const grow = region.pad;
24262
+ const srcX = rect.srcX - grow - region.x;
24263
+ const srcY = rect.srcY - grow - region.y;
24264
+ const srcW = rect.srcW + 2 * grow;
24265
+ const srcH = rect.srcH + 2 * grow;
24266
+ const shift = [1, 0, 0, 1, region.x, region.y];
24267
+ const toWorld = mulMatrix(rect.toWorld(rect.srcX, rect.srcY, rect.srcW, rect.srcH), shift);
24268
+ return {
24269
+ bitmap: out,
24270
+ draw: {
24271
+ ...draw,
24272
+ // The Canvas fallback scales the whole effected bitmap.
24273
+ transform: mulMatrix(draw.transform, toWorld),
24274
+ dx: 0,
24275
+ dy: 0,
24276
+ dw: region.width,
24277
+ dh: region.height,
24278
+ resample: draw.resample && {
24279
+ ...draw.resample,
24280
+ srcX,
24281
+ srcY,
24282
+ srcW,
24283
+ srcH,
24284
+ toDevice: mulMatrix(draw.resample.toDevice, shift)
24285
+ }
24286
+ }
24287
+ };
24090
24288
  }
24091
24289
  function drawEmfPlusImageNow(rCtx, img, draw, source, effect = null) {
24092
24290
  const cached = img.cacheKey !== void 0 ? rCtx.imageCache?.get(img.cacheKey) : void 0;
24093
24291
  if (img.type === 2) {
24094
24292
  return drawNestedMetafile(rCtx, img, cached, source);
24095
24293
  }
24096
- let bitmap = cached && cached.kind === "bitmap" ? cached : null;
24097
- const effected = effect ? effectedBitmap(img, bitmap, effect) : null;
24294
+ const bitmap = cached && cached.kind === "bitmap" ? cached : null;
24295
+ const effected = effect ? effectedDraw(img, bitmap, effect, draw, source) : null;
24098
24296
  if (effected) {
24099
- bitmap = effected;
24297
+ return isSvgContext(rCtx.ctx) ? paintBitmapSvg(rCtx.ctx, img, effected.bitmap, effected.draw, true) : paintBitmapRaster(rCtx.ctx, effected.bitmap, effected.draw);
24100
24298
  }
24101
24299
  if (isSvgContext(rCtx.ctx)) {
24102
- return paintBitmapSvg(rCtx.ctx, img, bitmap, draw, effected !== null);
24300
+ return paintBitmapSvg(rCtx.ctx, img, bitmap, draw);
24103
24301
  }
24104
24302
  return bitmap ? paintBitmapRaster(rCtx.ctx, bitmap, draw) : false;
24105
24303
  }
@@ -24391,28 +24589,32 @@ function strokePlusGeometry(rCtx, pen, buildPath, points, closedFigure = false)
24391
24589
  if (mode !== "canvas" && size && strokeGdiplus(rCtx, pen, nib, sampler, buildPath, device, size, mode, closedFigure)) {
24392
24590
  return;
24393
24591
  }
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;
24592
+ const outline = hasCustomCap(pen) ? penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure) : null;
24593
+ let box = null;
24594
+ if (size && outline) {
24595
+ box = deviceBounds(outline.flat(), device, size, 2);
24596
+ } else if (size) {
24597
+ const miter = Math.max(1, ctx.miterLimit || 10);
24598
+ const margin = pen.width * scale * miter / 2 + 2;
24599
+ box = deviceBounds(points, device, size, margin);
24600
+ }
24397
24601
  if (size && !box) {
24398
24602
  return;
24399
24603
  }
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
- )) {
24604
+ const drawMask = outline ? (c) => {
24605
+ traceOutline(c, outline);
24606
+ c.fill("nonzero");
24607
+ } : (c) => {
24608
+ applyPlusPenStyle(c, pen, widthScale);
24609
+ stroke(c);
24610
+ c.beginPath();
24611
+ buildPath(c);
24612
+ if (nib) {
24613
+ c.transform(nib[0], nib[1], nib[2], nib[3], 0, 0);
24614
+ }
24615
+ };
24616
+ const hitTest = outline ? (c, x, y) => c.isPointInPath(x, y, "nonzero") : (c, x, y) => c.isPointInStroke(x, y) && (!inset || c.isPointInPath(x, y));
24617
+ if (box && paintBrushThroughMask(rCtx, sampler, box, drawMask, mode, hitTest)) {
24416
24618
  return;
24417
24619
  }
24418
24620
  }
@@ -24577,18 +24779,18 @@ function customCapOutline(figures, width, devicePen, pen) {
24577
24779
  outline.push(...geo.polygons);
24578
24780
  return outline;
24579
24781
  }
24580
- function fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure) {
24782
+ function penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure) {
24581
24783
  const device = plusWorldMatrix(rCtx);
24582
24784
  const frame2 = penFrame(device, nib);
24583
24785
  const deviceInv = invertAffine(device);
24584
24786
  if (!frame2 || !deviceInv || !(frame2.unit > 0)) {
24585
- return false;
24787
+ return null;
24586
24788
  }
24587
24789
  let figures;
24588
24790
  try {
24589
24791
  figures = recordDeviceFigures(buildPath, frame2.record);
24590
24792
  } catch {
24591
- return false;
24793
+ return null;
24592
24794
  }
24593
24795
  if (closedFigure) {
24594
24796
  figures = figures.map((f) => ({ ...f, closed: true }));
@@ -24596,25 +24798,33 @@ function fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure) {
24596
24798
  const width = (pen.width || 1) * frame2.unit;
24597
24799
  const devicePen = framePen(pen, frame2.unit);
24598
24800
  const outline = hasCustomCap(pen) ? customCapOutline(figures, width, devicePen, pen) : widenFigures(figures, devicePen);
24599
- const toWorld = mulMatrix(deviceInv, frame2.toDevice);
24801
+ const m = mulMatrix(deviceInv, frame2.toDevice);
24802
+ 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] })));
24803
+ }
24804
+ function traceOutline(c, outline) {
24805
+ c.beginPath();
24806
+ for (const poly of outline) {
24807
+ poly.forEach((p, i) => {
24808
+ if (i === 0) {
24809
+ c.moveTo(p.x, p.y);
24810
+ } else {
24811
+ c.lineTo(p.x, p.y);
24812
+ }
24813
+ });
24814
+ c.closePath();
24815
+ }
24816
+ }
24817
+ function fillPenOutline(rCtx, pen, nib, paint, buildPath, closedFigure) {
24818
+ const outline = penOutlineWorld(rCtx, pen, nib, buildPath, closedFigure);
24819
+ if (!outline) {
24820
+ return false;
24821
+ }
24600
24822
  const { ctx } = rCtx;
24601
24823
  ctx.save();
24602
24824
  try {
24603
24825
  ctx.fillStyle = paint;
24604
24826
  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
- }
24827
+ traceOutline(ctx, outline);
24618
24828
  ctx.fill("nonzero");
24619
24829
  } finally {
24620
24830
  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.3.0",
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"