UNPKG

one

Version:

One is a React framework for web, iOS, and Android apps.

106 lines (103 loc) • 4.37 kB
import { missingNativeBuild } from '../nativeError' import type { ImageTransformOptions, ImageTransformResult, } from '../specs/OneImageManipulator.nitro' export type { ImageCrop, OneImageFlip as ImageFlip, OneImageFormat as ImageFormat, ImageResize, ImageTransformResult, } from '../specs/OneImageManipulator.nitro' export type ImageManipulatorOptions = Omit<ImageTransformOptions, 'format'> & { format?: ImageTransformOptions['format'] } function pixels(value: number, zero = false): number { if (!Number.isInteger(value) || value < (zero ? 0 : 1) || value > 100_000) throw new Error('ImageManipulator.transform: expected a whole pixel count') return value } function size(width: number, height: number): void { if (width > 10_000 || height > 10_000 || width * height > 25_000_000) throw new Error( 'ImageManipulator.transform: output is limited to 25 megapixels and 10,000 pixels per side' ) } async function transform( uri: string, options: ImageManipulatorOptions = {} ): Promise<ImageTransformResult> { if (typeof window === 'undefined') throw missingNativeBuild('ImageManipulator.transform') const { crop, resize, rotate = 0, flip, format = 'jpeg', quality } = options if (!['jpeg', 'png', 'webp'].includes(format) || ![0, 90, 180, 270].includes(rotate)) throw new Error('ImageManipulator.transform: invalid format or rotation') if (flip != null && flip !== 'horizontal' && flip !== 'vertical') throw new Error("ImageManipulator.transform: flip must be 'horizontal' or 'vertical'") if ( (format === 'png' && quality != null) || (quality != null && (!Number.isFinite(quality) || quality < 0 || quality > 1)) ) throw new Error( 'ImageManipulator.transform: quality applies only to JPEG or WebP and must be between 0 and 1' ) const image = new window.Image() image.crossOrigin = 'anonymous' image.src = uri await image.decode() const x = pixels(crop?.x ?? 0, true), y = pixels(crop?.y ?? 0, true) const cw = pixels(crop?.width ?? image.naturalWidth), ch = pixels(crop?.height ?? image.naturalHeight) if (x + cw > image.naturalWidth || y + ch > image.naturalHeight) throw new Error('ImageManipulator.transform: crop must fit inside the source image') if (resize && resize.width == null && resize.height == null) throw new Error('ImageManipulator.transform: resize needs a width or height') let width = resize?.width == null ? cw : pixels(resize.width) let height = resize?.height == null ? ch : pixels(resize.height) if (resize?.width == null && resize?.height != null) width = Math.max(1, Math.round((cw * height) / ch)) if (resize?.height == null && resize?.width != null) height = Math.max(1, Math.round((ch * width) / cw)) size(width, height) const canvas = document.createElement('canvas') canvas.width = rotate === 90 || rotate === 270 ? height : width canvas.height = rotate === 90 || rotate === 270 ? width : height const context = canvas.getContext('2d') if (!context) throw new Error('ImageManipulator.transform: canvas is unavailable') // canvas transforms apply to the drawn image last-first, so the flip goes // before the rotation here to mirror the already rotated image, as native does. context.translate(canvas.width / 2, canvas.height / 2) if (flip === 'horizontal') context.scale(-1, 1) else if (flip === 'vertical') context.scale(1, -1) context.rotate((rotate * Math.PI) / 180) let source: CanvasImageSource = image if (crop) { const cropped = document.createElement('canvas') cropped.width = cw cropped.height = ch const cropContext = cropped.getContext('2d') if (!cropContext) throw new Error('ImageManipulator.transform: canvas is unavailable') cropContext.drawImage(image, x, y, cw, ch, 0, 0, cw, ch) source = cropped } context.drawImage(source, -width / 2, -height / 2, width, height) const blob = await new Promise<Blob>((resolve, reject) => canvas.toBlob( (value) => value ? resolve(value) : reject(new Error('ImageManipulator.transform: encoding failed')), `image/${format}`, quality ?? 0.9 ) ) return { uri: URL.createObjectURL(blob), width: canvas.width, height: canvas.height, size: blob.size, } } export const ImageManipulator = Object.freeze({ transform })