From a7a08b68c5aae01c95243acb35ab4a59aa90a0ec Mon Sep 17 00:00:00 2001 From: nd0ut Date: Fri, 16 Feb 2024 21:15:01 +0300 Subject: [PATCH] chore: refactor image-shrink types and unwrap promise chains --- .../src/utils/IccProfile/getIccProfile.ts | 4 +- .../src/utils/IccProfile/replaceIccProfile.ts | 5 +- .../src/utils/IccProfile/stripIccProfile.ts | 14 +- .../src/utils/canvas/canvasResize.ts | 38 ++--- .../src/utils/canvas/canvasToBlob.ts | 17 ++- .../src/utils/canvas/testCanvasSize.ts | 35 ++--- .../src/utils/exif/findExifOrientation.ts | 14 +- .../image-shrink/src/utils/exif/getExif.ts | 17 +-- .../src/utils/exif/isBrowserApplyExif.ts | 3 +- .../src/utils/exif/replaceExif.ts | 10 +- .../src/utils/image/JPEG/readJpegChunks.ts | 37 +++-- .../src/utils/image/JPEG/replaceJpegChunk.ts | 2 +- .../image-shrink/src/utils/render/fallback.ts | 50 +++--- .../image-shrink/src/utils/render/native.ts | 7 + .../src/utils/shouldSkipShrink.ts | 4 +- packages/image-shrink/src/utils/shrinkFile.ts | 142 +++++++++--------- .../image-shrink/src/utils/shrinkImage.ts | 44 +++--- tsconfig.json | 2 +- 18 files changed, 229 insertions(+), 216 deletions(-) diff --git a/packages/image-shrink/src/utils/IccProfile/getIccProfile.ts b/packages/image-shrink/src/utils/IccProfile/getIccProfile.ts index 3f8a8d97..cbb24044 100644 --- a/packages/image-shrink/src/utils/IccProfile/getIccProfile.ts +++ b/packages/image-shrink/src/utils/IccProfile/getIccProfile.ts @@ -1,10 +1,10 @@ import { readJpegChunks } from '../image/JPEG/readJpegChunks' -export const getIccProfile = async (file: File) => { +export const getIccProfile = async (blob: Blob) => { const iccProfile: DataView[] = [] const { promiseReadJpegChunks, stack } = readJpegChunks() - return await promiseReadJpegChunks(file) + return await promiseReadJpegChunks(blob) .then(() => { stack.forEach(({ marker, view }) => { if (marker === 0xe2) { diff --git a/packages/image-shrink/src/utils/IccProfile/replaceIccProfile.ts b/packages/image-shrink/src/utils/IccProfile/replaceIccProfile.ts index 81abc879..6fccd3ae 100644 --- a/packages/image-shrink/src/utils/IccProfile/replaceIccProfile.ts +++ b/packages/image-shrink/src/utils/IccProfile/replaceIccProfile.ts @@ -1,10 +1,7 @@ import { replaceJpegChunk } from '../image/JPEG/replaceJpegChunk' export const MARKER = 0xe2 -export const replaceIccProfile = ( - blob: Blob | File, - iccProfiles: DataView[] -) => { +export const replaceIccProfile = (blob: Blob, iccProfiles: DataView[]) => { return replaceJpegChunk( blob, MARKER, diff --git a/packages/image-shrink/src/utils/IccProfile/stripIccProfile.ts b/packages/image-shrink/src/utils/IccProfile/stripIccProfile.ts index f8b28163..4f7d17b7 100644 --- a/packages/image-shrink/src/utils/IccProfile/stripIccProfile.ts +++ b/packages/image-shrink/src/utils/IccProfile/stripIccProfile.ts @@ -1,17 +1,9 @@ import { replaceIccProfile } from './replaceIccProfile' -import { imageLoader } from '../image/imageLoader' -export const stripIccProfile = async ( - inputFile: File -): Promise => { +export const stripIccProfile = async (blob: Blob): Promise => { try { - const file = await replaceIccProfile(inputFile, []) - const image = await imageLoader(URL.createObjectURL(file as Blob)) - - URL.revokeObjectURL(image.src) - - return image + return await replaceIccProfile(blob, []) } catch (e) { - throw new Error(`Failed to strip ICC profile and not image ${e}`) + throw new Error(`Failed to strip ICC profile: ${e}`) } } diff --git a/packages/image-shrink/src/utils/canvas/canvasResize.ts b/packages/image-shrink/src/utils/canvas/canvasResize.ts index fd7124c9..e611abfe 100644 --- a/packages/image-shrink/src/utils/canvas/canvasResize.ts +++ b/packages/image-shrink/src/utils/canvas/canvasResize.ts @@ -1,24 +1,28 @@ import { createCanvas } from './createCanvas' -export const canvasResize = (img: CanvasImageSource, w: number, h: number) => { - return new Promise((resolve, reject) => { - try { - const { ctx, canvas } = createCanvas() +export const canvasResize = async ( + img: CanvasImageSource, + w: number, + h: number +) => { + try { + const { ctx, canvas } = createCanvas() - canvas.width = w - canvas.height = h + canvas.width = w + canvas.height = h - ctx.imageSmoothingQuality = 'high' - ctx.drawImage(img, 0, 0, w, h) + ctx.imageSmoothingQuality = 'high' + ctx.drawImage(img, 0, 0, w, h) - // @ts-expect-error TODO: fix this - img.src = '//:0' // for image - // @ts-expect-error TODO: fix this - img.width = img.height = 1 // for canvas - - resolve(canvas) - } catch (e) { - reject(`Failed to resize image. ${e}`) + if (img instanceof HTMLImageElement) { + img.src = '//:0' // free memory + } + if (img instanceof HTMLCanvasElement) { + img.width = img.height = 1 // free memory } - }) + + return canvas + } catch (e) { + throw new Error('Canvas resize error', { cause: e }) + } } diff --git a/packages/image-shrink/src/utils/canvas/canvasToBlob.ts b/packages/image-shrink/src/utils/canvas/canvasToBlob.ts index 5518b96f..8e8e7005 100644 --- a/packages/image-shrink/src/utils/canvas/canvasToBlob.ts +++ b/packages/image-shrink/src/utils/canvas/canvasToBlob.ts @@ -1,8 +1,17 @@ export const canvasToBlob = ( canvas: HTMLCanvasElement, type: string, - quality: number | undefined, - callback: BlobCallback -): void => { - return canvas.toBlob(callback, type, quality) + quality: number | undefined +): Promise => { + return new Promise((resolve, reject) => { + const callback: BlobCallback = (blob) => { + if (!blob) { + reject('Failed to convert canvas to blob') + return + } + resolve(blob) + } + canvas.toBlob(callback, type, quality) + canvas.width = canvas.height = 1 + }) } diff --git a/packages/image-shrink/src/utils/canvas/testCanvasSize.ts b/packages/image-shrink/src/utils/canvas/testCanvasSize.ts index 5361196f..b1c82dc4 100644 --- a/packages/image-shrink/src/utils/canvas/testCanvasSize.ts +++ b/packages/image-shrink/src/utils/canvas/testCanvasSize.ts @@ -16,25 +16,22 @@ function wrapAsync(fn: (...args: A) => R) { const squareTest = wrapAsync(memoize(canvasTest, memoKeySerializer)) const dimensionTest = wrapAsync(memoize(canvasTest, memoKeySerializer)) -export const testCanvasSize = (w: number, h: number) => { - return new Promise((resolve, reject) => { - const testSquareSide = sizes.squareSide.find((side) => side * side >= w * h) - const testDimension = sizes.dimension.find((side) => side >= w && side >= h) +export const testCanvasSize = async (w: number, h: number) => { + const testSquareSide = sizes.squareSide.find((side) => side * side >= w * h) + const testDimension = sizes.dimension.find((side) => side >= w && side >= h) - if (!testSquareSide || !testDimension) { - reject() - return - } + if (!testSquareSide || !testDimension) { + throw new Error('Not supported') + } - Promise.all([ - squareTest(testSquareSide, testSquareSide), - dimensionTest(testDimension, 1) - ]).then(([squareSupported, dimensionSupported]) => { - if (squareSupported && dimensionSupported) { - resolve(true) - } else { - reject() - } - }) - }) + const [squareSupported, dimensionSupported] = await Promise.all([ + squareTest(testSquareSide, testSquareSide), + dimensionTest(testDimension, 1) + ]) + + if (squareSupported && dimensionSupported) { + return true + } else { + throw new Error('Not supported') + } } diff --git a/packages/image-shrink/src/utils/exif/findExifOrientation.ts b/packages/image-shrink/src/utils/exif/findExifOrientation.ts index d4a54d36..da47055b 100644 --- a/packages/image-shrink/src/utils/exif/findExifOrientation.ts +++ b/packages/image-shrink/src/utils/exif/findExifOrientation.ts @@ -1,7 +1,6 @@ -// TODO: rename to littleEndian export const findExifOrientation = ( exif: DataView, - exifCallback: (offset: number, little: boolean) => void + exifCallback: (offset: number, littleEndian: boolean) => void ) => { let j, little, offset, ref if ( @@ -10,28 +9,27 @@ export const findExifOrientation = ( exif.getUint32(0) !== 0x45786966 || exif.getUint16(4) !== 0 ) { - return null + return } if (exif.getUint16(6) === 0x4949) { little = true } else if (exif.getUint16(6) === 0x4d4d) { little = false } else { - return null + return } if (exif.getUint16(8, little) !== 0x002a) { - return null + return } offset = 8 + exif.getUint32(10, little) const count = exif.getUint16(offset - 2, little) for (j = 0, ref = count; ref >= 0 ? j < ref : j > ref; ref >= 0 ? ++j : --j) { if (exif.byteLength < offset + 10) { - return null + return } if (exif.getUint16(offset, little) === 0x0112) { - return exifCallback(offset + 8, little) + exifCallback(offset + 8, little) } offset += 12 } - return null } diff --git a/packages/image-shrink/src/utils/exif/getExif.ts b/packages/image-shrink/src/utils/exif/getExif.ts index 716a0fb7..47fad820 100644 --- a/packages/image-shrink/src/utils/exif/getExif.ts +++ b/packages/image-shrink/src/utils/exif/getExif.ts @@ -1,27 +1,26 @@ import { readJpegChunks } from '../image/JPEG/readJpegChunks' -export const getExif = async (file: File) => { - let isExif: DataView | null = null +export const getExif = async (blob: Blob) => { + let exif: DataView | null = null const { promiseReadJpegChunks, stack } = readJpegChunks() - return promiseReadJpegChunks(file) + return promiseReadJpegChunks(blob) .then(() => { stack.forEach(({ marker, view }) => { - if (!isExif && marker === 0xe1) { + if (!exif && marker === 0xe1) { if (view.byteLength >= 14) { if ( // check for "Exif\0" view.getUint32(0) === 0x45786966 && view.getUint16(4) === 0 ) { - isExif = view - return isExif + exif = view + return } } } - - return isExif }) + return exif }) - .catch(() => isExif) + .catch(() => exif) } diff --git a/packages/image-shrink/src/utils/exif/isBrowserApplyExif.ts b/packages/image-shrink/src/utils/exif/isBrowserApplyExif.ts index 57208a47..5a8bfd75 100644 --- a/packages/image-shrink/src/utils/exif/isBrowserApplyExif.ts +++ b/packages/image-shrink/src/utils/exif/isBrowserApplyExif.ts @@ -1,3 +1,4 @@ +// 2x1 pixel image 90CW rotated with orientation header const base64ImageSrc = 'data:image/jpg;base64,' + '/9j/4AAQSkZJRgABAQEASABIAAD/4QA6RXhpZgAATU0AKgAAAAgAAwESAAMAAAABAAYAAAEo' + @@ -7,7 +8,7 @@ const base64ImageSrc = let isApplied: boolean | undefined = undefined -export const isBrowserApplyExif = () => { +export const isBrowserApplyExifOrientation = () => { return new Promise((resolve) => { if (isApplied !== undefined) { resolve(isApplied) diff --git a/packages/image-shrink/src/utils/exif/replaceExif.ts b/packages/image-shrink/src/utils/exif/replaceExif.ts index b3b2171a..52601f23 100644 --- a/packages/image-shrink/src/utils/exif/replaceExif.ts +++ b/packages/image-shrink/src/utils/exif/replaceExif.ts @@ -2,14 +2,12 @@ import { replaceJpegChunk } from '../image/JPEG/replaceJpegChunk' import { findExifOrientation } from './findExifOrientation' export const setExifOrientation = (exif: DataView, orientation: number) => { - // TODO: rename to littleEndian - findExifOrientation(exif, (offset, little) => - exif.setUint16(offset, orientation, little) + findExifOrientation(exif, (offset, littleEndian) => + exif.setUint16(offset, orientation, littleEndian) ) } export const replaceExif = async ( - // TODO: rename to blob - file: Blob, + blob: Blob, exif: DataView, isExifApplied: boolean ) => { @@ -17,5 +15,5 @@ export const replaceExif = async ( setExifOrientation(exif, 1) } - return replaceJpegChunk(file, 0xe1, [exif.buffer]) + return replaceJpegChunk(blob, 0xe1, [exif.buffer]) } diff --git a/packages/image-shrink/src/utils/image/JPEG/readJpegChunks.ts b/packages/image-shrink/src/utils/image/JPEG/readJpegChunks.ts index 903225f7..e4b070e4 100644 --- a/packages/image-shrink/src/utils/image/JPEG/readJpegChunks.ts +++ b/packages/image-shrink/src/utils/image/JPEG/readJpegChunks.ts @@ -7,11 +7,10 @@ type TChunk = { export const readJpegChunks = () => { const stack: TChunk[] = [] - // TODO: rename to blob - const promiseReadJpegChunks = (file: Blob) => - new Promise((resolve, reject) => { - let pos: number - const readToView = (file: Blob, cb: (view: DataView) => void) => { + const promiseReadJpegChunks = (blob: Blob) => + new Promise((resolve, reject) => { + let pos = 2 + const readToView = (blob: Blob, cb: (view: DataView) => void) => { const reader = new FileReader() reader.addEventListener('load', () => { @@ -22,12 +21,11 @@ export const readJpegChunks = () => { reject(`Reader error: ${e}`) }) - reader.readAsArrayBuffer(file) + reader.readAsArrayBuffer(blob) } - // @ts-expect-error TODO: fix this const readNext = () => - readToView(file.slice(pos, pos + 128), (view: DataView) => { + readToView(blob.slice(pos, pos + 128), (view: DataView) => { let i, j, ref for ( i = j = 0, ref = view.byteLength; @@ -40,34 +38,36 @@ export const readJpegChunks = () => { } } - return readNextChunk() + readNextChunk() }) - // @ts-expect-error TODO: fix this const readNextChunk = () => { const startPos = pos - return readToView(file.slice(pos, (pos += 4)), (view: DataView) => { + return readToView(blob.slice(pos, (pos += 4)), (view: DataView) => { if (view.byteLength !== 4 || view.getUint8(0) !== 0xff) { - return reject('Corrupted') + reject('Corrupted') + return } const marker = view?.getUint8(1) if (marker === 0xda) { - return resolve(true) + resolve(true) + return } const length = view.getUint16(2) - 2 return readToView( - file.slice(pos, (pos += length)), + blob.slice(pos, (pos += length)), (view: DataView) => { if (view.byteLength !== length) { - return reject('Corrupted') + reject('Corrupted') + return } stack.push({ startPos, length, marker, view }) - return readNext() + readNext() } ) }) @@ -77,13 +77,12 @@ export const readJpegChunks = () => { reject('Not Support') } - pos = 2 - readToView(file.slice(0, 2), function (view: DataView) { + readToView(blob.slice(0, 2), (view: DataView) => { if (view.getUint16(0) !== 0xffd8) { reject('Not jpeg') } - return readNext() + readNext() }) }) diff --git a/packages/image-shrink/src/utils/image/JPEG/replaceJpegChunk.ts b/packages/image-shrink/src/utils/image/JPEG/replaceJpegChunk.ts index 001fa60e..8d57887a 100644 --- a/packages/image-shrink/src/utils/image/JPEG/replaceJpegChunk.ts +++ b/packages/image-shrink/src/utils/image/JPEG/replaceJpegChunk.ts @@ -5,7 +5,7 @@ export const replaceJpegChunk = ( marker: number, chunks: ArrayBuffer[] ) => { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const oldChunkPos: number[] = [] const oldChunkLength: number[] = [] diff --git a/packages/image-shrink/src/utils/render/fallback.ts b/packages/image-shrink/src/utils/render/fallback.ts index 6ab485d7..f553e49e 100644 --- a/packages/image-shrink/src/utils/render/fallback.ts +++ b/packages/image-shrink/src/utils/render/fallback.ts @@ -1,6 +1,14 @@ import { testCanvasSize } from '../canvas/testCanvasSize' import { canvasResize } from '../canvas/canvasResize' +/** + * Goes from target to source by step, the last incomplete step is dropped. + * Always returns at least one step - target. Source step is not included. + * Sorted descending. + * + * Example with step = 0.71, source = 2000, target = 400 400 (target) <- 563 <- + * 793 <- 1117 <- 1574 (dropped) <- [2000 (source)] + */ const calcShrinkSteps = function ( sourceW: number, targetW: number, @@ -24,6 +32,15 @@ const calcShrinkSteps = function ( return steps.reverse() } +/** + * Fallback resampling algorithm + * + * Reduces dimensions by step until reaches target dimensions, this gives a + * better output quality than one-step method + * + * Target dimensions expected to be supported by browser, unsupported steps will + * be dropped. + */ export const fallback = ({ img, sourceW, @@ -39,24 +56,17 @@ export const fallback = ({ }): Promise => { const steps = calcShrinkSteps(sourceW, targetW, targetH, step) - return ( - steps - // @ts-expect-error TODO: fix this - .reduce((chain, [w, h]) => { - return chain - .then((canvas) => { - return testCanvasSize(w, h) - .then(() => canvas) - .catch(() => canvasResize(canvas, w, h)) - }) - .then((canvas) => { - const progress = (sourceW - w) / (sourceW - targetW) - return { canvas, progress } - }) - }, Promise.resolve(img)) - // @ts-expect-error TODO: fix this - .then(({ canvas }) => canvas) - // @ts-expect-error TODO: remove this - .catch((error) => Promise.reject(error)) - ) + return steps.reduce( + (chain, [w, h]) => { + return chain.then((canvas) => { + return ( + testCanvasSize(w, h) + .then(() => canvasResize(canvas, w, h)) + // Here we assume that at least one step will be supported and HTMLImageElement will be converted to HTMLCanvasElement + .catch(() => canvas as unknown as HTMLCanvasElement) + ) + }) + }, + Promise.resolve(img as HTMLCanvasElement | HTMLImageElement) + ) as Promise } diff --git a/packages/image-shrink/src/utils/render/native.ts b/packages/image-shrink/src/utils/render/native.ts index efaa6f53..6a7d2dc3 100644 --- a/packages/image-shrink/src/utils/render/native.ts +++ b/packages/image-shrink/src/utils/render/native.ts @@ -1,5 +1,12 @@ import { canvasResize } from '../canvas/canvasResize' +/** + * Native high-quality canvas resampling + * + * Browser support: + * https://caniuse.com/mdn-api_canvasrenderingcontext2d_imagesmoothingenabled + * Target dimensions expected to be supported by browser. + */ export const native = ({ img, targetW, diff --git a/packages/image-shrink/src/utils/shouldSkipShrink.ts b/packages/image-shrink/src/utils/shouldSkipShrink.ts index 99ac17a3..ba427770 100644 --- a/packages/image-shrink/src/utils/shouldSkipShrink.ts +++ b/packages/image-shrink/src/utils/shouldSkipShrink.ts @@ -1,12 +1,12 @@ import { readJpegChunks } from './image/JPEG/readJpegChunks' import { allowLayers, markers } from '../constants' -export const shouldSkipShrink = async (file: File) => { +export const shouldSkipShrink = async (blob: Blob) => { let skip = false const { promiseReadJpegChunks, stack } = readJpegChunks() - return await promiseReadJpegChunks(file) + return await promiseReadJpegChunks(blob) .then(() => { stack.forEach(({ marker, view }) => { if (!skip && markers.indexOf(marker) >= 0) { diff --git a/packages/image-shrink/src/utils/shrinkFile.ts b/packages/image-shrink/src/utils/shrinkFile.ts index 2366e22e..33bdb824 100644 --- a/packages/image-shrink/src/utils/shrinkFile.ts +++ b/packages/image-shrink/src/utils/shrinkFile.ts @@ -3,97 +3,99 @@ import { stripIccProfile } from './IccProfile/stripIccProfile' import { shouldSkipShrink } from './shouldSkipShrink' import { canvasToBlob } from './canvas/canvasToBlob' import { hasTransparency } from './canvas/hasTransparency' -import { isBrowserApplyExif } from './exif/isBrowserApplyExif' +import { isBrowserApplyExifOrientation } from './exif/isBrowserApplyExif' import { getExif } from './exif/getExif' import { getIccProfile } from './IccProfile/getIccProfile' import { replaceExif } from './exif/replaceExif' import { replaceIccProfile } from './IccProfile/replaceIccProfile' +import { imageLoader } from './image/imageLoader' export type TSetting = { size: number quality?: number } -export const shrinkFile = (file: File, settings: TSetting): Promise => { - /*eslint no-async-promise-executor: "off"*/ - return new Promise(async (resolve, reject) => { - if (!(URL && DataView && Blob)) { - reject('Not support') +export const shrinkFile = async ( + inputBlob: Blob, + settings: TSetting +): Promise => { + try { + const shouldSkip = await shouldSkipShrink(inputBlob) + if (shouldSkip) { + throw new Error('Should skipped') } + inputBlob = await stripIccProfile(inputBlob) - try { - const image = await shouldSkipShrink(file) - .then((shouldSkip) => { - if (shouldSkip) { - return reject('Should skipped') - } - }) - .then(() => { - return stripIccProfile(file).catch(() => { - reject('Failed to strip ICC profile and not image') - }) - }) + // Try to extract EXIF and ICC profile + const exifResults = await Promise.allSettled([ + getExif(inputBlob), + isBrowserApplyExifOrientation(), + getIccProfile(inputBlob) + ]) - const exifList = Promise.allSettled([ - getExif(file), - isBrowserApplyExif(), - getIccProfile(file) - ]) + const isRejected = exifResults.some( + (result) => result.status === 'rejected' + ) + // If any of the promises is rejected, this is not a JPEG image + const isJPEG = !isRejected - exifList.then(async (results) => { - const isRejected = results.some( - (result) => result.status === 'rejected' - ) + const [exifResult, isExifOrientationAppliedResult, iccProfileResult] = + exifResults - const [exif, isExifApplied, iccProfile] = results as { - // TODO: fix this - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: any - status: string - }[] - const isJPEG = !isRejected + // Load blob into the image + const image = await imageLoader(URL.createObjectURL(inputBlob)) + URL.revokeObjectURL(image.src) - return shrinkImage(image as HTMLImageElement, settings) - .then(async (canvas) => { - let format = 'image/jpeg' - let quality: number | undefined = settings?.quality || 0.8 + // Shrink the image + const canvas = await shrinkImage(image, settings) - if (!isJPEG && hasTransparency(canvas)) { - format = 'image/png' - quality = undefined - } + let format = 'image/jpeg' + let quality: number | undefined = settings?.quality || 0.8 - canvasToBlob(canvas, format, quality, (blob) => { - canvas.width = canvas.height = 1 + if (!isJPEG && hasTransparency(canvas)) { + format = 'image/png' + quality = undefined + } - let replaceChain = Promise.resolve(blob) + // Convert canvas to blob + const newBlob = await canvasToBlob(canvas, format, quality) - if (exif.value) { - // @ts-expect-error TODO: fix this - replaceChain = replaceChain - .then((blob) => - // @ts-expect-error TODO: fix this - replaceExif(blob, exif.value, isExifApplied.value) - ) - .catch(() => blob) - } + const replaceChain = Promise.resolve(newBlob) - if (iccProfile?.value?.length > 0) { - // @ts-expect-error TODO: fix this - replaceChain = replaceChain - // @ts-expect-error TODO: fix this - .then((blob) => replaceIccProfile(blob, iccProfile.value)) - .catch(() => blob) - } + // Set EXIF for the new blob + if (exifResult.status === 'fulfilled' && exifResult.value) { + const exif = exifResult.value + const isExifOrientationApplied = + isExifOrientationAppliedResult.status === 'fulfilled' + ? isExifOrientationAppliedResult.value + : false + replaceChain + .then((blob) => replaceExif(blob, exif, isExifOrientationApplied)) + .catch(() => newBlob) + } - // @ts-expect-error TODO: fix this - replaceChain.then(resolve).catch(() => resolve(blob)) - }) - }) - .catch(() => reject(file)) - }) - } catch (e) { - reject(`Failed to shrink image: ${e}`) + // Set ICC profile for the new blob + if ( + iccProfileResult.status === 'fulfilled' && + iccProfileResult.value.length > 0 + ) { + replaceChain + .then((blob) => replaceIccProfile(blob, iccProfileResult.value)) + .catch(() => newBlob) + } + + return replaceChain + } catch (e) { + let message: string | undefined + if (e instanceof Error) { + message = e.message + } + if (typeof e === 'string') { + message = e } - }) + throw new Error( + `Failed to shrink image. ${message ? `Message: "${message}".` : ''}`, + { cause: e } + ) + } } diff --git a/packages/image-shrink/src/utils/shrinkImage.ts b/packages/image-shrink/src/utils/shrinkImage.ts index a203c4d4..a5a37c43 100644 --- a/packages/image-shrink/src/utils/shrinkImage.ts +++ b/packages/image-shrink/src/utils/shrinkImage.ts @@ -11,31 +11,31 @@ export const shrinkImage = ( img: HTMLImageElement, settings: TSetting ): Promise => { - return new Promise((resolve, reject) => { - if (img.width * STEP * img.height * STEP < settings.size) { - reject('Not required') - } + // do not shrink image if original resolution / target resolution ratio falls behind 2.0 + if (img.width * STEP * img.height * STEP < settings.size) { + throw new Error('Not required') + } - const sourceW = img.width - const sourceH = img.height - const ratio = sourceW / sourceH + const sourceW = img.width + const sourceH = img.height + const ratio = sourceW / sourceH - // target size shouldn't be greater than settings.size in any case - const targetW = Math.floor(Math.sqrt(settings.size * ratio)) - const targetH = Math.floor(settings.size / Math.sqrt(settings.size * ratio)) + // target size shouldn't be greater than settings.size in any case + const targetW = Math.floor(Math.sqrt(settings.size * ratio)) + const targetH = Math.floor(settings.size / Math.sqrt(settings.size * ratio)) - return testCanvasSize(targetW, targetH) - .then(() => { - const { ctx } = createCanvas() - const supportNative = 'imageSmoothingQuality' in ctx + // we test the last step because we can skip all intermediate steps + return testCanvasSize(targetW, targetH) + .then(() => { + const { ctx } = createCanvas() + const supportNative = 'imageSmoothingQuality' in ctx - const useNativeScaling = supportNative && !isIOS() && !isIpadOS + // native scaling on ios gives blurry results + const useNativeScaling = supportNative && !isIOS() && !isIpadOS - return useNativeScaling - ? native({ img, targetW, targetH }) - : fallback({ img, sourceW, targetW, targetH, step: STEP }) - }) - .then((canvas) => resolve(canvas)) - .catch(() => reject('Not supported')) - }) + return useNativeScaling + ? native({ img, targetW, targetH }) + : fallback({ img, sourceW, targetW, targetH, step: STEP }) + }) + .catch(() => Promise.reject('Not supported')) } diff --git a/tsconfig.json b/tsconfig.json index ed54eea4..05be5bf8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ESNext", "module": "ESNext", - "lib": ["DOM", "DOM.Iterable"], + "lib": ["DOM", "DOM.Iterable", "ESNext"], "noImplicitAny": true, "strictNullChecks": true, "strict": true,