Skip to content
Snippets Groups Projects
Verified Commit b73f52f5 authored by Loïs Poujade's avatar Loïs Poujade
Browse files

Simplification of image edition in annotation

Removed non standard properties for better IIIF/WebAnnotation compliance
parent d28b5feb
No related tags found
1 merge request!5Create image annotation creation dialog
Pipeline #1281 passed
...@@ -4,6 +4,7 @@ import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'; ...@@ -4,6 +4,7 @@ import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
import AnnotationCreation from '../src/AnnotationCreation'; import AnnotationCreation from '../src/AnnotationCreation';
import AnnotationDrawing from '../src/AnnotationDrawing'; import AnnotationDrawing from '../src/AnnotationDrawing';
import TextEditor from '../src/TextEditor'; import TextEditor from '../src/TextEditor';
import ImageFormField from '../src/ImageFormField';
/** */ /** */
function createWrapper(props) { function createWrapper(props) {
...@@ -36,6 +37,10 @@ describe('AnnotationCreation', () => { ...@@ -36,6 +37,10 @@ describe('AnnotationCreation', () => {
wrapper = createWrapper(); wrapper = createWrapper();
expect(wrapper.dive().find(TextEditor).length).toBe(1); expect(wrapper.dive().find(TextEditor).length).toBe(1);
}); });
it('adds the ImageFormField component', () => {
wrapper = createWrapper();
expect(wrapper.dive().find(ImageFormField).length).toBe(1);
});
it('can handle annotations without target selector', () => { it('can handle annotations without target selector', () => {
wrapper = createWrapper({ wrapper = createWrapper({
annotation: { annotation: {
......
...@@ -131,23 +131,21 @@ describe('WebAnnotation', () => { ...@@ -131,23 +131,21 @@ describe('WebAnnotation', () => {
}); });
}); });
it('with image and text', () => { it('with image and text', () => {
subject = createSubject({ body: { value: 'hello' }, image: { url: 'http://example.photo/pic.jpg' }, tags: null }); subject = createSubject({ body: { value: 'hello' }, image: { id: 'http://example.photo/pic.jpg' }, tags: null });
expect(subject.createBody()).toEqual([ expect(subject.createBody()).toEqual([
{ {
type: 'TextualBody', type: 'TextualBody',
value: 'hello', value: 'hello',
}, },
{ {
format: 'image/jpg',
id: 'http://example.photo/pic.jpg', id: 'http://example.photo/pic.jpg',
type: 'Image', type: 'Image',
}, },
]); ]);
}); });
it('with image only', () => { it('with image only', () => {
subject = createSubject({ body: null, image: { url: 'http://example.photo/pic.jpg' }, tags: null }); subject = createSubject({ body: null, image: { id: 'http://example.photo/pic.jpg' }, tags: null });
expect(subject.createBody()).toEqual({ expect(subject.createBody()).toEqual({
format: 'image/jpg',
id: 'http://example.photo/pic.jpg', id: 'http://example.photo/pic.jpg',
type: 'Image', type: 'Image',
}); });
......
...@@ -19,14 +19,7 @@ import StrokeColorIcon from '@material-ui/icons/BorderColor'; ...@@ -19,14 +19,7 @@ import StrokeColorIcon from '@material-ui/icons/BorderColor';
import LineWeightIcon from '@material-ui/icons/LineWeight'; import LineWeightIcon from '@material-ui/icons/LineWeight';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import FormatShapesIcon from '@material-ui/icons/FormatShapes'; import FormatShapesIcon from '@material-ui/icons/FormatShapes';
import InsertPhotoIcon from '@material-ui/icons/InsertPhoto';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';
import TextField from '@material-ui/core/TextField'; import TextField from '@material-ui/core/TextField';
import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import { SketchPicker } from 'react-color'; import { SketchPicker } from 'react-color';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
...@@ -38,6 +31,7 @@ import TextEditor from './TextEditor'; ...@@ -38,6 +31,7 @@ import TextEditor from './TextEditor';
import WebAnnotation from './WebAnnotation'; import WebAnnotation from './WebAnnotation';
import CursorIcon from './icons/Cursor'; import CursorIcon from './icons/Cursor';
import HMSInput from './HMSInput'; import HMSInput from './HMSInput';
import ImageFormField from './ImageFormField';
import { secondsToHMS } from './utils'; import { secondsToHMS } from './utils';
/** Extract time information from annotation target */ /** Extract time information from annotation target */
...@@ -63,31 +57,11 @@ function geomFromAnnoTarget(annotarget) { ...@@ -63,31 +57,11 @@ function geomFromAnnoTarget(annotarget) {
/** */ /** */
class AnnotationCreation extends Component { class AnnotationCreation extends Component {
/** */
static checkURL(url) {
const expression = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
const regex = new RegExp(expression);
return url.match(regex);
}
/** */
static loadImg(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = url;
img.onload = () => resolve({ height: img.height, width: img.width });
img.onerror = reject;
});
}
/** */ /** */
constructor(props) { constructor(props) {
super(props); super(props);
const annoState = {}; const annoState = {};
annoState.image = false;
if (props.annotation) { if (props.annotation) {
// //
...@@ -155,26 +129,8 @@ class AnnotationCreation extends Component { ...@@ -155,26 +129,8 @@ class AnnotationCreation extends Component {
closedMode: 'closed', closedMode: 'closed',
currentColorType: false, currentColorType: false,
fillColor: null, fillColor: null,
imgConstrain: false, image: { id: null },
imgHeight: {
lastSubmittedValue: '',
srcValue: '',
validity: 0,
value: '',
},
imgUrl: {
lastSubmittedValue: '',
validity: 0,
value: '',
},
imgWidth: {
lastSubmittedValue: '',
srcValue: '',
validity: 0,
value: '',
},
lineWeightPopoverOpen: false, lineWeightPopoverOpen: false,
openAddImgDialog: false,
popoverAnchorEl: null, popoverAnchorEl: null,
popoverLineWeightAnchorEl: null, popoverLineWeightAnchorEl: null,
svg: null, svg: null,
...@@ -187,9 +143,6 @@ class AnnotationCreation extends Component { ...@@ -187,9 +143,6 @@ class AnnotationCreation extends Component {
this.submitForm = this.submitForm.bind(this); this.submitForm = this.submitForm.bind(this);
// this.updateBody = this.updateBody.bind(this); // this.updateBody = this.updateBody.bind(this);
this.updateTextBody = this.updateTextBody.bind(this); this.updateTextBody = this.updateTextBody.bind(this);
this.getImgDimensions = this.getImgDimensions.bind(this);
this.setImgWidth = this.setImgWidth.bind(this);
this.setImgHeight = this.setImgHeight.bind(this);
this.updateTstart = this.updateTstart.bind(this); this.updateTstart = this.updateTstart.bind(this);
this.updateTend = this.updateTend.bind(this); this.updateTend = this.updateTend.bind(this);
this.setTstartNow = this.setTstartNow.bind(this); this.setTstartNow = this.setTstartNow.bind(this);
...@@ -205,79 +158,13 @@ class AnnotationCreation extends Component { ...@@ -205,79 +158,13 @@ class AnnotationCreation extends Component {
this.handleCloseLineWeight = this.handleCloseLineWeight.bind(this); this.handleCloseLineWeight = this.handleCloseLineWeight.bind(this);
this.closeChooseColor = this.closeChooseColor.bind(this); this.closeChooseColor = this.closeChooseColor.bind(this);
this.updateStrokeColor = this.updateStrokeColor.bind(this); this.updateStrokeColor = this.updateStrokeColor.bind(this);
this.isConstrained = this.isConstrained.bind(this); this.handleImgChange = this.handleImgChange.bind(this);
this.handleConstrainCheck = this.handleConstrainCheck.bind(this);
this.handleImgDialogChange = this.handleImgDialogChange.bind(this);
this.handleImgDialogSubmit = this.handleImgDialogSubmit.bind(this);
} }
/** */ /** */
handleImgDialogChange(open) { handleImgChange(newUrl, imgRef) {
const { imgHeight, imgWidth, imgUrl } = this.state; const { image } = this.state;
this.setState({ image: { ...image, id: newUrl } });
this.setState({
imgHeight: {
...imgHeight,
validity: 1,
value: imgHeight.lastSubmittedValue,
},
imgUrl: {
...imgUrl,
validity: 1,
value: imgUrl.lastSubmittedValue,
},
imgWidth: {
...imgWidth,
validity: 1,
value: imgWidth.lastSubmittedValue,
},
openAddImgDialog: open,
});
}
/** */
handleConstrainCheck(event) {
const value = event.target.checked;
this.setState({
imgConstrain: value,
});
}
/** */
handleImgDialogSubmit() {
let open = true;
const { imgUrl, imgHeight, imgWidth } = this.state;
const urlValidity = AnnotationCreation.checkURL(imgUrl.value) ? 1 : 2;
const widthValidity = imgWidth.value > 0 ? 1 : 2;
const heightValidity = imgHeight.value > 0 ? 1 : 2;
if (urlValidity === 1 && widthValidity === 1 && heightValidity === 1) {
open = false;
}
this.setState({
image: { id: imgUrl.value },
imgHeight: {
...imgHeight,
lastSubmittedValue: heightValidity === 1 ? imgHeight.value : imgHeight.lastSubmittedValue,
validity: heightValidity,
value: imgHeight.value,
},
imgUrl: {
...imgUrl,
lastSubmittedValue: urlValidity === 1 ? imgUrl.value : imgUrl.lastSubmittedValue,
validity: urlValidity,
value: imgUrl.value,
},
imgWidth: {
...imgWidth,
lastSubmittedValue: widthValidity === 1 ? imgWidth.value : imgWidth.lastSubmittedValue,
validity: widthValidity,
value: imgWidth.value,
},
openAddImgDialog: open,
});
} }
/** */ /** */
...@@ -309,75 +196,6 @@ class AnnotationCreation extends Component { ...@@ -309,75 +196,6 @@ class AnnotationCreation extends Component {
this.setState({ tend: Math.floor(this.props.currentTime) }); this.setState({ tend: Math.floor(this.props.currentTime) });
} }
/** */
async getImgDimensions(url) {
const { imgHeight, imgWidth, imgUrl } = this.state;
const urlValidity = AnnotationCreation.checkURL(url) ? 1 : 2;
try {
const dimensions = await AnnotationCreation.loadImg(url);
this.setState({
imgHeight: {
...imgHeight,
srcValue: dimensions.height || '',
value: dimensions.height || '',
},
imgUrl: {
...imgUrl,
validity: 1,
value: url,
},
imgWidth: {
...imgWidth,
srcValue: dimensions.width || '',
value: dimensions.width || '',
},
});
} catch (e) {
this.setState({
imgUrl: {
...imgUrl,
validity: urlValidity,
value: url,
},
});
}
}
/** */
setImgWidth(value) {
const { imgWidth } = this.state;
this.setState({
imgWidth: {
...imgWidth,
value,
},
});
}
/** */
setImgUrl(value) {
const { imgUrl } = this.state;
this.setState({
imgUrl: {
...imgUrl,
value,
},
});
}
/** */
setImgHeight(value) {
const { imgHeight } = this.state;
this.setState({
imgHeight: {
...imgHeight,
value,
},
});
}
/** seekTo/goto annotation start time */ /** seekTo/goto annotation start time */
seekToTstart() { seekToTstart() {
const { paused, setCurrentTime, setSeekTo } = this.props; const { paused, setCurrentTime, setSeekTo } = this.props;
...@@ -406,37 +224,6 @@ class AnnotationCreation extends Component { ...@@ -406,37 +224,6 @@ class AnnotationCreation extends Component {
/** update annotation end time */ /** update annotation end time */
updateTend(value) { this.setState({ tend: value }); } updateTend(value) { this.setState({ tend: value }); }
/** */
isConstrained(event) { // adjust other dimension in proportion to inputted dimension
const { imgConstrain, imgWidth, imgHeight } = this.state;
const ratio = imgWidth.srcValue / imgHeight.srcValue;
if (imgConstrain) {
if (event.target.id === 'width' && imgWidth.srcValue !== '') {
// set height to be the same as width if width is less than 0
const height = imgWidth.value > 0 ? imgWidth.value * (1 / ratio) : imgWidth.value;
this.setState({
imgHeight: {
...imgHeight,
validity: 1,
value: height,
},
});
} else if (event.target.id === 'height' && imgHeight.srcValue !== '') {
// set width to be the same as height if height is less than 0
const width = imgHeight.value > 0 ? imgHeight.value * ratio : imgHeight.value;
this.setState({
imgWidth: {
...imgWidth,
validity: 1,
value: width,
},
});
}
}
}
/** */ /** */
openChooseColor(e) { openChooseColor(e) {
this.setState({ this.setState({
...@@ -478,33 +265,20 @@ class AnnotationCreation extends Component { ...@@ -478,33 +265,20 @@ class AnnotationCreation extends Component {
annotation, canvases, receiveAnnotation, config, annotation, canvases, receiveAnnotation, config,
} = this.props; } = this.props;
const { const {
textBody, image, imgWidth, imgHeight, imgUrl, tags, xywh, svg, textBody, image, tags, xywh, svg, tstart, tend, textEditorStateBustingKey,
imgConstrain, tstart, tend, textEditorStateBustingKey,
} = this.state; } = this.state;
const t = (tstart && tend) ? `${tstart},${tend}` : null; const t = (tstart && tend) ? `${tstart},${tend}` : null;
const annoBody = { value: (!textBody.length && t) ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : textBody }; const body = { value: (!textBody.length && t) ? `${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}` : textBody };
let imgBody;
if (imgWidth.validity === 1 && imgHeight.validity === 1 && imgUrl.validity === 1) {
imgBody = {
constrain: imgConstrain,
h: imgHeight.value,
url: imgUrl.value,
w: imgWidth.value,
};
} else {
imgBody = image;
}
canvases.forEach((canvas) => { canvases.forEach((canvas) => {
const storageAdapter = config.annotation.adapter(canvas.id); const storageAdapter = config.annotation.adapter(canvas.id);
const anno = new WebAnnotation({ const anno = new WebAnnotation({
body: annoBody, body,
canvasId: canvas.id, canvasId: canvas.id,
fragsel: { t, xywh }, fragsel: { t, xywh },
id: (annotation && annotation.id) || `${uuid()}`, id: (annotation && annotation.id) || `${uuid()}`,
image: imgBody, image,
manifestId: canvas.options.resource.id, manifestId: canvas.options.resource.id,
svg, svg,
tags, tags,
...@@ -522,7 +296,7 @@ class AnnotationCreation extends Component { ...@@ -522,7 +296,7 @@ class AnnotationCreation extends Component {
}); });
this.setState({ this.setState({
image: false, image: { id: null },
svg: null, svg: null,
tend: null, tend: null,
textBody: '', textBody: '',
...@@ -565,9 +339,9 @@ class AnnotationCreation extends Component { ...@@ -565,9 +339,9 @@ class AnnotationCreation extends Component {
} = this.props; } = this.props;
const { const {
activeTool, colorPopoverOpen, currentColorType, fillColor, openAddImgDialog, popoverAnchorEl, activeTool, colorPopoverOpen, currentColorType, fillColor, popoverAnchorEl,
strokeColor, popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode, strokeColor, popoverLineWeightAnchorEl, lineWeightPopoverOpen, strokeWidth, closedMode,
textBody, imgUrl, imgWidth, imgHeight, imgConstrain, svg, tstart, tend, textBody, svg, tstart, tend,
textEditorStateBustingKey, image, textEditorStateBustingKey, image,
} = this.state; } = this.state;
...@@ -740,79 +514,8 @@ class AnnotationCreation extends Component { ...@@ -740,79 +514,8 @@ class AnnotationCreation extends Component {
</Typography> </Typography>
</Grid> </Grid>
<Grid item xs={12} style={{ marginBottom: 10 }}> <Grid item xs={12} style={{ marginBottom: 10 }}>
<ToggleButton value="image-icon" aria-label="insert an image" onClick={() => this.handleImgDialogChange(true)}> <ImageFormField value={image} onChange={this.handleImgChange} />
{ image === false && <InsertPhotoIcon /> }
{ image !== false && <img src={image.id} width="100" height="auto" alt="loading failed" /> }
</ToggleButton>
</Grid> </Grid>
<Dialog open={openAddImgDialog} fullWidth onClose={() => this.handleImgDialogChange(false)} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title" disableTypography>
<Typography variant="h2">Insert image</Typography>
</DialogTitle>
<DialogContent>
<DialogTitle id="form-dialog-subtitle-1" style={{ paddingLeft: 0 }} disableTypography>
<Typography variant="h5">Image source</Typography>
</DialogTitle>
<TextField
value={imgUrl.value}
onChange={(e) => this.setImgUrl(e.target.value)}
onBlur={(e) => this.getImgDimensions(e.target.value)}
error={imgUrl.validity === 2}
helperText={imgUrl.validity === 2 ? 'Invalid URL' : ''}
margin="dense"
id="source"
label="Image URL"
type="url"
fullWidth
/>
</DialogContent>
<DialogContent>
<DialogTitle id="form-dialog-subtitle-2" style={{ paddingLeft: 0 }} disableTypography>
<Typography variant="h5">Image dimensions</Typography>
</DialogTitle>
<TextField
value={imgWidth.value}
style={{ marginRight: 10, width: 100 }}
onChange={(e) => this.setImgWidth(e.target.value)}
onBlur={(e) => this.isConstrained(e)}
error={imgWidth.validity === 2}
helperText={imgWidth.validity === 2 ? 'Invalid width' : ''}
margin="dense"
id="width"
label="Width"
type="number"
variant="outlined"
/>
<TextField
value={imgHeight.value}
style={{ marginLeft: 10, width: 100 }}
onChange={(e) => this.setImgHeight(e.target.value)}
onBlur={(e) => this.isConstrained(e)}
error={imgHeight.validity === 2}
helperText={imgHeight.validity === 2 ? 'Invalid height' : ''}
margin="dense"
id="height"
label="Height"
type="number"
variant="outlined"
/>
<FormControlLabel
control={(
<Checkbox
checked={imgConstrain}
onChange={(e) => this.handleConstrainCheck(e)}
inputProps={{ 'aria-label': 'primary checkbox' }}
style={{ marginLeft: 30 }}
/>
)}
label="Constrain proportions"
/>
</DialogContent>
<DialogActions>
<Button onClick={() => this.handleImgDialogChange(false)}>Cancel</Button>
<Button variant="contained" onClick={this.handleImgDialogSubmit} color="primary">Add</Button>
</DialogActions>
</Dialog>
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="overline"> <Typography variant="overline">
Text Content Text Content
......
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import { TextField } from '@material-ui/core';
/** URL input with an <img> preview */
class ImageFormField extends Component {
/** */
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
/** Render input field and a preview if the input is valid */
render() {
const { value: image, classes, onChange } = this.props;
const imgIsValid = this.inputRef.current
? (image.id && this.inputRef.current.checkValidity()) : image.id;
const imgUrl = image.id === null ? '' : image.id;
return (
<div className={classes.root}>
<TextField
value={imgUrl}
onChange={(ev) => onChange(ev.target.value)}
error={imgUrl !== '' && !imgIsValid}
margin="dense"
label="Image URL"
type="url"
fullWidth
inputRef={this.inputRef}
/>
{ imgIsValid
&& <img src={image.id} width="100%" height="auto" alt="loading failed" /> }
</div>
);
}
}
/** custom css */
const styles = (theme) => ({
root: {
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
},
});
ImageFormField.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
classes: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.shape({
id: PropTypes.string,
}).isRequired,
};
ImageFormField.defaultProps = {
};
export default withStyles(styles)(ImageFormField);
...@@ -37,11 +37,11 @@ export default class WebAnnotation { ...@@ -37,11 +37,11 @@ export default class WebAnnotation {
bodies.push(textBody); bodies.push(textBody);
} }
// TODO correct WebAnnotation format
if (this.image) { if (this.image) {
const imgBody = { type: 'Image' }; const imgBody = {
Object.assign(imgBody, this.image); id: this.image.id,
imgBody.id = imgBody.url; type: 'Image',
};
bodies.push(imgBody); bodies.push(imgBody);
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment