tre-images
Version:
tre-images ---
297 lines (283 loc) • 7.66 kB
JavaScript
const pull = require('pull-stream')
const detect = require('tre-image-size')
const debug = require('debug')('tre-image:common')
const FileSource = require('tre-file-importer/file-source')
const JpegMarkerStream = require('jpeg-marker-stream')
const BufferList = require('bl')
module.exports = {
factory,
importFiles,
parseFile
}
function factory(config) {
const type = 'image'
return {
type,
i18n: {
'en': 'Image'
},
prototype: function() {
return {
type,
width: 0,
height: 0,
schema: {
description: 'An image with meta data',
type: 'object',
required: ['type', 'width', 'height'],
properties: {
type: {
"const": type
},
name: { type: 'string' },
width: { type: 'number' },
height: { type: 'number' },
extractedMeta: {
type: 'object',
properties: {
image: {
type: 'object',
properties: {
Make: { type: 'string' },
Model: { type: 'string' },
XResolution: { type: 'number' },
YResolution: { type: 'number' },
Orientation: { type: 'number' }
}
},
exif: {
type: 'object',
properties: {
ExposureTime: { type: 'number' },
FNumber: { type: 'number' },
ISO: { type: 'number' },
LensModel: { type: 'string' },
BodySerialNumber: { type: 'string' },
FocalLength: { type: 'number' }
//ExifVersion: <Buffer 30 32 31 30>,
//DateTimeOriginal: 2001-10-02T14:57:31.000Z,
//DateTimeDigitized: 2001-10-02T14:57:31.000Z,
}
}
}
}
}
}
}
},
content: function() {
return {
type,
prototype: config.tre.prototypes[type]
}
}
}
}
function importFiles(ssb, files, opts, cb) {
opts = opts || {}
const {prototypes} = opts
const prototype = prototypes && prototypes.image
if (!prototype) return cb(new Error('no image prototype'))
if (files.length>1) {
debug('mult-file import is not supported')
return cb(true) // we don't do multiple files
}
const file = files[0]
const fileProps = getFileProps(file)
getMeta(file.source(), (err, meta) => {
if (err || !meta) {
if (err) debug('Error getting image meta data: %s', err.message)
return cb(true)
}
debug('It is an image!: %O', meta)
fileProps.type = `image/${meta.format}`
let extracted
//if (!/^image\//.test(file.type)) return cb(true)
const parser = parseFile(file, {
meta,
onExif: e => extracted = e
})
const isSVG = meta.format.toLowerCase().includes('svg')
let bl = null
if (isSVG) {
debug('Image is SVG, will inline if <4k')
bl = BufferList()
}
pull(
parser,
pull.through(buff=>{
if (bl) {
bl.append(buff)
if (bl.length > 4*1024) {
bl = null
debug('SVG is >4k')
}
}
}),
ssb.blobs.add( (err, hash) => {
parser.end()
if (err) return cb(err)
const name = titleize(file.name)
extractThumbnail(ssb, file, extracted, (err, thumbnail) => {
if (err) console.warn('Problem extracting thumbnail', err.message)
debug('Extracted thumbnail %o', thumbnail)
const content = {
type: 'image',
prototype,
name,
file: fileProps,
width: meta && meta.width,
height: meta && meta.height,
format: meta && meta.format,
extractedMeta: extracted,
blob: hash
}
if (bl) {
debug('SVG is inlined (stored in message JSON')
content.svg = bl.toString()
}
if (thumbnail) {
content.thumbnail = {
blob: thumbnail.blob,
width: thumbnail.meta.width,
height: thumbnail.meta.height,
format: thumbnail.meta.format
}
}
return cb(null, content)
})
})
)
})
}
function parseFile(file, opts) {
const {onExif, onMeta, forceExifParsing} = opts
const imagesize = onMeta && detect(onMeta)
let jpegParser
let meta = opts.meta
let exif
if ( (forceExifParsing || file.type == 'image/jpeg' || (meta && meta.format == 'jpeg')) && onExif) {
jpegParser = JpegParser((err, data) => {
exif = data
if (onExif) onExif(exif)
})
}
const result = pull(
file.source(),
imagesize || pull.through(),
pull.through( b => {
if (jpegParser) jpegParser(b)
})
)
result.end = function() {
if (jpegParser) jpegParser.end()
}
return result
}
function parseMeta(cb) {
const passThrough = Boolean(cb)
let meta, first = true
const parser = detect(_meta => {
meta = _meta
debug('meta %o', meta)
})
return pull(
parser,
pull.asyncMap( (b, _cb) => {
if (meta) {
if (first) {
first = false
if (passThrough) {
cb(null, meta)
_cb(null, b)
} else {
_cb(null, meta)
}
return
}
if (passThrough) {
_cb(null, b)
} else {
_cb(true)
}
return
}
return passThrough ? _cb(null, b) : _cb(null, null)
}),
pull.filter()
)
}
function getMeta(source, cb) {
pull(source, parseMeta(), pull.collect( (err, result) => {
cb(err, result && result[0])
}))
}
function JpegParser(cb) {
let done = false
const parser = JpegMarkerStream()
parser.on('error', err => {
console.error('Jpeg parse error', err.message)
if (done) return
done = true
cb(err)
})
parser.on('data', data => {
if (done) return
debug('jpeg %O', data)
if (data.type == 'EXIF') {
const exif = data
if (exif && exif.exif && exif.exif.MakerNote) {
delete exif.exif.MakerNote
}
cb(null, exif)
done = true
parser.end()
}
})
function write(buffer) {
if (done) return
parser.write(Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer))
}
write.end = function() {
parser.end()
}
return write
}
function extractThumbnail(ssb, file, exif, cb) {
const thumbnail = exif && exif.thumbnail
//console.log('thumbnail', thumbnail)
if (!thumbnail) return cb(null, null)
const {ThumbnailOffset, ThumbnailLength} = thumbnail
if (!ThumbnailOffset || !ThumbnailLength) return cb(
new Error('Missing property in exif.image.thumbnail')
)
const source = file.source({
start: 12 + ThumbnailOffset,
end: 12 + ThumbnailOffset + ThumbnailLength
})
let meta = {}
pull(
source,
parseMeta((err, _meta) => {
if (err) console.warn('Problem parsing meta data from thumbnail', err.message)
meta = _meta
}),
ssb.blobs.add((err, blob)=>{
if (err) return cb(err)
cb(null, {blob, meta})
})
)
}
function titleize(filename) {
return filename.replace(/\.\w{3,4}$/, '').replace(/-/g, ' ')
}
// -- utils
function getFileProps(file) {
// Object.assign does not work with file objects
return {
lastModified: file.lastModified,
name: file.name,
size: file.size,
type: file.type,
}
}