Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OCR tool to media editing modal #11566

Merged
merged 1 commit into from
Aug 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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 => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ 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 {

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;
Expand All @@ -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) }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -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 {
Expand All @@ -47,6 +58,7 @@ class FocalPointModal extends ImmutablePureComponent {
dragging: false,
description: '',
dirty: false,
progress: 0,
};

componentWillMount () {
Expand Down Expand Up @@ -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;
Expand All @@ -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
/>

<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} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} />
<Button disabled={!dirty || detecting || length(description) > 420} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} />
</div>

<div className='report-modal__statuses'>
Expand Down
81 changes: 67 additions & 14 deletions app/javascript/styles/mastodon/components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@
-ms-overflow-style: -ms-autohiding-scrollbar;
}

.link-button {
display: block;
font-size: 15px;
line-height: 20px;
color: $ui-highlight-color;
border: 0;
background: transparent;
padding: 0;
cursor: pointer;

&:hover,
&:active {
text-decoration: underline;
}

&:disabled {
color: $ui-primary-color;
cursor: default;
}
}

.button {
background-color: $ui-highlight-color;
border: 10px none;
Expand Down Expand Up @@ -637,18 +658,6 @@
.character-counter__wrapper {
align-self: center;
margin-right: 4px;

.character-counter {
cursor: default;
font-family: $font-sans-serif, sans-serif;
font-size: 14px;
font-weight: 600;
color: $lighter-text-color;

&.character-counter--over {
color: $warning-red;
}
}
}
}

Expand All @@ -665,6 +674,18 @@
}
}

.character-counter {
cursor: default;
font-family: $font-sans-serif, sans-serif;
font-size: 14px;
font-weight: 600;
color: $lighter-text-color;

&.character-counter--over {
color: $warning-red;
}
}

.no-reduce-motion .spoiler-input {
transition: height 0.4s ease, opacity 0.4s ease;
}
Expand Down Expand Up @@ -4555,16 +4576,48 @@ a.status-card.compact:hover {
padding: 10px;
font-family: inherit;
font-size: 14px;
resize: vertical;
resize: none;
border: 0;
outline: 0;
border-radius: 4px;
border: 1px solid $ui-secondary-color;
margin-bottom: 20px;
min-height: 100px;
max-height: 50vh;
margin-bottom: 10px;

&:focus {
border: 1px solid darken($ui-secondary-color, 8%);
}

&__wrapper {
background: $white;
border: 1px solid $ui-secondary-color;
margin-bottom: 10px;
border-radius: 4px;

.setting-text {
border: 0;
margin-bottom: 0;
border-radius: 0;

&:focus {
border: 0;
}
}

&__modifiers {
color: $inverted-text-color;
font-family: inherit;
font-size: 14px;
background: $white;
}
}

&__toolbar {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
}

.setting-text-label {
Expand Down
8 changes: 4 additions & 4 deletions config/initializers/content_security_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
if Rails.env.development?
webpacker_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{Webpacker.dev_server.host_with_port}" }

p.connect_src :self, :blob, assets_host, Rails.configuration.x.streaming_api_base_url, *webpacker_urls
p.script_src :self, :unsafe_inline, :unsafe_eval, assets_host
p.connect_src :self, :data, :blob, assets_host, Rails.configuration.x.streaming_api_base_url, *webpacker_urls
p.script_src :self, :blob, :unsafe_inline, :unsafe_eval, assets_host
else
p.connect_src :self, :blob, assets_host, Rails.configuration.x.streaming_api_base_url
p.script_src :self, assets_host
p.connect_src :self, :data, :blob, assets_host, Rails.configuration.x.streaming_api_base_url
p.script_src :self, :blob, assets_host
end
end

Expand Down
1 change: 1 addition & 0 deletions config/webpack/development.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,6 @@ module.exports = merge(sharedConfig, {
settings.dev_server.watch_options,
watchOptions
),
writeToDisk: filePath => /ocr/.test(filePath),
},
});
5 changes: 5 additions & 0 deletions config/webpack/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { basename, dirname, join, relative, resolve } = require('path');
const { sync } = require('glob');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const AssetsManifestPlugin = require('webpack-assets-manifest');
const CopyPlugin = require('copy-webpack-plugin');
const extname = require('path-complete-extname');
const { env, settings, themes, output } = require('./configuration');
const rules = require('./rules');
Expand Down Expand Up @@ -84,6 +85,10 @@ module.exports = {
writeToDisk: true,
publicPath: true,
}),
new CopyPlugin([
{ from: 'node_modules/tesseract.js/dist/worker.min.js', to: 'ocr' },
{ from: 'node_modules/tesseract.js-core/tesseract-core.wasm.js', to: 'ocr' },
]),
],

resolve: {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"blurhash": "^1.0.0",
"classnames": "^2.2.5",
"compression-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.0.4",
"cross-env": "^5.1.4",
"css-loader": "^3.2.0",
"cssnano": "^4.1.10",
Expand Down Expand Up @@ -155,6 +156,7 @@
"stringz": "^2.0.0",
"substring-trie": "^1.0.2",
"terser-webpack-plugin": "^1.4.1",
"tesseract.js": "^2.0.0-alpha.13",
"throng": "^4.0.0",
"tiny-queue": "^0.2.1",
"uuid": "^3.1.0",
Expand Down
Binary file added public/ocr/lang-data/eng.traineddata.gz
Binary file not shown.
Loading