diff --git a/src/client/lazy-app/Compress/Output/index.tsx b/src/client/lazy-app/Compress/Output/index.tsx index 21597b20..caf7052d 100644 --- a/src/client/lazy-app/Compress/Output/index.tsx +++ b/src/client/lazy-app/Compress/Output/index.tsx @@ -5,7 +5,7 @@ import './custom-els/PinchZoom'; import './custom-els/TwoUp'; import * as style from './style.css'; import 'add-css:./style.css'; -import { shallowEqual, drawDataToCanvas } from '../../util'; +import { shallowEqual } from '../../util'; import { ToggleBackgroundIcon, AddIcon, @@ -18,6 +18,7 @@ import type { PreprocessorState } from '../../feature-meta'; import { cleanSet } from '../../util/clean-modify'; import type { SourceImage } from '../../Compress'; import { linkRef } from 'shared/prerendered-app/util'; +import { drawDataToCanvas } from 'client/lazy-app/util/canvas'; interface Props { source?: SourceImage; diff --git a/src/client/lazy-app/Compress/index.tsx b/src/client/lazy-app/Compress/index.tsx index b10cab67..4793a5ee 100644 --- a/src/client/lazy-app/Compress/index.tsx +++ b/src/client/lazy-app/Compress/index.tsx @@ -4,7 +4,6 @@ import * as style from './style.css'; import 'add-css:./style.css'; import { blobToImg, - drawableToImageData, blobToText, builtinDecode, sniffMimeType, @@ -33,6 +32,7 @@ import WorkerBridge from '../worker-bridge'; import { resize } from 'features/processors/resize/client'; import type SnackBarElement from 'shared/custom-els/snack-bar'; import { generateCliInvocation } from '../util/cli'; +import { drawableToImageData } from '../util/canvas'; export type OutputType = EncoderType | 'identity'; diff --git a/src/client/lazy-app/util/canvas.ts b/src/client/lazy-app/util/canvas.ts new file mode 100644 index 00000000..04b88a3d --- /dev/null +++ b/src/client/lazy-app/util/canvas.ts @@ -0,0 +1,154 @@ +/** Replace the contents of a canvas with the given data */ +export function drawDataToCanvas(canvas: HTMLCanvasElement, data: ImageData) { + const ctx = canvas.getContext('2d'); + if (!ctx) throw Error('Canvas not initialized'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.putImageData(data, 0, 0); +} + +/** + * Encode some image data in a given format using the browser's encoders + * + * @param {ImageData} data + * @param {string} type A mime type, eg image/jpeg. + * @param {number} [quality] Between 0-1. + */ +export async function canvasEncode( + data: ImageData, + type: string, + quality?: number, +): Promise { + const canvas = document.createElement('canvas'); + canvas.width = data.width; + canvas.height = data.height; + const ctx = canvas.getContext('2d'); + if (!ctx) throw Error('Canvas not initialized'); + ctx.putImageData(data, 0, 0); + + let blob: Blob | null; + + if ('toBlob' in canvas) { + blob = await new Promise((r) => + canvas.toBlob(r, type, quality), + ); + } else { + // Welcome to Edge. + // TypeScript thinks `canvas` is 'never', so it needs casting. + const dataUrl = (canvas as HTMLCanvasElement).toDataURL(type, quality); + const result = /data:([^;]+);base64,(.*)$/.exec(dataUrl); + + if (!result) throw Error('Data URL reading failed'); + + const outputType = result[1]; + const binaryStr = atob(result[2]); + const data = new Uint8Array(binaryStr.length); + + for (let i = 0; i < data.length; i += 1) { + data[i] = binaryStr.charCodeAt(i); + } + + blob = new Blob([data], { type: outputType }); + } + + if (!blob) throw Error('Encoding failed'); + return blob; +} + +interface DrawableToImageDataOptions { + width?: number; + height?: number; + sx?: number; + sy?: number; + sw?: number; + sh?: number; +} + +function getWidth( + drawable: ImageBitmap | HTMLImageElement | VideoFrame, +): number { + if ('displayWidth' in drawable) { + return drawable.displayWidth; + } + return drawable.width; +} + +function getHeight( + drawable: ImageBitmap | HTMLImageElement | VideoFrame, +): number { + if ('displayHeight' in drawable) { + return drawable.displayHeight; + } + return drawable.height; +} + +export function drawableToImageData( + drawable: ImageBitmap | HTMLImageElement | VideoFrame, + opts: DrawableToImageDataOptions = {}, +): ImageData { + const { + width = getWidth(drawable), + height = getHeight(drawable), + sx = 0, + sy = 0, + sw = getWidth(drawable), + sh = getHeight(drawable), + } = opts; + + // Make canvas same size as image + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + // Draw image onto canvas + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('Could not create canvas context'); + ctx.drawImage(drawable, sx, sy, sw, sh, 0, 0, width, height); + return ctx.getImageData(0, 0, width, height); +} + +export type BuiltinResizeMethod = 'pixelated' | 'low' | 'medium' | 'high'; + +export function builtinResize( + data: ImageData, + sx: number, + sy: number, + sw: number, + sh: number, + dw: number, + dh: number, + method: BuiltinResizeMethod, +): ImageData { + const canvasSource = document.createElement('canvas'); + canvasSource.width = data.width; + canvasSource.height = data.height; + drawDataToCanvas(canvasSource, data); + + const canvasDest = document.createElement('canvas'); + canvasDest.width = dw; + canvasDest.height = dh; + const ctx = canvasDest.getContext('2d'); + if (!ctx) throw new Error('Could not create canvas context'); + + if (method === 'pixelated') { + ctx.imageSmoothingEnabled = false; + } else { + ctx.imageSmoothingQuality = method; + } + + ctx.drawImage(canvasSource, sx, sy, sw, sh, 0, 0, dw, dh); + return ctx.getImageData(0, 0, dw, dh); +} + +/** + * Test whether can encode to a particular type. + */ +export async function canvasEncodeTest(mimeType: string): Promise { + try { + const blob = await canvasEncode(new ImageData(1, 1), mimeType); + // According to the spec, the blob should be null if the format isn't supported… + if (!blob) return false; + // …but Safari & Firefox fall back to PNG, so we need to check the mime type. + return blob.type === mimeType; + } catch (err) { + return false; + } +} diff --git a/src/client/lazy-app/util/index.ts b/src/client/lazy-app/util/index.ts index 317dc059..24a21f93 100644 --- a/src/client/lazy-app/util/index.ts +++ b/src/client/lazy-app/util/index.ts @@ -12,6 +12,7 @@ */ import * as WebCodecs from '../util/web-codecs'; +import { drawableToImageData } from './canvas'; /** * Compare two objects, returning a boolean indicating if @@ -23,62 +24,6 @@ export function shallowEqual(one: any, two: any) { return true; } -/** Replace the contents of a canvas with the given data */ -export function drawDataToCanvas(canvas: HTMLCanvasElement, data: ImageData) { - const ctx = canvas.getContext('2d'); - if (!ctx) throw Error('Canvas not initialized'); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.putImageData(data, 0, 0); -} - -/** - * Encode some image data in a given format using the browser's encoders - * - * @param {ImageData} data - * @param {string} type A mime type, eg image/jpeg. - * @param {number} [quality] Between 0-1. - */ -export async function canvasEncode( - data: ImageData, - type: string, - quality?: number, -): Promise { - const canvas = document.createElement('canvas'); - canvas.width = data.width; - canvas.height = data.height; - const ctx = canvas.getContext('2d'); - if (!ctx) throw Error('Canvas not initialized'); - ctx.putImageData(data, 0, 0); - - let blob: Blob | null; - - if ('toBlob' in canvas) { - blob = await new Promise((r) => - canvas.toBlob(r, type, quality), - ); - } else { - // Welcome to Edge. - // TypeScript thinks `canvas` is 'never', so it needs casting. - const dataUrl = (canvas as HTMLCanvasElement).toDataURL(type, quality); - const result = /data:([^;]+);base64,(.*)$/.exec(dataUrl); - - if (!result) throw Error('Data URL reading failed'); - - const outputType = result[1]; - const binaryStr = atob(result[2]); - const data = new Uint8Array(binaryStr.length); - - for (let i = 0; i < data.length; i += 1) { - data[i] = binaryStr.charCodeAt(i); - } - - blob = new Blob([data], { type: outputType }); - } - - if (!blob) throw Error('Encoding failed'); - return blob; -} - async function decodeImage(url: string): Promise { const img = new Image(); img.decoding = 'async'; @@ -186,57 +131,6 @@ export async function blobToImg(blob: Blob): Promise { } } -interface DrawableToImageDataOptions { - width?: number; - height?: number; - sx?: number; - sy?: number; - sw?: number; - sh?: number; -} - -function getWidth( - drawable: ImageBitmap | HTMLImageElement | VideoFrame, -): number { - if ('displayWidth' in drawable) { - return drawable.displayWidth; - } - return drawable.width; -} - -function getHeight( - drawable: ImageBitmap | HTMLImageElement | VideoFrame, -): number { - if ('displayHeight' in drawable) { - return drawable.displayHeight; - } - return drawable.height; -} - -export function drawableToImageData( - drawable: ImageBitmap | HTMLImageElement | VideoFrame, - opts: DrawableToImageDataOptions = {}, -): ImageData { - const { - width = getWidth(drawable), - height = getHeight(drawable), - sx = 0, - sy = 0, - sw = getWidth(drawable), - sh = getHeight(drawable), - } = opts; - - // Make canvas same size as image - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - // Draw image onto canvas - const ctx = canvas.getContext('2d'); - if (!ctx) throw new Error('Could not create canvas context'); - ctx.drawImage(drawable, sx, sy, sw, sh, 0, 0, width, height); - return ctx.getImageData(0, 0, width, height); -} - export async function builtinDecode( signal: AbortSignal, blob: Blob, @@ -259,39 +153,6 @@ export async function builtinDecode( return drawableToImageData(drawable); } -export type BuiltinResizeMethod = 'pixelated' | 'low' | 'medium' | 'high'; - -export function builtinResize( - data: ImageData, - sx: number, - sy: number, - sw: number, - sh: number, - dw: number, - dh: number, - method: BuiltinResizeMethod, -): ImageData { - const canvasSource = document.createElement('canvas'); - canvasSource.width = data.width; - canvasSource.height = data.height; - drawDataToCanvas(canvasSource, data); - - const canvasDest = document.createElement('canvas'); - canvasDest.width = dw; - canvasDest.height = dh; - const ctx = canvasDest.getContext('2d'); - if (!ctx) throw new Error('Could not create canvas context'); - - if (method === 'pixelated') { - ctx.imageSmoothingEnabled = false; - } else { - ctx.imageSmoothingQuality = method; - } - - ctx.drawImage(canvasSource, sx, sy, sw, sh, 0, 0, dw, dh); - return ctx.getImageData(0, 0, dw, dh); -} - /** * @param field An HTMLInputElement, but the casting is done here to tidy up onChange. * @param defaultVal Value to return if 'field' doesn't exist. @@ -434,18 +295,3 @@ export async function abortable( }), ]); } - -/** - * Test whether can encode to a particular type. - */ -export async function canvasEncodeTest(mimeType: string): Promise { - try { - const blob = await canvasEncode(new ImageData(1, 1), mimeType); - // According to the spec, the blob should be null if the format isn't supported… - if (!blob) return false; - // …but Safari & Firefox fall back to PNG, so we need to check the mime type. - return blob.type === mimeType; - } catch (err) { - return false; - } -} diff --git a/src/client/lazy-app/util/web-codecs/index.ts b/src/client/lazy-app/util/web-codecs/index.ts index c0c39291..7f2b1949 100644 --- a/src/client/lazy-app/util/web-codecs/index.ts +++ b/src/client/lazy-app/util/web-codecs/index.ts @@ -1,12 +1,19 @@ -import { drawableToImageData } from 'client/lazy-app/util'; +import { drawableToImageData } from '../canvas'; const hasImageDecoder = typeof ImageDecoder !== 'undefined'; + export async function isTypeSupported(mimeType: string): Promise { - if (!hasImageDecoder) { + if (!hasImageDecoder) return false; + // Some old versions of this API threw here. + // It only impacted folks with experimental web platform flags enabled in Chrome 90. + // The API was updated in Chrome 91. + try { + return await ImageDecoder.isTypeSupported(mimeType); + } catch (err) { return false; } - return ImageDecoder.isTypeSupported(mimeType); } + export async function decode( blob: Blob | File, mimeType: string, diff --git a/src/features/encoders/browserGIF/client/index.ts b/src/features/encoders/browserGIF/client/index.ts index 5f4af200..ad7d5300 100644 --- a/src/features/encoders/browserGIF/client/index.ts +++ b/src/features/encoders/browserGIF/client/index.ts @@ -1,4 +1,4 @@ -import { canvasEncodeTest, canvasEncode } from 'client/lazy-app/util'; +import { canvasEncodeTest, canvasEncode } from 'client/lazy-app/util/canvas'; import WorkerBridge from 'client/lazy-app/worker-bridge'; import { EncodeOptions, mimeType } from '../shared/meta'; diff --git a/src/features/encoders/browserJPEG/client/index.ts b/src/features/encoders/browserJPEG/client/index.ts index 8837c93f..9c77f241 100644 --- a/src/features/encoders/browserJPEG/client/index.ts +++ b/src/features/encoders/browserJPEG/client/index.ts @@ -1,4 +1,4 @@ -import { canvasEncode } from 'client/lazy-app/util'; +import { canvasEncode } from 'client/lazy-app/util/canvas'; import WorkerBridge from 'client/lazy-app/worker-bridge'; import { qualityOption } from 'features/client-utils'; import { mimeType, EncodeOptions } from '../shared/meta'; diff --git a/src/features/encoders/browserPNG/client/index.ts b/src/features/encoders/browserPNG/client/index.ts index 3b893551..a3fcd19f 100644 --- a/src/features/encoders/browserPNG/client/index.ts +++ b/src/features/encoders/browserPNG/client/index.ts @@ -1,4 +1,4 @@ -import { canvasEncode } from 'client/lazy-app/util'; +import { canvasEncode } from 'client/lazy-app/util/canvas'; import WorkerBridge from 'client/lazy-app/worker-bridge'; import { EncodeOptions, mimeType } from '../shared/meta'; diff --git a/src/features/encoders/oxiPNG/client/index.tsx b/src/features/encoders/oxiPNG/client/index.tsx index 51fc5885..43eae515 100644 --- a/src/features/encoders/oxiPNG/client/index.tsx +++ b/src/features/encoders/oxiPNG/client/index.tsx @@ -1,5 +1,5 @@ +import { canvasEncode } from 'client/lazy-app/util/canvas'; import { - canvasEncode, abortable, blobToArrayBuffer, inputFieldChecked, diff --git a/src/features/processors/resize/client/index.tsx b/src/features/processors/resize/client/index.tsx index a60dd9d7..4203fe1c 100644 --- a/src/features/processors/resize/client/index.tsx +++ b/src/features/processors/resize/client/index.tsx @@ -2,7 +2,7 @@ import { builtinResize, BuiltinResizeMethod, drawableToImageData, -} from 'client/lazy-app/util'; +} from 'client/lazy-app/util/canvas'; import { BrowserResizeOptions, VectorResizeOptions,