Skip to content
Image KB Reducer
Esc
↑↓navigate↵open⌘Jpreview
On this page

Run a job

The recipe layer (compressJob, resizeJob, convertJob, cropJob, rotateJob) and runJob.

The recipe layer

Five functions turn “what you want” into the one ImageJob shape runJob actually executes — the same recipes the CLI’s compress/resize/convert/crop/rotate commands build from, so the library and the CLI produce the identical job for identical input. All five come from the root import (see Install for why there’s no separate ./jobs subpath to reach for).

import { compressJob, resizeJob, convertJob, cropJob, rotateJob, inspect } from "@imagekbreducer/image-core"

compressJob(source, options)

compressJob(source: SourceInfo, options: {
  targetBytes: number
  format?: ImageFormat | "keep"   // default "keep" (source's own format; heic -> jpeg)
  allowDownscale?: boolean        // default true
  maxEncodes?: number
  dpi?: number
}): ImageJob

The only recipe with a required byte target. format: "keep" resolves the way the website’s own converters do: the source’s own format, except HEIC, which is decode-only and can never be an output format.

resizeJob(source, spec, options)

resizeJob(source: SourceInfo, spec: ResizeSpec, options: {
  format?: ImageFormat | "keep"
  targetBytes?: number
  dpi?: number
  background?: string             // default "#ffffff"
}): ImageJob

spec is one of four modes:

type ResizeSpec =
  | { mode: "pixels"; width?: number; height?: number; fit: Fit }
  | { mode: "scale"; factor: number }
  | { mode: "longestEdge"; px: number }
  | { mode: "physical"; widthMm: number; heightMm: number; dpi: number; fit: Fit }

Without targetBytes, the resize re-encodes at a fixed quality of 90 — resizing is not compression, so a preset’s discretion never spends quality the caller didn’t ask to trade away.

convertJob(source, format, options)

convertJob(source: SourceInfo, format: ImageFormat, options: {
  targetBytes?: number
  quality?: number
  background?: string             // default "#ffffff"
}): ImageJob

cropJob(source, rect, options) / rotateJob(source, options)

cropJob(source: SourceInfo, rect: CropRect, options: { format?: ImageFormat | "keep"; quality?: number }): ImageJob
rotateJob(source: SourceInfo, options: { degrees?: 0 | 90 | 180 | 270; flip?: "x" | "y" | "both"; format?: ImageFormat | "keep"; quality?: number }): ImageJob

CropRect ({ x, y, width, height }) is in ORIENTED source pixels — post-EXIF, the coordinate space you’d actually see the photo in. Neither recipe takes targetBytes or background: a geometry-only edit changes no pixel values, so there’s no cross-format alpha loss to flatten and no byte target to negotiate for. Both default to a fixed re-encode quality of 92 — chosen to make an unavoidable JPEG re-encode pass (there’s no lossless path through a canvas) as small as the encoder can make it without visibly degrading a crop or rotate that users reasonably expect to look untouched.

runJob(job, ctx)

function runJob(job: ImageJob, ctx: RunContext): Promise<JobResult>
interface RunContext {
  file: BlobLike | Uint8Array   // the original, undecoded source bytes
  backend: PixelBackend         // canvas in a browser, wasm elsewhere -- see /library/backends
  signal?: AbortLike
  onProgress?: (event: ProgressEvent) => void
  resampleQuality?: "best" | "fast"   // default "best" (Lanczos3)
  maxSourcePixels?: number
  caps?: Capabilities
}

file is never required to already be materialized in memory — a File from a browser’s file picker travels through unread until the decode step actually needs its bytes, which matters on a memory-constrained tab (see the source’s own header comment on why nothing else stays alive longer than it has to).

JobResult: always something downloadable

type JobResult =
  | { ok: true; image: EncodedImage; warnings: Warning[] }
  | {
      ok: false
      error: { code: ErrorCode; message: string; reason?: string }
      partial?: EncodedImage
      warnings: Warning[]
      suggestions: Suggestion[]
    }

JobResult is a discriminated union on ok, not always a success — result.image only exists when result.ok, with no default it falls back to on failure. Check it before touching result.image, the way the example on Install does.

When result.ok, result.image.sizeBytes is at or under the target you asked for — runJob never overshoots on success, finding the highest quality that fits rather than a fixed quality that might not. A target runJob genuinely cannot reach doesn’t throw and doesn’t silently hand back an oversized file: it resolves with ok: false, and that failure carries the same negotiation the CLI’s --accept-closest/suggestion sentences are built from — see Give-up codes for what error.reason and suggestions mean and how to use them.

EncodedImage:

interface EncodedImage {
  data: Uint8Array
  format: ImageFormat
  width: number
  height: number
  sizeBytes: number
  quality?: number   // absent for png, which has no scalar quality knob
  encodes: number     // encode iterations the search performed; 1 for a non-"target" quality mode
}

warnings is never cosmetic — each entry names something the output no longer has (metadata, an ICC profile, an animation’s later frames) or a constraint the engine relaxed on your behalf (a downscale it applied to reach the target). Surface them; silence where one applies is the bug this array exists to prevent.

Was this page helpful?