@poe-platform/safe-bash 0.1.844 → 0.1.845

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.
@@ -13,7 +13,7 @@ import {
13
13
  scanMp4Frames,
14
14
  scanMp4Packets,
15
15
  wavPacketFrames
16
- } from "./chunk-LORSYIAF.js";
16
+ } from "./chunk-CMGVQM4Y.js";
17
17
 
18
18
  // packages/safe-bash-command-ffprobe/src/read-intervals.ts
19
19
  import { PagedStorage } from "@poe-platform/safe-fs/storage";
@@ -399724,19 +399724,29 @@ function serializeWav(doc, options = {}) {
399724
399724
  } else {
399725
399725
  throw new Error("WAV encoding requires decoded PCM audio");
399726
399726
  }
399727
+ const channelMask = audioTrack?.decodedAudio?.channelMask ?? (decodedAudio ? decodedAudio.channelMask : void 0) ?? [11, 263, 55, 63, 1807, 1599][channels - 3];
399728
+ const extensible = channelMask !== void 0 || channels > 2;
399729
+ const fmtSize = extensible ? 40 : 16;
399727
399730
  const padding = pcmBytes.byteLength % 2;
399728
- const writer = new BinaryWriter(44 + pcmBytes.byteLength + padding);
399731
+ const writer = new BinaryWriter(28 + fmtSize + pcmBytes.byteLength + padding);
399729
399732
  writer.writeFourCC("RIFF");
399730
- writer.writeU32LE(36 + pcmBytes.byteLength + padding);
399733
+ writer.writeU32LE(20 + fmtSize + pcmBytes.byteLength + padding);
399731
399734
  writer.writeFourCC("WAVE");
399732
399735
  writer.writeFourCC("fmt ");
399733
- writer.writeU32LE(16);
399734
- writer.writeU16LE(formatTag);
399736
+ writer.writeU32LE(fmtSize);
399737
+ writer.writeU16LE(extensible ? 65534 : formatTag);
399735
399738
  writer.writeU16LE(channels);
399736
399739
  writer.writeU32LE(sampleRate);
399737
399740
  writer.writeU32LE(sampleRate * channels * (bitsPerSample >>> 3));
399738
399741
  writer.writeU16LE(channels * (bitsPerSample >>> 3));
399739
399742
  writer.writeU16LE(bitsPerSample);
399743
+ if (extensible) {
399744
+ writer.writeU16LE(22);
399745
+ writer.writeU16LE(bitsPerSample);
399746
+ writer.writeU32LE(channelMask ?? 0);
399747
+ writer.writeU32LE(formatTag);
399748
+ writer.writeBytes(Uint8Array.of(0, 0, 16, 0, 128, 0, 0, 170, 0, 56, 155, 113));
399749
+ }
399740
399750
  writer.writeFourCC("data");
399741
399751
  writer.writeU32LE(pcmBytes.byteLength);
399742
399752
  writer.writeBytes(pcmBytes);
@@ -23,7 +23,7 @@ import {
23
23
  sliceMp4,
24
24
  srtAst,
25
25
  webvttAst
26
- } from "../safe-bash/command-chunks/chunk-LORSYIAF.js";
26
+ } from "../safe-bash/command-chunks/chunk-CMGVQM4Y.js";
27
27
  import "../safe-bash/command-chunks/chunk-65DS4IOB.js";
28
28
 
29
29
  // packages/safe-bash-command-ffmpeg/src/index.ts
@@ -78,6 +78,73 @@ async function resampleAudio(channels, outputLength, rateRatio, budget, signal)
78
78
 
79
79
  // packages/safe-bash-command-ffmpeg/src/audio-tempo.ts
80
80
  import { yieldTurn as yieldTurn2 } from "../safe-bash-contracts/yield.js";
81
+ async function filterUnityTempo(channels, sampleRate, pcm16, budget, signal) {
82
+ const length = channels[0]?.length ?? 0;
83
+ const window = 2 ** Math.ceil(Math.log2(Math.max(2, Math.floor(sampleRate / 24))));
84
+ const hop = window / 2;
85
+ if (length < hop) return channels.map(() => new Float32Array(0));
86
+ const hann = Float32Array.from({ length: window }, (_, i) => (1 - Math.cos(2 * Math.PI * i / (window - 1))) / 2);
87
+ const mono = new Float32Array(length);
88
+ for (let i = 0; i < length; i++) {
89
+ if (i % 16384 === 0) {
90
+ await yieldTurn2(signal);
91
+ signal?.throwIfAborted();
92
+ budget.checkCpu();
93
+ }
94
+ for (const channel of channels) {
95
+ if (Math.abs(channel[i]) > Math.abs(mono[i])) mono[i] = channel[i];
96
+ }
97
+ }
98
+ const output = channels.map(() => []);
99
+ let previous = -hop;
100
+ let position = -hop;
101
+ let work = 0;
102
+ while (true) {
103
+ let current = previous + hop;
104
+ position += hop;
105
+ const drift = position - hop - previous;
106
+ const start = Math.min(window, Math.max(0, -drift));
107
+ const end = Math.max(0, Math.min(window - drift, window - window / 16));
108
+ let correction = -drift;
109
+ let best = -Infinity;
110
+ for (let lag = start; lag < end; lag++) {
111
+ let correlation = 0;
112
+ for (let j = Math.max(0, -current); j < Math.min(window - lag, length - current, length - previous - lag); j++) {
113
+ correlation += (mono[previous + j + lag] ?? 0) * mono[current + j];
114
+ if (++work % 16384 === 0) {
115
+ await yieldTurn2(signal);
116
+ signal?.throwIfAborted();
117
+ budget.checkCpu();
118
+ }
119
+ }
120
+ const metric = correlation * (drift + lag) * (lag - start) * (end - lag);
121
+ if (metric > best) {
122
+ best = metric;
123
+ correction = lag - hop;
124
+ }
125
+ }
126
+ current -= correction;
127
+ const count = Math.max(0, Math.min(window, length - current));
128
+ const overlap = Math.min(hop, count, length - previous - hop);
129
+ for (let c = 0; c < channels.length; c++) {
130
+ const channel = channels[c];
131
+ const dst = output[c];
132
+ for (let i = 0; i < overlap; i++) {
133
+ const a = channel[previous + hop + i] ?? 0;
134
+ const b = channel[current + i] ?? 0;
135
+ const sample = current + i < 0 ? a : Math.fround(a * hann[hop + i] + Math.fround(b * hann[i]));
136
+ dst.push(pcm16 ? Math.trunc(sample * 32768) / 32768 : sample);
137
+ }
138
+ if (current + count >= length) {
139
+ for (let i = overlap; i < count; i++) dst.push(channel[current + i] ?? 0);
140
+ }
141
+ }
142
+ budget.checkDuration((output[0]?.length ?? 0) / sampleRate);
143
+ if (current + count >= length) break;
144
+ previous = current;
145
+ }
146
+ return output.map((channel) => Float32Array.from(channel));
147
+ }
81
148
  async function stretchAudio(channels, sampleRate, tempo, budget, signal) {
82
149
  const length = channels[0]?.length ?? 0;
83
150
  if (tempo === 1 || length === 0) return channels;
@@ -1411,8 +1478,10 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1411
1478
  if (!Number.isSafeInteger(val) || val < 0) return void 0;
1412
1479
  if (args.slice(i + 1).includes("-i")) pendingStartNumber = val;
1413
1480
  else outputStartNumber = val;
1414
- } else if (arg === "-ar") audioRate = parseInt(args[++i] ?? "0", 10) || void 0;
1415
- else if (arg === "-ac") audioChannels = parseInt(args[++i] ?? "0", 10) || void 0;
1481
+ } else if (arg === "-ar") {
1482
+ audioRate = parseInt(args[++i] ?? "0", 10) || void 0;
1483
+ if (audioRate !== void 0 && audioRate < 0) return void 0;
1484
+ } else if (arg === "-ac") audioChannels = parseInt(args[++i] ?? "0", 10) || void 0;
1416
1485
  else if (arg === "-hide_banner" || arg === "-nostdin" || arg === "-stats") continue;
1417
1486
  else if (arg === "-f") {
1418
1487
  const val = args[++i];
@@ -1483,7 +1552,7 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
1483
1552
  } else if (arg === "-vf" || arg === "-filter:v") {
1484
1553
  const f = args[++i];
1485
1554
  if (f) vfFilters.push(f);
1486
- } else if (arg === "-af" || arg === "-filter:a") {
1555
+ } else if (arg === "-af" || arg === "-filter:a" || arg.startsWith("-af:") || arg.startsWith("-filter:a:")) {
1487
1556
  return void 0;
1488
1557
  } else if (arg === "-disposition" || arg.startsWith("-disposition:")) {
1489
1558
  streamDispositions.push([arg.slice("-disposition:".length), args[++i] ?? ""]);
@@ -1778,6 +1847,10 @@ function createFfmpegCommand(options = {}) {
1778
1847
  else outputStartNumber = val;
1779
1848
  } else if (arg === "-ar") {
1780
1849
  audioRate = parseInt(args[++i] ?? "0", 10) || void 0;
1850
+ if (audioRate !== void 0 && audioRate < 0) {
1851
+ await writeBytes(context.stderr, encodeUtf8("ffmpeg: sample rate must be nonnegative\n"), context.signal);
1852
+ return { exitCode: 1 };
1853
+ }
1781
1854
  } else if (arg === "-ac") {
1782
1855
  audioChannels = parseInt(args[++i] ?? "0", 10) || void 0;
1783
1856
  } else if (arg === "-hide_banner" || arg === "-nostdin" || arg === "-stats") {
@@ -1890,9 +1963,14 @@ function createFfmpegCommand(options = {}) {
1890
1963
  } else if (arg === "-vf" || arg === "-filter:v") {
1891
1964
  const f = args[++i];
1892
1965
  if (f) vfFilters.push(f);
1893
- } else if (arg === "-af" || arg === "-filter:a") {
1966
+ } else if (arg === "-af" || arg === "-filter:a" || arg.startsWith("-af:") || arg.startsWith("-filter:a:")) {
1894
1967
  const f = args[++i];
1895
- if (f) afFilters.push(f);
1968
+ const selector = arg.startsWith("-af:") ? arg.slice(4) : arg.startsWith("-filter:a:") ? arg.slice(10) : void 0;
1969
+ const ordinal = selector === void 0 ? void 0 : Number(selector);
1970
+ if (selector !== void 0 && (!selector || !Array.from(selector).every((char) => char >= "0" && char <= "9") || !Number.isSafeInteger(ordinal))) {
1971
+ throw new Error(`Unsupported audio stream specifier: ${selector}`);
1972
+ }
1973
+ if (f) afFilters.push({ ordinal, expression: f });
1896
1974
  } else if (arg === "-filter_complex" || arg === "-lavfi") {
1897
1975
  filterComplex = args[++i];
1898
1976
  } else if (arg === "-disposition" || arg.startsWith("-disposition:")) {
@@ -2430,7 +2508,8 @@ To ignore this, add a trailing '?' to the map.
2430
2508
  }, context.signal);
2431
2509
  }
2432
2510
  validateAudioEncoder(audioCodec, workingDoc, astPlugins);
2433
- if (audioCodec === "copy" && afFilters.length > 0 && workingDoc.tracks.some((track) => track.type === "audio")) {
2511
+ const audioTrackCount = workingDoc.tracks.filter((track) => track.type === "audio").length;
2512
+ if (audioCodec === "copy" && audioTrackCount > 0 && afFilters.some((filter) => filter.ordinal === void 0 || filter.ordinal < audioTrackCount)) {
2434
2513
  await writeBytes(context.stderr, encodeUtf8("ffmpeg: Filtering and streamcopy cannot be used together for audio.\n"), context.signal);
2435
2514
  return { exitCode: 218 };
2436
2515
  }
@@ -2538,11 +2617,15 @@ To ignore this, add a trailing '?' to the map.
2538
2617
  workingDoc = limitAudioCopyPackets(workingDoc, maxAudioFrames);
2539
2618
  }
2540
2619
  if ((audioRate !== void 0 || audioChannels !== void 0 || afFilters.length > 0) && audioCodec !== "copy") {
2620
+ let audioOrdinal = 0;
2541
2621
  workingDoc = {
2542
2622
  ...workingDoc,
2543
2623
  tracks: await mapWork(workingDoc.tracks, async (t) => {
2544
2624
  let work = 0;
2545
2625
  if (t.type !== "audio") return t;
2626
+ const ordinal = audioOrdinal++;
2627
+ const filter = afFilters.filter((filter2) => filter2.ordinal === void 0 || filter2.ordinal === ordinal).at(-1);
2628
+ if (audioRate === void 0 && audioChannels === void 0 && !filter) return t;
2546
2629
  const audio = t.decodedAudio ?? (t.codecDescriptions[0]?.codecName === "aac" ? decodeAacTrack(workingDoc, t) : void 0);
2547
2630
  if (!audio) throw new Error("Audio filtering requires decoded PCM audio");
2548
2631
  const origRate = audio.sampleRate;
@@ -2552,19 +2635,15 @@ To ignore this, add a trailing '?' to the map.
2552
2635
  const channelMask = audio.channelMask;
2553
2636
  const surroundMono = origCh === 6 && targetCh === 1;
2554
2637
  const monoStereo = origCh === 1 && (targetCh === 2 || targetCh === 3);
2638
+ const pcm16MonoStereo = monoStereo && !filter && targetRate === origRate && t.codecDescriptions[0]?.codecName === "pcm_s16le" && (audioCodec === "pcm_s16le" || audioCodec === void 0 && (outputFormat ?? outputTarget2.split(".").pop()?.toLowerCase()) === "wav");
2639
+ const monoSurround = origCh === 1 && targetCh >= 4 && targetCh <= 8;
2555
2640
  if (targetCh !== origCh && origCh > 2 && (!surroundMono || channelMask && channelMask !== 63 && channelMask !== 1551)) {
2556
2641
  throw new Error(`Unsupported audio channel layout conversion: ${origCh} channels (mask ${channelMask ?? 0}) to ${targetCh}`);
2557
2642
  }
2558
- const durSec = (audio.channelData[0]?.length ?? 0) / origRate;
2559
- let volFactor = t.volume ?? 1;
2560
- let tempoFactor = 1;
2561
- let reverseAudio = false;
2562
- for (const af of afFilters) {
2563
- if (++work % 1024 === 0) {
2564
- await yieldTurn5(context.signal);
2565
- context.signal?.throwIfAborted();
2566
- }
2567
- for (const item of af.split(",")) {
2643
+ const volFactor = t.volume ?? 1;
2644
+ let stretched = audio.channelData;
2645
+ if (filter) {
2646
+ for (const item of filter.expression.split(",")) {
2568
2647
  if (++work % 1024 === 0) {
2569
2648
  await yieldTurn5(context.signal);
2570
2649
  context.signal?.throwIfAborted();
@@ -2576,22 +2655,31 @@ To ignore this, add a trailing '?' to the map.
2576
2655
  const gain = parseFilterExpression(decibels ? volSpec.slice(0, -2) : volSpec, "volume", {});
2577
2656
  const factor = decibels ? Math.pow(10, gain / 20) : gain;
2578
2657
  if (!Number.isFinite(factor)) throw new Error("volume: invalid gain");
2579
- volFactor *= factor;
2658
+ stretched = await mapWork(stretched, async (channel) => {
2659
+ const output = new Float32Array(channel.length);
2660
+ for (let i2 = 0; i2 < channel.length; i2++) {
2661
+ if (++work % 1024 === 0) {
2662
+ await yieldTurn5(context.signal);
2663
+ context.signal?.throwIfAborted();
2664
+ }
2665
+ output[i2] = channel[i2] * factor;
2666
+ }
2667
+ return output;
2668
+ }, context.signal);
2580
2669
  } else if (trimmed.startsWith("atempo=")) {
2581
2670
  const tempo = Number(trimmed.slice(7));
2582
2671
  if (!Number.isFinite(tempo) || tempo < 0.5 || tempo > 100) {
2583
2672
  throw new Error("atempo must be in the range [0.5, 100]");
2584
2673
  }
2585
- tempoFactor *= tempo;
2674
+ stretched = tempo === 1 ? await filterUnityTempo(stretched, origRate, t.codecDescriptions[0]?.codecName === "pcm_s16le", budget, context.signal) : await stretchAudio(stretched, origRate, tempo, budget, context.signal);
2586
2675
  } else if (trimmed === "areverse") {
2587
- reverseAudio = !reverseAudio;
2676
+ stretched = await mapWork(stretched, async (channel) => channel.slice().reverse(), context.signal);
2588
2677
  } else {
2589
2678
  throw new Error(`Unsupported audio filter: ${trimmed}`);
2590
2679
  }
2591
2680
  }
2592
2681
  }
2593
- const targetSamples = Math.max(0, Math.round(durSec / tempoFactor * targetRate));
2594
- let stretched = await stretchAudio(audio.channelData, origRate, tempoFactor, budget, context.signal);
2682
+ const targetSamples = Math.max(0, Math.round((stretched[0]?.length ?? 0) * targetRate / origRate));
2595
2683
  if (targetRate !== origRate) stretched = await resampleAudio(stretched, targetSamples, origRate / targetRate, budget, context.signal);
2596
2684
  const newChannelData = [];
2597
2685
  for (let c = 0; c < targetCh; c++) {
@@ -2600,7 +2688,7 @@ To ignore this, add a trailing '?' to the map.
2600
2688
  context.signal?.throwIfAborted();
2601
2689
  }
2602
2690
  const dst = new Float32Array(targetSamples);
2603
- const srcCh = monoStereo && c >= 2 ? void 0 : stretched[origCh === 1 ? 0 : c];
2691
+ const srcCh = monoStereo && c >= 2 || monoSurround && c !== 2 ? void 0 : stretched[origCh === 1 ? 0 : c];
2604
2692
  const rightCh = origCh === 2 && targetCh === 1 ? stretched[1] : void 0;
2605
2693
  if (srcCh && srcCh.length > 0) {
2606
2694
  for (let i2 = 0; i2 < targetSamples; i2++) {
@@ -2610,10 +2698,10 @@ To ignore this, add a trailing '?' to the map.
2610
2698
  }
2611
2699
  const srcIdx = Math.min(srcCh.length - 1, i2);
2612
2700
  const sample = surroundMono ? ((stretched[0][srcIdx] + stretched[1][srcIdx]) * Math.SQRT1_2 + stretched[2][srcIdx] + (stretched[4][srcIdx] + stretched[5][srcIdx]) / 2) / (2 + Math.SQRT2) : rightCh ? (srcCh[srcIdx] + rightCh[srcIdx]) / 2 : srcCh[srcIdx];
2613
- dst[i2] = Math.max(-1, Math.min(1, sample * volFactor * (monoStereo ? Math.SQRT1_2 : 1)));
2701
+ const mixed = pcm16MonoStereo ? Math.floor((sample * 32768 * 23170 + 16384) / 32768) / 32768 : sample * (monoStereo ? Math.SQRT1_2 : 1);
2702
+ dst[i2] = Math.max(-1, Math.min(1, mixed * volFactor));
2614
2703
  }
2615
2704
  }
2616
- if (reverseAudio) dst.reverse();
2617
2705
  newChannelData.push(dst);
2618
2706
  }
2619
2707
  const nextDescs = t.codecDescriptions.map((d) => ({
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createFfprobeCommand
3
- } from "../safe-bash/command-chunks/chunk-3SWIXYXT.js";
3
+ } from "../safe-bash/command-chunks/chunk-4KM2VJRB.js";
4
4
  import "../safe-bash/command-chunks/chunk-MGBHGZEX.js";
5
- import "../safe-bash/command-chunks/chunk-LORSYIAF.js";
5
+ import "../safe-bash/command-chunks/chunk-CMGVQM4Y.js";
6
6
  import "../safe-bash/command-chunks/chunk-65DS4IOB.js";
7
7
 
8
8
  // packages/safe-bash-command-ffprobe/src/index.ts
@@ -11,9 +11,9 @@ import {
11
11
  loadManifestResources,
12
12
  resolvePath,
13
13
  rethrowRuntimeError
14
- } from "../safe-bash/command-chunks/chunk-3SWIXYXT.js";
14
+ } from "../safe-bash/command-chunks/chunk-4KM2VJRB.js";
15
15
  import "../safe-bash/command-chunks/chunk-MGBHGZEX.js";
16
- import "../safe-bash/command-chunks/chunk-LORSYIAF.js";
16
+ import "../safe-bash/command-chunks/chunk-CMGVQM4Y.js";
17
17
  import "../safe-bash/command-chunks/chunk-65DS4IOB.js";
18
18
  export {
19
19
  createFfprobeCommand,
@@ -36,6 +36,7 @@ export interface MagickGeometry {
36
36
  readonly percentY?: number;
37
37
  readonly areaLimit?: number;
38
38
  readonly isSubdivide?: boolean;
39
+ readonly aspectRatio?: number;
39
40
  }
40
41
  export declare function parseMagickGeometry(raw: string): MagickGeometry;
41
42
  export declare function runIdentifyCli(argv: readonly string[], files: Map<string, Uint8Array> | IdentifyFileInput, stdinBytes?: Uint8Array, signal?: AbortSignal): Promise<ImageMagickCliResult>;
@@ -3947,6 +3947,13 @@ function parseMagickGeometry(raw) {
3947
3947
  break;
3948
3948
  }
3949
3949
  }
3950
+ if (s.includes(":")) {
3951
+ const parts = s.split(":");
3952
+ const ratio = Number(parts[0]) / Number(parts[1]);
3953
+ if (parts.length === 2 && Number(parts[0]) > 0 && Number(parts[1]) > 0 && Number.isFinite(ratio)) {
3954
+ return { aspectRatio: ratio, x, y, hasOffset, forceExact, shrinkOnly, enlargeOnly, fillArea, isPercent: false };
3955
+ }
3956
+ }
3950
3957
  if (isArea) {
3951
3958
  if (s.includes("x") || s.includes("X")) {
3952
3959
  const [wStr, hStr] = s.split(/[xX]/);
@@ -4803,14 +4810,16 @@ function* createCheckerboardImageSteps(width, height) {
4803
4810
  function* applyMagickSpliceSteps(img, geomStr, bg, gravity = "northwest", signal) {
4804
4811
  let cooperativeWork = 0;
4805
4812
  const g = parseMagickGeometry(geomStr);
4806
- const sw = Math.max(0, Math.round(g.width ?? 0));
4807
- const sh = Math.max(0, Math.round(g.height ?? 0));
4813
+ const sw = Math.max(0, Math.round((g.width ?? 0) * (g.isPercent ? img.width / 100 : 1)));
4814
+ const sh = Math.max(0, Math.round((g.height ?? 0) * (g.isPercent ? img.height / 100 : 1)));
4808
4815
  const box = gravityAdjustBox(img.width, img.height, sw, sh, g.x, g.y, gravity);
4809
4816
  const anchor = resolveGravityOffset(sw, sh, gravity);
4810
- const sx = Math.max(0, Math.min(img.width, box.x + anchor.left));
4811
- const sy = Math.max(0, Math.min(img.height, box.y + anchor.top));
4812
4817
  const outW = img.width + sw;
4813
4818
  const outH = img.height + sh;
4819
+ const xOffset = box.x + anchor.left;
4820
+ const sx = xOffset < 0 || xOffset > img.width ? outW : xOffset;
4821
+ if (box.y + anchor.top > outH) throw new Error("pixels are not authentic");
4822
+ const sy = Math.max(0, Math.min(img.height, box.y + anchor.top));
4814
4823
  const out = new Uint8Array(new ArrayBuffer(outW * outH * 4 + outH), 0, outW * outH * 4);
4815
4824
  for (let y = 0; y < outH; y++) {
4816
4825
  if (++cooperativeWork % 65536 === 0)
@@ -4825,7 +4834,7 @@ function* applyMagickSpliceSteps(img, geomStr, bg, gravity = "northwest", signal
4825
4834
  out[dstIdx + 2] = bg.b;
4826
4835
  out[dstIdx + 3] = bg.a;
4827
4836
  } else {
4828
- const origX = x < sx ? x : x - sw;
4837
+ const origX = Math.min(img.width - 1, x < sx ? x : x - sw);
4829
4838
  const origY = y < sy ? y : y - sh;
4830
4839
  const srcIdx = (origY * img.width + origX) * 4;
4831
4840
  out[dstIdx] = img.data[srcIdx];
@@ -6898,13 +6907,12 @@ function magickCropArea(img, geomStr, gravity) {
6898
6907
  }
6899
6908
  function magickExtentLayout(img, geomStr, state) {
6900
6909
  const g = parseMagickGeometry(geomStr);
6901
- const targetW = Math.max(1, Math.round(g.width ?? img.width));
6902
- const targetH = Math.max(1, Math.round(g.height ?? img.height));
6903
- const offset = resolveGravityOffset(targetW - img.width, targetH - img.height, state.gravity);
6904
- const isEast = state.gravity === "east" || state.gravity === "northeast" || state.gravity === "southeast";
6905
- const isSouth = state.gravity === "south" || state.gravity === "southwest" || state.gravity === "southeast";
6906
- const left = Math.round(offset.left - (isEast ? -g.x : g.x));
6907
- const top = Math.round(offset.top - (isSouth ? -g.y : g.y));
6910
+ const targetW = Math.max(1, Math.round(g.aspectRatio !== void 0 ? Math.min(img.width, img.height * g.aspectRatio) : g.isPercent ? img.width * (g.percentX ?? 100) / 100 : g.width ?? img.width));
6911
+ const targetH = Math.max(1, Math.round(g.aspectRatio !== void 0 ? Math.min(img.height, img.width / g.aspectRatio) : g.isPercent ? img.height * (g.percentY ?? 100) / 100 : g.height ?? img.height));
6912
+ if (g.enlargeOnly && img.width > targetW && img.height > targetH || g.shrinkOnly && img.width < targetW && img.height < targetH) return void 0;
6913
+ const offset = gravityAdjustBox(img.width, img.height, targetW, targetH, g.x, g.y, state.gravity);
6914
+ const left = -offset.x;
6915
+ const top = -offset.y;
6908
6916
  const dstX0 = Math.max(0, left);
6909
6917
  const dstY0 = Math.max(0, top);
6910
6918
  const dstX1 = Math.min(targetW, left + img.width);
@@ -6914,6 +6922,7 @@ function magickExtentLayout(img, geomStr, state) {
6914
6922
  function* applyMagickExtentSteps(img, geomStr, state, signal) {
6915
6923
  yield;
6916
6924
  const layout = magickExtentLayout(img, geomStr, state);
6925
+ if (!layout) return img;
6917
6926
  const virtualCanvas = magickResizeCanvas(img, layout.width, layout.height);
6918
6927
  const source = img[magickInput];
6919
6928
  const canvas = {
@@ -8950,18 +8959,22 @@ function* evaluatePipelineTokensSteps(tokens, files, state, parentStack, stdinBy
8950
8959
  return { ...canvas, virtualCanvas: magickBorderCanvas(im, bw, bh) };
8951
8960
  });
8952
8961
  } else if (t === "-shave") {
8953
- const g = parseMagickGeometry(tokens[++i] ?? "0x0");
8962
+ const geometry = tokens[++i] ?? "0x0";
8963
+ const g = parseMagickGeometry(geometry);
8954
8964
  const sw = Math.max(0, Math.round(g.width ?? 0));
8955
8965
  const sh = Math.max(0, Math.round(g.height ?? sw));
8956
8966
  stack = yield* mapSteps(stack, function* (im) {
8957
- const clampedSw = Math.min(Math.floor((im.width - 1) / 2), sw);
8958
- const clampedSh = Math.min(Math.floor((im.height - 1) / 2), sh);
8967
+ if (sw * 2 >= im.width || sh * 2 >= im.height) {
8968
+ analysis.warnings += `magick: geometry does not contain image '${geometry}'
8969
+ `;
8970
+ return im;
8971
+ }
8959
8972
  return { ...yield* extractImageSteps2(im, {
8960
- left: clampedSw,
8961
- top: clampedSh,
8962
- width: Math.max(1, im.width - clampedSw * 2),
8963
- height: Math.max(1, im.height - clampedSh * 2)
8964
- }), virtualCanvas: magickBorderCanvas(im, -clampedSw, -clampedSh) };
8973
+ left: sw,
8974
+ top: sh,
8975
+ width: im.width - sw * 2,
8976
+ height: im.height - sh * 2
8977
+ }), virtualCanvas: magickBorderCanvas(im, -sw, -sh) };
8965
8978
  });
8966
8979
  } else if (t === "-trim") {
8967
8980
  stack = yield* mapSteps(stack, function* (im) {
@@ -9525,14 +9538,17 @@ async function transformStoredMagickCoordinates(image, backend, token, geometry,
9525
9538
  xs = [{ source: w - dx, target: 0, length: dx }, { source: 0, target: dx, length: w - dx }];
9526
9539
  ys = [{ source: h - dy, target: 0, length: dy }, { source: 0, target: dy, length: h - dy }];
9527
9540
  } else if (token === "-splice") {
9528
- const sw = Math.max(0, Math.round(g.width ?? 0)), sh = Math.max(0, Math.round(g.height ?? 0));
9541
+ const sw = Math.max(0, Math.round((g.width ?? 0) * (g.isPercent ? w / 100 : 1)));
9542
+ const sh = Math.max(0, Math.round((g.height ?? 0) * (g.isPercent ? h / 100 : 1)));
9529
9543
  const box = gravityAdjustBox(w, h, sw, sh, g.x, g.y, state.gravity);
9530
9544
  const anchor = resolveGravityOffset(sw, sh, state.gravity);
9531
- const x = Math.max(0, Math.min(w, box.x + anchor.left));
9532
- const y = Math.max(0, Math.min(h, box.y + anchor.top));
9533
9545
  width += sw;
9534
9546
  height += sh;
9535
- xs = [{ source: 0, target: 0, length: x }, { source: x, target: x + sw, length: w - x }];
9547
+ const xOffset = box.x + anchor.left;
9548
+ const x = xOffset < 0 || xOffset > w ? width : xOffset;
9549
+ if (box.y + anchor.top > height) throw new Error("pixels are not authentic");
9550
+ const y = Math.max(0, Math.min(h, box.y + anchor.top));
9551
+ xs = [{ source: 0, target: 0, length: x }, { source: x, target: x + sw, length: Math.max(0, w - x) }];
9536
9552
  ys = [{ source: 0, target: 0, length: y }, { source: y, target: y + sh, length: h - y }];
9537
9553
  } else {
9538
9554
  const cw = Math.max(0, Math.round((g.width ?? 0) * (g.isPercent ? w / 100 : 1)));
@@ -9553,7 +9569,16 @@ async function transformStoredMagickCoordinates(image, backend, token, geometry,
9553
9569
  for (let offset = 0; offset < x.length; offset += 4096) {
9554
9570
  if (work++ % 64 === 0) await yieldTurn11(signal);
9555
9571
  const length = Math.min(4096, x.length - offset) * 4;
9556
- const bytes = await backend.storage.read(image.position + ((y.source + row) * w + x.source + offset) * 4, length);
9572
+ const sourceX = Math.min(w - 1, x.source + offset);
9573
+ const sourceLength = Math.min(length, (w - sourceX) * 4);
9574
+ let bytes = await backend.storage.read(image.position + ((y.source + row) * w + sourceX) * 4, sourceLength);
9575
+ if (sourceLength < length) {
9576
+ const extended = new Uint8Array(length);
9577
+ extended.set(bytes);
9578
+ const edge = bytes.subarray(sourceLength - 4);
9579
+ for (let i = sourceLength; i < length; i += 4) extended.set(edge, i);
9580
+ bytes = extended;
9581
+ }
9557
9582
  await backend.storage.write(position + ((y.target + row) * width + x.target + offset) * 4, bytes);
9558
9583
  }
9559
9584
  }
@@ -10290,13 +10315,18 @@ async function tryConvertFilesResult(argv, input, stdinBytes, signal, analysis)
10290
10315
  return frames;
10291
10316
  });
10292
10317
  } else if (!operandsOnly && token === "-shave") {
10293
- const geometry = parseMagickGeometry(tokens[++i] ?? "0x0"), width = Math.max(0, Math.round(geometry.width ?? 0)), height = Math.max(0, Math.round(geometry.height ?? width));
10318
+ const geometry = tokens[++i] ?? "0x0", parsed = parseMagickGeometry(geometry);
10319
+ const width = Math.max(0, Math.round(parsed.width ?? 0)), height = Math.max(0, Math.round(parsed.height ?? width));
10294
10320
  steps.push(async (image, backend) => {
10295
10321
  if (!image) return;
10296
- const left = Math.min(Math.floor((image.width - 1) / 2), width), top = Math.min(Math.floor((image.height - 1) / 2), height);
10322
+ if (width * 2 >= image.width || height * 2 >= image.height) {
10323
+ analysis.warnings += `magick: geometry does not contain image '${geometry}'
10324
+ `;
10325
+ return image;
10326
+ }
10297
10327
  return {
10298
- ...await transformStoredImage2(image, backend.storage, { kind: "extract", left, top, width: Math.max(1, image.width - left * 2), height: Math.max(1, image.height - top * 2) }, signal),
10299
- virtualCanvas: magickBorderCanvas(image, -left, -top)
10328
+ ...await transformStoredImage2(image, backend.storage, { kind: "extract", left: width, top: height, width: image.width - width * 2, height: image.height - height * 2 }, signal),
10329
+ virtualCanvas: magickBorderCanvas(image, -width, -height)
10300
10330
  };
10301
10331
  });
10302
10332
  } else if (!operandsOnly && token === "-trim") {
@@ -10310,6 +10340,7 @@ async function tryConvertFilesResult(argv, input, stdinBytes, signal, analysis)
10310
10340
  steps.push(async (image, backend) => {
10311
10341
  if (!image) return;
10312
10342
  const layout = magickExtentLayout(image, geometry, captured);
10343
+ if (!layout) return image;
10313
10344
  const virtualCanvas = magickResizeCanvas(image, layout.width, layout.height);
10314
10345
  const source = image[magickInput];
10315
10346
  const canvas = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-bash",
3
- "version": "0.1.844",
3
+ "version": "0.1.845",
4
4
  "description": "Extensible virtual shell with pluggable filesystems and streaming commands",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1415,7 +1415,7 @@
1415
1415
  "access": "public"
1416
1416
  },
1417
1417
  "dependencies": {
1418
- "@poe-platform/safe-fs": "0.1.844",
1418
+ "@poe-platform/safe-fs": "0.1.845",
1419
1419
  "pdf-lib": "1.17.1",
1420
1420
  "@types/node": "^25.2.2",
1421
1421
  "@formatjs/intl-numberformat": "9.4.0",