browser
The package's one client-safe subpath — HEIC transcode, EXIF read, and orientation bake for a File before upload.
What it does
@takazudo/ is a client-side pre-upload pipeline: given a File picked in a browser, it transcodes HEIC/HEIF to JPEG, reads EXIF capture date and orientation, bakes any needed rotation into the pixels via canvas, and measures the final dimensions — so the bytes leaving the browser need no further EXIF-orientation handling downstream.
Client-safe guarantee
No sharp, no node: builtins
Every other subpath in this package assumes a Node/sharp environment./ is the one exception, and it's enforced by an automated build check: the built / chunk is inspected for sharp imports or anynode: specifier, and the check fails if either appears — including a transitive import introduced by a future change, not just a direct one.
Exports
function prepareImageForUpload(
file: File,
opts?: PrepareImageForUploadOptions,
): Promise<PreparedImage>;
function deriveGeometry(w: number, h: number): DerivedGeometry;
function deriveOrientation(w: number, h: number): Orientation;
function roundAspectRatio(w: number, h: number): number;
function needsOrientationBake(exifOrientation: number | undefined): boolean;
const HEIC_DECODE_FAILED_MESSAGE: string;
const ORIENTATION_BAKE_FAILED_MESSAGE_PREFIX: string;
type Orientation = 'landscape' | 'portrait' | 'square';
interface Dimensions {
w: number;
h: number;
}
interface DerivedGeometry {
dimensions: Dimensions;
aspectRatio: number;
orientation: Orientation;
}prepareImageForUpload— the pipeline entry point; see HEIC keep-raw fallback below for its exact step order.deriveGeometry/deriveOrientation/roundAspectRatio— pure dimension utilities, framework-agnostic so they're unit-testable without touching the DOM or the Canvas API.roundAspectRatiorounds to 4 decimal places and returns0(notInfinity/NaN) whenhis0or either input is non-finite — callers should treat a0aspect ratio as "unknown."needsOrientationBake— decides whether a raw EXIFOrientationtag value (1-8) requires a canvas rotate/flip before upload.1(orundefined) means already upright;2-8need a bake.
Optional peers
exifr and heic2any are both dynamic-imported only when prepareImageForUpload actually runs, so they're never forced into a consumer's bundle otherwise. Install them only if you use this module:
pnpm add exifr heic2anySee Security & Heavy Dependencies for what each optional peer is used for, and Installation for the general optional-peer pattern this package uses.
Options
PrepareImageForUploadOptions
| Name | Type | Default | Effect |
|---|---|---|---|
heicQuality | number | 0.9 | JPEG quality (0-1) used when transcoding HEIC/HEIF to JPEG via heic2any. |
bakeQuality | number | 0.92 | Quality (0-1) passed to the canvas re-encode after an orientation bake. Applies to JPEG and WebP output (both lossy); ignored for PNG output, which stays lossless. |
PreparedImage result
| Name | Type | Effect |
|---|---|---|
file | File | Blob | Upload-ready bytes: JPEG when HEIC was transcoded; when orientation was baked, the canvas re-encode keeps the original PNG/WebP MIME type or falls back to JPEG for any other type; otherwise the original file untouched. |
width / height | number | Measured dimensions of the final bytes. |
takenAt | Date | null | EXIF capture date, if present and parseable. |
orientation | Orientation | Derived from the final width/height. |
transcodedFromHeic | boolean | Whether the HEIC → JPEG transcode succeeded. |
Example
import { prepareImageForUpload } from '@takazudo/zudo-image-tweaker/browser';
async function handleFileSelect(file: File) {
const { file: uploadFile, width, height, orientation, transcodedFromHeic } =
await prepareImageForUpload(file);
console.log(`${width}x${height} ${orientation}, transcoded from HEIC: ${transcodedFromHeic}`);
return uploadFile;
}HEIC keep-raw fallback
prepareImageForUpload never lets a HEIC transcode failure block the upload outright — it degrades in stages:
Detect.
isHeic(file)checks the file's MIME type or filename extension for HEIC/HEIF.Transcode, with graceful fallback. If HEIC,
heic2anyattempts a JPEG transcode. On failure, the raw HEIC bytes are kept as-is (transcodedFromHeicstaysfalse) rather than throwing immediately — a later step, or the server, may still be able to deal with them.Read EXIF from the original file, always.
heic2anystrips all metadata from its JPEG output, so readingtakenAtfrom the transcoded blob would silently lose it. TheOrientationtag is requested only for non-transcoded bytes — a successfulheic2anytranscode already bakes rotation into its own JPEG output, so re-checking orientation on those bytes would compare against metadata that's no longer meaningful.Bake orientation (if needed) or just measure dimensions. If the final decode step fails:
and the input was raw, untranscoded HEIC (the browser can't natively decode HEIC either), it throws
HEIC_DECODE_FAILED_MESSAGErather than silently returning a0x0placeholder;or if an orientation bake was required (EXIF said the image isn't upright) but failed, it throws
`${ORIENTATION_BAKE_FAILED_MESSAGE_PREFIX}${message}`, again rather than silently shipping un-rotated bytes under a placeholder that pretends nothing is wrong;otherwise (a non-HEIC, no-bake-needed input that still fails to decode — unexpected, but handled defensively) it keeps a
0x0placeholder, preserving existing behavior for callers/servers that validate dimensions themselves.
Caveats
Canvas-based rotation, not a hand-rolled matrix. Baking delegates the actual rotate/flip math to
createImageBitmap(blob, { imageOrientation: 'from-image' }), which every evergreen browser implements, instead of hand-rolling the 8-case EXIF orientation matrix.An orientation bake preserves the original's PNG/WebP MIME type; every other type re-encodes as JPEG. Only the PNG case is actually lossless — a WebP source is still re-encoded through canvas's lossy WebP path and does honor
bakeQuality, it just isn't converted to JPEG the way a non-PNG/WebP source is.exifris asked not to translate values.exifr's default (translateValues: true) turns the numericOrientationtag into a human-readable string (e.g."Rotate 90 CW"); this module requests raw values soneedsOrientationBake's numeric check actually matches.A failed or absent EXIF read never throws —
exifr.parseerrors (malformed or missing EXIF) are caught internally and resolve to{ takenAt: null, exifOrientation: undefined }.