Add image-upload
This commit is contained in:
parent
a20ac8cbbc
commit
7de6157129
3
package-lock.json
generated
3
package-lock.json
generated
@ -1519,6 +1519,9 @@
|
|||||||
"@rondo/common": {
|
"@rondo/common": {
|
||||||
"version": "file:packages/common"
|
"version": "file:packages/common"
|
||||||
},
|
},
|
||||||
|
"@rondo/image-upload": {
|
||||||
|
"version": "file:packages/image-upload"
|
||||||
|
},
|
||||||
"@rondo/server": {
|
"@rondo/server": {
|
||||||
"version": "file:packages/server",
|
"version": "file:packages/server",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|||||||
@ -5,7 +5,8 @@
|
|||||||
"@rondo/server": "file:packages/server",
|
"@rondo/server": "file:packages/server",
|
||||||
"@rondo/comments-server": "file:packages/comments-server",
|
"@rondo/comments-server": "file:packages/comments-server",
|
||||||
"@rondo/comments-common": "file:packages/comments-common",
|
"@rondo/comments-common": "file:packages/comments-common",
|
||||||
"@rondo/comments-client": "file:packages/comments-client"
|
"@rondo/comments-client": "file:packages/comments-client",
|
||||||
|
"@rondo/image-upload": "file:packages/image-upload"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcrypt": "^3.0.0",
|
"@types/bcrypt": "^3.0.0",
|
||||||
|
|||||||
8
packages/image-upload/Buildfile
Normal file
8
packages/image-upload/Buildfile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
build:
|
||||||
|
tsc
|
||||||
|
|
||||||
|
esm:
|
||||||
|
tsc -p tsconfig.esm.json
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf lib/
|
||||||
16
packages/image-upload/jest.config.js
Normal file
16
packages/image-upload/jest.config.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
roots: [
|
||||||
|
'<rootDir>/src'
|
||||||
|
],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.tsx?$': 'ts-jest'
|
||||||
|
},
|
||||||
|
testRegex: '(/__tests__/.*|\\.(test|spec))\\.tsx?$',
|
||||||
|
moduleFileExtensions: [
|
||||||
|
'ts',
|
||||||
|
'tsx',
|
||||||
|
'js',
|
||||||
|
'jsx'
|
||||||
|
],
|
||||||
|
setupFiles: ['<rootDir>/jest.setup.js']
|
||||||
|
}
|
||||||
4
packages/image-upload/jest.setup.js
Normal file
4
packages/image-upload/jest.setup.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
if (!process.env.LOG) {
|
||||||
|
process.env.LOG = 'sql:warn'
|
||||||
|
}
|
||||||
|
process.chdir(__dirname)
|
||||||
11
packages/image-upload/package.json
Normal file
11
packages/image-upload/package.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@rondo/image-upload",
|
||||||
|
"private": true,
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.7.0",
|
||||||
|
"react-dom": "^16.7.0"
|
||||||
|
},
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
|
"module": "esm/index.js"
|
||||||
|
}
|
||||||
45
packages/image-upload/src/ImageUpload.tsx
Normal file
45
packages/image-upload/src/ImageUpload.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export interface IImage {
|
||||||
|
base64: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IImageUploadProps {
|
||||||
|
onChange: (images: IImage[]) => void
|
||||||
|
multiple: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImageUpload extends React.PureComponent<IImageUploadProps> {
|
||||||
|
fileInput: React.RefObject<HTMLInputElement>
|
||||||
|
constructor(props: IImageUploadProps) {
|
||||||
|
super(props)
|
||||||
|
this.fileInput = React.createRef()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||||
|
const files = this.fileInput.current!.files!
|
||||||
|
|
||||||
|
Array.prototype.forEach.call(files, (file: File) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.addEventListener('load', function() {
|
||||||
|
const {result} = this
|
||||||
|
console.log(file.name, result)
|
||||||
|
// TODO resize image...
|
||||||
|
})
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className='image-upload'>
|
||||||
|
<input
|
||||||
|
multiple={this.props.multiple}
|
||||||
|
type='file'
|
||||||
|
accept='image/*'
|
||||||
|
ref={this.fileInput}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
208
packages/image-upload/src/Resizer.ts
Normal file
208
packages/image-upload/src/Resizer.ts
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
interface IPartition {
|
||||||
|
source: ImageData
|
||||||
|
target: ImageData
|
||||||
|
startY: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWorkerParam {
|
||||||
|
sourceWidth: number
|
||||||
|
sourceHeight: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
core: number
|
||||||
|
source: ArrayBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWorkerParamMessage extends MessageEvent {
|
||||||
|
data: IWorkerParam
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWorkerResult {
|
||||||
|
core: number
|
||||||
|
target: Uint8ClampedArray
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWorkerResultMessage extends MessageEvent {
|
||||||
|
data: IWorkerResult
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResizeWorker() {
|
||||||
|
function onmessage(event: IWorkerParamMessage) {
|
||||||
|
const core = event.data.core
|
||||||
|
const sourceWidth = event.data.sourceWidth
|
||||||
|
const sourceHeight = event.data.sourceHeight
|
||||||
|
const width = event.data.width
|
||||||
|
const height = event.data.height
|
||||||
|
|
||||||
|
const ratioW = sourceWidth / width
|
||||||
|
const ratioH = sourceHeight / height
|
||||||
|
const ratioHalfW = Math.ceil(ratioW / 2)
|
||||||
|
const ratioHalfH = Math.ceil(ratioH / 2)
|
||||||
|
|
||||||
|
const source = new Uint8ClampedArray(event.data.source)
|
||||||
|
const sourceH = source.length / sourceWidth / 4
|
||||||
|
const targetSize = width * height * 4
|
||||||
|
const targetMemory = new ArrayBuffer(targetSize)
|
||||||
|
const target = new Uint8ClampedArray(targetMemory, 0, targetSize)
|
||||||
|
// calculate
|
||||||
|
for (let j = 0; j < height; j++) {
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
const x2 = (i + j * width) * 4
|
||||||
|
let weight = 0
|
||||||
|
let weights = 0
|
||||||
|
let weightsAlpha = 0
|
||||||
|
let gxR = 0
|
||||||
|
let gxG = 0
|
||||||
|
let gxB = 0
|
||||||
|
let gxA = 0
|
||||||
|
const centerY = j * ratioH
|
||||||
|
|
||||||
|
const xxStart = Math.floor(i * ratioW)
|
||||||
|
let xxStop = Math.ceil((i + 1) * ratioW)
|
||||||
|
const yyStart = Math.floor(j * ratioH)
|
||||||
|
let yyStop = Math.ceil((j + 1) * ratioH)
|
||||||
|
|
||||||
|
xxStop = Math.min(xxStop, sourceWidth)
|
||||||
|
yyStop = Math.min(yyStop, sourceHeight)
|
||||||
|
|
||||||
|
for (let yy = yyStart; yy < yyStop; yy++) {
|
||||||
|
const dy = Math.abs(centerY - yy) / ratioHalfH
|
||||||
|
const centerX = i * ratioW
|
||||||
|
const w0 = dy * dy // pre-calc part of w
|
||||||
|
for (let xx = xxStart; xx < xxStop; xx++) {
|
||||||
|
const dx = Math.abs(centerX - xx) / ratioHalfW
|
||||||
|
const w = Math.sqrt(w0 + dx * dx)
|
||||||
|
if (w >= 1) {
|
||||||
|
// pixel too far
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// hermite filter
|
||||||
|
weight = 2 * w * w * w - 3 * w * w + 1
|
||||||
|
// calc source pixel location
|
||||||
|
const posX = 4 * (xx + yy * sourceWidth)
|
||||||
|
// alpha
|
||||||
|
gxA += weight * source[posX + 3]
|
||||||
|
weightsAlpha += weight
|
||||||
|
// colors
|
||||||
|
if (source[posX + 3] < 255) {
|
||||||
|
weight = weight * source[posX + 3] / 250
|
||||||
|
}
|
||||||
|
gxR += weight * source[posX]
|
||||||
|
gxG += weight * source[posX + 1]
|
||||||
|
gxB += weight * source[posX + 2]
|
||||||
|
weights += weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target[x2] = gxR / weights
|
||||||
|
target[x2 + 1] = gxG / weights
|
||||||
|
target[x2 + 2] = gxB / weights
|
||||||
|
target[x2 + 3] = gxA / weightsAlpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const objData: IWorkerResult = {
|
||||||
|
core,
|
||||||
|
target,
|
||||||
|
}
|
||||||
|
postMessage(objData, [target.buffer] as any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Resizer {
|
||||||
|
readonly cores = navigator.hardwareConcurrency || 4
|
||||||
|
readonly workerBlobURL = window.URL.createObjectURL(
|
||||||
|
new Blob(
|
||||||
|
['(', ResizeWorker.toString(), ')()'],
|
||||||
|
{type: 'application/javascript'},
|
||||||
|
))
|
||||||
|
|
||||||
|
async resample(canvas: HTMLCanvasElement, width: number, height: number) {
|
||||||
|
const {cores} = this
|
||||||
|
const sourceWidth = canvas.width
|
||||||
|
const sourceHeight = canvas.height
|
||||||
|
width = Math.round(width)
|
||||||
|
height = Math.round(height)
|
||||||
|
const ratioH = sourceHeight / height
|
||||||
|
|
||||||
|
const workers = new Array(cores)
|
||||||
|
// TODO handle null
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
|
|
||||||
|
// prepare source and target data for workers
|
||||||
|
const partitions: IPartition[] = new Array(cores)
|
||||||
|
const blockHeight = Math.ceil(sourceHeight / cores / 2) * 2
|
||||||
|
let endY = -1
|
||||||
|
for (let c = 0; c < cores; c++) {
|
||||||
|
// source
|
||||||
|
const offsetY = endY + 1
|
||||||
|
if (offsetY >= sourceHeight) {
|
||||||
|
// size too small, nothing left for this core
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
endY = offsetY + blockHeight - 1
|
||||||
|
endY = Math.min(endY, sourceHeight - 1)
|
||||||
|
|
||||||
|
let currentBlockHeight = blockHeight
|
||||||
|
currentBlockHeight = Math.min(blockHeight, sourceHeight - offsetY)
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// 'source split: ', '#'+c, offsetY, endY, 'height: '+currentBlockHeight);
|
||||||
|
|
||||||
|
partitions[c] = {
|
||||||
|
source: ctx.getImageData(0, offsetY, sourceWidth, blockHeight),
|
||||||
|
target: ctx.createImageData(
|
||||||
|
width, Math.ceil(currentBlockHeight / ratioH)),
|
||||||
|
startY: Math.ceil(offsetY / ratioH),
|
||||||
|
height: currentBlockHeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, sourceWidth, sourceHeight)
|
||||||
|
|
||||||
|
let resolve: () => void
|
||||||
|
const promise = new Promise(r => resolve = r)
|
||||||
|
|
||||||
|
// start
|
||||||
|
let activeWorkers = 0
|
||||||
|
for (let c = 0; c < cores; c++) {
|
||||||
|
if (partitions[c] === undefined) {
|
||||||
|
// no job for this worker
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
activeWorkers++
|
||||||
|
const worker = new Worker(this.workerBlobURL)
|
||||||
|
workers[c] = worker
|
||||||
|
|
||||||
|
worker.onmessage = (event: IWorkerResultMessage) => {
|
||||||
|
activeWorkers--
|
||||||
|
const core = event.data.core
|
||||||
|
workers[core].terminate()
|
||||||
|
delete workers[core]
|
||||||
|
|
||||||
|
// draw
|
||||||
|
// const height_part = Math.ceil(partitions[core].height / ratioH)
|
||||||
|
// partitions[core].target = ctx.createImageData(width, height_part)
|
||||||
|
partitions[core].target.data.set(event.data.target)
|
||||||
|
ctx.putImageData(partitions[core].target, 0, partitions[core].startY)
|
||||||
|
|
||||||
|
if (activeWorkers <= 0) {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const message: IWorkerParam = {
|
||||||
|
sourceWidth,
|
||||||
|
sourceHeight: partitions[c].height,
|
||||||
|
width,
|
||||||
|
height: Math.ceil(partitions[c].height / ratioH),
|
||||||
|
core: c,
|
||||||
|
source: partitions[c].source.data.buffer,
|
||||||
|
}
|
||||||
|
worker.postMessage(message, [message.source])
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/image-upload/src/index.ts
Normal file
1
packages/image-upload/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './ImageUpload'
|
||||||
7
packages/image-upload/tsconfig.esm.json
Normal file
7
packages/image-upload/tsconfig.esm.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "esm"
|
||||||
|
},
|
||||||
|
"references": []
|
||||||
|
}
|
||||||
8
packages/image-upload/tsconfig.json
Normal file
8
packages/image-upload/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.common.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "lib",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"references": []
|
||||||
|
}
|
||||||
5
packages/image-upload/tslint.json
Normal file
5
packages/image-upload/tslint.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"../tslint.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user