Initial commit
This commit is contained in:
298
server/src/image/ImageHandlers.js
Normal file
298
server/src/image/ImageHandlers.js
Normal file
@@ -0,0 +1,298 @@
|
||||
import Canvas from 'canvas'
|
||||
import fs from 'fs'
|
||||
import util from 'util'
|
||||
import createError from 'http-errors'
|
||||
import autoBind from 'auto-bind2'
|
||||
import stream from 'stream'
|
||||
|
||||
function streamToBuffer(readable) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var chunks = []
|
||||
var writeable = new stream.Writable()
|
||||
|
||||
writeable._write = function (chunk, enc, done) {
|
||||
chunks.push(chunk)
|
||||
done()
|
||||
}
|
||||
|
||||
readable.on('end', function () {
|
||||
resolve(Buffer.concat(chunks))
|
||||
})
|
||||
|
||||
readable.on('error', (err) => {
|
||||
reject(err)
|
||||
})
|
||||
|
||||
readable.pipe(writeable);
|
||||
})
|
||||
}
|
||||
|
||||
function pipeToGridFS(readable, gfsWriteable) {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
readable.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
gfsWriteable.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
gfsWriteable.on('close', (file) => {
|
||||
resolve(file)
|
||||
})
|
||||
})
|
||||
readable.pipe(gfsWriteable)
|
||||
return promise
|
||||
}
|
||||
|
||||
function loadImage(buf) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Canvas.Image()
|
||||
|
||||
function cleanup () {
|
||||
image.onload = null
|
||||
image.onerror = null
|
||||
}
|
||||
|
||||
image.onload = () => {
|
||||
cleanup();
|
||||
resolve(image)
|
||||
}
|
||||
image.onerror = () => {
|
||||
cleanup();
|
||||
reject(new Error(`Failed to load the image "${buf}"`))
|
||||
}
|
||||
|
||||
image.src = buf
|
||||
})
|
||||
}
|
||||
|
||||
// Derived from https://stackoverflow.com/questions/20600800/js-client-side-exif-orientation-rotate-and-mirror-jpeg-images
|
||||
function getExifOrientation(buf) {
|
||||
if (buf.length < 2 || buf.readUInt16BE(0) != 0xFFD8) {
|
||||
return -2
|
||||
}
|
||||
|
||||
let length = buf.byteLength
|
||||
let offset = 2
|
||||
|
||||
while (offset < length) {
|
||||
let marker = buf.readUInt16BE(offset)
|
||||
offset += 2
|
||||
|
||||
if (marker == 0xFFE1) {
|
||||
if (buf.readUInt32BE(offset += 2) != 0x45786966) {
|
||||
return -1
|
||||
}
|
||||
let little = (buf.readUInt16BE(offset += 6) == 0x4949)
|
||||
offset += (little ? buf.readUInt32LE(offset + 4) : buf.readUInt32BE(offset + 4))
|
||||
|
||||
const numTags = (little ? buf.readUInt16LE(offset) : buf.readUInt16BE(offset))
|
||||
|
||||
offset += 2
|
||||
for (let i = 0; i < numTags; i++) {
|
||||
let val = (little ? buf.readUInt16LE(offset + (i * 12)) : buf.readUInt16BE(offset + (i * 12)))
|
||||
|
||||
if (val === 0x0112) {
|
||||
return (little ? buf.readUInt16LE(offset + (i * 12) + 8) : buf.readUInt16BE(offset + (i * 12) + 8))
|
||||
}
|
||||
}
|
||||
} else if ((marker & 0xFF00) != 0xFF00) {
|
||||
break
|
||||
} else {
|
||||
offset += buf.readUInt16BE(offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOrientation(image, orientation) {
|
||||
let width = image.width
|
||||
let height = image.height
|
||||
let canvas = new Canvas(width, height)
|
||||
let ctx = canvas.getContext("2d")
|
||||
|
||||
if (4 < orientation && orientation < 9) {
|
||||
canvas.width = height
|
||||
canvas.height = width
|
||||
} else {
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
}
|
||||
|
||||
switch (orientation) {
|
||||
case 2: ctx.transform(-1, 0, 0, 1, width, 0); break
|
||||
case 3: ctx.transform(-1, 0, 0, -1, width, height ); break
|
||||
case 4: ctx.transform(1, 0, 0, -1, 0, height ); break
|
||||
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break
|
||||
case 6: ctx.transform(0, 1, -1, 0, height , 0); break
|
||||
case 7: ctx.transform(0, -1, -1, 0, height , width); break
|
||||
case 8: ctx.transform(0, -1, 1, 0, 0, width); break
|
||||
default: return Promise.resolve(image)
|
||||
}
|
||||
|
||||
ctx.drawImage(image, 0, 0)
|
||||
return loadImage(canvas.toBuffer())
|
||||
}
|
||||
|
||||
export class ImageHandlers {
|
||||
constructor(container) {
|
||||
this.db = container.db
|
||||
this.log = container.log
|
||||
autoBind(this)
|
||||
}
|
||||
|
||||
scaleImage(options) {
|
||||
const {
|
||||
newWidth = 400,
|
||||
newHeight = 100,
|
||||
scaleMode = 'scaleToFill',
|
||||
inputFile,
|
||||
inputAssetId,
|
||||
outputFile
|
||||
} = options
|
||||
|
||||
if (!inputFile && !inputAssetId) {
|
||||
return Promise.reject(createError.BadRequest(`No inputAssetId or inputFile given`))
|
||||
}
|
||||
|
||||
let canvas = new Canvas(newWidth, newHeight)
|
||||
let ctx = canvas.getContext("2d")
|
||||
|
||||
ctx.imageSmoothingEnabled = true
|
||||
|
||||
let loadPromise = null
|
||||
|
||||
if (inputFile) {
|
||||
loadPromise = util.promisify(fs.readFile)(inputFile)
|
||||
} else {
|
||||
loadPromise = streamToBuffer(this.db.gridfs.createReadStream({ _id: inputAssetId }))
|
||||
}
|
||||
|
||||
let orientation
|
||||
|
||||
return loadPromise.then((buf) => {
|
||||
orientation = getExifOrientation(buf)
|
||||
return loadImage(buf)
|
||||
}).then((img) => {
|
||||
return normalizeOrientation(img, orientation)
|
||||
}).then((img) => {
|
||||
let x = 0
|
||||
let y = 0
|
||||
let scale = 1
|
||||
|
||||
switch (scaleMode) {
|
||||
case 'aspectFill':
|
||||
if (img.width - newWidth > img.height - newHeight) {
|
||||
scale = newHeight / img.height
|
||||
x = -(img.width * scale - newWidth) / 2
|
||||
} else {
|
||||
scale = newWidth / img.width
|
||||
y = -(img.height * scale - newHeight) / 2
|
||||
}
|
||||
ctx.drawImage(img, x, y, img.width * scale, img.height * scale)
|
||||
break
|
||||
case 'aspectFit':
|
||||
if (img.width - newWidth > img.height - newHeight) {
|
||||
scale = newWidth / img.width
|
||||
y = (newHeight - img.height * scale) / 2
|
||||
} else {
|
||||
scale = newHeight / img.height
|
||||
x = (newWidth - img.width * scale) / 2
|
||||
}
|
||||
ctx.drawImage(img, x, y, img.width * scale, img.height * scale)
|
||||
break
|
||||
case 'scaleToFill':
|
||||
default:
|
||||
ctx.drawImage(img, 0, 0, newWidth, newHeight)
|
||||
break
|
||||
}
|
||||
|
||||
let savePromise = null
|
||||
let readable = canvas.createPNGStream()
|
||||
let writeable = null
|
||||
|
||||
if (outputFile) {
|
||||
writeable = fs.createWriteStream(outputFile)
|
||||
} else {
|
||||
const _id = this.db.newObjectId()
|
||||
writeable = this.db.gridfs.createWriteStream({
|
||||
_id,
|
||||
filename: _id + '.png',
|
||||
content_type: 'image/png',
|
||||
metadata: {
|
||||
scaledFrom: this.db.newObjectId(inputAssetId),
|
||||
width: newWidth,
|
||||
height: newHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return pipeToGridFS(readable, writeable).then((file) => {
|
||||
let res = {}
|
||||
if (outputFile) {
|
||||
res.outputFile = outputFile
|
||||
} else if (file) {
|
||||
res.outputAssetId = file._id
|
||||
}
|
||||
return Promise.resolve(res)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
createPlaceholder(options) {
|
||||
const {
|
||||
width = 400,
|
||||
height = 100,
|
||||
fontSize = 36,
|
||||
fontName = 'helvetica neue, arial black, sans serif',
|
||||
fontWeight = 'bold',
|
||||
background = '#1B1B1B',
|
||||
foreground = '#333333',
|
||||
outputFile
|
||||
} = options
|
||||
|
||||
let canvas = new Canvas(width, height)
|
||||
let ctx = canvas.getContext("2d")
|
||||
const text = `${width}x${height}`
|
||||
|
||||
ctx.fillStyle = background
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
ctx.font = `${fontWeight} ${fontSize}px ${fontName}`
|
||||
ctx.fillStyle = foreground
|
||||
const tm = ctx.measureText(text)
|
||||
|
||||
if (tm.width <= width / 2 && fontSize <= height / 2) {
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, height / 2, width)
|
||||
}
|
||||
|
||||
let promise = null
|
||||
let readable = canvas.createPNGStream()
|
||||
let writeable = null
|
||||
|
||||
if (outputFile) {
|
||||
writeable = fs.createWriteStream(outputFile)
|
||||
} else {
|
||||
const _id = this.db.newObjectId()
|
||||
writeable = this.db.gridfs.createWriteStream({
|
||||
_id,
|
||||
filename: `${width}x${height}.png`,
|
||||
content_type: 'image/png',
|
||||
metadata: {
|
||||
width: width,
|
||||
height: height,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return pipeToGridFS(readable, writeable).then((file) => {
|
||||
let res = {}
|
||||
if (outputFile) {
|
||||
res.outputFile = outputFile
|
||||
} else if (file) {
|
||||
res.outputAssetId = file._id
|
||||
}
|
||||
return Promise.resolve(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
45
server/src/image/index.js
Normal file
45
server/src/image/index.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import config from 'config'
|
||||
import pino from 'pino'
|
||||
import * as pinoExpress from 'pino-pretty-express'
|
||||
import { DB } from '../database'
|
||||
import { MS } from '../message-service'
|
||||
import { ImageHandlers } from './ImageHandlers'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
const serviceName = 'dar-image'
|
||||
const isProduction = (process.env.NODE_ENV == 'production')
|
||||
let log = null
|
||||
|
||||
if (isProduction) {
|
||||
log = pino( { name: serviceName },
|
||||
fs.createWriteStream(path.join(config.get('logDir'), serviceName + '.log'))
|
||||
)
|
||||
} else {
|
||||
const pretty = pinoExpress.pretty({})
|
||||
pretty.pipe(process.stdout)
|
||||
log = pino({ name: serviceName }, pretty)
|
||||
}
|
||||
|
||||
const ms = new MS(serviceName, { durable: false }, log)
|
||||
const db = new DB()
|
||||
let container = { db, ms, log }
|
||||
const mongoUri = config.get('uri.mongo')
|
||||
const amqpUri = config.get('uri.amqp')
|
||||
|
||||
Promise.all([
|
||||
db.connect(mongoUri),
|
||||
ms.connect(amqpUri)
|
||||
]).then(() => {
|
||||
log.info(`Connected to MongoDB at ${mongoUri}`)
|
||||
log.info(`Connected to RabbitMQ at ${amqpUri}`)
|
||||
|
||||
container = {
|
||||
...container,
|
||||
handlers: new ImageHandlers(container)
|
||||
}
|
||||
|
||||
ms.listen(container.handlers)
|
||||
}).catch((err) => {
|
||||
log.error(isProduction ? err.message : err)
|
||||
})
|
||||
Reference in New Issue
Block a user