From b1ee6f2ae5a66e4194abc2562333ed4a0985fa43 Mon Sep 17 00:00:00 2001 From: Ali Abid Date: Wed, 13 Oct 2021 21:06:47 +0000 Subject: [PATCH] restore cropper functionality --- demo/image_mod.py | 6 +- frontend/package-lock.json | 13 ++ frontend/package.json | 1 + frontend/src/components.jsx | 197 ++++++++++++++++---- frontend/src/components/base_component.jsx | 3 + frontend/src/components/input/image.jsx | 107 +++++++---- frontend/src/components/output/carousel.jsx | 6 +- frontend/src/gradio.jsx | 48 +++-- 8 files changed, 289 insertions(+), 92 deletions(-) diff --git a/demo/image_mod.py b/demo/image_mod.py index 5e8d195bb9..a0bfb8d848 100644 --- a/demo/image_mod.py +++ b/demo/image_mod.py @@ -8,15 +8,13 @@ def image_mod(image): iface = gr.Interface(image_mod, - gr.inputs.Image(type="pil", optional=True), + gr.inputs.Image(type="pil", optional=True, tool="select"), "image", examples=[ ["images/cheetah1.jpg"], ["images/cheetah2.jpg"], ["images/lion.jpg"], - ]) - -iface.test_launch() + ], live=True) if __name__ == "__main__": iface.launch() diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 645f7f9ed8..42b1f78f3e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5005,6 +5005,11 @@ "sha.js": "^2.4.8" } }, + "cropperjs": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.5.12.tgz", + "integrity": "sha512-re7UdjE5UnwdrovyhNzZ6gathI4Rs3KGCBSc8HCIjUo5hO42CtzyblmWLj6QWVw7huHyDMfpKxhiO2II77nhDw==" + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -15446,6 +15451,14 @@ "lodash.curry": "^4.1.1" } }, + "react-cropper": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/react-cropper/-/react-cropper-2.1.8.tgz", + "integrity": "sha512-QEj6CE9et/gMRqpaKMgZQdBgtzLjjq+zj1pmHwtoWG6GqscDl4QpTwoEElWN2pieYxkwFaZa0lPiD2b9nwqLKQ==", + "requires": { + "cropperjs": "^1.5.12" + } + }, "react-dev-utils": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1eac5ea556..0b591219af 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "prettier-eslint": "^13.0.0", "prettier-eslint-cli": "^5.0.1", "react": "^17.0.2", + "react-cropper": "^2.1.8", "react-dom": "^17.0.2", "react-json-tree": "^0.15.0", "react-plotly.js": "^2.5.1", diff --git a/frontend/src/components.jsx b/frontend/src/components.jsx index 519a61dca4..5ba2ee9003 100644 --- a/frontend/src/components.jsx +++ b/frontend/src/components.jsx @@ -61,42 +61,169 @@ import { } from "./components/output/timeseries"; import { VideoOutput, VideoOutputExample } from "./components/output/video"; -let input_component_map = { - audio: [AudioInput, AudioInputExample], - checkboxgroup: [CheckboxGroupInput, CheckboxGroupInputExample], - checkbox: [CheckboxInput, CheckboxInputExample], - dataframe: [DataframeInput, DataframeInputExample], - dropdown: [DropdownInput, DropdownInputExample], - file: [FileInput, FileInputExample], - image: [ImageInput, ImageInputExample], - number: [NumberInput, NumberInputExample], - radio: [RadioInput, RadioInputExample], - slider: [SliderInput, SliderInputExample], - textbox: [TextboxInput, TextboxInputExample], - timeseries: [TimeseriesInput, TimeseriesInputExample], - video: [VideoInput, VideoInputExample] -}; -let output_component_map = { - audio: [AudioOutput, AudioOutputExample], - carousel: [CarouselOutput, CarouselOutputExample], - dataframe: [DataframeOutput, DataframeOutputExample], - file: [FileOutput, FileOutputExample], - highlightedtext: [HighlightedTextOutput, HighlightedTextOutputExample], - html: [HTMLOutput, HTMLOutputExample], - image: [ImageOutput, ImageOutputExample], - json: [JSONOutput, JSONOutputExample], - keyvalues: [KeyValuesOutput, KeyValuesOutputExample], - label: [LabelOutput, LabelOutputExample], - textbox: [TextboxOutput, TextboxOutputExample], - timeseries: [TimeseriesOutput, TimeseriesOutputExample], - video: [VideoOutput, VideoOutputExample] -}; +let input_component_set = [ + { + name: "audio", + component: AudioInput, + memoized_component: null, + example_component: AudioInputExample + }, + { + name: "checkboxgroup", + component: CheckboxGroupInput, + memoized_component: null, + example_component: CheckboxGroupInputExample + }, + { + name: "checkbox", + component: CheckboxInput, + memoized_component: null, + example_component: CheckboxInputExample + }, + { + name: "dataframe", + component: DataframeInput, + memoized_component: null, + example_component: DataframeInputExample + }, + { + name: "dropdown", + component: DropdownInput, + memoized_component: null, + example_component: DropdownInputExample + }, + { + name: "file", + component: FileInput, + memoized_component: null, + example_component: FileInputExample + }, + { + name: "image", + component: ImageInput, + memoized_component: null, + example_component: ImageInputExample + }, + { + name: "number", + component: NumberInput, + memoized_component: null, + example_component: NumberInputExample + }, + { + name: "radio", + component: RadioInput, + memoized_component: null, + example_component: RadioInputExample + }, + { + name: "slider", + component: SliderInput, + memoized_component: null, + example_component: SliderInputExample + }, + { + name: "textbox", + component: TextboxInput, + memoized_component: null, + example_component: TextboxInputExample + }, + { + name: "timeseries", + component: TimeseriesInput, + memoized_component: null, + example_component: TimeseriesInputExample + }, + { name: "video", component: VideoInput, example_component: VideoInputExample } +]; +let output_component_set = [ + { + name: "audio", + component: AudioOutput, + memoized_component: null, + example_component: AudioOutputExample + }, + { + name: "carousel", + component: CarouselOutput, + memoized_component: null, + example_component: CarouselOutputExample + }, + { + name: "dataframe", + component: DataframeOutput, + memoized_component: null, + example_component: DataframeOutputExample + }, + { + name: "file", + component: FileOutput, + memoized_component: null, + example_component: FileOutputExample + }, + { + name: "highlightedtext", + component: HighlightedTextOutput, + memoized_component: null, + example_component: HighlightedTextOutputExample + }, + { + name: "html", + component: HTMLOutput, + memoized_component: null, + example_component: HTMLOutputExample + }, + { + name: "image", + component: ImageOutput, + memoized_component: null, + example_component: ImageOutputExample + }, + { + name: "json", + component: JSONOutput, + memoized_component: null, + example_component: JSONOutputExample + }, + { + name: "keyvalues", + component: KeyValuesOutput, + memoized_component: null, + example_component: KeyValuesOutputExample + }, + { + name: "label", + component: LabelOutput, + memoized_component: null, + example_component: LabelOutputExample + }, + { + name: "textbox", + component: TextboxOutput, + memoized_component: null, + example_component: TextboxOutputExample + }, + { + name: "timeseries", + component: TimeseriesOutput, + memoized_component: null, + example_component: TimeseriesOutputExample + }, + { + name: "video", + component: VideoOutput, + memoized_component: null, + example_component: VideoOutputExample + } +]; -for (let component_map of [input_component_map, output_component_map]) { - for (let [key, components] of Object.entries(component_map)) { - let component = components[0]; - component_map[key][0] = React.memo(component, component.memo); +for (let component_set of [input_component_set, output_component_set]) { + for (let component_data of component_set) { + component_data.memoized_component = React.memo( + component_data.component, + component_data.component.memo + ); } } -export { input_component_map, output_component_map }; +export { input_component_set, output_component_set }; diff --git a/frontend/src/components/base_component.jsx b/frontend/src/components/base_component.jsx index d0853f28a8..8c6f65a6ae 100644 --- a/frontend/src/components/base_component.jsx +++ b/frontend/src/components/base_component.jsx @@ -4,4 +4,7 @@ export default class BaseComponent extends React.Component { static memo = (a, b) => { return a.value === b.value && a.interpretation === b.interpretation; }; + static postprocess = (y) => { + return y; + }; } diff --git a/frontend/src/components/input/image.jsx b/frontend/src/components/input/image.jsx index 3a8342eef5..a74bd1f527 100644 --- a/frontend/src/components/input/image.jsx +++ b/frontend/src/components/input/image.jsx @@ -6,6 +6,8 @@ import { SketchField, Tools } from "../../vendor/ReactSketch"; import { getObjectFitSize, paintSaliency } from "../../utils"; import "tui-image-editor/dist/tui-image-editor.css"; import ImageEditor from "@toast-ui/react-image-editor"; +import Cropper from "react-cropper"; +import "cropperjs/dist/cropper.css"; class ImageInput extends BaseComponent { constructor(props) { @@ -26,9 +28,21 @@ class ImageInput extends BaseComponent { this.webcamRef = React.createRef(); this.sketchRef = React.createRef(); this.editorRef = React.createRef(); + this.cropperRef = React.createRef(); this.sketchKey = 0; this.state = { editorMode: false }; } + static memo = (a, b) => { + if (a.interpretation != b.interpretation) { + return false; + } else if (a.value === null && b.value === null) { + return true; + } else if (a.value === null || b.value === null) { + return false; + } else { + return a.value.src === b.value.src; + } + }; handleChange(data) { this.props.handleChange(data); } @@ -37,18 +51,18 @@ class ImageInput extends BaseComponent { } snapshot() { let imageSrc = this.webcamRef.current.getScreenshot(); - this.handleChange(imageSrc); + this.handleChange({ src: imageSrc, crop: null }); } getSketch() { let imageSrc = this.sketchRef.current.toDataURL(); - this.handleChange(imageSrc); + this.handleChange({ src: imageSrc, crop: null }); } cancelEditor() { this.setState({ editorMode: false }); } saveEditor() { const editorInstance = this.editorRef.current.getInstance(); - this.handleChange(editorInstance.toDataURL()); + this.handleChange({ src: editorInstance.toDataURL(), crop: null }); this.setState({ editorMode: false }); } onImgLoad({ target: img }) { @@ -62,6 +76,10 @@ class ImageInput extends BaseComponent { openEditor() { this.setState({ editorMode: true }); } + onCrop = () => { + const crop = this.cropperRef.current.cropper.getCroppedCanvas().toDataURL(); + this.handleChange({ src: this.props.value.src, crop: crop }); + }; render() { let no_action = (evt) => { evt.preventDefault(); @@ -107,39 +125,53 @@ class ImageInput extends BaseComponent { return (
- {this.state.editorMode ? ( -
-
- - + {this.props.tool === "editor" ? ( + this.state.editorMode ? ( +
+
+ + +
+
- -
+ ) : ( + + ) ) : ( - + false + )} + {this.props.tool === "select" ? ( + + ) : ( + )} -
{interpretation}
@@ -229,12 +261,19 @@ class ImageInput extends BaseComponent { var ReaderObj = new FileReader(); ReaderObj.readAsDataURL(files[0]); ReaderObj.onloadend = function () { - component.props.handleChange(this.result); + component.props.handleChange({ src: this.result, crop: null }); }; } + static postprocess = (y) => { + return y.crop === null ? y.src : y.crop; + }; } class ImageInputExample extends DataURLComponentExample { + static async preprocess(x, examples_dir) { + let src = await DataURLComponentExample.preprocess(x, examples_dir); + return { src: src, crop: null }; + } render() { return ( {this.props.components.map((config, index) => { - let Component = output_component_map[config.name][0]; + let Component = output_component_set.find( + (c) => c.name === config.name + ).memoized_component; return (
{config.label ? ( diff --git a/frontend/src/gradio.jsx b/frontend/src/gradio.jsx index c2b750614b..62469bfe43 100644 --- a/frontend/src/gradio.jsx +++ b/frontend/src/gradio.jsx @@ -1,6 +1,6 @@ import React from "react"; import html2canvas from "html2canvas-objectfit-fix"; -import { input_component_map, output_component_map } from "./components"; +import { input_component_set, output_component_set } from "./components"; import { saveAs } from "./utils"; import ReactDOM from "react-dom"; import classNames from "classnames"; @@ -104,14 +104,17 @@ export class GradioInterface extends React.Component { } this.pending_response = true; let input_state = []; - for (let i = 0; i < this.props.input_components.length; i++) { + for (let [i, input_component] of this.props.input_components.entries()) { if ( this.state[i] === null && this.props.input_components[i].optional !== true ) { return; } - input_state[i] = this.state[i]; + let InputComponentClass = input_component_set.find( + (c) => c.name === input_component.name + ).component; + input_state[i] = InputComponentClass.postprocess(this.state[i]); } this.setState({ submitting: true, @@ -237,14 +240,22 @@ export class GradioInterface extends React.Component { handleExampleChange = (example_id) => { this.setState({ example_id: example_id }); for (let [i, item] of this.props.examples[example_id].entries()) { - let ExampleComponent = - i < this.props.input_components.length - ? input_component_map[this.props.input_components[i].name][1] - : output_component_map[ - this.props.output_components[ - i - this.props.input_components.length - ].name - ][1]; + let ExampleComponent; + if (i < this.props.input_components.length) { + let component_name = this.props.input_components[i].name; + let component_data = input_component_set.find( + (c) => c.name === component_name + ); + ExampleComponent = component_data.example_component; + } else { + let component_name = + this.props.output_components[i - this.props.input_components.length] + .name; + let component_data = output_component_set.find( + (c) => c.name === component_name + ); + ExampleComponent = component_data.example_component; + } ExampleComponent.preprocess(item, this.examples_dir).then((data) => { this.handleChange(i, data); }); @@ -290,7 +301,9 @@ export class GradioInterface extends React.Component { >
{this.props.input_components.map((component, index) => { - const Component = input_component_map[component.name][0]; + const Component = input_component_set.find( + (c) => c.name === component.name + ).memoized_component; return (
{component.label}
@@ -335,7 +348,9 @@ export class GradioInterface extends React.Component { > {status} {this.props.output_components.map((component, index) => { - const Component = output_component_map[component.name][0]; + const Component = output_component_set.find( + (c) => c.name === component.name + ).memoized_component; const key = this.props.input_components.length + index; return this.state[key] === null ? ( false @@ -499,10 +514,9 @@ class GradioInterfaceExamples extends React.Component { onClick={() => this.props.handleExampleChange(i)} > {example_row.map((example_data, j) => { - let ExampleComponent = - input_component_map[ - this.props.input_components[j].name - ][1]; + let ExampleComponent = input_component_set.find( + (c) => c.name === this.props.input_components[j].name + ).example_component; return (