emf-converter 4.2.1 → 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. 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`).
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
@@ -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
  }
package/dist/index.mjs CHANGED
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emf-converter",
3
- "version": "4.2.1",
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",