mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-14 09:39:15 +00:00
Avoid preprocessing images that have already been preprocessed. (#125)
* Avoid preprocessing images that have already been preprocessed. * Using cleanMerge an cleanSet, and fixing bugs in our compression.
This commit is contained in:
@@ -35,16 +35,16 @@ import {
|
|||||||
} from '../../codecs/preprocessors';
|
} from '../../codecs/preprocessors';
|
||||||
|
|
||||||
import { decodeImage } from '../../codecs/decoders';
|
import { decodeImage } from '../../codecs/decoders';
|
||||||
import { cleanMerge } from '../../lib/clean-modify';
|
import { cleanMerge, cleanSet } from '../../lib/clean-modify';
|
||||||
|
|
||||||
interface SourceImage {
|
interface SourceImage {
|
||||||
file: File;
|
file: File;
|
||||||
bmp: ImageBitmap;
|
bmp: ImageBitmap;
|
||||||
data: ImageData;
|
data: ImageData;
|
||||||
preprocessed?: ImageData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EncodedImage {
|
interface EncodedImage {
|
||||||
|
preprocessed?: ImageData;
|
||||||
bmp?: ImageBitmap;
|
bmp?: ImageBitmap;
|
||||||
file?: File;
|
file?: File;
|
||||||
downloadUrl?: string;
|
downloadUrl?: string;
|
||||||
@@ -66,6 +66,10 @@ interface State {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UpdateImageOptions {
|
||||||
|
skipPreprocessing?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const filesize = partial({});
|
const filesize = partial({});
|
||||||
|
|
||||||
async function preprocessImage(
|
async function preprocessImage(
|
||||||
@@ -79,28 +83,22 @@ async function preprocessImage(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
async function compressImage(
|
async function compressImage(
|
||||||
source: SourceImage,
|
image: ImageData,
|
||||||
encodeData: EncoderState,
|
encodeData: EncoderState,
|
||||||
|
sourceFilename: string,
|
||||||
): Promise<File> {
|
): Promise<File> {
|
||||||
// Special case for identity
|
|
||||||
if (encodeData.type === identity.type) return source.file;
|
|
||||||
|
|
||||||
let sourceData = source.data;
|
|
||||||
if (source.preprocessed) {
|
|
||||||
sourceData = source.preprocessed;
|
|
||||||
}
|
|
||||||
const compressedData = await (() => {
|
const compressedData = await (() => {
|
||||||
switch (encodeData.type) {
|
switch (encodeData.type) {
|
||||||
case mozJPEG.type: return mozJPEG.encode(sourceData, encodeData.options);
|
case mozJPEG.type: return mozJPEG.encode(image, encodeData.options);
|
||||||
case webP.type: return webP.encode(sourceData, encodeData.options);
|
case webP.type: return webP.encode(image, encodeData.options);
|
||||||
case browserPNG.type: return browserPNG.encode(sourceData, encodeData.options);
|
case browserPNG.type: return browserPNG.encode(image, encodeData.options);
|
||||||
case browserJPEG.type: return browserJPEG.encode(sourceData, encodeData.options);
|
case browserJPEG.type: return browserJPEG.encode(image, encodeData.options);
|
||||||
case browserWebP.type: return browserWebP.encode(sourceData, encodeData.options);
|
case browserWebP.type: return browserWebP.encode(image, encodeData.options);
|
||||||
case browserGIF.type: return browserGIF.encode(sourceData, encodeData.options);
|
case browserGIF.type: return browserGIF.encode(image, encodeData.options);
|
||||||
case browserTIFF.type: return browserTIFF.encode(sourceData, encodeData.options);
|
case browserTIFF.type: return browserTIFF.encode(image, encodeData.options);
|
||||||
case browserJP2.type: return browserJP2.encode(sourceData, encodeData.options);
|
case browserJP2.type: return browserJP2.encode(image, encodeData.options);
|
||||||
case browserBMP.type: return browserBMP.encode(sourceData, encodeData.options);
|
case browserBMP.type: return browserBMP.encode(image, encodeData.options);
|
||||||
case browserPDF.type: return browserPDF.encode(sourceData, encodeData.options);
|
case browserPDF.type: return browserPDF.encode(image, encodeData.options);
|
||||||
default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`);
|
default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -109,7 +107,7 @@ async function compressImage(
|
|||||||
|
|
||||||
return new File(
|
return new File(
|
||||||
[compressedData],
|
[compressedData],
|
||||||
source.file.name.replace(/\..+$/, '.' + encoder.extension),
|
sourceFilename.replace(/\..+$/, '.' + encoder.extension),
|
||||||
{ type: encoder.mimeType },
|
{ type: encoder.mimeType },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -150,44 +148,25 @@ export default class App extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(
|
|
||||||
index: 0 | 1,
|
|
||||||
preprocessorState: PreprocessorState,
|
|
||||||
type: EncoderType,
|
|
||||||
options?: EncoderOptions,
|
|
||||||
): void {
|
|
||||||
// Some type cheating here.
|
|
||||||
// encoderMap[type].defaultOptions is always safe.
|
|
||||||
// options should always be correct for the type, but TypeScript isn't smart enough.
|
|
||||||
const encoderState: EncoderState = {
|
|
||||||
type,
|
|
||||||
options: options || encoderMap[type].defaultOptions,
|
|
||||||
} as EncoderState;
|
|
||||||
|
|
||||||
const images = cleanMerge(this.state.images, index, { encoderState, preprocessorState });
|
|
||||||
this.setState({ images });
|
|
||||||
}
|
|
||||||
|
|
||||||
onEncoderTypeChange(index: 0 | 1, newType: EncoderType): void {
|
onEncoderTypeChange(index: 0 | 1, newType: EncoderType): void {
|
||||||
this.onChange(index, this.state.images[index].preprocessorState, newType);
|
this.setState({
|
||||||
|
images: cleanSet(this.state.images, `${index}.encoderState`, {
|
||||||
|
type: newType,
|
||||||
|
options: encoderMap[newType].defaultOptions,
|
||||||
|
}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onPreprocessorOptionsChange(index: 0 | 1, options: PreprocessorState): void {
|
onPreprocessorOptionsChange(index: 0 | 1, options: PreprocessorState): void {
|
||||||
this.onChange(
|
this.setState({
|
||||||
index,
|
images: cleanSet(this.state.images, `${index}.preprocessorState`, options),
|
||||||
options,
|
});
|
||||||
this.state.images[index].encoderState.type,
|
|
||||||
this.state.images[index].encoderState.options,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEncoderOptionsChange(index: 0 | 1, options: EncoderOptions): void {
|
onEncoderOptionsChange(index: 0 | 1, options: EncoderOptions): void {
|
||||||
this.onChange(
|
this.setState({
|
||||||
index,
|
images: cleanSet(this.state.images, `${index}.encoderState.options`, options),
|
||||||
this.state.images[index].preprocessorState,
|
});
|
||||||
this.state.images[index].encoderState.type,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props, prevState: State): void {
|
componentDidUpdate(prevProps: Props, prevState: State): void {
|
||||||
@@ -195,12 +174,17 @@ export default class App extends Component<Props, State> {
|
|||||||
|
|
||||||
for (const [i, image] of images.entries()) {
|
for (const [i, image] of images.entries()) {
|
||||||
const prevImage = prevState.images[i];
|
const prevImage = prevState.images[i];
|
||||||
|
const sourceChanged = source !== prevState.source;
|
||||||
|
const encoderChanged = image.encoderState !== prevImage.encoderState;
|
||||||
|
const preprocessorChanged = image.preprocessorState !== prevImage.preprocessorState;
|
||||||
|
|
||||||
// The image only needs updated if the encoder settings have changed, or the source has
|
// The image only needs updated if the encoder settings have changed, or the source has
|
||||||
// changed.
|
// changed.
|
||||||
if (source !== prevState.source || image.encoderState !== prevImage.encoderState) {
|
if (sourceChanged || encoderChanged || preprocessorChanged) {
|
||||||
if (prevImage.downloadUrl) URL.revokeObjectURL(prevImage.downloadUrl);
|
if (prevImage.downloadUrl) URL.revokeObjectURL(prevImage.downloadUrl);
|
||||||
this.updateImage(i).catch((err) => {
|
this.updateImage(i, {
|
||||||
|
skipPreprocessing: !sourceChanged && !preprocessorChanged,
|
||||||
|
}).catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -240,7 +224,8 @@ export default class App extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateImage(index: number): Promise<void> {
|
async updateImage(index: number, options: UpdateImageOptions = {}): Promise<void> {
|
||||||
|
const { skipPreprocessing = false } = options;
|
||||||
const { source } = this.state;
|
const { source } = this.state;
|
||||||
if (!source) return;
|
if (!source) return;
|
||||||
|
|
||||||
@@ -258,8 +243,15 @@ export default class App extends Component<Props, State> {
|
|||||||
|
|
||||||
let file;
|
let file;
|
||||||
try {
|
try {
|
||||||
source.preprocessed = await preprocessImage(source, image.preprocessorState);
|
// Special case for identity
|
||||||
file = await compressImage(source, image.encoderState);
|
if (image.encoderState.type === identity.type) {
|
||||||
|
file = source.file;
|
||||||
|
} else {
|
||||||
|
if (!skipPreprocessing || !image.preprocessed) {
|
||||||
|
image.preprocessed = await preprocessImage(source, image.preprocessorState);
|
||||||
|
}
|
||||||
|
file = await compressImage(image.preprocessed, image.encoderState, source.file.name);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.showError(`Encoding error (type=${image.encoderState.type}): ${err}`);
|
this.showError(`Encoding error (type=${image.encoderState.type}): ${err}`);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -273,7 +265,7 @@ export default class App extends Component<Props, State> {
|
|||||||
|
|
||||||
let bmp;
|
let bmp;
|
||||||
try {
|
try {
|
||||||
bmp = await createImageBitmap(file);
|
bmp = await decodeImage(file);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: `Encoding error (type=${image.encoderState.type}): ${err}` });
|
this.setState({ error: `Encoding error (type=${image.encoderState.type}): ${err}` });
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
Reference in New Issue
Block a user