Skip to content

Commit

Permalink
Allowing rotation to be changed
Browse files Browse the repository at this point in the history
  • Loading branch information
jakearchibald committed Nov 29, 2018
1 parent cdb4365 commit cca7368
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 32 deletions.
37 changes: 31 additions & 6 deletions src/components/Output/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,20 @@ import {
RotateIcon,
} from '../../lib/icons';
import { twoUpHandle } from './custom-els/TwoUp/styles.css';
import { InputProcessorState } from '../../codecs/input-processors';
import { cleanSet } from '../../lib/clean-modify';
import { SourceImage } from '../compress';

interface Props {
originalImage?: ImageData;
source?: SourceImage;
inputProcessorState?: InputProcessorState;
mobileView: boolean;
leftCompressed?: ImageData;
rightCompressed?: ImageData;
leftImgContain: boolean;
rightImgContain: boolean;
onBack: () => void;
onInputProcessorChange: (newState: InputProcessorState) => void;
}

interface State {
Expand Down Expand Up @@ -77,6 +82,11 @@ export default class Output extends Component<Props, State> {
const prevRightDraw = this.rightDrawable(prevProps);
const leftDraw = this.leftDrawable();
const rightDraw = this.rightDrawable();
const sourceFileChanged =
// Has the value become (un)defined?
(!!this.props.source !== !!prevProps.source) ||
// Or has the file changed?
(this.props.source && prevProps.source && this.props.source.file !== prevProps.source.file);

if (leftDraw && leftDraw !== prevLeftDraw && this.canvasLeft) {
drawDataToCanvas(this.canvasLeft, leftDraw);
Expand All @@ -85,7 +95,7 @@ export default class Output extends Component<Props, State> {
drawDataToCanvas(this.canvasRight, rightDraw);
}

if (this.props.originalImage !== prevProps.originalImage && this.pinchZoomLeft) {
if (sourceFileChanged && this.pinchZoomLeft) {
// New image? Reset the pinch-zoom.
this.pinchZoomLeft.setTransform({
allowChangeEvent: true,
Expand All @@ -101,11 +111,11 @@ export default class Output extends Component<Props, State> {
}

private leftDrawable(props: Props = this.props): ImageData | undefined {
return props.leftCompressed || props.originalImage;
return props.leftCompressed || (props.source && props.source.processed);
}

private rightDrawable(props: Props = this.props): ImageData | undefined {
return props.rightCompressed || props.originalImage;
return props.rightCompressed || (props.source && props.source.processed);
}

@bind
Expand All @@ -129,6 +139,20 @@ export default class Output extends Component<Props, State> {
this.pinchZoomLeft.scaleTo(this.state.scale / 1.25, scaleToOpts);
}

@bind
private onRotateClick() {
const { inputProcessorState } = this.props;
if (!inputProcessorState) return;

const newState = cleanSet(
inputProcessorState,
'rotateFlip.rotate',
(inputProcessorState.rotateFlip.rotate + 90) % 360,
);

this.props.onInputProcessorChange(newState);
}

@bind
private onScaleValueFocus() {
this.setState({ editingScale: true }, () => {
Expand Down Expand Up @@ -208,12 +232,13 @@ export default class Output extends Component<Props, State> {
}

render(
{ mobileView, leftImgContain, rightImgContain, originalImage, onBack }: Props,
{ mobileView, leftImgContain, rightImgContain, source, onBack }: Props,
{ scale, editingScale, altBackground }: State,
) {
const leftDraw = this.leftDrawable();
const rightDraw = this.rightDrawable();
// To keep position stable, the output is put in a square using the longest dimension.
const originalImage = source && source.processed;
const maxDimension = originalImage && Math.max(originalImage.width, originalImage.height);
const pinchTargetStyle = {
width: maxDimension,
Expand Down Expand Up @@ -301,7 +326,7 @@ export default class Output extends Component<Props, State> {
<AddIcon />
</button>
</div>
<button class={style.button} onClick={this.toggleBackground}>
<button class={style.button} onClick={this.onRotateClick}>
<RotateIcon />
</button>
<button
Expand Down
113 changes: 91 additions & 22 deletions src/components/compress/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,26 @@ async function compressImage(
);
}

function stateForNewSourceData(state: State, newSource: SourceImage): State {
let newState = { ...state };

for (const i of [0, 1]) {
// Ditch previous encodings
const downloadUrl = state.sides[i].downloadUrl;
if (downloadUrl) URL.revokeObjectURL(downloadUrl!);

newState = cleanMerge(state, `sides.${i}`, {
preprocessed: undefined,
file: undefined,
downloadUrl: undefined,
data: undefined,
encodedSettings: undefined,
});
}

return newState;
}

async function processSvg(blob: Blob): Promise<HTMLImageElement> {
// 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.
Expand Down Expand Up @@ -249,7 +269,7 @@ export default class Compress extends Component<Props, State> {
});
}

private updateDocumentTitle(filename: string = '') {
private updateDocumentTitle(filename: string = ''): void {
document.title = filename ? `${filename} - ${originalDocumentTitle}` : originalDocumentTitle;
}

Expand All @@ -266,18 +286,23 @@ export default class Compress extends Component<Props, State> {
componentDidUpdate(prevProps: Props, prevState: State): void {
const { source, sides } = this.state;

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 sourceChanged = source !== prevState.source;
const encoderChanged = side.latestSettings.encoderState !== prevSettings.encoderState;
const preprocessorChanged =
side.latestSettings.preprocessorState !== prevSettings.preprocessorState;

// The image only needs updated if the encoder/preprocessor settings have changed, or the
// source has changed.
if (sourceChanged || encoderChanged || preprocessorChanged) {
if (sourceDataChanged || encoderChanged || preprocessorChanged) {
this.updateImage(i, {
skipPreprocessing: !sourceChanged && !preprocessorChanged,
skipPreprocessing: !sourceDataChanged && !preprocessorChanged,
}).catch((err) => {
console.error(err);
});
Expand Down Expand Up @@ -305,6 +330,58 @@ export default class Compress extends Component<Props, State> {
});
}

@bind
private async onInputProcessorChange(options: InputProcessorState): Promise<void> {
const source = this.state.source;
if (!source) return;

const oldRotate = source.inputProcessorState.rotateFlip.rotate;
const newRotate = options.rotateFlip.rotate;
const orientationChanged = oldRotate % 180 !== newRotate % 180;
const loadingCounter = this.state.loadingCounter + 1;
// Either processor is good enough here.
const processor = this.leftProcessor;

this.setState({
loadingCounter, loading: true,
source: cleanSet(source, 'inputProcessorState', options),
});

// Abort any current encode jobs, as they're redundant now.
this.leftProcessor.abortCurrent();
this.rightProcessor.abortCurrent();

try {
const processed = await processInput(source.decoded, options, processor);

// Another file has been opened/processed before this one processed.
if (this.state.loadingCounter !== loadingCounter) return;

let newState = { ...this.state, loading: false };
newState = cleanSet(newState, 'source.processed', processed);
newState = stateForNewSourceData(newState, newState.source!);

if (orientationChanged) {
// If orientation has changed, we should flip the resize values.
for (const i of [0, 1]) {
const resizeSettings = newState.sides[i].latestSettings.preprocessorState.resize;
newState = cleanMerge(newState, `sides.${i}.latestSettings.preprocessorState.resize`, {
width: resizeSettings.height,
height: resizeSettings.width,
});
}
}
this.setState(newState);
} catch (err) {
if (err.name === 'AbortError') return;
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.setState({ loading: false });
}
}

@bind
private async updateFile(file: File | Fileish) {
const loadingCounter = this.state.loadingCounter + 1;
Expand Down Expand Up @@ -334,7 +411,7 @@ export default class Compress extends Component<Props, State> {

const processed = await processInput(decoded, defaultInputProcessorState, processor);

// Another file has been opened before this one processed.
// Another file has been opened/processed before this one processed.
if (this.state.loadingCounter !== loadingCounter) return;

let newState: State = {
Expand All @@ -346,19 +423,9 @@ export default class Compress extends Component<Props, State> {
loading: false,
};

for (const i of [0, 1]) {
// Ditch previous encodings
const downloadUrl = this.state.sides[i].downloadUrl;
if (downloadUrl) URL.revokeObjectURL(downloadUrl!);

newState = cleanMerge(newState, `sides.${i}`, {
preprocessed: undefined,
file: undefined,
downloadUrl: undefined,
data: undefined,
encodedSettings: undefined,
});
newState = stateForNewSourceData(newState, newState.source!);

for (const i of [0, 1]) {
// Default resize values come from the image:
newState = cleanMerge(newState, `sides.${i}.latestSettings.preprocessorState.resize`, {
width: processed.width,
Expand All @@ -372,7 +439,7 @@ export default class Compress extends Component<Props, State> {
} catch (err) {
if (err.name === 'AbortError') return;
console.error(err);
// Another file has been opened before this one processed.
// Another file has been opened/processed before this one processed.
if (this.state.loadingCounter !== loadingCounter) return;
this.props.showSnack('Invalid image');
this.setState({ loading: false });
Expand Down Expand Up @@ -403,7 +470,7 @@ export default class Compress extends Component<Props, State> {
let preprocessed: ImageData | undefined;
let data: ImageData | undefined;
const cacheResult = this.encodeCache.match(
source, settings.preprocessorState, settings.encoderState,
source.processed, settings.preprocessorState, settings.encoderState,
);
const processor = (index === 0) ? this.leftProcessor : this.rightProcessor;

Expand All @@ -419,7 +486,7 @@ export default class Compress extends Component<Props, State> {
// Special case for identity
if (settings.encoderState.type === identity.type) {
file = source.file;
data = await source.processed;
data = source.processed;
} else {
preprocessed = (skipPreprocessing && side.preprocessed)
? side.preprocessed
Expand All @@ -431,10 +498,10 @@ export default class Compress extends Component<Props, State> {
data = await decodeImage(file, processor);

this.encodeCache.add({
source,
data,
preprocessed,
file,
sourceData: source.processed,
encoderState: settings.encoderState,
preprocessorState: settings.preprocessorState,
});
Expand Down Expand Up @@ -515,13 +582,15 @@ export default class Compress extends Component<Props, State> {
return (
<div class={style.compress}>
<Output
originalImage={source && source.processed}
source={source}
mobileView={mobileView}
leftCompressed={leftImageData}
rightCompressed={rightImageData}
leftImgContain={leftImgContain}
rightImgContain={rightImgContain}
onBack={onBack}
inputProcessorState={source && source.inputProcessorState}
onInputProcessorChange={this.onInputProcessorChange}
/>
{mobileView
? (
Expand Down
7 changes: 3 additions & 4 deletions src/components/compress/result-cache.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { EncoderState } from '../../codecs/encoders';
import { Fileish } from '../../lib/initial-util';
import { shallowEqual } from '../../lib/util';
import { SourceImage } from '.';
import { PreprocessorState } from '../../codecs/preprocessors';

import * as identity from '../../codecs/identity/encoder-meta';
Expand All @@ -15,7 +14,7 @@ interface CacheResult {
interface CacheEntry extends CacheResult {
preprocessorState: PreprocessorState;
encoderState: EncoderState;
source: SourceImage;
sourceData: ImageData;
}

const SIZE = 5;
Expand All @@ -32,13 +31,13 @@ export default class ResultCache {
}

match(
source: SourceImage,
sourceData: ImageData,
preprocessorState: PreprocessorState,
encoderState: EncoderState,
): CacheResult | undefined {
const matchingIndex = this._entries.findIndex((entry) => {
// Check for quick exits:
if (entry.source !== source) return false;
if (entry.sourceData !== sourceData) return false;
if (entry.encoderState.type !== encoderState.type) return false;

// Check that each set of options in the preprocessor are the same
Expand Down

0 comments on commit cca7368

Please sign in to comment.