{"version":3,"file":"view-render-action.mjs","names":[],"sources":["../../src/actions/view-render-action.ts"],"sourcesContent":["import { createWriteStream } from 'node:fs';\n\nimport type { Command } from 'commander';\nimport gifenc from 'gifenc';\nimport sharp, { type Sharp } from 'sharp';\n\nimport { decompress, parseView } from '@4bitlabs/sci0';\nimport {\n  createImageData,\n  createPaletteFilter,\n  padPixelsFilter,\n  renderPixelData,\n} from '@4bitlabs/image';\nimport { loopPaddingFilter } from '@4bitlabs/sci0-renderer';\nimport { loadContentFromMap } from './load-content-from-map.js';\nimport { viewMatcher } from '../helpers/resource-matchers.js';\nimport { getRootOptions } from './get-root-options.js';\nimport {\n  generatePalette,\n  getScalerFromOptions,\n} from '../helpers/create-pic-pipeline.js';\nimport type { ScalerID } from '../models/render-pic-options.js';\n\nconst { GIFEncoder, quantize, applyPalette } = gifenc;\n\ntype Padding =\n  | [number]\n  | [number, number]\n  | [number, number, number]\n  | [number, number, number, number];\n\nconst hasPadding = (it: Padding | undefined): it is Padding => {\n  return !(it?.every((v) => v === 0) ?? true);\n};\n\ninterface ViewRenderActionOptions {\n  padding: Padding | undefined;\n  animated: boolean;\n  palette: 'cga' | 'true-cga' | 'dga';\n  contrast: number;\n  scaler: ScalerID;\n  output: string;\n  format: 'jpg' | 'png' | 'webp' | 'raw';\n  grayscale: boolean;\n}\n\nconst FORMAT_MAPPING = {\n  png: (source: Sharp) => source.png().toBuffer(),\n  jpg: (source: Sharp) => source.jpeg().toBuffer(),\n  webp: (source: Sharp) => source.webp().toBuffer(),\n  raw: (source: Sharp) => source.raw().toBuffer(),\n};\n\nexport async function viewRenderAction(\n  id: number,\n  loopId: number,\n  options: ViewRenderActionOptions,\n  cmd: Command,\n) {\n  const {\n    format,\n    animated,\n    output = `view.${id.toString(10).padStart(3, '0')}-${loopId.toString(10)}.${format}`,\n  } = options;\n  const { root, engine } = getRootOptions(cmd);\n\n  const [header, compressed] = await loadContentFromMap(root, viewMatcher(id));\n  const viewData = decompress(engine, header.compression, compressed);\n  const view = parseView(viewData);\n\n  const loop = view[loopId];\n\n  if (!loop)\n    cmd.error(`Error: loop ${loopId} is not valid`, { code: 'INVALID_LOOP' });\n\n  const backgroundColor = 0x00000000;\n\n  const palette = generatePalette(options);\n  const pipeline = {\n    pre: [\n      loopPaddingFilter(loop),\n      hasPadding(options.padding) && padPixelsFilter(options.padding),\n    ],\n    render: createPaletteFilter(palette, { backgroundColor }),\n    post: [getScalerFromOptions(options.scaler)],\n  };\n\n  const allFrames = loop.frames.map((frame) =>\n    renderPixelData(frame, pipeline),\n  );\n\n  if (animated) {\n    const gif = GIFEncoder();\n\n    if (output !== '-' && !output.endsWith('.gif'))\n      cmd.error('only .gif files are supported for animated output');\n\n    allFrames.forEach(({ data, width, height }) => {\n      const pal = quantize(data, 256, { format: 'rgba4444' });\n      const index = applyPalette(data, pal, 'rgba4444');\n      gif.writeFrame(index, width, height, { palette: pal, transparent: true });\n    });\n    gif.finish();\n\n    if (output === '-') {\n      process.stdout.write(gif.bytes());\n    } else {\n      await new Promise<void>((yes, no) =>\n        createWriteStream(output, { flags: 'w' }).write(\n          gif.bytes(),\n          (err: unknown) => {\n            if (err) no(err);\n            yes();\n          },\n        ),\n      );\n    }\n  } else {\n    const [totalWidth, totalHeight] = allFrames.reduce(\n      ([prevW, prevH], imgData) => [\n        Math.max(prevW, imgData.width),\n        prevH + imgData.height,\n      ],\n      [0, 0],\n    );\n\n    const [compositeImage] = allFrames.reduce(\n      ([sheet, offset], current) => {\n        const { data, width, height } = current;\n        const size = width * height * 4;\n        sheet.data.set(data, offset);\n        return [sheet, offset + size];\n      },\n      [createImageData(totalWidth, totalHeight), 0],\n    );\n\n    const image = sharp(compositeImage.data, {\n      raw: {\n        width: compositeImage.width,\n        height: compositeImage.height,\n        channels: 4,\n      },\n    });\n\n    if (output === '-') {\n      process.stdout.write(await FORMAT_MAPPING[format](image));\n    } else {\n      image.toFile(output);\n    }\n  }\n}\n"],"mappings":"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"}