forked from external-repos/squoosh
Progress on single process pass
This commit is contained in:
@@ -40,7 +40,6 @@ export interface SourceImage {
|
|||||||
decoded: ImageData;
|
decoded: ImageData;
|
||||||
processed: ImageData;
|
processed: ImageData;
|
||||||
vectorImage?: HTMLImageElement;
|
vectorImage?: HTMLImageElement;
|
||||||
preprocessorState: PreprocessorState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SideSettings {
|
interface SideSettings {
|
||||||
@@ -49,7 +48,7 @@ interface SideSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Side {
|
interface Side {
|
||||||
preprocessed?: ImageData;
|
processed?: ImageData;
|
||||||
file?: File;
|
file?: File;
|
||||||
downloadUrl?: string;
|
downloadUrl?: string;
|
||||||
data?: ImageData;
|
data?: ImageData;
|
||||||
@@ -71,12 +70,24 @@ interface State {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
mobileView: boolean;
|
mobileView: boolean;
|
||||||
|
preprocessorState: PreprocessorState;
|
||||||
|
encodedPreprocessorState?: PreprocessorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateImageOptions {
|
interface UpdateImageOptions {
|
||||||
skipPreprocessing?: boolean;
|
skipPreprocessing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MainJob {
|
||||||
|
file: File;
|
||||||
|
preprocessorState: PreprocessorState;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SideJob {
|
||||||
|
processorState: ProcessorState;
|
||||||
|
encoderState?: EncoderState;
|
||||||
|
}
|
||||||
|
|
||||||
async function decodeImage(
|
async function decodeImage(
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
blob: Blob,
|
blob: Blob,
|
||||||
@@ -191,12 +202,16 @@ function stateForNewSourceData(state: State): State {
|
|||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processSvg(blob: Blob): Promise<HTMLImageElement> {
|
async function processSvg(
|
||||||
|
signal: AbortSignal,
|
||||||
|
blob: Blob,
|
||||||
|
): Promise<HTMLImageElement> {
|
||||||
|
assertSignal(signal);
|
||||||
// Firefox throws if you try to draw an SVG to canvas that doesn't have width/height.
|
// Firefox throws if you try to draw an SVG to canvas that doesn't have width/height.
|
||||||
// In Chrome it loads, but drawImage behaves weirdly.
|
// In Chrome it loads, but drawImage behaves weirdly.
|
||||||
// This function sets width/height if it isn't already set.
|
// This function sets width/height if it isn't already set.
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const text = await blobToText(blob);
|
const text = await abortable(signal, blobToText(blob));
|
||||||
const document = parser.parseFromString(text, 'image/svg+xml');
|
const document = parser.parseFromString(text, 'image/svg+xml');
|
||||||
const svg = document.documentElement!;
|
const svg = document.documentElement!;
|
||||||
|
|
||||||
@@ -213,7 +228,10 @@ async function processSvg(blob: Blob): Promise<HTMLImageElement> {
|
|||||||
|
|
||||||
const serializer = new XMLSerializer();
|
const serializer = new XMLSerializer();
|
||||||
const newSource = serializer.serializeToString(document);
|
const newSource = serializer.serializeToString(document);
|
||||||
return blobToImg(new Blob([newSource], { type: 'image/svg+xml' }));
|
return abortable(
|
||||||
|
signal,
|
||||||
|
blobToImg(new Blob([newSource], { type: 'image/svg+xml' })),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// These are only used in the mobile view
|
// These are only used in the mobile view
|
||||||
@@ -223,6 +241,12 @@ const buttonPositions = ['download-left', 'download-right'] as const;
|
|||||||
|
|
||||||
const originalDocumentTitle = document.title;
|
const originalDocumentTitle = document.title;
|
||||||
|
|
||||||
|
function updateDocumentTitle(filename: string = ''): void {
|
||||||
|
document.title = filename
|
||||||
|
? `${filename} - ${originalDocumentTitle}`
|
||||||
|
: originalDocumentTitle;
|
||||||
|
}
|
||||||
|
|
||||||
export default class Compress extends Component<Props, State> {
|
export default class Compress extends Component<Props, State> {
|
||||||
widthQuery = window.matchMedia('(max-width: 599px)');
|
widthQuery = window.matchMedia('(max-width: 599px)');
|
||||||
|
|
||||||
@@ -252,8 +276,13 @@ export default class Compress extends Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly encodeCache = new ResultCache();
|
private readonly encodeCache = new ResultCache();
|
||||||
private readonly leftWorkerBridge = new WorkerBridge();
|
// One for each side
|
||||||
private readonly rightWorkerBridge = new WorkerBridge();
|
private readonly workerBridges = [new WorkerBridge(), new WorkerBridge()];
|
||||||
|
/** Abort controller for actions that impact both sites, like source image decoding and preprocessing */
|
||||||
|
private mainAbortController = new AbortController();
|
||||||
|
// And again one for each side
|
||||||
|
private sideAbortControllers = [new AbortController(), new AbortController()];
|
||||||
|
|
||||||
// For debouncing calls to updateImage for each side.
|
// For debouncing calls to updateImage for each side.
|
||||||
private readonly updateImageTimeoutIds: [number?, number?] = [
|
private readonly updateImageTimeoutIds: [number?, number?] = [
|
||||||
undefined,
|
undefined,
|
||||||
@@ -263,7 +292,19 @@ export default class Compress extends Component<Props, State> {
|
|||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.widthQuery.addListener(this.onMobileWidthChange);
|
this.widthQuery.addListener(this.onMobileWidthChange);
|
||||||
this.updateFile(props.file);
|
this.sourceFile = props.file;
|
||||||
|
this.updateJob(
|
||||||
|
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());
|
||||||
}
|
}
|
||||||
@@ -310,12 +351,6 @@ export default class Compress extends Component<Props, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateDocumentTitle(filename: string = ''): void {
|
|
||||||
document.title = filename
|
|
||||||
? `${filename} - ${originalDocumentTitle}`
|
|
||||||
: originalDocumentTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.updateFile(nextProps.file);
|
||||||
@@ -323,7 +358,7 @@ export default class Compress extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
this.updateDocumentTitle();
|
updateDocumentTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props, prevState: State): void {
|
componentDidUpdate(prevProps: Props, prevState: State): void {
|
||||||
@@ -355,14 +390,15 @@ export default class Compress extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async onCopyToOtherClick(index: 0 | 1) {
|
private async onCopyToOtherClick(index: 0 | 1) {
|
||||||
const otherIndex = (index + 1) % 2;
|
const otherIndex = index ? 0 : 1;
|
||||||
const oldSettings = this.state.sides[otherIndex];
|
const oldSettings = this.state.sides[otherIndex];
|
||||||
const newSettings = { ...this.state.sides[index] };
|
const newSettings = { ...this.state.sides[index] };
|
||||||
|
|
||||||
// Create a new object URL for the new settings. This avoids both sides sharing a URL, which
|
// Create a new object URL for the new settings. This avoids both sides sharing a URL, which
|
||||||
// means it can be safely revoked without impacting the other side.
|
// means it can be safely revoked without impacting the other side.
|
||||||
if (newSettings.file)
|
if (newSettings.file) {
|
||||||
newSettings.downloadUrl = URL.createObjectURL(newSettings.file);
|
newSettings.downloadUrl = URL.createObjectURL(newSettings.file);
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
sides: cleanSet(this.state.sides, otherIndex, newSettings),
|
sides: cleanSet(this.state.sides, otherIndex, newSettings),
|
||||||
@@ -380,8 +416,8 @@ export default class Compress extends Component<Props, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onInputProcessorChange = async (
|
private onPreprocessorChange = async (
|
||||||
options: InputProcessorState,
|
options: PreprocessorState,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const source = this.state.source;
|
const source = this.state.source;
|
||||||
if (!source) return;
|
if (!source) return;
|
||||||
@@ -389,33 +425,37 @@ export default class Compress extends Component<Props, State> {
|
|||||||
const oldRotate = source.preprocessorState.rotate.rotate;
|
const oldRotate = source.preprocessorState.rotate.rotate;
|
||||||
const newRotate = options.rotate.rotate;
|
const newRotate = options.rotate.rotate;
|
||||||
const orientationChanged = oldRotate % 180 !== newRotate % 180;
|
const orientationChanged = oldRotate % 180 !== newRotate % 180;
|
||||||
const loadingCounter = this.state.loadingCounter + 1;
|
// Either worker bridge is good enough here.
|
||||||
// Either processor is good enough here.
|
const workerBridge = this.workerBridges[0];
|
||||||
const processor = this.leftWorkerBridge;
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loadingCounter,
|
|
||||||
loading: true,
|
loading: true,
|
||||||
|
// TODO: this is wrong
|
||||||
source: cleanSet(source, 'inputProcessorState', options),
|
source: cleanSet(source, 'inputProcessorState', options),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Abort any current encode jobs, as they're redundant now.
|
|
||||||
this.leftWorkerBridge.abortCurrent();
|
|
||||||
this.rightWorkerBridge.abortCurrent();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const processed = await preprocessImage(
|
const processed = await preprocessImage(
|
||||||
|
signal,
|
||||||
source.decoded,
|
source.decoded,
|
||||||
options,
|
options,
|
||||||
processor,
|
workerBridge,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Another file has been opened/processed before this one processed.
|
|
||||||
if (this.state.loadingCounter !== loadingCounter) return;
|
|
||||||
|
|
||||||
let newState = { ...this.state, loading: false };
|
let newState = { ...this.state, loading: false };
|
||||||
newState = cleanSet(newState, 'source.processed', processed);
|
newState = cleanSet(newState, 'source.processed', processed);
|
||||||
newState = stateForNewSourceData(newState, newState.source!);
|
newState = stateForNewSourceData(newState);
|
||||||
|
|
||||||
if (orientationChanged) {
|
if (orientationChanged) {
|
||||||
// If orientation has changed, we should flip the resize values.
|
// If orientation has changed, we should flip the resize values.
|
||||||
@@ -424,7 +464,7 @@ export default class Compress extends Component<Props, State> {
|
|||||||
newState.sides[i].latestSettings.processorState.resize;
|
newState.sides[i].latestSettings.processorState.resize;
|
||||||
newState = cleanMerge(
|
newState = cleanMerge(
|
||||||
newState,
|
newState,
|
||||||
`sides.${i}.latestSettings.preprocessorState.resize`,
|
`sides.${i}.latestSettings.processorState.resize`,
|
||||||
{
|
{
|
||||||
width: resizeSettings.height,
|
width: resizeSettings.height,
|
||||||
height: resizeSettings.width,
|
height: resizeSettings.width,
|
||||||
@@ -436,23 +476,27 @@ export default class Compress extends Component<Props, State> {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.name === 'AbortError') return;
|
if (err.name === 'AbortError') return;
|
||||||
console.error(err);
|
console.error(err);
|
||||||
// Another file has been opened/processed before this one processed.
|
|
||||||
if (this.state.loadingCounter !== loadingCounter) return;
|
|
||||||
this.props.showSnack('Processing error');
|
this.props.showSnack('Processing error');
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private updateFile = async (file: File) => {
|
private updateFile = async (file: File) => {
|
||||||
const loadingCounter = this.state.loadingCounter + 1;
|
|
||||||
// Either processor is good enough here.
|
// Either processor is good enough here.
|
||||||
const processor = this.leftWorkerBridge;
|
const workerBridge = this.workerBridges[0];
|
||||||
|
|
||||||
this.setState({ loadingCounter, loading: true });
|
this.setState({ loading: true });
|
||||||
|
|
||||||
// Abort any current encode jobs, as they're redundant now.
|
// Abort any current jobs, as they're redundant now.
|
||||||
this.leftWorkerBridge.abortCurrent();
|
for (const controller of [
|
||||||
this.rightWorkerBridge.abortCurrent();
|
this.mainAbortController,
|
||||||
|
...this.sideAbortControllers,
|
||||||
|
]) {
|
||||||
|
controller.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mainAbortController = new AbortController();
|
||||||
|
const { signal } = this.mainAbortController;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let decoded: ImageData;
|
let decoded: ImageData;
|
||||||
@@ -462,22 +506,20 @@ export default class Compress extends Component<Props, State> {
|
|||||||
// 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 (file.type.startsWith('image/svg+xml')) {
|
if (file.type.startsWith('image/svg+xml')) {
|
||||||
vectorImage = await processSvg(file);
|
vectorImage = await processSvg(signal, file);
|
||||||
decoded = drawableToImageData(vectorImage);
|
decoded = drawableToImageData(vectorImage);
|
||||||
} else {
|
} else {
|
||||||
// Either processor is good enough here.
|
// Either processor is good enough here.
|
||||||
decoded = await decodeImage(file, processor);
|
decoded = await decodeImage(signal, file, workerBridge);
|
||||||
}
|
}
|
||||||
|
|
||||||
const processed = await preprocessImage(
|
const processed = await preprocessImage(
|
||||||
|
signal,
|
||||||
decoded,
|
decoded,
|
||||||
defaultInputProcessorState,
|
defaultPreprocessorState,
|
||||||
processor,
|
workerBridge,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Another file has been opened/processed before this one processed.
|
|
||||||
if (this.state.loadingCounter !== loadingCounter) return;
|
|
||||||
|
|
||||||
let newState: State = {
|
let newState: State = {
|
||||||
...this.state,
|
...this.state,
|
||||||
source: {
|
source: {
|
||||||
@@ -485,18 +527,18 @@ export default class Compress extends Component<Props, State> {
|
|||||||
file,
|
file,
|
||||||
vectorImage,
|
vectorImage,
|
||||||
processed,
|
processed,
|
||||||
preprocessorState: defaultInputProcessorState,
|
preprocessorState: defaultPreprocessorState,
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
newState = stateForNewSourceData(newState, newState.source!);
|
newState = stateForNewSourceData(newState);
|
||||||
|
|
||||||
for (const i of [0, 1]) {
|
for (const i of [0, 1]) {
|
||||||
// Default resize values come from the image:
|
// Default resize values come from the image:
|
||||||
newState = cleanMerge(
|
newState = cleanMerge(
|
||||||
newState,
|
newState,
|
||||||
`sides.${i}.latestSettings.preprocessorState.resize`,
|
`sides.${i}.latestSettings.processorState.resize`,
|
||||||
{
|
{
|
||||||
width: processed.width,
|
width: processed.width,
|
||||||
height: processed.height,
|
height: processed.height,
|
||||||
@@ -505,13 +547,11 @@ export default class Compress extends Component<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateDocumentTitle(file.name);
|
updateDocumentTitle(file.name);
|
||||||
this.setState(newState);
|
this.setState(newState);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.name === 'AbortError') return;
|
if (err.name === 'AbortError') return;
|
||||||
console.error(err);
|
console.error(err);
|
||||||
// Another file has been opened/processed before this one processed.
|
|
||||||
if (this.state.loadingCounter !== loadingCounter) return;
|
|
||||||
this.props.showSnack('Invalid image');
|
this.props.showSnack('Invalid image');
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
}
|
}
|
||||||
@@ -542,15 +582,16 @@ export default class Compress extends Component<Props, State> {
|
|||||||
index: number,
|
index: number,
|
||||||
options: UpdateImageOptions = {},
|
options: UpdateImageOptions = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { skipPreprocessing = false } = options;
|
const { skipPreprocessing } = options;
|
||||||
const { source } = this.state;
|
const { source } = this.state;
|
||||||
if (!source) return;
|
if (!source) return;
|
||||||
|
|
||||||
// Each time we trigger an async encode, the counter changes.
|
// Abort any current tasks on this side
|
||||||
const loadingCounter = this.state.sides[index].loadingCounter + 1;
|
this.sideAbortControllers[index].abort();
|
||||||
|
this.sideAbortControllers[index] = new AbortController();
|
||||||
|
const { signal } = this.sideAbortControllers[index];
|
||||||
|
|
||||||
let sides = cleanMerge(this.state.sides, index, {
|
let sides = cleanMerge(this.state.sides, index, {
|
||||||
loadingCounter,
|
|
||||||
loading: true,
|
loading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -562,44 +603,42 @@ export default class Compress extends Component<Props, State> {
|
|||||||
let file: File | undefined;
|
let file: File | undefined;
|
||||||
let preprocessed: ImageData | undefined;
|
let preprocessed: ImageData | undefined;
|
||||||
let data: 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(
|
const cacheResult = this.encodeCache.match(
|
||||||
source.processed,
|
source.processed,
|
||||||
settings.preprocessorState,
|
settings.processorState,
|
||||||
settings.encoderState,
|
settings.encoderState,
|
||||||
);
|
);
|
||||||
const processor =
|
|
||||||
index === 0 ? this.leftWorkerBridge : this.rightWorkerBridge;
|
|
||||||
|
|
||||||
// Abort anything the processor is currently doing.
|
|
||||||
// Although the processor will abandon current tasks when a new one is called,
|
|
||||||
// we might not call another task here. Eg, we might get the result from the cache.
|
|
||||||
processor.abortCurrent();
|
|
||||||
|
|
||||||
if (cacheResult) {
|
if (cacheResult) {
|
||||||
({ file, preprocessed, data } = cacheResult);
|
({ file, preprocessed, data } = cacheResult);
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
// Special case for identity
|
|
||||||
if (settings.encoderState.type === identity.type) {
|
|
||||||
file = source.file;
|
|
||||||
data = source.processed;
|
|
||||||
} else {
|
} else {
|
||||||
preprocessed =
|
preprocessed =
|
||||||
skipPreprocessing && side.preprocessed
|
skipPreprocessing && side.processed
|
||||||
? side.preprocessed
|
? side.processed
|
||||||
: await processImage(
|
: await processImage(
|
||||||
|
signal,
|
||||||
source,
|
source,
|
||||||
settings.preprocessorState,
|
settings.processorState,
|
||||||
processor,
|
workerBridge,
|
||||||
);
|
);
|
||||||
|
|
||||||
file = await compressImage(
|
file = await compressImage(
|
||||||
|
signal,
|
||||||
preprocessed,
|
preprocessed,
|
||||||
settings.encoderState,
|
settings.encoderState,
|
||||||
source.file.name,
|
source.file.name,
|
||||||
processor,
|
workerBridge,
|
||||||
);
|
);
|
||||||
data = await decodeImage(file, processor);
|
data = await decodeImage(signal, file, workerBridge);
|
||||||
|
|
||||||
this.encodeCache.add({
|
this.encodeCache.add({
|
||||||
data,
|
data,
|
||||||
@@ -607,37 +646,164 @@ export default class Compress extends Component<Props, State> {
|
|||||||
file,
|
file,
|
||||||
sourceData: source.processed,
|
sourceData: source.processed,
|
||||||
encoderState: settings.encoderState,
|
encoderState: settings.encoderState,
|
||||||
preprocessorState: settings.preprocessorState,
|
processorState: settings.processorState,
|
||||||
});
|
});
|
||||||
}
|
assertSignal(signal);
|
||||||
} catch (err) {
|
|
||||||
if (err.name === 'AbortError') return;
|
|
||||||
this.props.showSnack(
|
|
||||||
`Processing error (type=${settings.encoderState.type}): ${err}`,
|
|
||||||
);
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestData = this.state.sides[index];
|
const latestData = this.state.sides[index];
|
||||||
// If a later encode has landed before this one, return.
|
|
||||||
if (loadingCounter < latestData.loadedCounter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latestData.downloadUrl) URL.revokeObjectURL(latestData.downloadUrl);
|
if (latestData.downloadUrl) URL.revokeObjectURL(latestData.downloadUrl);
|
||||||
|
|
||||||
|
assertSignal(signal);
|
||||||
|
|
||||||
sides = cleanMerge(this.state.sides, index, {
|
sides = cleanMerge(this.state.sides, index, {
|
||||||
file,
|
file,
|
||||||
data,
|
data,
|
||||||
preprocessed,
|
preprocessed,
|
||||||
downloadUrl: URL.createObjectURL(file),
|
downloadUrl: URL.createObjectURL(file),
|
||||||
loading: sides[index].loadingCounter !== loadingCounter,
|
loading: false,
|
||||||
loadedCounter: loadingCounter,
|
|
||||||
encodedSettings: settings,
|
encodedSettings: settings,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({ sides });
|
this.setState({ sides });
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name === 'AbortError') return;
|
||||||
|
this.props.showSnack(`Processing error: ${err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sourceFile: File;
|
||||||
|
/** The in-progress job for decoding and preprocessing */
|
||||||
|
private activeMainJob?: MainJob;
|
||||||
|
/** The in-progress job for each side (processing and encoding) */
|
||||||
|
private activeSideJobs: [SideJob?, SideJob?] = [undefined, undefined];
|
||||||
|
|
||||||
|
private async updateJob() {
|
||||||
|
const currentState = this.state;
|
||||||
|
|
||||||
|
// State of the last completed job, or ongoing job
|
||||||
|
const latestMainJobState: Partial<MainJob> = this.activeMainJob || {
|
||||||
|
file: currentState.source && currentState.source.file,
|
||||||
|
preprocessorState: currentState.encodedPreprocessorState,
|
||||||
|
};
|
||||||
|
const latestSideJobStates: Partial<SideJob>[] = currentState.sides.map(
|
||||||
|
(side, i) =>
|
||||||
|
this.activeSideJobs[i] || {
|
||||||
|
processorState:
|
||||||
|
side.encodedSettings && side.encodedSettings.processorState,
|
||||||
|
encoderState:
|
||||||
|
side.encodedSettings && side.encodedSettings.encoderState,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// State for this job
|
||||||
|
const mainJobState: MainJob = {
|
||||||
|
file: this.sourceFile,
|
||||||
|
preprocessorState: currentState.preprocessorState,
|
||||||
|
};
|
||||||
|
const sideJobStates: SideJob[] = currentState.sides.map((side) => ({
|
||||||
|
processorState: side.latestSettings.processorState,
|
||||||
|
encoderState: side.latestSettings.encoderState,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Figure out what needs doing:
|
||||||
|
const needsDecoding = latestMainJobState.file != mainJobState.file;
|
||||||
|
const needsPreprocessing =
|
||||||
|
needsDecoding ||
|
||||||
|
latestMainJobState.preprocessorState !== mainJobState.preprocessorState;
|
||||||
|
const sideWorksNeeded = latestSideJobStates.map((latestSideJob, i) => ({
|
||||||
|
processing:
|
||||||
|
needsPreprocessing ||
|
||||||
|
latestSideJob.processorState !== sideJobStates[i].processorState,
|
||||||
|
encoding:
|
||||||
|
needsPreprocessing ||
|
||||||
|
latestSideJob.encoderState !== sideJobStates[i].encoderState,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let jobNeeded = false;
|
||||||
|
|
||||||
|
// Abort running tasks & cycle the controllers
|
||||||
|
if (needsDecoding || needsPreprocessing) {
|
||||||
|
this.mainAbortController.abort();
|
||||||
|
this.mainAbortController = new AbortController();
|
||||||
|
jobNeeded = true;
|
||||||
|
this.activeMainJob = mainJobState;
|
||||||
|
}
|
||||||
|
for (const [i, sideWorkNeeded] of sideWorksNeeded.entries()) {
|
||||||
|
if (sideWorkNeeded.processing || sideWorkNeeded.encoding) {
|
||||||
|
this.sideAbortControllers[i].abort();
|
||||||
|
this.sideAbortControllers[i] = new AbortController();
|
||||||
|
jobNeeded = true;
|
||||||
|
this.activeSideJobs[i] = sideJobStates[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jobNeeded) return;
|
||||||
|
|
||||||
|
const mainSignal = this.mainAbortController.signal;
|
||||||
|
const sideSignals = this.sideAbortControllers.map((ac) => ac.signal);
|
||||||
|
|
||||||
|
let decoded: ImageData;
|
||||||
|
let vectorImage: HTMLImageElement | undefined;
|
||||||
|
|
||||||
|
if (needsDecoding) {
|
||||||
|
assertSignal(mainSignal);
|
||||||
|
this.setState({
|
||||||
|
source: undefined,
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 (mainJobState.file.type.startsWith('image/svg+xml')) {
|
||||||
|
vectorImage = await processSvg(mainSignal, mainJobState.file);
|
||||||
|
decoded = drawableToImageData(vectorImage);
|
||||||
|
} else {
|
||||||
|
decoded = await decodeImage(
|
||||||
|
mainSignal,
|
||||||
|
mainJobState.file,
|
||||||
|
// Either worker is good enough here.
|
||||||
|
this.workerBridges[0],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
({ decoded, vectorImage } = currentState.source!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsPreprocessing) {
|
||||||
|
assertSignal(mainSignal);
|
||||||
|
this.setState({
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const processed = await preprocessImage(
|
||||||
|
mainSignal,
|
||||||
|
decoded,
|
||||||
|
mainJobState.preprocessorState,
|
||||||
|
// Either worker is good enough here.
|
||||||
|
this.workerBridges[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
let newState: State = {
|
||||||
|
...currentState,
|
||||||
|
loading: false,
|
||||||
|
source: {
|
||||||
|
decoded,
|
||||||
|
vectorImage,
|
||||||
|
processed,
|
||||||
|
file: mainJobState.file,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
newState = stateForNewSourceData(newState);
|
||||||
|
this.setState(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeMainJob = undefined;
|
||||||
|
|
||||||
|
// TODO: you are here. Fork for each side. Perform processing and encoding.
|
||||||
}
|
}
|
||||||
|
|
||||||
render({ onBack }: Props, { loading, sides, source, mobileView }: State) {
|
render({ onBack }: Props, { loading, sides, source, mobileView }: State) {
|
||||||
@@ -716,7 +882,7 @@ export default class Compress extends Component<Props, State> {
|
|||||||
rightImgContain={rightImgContain}
|
rightImgContain={rightImgContain}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
inputProcessorState={source && source.preprocessorState}
|
inputProcessorState={source && source.preprocessorState}
|
||||||
onInputProcessorChange={this.onInputProcessorChange}
|
onInputProcessorChange={this.onPreprocessorChange}
|
||||||
/>
|
/>
|
||||||
{mobileView ? (
|
{mobileView ? (
|
||||||
<div class={style.options}>
|
<div class={style.options}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EncoderState, ProcessorState } from '../feature-meta';
|
import { EncoderState, ProcessorState } from '../feature-meta';
|
||||||
import { shallowEqual } from '../../util';
|
import { shallowEqual } from '../util';
|
||||||
|
|
||||||
interface CacheResult {
|
interface CacheResult {
|
||||||
preprocessed: ImageData;
|
preprocessed: ImageData;
|
||||||
@@ -19,9 +19,6 @@ export default class ResultCache {
|
|||||||
private readonly _entries: CacheEntry[] = [];
|
private readonly _entries: CacheEntry[] = [];
|
||||||
|
|
||||||
add(entry: CacheEntry) {
|
add(entry: CacheEntry) {
|
||||||
if (entry.encoderState.type === 'identity') {
|
|
||||||
throw Error('Cannot cache identity encodes');
|
|
||||||
}
|
|
||||||
// Add the new entry to the start
|
// Add the new entry to the start
|
||||||
this._entries.unshift(entry);
|
this._entries.unshift(entry);
|
||||||
// Remove the last entry if we're now bigger than SIZE
|
// Remove the last entry if we're now bigger than SIZE
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ export default function () {
|
|||||||
|
|
||||||
…will be bundled into the worker and exposed via comlink as `shout()`.
|
…will be bundled into the worker and exposed via comlink as `shout()`.
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
|
||||||
|
Within a feature, files in the:
|
||||||
|
|
||||||
|
- `client` folder will be part of the client project.
|
||||||
|
- `worker` folder will be part of the worker project.
|
||||||
|
- `shared` folder will be part of the shared project. Both the client and worker projects can access the shared project.
|
||||||
|
|
||||||
# Encoder format
|
# Encoder format
|
||||||
|
|
||||||
Encoders must have the following:
|
Encoders must have the following:
|
||||||
|
|||||||
Reference in New Issue
Block a user