Multi-Frame Images

Examples of working with animated GIFs, APNGs, and multi-page TIFFs.

Animated GIFs

Decode GIF Frames

import { Image } from "@cross/image";

// Decode all frames from animated GIF
const gifData = await Deno.readFile("animated.gif");
const multiFrame = await Image.decodeFrames(gifData);

console.log(`Canvas size: ${multiFrame.width}x${multiFrame.height}`);
console.log(`Number of frames: ${multiFrame.frames.length}`);

// Access individual frames
for (let i = 0; i < multiFrame.frames.length; i++) {
  const frame = multiFrame.frames[i];
  console.log(`Frame ${i}:`);
  console.log(`  Size: ${frame.width}x${frame.height}`);
  console.log(`  Delay: ${frame.frameMetadata?.delay}ms`);
  console.log(`  Disposal: ${frame.frameMetadata?.disposal}`);
}

Extract Single Frame

import { Image } from "@cross/image";

const gifData = await Deno.readFile("animated.gif");
const multiFrame = await Image.decodeFrames(gifData);

// Get first frame
const firstFrame = multiFrame.frames[0];
const image = Image.fromRGBA(
  firstFrame.width,
  firstFrame.height,
  firstFrame.data,
);

// Save as static image
await Deno.writeFile("first-frame.png", await image.encode("png"));

Extract All Frames

import { Image } from "@cross/image";

const gifData = await Deno.readFile("animated.gif");
const multiFrame = await Image.decodeFrames(gifData);

// Save each frame
for (let i = 0; i < multiFrame.frames.length; i++) {
  const frame = multiFrame.frames[i];
  const image = Image.fromRGBA(frame.width, frame.height, frame.data);

  const filename = `frame-${String(i).padStart(3, "0")}.png`;
  await Deno.writeFile(filename, await image.encode("png"));

  console.log(`Saved ${filename}`);
}

Create Animated GIF

import { Image } from "@cross/image";

// Create frames
const frame1 = Image.create(200, 200, 255, 0, 0); // Red
const frame2 = Image.create(200, 200, 0, 255, 0); // Green
const frame3 = Image.create(200, 200, 0, 0, 255); // Blue

// Build multi-frame structure
const multiFrame = {
  width: 200,
  height: 200,
  frames: [
    {
      width: 200,
      height: 200,
      data: frame1.data,
      frameMetadata: { delay: 500, disposal: 0 },
    },
    {
      width: 200,
      height: 200,
      data: frame2.data,
      frameMetadata: { delay: 500, disposal: 0 },
    },
    {
      width: 200,
      height: 200,
      data: frame3.data,
      frameMetadata: { delay: 500, disposal: 0 },
    },
  ],
};

// Encode as animated GIF
const gifData = await Image.encodeFrames("gif", multiFrame);
await Deno.writeFile("animated.gif", gifData);

Process Each Frame

import { Image } from "@cross/image";

// Load animated GIF
const gifData = await Deno.readFile("animated.gif");
const multiFrame = await Image.decodeFrames(gifData);

// Process each frame
const processedFrames = multiFrame.frames.map((frame) => {
  const image = Image.fromRGBA(frame.width, frame.height, frame.data);

  // Apply processing
  image.brightness(0.1).contrast(0.2).sharpen(0.5);

  return {
    width: frame.width,
    height: frame.height,
    data: image.data,
    frameMetadata: frame.frameMetadata,
  };
});

// Create new multi-frame
const processed = {
  width: multiFrame.width,
  height: multiFrame.height,
  frames: processedFrames,
};

// Save processed GIF
const output = await Image.encodeFrames("gif", processed);
await Deno.writeFile("processed.gif", output);

Animated PNGs (APNG)

Decode APNG Frames

import { Image } from "@cross/image";

// Decode all frames from APNG
const apngData = await Deno.readFile("animated.png");
const multiFrame = await Image.decodeFrames(apngData);

console.log(`Canvas size: ${multiFrame.width}x${multiFrame.height}`);
console.log(`Number of frames: ${multiFrame.frames.length}`);

// Process frames
for (let i = 0; i < multiFrame.frames.length; i++) {
  const frame = multiFrame.frames[i];
  console.log(`Frame ${i}: ${frame.width}x${frame.height}`);
  console.log(`  Delay: ${frame.frameMetadata?.delay}ms`);
}

Create Animated PNG

import { Image } from "@cross/image";

// Create animation frames
const frames = [];
for (let i = 0; i < 10; i++) {
  const frame = Image.create(200, 200, i * 25, 100, 255 - i * 25);
  frames.push({
    width: 200,
    height: 200,
    data: frame.data,
    frameMetadata: { delay: 100, disposal: 0 },
  });
}

// Build multi-frame structure
const multiFrame = {
  width: 200,
  height: 200,
  frames,
};

// Encode as APNG
const apngData = await Image.encodeFrames("apng", multiFrame);
await Deno.writeFile("animated.png", apngData);

Convert GIF to APNG

import { Image } from "@cross/image";

// Load GIF
const gifData = await Deno.readFile("animated.gif");
const multiFrame = await Image.decodeFrames(gifData);

// Re-encode as APNG (higher quality)
const apngData = await Image.encodeFrames("apng", multiFrame);
await Deno.writeFile("animated.png", apngData);

console.log("Converted GIF to APNG");

Multi-Page TIFFs

Decode TIFF Pages

import { Image } from "@cross/image";

// Decode all pages from multi-page TIFF
const tiffData = await Deno.readFile("document.tiff");
const multiPage = await Image.decodeFrames(tiffData);

console.log(`Number of pages: ${multiPage.frames.length}`);

// Access individual pages
for (let i = 0; i < multiPage.frames.length; i++) {
  const page = multiPage.frames[i];
  console.log(`Page ${i + 1}: ${page.width}x${page.height}`);
}

Extract TIFF Pages

import { Image } from "@cross/image";

const tiffData = await Deno.readFile("multipage.tiff");
const multiPage = await Image.decodeFrames(tiffData);

// Save each page as PNG
for (let i = 0; i < multiPage.frames.length; i++) {
  const page = multiPage.frames[i];
  const image = Image.fromRGBA(page.width, page.height, page.data);

  const filename = `page-${i + 1}.png`;
  await Deno.writeFile(filename, await image.encode("png"));

  console.log(`Saved ${filename}`);
}

Create Multi-Page TIFF

import { Image } from "@cross/image";

// Create pages
const page1 = Image.create(800, 600, 255, 255, 255);
page1.fillRect(100, 100, 200, 200, 255, 0, 0);

const page2 = Image.create(800, 600, 255, 255, 255);
page2.fillRect(200, 200, 200, 200, 0, 255, 0);

const page3 = Image.create(800, 600, 255, 255, 255);
page3.fillRect(300, 300, 200, 200, 0, 0, 255);

// Build multi-page structure
const multiPage = {
  width: 800,
  height: 600,
  frames: [
    { width: 800, height: 600, data: page1.data },
    { width: 800, height: 600, data: page2.data },
    { width: 800, height: 600, data: page3.data },
  ],
};

// Encode as multi-page TIFF with LZW compression
const tiffData = await Image.encodeFrames("tiff", multiPage, {
  compression: "lzw",
});
await Deno.writeFile("multipage.tiff", tiffData);

Create Document from Images

import { Image } from "@cross/image";

// Load individual images
const files = ["scan1.png", "scan2.png", "scan3.png"];
const pages = [];

for (const file of files) {
  const data = await Deno.readFile(file);
  const image = await Image.decode(data);

  pages.push({
    width: image.width,
    height: image.height,
    data: image.data,
  });
}

// Create multi-page TIFF
const multiPage = {
  width: pages[0].width,
  height: pages[0].height,
  frames: pages,
};

const tiffData = await Image.encodeFrames("tiff", multiPage, {
  compression: "lzw",
});
await Deno.writeFile("document.tiff", tiffData);

console.log(`Created document with ${pages.length} pages`);

Frame Manipulation

Resize All Frames

import { Image } from "@cross/image";

const gifData = await Deno.readFile("animated.gif");
const multiFrame = await Image.decodeFrames(gifData);

// Resize all frames
const targetSize = { width: 400, height: 300 };
const resizedFrames = multiFrame.frames.map((frame) => {
  const image = Image.fromRGBA(frame.width, frame.height, frame.data);
  image.resize(targetSize);

  return {
    width: targetSize.width,
    height: targetSize.height,
    data: image.data,
    frameMetadata: frame.frameMetadata,
  };
});

// Create resized animation
const resized = {
  width: targetSize.width,
  height: targetSize.height,
  frames: resizedFrames,
};

const output = await Image.encodeFrames("gif", resized);
await Deno.writeFile("resized.gif", output);

Apply Filter to Animation

import { Image } from "@cross/image";

const gifData = await Deno.readFile("animated.gif");
const multiFrame = await Image.decodeFrames(gifData);

// Apply grayscale to all frames
const filteredFrames = multiFrame.frames.map((frame) => {
  const image = Image.fromRGBA(frame.width, frame.height, frame.data);
  image.grayscale();

  return {
    width: frame.width,
    height: frame.height,
    data: image.data,
    frameMetadata: frame.frameMetadata,
  };
});

// Create filtered animation
const filtered = {
  width: multiFrame.width,
  height: multiFrame.height,
  frames: filteredFrames,
};

const output = await Image.encodeFrames("gif", filtered);
await Deno.writeFile("grayscale.gif", output);

Adjust Frame Delays

import { Image } from "@cross/image";

const gifData = await Deno.readFile("animated.gif");
const multiFrame = await Image.decodeFrames(gifData);

// Double all frame delays (slow down)
const slowedFrames = multiFrame.frames.map((frame) => ({
  ...frame,
  frameMetadata: {
    ...frame.frameMetadata,
    delay: (frame.frameMetadata?.delay || 100) * 2,
  },
}));

// Create slowed animation
const slowed = {
  width: multiFrame.width,
  height: multiFrame.height,
  frames: slowedFrames,
};

const output = await Image.encodeFrames("gif", slowed);
await Deno.writeFile("slowed.gif", output);

Batch Processing

Convert Multiple GIFs

import { Image } from "@cross/image";

const files = ["anim1.gif", "anim2.gif", "anim3.gif"];

for (const file of files) {
  const gifData = await Deno.readFile(file);
  const multiFrame = await Image.decodeFrames(gifData);

  // Convert to APNG
  const apngData = await Image.encodeFrames("apng", multiFrame);
  const outputFile = file.replace(".gif", ".png");
  await Deno.writeFile(outputFile, apngData);

  console.log(`Converted ${file} to ${outputFile}`);
}

Extract First Frames

import { Image } from "@cross/image";

const files = ["anim1.gif", "anim2.gif", "anim3.gif"];

for (const file of files) {
  const gifData = await Deno.readFile(file);
  const multiFrame = await Image.decodeFrames(gifData);

  // Extract first frame
  const frame = multiFrame.frames[0];
  const image = Image.fromRGBA(frame.width, frame.height, frame.data);

  const outputFile = file.replace(".gif", "-thumb.png");
  await Deno.writeFile(outputFile, await image.encode("png"));

  console.log(`Extracted thumbnail from ${file}`);
}

Node.js Examples

Decode GIF in Node.js

import { readFile, writeFile } from "node:fs/promises";
import { Image } from "@cross/image";

const gifData = await readFile("animated.gif");
const multiFrame = await Image.decodeFrames(gifData);

console.log(`Frames: ${multiFrame.frames.length}`);

// Extract first frame
const frame = multiFrame.frames[0];
const image = Image.fromRGBA(frame.width, frame.height, frame.data);

const pngData = await image.encode("png");
await writeFile("first-frame.png", pngData);

Create TIFF in Node.js

import { readFile, writeFile } from "node:fs/promises";
import { Image } from "@cross/image";

const page1Data = await readFile("page1.png");
const page1 = await Image.decode(page1Data);

const page2Data = await readFile("page2.png");
const page2 = await Image.decode(page2Data);

const multiPage = {
  width: page1.width,
  height: page1.height,
  frames: [
    { width: page1.width, height: page1.height, data: page1.data },
    { width: page2.width, height: page2.height, data: page2.data },
  ],
};

const tiffData = await Image.encodeFrames("tiff", multiPage, {
  compression: "lzw",
});
await writeFile("document.tiff", tiffData);

Bun Examples

Process GIF in Bun

import { Image } from "@cross/image";

const file = Bun.file("animated.gif");
const gifData = new Uint8Array(await file.arrayBuffer());
const multiFrame = await Image.decodeFrames(gifData);

console.log(`Frames: ${multiFrame.frames.length}`);

// Convert to APNG
const apngData = await Image.encodeFrames("apng", multiFrame);
await Bun.write("animated.png", apngData);