stablyai/orca · error · Error

Spritesheet ${dims.width}×${dims.height} is not a clean mult

Error message

Spritesheet ${dims.width}×${dims.height} is not a clean multiple of frame ${fw}×${fh}.

What it means

Thrown at pet.ts:340-343 when the decoded spritesheet's pixel width or height is not evenly divisible by the manifest's frame.width / frame.height. The sprite engine slices the sheet into a grid of frame cells, so the sheet dimensions must be exact integer multiples of the frame size. columns and rows (computed at lines 345-346) would otherwise be fractional.

Source

Thrown at src/main/ipc/pet.ts:341

        `Spritesheet is too large (${(sheetStat.size / (1024 * 1024)).toFixed(1)} MB).`
      )
    }

    let sprite: NonNullable<CustomPet['sprite']> | undefined
    if (manifest.frame) {
      // Why: only decode when a frame layout needs validating — nativeImage can fail on some WebP variants in headless contexts.
      const sheetBuf = await readFile(sheetSrc)
      // Why: defend against TOCTOU — file may have grown between stat and read.
      if (sheetBuf.byteLength > MAX_BYTES) {
        throw new Error('Spritesheet exceeded the size limit.')
      }
      const dims = await readSheetDimensions(sheetBuf)
      if (!dims) {
        throw new Error('Could not decode the spritesheet image.')
      }
      const { width: fw, height: fh } = manifest.frame
      if (dims.width % fw !== 0 || dims.height % fh !== 0) {
        throw new Error(
          `Spritesheet ${dims.width}×${dims.height} is not a clean multiple of frame ${fw}×${fh}.`
        )
      }
      const columns = dims.width / fw
      const rows = dims.height / fh
      if (manifest.animations) {
        for (const [name, anim] of Object.entries(manifest.animations)) {
          if (anim.row >= rows) {
            throw new Error(`Animation "${name}" references row ${anim.row} but sheet has ${rows}.`)
          }
          if (anim.frames > columns) {
            throw new Error(
              `Animation "${name}" has ${anim.frames} frames but sheet only has ${columns} columns.`
            )
          }
          if (anim.frameDurationsMs && anim.frameDurationsMs.length !== anim.frames) {
            throw new Error(
              `Animation "${name}" declares ${anim.frameDurationsMs.length} frame durations but ${anim.frames} frames.`

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Set frame.width and frame.height in pet.json to divisors of the actual image dimensions (the message tells you both numbers).
  2. Or re-export the image so its dimensions are exact multiples of the intended frame size.
  3. Double-check the frame size matches the art (e.g. a 256x256 sheet with 32x32 frames gives 8x8 grid).

Example fix

// before — 100x100 sheet, 32x32 frame (100%32 != 0)
{ "frame": { "width": 32, "height": 32 } }
// after — re-export sheet at 96x96 (96/32=3) and keep frame 32x32
{ "frame": { "width": 32, "height": 32 } }
Defensive patterns

Strategy: validation

Validate before calling

async function assertCleanFrameMultiple(imagePath: string, frameW: number, frameH: number) {
  const img = nativeImage.createFromBuffer(await readFile(imagePath))
  const { width, height } = img.getSize()
  if (!width || !height) throw new Error('cannot read dimensions')
  if (width % frameW !== 0 || height % frameH !== 0) throw new Error(`sheet ${width}x${height} not a multiple of frame ${frameW}x${frameH}`)
}

Try / catch

try { await importPetBundle(p) }
catch (e) { if (e instanceof Error && e.message.includes('not a clean multiple of frame')) { /* fix frame size or re-export sheet */ } else throw e }

Prevention

When it happens

Trigger: pet.json declares frame {width:32,height:32} but the image is 100x100 (100/32 is not integer), or the image dimensions were changed after the manifest was written, or the frame size was picked wrong.

Common situations: Author eyeballed the frame size; the sheet was re-exported at a different resolution without updating frame; off-by-one (e.g. 33px frames instead of 32).

Related errors


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/0937553b0b44bc78. Report an issue: GitHub.