Initial commit

This commit is contained in:
John Lyon-Smith
2018-02-22 17:57:27 -08:00
commit e80f5490d5
196 changed files with 38982 additions and 0 deletions

View 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
View 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)
})