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

Merge branch '5-images-in-annotations' into demo_gitlab_ci

parents e8a0a872 b73f52f5
No related branches found
No related tags found
No related merge requests found
...@@ -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