summaryrefslogtreecommitdiff
path: root/animism-align
diff options
context:
space:
mode:
authorJules Laplace <julescarbon@gmail.com>2020-07-16 18:25:18 +0200
committerJules Laplace <julescarbon@gmail.com>2020-07-16 18:25:18 +0200
commite2e27ed91b8ed8a024223ad03be9d2566750e880 (patch)
tree24ab0b65ce5ca7987bdaf8a7d99d3d942bc1f19e /animism-align
parentc4d20db0c6a8a0ba45a453ad1b5a3296db7a127e (diff)
cropping images and uploading multiple versions
Diffstat (limited to 'animism-align')
-rw-r--r--animism-align/cli/app/controllers/upload_controller.py11
-rw-r--r--animism-align/cli/app/sql/models/upload.py2
-rw-r--r--animism-align/cli/app/utils/file_utils.py2
-rw-r--r--animism-align/frontend/api/crud.reducer.js4
-rw-r--r--animism-align/frontend/api/crud.upload.js10
-rw-r--r--animism-align/frontend/common/upload.helpers.js28
-rw-r--r--animism-align/frontend/util/index.js82
-rw-r--r--animism-align/frontend/views/media/components/media.form.js5
-rw-r--r--animism-align/frontend/views/media/components/media.formImage.js140
-rw-r--r--animism-align/frontend/views/media/media.css3
-rw-r--r--animism-align/frontend/views/paragraph/components/paragraph.types.js1
11 files changed, 190 insertions, 98 deletions
diff --git a/animism-align/cli/app/controllers/upload_controller.py b/animism-align/cli/app/controllers/upload_controller.py
index 5fec077..1a324cb 100644
--- a/animism-align/cli/app/controllers/upload_controller.py
+++ b/animism-align/cli/app/controllers/upload_controller.py
@@ -53,6 +53,11 @@ class UploadView(FlaskView):
except:
raise APIError('No username specified')
+ try:
+ tag = request.form.get('tag')
+ except:
+ raise APIError('No tag specified')
+
param_name = 'image'
if param_name not in request.files:
raise APIError('No file uploaded')
@@ -93,11 +98,10 @@ class UploadView(FlaskView):
session.close()
return jsonify(response)
- tag = request.form.get('tag')
uploaded_im_fn = secure_filename(file.filename)
uploaded_im_abspath = os.path.join(app_cfg.DIR_UPLOADS, tag)
uploaded_im_fullpath = os.path.join(uploaded_im_abspath, uploaded_im_fn)
- uploaded_im_stored_fn = os.path.join(tag, uploaded_im_fn)
+ uploaded_im_stored_fn = uploaded_im_fn
os.makedirs(uploaded_im_abspath, exist_ok=True)
# nparr.tofile(uploaded_im_fullpath)
@@ -128,9 +132,10 @@ class UploadView(FlaskView):
sha256 = upload.sha256
fn = upload.fn
+ tag = upload.tag
# uploaded_im_fn = secure_filename(fn)
- uploaded_im_abspath = os.path.join(app_cfg.DIR_UPLOADS, fn)
+ uploaded_im_abspath = os.path.join(app_cfg.DIR_UPLOADS, tag, fn)
# uploaded_im_fullpath = os.path.join(uploaded_im_abspath, fn)
if os.path.exists(uploaded_im_abspath):
print("Removing " + uploaded_im_abspath)
diff --git a/animism-align/cli/app/sql/models/upload.py b/animism-align/cli/app/sql/models/upload.py
index 462c236..ae30a54 100644
--- a/animism-align/cli/app/sql/models/upload.py
+++ b/animism-align/cli/app/sql/models/upload.py
@@ -43,5 +43,7 @@ class Upload(Base):
# return join(self.filepath(), self.filename())
def url(self):
+ if self.tag:
+ return join('/static/data_store/uploads', self.tag, self.fn)
return join('/static/data_store/uploads', self.fn)
# return join(app_cfg.URL_UPLOADS, sha256_tree(self.sha256), self.filename())
diff --git a/animism-align/cli/app/utils/file_utils.py b/animism-align/cli/app/utils/file_utils.py
index 7f1f417..1d19fd6 100644
--- a/animism-align/cli/app/utils/file_utils.py
+++ b/animism-align/cli/app/utils/file_utils.py
@@ -39,7 +39,7 @@ from tqdm import tqdm
ZERO_PADDING = 6 # padding for enumerated image filenames
HASH_TREE_DEPTH = 2
HASH_BRANCH_SIZE = 2
-VALID_IMAGE_EXTS = ['jpg', 'jpeg', 'png']
+VALID_IMAGE_EXTS = ['gif', 'jpg', 'jpeg', 'png']
VALID_VIDEO_EXTS = ['mp4', 'mov']
# ------------------------------------------
diff --git a/animism-align/frontend/api/crud.reducer.js b/animism-align/frontend/api/crud.reducer.js
index 38aa4f8..ece6cec 100644
--- a/animism-align/frontend/api/crud.reducer.js
+++ b/animism-align/frontend/api/crud.reducer.js
@@ -115,6 +115,10 @@ export const crudReducer = (type) => {
...state,
update: action.data,
index: addToIndex(state.index, action.data.res, state.options.sort),
+ show: state.show.res.id === action.data.res.id ? {
+ ...state.show.res,
+ ...action.data.res,
+ } : state.show.res
}
case crud_type.index_error:
return {
diff --git a/animism-align/frontend/api/crud.upload.js b/animism-align/frontend/api/crud.upload.js
index 8a711c7..beec86c 100644
--- a/animism-align/frontend/api/crud.upload.js
+++ b/animism-align/frontend/api/crud.upload.js
@@ -6,9 +6,17 @@ export function crud_upload(type, data, dispatch) {
const { id } = data
const fd = new FormData()
+ if (!data.tag) {
+ data.tag = 'misc'
+ }
Object.keys(data).forEach(key => {
- if (key !== 'id') {
+ if (key.indexOf('__') !== -1) return
+ if (key === 'id') return
+ const fn_key = '__' + key + '_filename'
+ if (fn_key in data) {
+ fd.append(key, data[key], data[fn_key])
+ } else {
fd.append(key, data[key])
}
})
diff --git a/animism-align/frontend/common/upload.helpers.js b/animism-align/frontend/common/upload.helpers.js
index f26e2cc..60d5b82 100644
--- a/animism-align/frontend/common/upload.helpers.js
+++ b/animism-align/frontend/common/upload.helpers.js
@@ -107,6 +107,23 @@ function getScale(width, height, viewportWidth, viewportHeight, fillViewport) {
return 1
}
+function getImageProperties(img) {
+ // img is an image
+ if ('naturalWidth' in img) {
+ const { naturalWidth, naturalHeight } = img
+ const jpeg = !!img.src.match(/data:image\/jpeg|\.jpeg$|\.jpg$/i)
+ const hasDataURI = !!img.src.match(/^data:/)
+ return { naturalWidth, naturalHeight, jpeg, hasDataURI }
+ }
+ // img is a canvas
+ return {
+ naturalWidth: img.width,
+ naturalHeight: img.height,
+ jpeg: false,
+ hasDataURI: false,
+ }
+}
+
export function renderToCanvas(img, options) {
if (!img) return null
options = options || {}
@@ -126,7 +143,7 @@ export function renderToCanvas(img, options) {
canvas.width = Math.round(img.naturalWidth * scale)
canvas.height = Math.round(img.naturalHeight * scale)
*/
- const { naturalWidth, naturalHeight } = img
+ const { naturalWidth, naturalHeight, jpeg, hasDataURI } = getImageProperties(img)
if (maxSide > 0) {
if (naturalWidth > naturalHeight) {
canvas.width = Math.min(maxSide, naturalWidth)
@@ -140,8 +157,6 @@ export function renderToCanvas(img, options) {
canvas.height = naturalHeight
}
const { correctOrientation } = options
- const jpeg = !!img.src.match(/data:image\/jpeg|\.jpeg$|\.jpg$/i)
- const hasDataURI = !!img.src.match(/^data:/)
ctx.save()
@@ -161,8 +176,11 @@ export function renderToCanvas(img, options) {
return canvas
}
-export function renderThumbnail(img) {
- const resized = renderToCanvas(img, { correctOrientation: true })
+export function renderThumbnail(img, options) {
+ const resized = renderToCanvas(img, {
+ correctOrientation: true,
+ ...options,
+ })
// const canvas = document.createElement('canvas') // document.querySelector('#user_photo_canvas')
// const ctx = canvas.getContext('2d')
// ctx.fillStyle = 'black'
diff --git a/animism-align/frontend/util/index.js b/animism-align/frontend/util/index.js
index afebe13..37369f0 100644
--- a/animism-align/frontend/util/index.js
+++ b/animism-align/frontend/util/index.js
@@ -90,20 +90,11 @@ export const sha256_tree = (sha256, branch_size=2, tree_depth=2) => {
return tree
}
-export const imageUrl = (sha256, frame, size = 'th') => [
- 'https://' + process.env.S3_HOST + '/v1/media/keyframes',
- sha256_tree(sha256),
- pad(frame, 6),
- size,
- 'index.jpg'
-].filter(s => !!s).join('/')
-
-export const uploadUri = ({ sha256, ext }) => '/static/data/uploads' + sha256_tree(sha256) + '/' + sha256 + ext
-export const metadataUri = (sha256, tag) => '/metadata/' + sha256 + '/' + tag + '/'
-export const keyframeUri = (sha256, frame) => '/metadata/' + sha256 + '/keyframe/' + pad(frame, 6) + '/'
-
-export const preloadImage = url => (
+export const preloadImage = (url, anonymous=false) => (
new Promise((resolve, reject) => {
+ if (typeof url === 'object' && url instanceof Image) {
+ return resolve(url)
+ }
const image = new Image()
let loaded = false
image.onload = () => {
@@ -120,7 +111,9 @@ export const preloadImage = url => (
resolve(image)
}
// console.log(img.src)
- // image.crossOrigin = 'anonymous'
+ if (anonymous) {
+ image.crossOrigin = 'anonymous'
+ }
image.src = url
if (image.complete) {
image.onload()
@@ -130,57 +123,50 @@ export const preloadImage = url => (
export const cropImage = (url, crop, maxSide) => {
return new Promise((resolve, reject) => {
- let { x, y, w, h } = crop
- const image = new Image()
- let loaded = false
- x = parseFloat(x)
- y = parseFloat(y)
- w = parseFloat(w)
- h = parseFloat(h)
- image.onload = () => {
- if (loaded) return
- loaded = true
- image.onload = null
+ preloadImage(url, true)
+ .then(image => {
+ let { x, y, w, h } = crop
+ x = parseFloat(x)
+ y = parseFloat(y)
+ w = parseFloat(w)
+ h = parseFloat(h)
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const { naturalWidth, naturalHeight } = image
- let height, width
+
+ let width, height
+ let cropWidth = naturalWidth * w
+ let cropHeight = naturalHeight * h
if (maxSide > 0) {
- if (naturalWidth > naturalHeight) {
- width = Math.min(maxSide, naturalWidth)
- height = naturalHeight * canvas.width / naturalWidth
+ if (cropWidth > cropHeight) {
+ width = Math.min(maxSide, cropWidth)
+ height = cropHeight * width / cropWidth
} else {
- height = Math.min(maxSide, naturalHeight)
- width = naturalWidth * canvas.height / naturalHeight
+ height = Math.min(maxSide, cropHeight)
+ width = cropWidth * height / cropHeight
}
} else {
- width = naturalWidth
- height = naturalHeight
+ width = cropWidth
+ height = cropHeight
}
canvas.width = w * width
canvas.height = h * height
+
ctx.drawImage(
image,
- Math.round(x * width),
- Math.round(y * height),
- Math.round(w * width),
- Math.round(h * height),
+ Math.round(x * naturalWidth),
+ Math.round(y * naturalHeight),
+ Math.round(w * naturalWidth),
+ Math.round(h * naturalHeight),
0, 0, canvas.width, canvas.height
)
+ // console.log(x, y, w, h)
+ // console.log(naturalWidth, naturalHeight)
+ // console.log(width, height)
resolve(canvas)
- }
- image.onerror = () => {
- console.log('image error')
- reject()
- }
- // console.log(img.src)
- image.crossOrigin = 'anonymous'
- image.src = url
- if (image.complete) {
- image.onload()
- }
+ })
})
}
export const urlSearchParamsToDict = search => {
diff --git a/animism-align/frontend/views/media/components/media.form.js b/animism-align/frontend/views/media/components/media.form.js
index c82b384..9b93788 100644
--- a/animism-align/frontend/views/media/components/media.form.js
+++ b/animism-align/frontend/views/media/components/media.form.js
@@ -79,12 +79,15 @@ export default class MediaForm extends Component {
}
handleSettingsChange(name, value) {
+ if (name !== 'multiple') {
+ value = { [name]: value }
+ }
this.setState({
data: {
...this.state.data,
settings: {
...this.state.data.settings,
- [name]: value,
+ ...value,
}
}
})
diff --git a/animism-align/frontend/views/media/components/media.formImage.js b/animism-align/frontend/views/media/components/media.formImage.js
index d86a6d8..23fd7e7 100644
--- a/animism-align/frontend/views/media/components/media.formImage.js
+++ b/animism-align/frontend/views/media/components/media.formImage.js
@@ -2,14 +2,22 @@ import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import { session } from '../../../session'
-import { capitalize } from '../../../util'
+import actions from '../../../actions'
+import { capitalize, preloadImage, cropImage } from '../../../util'
-import { TextInput, LabelDescription, FileInputField, Select, TextArea, Checkbox, SubmitButton, Loader } from '../../../common'
+import { TextInput, LabelDescription, UploadImage, Select, TextArea, Checkbox, SubmitButton, Loader } from '../../../common'
+import { renderThumbnail } from '../../../common/upload.helpers'
import ImageSelection from './media.formImageSelection'
+const DISPLAY_SIZE = 1024
+const DISPLAY_QUALITY= 80
+const THUMBNAIL_SIZE = 320
+const THUMBNAIL_QUALITY = 80
+
export default class MediaImageForm extends Component {
state = {
+ img: null,
}
constructor(props) {
@@ -18,6 +26,17 @@ export default class MediaImageForm extends Component {
this.handleChange = this.handleChange.bind(this)
this.handleSettingsChange = this.handleSettingsChange.bind(this)
this.handleUpload = this.handleUpload.bind(this)
+ this.handleCrop = this.handleCrop.bind(this)
+ this.replaceTaggedSize = this.replaceTaggedSize.bind(this)
+ this.uploadTaggedSize = this.uploadTaggedSize.bind(this)
+ }
+
+ componentDidMount() {
+ // this.setState({ })
+ if (this.props.data.settings.fullsize) {
+ preloadImage(this.props.data.settings.fullsize.url)
+ .then(img => this.setState({ img }))
+ }
}
handleChange(e) {
@@ -33,64 +52,109 @@ export default class MediaImageForm extends Component {
this.props.onSettingsChange(name, value)
}
- handleUpload(image) {
- // upload fullsize
- this.uploadFullSize(image)
- .then(res => {
- this.props.onSettingsChange('fullsize', data.res)
- setTimeout(() => {
- })
+ handleUpload({ file, img, canvas, blob }) {
+ // sizes: fullsize, display, thumbnail
+ this.replaceTaggedSize(file, 'fullsize')
+ .then(data => {
+ this.setState({ img })
+ this.props.onSettingsChange('multiple', {
+ fullsize: data,
+ crop: {},
+ })
+ return this.replaceTaggedSize(blob, 'display', file.name)
+ }).then(data => {
+ this.props.onSettingsChange('multiple', {
+ display: data,
+ })
+ this.uploadThumbnail(img)
})
}
- uploadFullSize(image) {
- actions.upload.upload({
- image,
- tag: 'fullsize',
- username: 'animism',
- }).then(data => {
- console.log(data.res)
- return data.res
- })
+ uploadThumbnail(img) {
+ const { fn } = this.props.data.settings.fullsize
+ const thumbnailCanvas = renderThumbnail(img, { maxSide: THUMBNAIL_SIZE })
+ thumbnailCanvas.toBlob(thumbnail => {
+ this.replaceTaggedSize(thumbnail, 'thumbnail', fn).then(data => {
+ this.props.onSettingsChange('multiple', {
+ thumbnail: data,
+ })
+ })
+ }, 'image/jpeg', THUMBNAIL_QUALITY)
}
-
- uploadThumbnail(image) {
- actions.upload.upload({
- image,
- tag: 'thumbnail',
- username: 'animism',
- }).then(data => {
- console.log(data.res)
- })
+
+ replaceTaggedSize(image, tag, fn) {
+ // when we upload an image, if the image already exists in this "position"
+ // on the record, we should also delete it
+ if (this.props.data.settings[tag] && this.props.data.settings[tag].id) {
+ return actions.upload.destroy(this.props.data.settings[tag])
+ .then(() => {
+ return this.uploadTaggedSize(image, tag, fn)
+ })
+ }
+ return this.uploadTaggedSize(image, tag, fn)
}
- uploadCrop(image) {
- actions.upload.upload({
+ uploadTaggedSize(image, tag, fn) {
+ const uploadData = {
image,
- tag: 'crop',
+ tag,
username: 'animism',
- }).then(data => {
- console.log(data.res)
- this.props.onSelect('url', data.res.url)
+ }
+ if (fn) {
+ uploadData['__image_filename'] = fn
+ }
+ // console.log(uploadData)
+ return actions.upload.upload(uploadData).then(data => {
+ // console.log(data)
+ return data.res
})
}
+ handleCrop(crop) {
+ // when cropping an image, re-upload the display image and thumbnail
+ // console.log(crop)
+ cropImage(this.state.img, crop, DISPLAY_SIZE)
+ .then(canvas => {
+ canvas.toBlob(blob => {
+ // console.log(canvas, canvas.width, canvas.height, blob)
+ this.replaceTaggedSize(blob, 'display', this.props.data.settings.fullsize.fn)
+ .then(data => {
+ this.props.onSettingsChange('multiple', {
+ crop,
+ display: data,
+ })
+ this.uploadThumbnail(canvas)
+ })
+ }, 'image/jpeg', DISPLAY_QUALITY)
+ })
+ }
+
render() {
const { data } = this.props
- console.log(data)
+ // console.log(data)
return (
<div className='imageForm'>
{!data.url &&
- <FileInputField
- title='Upload image'
- onChange={this.handleUpload}
- />
+ <label className={'text fileInput'}>
+ <span>{"Upload image"}</span>
+ <div className="row">
+ <button>
+ {"Choose image"}
+ </button>
+ <UploadImage
+ onUpload={this.handleUpload}
+ maxSide={DISPLAY_SIZE}
+ quality={DISPLAY_QUALITY}
+ />
+ </div>
+ </label>
}
{data.settings.fullsize &&
<div>
<ImageSelection
url={data.settings.fullsize.url}
crop={data.settings.crop}
+ onCrop={this.handleCrop}
/>
</div>
}
diff --git a/animism-align/frontend/views/media/media.css b/animism-align/frontend/views/media/media.css
index 251afd6..e6e6f5d 100644
--- a/animism-align/frontend/views/media/media.css
+++ b/animism-align/frontend/views/media/media.css
@@ -24,6 +24,9 @@
.imageForm {
background: #315;
}
+.imageForm .fileInput .row {
+ position: relative;
+}
/* video form */
diff --git a/animism-align/frontend/views/paragraph/components/paragraph.types.js b/animism-align/frontend/views/paragraph/components/paragraph.types.js
index 6fd8558..fe8158a 100644
--- a/animism-align/frontend/views/paragraph/components/paragraph.types.js
+++ b/animism-align/frontend/views/paragraph/components/paragraph.types.js
@@ -44,7 +44,6 @@ export const MediaVideo = ({ paragraph, media, selectedParagraph, selectedAnnota
const annotation = paragraph.annotations[0]
const item = media.lookup[annotation.settings.media_id]
if (!item) return <div>Media not found: {annotation.settings.media_id}</div>
- console.log(item)
return (
<div
className={className}