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.