mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-15 18:19:47 +00:00
Compress module done (aside from imports)
This commit is contained in:
@@ -38,7 +38,7 @@ type OutputType = EncoderType | 'identity';
|
|||||||
export interface SourceImage {
|
export interface SourceImage {
|
||||||
file: File;
|
file: File;
|
||||||
decoded: ImageData;
|
decoded: ImageData;
|
||||||
processed: ImageData;
|
preprocessed: ImageData;
|
||||||
vectorImage?: HTMLImageElement;
|
vectorImage?: HTMLImageElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,10 +74,6 @@ interface State {
|
|||||||
encodedPreprocessorState?: PreprocessorState;
|
encodedPreprocessorState?: PreprocessorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateImageOptions {
|
|
||||||
skipPreprocessing?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MainJob {
|
interface MainJob {
|
||||||
file: File;
|
file: File;
|
||||||
preprocessorState: PreprocessorState;
|
preprocessorState: PreprocessorState;
|
||||||
@@ -142,7 +138,7 @@ async function processImage(
|
|||||||
workerBridge: WorkerBridge,
|
workerBridge: WorkerBridge,
|
||||||
): Promise<ImageData> {
|
): Promise<ImageData> {
|
||||||
assertSignal(signal);
|
assertSignal(signal);
|
||||||
let result = source.processed;
|
let result = source.preprocessed;
|
||||||
|
|
||||||
if (processorState.resize.enabled) {
|
if (processorState.resize.enabled) {
|
||||||
result = await resize(signal, source, processorState.resize, workerBridge);
|
result = await resize(signal, source, processorState.resize, workerBridge);
|
||||||
@@ -253,6 +249,7 @@ export default class Compress extends Component<Props, State> {
|
|||||||
state: State = {
|
state: State = {
|
||||||
source: undefined,
|
source: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
preprocessorState: defaultPreprocessorState,
|
||||||
sides: [
|
sides: [
|
||||||
{
|
{
|
||||||
latestSettings: {
|
latestSettings: {
|
||||||
@@ -282,29 +279,14 @@ export default class Compress extends Component<Props, State> {
|
|||||||
private mainAbortController = new AbortController();
|
private mainAbortController = new AbortController();
|
||||||
// And again one for each side
|
// And again one for each side
|
||||||
private sideAbortControllers = [new AbortController(), new AbortController()];
|
private sideAbortControllers = [new AbortController(), new AbortController()];
|
||||||
|
/** For debouncing calls to updateImage for each side. */
|
||||||
// For debouncing calls to updateImage for each side.
|
private updateImageTimeout?: number;
|
||||||
private readonly updateImageTimeoutIds: [number?, number?] = [
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.widthQuery.addListener(this.onMobileWidthChange);
|
this.widthQuery.addListener(this.onMobileWidthChange);
|
||||||
this.sourceFile = props.file;
|
this.sourceFile = props.file;
|
||||||
this.updateJob(
|
this.queueUpdateImage({ immediate: true });
|
||||||
props.file,
|
|
||||||
defaultPreprocessorState,
|
|
||||||
this.state.sides.map((side) => side.latestSettings.processorState) as [
|
|
||||||
ProcessorState,
|
|
||||||
ProcessorState,
|
|
||||||
],
|
|
||||||
this.state.sides.map((side) => side.latestSettings.encoderState) as [
|
|
||||||
EncoderState,
|
|
||||||
EncoderState,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
import('../sw-bridge').then(({ mainAppLoaded }) => mainAppLoaded());
|
import('../sw-bridge').then(({ mainAppLoaded }) => mainAppLoaded());
|
||||||
}
|
}
|
||||||
@@ -353,7 +335,8 @@ export default class Compress extends Component<Props, State> {
|
|||||||
|
|
||||||
componentWillReceiveProps(nextProps: Props): void {
|
componentWillReceiveProps(nextProps: Props): void {
|
||||||
if (nextProps.file !== this.props.file) {
|
if (nextProps.file !== this.props.file) {
|
||||||
this.updateFile(nextProps.file);
|
this.sourceFile = nextProps.file;
|
||||||
|
this.queueUpdateImage({ immediate: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,31 +345,7 @@ export default class Compress extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props, prevState: State): void {
|
componentDidUpdate(prevProps: Props, prevState: State): void {
|
||||||
const { source, sides } = this.state;
|
this.queueUpdateImage();
|
||||||
|
|
||||||
const sourceDataChanged =
|
|
||||||
// Has the source object become set/unset?
|
|
||||||
!!source !== !!prevState.source ||
|
|
||||||
// Or has the processed data changed?
|
|
||||||
(source &&
|
|
||||||
prevState.source &&
|
|
||||||
source.processed !== prevState.source.processed);
|
|
||||||
|
|
||||||
for (const [i, side] of sides.entries()) {
|
|
||||||
const prevSettings = prevState.sides[i].latestSettings;
|
|
||||||
const encoderChanged =
|
|
||||||
side.latestSettings.encoderState !== prevSettings.encoderState;
|
|
||||||
const preprocessorChanged =
|
|
||||||
side.latestSettings.processorState !== prevSettings.processorState;
|
|
||||||
|
|
||||||
// The image only needs updated if the encoder/preprocessor settings have changed, or the
|
|
||||||
// source has changed.
|
|
||||||
if (sourceDataChanged || encoderChanged || preprocessorChanged) {
|
|
||||||
this.queueUpdateImage(i, {
|
|
||||||
skipPreprocessing: !sourceDataChanged && !preprocessorChanged,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onCopyToOtherClick(index: 0 | 1) {
|
private async onCopyToOtherClick(index: 0 | 1) {
|
||||||
@@ -417,260 +376,51 @@ export default class Compress extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onPreprocessorChange = async (
|
private onPreprocessorChange = async (
|
||||||
options: PreprocessorState,
|
preprocessorState: PreprocessorState,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const source = this.state.source;
|
const source = this.state.source;
|
||||||
if (!source) return;
|
if (!source) return;
|
||||||
|
|
||||||
const oldRotate = source.preprocessorState.rotate.rotate;
|
const oldRotate = this.state.preprocessorState.rotate.rotate;
|
||||||
const newRotate = options.rotate.rotate;
|
const newRotate = preprocessorState.rotate.rotate;
|
||||||
const orientationChanged = oldRotate % 180 !== newRotate % 180;
|
const orientationChanged = oldRotate % 180 !== newRotate % 180;
|
||||||
// Either worker bridge is good enough here.
|
|
||||||
const workerBridge = this.workerBridges[0];
|
|
||||||
|
|
||||||
// Abort any current jobs, as they're redundant now.
|
this.setState((state) => ({
|
||||||
for (const controller of [
|
|
||||||
this.mainAbortController,
|
|
||||||
...this.sideAbortControllers,
|
|
||||||
]) {
|
|
||||||
controller.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mainAbortController = new AbortController();
|
|
||||||
const { signal } = this.mainAbortController;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
loading: true,
|
loading: true,
|
||||||
// TODO: this is wrong
|
preprocessorState,
|
||||||
source: cleanSet(source, 'inputProcessorState', options),
|
// Flip resize values if orientation has changed
|
||||||
});
|
sides: !orientationChanged
|
||||||
|
? state.sides
|
||||||
try {
|
: (state.sides.map((side) => {
|
||||||
const processed = await preprocessImage(
|
const currentResizeSettings =
|
||||||
signal,
|
side.latestSettings.processorState.resize;
|
||||||
source.decoded,
|
const resizeSettings: Partial<ProcessorState['resize']> = {
|
||||||
options,
|
width: currentResizeSettings.height,
|
||||||
workerBridge,
|
height: currentResizeSettings.width,
|
||||||
);
|
};
|
||||||
|
return cleanMerge(
|
||||||
let newState = { ...this.state, loading: false };
|
side,
|
||||||
newState = cleanSet(newState, 'source.processed', processed);
|
'latestSettings.processorState.resize',
|
||||||
newState = stateForNewSourceData(newState);
|
resizeSettings,
|
||||||
|
);
|
||||||
if (orientationChanged) {
|
}) as [Side, Side]),
|
||||||
// If orientation has changed, we should flip the resize values.
|
}));
|
||||||
for (const i of [0, 1]) {
|
|
||||||
const resizeSettings =
|
|
||||||
newState.sides[i].latestSettings.processorState.resize;
|
|
||||||
newState = cleanMerge(
|
|
||||||
newState,
|
|
||||||
`sides.${i}.latestSettings.processorState.resize`,
|
|
||||||
{
|
|
||||||
width: resizeSettings.height,
|
|
||||||
height: resizeSettings.width,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.setState(newState);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.name === 'AbortError') return;
|
|
||||||
console.error(err);
|
|
||||||
this.props.showSnack('Processing error');
|
|
||||||
this.setState({ loading: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private updateFile = async (file: File) => {
|
|
||||||
// Either processor is good enough here.
|
|
||||||
const workerBridge = this.workerBridges[0];
|
|
||||||
|
|
||||||
this.setState({ loading: true });
|
|
||||||
|
|
||||||
// Abort any current jobs, as they're redundant now.
|
|
||||||
for (const controller of [
|
|
||||||
this.mainAbortController,
|
|
||||||
...this.sideAbortControllers,
|
|
||||||
]) {
|
|
||||||
controller.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mainAbortController = new AbortController();
|
|
||||||
const { signal } = this.mainAbortController;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let decoded: ImageData;
|
|
||||||
let vectorImage: HTMLImageElement | undefined;
|
|
||||||
|
|
||||||
// Special-case SVG. We need to avoid createImageBitmap because of
|
|
||||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=606319.
|
|
||||||
// Also, we cache the HTMLImageElement so we can perform vector resizing later.
|
|
||||||
if (file.type.startsWith('image/svg+xml')) {
|
|
||||||
vectorImage = await processSvg(signal, file);
|
|
||||||
decoded = drawableToImageData(vectorImage);
|
|
||||||
} else {
|
|
||||||
// Either processor is good enough here.
|
|
||||||
decoded = await decodeImage(signal, file, workerBridge);
|
|
||||||
}
|
|
||||||
|
|
||||||
const processed = await preprocessImage(
|
|
||||||
signal,
|
|
||||||
decoded,
|
|
||||||
defaultPreprocessorState,
|
|
||||||
workerBridge,
|
|
||||||
);
|
|
||||||
|
|
||||||
let newState: State = {
|
|
||||||
...this.state,
|
|
||||||
source: {
|
|
||||||
decoded,
|
|
||||||
file,
|
|
||||||
vectorImage,
|
|
||||||
processed,
|
|
||||||
preprocessorState: defaultPreprocessorState,
|
|
||||||
},
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
newState = stateForNewSourceData(newState);
|
|
||||||
|
|
||||||
for (const i of [0, 1]) {
|
|
||||||
// Default resize values come from the image:
|
|
||||||
newState = cleanMerge(
|
|
||||||
newState,
|
|
||||||
`sides.${i}.latestSettings.processorState.resize`,
|
|
||||||
{
|
|
||||||
width: processed.width,
|
|
||||||
height: processed.height,
|
|
||||||
method: vectorImage ? 'vector' : 'lanczos3',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDocumentTitle(file.name);
|
|
||||||
this.setState(newState);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.name === 'AbortError') return;
|
|
||||||
console.error(err);
|
|
||||||
this.props.showSnack('Invalid image');
|
|
||||||
this.setState({ loading: false });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounce the heavy lifting of updateImage.
|
* Debounce the heavy lifting of updateImage.
|
||||||
* Otherwise, the thrashing causes jank, and sometimes crashes iOS Safari.
|
* Otherwise, the thrashing causes jank, and sometimes crashes iOS Safari.
|
||||||
*/
|
*/
|
||||||
private queueUpdateImage(
|
private queueUpdateImage({ immediate }: { immediate?: boolean } = {}): void {
|
||||||
index: number,
|
// Call updateImage after this delay, unless queueUpdateImage is called
|
||||||
options: UpdateImageOptions = {},
|
// again, in which case the timeout is reset.
|
||||||
): void {
|
|
||||||
// Call updateImage after this delay, unless queueUpdateImage is called again, in which case the
|
|
||||||
// timeout is reset.
|
|
||||||
const delay = 100;
|
const delay = 100;
|
||||||
|
|
||||||
clearTimeout(this.updateImageTimeoutIds[index]);
|
clearTimeout(this.updateImageTimeout);
|
||||||
|
if (immediate) {
|
||||||
this.updateImageTimeoutIds[index] = self.setTimeout(() => {
|
this.updateImage();
|
||||||
this.updateImage(index, options).catch((err) => {
|
} else {
|
||||||
console.error(err);
|
this.updateImageTimeout = setTimeout(() => this.updateImage(), delay);
|
||||||
});
|
|
||||||
}, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateImage(
|
|
||||||
index: number,
|
|
||||||
options: UpdateImageOptions = {},
|
|
||||||
): Promise<void> {
|
|
||||||
const { skipPreprocessing } = options;
|
|
||||||
const { source } = this.state;
|
|
||||||
if (!source) return;
|
|
||||||
|
|
||||||
// Abort any current tasks on this side
|
|
||||||
this.sideAbortControllers[index].abort();
|
|
||||||
this.sideAbortControllers[index] = new AbortController();
|
|
||||||
const { signal } = this.sideAbortControllers[index];
|
|
||||||
|
|
||||||
let sides = cleanMerge(this.state.sides, index, {
|
|
||||||
loading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({ sides });
|
|
||||||
|
|
||||||
const side = sides[index];
|
|
||||||
const settings = side.latestSettings;
|
|
||||||
|
|
||||||
let file: File | undefined;
|
|
||||||
let preprocessed: ImageData | undefined;
|
|
||||||
let data: ImageData | undefined;
|
|
||||||
|
|
||||||
const workerBridge = this.workerBridges[index];
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!settings.encoderState) {
|
|
||||||
// Original image
|
|
||||||
file = source.file;
|
|
||||||
data = source.processed;
|
|
||||||
} else {
|
|
||||||
const cacheResult = this.encodeCache.match(
|
|
||||||
source.processed,
|
|
||||||
settings.processorState,
|
|
||||||
settings.encoderState,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (cacheResult) {
|
|
||||||
({ file, preprocessed, data } = cacheResult);
|
|
||||||
} else {
|
|
||||||
preprocessed =
|
|
||||||
skipPreprocessing && side.processed
|
|
||||||
? side.processed
|
|
||||||
: await processImage(
|
|
||||||
signal,
|
|
||||||
source,
|
|
||||||
settings.processorState,
|
|
||||||
workerBridge,
|
|
||||||
);
|
|
||||||
|
|
||||||
file = await compressImage(
|
|
||||||
signal,
|
|
||||||
preprocessed,
|
|
||||||
settings.encoderState,
|
|
||||||
source.file.name,
|
|
||||||
workerBridge,
|
|
||||||
);
|
|
||||||
data = await decodeImage(signal, file, workerBridge);
|
|
||||||
|
|
||||||
this.encodeCache.add({
|
|
||||||
data,
|
|
||||||
preprocessed,
|
|
||||||
file,
|
|
||||||
sourceData: source.processed,
|
|
||||||
encoderState: settings.encoderState,
|
|
||||||
processorState: settings.processorState,
|
|
||||||
});
|
|
||||||
assertSignal(signal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestData = this.state.sides[index];
|
|
||||||
if (latestData.downloadUrl) URL.revokeObjectURL(latestData.downloadUrl);
|
|
||||||
|
|
||||||
assertSignal(signal);
|
|
||||||
|
|
||||||
sides = cleanMerge(this.state.sides, index, {
|
|
||||||
file,
|
|
||||||
data,
|
|
||||||
preprocessed,
|
|
||||||
downloadUrl: URL.createObjectURL(file),
|
|
||||||
loading: false,
|
|
||||||
encodedSettings: settings,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({ sides });
|
|
||||||
} catch (err) {
|
|
||||||
if (err.name === 'AbortError') return;
|
|
||||||
this.props.showSnack(`Processing error: ${err}`);
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,7 +430,14 @@ export default class Compress extends Component<Props, State> {
|
|||||||
/** The in-progress job for each side (processing and encoding) */
|
/** The in-progress job for each side (processing and encoding) */
|
||||||
private activeSideJobs: [SideJob?, SideJob?] = [undefined, undefined];
|
private activeSideJobs: [SideJob?, SideJob?] = [undefined, undefined];
|
||||||
|
|
||||||
private async updateJob() {
|
/**
|
||||||
|
* Perform image processing.
|
||||||
|
*
|
||||||
|
* This function is a monster, but I didn't want to break it up, because it
|
||||||
|
* never gets partially called. Instead, it looks at the current state, and
|
||||||
|
* decides which steps can be skipped, and which can be cached.
|
||||||
|
*/
|
||||||
|
private async updateImage() {
|
||||||
const currentState = this.state;
|
const currentState = this.state;
|
||||||
|
|
||||||
// State of the last completed job, or ongoing job
|
// State of the last completed job, or ongoing job
|
||||||
@@ -748,70 +505,249 @@ export default class Compress extends Component<Props, State> {
|
|||||||
let decoded: ImageData;
|
let decoded: ImageData;
|
||||||
let vectorImage: HTMLImageElement | undefined;
|
let vectorImage: HTMLImageElement | undefined;
|
||||||
|
|
||||||
|
// Handle decoding
|
||||||
if (needsDecoding) {
|
if (needsDecoding) {
|
||||||
assertSignal(mainSignal);
|
try {
|
||||||
this.setState({
|
assertSignal(mainSignal);
|
||||||
source: undefined,
|
this.setState({
|
||||||
loading: true,
|
source: undefined,
|
||||||
});
|
loading: true,
|
||||||
|
});
|
||||||
|
|
||||||
// Special-case SVG. We need to avoid createImageBitmap because of
|
// Special-case SVG. We need to avoid createImageBitmap because of
|
||||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=606319.
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=606319.
|
||||||
// Also, we cache the HTMLImageElement so we can perform vector resizing later.
|
// Also, we cache the HTMLImageElement so we can perform vector resizing later.
|
||||||
if (mainJobState.file.type.startsWith('image/svg+xml')) {
|
if (mainJobState.file.type.startsWith('image/svg+xml')) {
|
||||||
vectorImage = await processSvg(mainSignal, mainJobState.file);
|
vectorImage = await processSvg(mainSignal, mainJobState.file);
|
||||||
decoded = drawableToImageData(vectorImage);
|
decoded = drawableToImageData(vectorImage);
|
||||||
} else {
|
} else {
|
||||||
decoded = await decodeImage(
|
decoded = await decodeImage(
|
||||||
mainSignal,
|
mainSignal,
|
||||||
mainJobState.file,
|
mainJobState.file,
|
||||||
// Either worker is good enough here.
|
// Either worker is good enough here.
|
||||||
this.workerBridges[0],
|
this.workerBridges[0],
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default resize values
|
||||||
|
this.setState((currentState) => {
|
||||||
|
if (mainSignal.aborted) return {};
|
||||||
|
const sides = currentState.sides.map((side) => {
|
||||||
|
const resizeState: Partial<ProcessorState['resize']> = {
|
||||||
|
width: decoded.width,
|
||||||
|
height: decoded.height,
|
||||||
|
// Disable resizing, to make it clearer to the user that something changed here
|
||||||
|
enabled: false,
|
||||||
|
};
|
||||||
|
return cleanMerge(
|
||||||
|
side,
|
||||||
|
'latestSettings.processorState.resize',
|
||||||
|
resizeState,
|
||||||
|
);
|
||||||
|
}) as [Side, Side];
|
||||||
|
return { sides };
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name === 'AbortError') return;
|
||||||
|
this.props.showSnack(`Source decoding error: ${err}`);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
({ decoded, vectorImage } = currentState.source!);
|
({ decoded, vectorImage } = currentState.source!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let source: SourceImage;
|
||||||
|
|
||||||
|
// Handle preprocessing
|
||||||
if (needsPreprocessing) {
|
if (needsPreprocessing) {
|
||||||
assertSignal(mainSignal);
|
try {
|
||||||
this.setState({
|
assertSignal(mainSignal);
|
||||||
loading: true,
|
this.setState({
|
||||||
});
|
loading: true,
|
||||||
|
});
|
||||||
|
|
||||||
const processed = await preprocessImage(
|
const preprocessed = await preprocessImage(
|
||||||
mainSignal,
|
mainSignal,
|
||||||
decoded,
|
decoded,
|
||||||
mainJobState.preprocessorState,
|
mainJobState.preprocessorState,
|
||||||
// Either worker is good enough here.
|
// Either worker is good enough here.
|
||||||
this.workerBridges[0],
|
this.workerBridges[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
let newState: State = {
|
source = {
|
||||||
...currentState,
|
|
||||||
loading: false,
|
|
||||||
source: {
|
|
||||||
decoded,
|
decoded,
|
||||||
vectorImage,
|
vectorImage,
|
||||||
processed,
|
preprocessed,
|
||||||
file: mainJobState.file,
|
file: mainJobState.file,
|
||||||
},
|
};
|
||||||
};
|
|
||||||
newState = stateForNewSourceData(newState);
|
// Update state for process completion, including intermediate render
|
||||||
this.setState(newState);
|
this.setState((currentState) => {
|
||||||
|
if (mainSignal.aborted) return {};
|
||||||
|
let newState: State = {
|
||||||
|
...currentState,
|
||||||
|
loading: false,
|
||||||
|
source,
|
||||||
|
encodedPreprocessorState: mainJobState.preprocessorState,
|
||||||
|
sides: currentState.sides.map((side) => {
|
||||||
|
if (side.downloadUrl) URL.revokeObjectURL(side.downloadUrl);
|
||||||
|
|
||||||
|
const newSide: Side = {
|
||||||
|
...side,
|
||||||
|
// Intermediate render
|
||||||
|
data: preprocessed,
|
||||||
|
processed: undefined,
|
||||||
|
encodedSettings: undefined,
|
||||||
|
};
|
||||||
|
return newSide;
|
||||||
|
}) as [Side, Side],
|
||||||
|
};
|
||||||
|
newState = stateForNewSourceData(newState);
|
||||||
|
updateDocumentTitle(source.file.name);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name === 'AbortError') return;
|
||||||
|
this.props.showSnack(`Preprocessing error: ${err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
source = currentState.source!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// That's the main part of the job done.
|
||||||
this.activeMainJob = undefined;
|
this.activeMainJob = undefined;
|
||||||
|
|
||||||
// TODO: you are here. Fork for each side. Perform processing and encoding.
|
// Allow side jobs to happen in parallel
|
||||||
|
sideWorksNeeded.forEach(async (sideWorkNeeded, sideIndex) => {
|
||||||
|
try {
|
||||||
|
// If processing is true, encoding is always true.
|
||||||
|
if (!sideWorkNeeded.encoding) return;
|
||||||
|
|
||||||
|
const signal = sideSignals[sideIndex];
|
||||||
|
const jobState = sideJobStates[sideIndex];
|
||||||
|
const workerBridge = this.workerBridges[sideIndex];
|
||||||
|
let file: File;
|
||||||
|
let data: ImageData;
|
||||||
|
let processed: ImageData | undefined = undefined;
|
||||||
|
|
||||||
|
// If there's no encoder state, this is "original image", which also
|
||||||
|
// doesn't allow processing.
|
||||||
|
if (!jobState.encoderState) {
|
||||||
|
file = currentState.source!.file;
|
||||||
|
data = source.preprocessed;
|
||||||
|
} else {
|
||||||
|
const cacheResult = this.encodeCache.match(
|
||||||
|
source.preprocessed,
|
||||||
|
jobState.processorState,
|
||||||
|
jobState.encoderState,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cacheResult) {
|
||||||
|
({ file, processed, data } = cacheResult);
|
||||||
|
} else {
|
||||||
|
// Set loading state for this side
|
||||||
|
this.setState((currentState) => {
|
||||||
|
if (signal.aborted) return {};
|
||||||
|
const sides = cleanMerge(currentState.sides, sideIndex, {
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
return { sides };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sideWorkNeeded.processing) {
|
||||||
|
processed = await processImage(
|
||||||
|
signal,
|
||||||
|
source,
|
||||||
|
jobState.processorState,
|
||||||
|
workerBridge,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update state for process completion, including intermediate render
|
||||||
|
this.setState((currentState) => {
|
||||||
|
if (signal.aborted) return {};
|
||||||
|
const currentSide = currentState.sides[sideIndex];
|
||||||
|
const side: Side = {
|
||||||
|
...currentSide,
|
||||||
|
processed,
|
||||||
|
// Intermediate render
|
||||||
|
data: processed,
|
||||||
|
encodedSettings: {
|
||||||
|
...currentSide.encodedSettings,
|
||||||
|
processorState: jobState.processorState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const sides = cleanSet(currentState.sides, sideIndex, side);
|
||||||
|
return { sides };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
processed = currentState.sides[sideIndex].processed!;
|
||||||
|
}
|
||||||
|
|
||||||
|
file = await compressImage(
|
||||||
|
signal,
|
||||||
|
processed,
|
||||||
|
jobState.encoderState,
|
||||||
|
source.file.name,
|
||||||
|
workerBridge,
|
||||||
|
);
|
||||||
|
data = await decodeImage(signal, file, workerBridge);
|
||||||
|
|
||||||
|
this.encodeCache.add({
|
||||||
|
data,
|
||||||
|
processed,
|
||||||
|
file,
|
||||||
|
preprocessed: source.preprocessed,
|
||||||
|
encoderState: jobState.encoderState,
|
||||||
|
processorState: jobState.processorState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState((currentState) => {
|
||||||
|
if (signal.aborted) return {};
|
||||||
|
const currentSide = currentState.sides[sideIndex];
|
||||||
|
|
||||||
|
if (currentSide.downloadUrl) {
|
||||||
|
URL.revokeObjectURL(currentSide.downloadUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const side: Side = {
|
||||||
|
...currentSide,
|
||||||
|
data,
|
||||||
|
file,
|
||||||
|
downloadUrl: URL.createObjectURL(file),
|
||||||
|
loading: false,
|
||||||
|
processed,
|
||||||
|
encodedSettings: {
|
||||||
|
// If we didn't encode, we didn't preprocess either
|
||||||
|
processorState: jobState.encoderState
|
||||||
|
? jobState.processorState
|
||||||
|
: defaultProcessorState,
|
||||||
|
encoderState: jobState.encoderState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const sides = cleanSet(currentState.sides, sideIndex, side);
|
||||||
|
return { sides };
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activeSideJobs[sideIndex] = undefined;
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name === 'AbortError') return;
|
||||||
|
this.props.showSnack(`Processing error: ${err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render({ onBack }: Props, { loading, sides, source, mobileView }: State) {
|
render(
|
||||||
|
{ onBack }: Props,
|
||||||
|
{ loading, sides, source, mobileView, preprocessorState }: State,
|
||||||
|
) {
|
||||||
const [leftSide, rightSide] = sides;
|
const [leftSide, rightSide] = sides;
|
||||||
const [leftImageData, rightImageData] = sides.map((i) => i.data);
|
const [leftImageData, rightImageData] = sides.map((i) => i.data);
|
||||||
|
|
||||||
const options = sides.map((side, index) => (
|
const options = sides.map((side, index) => (
|
||||||
// tslint:disable-next-line:jsx-key
|
|
||||||
<Options
|
<Options
|
||||||
source={source}
|
source={source}
|
||||||
mobileView={mobileView}
|
mobileView={mobileView}
|
||||||
@@ -837,7 +773,6 @@ export default class Compress extends Component<Props, State> {
|
|||||||
: ['right', 'left']) as CopyAcrossIconProps['copyDirection'][];
|
: ['right', 'left']) as CopyAcrossIconProps['copyDirection'][];
|
||||||
|
|
||||||
const results = sides.map((side, index) => (
|
const results = sides.map((side, index) => (
|
||||||
// tslint:disable-next-line:jsx-key
|
|
||||||
<Results
|
<Results
|
||||||
downloadUrl={side.downloadUrl}
|
downloadUrl={side.downloadUrl}
|
||||||
imageFile={side.file}
|
imageFile={side.file}
|
||||||
@@ -852,14 +787,16 @@ export default class Compress extends Component<Props, State> {
|
|||||||
: [
|
: [
|
||||||
<ExpandIcon class={style.expandIcon} key="expand-icon" />,
|
<ExpandIcon class={style.expandIcon} key="expand-icon" />,
|
||||||
`${resultTitles[index]} (${
|
`${resultTitles[index]} (${
|
||||||
encoderMap[side.latestSettings.encoderState.type].label
|
side.latestSettings.encoderState
|
||||||
|
? encoderMap[side.latestSettings.encoderState.type].meta.label
|
||||||
|
: 'Original Image'
|
||||||
})`,
|
})`,
|
||||||
]}
|
]}
|
||||||
</Results>
|
</Results>
|
||||||
));
|
));
|
||||||
|
|
||||||
// For rendering, we ideally want the settings that were used to create the data, not the latest
|
// For rendering, we ideally want the settings that were used to create the
|
||||||
// settings.
|
// data, not the latest settings.
|
||||||
const leftDisplaySettings =
|
const leftDisplaySettings =
|
||||||
leftSide.encodedSettings || leftSide.latestSettings;
|
leftSide.encodedSettings || leftSide.latestSettings;
|
||||||
const rightDisplaySettings =
|
const rightDisplaySettings =
|
||||||
@@ -881,8 +818,8 @@ export default class Compress extends Component<Props, State> {
|
|||||||
leftImgContain={leftImgContain}
|
leftImgContain={leftImgContain}
|
||||||
rightImgContain={rightImgContain}
|
rightImgContain={rightImgContain}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
inputProcessorState={source && source.preprocessorState}
|
preprocessorState={preprocessorState}
|
||||||
onInputProcessorChange={this.onPreprocessorChange}
|
onPreprocessorChange={this.onPreprocessorChange}
|
||||||
/>
|
/>
|
||||||
{mobileView ? (
|
{mobileView ? (
|
||||||
<div class={style.options}>
|
<div class={style.options}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { EncoderState, ProcessorState } from '../feature-meta';
|
|||||||
import { shallowEqual } from '../util';
|
import { shallowEqual } from '../util';
|
||||||
|
|
||||||
interface CacheResult {
|
interface CacheResult {
|
||||||
preprocessed: ImageData;
|
processed: ImageData;
|
||||||
data: ImageData;
|
data: ImageData;
|
||||||
file: File;
|
file: File;
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ interface CacheResult {
|
|||||||
interface CacheEntry extends CacheResult {
|
interface CacheEntry extends CacheResult {
|
||||||
processorState: ProcessorState;
|
processorState: ProcessorState;
|
||||||
encoderState: EncoderState;
|
encoderState: EncoderState;
|
||||||
sourceData: ImageData;
|
preprocessed: ImageData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SIZE = 5;
|
const SIZE = 5;
|
||||||
@@ -26,13 +26,13 @@ export default class ResultCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match(
|
match(
|
||||||
sourceData: ImageData,
|
preprocessed: ImageData,
|
||||||
processorState: ProcessorState,
|
processorState: ProcessorState,
|
||||||
encoderState: EncoderState,
|
encoderState: EncoderState,
|
||||||
): CacheResult | undefined {
|
): CacheResult | undefined {
|
||||||
const matchingIndex = this._entries.findIndex((entry) => {
|
const matchingIndex = this._entries.findIndex((entry) => {
|
||||||
// Check for quick exits:
|
// Check for quick exits:
|
||||||
if (entry.sourceData !== sourceData) return false;
|
if (entry.preprocessed !== preprocessed) return false;
|
||||||
if (entry.encoderState.type !== encoderState.type) return false;
|
if (entry.encoderState.type !== encoderState.type) return false;
|
||||||
|
|
||||||
// Check that each set of options in the preprocessor are the same
|
// Check that each set of options in the preprocessor are the same
|
||||||
@@ -65,10 +65,6 @@ export default class ResultCache {
|
|||||||
this._entries.unshift(matchingEntry);
|
this._entries.unshift(matchingEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return { ...matchingEntry };
|
||||||
data: matchingEntry.data,
|
|
||||||
preprocessed: matchingEntry.preprocessed,
|
|
||||||
file: matchingEntry.file,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export async function resize(
|
|||||||
return vectorResize(source.vectorImage, options);
|
return vectorResize(source.vectorImage, options);
|
||||||
}
|
}
|
||||||
if (isWorkerOptions(options)) {
|
if (isWorkerOptions(options)) {
|
||||||
return workerBridge.resize(signal, source.processed, options);
|
return workerBridge.resize(signal, source.preprocessed, options);
|
||||||
}
|
}
|
||||||
return browserResize(source.processed, options);
|
return browserResize(source.preprocessed, options);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user