@poe-platform/safe-bash 0.1.836 → 0.1.837

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.
@@ -5,7 +5,7 @@ import {
5
5
  PdfRetainedReader,
6
6
  renderPdfPageToPng,
7
7
  renderRetainedPagePixels
8
- } from "../safe-bash/command-chunks/chunk-YHRESVJ7.js";
8
+ } from "../safe-bash/command-chunks/chunk-4BSD2RGK.js";
9
9
  import "../safe-bash/command-chunks/chunk-E6SK3OU2.js";
10
10
  import "../safe-bash/command-chunks/chunk-NUGQQDGE.js";
11
11
  import "../safe-bash/command-chunks/chunk-65DS4IOB.js";
@@ -257,7 +257,7 @@ import {
257
257
  transformNodeStringsAndStreamsSteps,
258
258
  transformPathSegment,
259
259
  transformPoint
260
- } from "../safe-bash/command-chunks/chunk-YHRESVJ7.js";
260
+ } from "../safe-bash/command-chunks/chunk-4BSD2RGK.js";
261
261
  import "../safe-bash/command-chunks/chunk-E6SK3OU2.js";
262
262
  import "../safe-bash/command-chunks/chunk-NUGQQDGE.js";
263
263
  import "../safe-bash/command-chunks/chunk-65DS4IOB.js";
@@ -3,7 +3,7 @@ import { type PdfRetainedPixelOptions } from "./retained-page-pixels.js";
3
3
  import type { PdfIndexStorage } from "../cos/object-index.js";
4
4
  import type { PdfRetainedPngOptions } from "./retained-png.js";
5
5
  import { type ParsedCosDocument } from "../cos/parser.js";
6
- import type { PdfDisplayList, PdfPaintOperation, PdfRgbColor, PdfSoftMask } from "../ast.js";
6
+ import type { PdfStoredOperations, PdfDisplayList, PdfPaintOperation, PdfRgbColor, PdfSoftMask } from "../ast.js";
7
7
  export interface RgbaBitmap {
8
8
  readonly width: number;
9
9
  readonly height: number;
@@ -48,10 +48,11 @@ export type PdfRasterWindowOptions = Pick<RenderToPngOptions, "scale" | "dpi" |
48
48
  * by the caller. Crop, rotation and anisotropic resampling belong to the driver. */
49
49
  export declare function renderDisplayListWindowSteps(displayList: PdfDisplayList, window: PdfCropRect, options?: PdfRasterWindowOptions): Generator<void, RgbaBitmap, void>;
50
50
  /** Consume a replayable operation source without collecting a page display list.
51
- * Opaque output makes a compositing prepass to preserve background quantization.
51
+ * Opaque output uses stored effect flags or a compositing prepass to preserve
52
+ * background quantization.
52
53
  * Each operation is painted before advancing its source. Individual paths,
53
54
  * resources and nested captures still have the evaluator's memory ownership. */
54
- export declare function renderOperationStreamWindow(page: Pick<PdfDisplayList, "width" | "height" | "origin">, operations: () => AsyncIterable<PdfPaintOperation>, window: PdfCropRect, options?: PdfRasterWindowOptions & {
55
+ export declare function renderOperationStreamWindow(page: Pick<PdfDisplayList, "width" | "height" | "origin">, operations: (() => AsyncIterable<PdfPaintOperation>) | PdfStoredOperations, window: PdfCropRect, options?: PdfRasterWindowOptions & {
55
56
  readonly signal?: AbortSignal;
56
57
  }): Promise<RgbaBitmap>;
57
58
  export declare function renderDisplayListToBitmapSteps(displayList: PdfDisplayList, options?: RenderToPngOptions): Generator<void, RgbaBitmap, void>;
@@ -15,6 +15,7 @@ export interface PdfRetainedPixels {
15
15
  }
16
16
  /** Render one bounded window at a time into caller-backed tile storage, then
17
17
  * read transformed pixels without allocating the full page or collecting paints.
18
- * Replays page evaluation per tile; nested captures/resources retain their own
19
- * admission contracts. Storage is acquired lazily and owned by the iterator. */
18
+ * With caller-owned image backing, evaluate once and replay stored paints per
19
+ * tile. Otherwise replay evaluation. Nested captures/resources retain their own
20
+ * admission contracts. Tile storage is acquired lazily and owned by the iterator. */
20
21
  export declare function renderRetainedPagePixels(page: PdfRetainedPage, storage: PdfIndexStorage, options?: PdfRetainedPixelOptions): Promise<PdfRetainedPixels>;
@@ -26663,6 +26663,71 @@ var PdfStagingStorage = class {
26663
26663
  }
26664
26664
  };
26665
26665
 
26666
+ // packages/pdf-ast/src/content/stored-operations.ts
26667
+ function operationEffects(operation) {
26668
+ const value = operation.value;
26669
+ let effects = (value.softMask ? 2 : 0) | (value.blendMode && value.blendMode !== "Normal" && value.blendMode !== "Compatible" ? 1 : 0);
26670
+ if (operation.kind === "group") {
26671
+ let children = operation.value.storedOperations?.effects ?? 0;
26672
+ for (const child of operation.value.operations) children |= operationEffects(child);
26673
+ if (operation.value.isolated === false && children & 3) effects |= 4;
26674
+ effects |= children;
26675
+ }
26676
+ return effects;
26677
+ }
26678
+ var StoredOperationsWriter = class {
26679
+ constructor(storage, signal) {
26680
+ this.storage = storage;
26681
+ this.signal = signal;
26682
+ }
26683
+ storage;
26684
+ signal;
26685
+ first = -1;
26686
+ last = -1;
26687
+ count = 0;
26688
+ effects = 0;
26689
+ async append(operation) {
26690
+ const position = await writeStoredRecord(this.storage, operation, -1, this.signal);
26691
+ const header2 = new Uint8Array(8), view = new DataView(header2.buffer);
26692
+ if (this.last >= 0) {
26693
+ view.setFloat64(0, position, true);
26694
+ await this.storage.write(
26695
+ this.last,
26696
+ header2.subarray(0, 8),
26697
+ this.signal ? { signal: this.signal } : void 0
26698
+ );
26699
+ }
26700
+ this.signal?.throwIfAborted();
26701
+ if (this.first < 0) this.first = position;
26702
+ this.last = position;
26703
+ this.count++;
26704
+ this.effects |= operationEffects(operation);
26705
+ }
26706
+ snapshot() {
26707
+ return {
26708
+ kind: "stored-operations",
26709
+ storage: this.storage,
26710
+ position: this.first,
26711
+ count: this.count,
26712
+ effects: this.effects
26713
+ };
26714
+ }
26715
+ };
26716
+ async function* readStoredOperations(source, signal) {
26717
+ if (!Number.isSafeInteger(source.count) || source.count < 0)
26718
+ throw new RangeError("Invalid capture count");
26719
+ let position = source.position;
26720
+ for (let record = 0; record < source.count; record++) {
26721
+ const { value, next } = await readStoredRecord(
26722
+ source.storage,
26723
+ position,
26724
+ signal
26725
+ );
26726
+ yield value;
26727
+ position = next;
26728
+ }
26729
+ }
26730
+
26666
26731
  // packages/pdf-ast/src/render/raster.ts
26667
26732
  import { PagedStorage as PagedStorage3, IntegerTable as IntegerTable3 } from "@poe-platform/safe-fs/storage";
26668
26733
 
@@ -27077,71 +27142,6 @@ async function* encodeSvgPathDataChunks(input, height, options = {}) {
27077
27142
  if (used) yield buffer.slice(0, used);
27078
27143
  }
27079
27144
 
27080
- // packages/pdf-ast/src/content/stored-operations.ts
27081
- function operationEffects(operation) {
27082
- const value = operation.value;
27083
- let effects = (value.softMask ? 2 : 0) | (value.blendMode && value.blendMode !== "Normal" && value.blendMode !== "Compatible" ? 1 : 0);
27084
- if (operation.kind === "group") {
27085
- let children = operation.value.storedOperations?.effects ?? 0;
27086
- for (const child of operation.value.operations) children |= operationEffects(child);
27087
- if (operation.value.isolated === false && children & 3) effects |= 4;
27088
- effects |= children;
27089
- }
27090
- return effects;
27091
- }
27092
- var StoredOperationsWriter = class {
27093
- constructor(storage, signal) {
27094
- this.storage = storage;
27095
- this.signal = signal;
27096
- }
27097
- storage;
27098
- signal;
27099
- first = -1;
27100
- last = -1;
27101
- count = 0;
27102
- effects = 0;
27103
- async append(operation) {
27104
- const position = await writeStoredRecord(this.storage, operation, -1, this.signal);
27105
- const header2 = new Uint8Array(8), view = new DataView(header2.buffer);
27106
- if (this.last >= 0) {
27107
- view.setFloat64(0, position, true);
27108
- await this.storage.write(
27109
- this.last,
27110
- header2.subarray(0, 8),
27111
- this.signal ? { signal: this.signal } : void 0
27112
- );
27113
- }
27114
- this.signal?.throwIfAborted();
27115
- if (this.first < 0) this.first = position;
27116
- this.last = position;
27117
- this.count++;
27118
- this.effects |= operationEffects(operation);
27119
- }
27120
- snapshot() {
27121
- return {
27122
- kind: "stored-operations",
27123
- storage: this.storage,
27124
- position: this.first,
27125
- count: this.count,
27126
- effects: this.effects
27127
- };
27128
- }
27129
- };
27130
- async function* readStoredOperations(source, signal) {
27131
- if (!Number.isSafeInteger(source.count) || source.count < 0)
27132
- throw new RangeError("Invalid capture count");
27133
- let position = source.position;
27134
- for (let record = 0; record < source.count; record++) {
27135
- const { value, next } = await readStoredRecord(
27136
- source.storage,
27137
- position,
27138
- signal
27139
- );
27140
- yield value;
27141
- position = next;
27142
- }
27143
- }
27144
-
27145
27145
  // packages/pdf-ast/src/content/stored-clips.ts
27146
27146
  var PAGE_BYTES = 4096;
27147
27147
  var RECORD_BYTES2 = 64;
@@ -48242,8 +48242,46 @@ function strokeEdges(contours) {
48242
48242
  } };
48243
48243
  }
48244
48244
  var SUB_OFFSETS_4X4 = [0.125, 0.375, 0.625, 0.875];
48245
+ function* cacheSmallEdgesSteps(edges) {
48246
+ if (Array.isArray(edges)) return edges;
48247
+ const cached = [];
48248
+ for (const edge of edges) {
48249
+ if (!edge) {
48250
+ yield;
48251
+ continue;
48252
+ }
48253
+ if (cached.length === 128) return edges;
48254
+ cached.push(edge);
48255
+ }
48256
+ return cached;
48257
+ }
48245
48258
  function* computeSubScanlineCountsSteps(edges, scanY, fillRule, deltas, winding, rowCounts) {
48246
48259
  const samples = winding.length;
48260
+ if (Array.isArray(edges)) {
48261
+ const crossings = [];
48262
+ for (const edge of edges) {
48263
+ if (edge.y0 <= scanY && edge.y1 > scanY || edge.y1 <= scanY && edge.y0 > scanY) {
48264
+ crossings.push({ x: edge.x0 + (scanY - edge.y0) / (edge.y1 - edge.y0) * (edge.x1 - edge.x0), direction: edge.y0 < edge.y1 ? 1 : -1 });
48265
+ }
48266
+ }
48267
+ crossings.sort((a, b) => a.x - b.x);
48268
+ let value2 = 0, start = 0;
48269
+ for (const crossing of crossings) {
48270
+ const before = value2;
48271
+ value2 = fillRule === "evenodd" ? 1 - value2 : value2 + crossing.direction;
48272
+ if (before === 0 && value2 !== 0) start = crossing.x;
48273
+ else if (before !== 0 && value2 === 0) {
48274
+ const first = Math.max(0, Math.ceil((start - 0.125) * 4));
48275
+ const end = Math.min(samples, Math.floor((crossing.x - 0.125) * 4) + 1);
48276
+ for (let index2 = first; index2 < end; ) {
48277
+ const count = Math.min(end - index2, 4 - index2 % 4);
48278
+ rowCounts[index2 >>> 2] = rowCounts[index2 >>> 2] + count;
48279
+ index2 += count;
48280
+ }
48281
+ }
48282
+ }
48283
+ return;
48284
+ }
48247
48285
  deltas.fill(0);
48248
48286
  let work = 0;
48249
48287
  for (const edge of edges) {
@@ -48279,6 +48317,7 @@ function* computeSubScanlineCountsSteps(edges, scanY, fillRule, deltas, winding,
48279
48317
  }
48280
48318
  }
48281
48319
  function* fillEdgesScanline4x4Steps(rgba, width, height, edges, color, alpha = 1, fillRule = "nonzero", clipScreen, antialias = true, blendMode, clipMask, groupAlpha) {
48320
+ edges = yield* cacheSmallEdgesSteps(edges);
48282
48321
  let work = 0;
48283
48322
  let minY = Infinity;
48284
48323
  let maxY = -Infinity;
@@ -48318,6 +48357,7 @@ function* fillEdgesScanline4x4Steps(rgba, width, height, edges, color, alpha = 1
48318
48357
  }
48319
48358
  }
48320
48359
  function* fillMaskScanline4x4Steps(mask, width, height, edges, fillRule = "nonzero", intersect = false) {
48360
+ edges = yield* cacheSmallEdgesSteps(edges);
48321
48361
  let work = 0;
48322
48362
  let minY = Infinity;
48323
48363
  let maxY = -Infinity;
@@ -48536,6 +48576,21 @@ function* renderDisplayListLayerSteps(displayList, options, scale, backdrop, win
48536
48576
  if (++work % 16384 === 0) yield;
48537
48577
  if (original.kind === "glyph" && (original.value.renderMode === 3 || !original.value.outline && !original.value.unicode.trim())) continue;
48538
48578
  let operation = original.kind === "glyph" ? { kind: "path", value: glyphPaint(original.value) } : original;
48579
+ let fillEdges;
48580
+ if (operation.kind === "path" && operation.value.fillColor && !operation.value.strokeColor) {
48581
+ const path = operation.value;
48582
+ fillEdges = yield* cacheSmallEdgesSteps(screenFillEdges(path.segments, toScreen, path.storedSegments, images));
48583
+ if (Array.isArray(fillEdges)) {
48584
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
48585
+ for (const edge of fillEdges) {
48586
+ minX = Math.min(minX, edge.x0, edge.x1);
48587
+ minY = Math.min(minY, edge.y0, edge.y1);
48588
+ maxX = Math.max(maxX, edge.x0, edge.x1);
48589
+ maxY = Math.max(maxY, edge.y0, edge.y1);
48590
+ }
48591
+ if (maxX < 0 || maxY < 0 || minX > width || minY > height) continue;
48592
+ }
48593
+ }
48539
48594
  if (operation.kind === "group") {
48540
48595
  const group = operation.value;
48541
48596
  const bitmap = yield* renderDisplayListLayerSteps(
@@ -48637,7 +48692,7 @@ function* renderDisplayListLayerSteps(displayList, options, scale, backdrop, win
48637
48692
  if (operation.kind === "path") {
48638
48693
  const path = operation.value;
48639
48694
  if (path.fillColor) {
48640
- const edges = screenFillEdges(path.segments, toScreen, path.storedSegments, images);
48695
+ const edges = fillEdges ?? screenFillEdges(path.segments, toScreen, path.storedSegments, images);
48641
48696
  const clipScreen = path.clipRect ? [(path.clipRect[0] - originX) * scale - offsetX, (pageTop - path.clipRect[3]) * scale - offsetY, (path.clipRect[2] - originX) * scale - offsetX, (pageTop - path.clipRect[1]) * scale - offsetY] : void 0;
48642
48697
  yield* fillEdgesScanline4x4Steps(rgba, width, height, edges, path.fillColor, path.fillAlpha ?? 1, path.fillRule ?? "nonzero", clipScreen, original.kind === "glyph" ? aaTxt : aaVec, path.blendMode, clipMask, groupAlpha);
48643
48698
  }
@@ -48797,7 +48852,7 @@ async function renderOperationStreamWindow(page2, operations, window, options =
48797
48852
  const { signal } = options;
48798
48853
  signal?.throwIfAborted();
48799
48854
  async function* cursor() {
48800
- const source2 = operations()[Symbol.asyncIterator]();
48855
+ const source2 = (typeof operations === "function" ? operations() : readStoredOperations(operations, signal))[Symbol.asyncIterator]();
48801
48856
  let failed2 = false, pulls = 0;
48802
48857
  try {
48803
48858
  while (true) {
@@ -48819,8 +48874,8 @@ async function renderOperationStreamWindow(page2, operations, window, options =
48819
48874
  }
48820
48875
  }
48821
48876
  }
48822
- const input = { requested: false, next: void 0, compositing: false };
48823
- if (!options.transparent) for await (const operation of cursor()) {
48877
+ const input = { requested: false, next: void 0, compositing: typeof operations !== "function" && !!(operations.effects & 1) };
48878
+ if (!options.transparent && typeof operations === "function") for await (const operation of cursor()) {
48824
48879
  if (hasCompositingEffects([operation])) {
48825
48880
  input.compositing = true;
48826
48881
  break;
@@ -49671,10 +49726,17 @@ async function renderRetainedPagePixels(page2, storage, options = {}) {
49671
49726
  let source, failed = false;
49672
49727
  async function* tiles() {
49673
49728
  let work = 0;
49729
+ const paints = options.imageStorage ? new StoredOperationsWriter(options.imageStorage, signal) : void 0;
49730
+ if (paints) for await (const event of page2.evaluateSteps(shared, options)) {
49731
+ if (!event.captured) await paints.append(event.operation);
49732
+ if (++work % 256 === 0) await new Promise((resolve2) => setTimeout(resolve2, 0));
49733
+ signal?.throwIfAborted();
49734
+ }
49735
+ const stored = paints?.snapshot();
49674
49736
  for (let y = 0; y < height; y += tileSize) for (let x = 0; x < width; x += tileSize) {
49675
49737
  if (++work % 32 === 0) await new Promise((resolve2) => setTimeout(resolve2, 0));
49676
49738
  signal?.throwIfAborted();
49677
- const tile = await renderOperationStreamWindow(geometry, async function* () {
49739
+ const tile = await renderOperationStreamWindow(geometry, stored ?? async function* () {
49678
49740
  for await (const event of page2.evaluateSteps(shared, options)) if (!event.captured) yield event.operation;
49679
49741
  }, { x, y, width: Math.min(tileSize, width - x), height: Math.min(tileSize, height - y) }, { ...options, scale: scaleX });
49680
49742
  yield tile.data;
@@ -278,19 +278,22 @@ function* lavfiSteps(spec, budget, durationOverride) {
278
278
  )
279
279
  };
280
280
  }
281
- async function ensureDecodedFrames(track, budget, signal) {
282
- if (track.decodedVideoFrames && track.decodedVideoFrames.length > 0) {
283
- return [...track.decodedVideoFrames];
284
- }
281
+ async function ensureDecodedFrames(track, movieTimescale, budget, signal, maxFrames) {
285
282
  const width = track.width ?? track.codecDescriptions[0]?.width ?? 64;
286
283
  const height = track.height ?? track.codecDescriptions[0]?.height ?? 64;
287
284
  const ts = track.timescale || 9e4;
288
285
  const frames = [];
289
- for (const frame of decodeH264Samples(track.samples, track.codecDescriptions, width, height, ts)) {
286
+ const decoded = track.decodedVideoFrames ?? decodeH264Samples(track.samples, track.codecDescriptions, width, height, ts);
287
+ for (const frame of decoded) {
290
288
  await yieldTurn(signal);
291
289
  signal?.throwIfAborted();
292
- budget.recordFrame(frame.width, frame.height);
290
+ if (!track.decodedVideoFrames) budget.recordFrame(frame.width, frame.height);
291
+ const pts = Math.round(frame.ptsSeconds * ts);
292
+ if (track.editList?.length && !track.editList.some(
293
+ (edit) => edit.mediaTime >= 0 && pts >= edit.mediaTime && pts < edit.mediaTime + edit.segmentDuration * ts / movieTimescale
294
+ )) continue;
293
295
  frames.push(frame);
296
+ if (maxFrames !== void 0 && frames.length >= maxFrames) break;
294
297
  }
295
298
  return frames;
296
299
  }
@@ -942,6 +945,8 @@ function evalSyncFfmpeg(inBytesOrArgs, maybeArgs, readFileSync, writeFileSync) {
942
945
  const plugin = registry.detect(rawBytes, inp.path, inp.format);
943
946
  if (!plugin || !plugin.canDemux) return void 0;
944
947
  doc = plugin.parse(rawBytes, { filename: inp.path, budget });
948
+ const imageOutput = (outputFormat ? registry.findByFormatName(outputFormat) : registry.findByFilename(outputTarget))?.id === "image2";
949
+ if ((imageOutput || maxVideoFrames !== void 0) && doc.tracks.some((track) => track.type === "video" && !track.decodedVideoFrames)) return void 0;
945
950
  }
946
951
  if (inp.startSeconds !== void 0 || inp.endSeconds !== void 0 || inp.durationSeconds !== void 0) {
947
952
  doc = sliceMp4(doc, { startSeconds: inp.startSeconds, endSeconds: inp.endSeconds, durationSeconds: inp.durationSeconds });
@@ -1449,11 +1454,13 @@ function createFfmpegCommand(options = {}) {
1449
1454
  } else if (filterComplex.includes("xfade") && loadedDocs.length >= 2) {
1450
1455
  const f0 = await ensureDecodedFrames(
1451
1456
  loadedDocs[0].tracks.find((t) => t.type === "video"),
1457
+ loadedDocs[0].timescale,
1452
1458
  budget,
1453
1459
  context.signal
1454
1460
  );
1455
1461
  const f1 = await ensureDecodedFrames(
1456
1462
  loadedDocs[1].tracks.find((t) => t.type === "video"),
1463
+ loadedDocs[1].timescale,
1457
1464
  budget,
1458
1465
  context.signal
1459
1466
  );
@@ -1507,11 +1514,13 @@ function createFfmpegCommand(options = {}) {
1507
1514
  } else if (filterComplex.includes("vstack") && loadedDocs.length >= 2) {
1508
1515
  const f0 = await ensureDecodedFrames(
1509
1516
  loadedDocs[0].tracks.find((t) => t.type === "video"),
1517
+ loadedDocs[0].timescale,
1510
1518
  budget,
1511
1519
  context.signal
1512
1520
  );
1513
1521
  const f1 = await ensureDecodedFrames(
1514
1522
  loadedDocs[1].tracks.find((t) => t.type === "video"),
1523
+ loadedDocs[1].timescale,
1515
1524
  budget,
1516
1525
  context.signal
1517
1526
  );
@@ -1553,11 +1562,13 @@ function createFfmpegCommand(options = {}) {
1553
1562
  } else if (filterComplex.includes("hstack") && loadedDocs.length >= 2) {
1554
1563
  const f0 = await ensureDecodedFrames(
1555
1564
  loadedDocs[0].tracks.find((t) => t.type === "video"),
1565
+ loadedDocs[0].timescale,
1556
1566
  budget,
1557
1567
  context.signal
1558
1568
  );
1559
1569
  const f1 = await ensureDecodedFrames(
1560
1570
  loadedDocs[1].tracks.find((t) => t.type === "video"),
1571
+ loadedDocs[1].timescale,
1561
1572
  budget,
1562
1573
  context.signal
1563
1574
  );
@@ -1599,11 +1610,13 @@ function createFfmpegCommand(options = {}) {
1599
1610
  } else if (filterComplex.includes("overlay") && loadedDocs.length >= 2) {
1600
1611
  const f0 = await ensureDecodedFrames(
1601
1612
  loadedDocs[0].tracks.find((t) => t.type === "video"),
1613
+ loadedDocs[0].timescale,
1602
1614
  budget,
1603
1615
  context.signal
1604
1616
  );
1605
1617
  const f1 = await ensureDecodedFrames(
1606
1618
  loadedDocs[1].tracks.find((t) => t.type === "video"),
1619
+ loadedDocs[1].timescale,
1607
1620
  budget,
1608
1621
  context.signal
1609
1622
  );
@@ -1689,20 +1702,16 @@ function createFfmpegCommand(options = {}) {
1689
1702
  if (outputFps && videoCodec !== "copy") {
1690
1703
  vfFilters.push(`fps=${outputFps}`);
1691
1704
  }
1692
- if (maxVideoFrames !== void 0 && !vfFilters.some((f) => /\b(fps|select|reverse|trim|tile|setpts)\b/i.test(f))) {
1705
+ const imageOutput = (outputFormat ? registry.findByFormatName(outputFormat) : registry.findByFilename(outputTarget))?.id === "image2";
1706
+ if (imageOutput || maxVideoFrames !== void 0 && videoCodec !== "copy") {
1693
1707
  workingDoc = {
1694
1708
  ...workingDoc,
1695
- tracks: workingDoc.tracks.map((t) => {
1696
- if (t.type !== "video") return t;
1697
- if (t.decodedVideoFrames && t.decodedVideoFrames.length > maxVideoFrames) {
1698
- return { ...t, samples: [], decodedVideoFrames: t.decodedVideoFrames.slice(0, maxVideoFrames) };
1699
- }
1700
- if (t.samples.length > maxVideoFrames) {
1701
- const slicedSamples = t.samples.slice(0, maxVideoFrames);
1702
- return { ...t, duration: slicedSamples.reduce((acc, s) => acc + s.duration, 0), samples: slicedSamples };
1703
- }
1704
- return t;
1705
- })
1709
+ tracks: await mapWork(workingDoc.tracks, async (track) => {
1710
+ if (track.type !== "video") return track;
1711
+ const limit = vfFilters.length === 0 ? maxVideoFrames : void 0;
1712
+ const frames = await ensureDecodedFrames(track, workingDoc.timescale, budget, context.signal, limit);
1713
+ return { ...track, samples: [], decodedVideoFrames: frames };
1714
+ }, context.signal)
1706
1715
  };
1707
1716
  }
1708
1717
  if (vfFilters.length > 0) {
@@ -1740,7 +1749,7 @@ function createFfmpegCommand(options = {}) {
1740
1749
  }
1741
1750
  }
1742
1751
  const offsetTicks = Math.round(presentationOffset * t.timescale);
1743
- const decoded = await ensureDecodedFrames(t, budget, context.signal);
1752
+ const decoded = await ensureDecodedFrames(t, workingDoc.timescale, budget, context.signal);
1744
1753
  const displayed = decoded.map((frame) => ({ ...frame, ptsSeconds: (Math.round(frame.ptsSeconds * t.timescale) - offsetTicks) / t.timescale }));
1745
1754
  const filtered = await applyVideoFilterChain(displayed, combinedChain, budget, loadedSubtitleCues, context.signal);
1746
1755
  const first = filtered[0];
@@ -1954,6 +1963,9 @@ function createFfmpegCommand(options = {}) {
1954
1963
  options.onMetrics?.(budget.getStats());
1955
1964
  return { exitCode: 0 };
1956
1965
  }
1966
+ if (imageOutput && !workingDoc.tracks.some((track) => track.type === "video" && track.decodedVideoFrames?.length)) {
1967
+ throw new Error("No video frame in the requested presentation interval");
1968
+ }
1957
1969
  if (/%0?\d*d/.test(outputTarget)) {
1958
1970
  const outExt2 = outputTarget.split(".").pop()?.toLowerCase() ?? "png";
1959
1971
  const imgPlugin = registry.findByFilename(`x.${outExt2}`);
@@ -1961,7 +1973,7 @@ function createFfmpegCommand(options = {}) {
1961
1973
  throw new Error(`Unsupported output format for '${outputTarget}' (AST not registered)`);
1962
1974
  }
1963
1975
  const vTrack = workingDoc.tracks.find((t) => t.type === "video");
1964
- const frames = vTrack ? await ensureDecodedFrames(vTrack, budget, context.signal) : [];
1976
+ const frames = vTrack ? await ensureDecodedFrames(vTrack, workingDoc.timescale, budget, context.signal) : [];
1965
1977
  const limit = maxVideoFrames ? Math.min(frames.length, maxVideoFrames) : frames.length;
1966
1978
  const imgFmt = outExt2 === "jpg" || outExt2 === "jpeg" ? "jpeg" : outExt2 === "webp" ? "webp" : outExt2 === "gif" ? "gif" : outExt2 === "bmp" ? "bmp" : outExt2 === "ppm" ? "ppm" : "png";
1967
1979
  for (let idx = 0; idx < limit; idx++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-bash",
3
- "version": "0.1.836",
3
+ "version": "0.1.837",
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.836",
1418
+ "@poe-platform/safe-fs": "0.1.837",
1419
1419
  "pdf-lib": "1.17.1",
1420
1420
  "@types/node": "^25.2.2",
1421
1421
  "@formatjs/intl-numberformat": "9.4.0",