@poe-platform/safe-bash 0.1.839 → 0.1.840

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.
Files changed (61) hide show
  1. package/dist/mp4-ast/types.d.ts +4 -2
  2. package/dist/pdf-ast/cos/security.d.ts +10 -0
  3. package/dist/pdf-ast/edit/retained-attachment-edits.d.ts +4 -0
  4. package/dist/pdf-ast/edit/retained-graph.d.ts +1 -1
  5. package/dist/pdf-ast/image.js +1 -1
  6. package/dist/pdf-ast/index.d.ts +2 -1
  7. package/dist/pdf-ast/index.js +7 -1
  8. package/dist/pdf-ast/vendor/pdfjs-fonts.d.mts +8 -0
  9. package/dist/pdf-ast/vendor/pdfjs-fonts.mjs +28 -0
  10. package/dist/safe-bash/command-chunks/{chunk-SG2KOOD4.js → chunk-3SAYZFMD.js} +2 -2
  11. package/dist/safe-bash/command-chunks/{chunk-FH5VMAPS.js → chunk-45ZH6CNF.js} +20 -4
  12. package/dist/safe-bash/command-chunks/{chunk-T6OXG7QQ.js → chunk-AAOPVEPF.js} +66 -1
  13. package/dist/safe-bash/command-chunks/{chunk-46GA576V.js → chunk-CPKVUB7S.js} +2 -2
  14. package/dist/safe-bash/command-chunks/{chunk-MBXNG6C3.js → chunk-DLLN3C3Q.js} +1 -1
  15. package/dist/safe-bash/command-chunks/{chunk-ZSQCROFE.js → chunk-DM5T5D2K.js} +5 -1
  16. package/dist/safe-bash/command-chunks/{chunk-3EAZRKI4.js → chunk-G7HQYFIU.js} +10 -1
  17. package/dist/safe-bash/command-chunks/{chunk-GYKQ3IOV.js → chunk-LMYQMJYY.js} +94 -18
  18. package/dist/safe-bash/command-chunks/{chunk-DX7KU6YU.js → chunk-N52AHO6S.js} +1 -1
  19. package/dist/safe-bash/command-chunks/{chunk-4BSD2RGK.js → chunk-QPKL5GWH.js} +102 -42
  20. package/dist/safe-bash/command-chunks/{chunk-Z2T7E4ID.js → chunk-R7QLP5RH.js} +1212 -1119
  21. package/dist/safe-bash/command-chunks/{chunk-RSFKQE2U.js → chunk-T5WFJ44Z.js} +1 -1
  22. package/dist/safe-bash/command-chunks/{chunk-77ROD2TR.js → chunk-X3WNWRVT.js} +11 -5
  23. package/dist/safe-bash/command-chunks/{chunk-TCXM7BSZ.js → chunk-YFMNXPLP.js} +4 -4
  24. package/dist/safe-bash/command-chunks/{chunk-DQVOBGI2.js → chunk-YVFQ5I36.js} +7335 -7162
  25. package/dist/safe-bash-command-exiftool/index.js +25 -1
  26. package/dist/safe-bash-command-ffmpeg/index.js +174 -41
  27. package/dist/safe-bash-command-ffprobe/index.js +2 -2
  28. package/dist/safe-bash-command-ffprobe/media.js +2 -2
  29. package/dist/safe-bash-command-imagemagick/index.js +482 -98
  30. package/dist/safe-bash-command-mdq/index.js +2 -0
  31. package/dist/safe-bash-command-pandoc/citeproc-filters.js +7 -7
  32. package/dist/safe-bash-command-pandoc/command.js +8 -8
  33. package/dist/safe-bash-command-pandoc/index.js +51 -51
  34. package/dist/safe-bash-command-qpdf/index.js +1037 -654
  35. package/dist/safe-bash-command-qpdf/page-range.d.ts +2 -0
  36. package/dist/safe-bash-command-soffice/index.js +28 -8
  37. package/dist/safe-bash-command-sqlite3/btree.d.ts +2 -1
  38. package/dist/safe-bash-command-sqlite3/engine.d.ts +15 -1
  39. package/dist/safe-bash-command-sqlite3/index.js +294 -68
  40. package/dist/safe-bash-command-tar/create.js +2 -2
  41. package/dist/safe-bash-command-tar/extract.js +2 -2
  42. package/dist/safe-bash-command-tar/format.js +1 -1
  43. package/dist/safe-bash-command-tar/index.js +4 -4
  44. package/dist/safe-bash-command-tar/modes.js +4 -4
  45. package/dist/safe-bash-command-unzip/index.js +1 -1
  46. package/dist/safe-bash-command-unzip/unzip.js +1 -1
  47. package/dist/safe-bash-markdown-engine/commonmark-blocks.d.ts +17 -1
  48. package/dist/safe-bash-markdown-engine/commonmark-blocks.js +3 -3
  49. package/dist/safe-bash-markdown-engine/commonmark-inlines.d.ts +1 -1
  50. package/dist/safe-bash-markdown-engine/commonmark-inlines.js +3 -3
  51. package/dist/safe-bash-markdown-engine/commonmark-syntax.d.ts +5 -0
  52. package/dist/safe-bash-markdown-engine/commonmark-syntax.js +5 -1
  53. package/dist/safe-bash-markdown-engine/gfm-syntax.js +2 -2
  54. package/dist/safe-bash-markdown-engine/index.js +4 -4
  55. package/dist/safe-bash-zip-engine/index.js +1 -1
  56. package/dist/safe-bash-zip-engine/zip/repair.js +1 -1
  57. package/dist/safe-bash-zip-engine/zip/sfx.js +1 -1
  58. package/dist/safe-bash-zip-engine/zip-format.d.ts +2 -0
  59. package/dist/safe-bash-zip-engine/zip-format.js +1 -1
  60. package/package.json +2 -2
  61. package/third-party/pdf-ast/THIRD_PARTY_NOTICES.md +2 -0
@@ -1,5 +1,96 @@
1
1
  import "../safe-bash/command-chunks/chunk-65DS4IOB.js";
2
2
 
3
+ // packages/safe-bash-command-imagemagick/src/quantize.ts
4
+ function* quantizePixelSteps(length, requested) {
5
+ if (!Number.isSafeInteger(requested) || requested < 1) throw new Error("colors must be a positive integer");
6
+ let bins = /* @__PURE__ */ new Map(), mask = 4294967295, precision = 8;
7
+ const key = (data, i) => (data[i] << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[i + 3]) & mask;
8
+ for (let position = 0; position < length; position += 4096) {
9
+ yield;
10
+ const size = Math.min(4096, length - position), data = yield { kind: "read", position, length: size };
11
+ if (!data || data.length !== size) throw new Error("Truncated quantization pixels");
12
+ for (let i = 0; i < size; i += 4) {
13
+ const id = key(data, i);
14
+ let bin = bins.get(id);
15
+ if (!bin) {
16
+ bin = { count: 0, sums: [0, 0, 0, 0], color: [] };
17
+ bins.set(id, bin);
18
+ }
19
+ bin.count++;
20
+ for (let c = 0; c < 4; c++) bin.sums[c] += data[i + c];
21
+ }
22
+ if (bins.size > 65536) {
23
+ const channelMask = 255 << 8 - --precision & 255;
24
+ mask = channelMask * 16843009;
25
+ const reduced = /* @__PURE__ */ new Map();
26
+ let work = 0;
27
+ for (const [id, bin] of bins) {
28
+ if (++work % 1024 === 0) yield;
29
+ const next = id & mask, existing = reduced.get(next);
30
+ if (!existing) reduced.set(next, bin);
31
+ else {
32
+ existing.count += bin.count;
33
+ for (let c = 0; c < 4; c++) existing.sums[c] += bin.sums[c];
34
+ }
35
+ }
36
+ bins = reduced;
37
+ }
38
+ }
39
+ const pending = [{ colors: [...bins.values()], budget: requested }];
40
+ while (pending.length) {
41
+ yield;
42
+ const { colors, budget } = pending.pop();
43
+ if (!colors.length) continue;
44
+ const sums = [0, 0, 0, 0], squares = [0, 0, 0, 0];
45
+ let count = 0;
46
+ for (let i = 0; i < colors.length; i++) {
47
+ if (i % 1024 === 0) yield;
48
+ const bin = colors[i];
49
+ count += bin.count;
50
+ for (let c = 0; c < 4; c++) {
51
+ sums[c] += bin.sums[c];
52
+ squares[c] += bin.sums[c] ** 2 / bin.count;
53
+ }
54
+ }
55
+ if (colors.length <= budget) {
56
+ for (let i = 0; i < colors.length; i++) {
57
+ if (i % 1024 === 0) yield;
58
+ colors[i].color = colors[i].sums.map((value) => Math.round(value / colors[i].count));
59
+ }
60
+ } else if (budget === 1) {
61
+ const average = sums.map((value) => Math.round(value / count));
62
+ for (let i = 0; i < colors.length; i++) {
63
+ if (i % 1024 === 0) yield;
64
+ colors[i].color = average;
65
+ }
66
+ } else {
67
+ let axis = 0, variance = -1;
68
+ for (let c = 0; c < 4; c++) {
69
+ const next = squares[c] - sums[c] ** 2 / count;
70
+ if (next >= variance) {
71
+ variance = next;
72
+ axis = c;
73
+ }
74
+ }
75
+ colors.sort((a, b) => a.sums[axis] / a.count - b.sums[axis] / b.count);
76
+ let population = 0, split = 0;
77
+ do {
78
+ population += colors[split++].count;
79
+ } while (population < count / 2 && split < colors.length - 1);
80
+ const leftBudget = Math.min(split, Math.max(1, Math.floor(budget / 2)));
81
+ pending.push({ colors: colors.slice(0, split), budget: leftBudget }, { colors: colors.slice(split), budget: budget - leftBudget });
82
+ }
83
+ }
84
+ for (let position = 0; position < length; position += 4096) {
85
+ yield;
86
+ const size = Math.min(4096, length - position), source = yield { kind: "read", position, length: size };
87
+ if (!source || source.length !== size) throw new Error("Truncated quantization pixels");
88
+ const data = new Uint8Array(size);
89
+ for (let i = 0; i < size; i += 4) data.set(bins.get(key(source, i)).color, i);
90
+ yield { kind: "write", position, data };
91
+ }
92
+ }
93
+
3
94
  // packages/safe-bash-command-imagemagick/src/connected-components.ts
4
95
  import { yieldTurn as yieldTurn2 } from "../safe-bash-contracts/yield.js";
5
96
 
@@ -1337,6 +1428,7 @@ function* compareImageSteps(imgA, imgB, options) {
1337
1428
  const height = Math.max(imgA.height, imgB.height);
1338
1429
  const totalPixels = Math.max(1, width * height);
1339
1430
  let aeCount = 0;
1431
+ let aeMagnitude = 0;
1340
1432
  let sumAbs = 0;
1341
1433
  let sumSq = 0;
1342
1434
  let maxAbs = 0;
@@ -1346,6 +1438,15 @@ function* compareImageSteps(imgA, imgB, options) {
1346
1438
  let sumLumSqB = 0;
1347
1439
  let sumLumAB = 0;
1348
1440
  const hasAlpha = imgA.hasAlpha || imgB.hasAlpha;
1441
+ const nccChannels = metric === "ncc" ? Array.from({ length: imgA.hasAlpha && imgB.hasAlpha ? 4 : 3 }, () => ({
1442
+ rawA: 0,
1443
+ rawB: 0,
1444
+ sumA: 0,
1445
+ sumB: 0,
1446
+ squareA: 0,
1447
+ squareB: 0,
1448
+ product: 0
1449
+ })) : [];
1349
1450
  for (let y = 0; y < height; y++) {
1350
1451
  if ((y & 511) === 511)
1351
1452
  yield;
@@ -1358,6 +1459,7 @@ function* compareImageSteps(imgA, imgB, options) {
1358
1459
  dataA = yield { kind: "read", image: 0, position: (y * imgA.width + left) * 4, length };
1359
1460
  if (!(dataA instanceof Uint8Array) || dataA.length !== length)
1360
1461
  throw new Error("Truncated comparison pixels");
1462
+ dataA = dataA.slice();
1361
1463
  }
1362
1464
  if (y < imgB.height && left < imgB.width) {
1363
1465
  const length = Math.min(count, imgB.width - left) * 4;
@@ -1380,6 +1482,20 @@ function* compareImageSteps(imgA, imgB, options) {
1380
1482
  const gB = offB >= 0 ? dataB[offB + 1] : 0;
1381
1483
  const bB = offB >= 0 ? dataB[offB + 2] : 0;
1382
1484
  const aB = offB >= 0 ? dataB[offB + 3] : 0;
1485
+ for (let channel = 0; channel < nccChannels.length; channel++) {
1486
+ const stats = nccChannels[channel];
1487
+ const rawA = offA >= 0 ? dataA[offA + channel] : 0;
1488
+ const rawB = offB >= 0 ? dataB[offB + channel] : 0;
1489
+ const a = channel === 3 ? rawA : rawA * aA / 255;
1490
+ const b = channel === 3 ? rawB : rawB * aB / 255;
1491
+ stats.rawA += rawA;
1492
+ stats.rawB += rawB;
1493
+ stats.sumA += a;
1494
+ stats.sumB += b;
1495
+ stats.squareA += a * a;
1496
+ stats.squareB += b * b;
1497
+ stats.product += a * b;
1498
+ }
1383
1499
  const dr = Math.abs(rA - rB);
1384
1500
  const dg = Math.abs(gA - gB);
1385
1501
  const db = Math.abs(bA - bB);
@@ -1389,6 +1505,8 @@ function* compareImageSteps(imgA, imgB, options) {
1389
1505
  const pdr = Math.abs(rA * alphaA - rB * alphaB);
1390
1506
  const pdg = Math.abs(gA * alphaA - gB * alphaB);
1391
1507
  const pdb = Math.abs(bA * alphaA - bB * alphaB);
1508
+ const aeR = hasAlpha ? pdr : dr, aeG = hasAlpha ? pdg : dg, aeB = hasAlpha ? pdb : db;
1509
+ aeMagnitude += (aeR > fuzz ? aeR : 0) + (aeG > fuzz ? aeG : 0) + (aeB > fuzz ? aeB : 0) + (hasAlpha && da > fuzz ? da : 0);
1392
1510
  const maxDelta = hasAlpha ? Math.max(pdr, pdg, pdb, da) : Math.max(dr, dg, db);
1393
1511
  if (!inBoundsA || !inBoundsB || maxDelta > fuzz) {
1394
1512
  aeCount++;
@@ -1450,11 +1568,10 @@ function* compareImageSteps(imgA, imgB, options) {
1450
1568
  const c1 = 1e-4;
1451
1569
  const c2 = 9e-4;
1452
1570
  const ssim = (2 * muA * muB + c1) * (2 * covAB + c2) / ((muA * muA + muB * muB + c1) * (varA + varB + c2));
1453
- const ncc = varA === 0 && varB === 0 ? 1 : covAB / (Math.sqrt(varA * varB) || 1);
1454
1571
  let metricStr;
1455
1572
  switch (metric) {
1456
1573
  case "ae":
1457
- metricStr = String(aeCount);
1574
+ metricStr = `${formatSignificantMetric(aeMagnitude / (numCh * 255))} (${formatSignificantMetric(aeMagnitude / (totalPixels * numCh * 255))})`;
1458
1575
  break;
1459
1576
  case "mae":
1460
1577
  metricStr = `${formatMetricNum(maeNorm * 65535)} (${formatMetricNum(maeNorm)})`;
@@ -1466,23 +1583,37 @@ function* compareImageSteps(imgA, imgB, options) {
1466
1583
  metricStr = `${formatMetricNum(paeNorm * 65535)} (${formatMetricNum(paeNorm)})`;
1467
1584
  break;
1468
1585
  case "psnr":
1469
- metricStr = mseNorm === 0 ? "inf" : formatMetricNum(10 * Math.log10(1 / mseNorm));
1586
+ metricStr = mseNorm === 0 ? "120 (1)" : formatMetricNum(10 * Math.log10(1 / mseNorm));
1470
1587
  break;
1471
- case "ssim":
1472
- metricStr = formatMetricNum(ssim);
1588
+ case "ssim": {
1589
+ const distortion = yield* compareSsimSteps(imgA, imgB);
1590
+ metricStr = `${formatSignificantMetric(distortion * 65535)} (${formatSignificantMetric(distortion)})`;
1473
1591
  break;
1592
+ }
1474
1593
  case "dssim":
1475
1594
  metricStr = formatMetricNum((1 - ssim) / 2);
1476
1595
  break;
1477
- case "ncc":
1478
- metricStr = formatMetricNum(ncc);
1596
+ case "ncc": {
1597
+ let varianceA = 0, varianceB = 0, covariance = 0;
1598
+ for (const stats of nccChannels) {
1599
+ const meanA = stats.rawA / (imgA.width * imgA.height);
1600
+ const meanB = stats.rawB / (imgB.width * imgB.height);
1601
+ varianceA += Math.max(0, stats.squareA - 2 * meanA * stats.sumA + totalPixels * meanA * meanA);
1602
+ varianceB += Math.max(0, stats.squareB - 2 * meanB * stats.sumB + totalPixels * meanB * meanB);
1603
+ covariance += stats.product - meanA * stats.sumB - meanB * stats.sumA + totalPixels * meanA * meanB;
1604
+ }
1605
+ const denominator = Math.sqrt(varianceA) * Math.sqrt(varianceB);
1606
+ const similarity = denominator === 0 ? 0 : covariance / denominator;
1607
+ const distortion = Math.abs(1 - similarity) < 2e-12 ? 0 : (1 - similarity) / 2;
1608
+ metricStr = `${formatSignificantMetric(distortion * 65535)} (${formatSignificantMetric(distortion)})`;
1479
1609
  break;
1610
+ }
1480
1611
  case "rmse":
1481
1612
  default:
1482
1613
  metricStr = `${formatMetricNum(rmseNorm * 65535)} (${formatMetricNum(rmseNorm)})`;
1483
1614
  break;
1484
1615
  }
1485
- const exitCode = dissimilarityThreshold !== void 0 ? rmseNorm > dissimilarityThreshold ? 1 : 0 : aeCount > 0 ? 1 : 0;
1616
+ const exitCode = metric === "psnr" && mseNorm === 0 ? 1 : dissimilarityThreshold !== void 0 ? rmseNorm > dissimilarityThreshold ? 1 : 0 : aeCount > 0 ? 1 : 0;
1486
1617
  return { width, height, metric: metricStr, exitCode };
1487
1618
  }
1488
1619
  function formatMetricNum(n) {
@@ -1493,6 +1624,65 @@ function formatMetricNum(n) {
1493
1624
  const fixed = n.toFixed(6).replace(/\.?0+$/, "");
1494
1625
  return fixed === "-0" ? "0" : fixed;
1495
1626
  }
1627
+ function formatSignificantMetric(n) {
1628
+ const rounded = Number(n.toPrecision(6));
1629
+ if (rounded === 0 || rounded >= 1e-4 && rounded < 1e6)
1630
+ return String(rounded);
1631
+ const [mantissa, exponent] = rounded.toExponential().split("e");
1632
+ return `${mantissa}e${exponent[0]}${exponent.slice(1).padStart(2, "0")}`;
1633
+ }
1634
+ function* compareSsimSteps(imgA, imgB) {
1635
+ const width = Math.max(imgA.width, imgB.width), height = Math.max(imgA.height, imgB.height);
1636
+ const channels = imgA.hasAlpha && imgB.hasAlpha ? 4 : 3;
1637
+ const weights = Array.from({ length: 11 }, (_, i) => Math.exp(-((i - 5) ** 2) / (2 * 1.5 ** 2)));
1638
+ const weightSum = weights.reduce((sum, value) => sum + value, 0);
1639
+ for (let i = 0; i < weights.length; i++)
1640
+ weights[i] = weights[i] / weightSum;
1641
+ let similarity = 0;
1642
+ for (let y = 0; y < height; y++) {
1643
+ for (let left = 0; left < width; left += 1014) {
1644
+ yield;
1645
+ const count = Math.min(1014, width - left);
1646
+ const rows = [[], []];
1647
+ const starts = [];
1648
+ for (const image of [0, 1]) {
1649
+ const info = image === 0 ? imgA : imgB;
1650
+ const start = Math.min(info.width - 1, Math.max(0, left - 5));
1651
+ const end = Math.min(info.width, left + count + 5);
1652
+ starts[image] = start;
1653
+ for (let v = -5; v <= 5; v++) {
1654
+ const row = Math.max(0, Math.min(info.height - 1, y + v));
1655
+ const length = (end - start) * 4;
1656
+ const data = yield { kind: "read", image, position: (row * info.width + start) * 4, length };
1657
+ if (!(data instanceof Uint8Array) || data.length !== length)
1658
+ throw new Error("Truncated comparison pixels");
1659
+ rows[image].push(data.slice());
1660
+ }
1661
+ }
1662
+ for (let x = left; x < left + count; x++) {
1663
+ for (let channel = 0; channel < channels; channel++) {
1664
+ let muA = 0, muB = 0, squareA = 0, squareB = 0, product = 0;
1665
+ for (let v = 0; v < 11; v++) {
1666
+ for (let u = 0; u < 11; u++) {
1667
+ const offsetA = (Math.max(0, Math.min(imgA.width - 1, x + u - 5)) - starts[0]) * 4 + channel;
1668
+ const offsetB = (Math.max(0, Math.min(imgB.width - 1, x + u - 5)) - starts[1]) * 4 + channel;
1669
+ const a = rows[0][v][offsetA] / 255, b = rows[1][v][offsetB] / 255;
1670
+ const weight = weights[u] * weights[v];
1671
+ muA += weight * a;
1672
+ muB += weight * b;
1673
+ squareA += weight * a * a;
1674
+ squareB += weight * b * b;
1675
+ product += weight * a * b;
1676
+ }
1677
+ }
1678
+ similarity += (2 * muA * muB + 1e-4) * (2 * (product - muA * muB) + 9e-4) / ((muA * muA + muB * muB + 1e-4) * (squareA - muA * muA + squareB - muB * muB + 9e-4));
1679
+ }
1680
+ }
1681
+ }
1682
+ }
1683
+ const distortion = (1 - similarity / (width * height * channels)) / 2;
1684
+ return Math.abs(distortion) < 1e-12 ? 0 : distortion;
1685
+ }
1496
1686
 
1497
1687
  // packages/safe-bash-command-imagemagick/src/identify-file.ts
1498
1688
  import { readImageMetadataFromSource as readImageMetadataFromSource2, tryPdfMetadata as tryPdfMetadata2, computeStoredImageStats, readImageMetadata as readImageMetadata2, decodeImage as decodeImage2, computeImageStatsSteps, UnsupportedStoredResource as UnsupportedStoredResource3, UnsupportedImageFormat as UnsupportedImageFormat3 } from "../image-ast/portable.js";
@@ -1589,7 +1779,7 @@ import {
1589
1779
  } from "../safe-bash-contracts/command.js";
1590
1780
  import { readBytes as readBytes2, writeBytes as writeBytes2 } from "../safe-bash-contracts/io.js";
1591
1781
  import { createOutputOperation } from "../safe-bash-contracts/output.js";
1592
- import { readImageMetadataFromSource as readImageMetadataFromSource3, decodeImageToStorage as decodeImageToStorage2, transformStoredImage, UnsupportedStoredResource as UnsupportedStoredResource4, decodeImage as decodeImage3, decodePngToCanvas, FONT_5X7, encodeImage, readImageMetadata as readImageMetadata3, parseColor as baseParseColor, applyExifOrientationSteps, blurImageSteps, compositeImageSteps, computeImageStatsSteps as computeImageStatsSteps2, ensureAlphaImageSteps, extendImageSteps, extractChannelImageSteps, extractImageSteps, flattenImageSteps, flipImageSteps, flopImageSteps, gammaImageSteps, grayscaleImageSteps, linearImageSteps, medianImageSteps, modulateImageSteps, negateImageSteps, removeAlphaImageSteps, resizeImageSteps, rotateImageSteps, sharpenImageSteps, thresholdImageSteps, tintImageSteps, trimImageSteps } from "../image-ast/portable.js";
1782
+ import { readImageMetadataFromSource as readImageMetadataFromSource3, decodeImageToStorage as decodeImageToStorage2, transformStoredImage, UnsupportedStoredResource as UnsupportedStoredResource4, decodeImage as decodeImage3, decodePngToCanvas, FONT_5X7, encodeImage, readImageMetadata as readImageMetadata3, parseColor as baseParseColor, applyExifOrientationSteps, blurImageSteps, compositeImageSteps, computeImageStatsSteps as computeImageStatsSteps2, ensureAlphaImageSteps, extendImageSteps, extractChannelImageSteps, extractImageSteps, flattenImageSteps, flipImageSteps, flopImageSteps, grayscaleImageSteps, linearImageSteps, medianImageSteps, negateImageSteps, removeAlphaImageSteps, resizeImageSteps, rotateImageSteps, sharpenImageSteps, thresholdImageSteps, trimImageSteps } from "../image-ast/portable.js";
1593
1783
  var X11_NAMED_COLORS = {
1594
1784
  aliceblue: [240, 248, 255, 255],
1595
1785
  antiquewhite: [250, 235, 215, 255],
@@ -2177,7 +2367,9 @@ function* applyMagickEvaluateSequenceSteps(stack, opRaw, signal) {
2177
2367
  }
2178
2368
  function parseMagickConvolveKernel(spec) {
2179
2369
  const afterColon = spec.includes(":") ? spec.slice(spec.indexOf(":") + 1) : spec;
2180
- return afterColon.trim().split(/[\s,]+/).filter((s) => s.length > 0).map(Number).filter(Number.isFinite);
2370
+ const kernel = afterColon.trim().split(/[\s,]+/).filter((s) => s.length > 0).map(Number).filter(Number.isFinite);
2371
+ const sum = kernel.reduce((total, weight) => total + weight, 0);
2372
+ return sum === 0 ? kernel : kernel.map((weight) => weight / sum);
2181
2373
  }
2182
2374
  function* applyMagickCustomConvolveSteps(img, spec, signal) {
2183
2375
  const kernel = parseMagickConvolveKernel(spec);
@@ -2813,7 +3005,9 @@ function* applyMagickOpaqueSteps(img, target, replacement, fuzz, invert, signal)
2813
3005
  const dr = Math.abs(out[i] - target.r);
2814
3006
  const dg = Math.abs(out[i + 1] - target.g);
2815
3007
  const db = Math.abs(out[i + 2] - target.b);
2816
- const matched = Math.max(dr, dg, db) <= fuzz;
3008
+ const alpha = out[i + 3];
3009
+ const distance = (alpha - target.a) ** 2 + (dr ** 2 + dg ** 2 + db ** 2) * (alpha / 255) * (target.a / 255) / 3;
3010
+ const matched = distance <= fuzz ** 2;
2817
3011
  if (invert ? !matched : matched) {
2818
3012
  out[i] = replacement.r;
2819
3013
  out[i + 1] = replacement.g;
@@ -2832,7 +3026,9 @@ function* applyMagickTransparentSteps(img, target, fuzz, invert, signal) {
2832
3026
  const dr = Math.abs(out[i] - target.r);
2833
3027
  const dg = Math.abs(out[i + 1] - target.g);
2834
3028
  const db = Math.abs(out[i + 2] - target.b);
2835
- const matched = Math.max(dr, dg, db) <= fuzz;
3029
+ const alpha = out[i + 3];
3030
+ const distance = (alpha - target.a) ** 2 + (dr ** 2 + dg ** 2 + db ** 2) * (alpha / 255) * (target.a / 255) / 3;
3031
+ const matched = distance <= fuzz ** 2;
2836
3032
  if (invert ? !matched : matched) {
2837
3033
  out[i + 3] = 0;
2838
3034
  }
@@ -2844,7 +3040,7 @@ function* applyMagickEvaluateSteps(img, opRaw, valStr, channels, signal) {
2844
3040
  const op = opRaw.toLowerCase().replace(/[-_]/g, "");
2845
3041
  const isPct = valStr.trim().endsWith("%");
2846
3042
  const rawNum = parseFloat(valStr);
2847
- const vByte = isPct ? rawNum / 100 * 255 : rawNum <= 1 && valStr.includes(".") ? rawNum * 255 : rawNum > 255 ? rawNum / 257 : rawNum;
3043
+ const vByte = isPct ? rawNum / 100 * 255 : rawNum / 257;
2848
3044
  const vFactor = isPct ? rawNum / 100 : rawNum;
2849
3045
  const out = new Uint8Array(img.data);
2850
3046
  const mask = [channels.r, channels.g, channels.b, channels.a];
@@ -3601,14 +3797,66 @@ function* applyMagickPosterizeSteps(img, levelsRaw, signal) {
3601
3797
  const levels = Math.max(2, Math.min(256, Math.round(levelsRaw)));
3602
3798
  const step = 255 / (levels - 1);
3603
3799
  const out = new Uint8Array(img.data);
3800
+ let palette;
3604
3801
  for (let i = 0; i < out.length; i += 4) {
3605
3802
  if (++cooperativeWork % 65536 === 0)
3606
3803
  yield;
3607
- out[i] = clampByteVal(Math.round(out[i] / 255 * (levels - 1)) * step);
3608
- out[i + 1] = clampByteVal(Math.round(out[i + 1] / 255 * (levels - 1)) * step);
3609
- out[i + 2] = clampByteVal(Math.round(out[i + 2] / 255 * (levels - 1)) * step);
3804
+ if (img.hasAlpha && out[i + 3] < 255 && levels <= 16) {
3805
+ if (!palette) {
3806
+ palette = { children: [] };
3807
+ for (let a = 0; a < levels; a++) for (let b = 0; b < levels; b++) for (let g = 0; g < levels; g++) for (let r = 0; r < levels; r++) {
3808
+ if (++cooperativeWork % 1024 === 0) {
3809
+ signal?.throwIfAborted();
3810
+ yield;
3811
+ }
3812
+ const color = a === 0 ? [0, 0, 0, 0] : [r * step, g * step, b * step, a * step];
3813
+ const bytes2 = color.map((value, channel) => Math.round(channel === 3 ? value : value * color[3] / 255));
3814
+ let node2 = palette;
3815
+ for (let bit = 7; bit >= 0; bit--) {
3816
+ const id = bytes2[0] >> bit & 1 | (bytes2[1] >> bit & 1) << 1 | (bytes2[2] >> bit & 1) << 2 | (bytes2[3] >> bit & 1) << 3;
3817
+ node2 = node2.children[id] ??= { children: [] };
3818
+ }
3819
+ node2.color = color;
3820
+ }
3821
+ }
3822
+ const alpha = out[i + 3] / 255;
3823
+ const target = [out[i] * alpha, out[i + 1] * alpha, out[i + 2] * alpha, out[i + 3]];
3824
+ const bytes = target.map(Math.round);
3825
+ let node = palette, parent = palette;
3826
+ for (let bit = 7; bit > 0; bit--) {
3827
+ const id = bytes[0] >> bit & 1 | (bytes[1] >> bit & 1) << 1 | (bytes[2] >> bit & 1) << 2 | (bytes[3] >> bit & 1) << 3;
3828
+ const child = node.children[id];
3829
+ if (!child) break;
3830
+ parent = node;
3831
+ node = child;
3832
+ }
3833
+ let distance = Infinity, closest = [0, 0, 0, 0];
3834
+ const pending = [parent];
3835
+ while (pending.length) {
3836
+ if (++cooperativeWork % 1024 === 0) {
3837
+ signal?.throwIfAborted();
3838
+ yield;
3839
+ }
3840
+ const candidate = pending.pop();
3841
+ for (let child = candidate.children.length - 1; child >= 0; child--) {
3842
+ if (candidate.children[child]) pending.push(candidate.children[child]);
3843
+ }
3844
+ if (!candidate.color) continue;
3845
+ const color = candidate.color, candidateAlpha = color[3] / 255;
3846
+ let error = (color[3] - target[3]) ** 2;
3847
+ for (let channel = 0; channel < 3; channel++) error += (color[channel] * candidateAlpha - target[channel] * alpha) ** 2;
3848
+ if (error <= distance) {
3849
+ distance = error;
3850
+ closest = color;
3851
+ }
3852
+ }
3853
+ for (let channel = 0; channel < 4; channel++) out[i + channel] = clampByteVal(closest[channel]);
3854
+ } else {
3855
+ for (let channel = 0; channel < 4; channel++) out[i + channel] = clampByteVal(Math.round(out[i + channel] / 255 * (levels - 1)) * step);
3856
+ }
3610
3857
  }
3611
- return { ...img, data: out };
3858
+ const { data16: ignored, ...metadata } = img;
3859
+ return { ...metadata, data: out };
3612
3860
  }
3613
3861
  function* applyMagickRasterSteps(img, steps, signal, dimensions = img, sources = [img]) {
3614
3862
  const data = new Uint8Array(dimensions.width * dimensions.height * 4);
@@ -3764,7 +4012,7 @@ function parseCompose(raw) {
3764
4012
  function magickCompositeMode(mode) {
3765
4013
  let name = "";
3766
4014
  for (const character of mode.toLowerCase()) if (character !== "-" && character !== "_" && character.trim()) name += character;
3767
- return { name, delegated: ["over", "srcover", "dstover", "destover", "plus", "add", "lineardodge", "colordodge", "colorburn", "hardlight", "softlight", "saturate"].includes(name) };
4015
+ return { name, delegated: ["over", "srcover", "dstover", "destover", "plus", "add", "lineardodge", "colordodge", "colorburn", "saturate"].includes(name) };
3768
4016
  }
3769
4017
  function* applyMagickCompositeLayerSteps(dst, src, modeRaw, left, top, composeArgs, signal) {
3770
4018
  let cooperativeWork = 0;
@@ -3832,7 +4080,7 @@ function* applyMagickCompositeLayerSteps(dst, src, modeRaw, left, top, composeAr
3832
4080
  out[dIdx] = sr;
3833
4081
  out[dIdx + 1] = sg;
3834
4082
  out[dIdx + 2] = sb;
3835
- out[dIdx + 3] = saByte;
4083
+ out[dIdx + 3] = dst.hasAlpha ? saByte : daByte;
3836
4084
  continue;
3837
4085
  }
3838
4086
  if (norm === "dst" || norm === "dest") {
@@ -3861,11 +4109,24 @@ function* applyMagickCompositeLayerSteps(dst, src, modeRaw, left, top, composeAr
3861
4109
  out[dIdx + 2] = sb;
3862
4110
  continue;
3863
4111
  }
4112
+ if (norm === "softlight") {
4113
+ const alpha = sa + da - sa * da;
4114
+ for (let c = 0; c < 3; c++) {
4115
+ const sc = src.data[sIdx + c] / 255 * sa;
4116
+ const dc = dst.data[dIdx + c] / 255 * da;
4117
+ const blend = sc <= 0.5 ? dc - (1 - 2 * sc) * dc * (1 - dc) : dc + (2 * sc - 1) * ((dc > 0.25 ? Math.sqrt(dc) : ((16 * dc - 12) * dc + 4) * dc) - dc);
4118
+ out[dIdx + c] = alpha === 0 ? 0 : clampByteVal(255 * (dc * sa + da * blend + sc * (1 - da) + dc * (1 - sa)) / alpha);
4119
+ }
4120
+ out[dIdx + 3] = clampByteVal(alpha * 255);
4121
+ continue;
4122
+ }
3864
4123
  if (norm === "difference") {
4124
+ if (saByte === 0)
4125
+ continue;
3865
4126
  out[dIdx] = Math.abs(sr - dr);
3866
4127
  out[dIdx + 1] = Math.abs(sg - dg);
3867
4128
  out[dIdx + 2] = Math.abs(sb - db);
3868
- out[dIdx + 3] = Math.abs(saByte - daByte);
4129
+ out[dIdx + 3] = dst.hasAlpha ? Math.abs(saByte - daByte) : daByte;
3869
4130
  continue;
3870
4131
  }
3871
4132
  if (norm === "in" || norm === "srcin") {
@@ -3919,10 +4180,10 @@ function* applyMagickCompositeLayerSteps(dst, src, modeRaw, left, top, composeAr
3919
4180
  out[dIdx] = clampByteVal(sr * sa * (1 - da) + dr * da * (1 - sa));
3920
4181
  out[dIdx + 1] = clampByteVal(sg * sa * (1 - da) + dg * da * (1 - sa));
3921
4182
  out[dIdx + 2] = clampByteVal(sb * sa * (1 - da) + db * da * (1 - sa));
3922
- out[dIdx + 3] = clampByteVal(outA * 255);
4183
+ out[dIdx + 3] = dst.hasAlpha ? clampByteVal(outA * 255) : daByte;
3923
4184
  continue;
3924
4185
  }
3925
- if (norm === "multiply" || norm === "screen" || norm === "overlay" || norm === "darken" || norm === "lighten" || norm === "exclusion") {
4186
+ if (norm === "multiply" || norm === "screen" || norm === "overlay" || norm === "hardlight" || norm === "darken" || norm === "lighten" || norm === "exclusion") {
3926
4187
  const gamma = sa + da - sa * da;
3927
4188
  const sChan = [sr / 255, sg / 255, sb / 255];
3928
4189
  const dChan = [dr / 255, dg / 255, db / 255];
@@ -3946,10 +4207,12 @@ function* applyMagickCompositeLayerSteps(dst, src, modeRaw, left, top, composeAr
3946
4207
  f = sc * dc;
3947
4208
  else if (norm === "screen")
3948
4209
  f = sc + dc - sc * dc;
4210
+ else if (norm === "hardlight")
4211
+ f = sc <= 0.5 ? 2 * sc * dc : 1 - 2 * (1 - sc) * (1 - dc);
3949
4212
  else
3950
4213
  f = dc <= 0.5 ? 2 * sc * dc : 1 - 2 * (1 - sc) * (1 - dc);
3951
4214
  const comp = f * sa * da + sc * sa * (1 - da) + dc * da * (1 - sa);
3952
- const unpremul = gamma > 1e-6 && norm === "overlay" ? comp / gamma : comp;
4215
+ const unpremul = gamma > 1e-6 && (norm === "overlay" || norm === "hardlight") ? comp / gamma : comp;
3953
4216
  out[dIdx + c] = clampByteVal(unpremul * 255);
3954
4217
  }
3955
4218
  }
@@ -3973,7 +4236,7 @@ function* applyMagickCompositeLayerSteps(dst, src, modeRaw, left, top, composeAr
3973
4236
  const clipped = yield* extractImageSteps(src, { left: sx0, top: sy0, width: sx1 - sx0, height: sy1 - sy0 });
3974
4237
  return yield* compositeImageSteps(dst, [rgbaToCompositeLayer(clipped, Math.max(0, ox), Math.max(0, oy), parseCompose(modeRaw))]);
3975
4238
  }
3976
- return { ...dst, data: out, hasAlpha: true };
4239
+ return { ...dst, data: out, hasAlpha: ["difference", "src", "source", "copy", "xor"].includes(norm) ? dst.hasAlpha : true };
3977
4240
  }
3978
4241
  function rgbaToCss(c) {
3979
4242
  if (c.a === 0) return "none";
@@ -4549,21 +4812,24 @@ function* appendStackImagesSteps(stack, vertical, state) {
4549
4812
  const totalW = vertical ? Math.max(...stack.map((im) => im.width)) : stack.reduce((acc, im) => acc + im.width, 0);
4550
4813
  const totalH = vertical ? stack.reduce((acc, im) => acc + im.height, 0) : Math.max(...stack.map((im) => im.height));
4551
4814
  const canvas = yield* createSolidRgbaImageSteps(totalW, totalH, state.background);
4815
+ let hasAlpha = canvas.hasAlpha;
4552
4816
  let cursor = 0;
4553
4817
  for (const im of stack) {
4554
4818
  if (++work % 16384 === 0)
4555
4819
  yield;
4556
- if (vertical) {
4557
- const off = resolveGravityOffset(totalW - im.width, 0, state.gravity);
4558
- yield* blitOverRgbaInPlaceSteps(canvas, im, off.left, cursor);
4559
- cursor += im.height;
4560
- } else {
4561
- const off = resolveGravityOffset(0, totalH - im.height, state.gravity);
4562
- yield* blitOverRgbaInPlaceSteps(canvas, im, cursor, off.top);
4563
- cursor += im.width;
4820
+ const off = resolveGravityOffset(vertical ? totalW - im.width : 0, vertical ? 0 : totalH - im.height, state.gravity);
4821
+ const left = vertical ? off.left : cursor, top = vertical ? cursor : off.top;
4822
+ for (let y = 0; y < im.height; y++) {
4823
+ for (let x = 0; x < im.width; x += 4096) {
4824
+ yield;
4825
+ const start = (y * im.width + x) * 4;
4826
+ canvas.data.set(im.data.subarray(start, start + Math.min(4096, im.width - x) * 4), ((top + y) * totalW + left + x) * 4);
4827
+ }
4564
4828
  }
4829
+ hasAlpha ||= im.hasAlpha;
4830
+ cursor += vertical ? im.height : im.width;
4565
4831
  }
4566
- return canvas;
4832
+ return { ...canvas, hasAlpha };
4567
4833
  }
4568
4834
  function inferOutputFormat(spec, fallback = "png") {
4569
4835
  const prefixMatch = /^([a-zA-Z0-9]+):(.*)$/.exec(spec);
@@ -5101,6 +5367,7 @@ function* accumulateMagickHistogram(data, histogram) {
5101
5367
  for (let i = 0; i < data.length; i += 4) {
5102
5368
  if (i && i % 65536 === 0) yield;
5103
5369
  for (let channel = 0; channel < 4; channel++) histogram[channel][data[i + channel]]++;
5370
+ if (histogram[4]) histogram[4][Math.round(257 * (0.212656 * data[i] + 0.715158 * data[i + 1] + 0.072186 * data[i + 2]))]++;
5104
5371
  }
5105
5372
  }
5106
5373
  function* applyMagickLookup(image, lookup) {
@@ -5122,9 +5389,9 @@ function parseMagickAnalysisOperation(tokens, state, start) {
5122
5389
  const token = tokens[start];
5123
5390
  if (!["-auto-level", "-auto-gamma", "-normalize", "-contrast-stretch", "-linear-stretch", "-equalize"].includes(token ?? "")) return;
5124
5391
  let end = start;
5125
- const argument = token === "-normalize" ? "2%x1%" : token === "-contrast-stretch" || token === "-linear-stretch" ? tokens[++end] ?? "0%x0%" : "";
5392
+ const argument = token === "-contrast-stretch" || token === "-linear-stretch" ? tokens[++end] ?? "0%x0%" : "";
5126
5393
  const independent = state.channelExplicit, channels = independent ? state.channels : { r: true, g: true, b: true, a: false };
5127
- return { end, lookup(histogram, image) {
5394
+ return { end, luminance: token === "-linear-stretch" || (token === "-normalize" || token === "-contrast-stretch" || token === "-equalize") && !independent, lookup(histogram, image) {
5128
5395
  const lookup = Array.from({ length: 4 }, () => Uint8Array.from({ length: 256 }, (_, value) => value));
5129
5396
  const count = Math.max(1, image.width * image.height);
5130
5397
  if (token === "-auto-gamma") {
@@ -5151,36 +5418,59 @@ function parseMagickAnalysisOperation(tokens, state, start) {
5151
5418
  const range = maxs[channel] - mins[channel];
5152
5419
  if (active[channel] && range > 0) for (let value = 0; value < 256; value++) lookup[channel][value] = clampByteVal((value - mins[channel]) / range * 255);
5153
5420
  }
5154
- } else if (token === "-equalize") {
5155
- for (let channel = 0; channel < 3; channel++) {
5156
- let cumulative = 0, first = 0;
5157
- for (let value = 0; value < 256; value++) {
5158
- cumulative += histogram[channel][value];
5159
- if (first === 0 && cumulative > 0) first = cumulative;
5160
- lookup[channel][value] = clampByteVal((cumulative - first) / Math.max(1, count - first) * 255);
5161
- }
5162
- }
5163
- } else {
5164
- const [black, white] = argument.split("x"), percentage = argument.includes("%"), blackValue = parseFloat(black || "0") || 0, whiteValue = white !== void 0 ? parseFloat(white) || 0 : blackValue;
5165
- const lowTarget = percentage ? blackValue / 100 * count : blackValue, highTarget = percentage ? (100 - whiteValue) / 100 * count : count - whiteValue;
5166
- for (let channel = 0; channel < 3; channel++) {
5167
- let low = 0, high = 255, sum = 0;
5168
- for (let value = 0; value < 256; value++) {
5169
- sum += histogram[channel][value];
5170
- if (sum > lowTarget) {
5171
- low = value;
5172
- break;
5173
- }
5421
+ } else if (token === "-linear-stretch") {
5422
+ const [black, white] = argument.split("x"), percentage = argument.includes("%");
5423
+ const lowTarget = (parseFloat(black || "0") || 0) * (percentage ? count / 100 : 1);
5424
+ const highTarget = white === void 0 ? count - lowTarget : (parseFloat(white) || 0) * (percentage ? count / 100 : 1);
5425
+ const bins = histogram[4];
5426
+ let low = 0, high = bins.length - 1, sum = 0;
5427
+ for (; low < bins.length - 1; low++) {
5428
+ sum += bins[low];
5429
+ if (sum >= lowTarget) break;
5430
+ }
5431
+ sum = 0;
5432
+ for (; high > 0; high--) {
5433
+ sum += bins[high];
5434
+ if (sum >= highTarget) break;
5435
+ }
5436
+ const active = [channels.r, channels.g, channels.b, (!independent || channels.a) && image.hasAlpha];
5437
+ for (let channel = 0; channel < 4; channel++) {
5438
+ if (!active[channel]) continue;
5439
+ for (let value = 0; value < 256; value++) lookup[channel][value] = high === low ? value * 257 > low ? 255 : 0 : clampByteVal((value * 257 - low) / (high - low) * 255);
5440
+ }
5441
+ } else if (token === "-normalize" || token === "-contrast-stretch") {
5442
+ const [black, white] = argument.split("x"), percentage = argument.includes("%");
5443
+ const blackValue = parseFloat(black || "0") || 0, whiteValue = white === void 0 ? blackValue : parseFloat(white) || 0;
5444
+ const lowTarget = token === "-normalize" ? 0.02 * count : percentage ? blackValue / 100 * count : blackValue;
5445
+ const highTarget = token === "-normalize" ? 0.01 * count : percentage ? whiteValue / 100 * count : whiteValue;
5446
+ const active = [channels.r, channels.g, channels.b, (!independent || channels.a) && image.hasAlpha];
5447
+ for (let channel = 0; channel < 4; channel++) {
5448
+ if (!active[channel]) continue;
5449
+ const bins = histogram[4] ?? histogram[channel], scale = histogram[4] ? 257 : 1;
5450
+ let low = 0, high = bins.length - 1, sum = 0;
5451
+ for (; low < bins.length - 1; low++) {
5452
+ sum += bins[low];
5453
+ if (sum > lowTarget) break;
5174
5454
  }
5175
5455
  sum = 0;
5456
+ for (; high > 0; high--) {
5457
+ sum += bins[high];
5458
+ if (sum > highTarget) break;
5459
+ }
5460
+ if (high > low) for (let value = 0; value < 256; value++) lookup[channel][value] = clampByteVal((value * scale - low) / (high - low) * 255);
5461
+ }
5462
+ } else if (token === "-equalize") {
5463
+ const active = [channels.r, channels.g, channels.b, (!independent || channels.a) && image.hasAlpha];
5464
+ for (let channel = 0; channel < 4; channel++) {
5465
+ if (!active[channel]) continue;
5466
+ const bins = histogram[4] ?? histogram[channel], scale = histogram[4] ? 257 : 1;
5467
+ const black = bins[0];
5468
+ if (count === black) continue;
5469
+ let cumulative = 0, bin = 0;
5176
5470
  for (let value = 0; value < 256; value++) {
5177
- sum += histogram[channel][value];
5178
- if (sum >= highTarget) {
5179
- high = value;
5180
- break;
5181
- }
5471
+ for (; bin <= value * scale; bin++) cumulative += bins[bin];
5472
+ lookup[channel][value] = clampByteVal((cumulative - black) / (count - black) * 255);
5182
5473
  }
5183
- if (high > low) for (let value = 0; value < 256; value++) lookup[channel][value] = clampByteVal((value - low) / (high - low) * 255);
5184
5474
  }
5185
5475
  }
5186
5476
  return lookup;
@@ -5189,7 +5479,25 @@ function parseMagickAnalysisOperation(tokens, state, start) {
5189
5479
  function parseMagickPixelOperation(tokens, settings, start, signal) {
5190
5480
  const state = { ...settings }, token = tokens[start];
5191
5481
  let i = start, apply;
5192
- if (token === "-alpha") {
5482
+ if (token === "-colorspace" || token === "-grayscale") {
5483
+ const space = (tokens[++i] ?? "gray").toLowerCase();
5484
+ apply = function* (image) {
5485
+ if (!space.includes("gray") && !space.includes("grey") && space !== "rec709luma" && space !== "rec601luma") return image;
5486
+ const out = new Uint8Array(image.data);
5487
+ for (let idx = 0; idx < out.length; idx += 4) {
5488
+ if (idx % 65536 === 0) yield;
5489
+ const gray = clampByteVal(space === "rec601luma" ? 0.298839 * out[idx] + 0.586811 * out[idx + 1] + 0.11435 * out[idx + 2] : 0.212656 * out[idx] + 0.715158 * out[idx + 1] + 0.072186 * out[idx + 2]);
5490
+ out[idx] = out[idx + 1] = out[idx + 2] = gray;
5491
+ }
5492
+ const { data16: ignored, ...metadata } = image;
5493
+ return { ...metadata, data: out, space: "b-w", channels: image.hasAlpha ? 2 : 1 };
5494
+ };
5495
+ } else if (token === "-gamma") {
5496
+ const gamma = Math.max(0.1, Number(tokens[++i] ?? 1));
5497
+ const channels = state.channelExplicit ? state.channels : { r: true, g: true, b: true, a: true };
5498
+ const lookup = [channels.r, channels.g, channels.b, channels.a].map((active) => Uint8Array.from({ length: 256 }, (_, value) => active ? clampByteVal(255 * Math.pow(value / 255, 1 / gamma)) : value));
5499
+ apply = (image) => applyMagickLookup(image, lookup);
5500
+ } else if (token === "-alpha") {
5193
5501
  const mode = (tokens[++i] ?? "on").toLowerCase();
5194
5502
  if (mode === "remove") {
5195
5503
  apply = function* (im) {
@@ -5253,10 +5561,10 @@ function parseMagickPixelOperation(tokens, settings, start, signal) {
5253
5561
  apply = (image) => applyMagickColorMatrixSteps(image, matrix, signal);
5254
5562
  } else if (token === "-opaque" || token === "+opaque") {
5255
5563
  const target = parseColor(tokens[++i] ?? "#000000");
5256
- apply = (image) => applyMagickOpaqueSteps(image, target, state.fill, state.fuzz, token === "+opaque", signal);
5564
+ apply = (image) => applyMagickOpaqueSteps(image, target, state.fill, state.alphaFuzz, token === "+opaque", signal);
5257
5565
  } else if (token === "-transparent" || token === "+transparent") {
5258
5566
  const target = parseColor(tokens[++i] ?? "#ffffff");
5259
- apply = (image) => applyMagickTransparentSteps(image, target, state.fuzz, token === "+transparent", signal);
5567
+ apply = (image) => applyMagickTransparentSteps(image, target, state.alphaFuzz, token === "+transparent", signal);
5260
5568
  } else if (token === "-evaluate") {
5261
5569
  const operation = tokens[++i] ?? "Add", value = tokens[++i] ?? "0";
5262
5570
  apply = (image) => applyMagickEvaluateSteps(image, operation, value, state.channels, signal);
@@ -5266,7 +5574,7 @@ function parseMagickPixelOperation(tokens, settings, start, signal) {
5266
5574
  } else if (token === "-sepia-tone" || token === "-solarize") {
5267
5575
  const value = tokens[++i] ?? (token === "-sepia-tone" ? "80%" : "50%");
5268
5576
  apply = (image) => token === "-sepia-tone" ? applyMagickSepiaToneSteps(image, value, signal) : applyMagickSolarizeSteps(image, value, signal);
5269
- } else if (token === "-posterize" || token === "-colors") {
5577
+ } else if (token === "-posterize") {
5270
5578
  const levels = Number(tokens[++i] ?? 8);
5271
5579
  apply = (image) => applyMagickPosterizeSteps(image, levels, signal);
5272
5580
  } else if (token === "-monochrome") {
@@ -5275,17 +5583,83 @@ function parseMagickPixelOperation(tokens, settings, start, signal) {
5275
5583
  };
5276
5584
  } else if (token === "-modulate") {
5277
5585
  const parts = (tokens[++i] ?? "100,100,100").split(",").map(Number);
5278
- const options = { brightness: (parts[0] ?? 100) / 100, saturation: (parts[1] ?? 100) / 100, hue: ((parts[2] ?? 100) - 100) * 1.8, lightness: 0 };
5279
- apply = (image) => modulateImageSteps(image, options);
5586
+ const brightness = (parts[0] ?? 100) / 100, saturation = (parts[1] ?? 100) / 100, hueShift = ((parts[2] ?? 100) - 100) * 3 / 100;
5587
+ apply = function* (image) {
5588
+ const out = new Uint8Array(image.data);
5589
+ for (let idx = 0; idx < out.length; idx += 4) {
5590
+ if (idx % 65536 === 0) {
5591
+ signal?.throwIfAborted();
5592
+ yield;
5593
+ }
5594
+ const r = out[idx] / 255, g = out[idx + 1] / 255, b = out[idx + 2] / 255;
5595
+ const max = Math.max(r, g, b), min = Math.min(r, g, b), delta = max - min;
5596
+ const lightness = (max + min) / 2;
5597
+ const sat = delta === 0 ? 0 : delta / (lightness <= 0.5 ? max + min : 2 - max - min);
5598
+ let hue = delta === 0 ? 0 : max === r ? (g - b) / delta : max === g ? 2 + (b - r) / delta : 4 + (r - g) / delta;
5599
+ hue = ((hue + hueShift) % 6 + 6) % 6;
5600
+ const light = lightness * brightness;
5601
+ const chroma = (1 - Math.abs(2 * light - 1)) * sat * saturation;
5602
+ const secondary = chroma * (1 - Math.abs(hue % 2 - 1));
5603
+ const offset = light - chroma / 2;
5604
+ const red = hue < 1 || hue >= 5 ? chroma : hue < 2 || hue >= 4 ? secondary : 0;
5605
+ const green = hue < 1 ? secondary : hue < 3 ? chroma : hue < 4 ? secondary : 0;
5606
+ const blue = hue < 2 ? 0 : hue < 3 ? secondary : hue < 5 ? chroma : secondary;
5607
+ out[idx] = clampByteVal((red + offset) * 255);
5608
+ out[idx + 1] = clampByteVal((green + offset) * 255);
5609
+ out[idx + 2] = clampByteVal((blue + offset) * 255);
5610
+ }
5611
+ const { data16: ignored, ...metadata } = image;
5612
+ return { ...metadata, data: out };
5613
+ };
5280
5614
  } else if (token === "-brightness-contrast") {
5281
- const geometry = parseMagickGeometry(tokens[++i] ?? "0x0"), slope = 1 + (geometry.height ?? 0) / 100, offset = (geometry.width ?? 0) / 100 * 255;
5282
- apply = (image) => linearImageSteps(image, [slope], [offset]);
5615
+ const [brightness = 0, contrast = 0] = (tokens[++i] ?? "0x0").split("x").map((value) => parseFloat(value) || 0);
5616
+ const slope = contrast < 0 ? 1 + contrast / 100 : 100 / Math.max(1e-12, 100 - contrast);
5617
+ const offset = (brightness / 100 * 255 - 127.5) * slope + 127.5;
5618
+ const channels = state.channelExplicit ? state.channels : { r: true, g: true, b: true, a: true };
5619
+ apply = function* (image) {
5620
+ const out = new Uint8Array(image.data);
5621
+ const mask = [channels.r, channels.g, channels.b, channels.a && image.hasAlpha];
5622
+ for (let idx = 0; idx < out.length; idx += 4) {
5623
+ if (idx % 65536 === 0) yield;
5624
+ for (let channel = 0; channel < 4; channel++) {
5625
+ if (mask[channel]) out[idx + channel] = clampByteVal(out[idx + channel] * slope + offset);
5626
+ }
5627
+ }
5628
+ return { ...image, data: out };
5629
+ };
5283
5630
  } else if (token === "-threshold") {
5284
5631
  const raw = tokens[++i] ?? "50%", value = Math.round(raw.endsWith("%") ? parseFloat(raw) / 100 * 255 : parseFloat(raw));
5285
5632
  apply = (image) => thresholdImageSteps(image, value, true);
5286
- } else if (token === "-tint" || token === "-colorize") {
5287
- i++;
5288
- apply = (image) => tintImageSteps(image, state.fill);
5633
+ } else if (token === "-colorize") {
5634
+ const strength = parseFloat(tokens[++i] ?? "100") / 100;
5635
+ const fill = [state.fill.r, state.fill.g, state.fill.b];
5636
+ apply = function* (image) {
5637
+ const out = new Uint8Array(image.data);
5638
+ for (let idx = 0; idx < out.length; idx += 4) {
5639
+ if (idx % 65536 === 0) yield;
5640
+ for (let channel = 0; channel < 3; channel++) {
5641
+ out[idx + channel] = clampByteVal(out[idx + channel] * (1 - strength) + fill[channel] * strength);
5642
+ }
5643
+ }
5644
+ return { ...image, data: out };
5645
+ };
5646
+ } else if (token === "-tint") {
5647
+ const strength = parseFloat(tokens[++i] ?? "100") / 100;
5648
+ const fill = [state.fill.r, state.fill.g, state.fill.b];
5649
+ const intensity = 0.212656 * state.fill.r + 0.715158 * state.fill.g + 0.072186 * state.fill.b;
5650
+ const vector = fill.map((channel) => channel * strength - intensity);
5651
+ apply = function* (image) {
5652
+ const out = new Uint8Array(image.data);
5653
+ for (let idx = 0; idx < out.length; idx += 4) {
5654
+ if (idx % 65536 === 0) yield;
5655
+ for (let channel = 0; channel < 3; channel++) {
5656
+ const value = out[idx + channel];
5657
+ const weight = value / 255 - 0.5;
5658
+ out[idx + channel] = clampByteVal(value + vector[channel] * (1 - 4 * weight * weight));
5659
+ }
5660
+ }
5661
+ return { ...image, data: out };
5662
+ };
5289
5663
  } else if (token === "-negate" || token === "+negate") {
5290
5664
  const onlyGray = token === "+negate";
5291
5665
  const ch = state.channelExplicit ? state.channels : { r: true, g: true, b: true, a: false };
@@ -5538,10 +5912,16 @@ function* evaluatePipelineTokensSteps(tokens, files, state, parentStack = [], st
5538
5912
  } else if (analysisOperation) {
5539
5913
  i = analysisOperation.end;
5540
5914
  stack = yield* mapSteps(stack, function* (image) {
5541
- const histogram = Array.from({ length: 4 }, () => new Float64Array(256));
5915
+ const histogram = Array.from({ length: analysisOperation.luminance ? 5 : 4 }, (_, channel) => new Float64Array(channel === 4 ? 65536 : 256));
5542
5916
  yield* accumulateMagickHistogram(image.data, histogram);
5543
5917
  return yield* applyMagickLookup(image, analysisOperation.lookup(histogram, image));
5544
5918
  });
5919
+ } else if (t === "-colors") {
5920
+ const colors = Number(tokens[++i]);
5921
+ stack = yield* mapSteps(stack, (image) => {
5922
+ const { data16: ignored, ...pixels } = image;
5923
+ return applyMagickRasterSteps(pixels, quantizePixelSteps(image.data.length, colors), signal);
5924
+ });
5545
5925
  } else if (pixelOperation) {
5546
5926
  i = pixelOperation.end;
5547
5927
  stack = yield* mapSteps(stack, pixelOperation.apply);
@@ -5765,18 +6145,6 @@ function* evaluatePipelineTokensSteps(tokens, files, state, parentStack = [], st
5765
6145
  stack = yield* mapSteps(stack, function* (im) {
5766
6146
  return yield* applyExifOrientationSteps(im);
5767
6147
  });
5768
- } else if (t === "-colorspace" || t === "-grayscale") {
5769
- const cs = (tokens[++i] ?? "gray").toLowerCase();
5770
- if (cs.includes("gray") || cs.includes("grey") || cs === "rec709luma" || cs === "rec601luma") {
5771
- stack = yield* mapSteps(stack, function* (im) {
5772
- return yield* grayscaleImageSteps(im);
5773
- });
5774
- }
5775
- } else if (t === "-gamma") {
5776
- const gammaVal = Math.max(0.1, Number(tokens[++i] ?? 1));
5777
- stack = yield* mapSteps(stack, function* (im) {
5778
- return yield* gammaImageSteps(im, gammaVal, gammaVal);
5779
- });
5780
6148
  } else if (t === "-raise" || t === "+raise") {
5781
6149
  const g = parseMagickGeometry(tokens[++i] ?? "4");
5782
6150
  const bw = Math.max(1, Math.round(g.width ?? 4));
@@ -5979,14 +6347,19 @@ function* evaluatePipelineTokensSteps(tokens, files, state, parentStack = [], st
5979
6347
  }
5980
6348
  stack = [appended];
5981
6349
  } else if (t === "-flatten" || t === "-mosaic" || t === "-layers" && ["flatten", "merge", "mosaic"].includes((tokens[i + 1] ?? "").toLowerCase())) {
6350
+ const flatten = t === "-flatten" || t === "-layers" && tokens[i + 1]?.toLowerCase() === "flatten";
5982
6351
  if (t === "-layers")
5983
6352
  i++;
5984
6353
  if (stack.length > 0) {
5985
6354
  const oldStack = stack;
5986
- const maxW = Math.max(...oldStack.map((im) => im.width));
5987
- const maxH = Math.max(...oldStack.map((im) => im.height));
6355
+ const maxW = flatten ? oldStack[0].width : Math.max(...oldStack.map((im) => im.width));
6356
+ const maxH = flatten ? oldStack[0].height : Math.max(...oldStack.map((im) => im.height));
5988
6357
  const canvas = yield* createSolidRgbaImageSteps(maxW, maxH, state.background);
5989
- const layers = oldStack.map((im) => rgbaToCompositeLayer(im, 0, 0, state.compose));
6358
+ const layers = [];
6359
+ for (const im of oldStack) {
6360
+ const clipped = im.width > maxW || im.height > maxH ? yield* extractImageSteps(im, { left: 0, top: 0, width: Math.min(im.width, maxW), height: Math.min(im.height, maxH) }) : im;
6361
+ layers.push(rgbaToCompositeLayer(clipped, 0, 0, state.compose));
6362
+ }
5990
6363
  const flattened = yield* compositeImageSteps(canvas, layers);
5991
6364
  if (canvas.data.buffer !== flattened.data.buffer) detachRgbaBuffer(canvas.data);
5992
6365
  for (const im of oldStack) {
@@ -6285,7 +6658,8 @@ async function transformStoredMagickCoordinates(image, backend, token, geometry,
6285
6658
  return { ...image, position, width, height, ...token === "-splice" ? { hasAlpha: true } : {} };
6286
6659
  }
6287
6660
  async function compositeStoredMagick(base, overlay, backend, signal, left, top, mode, args, generic = false, inPlace = false, montageRounding = false) {
6288
- let hasAlpha = base.hasAlpha || !generic && !magickCompositeMode(mode).delegated;
6661
+ const compositeMode = magickCompositeMode(mode);
6662
+ let hasAlpha = base.hasAlpha || !generic && !compositeMode.delegated && !["difference", "src", "source", "copy", "xor"].includes(compositeMode.name);
6289
6663
  const position = inPlace ? base.position : backend.storage.allocate(base.width * base.height * 4);
6290
6664
  for (let start = 0; !inPlace && start < base.width * base.height * 4; start += 16384) {
6291
6665
  await yieldTurn9(signal);
@@ -6486,6 +6860,11 @@ async function tryConvertFilesResult(argv, input, stdinBytes, signal, analysis)
6486
6860
  });
6487
6861
  continue;
6488
6862
  }
6863
+ if (!operandsOnly && token === "-colors") {
6864
+ const colors = Number(tokens[++i]);
6865
+ steps.push(async (image, backend) => image ? transformStoredMagickRaster(image, backend, quantizePixelSteps(image.width * image.height * 4, colors), signal) : void 0);
6866
+ continue;
6867
+ }
6489
6868
  const pixelOperation = operandsOnly ? void 0 : parseMagickPixelOperation(tokens, state, i, signal);
6490
6869
  if (pixelOperation) {
6491
6870
  i = pixelOperation.end;
@@ -6508,7 +6887,7 @@ async function tryConvertFilesResult(argv, input, stdinBytes, signal, analysis)
6508
6887
  i = analysisOperation.end;
6509
6888
  steps.push(async (image, backend) => {
6510
6889
  if (!image) return;
6511
- const histogram = Array.from({ length: 4 }, () => new Float64Array(256)), size = image.width * image.height * 4;
6890
+ const histogram = Array.from({ length: analysisOperation.luminance ? 5 : 4 }, (_, channel) => new Float64Array(channel === 4 ? 65536 : 256)), size = image.width * image.height * 4;
6512
6891
  for (let offset = 0; offset < size; offset += 16384) {
6513
6892
  if (offset % 1048576 === 0) await yieldTurn9(signal);
6514
6893
  await drainSteps(accumulateMagickHistogram(await backend.storage.read(image.position + offset, Math.min(16384, size - offset)), histogram), signal);
@@ -6519,6 +6898,7 @@ async function tryConvertFilesResult(argv, input, stdinBytes, signal, analysis)
6519
6898
  continue;
6520
6899
  }
6521
6900
  if (!operandsOnly && (["-append", "+append", "-flatten", "-mosaic", "-composite"].includes(token) || token === "-layers" && ["flatten", "merge", "mosaic"].includes((tokens[i + 1] ?? "").toLowerCase()))) {
6901
+ const flatten = token === "-flatten" || token === "-layers" && tokens[i + 1]?.toLowerCase() === "flatten";
6522
6902
  if (token === "-layers") i++;
6523
6903
  const settings = { ...state };
6524
6904
  stackStep(async (stack, backend) => {
@@ -6543,7 +6923,7 @@ async function tryConvertFilesResult(argv, input, stdinBytes, signal, analysis)
6543
6923
  const append = token.endsWith("append"), vertical = token === "-append";
6544
6924
  if (append && stack.length === 1) return stack;
6545
6925
  let width = 0, height = 0;
6546
- for (let n = 0; n < stack.length; n++) {
6926
+ for (let n = 0; n < (flatten ? 1 : stack.length); n++) {
6547
6927
  await yieldTurn9(signal);
6548
6928
  const frame = await stack.get(n);
6549
6929
  width = append && !vertical ? width + frame.width : Math.max(width, frame.width);
@@ -6553,7 +6933,17 @@ async function tryConvertFilesResult(argv, input, stdinBytes, signal, analysis)
6553
6933
  let cursor = 0;
6554
6934
  for (let n = 0; n < stack.length; n++) {
6555
6935
  const frame = await stack.get(n), offset = resolveGravityOffset(vertical ? width - frame.width : 0, vertical ? 0 : height - frame.height, settings.gravity);
6556
- result = await compositeStoredMagick(result, frame, backend, signal, append ? vertical ? offset.left : cursor : 0, append ? vertical ? cursor : offset.top : 0, append ? "over" : settings.compose, void 0, !append, true);
6936
+ if (append) {
6937
+ const left = vertical ? offset.left : cursor, top = vertical ? cursor : offset.top;
6938
+ for (let y = 0; y < frame.height; y++) {
6939
+ for (let x = 0; x < frame.width; x += 4096) {
6940
+ await yieldTurn9(signal);
6941
+ const pixels = await backend.storage.read(frame.position + (y * frame.width + x) * 4, Math.min(4096, frame.width - x) * 4);
6942
+ await backend.storage.write(result.position + ((top + y) * width + left + x) * 4, pixels);
6943
+ }
6944
+ }
6945
+ result = { ...result, hasAlpha: result.hasAlpha || frame.hasAlpha };
6946
+ } else result = await compositeStoredMagick(result, frame, backend, signal, 0, 0, settings.compose, void 0, true, true);
6557
6947
  cursor += vertical ? frame.height : frame.width;
6558
6948
  }
6559
6949
  stack.length = 0;
@@ -6769,12 +7159,6 @@ async function tryConvertFilesResult(argv, input, stdinBytes, signal, analysis)
6769
7159
  } else if (!operandsOnly && token === "-rotate") {
6770
7160
  const angle = Number(tokens[++i] ?? 0), background = state.background;
6771
7161
  transform(() => ({ kind: "rotate", angle, background }));
6772
- } else if (!operandsOnly && token === "-gamma") {
6773
- const gamma = Math.max(0.1, Number(tokens[++i] ?? 1));
6774
- transform(() => ({ kind: "gamma", gamma, gammaOut: gamma }));
6775
- } else if (!operandsOnly && ["-colorspace", "-grayscale"].includes(token)) {
6776
- const space = (tokens[++i] ?? "gray").toLowerCase();
6777
- if (space.includes("gray") || space.includes("grey") || space === "rec709luma" || space === "rec601luma") transform(() => ({ kind: "grayscale" }));
6778
7162
  } else if (!operandsOnly && ["-blur", "-gaussian-blur", "-sharpen", "-unsharp"].includes(token)) {
6779
7163
  const geometry = parseMagickGeometry(tokens[++i] ?? "0x1"), sigma = Math.max(0.3, geometry.height ?? geometry.width ?? 1);
6780
7164
  const kind = token === "-blur" || token === "-gaussian-blur" ? "blur" : "sharpen";