Add OCR tool to media editing modal (#11566)
This commit is contained in:
		
							parent
							
								
									f178a01c11
								
							
						
					
					
						commit
						28636f43e4
					
				
					 10 changed files with 275 additions and 34 deletions
				
			
		| 
						 | 
				
			
			@ -4,6 +4,7 @@ import UploadProgressContainer from '../containers/upload_progress_container';
 | 
			
		|||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import UploadContainer from '../containers/upload_container';
 | 
			
		||||
import SensitiveButtonContainer from '../containers/sensitive_button_container';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
export default class UploadForm extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +17,7 @@ export default class UploadForm extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='compose-form__upload-wrapper'>
 | 
			
		||||
        <UploadProgressContainer />
 | 
			
		||||
        <UploadProgressContainer icon='upload' message={<FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />} />
 | 
			
		||||
 | 
			
		||||
        <div className='compose-form__uploads-wrapper'>
 | 
			
		||||
          {mediaIds.map(id => (
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,6 @@ import React from 'react';
 | 
			
		|||
import PropTypes from 'prop-types';
 | 
			
		||||
import Motion from '../../ui/util/optional_motion';
 | 
			
		||||
import spring from 'react-motion/lib/spring';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
import Icon from 'mastodon/components/icon';
 | 
			
		||||
 | 
			
		||||
export default class UploadProgress extends React.PureComponent {
 | 
			
		||||
| 
						 | 
				
			
			@ -10,10 +9,12 @@ export default class UploadProgress extends React.PureComponent {
 | 
			
		|||
  static propTypes = {
 | 
			
		||||
    active: PropTypes.bool,
 | 
			
		||||
    progress: PropTypes.number,
 | 
			
		||||
    icon: PropTypes.string.isRequired,
 | 
			
		||||
    message: PropTypes.node.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { active, progress } = this.props;
 | 
			
		||||
    const { active, progress, icon, message } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!active) {
 | 
			
		||||
      return null;
 | 
			
		||||
| 
						 | 
				
			
			@ -22,11 +23,11 @@ export default class UploadProgress extends React.PureComponent {
 | 
			
		|||
    return (
 | 
			
		||||
      <div className='upload-progress'>
 | 
			
		||||
        <div className='upload-progress__icon'>
 | 
			
		||||
          <Icon id='upload' />
 | 
			
		||||
          <Icon id={icon} />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div className='upload-progress__message'>
 | 
			
		||||
          <FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' />
 | 
			
		||||
          {message}
 | 
			
		||||
 | 
			
		||||
          <div className='upload-progress__backdrop'>
 | 
			
		||||
            <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,11 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 | 
			
		|||
import IconButton from 'mastodon/components/icon_button';
 | 
			
		||||
import Button from 'mastodon/components/button';
 | 
			
		||||
import Video from 'mastodon/features/video';
 | 
			
		||||
import { TesseractWorker } from 'tesseract.js';
 | 
			
		||||
import Textarea from 'react-textarea-autosize';
 | 
			
		||||
import UploadProgress from 'mastodon/features/compose/components/upload_progress';
 | 
			
		||||
import CharacterCounter from 'mastodon/features/compose/components/character_counter';
 | 
			
		||||
import { length } from 'stringz';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  close: { id: 'lightbox.close', defaultMessage: 'Close' },
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +34,12 @@ const mapDispatchToProps = (dispatch, { id }) => ({
 | 
			
		|||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
 | 
			
		||||
  .replace(/\n/g, ' ')
 | 
			
		||||
  .replace(/\*\*\*\*\*\*/g, '\n\n');
 | 
			
		||||
 | 
			
		||||
const assetHost = process.env.CDN_HOST || '';
 | 
			
		||||
 | 
			
		||||
export default @connect(mapStateToProps, mapDispatchToProps)
 | 
			
		||||
@injectIntl
 | 
			
		||||
class FocalPointModal extends ImmutablePureComponent {
 | 
			
		||||
| 
						 | 
				
			
			@ -47,6 +58,7 @@ class FocalPointModal extends ImmutablePureComponent {
 | 
			
		|||
    dragging: false,
 | 
			
		||||
    description: '',
 | 
			
		||||
    dirty: false,
 | 
			
		||||
    progress: 0,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentWillMount () {
 | 
			
		||||
| 
						 | 
				
			
			@ -133,9 +145,27 @@ class FocalPointModal extends ImmutablePureComponent {
 | 
			
		|||
    this.node = c;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleTextDetection = () => {
 | 
			
		||||
    const { media } = this.props;
 | 
			
		||||
 | 
			
		||||
    const worker = new TesseractWorker({
 | 
			
		||||
      workerPath: `${assetHost}/packs/ocr/worker.min.js`,
 | 
			
		||||
      corePath: `${assetHost}/packs/ocr/tesseract-core.wasm.js`,
 | 
			
		||||
      langPath: `${assetHost}/ocr/lang-data`,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.setState({ detecting: true });
 | 
			
		||||
 | 
			
		||||
    worker.recognize(media.get('url'))
 | 
			
		||||
      .progress(({ progress }) => this.setState({ progress }))
 | 
			
		||||
      .finally(() => worker.terminate())
 | 
			
		||||
      .then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false }))
 | 
			
		||||
      .catch(() => this.setState({ detecting: false }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { media, intl, onClose } = this.props;
 | 
			
		||||
    const { x, y, dragging, description, dirty } = this.state;
 | 
			
		||||
    const { x, y, dragging, description, dirty, detecting, progress } = this.state;
 | 
			
		||||
 | 
			
		||||
    const width  = media.getIn(['meta', 'original', 'width']) || null;
 | 
			
		||||
    const height = media.getIn(['meta', 'original', 'height']) || null;
 | 
			
		||||
| 
						 | 
				
			
			@ -158,15 +188,27 @@ class FocalPointModal extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
            <label className='setting-text-label' htmlFor='upload-modal__description'><FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' /></label>
 | 
			
		||||
 | 
			
		||||
            <textarea
 | 
			
		||||
              id='upload-modal__description'
 | 
			
		||||
              className='setting-text light'
 | 
			
		||||
              value={description}
 | 
			
		||||
              onChange={this.handleChange}
 | 
			
		||||
              autoFocus
 | 
			
		||||
            />
 | 
			
		||||
            <div className='setting-text__wrapper'>
 | 
			
		||||
              <Textarea
 | 
			
		||||
                id='upload-modal__description'
 | 
			
		||||
                className='setting-text light'
 | 
			
		||||
                value={detecting ? '…' : description}
 | 
			
		||||
                onChange={this.handleChange}
 | 
			
		||||
                disabled={detecting}
 | 
			
		||||
                autoFocus
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
            <Button disabled={!dirty} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} />
 | 
			
		||||
              <div className='setting-text__modifiers'>
 | 
			
		||||
                <UploadProgress progress={progress * 100} active={detecting} icon='file-text-o' message={<FormattedMessage id='upload_modal.analyzing_picture' defaultMessage='Analyzing picture…' />} />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className='setting-text__toolbar'>
 | 
			
		||||
              <button disabled={detecting || media.get('type') !== 'image'} className='link-button' onClick={this.handleTextDetection}><FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' /></button>
 | 
			
		||||
              <CharacterCounter max={420} text={detecting ? '' : description} />
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <Button disabled={!dirty || detecting || length(description) > 420} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className='report-modal__statuses'>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Reference in a new issue