jpeg-marker-stream
Version:
parse markers from a JPEG, including EXIF data
258 lines (254 loc) • 7.17 kB
JavaScript
var through = require('through2')
var parseExif = require('exif-reader')
module.exports = function () {
var offset = 0
var pending = 0
var buffers = []
var pos = 0
var s1 = 0, s2 = 0
var state = 'ff'
var started = false
var dqtSeq = 0
var width = -1, height = -1
return through.obj(write, end)
function write (buf, enc, next) {
var j = 0
for (var i = 0; i < buf.length; i++) {
var b = buf[i]
if (state === 'data') {
if (b === 0xff) {
buffers.push(buf.slice(j, i))
state = flushMarker.call(this, state, buffers)
buffers = []
j = i
state = 'code'
}
pos++
continue
}
if (pending > 0) {
var n = Math.min(buf.length - i, pending)
buffers.push(buf.slice(i, i+n))
pending -= n
if (pending === 0) {
state = flushMarker.call(this, state, buffers)
if (state === 'data') j = i
buffers = []
}
i += n - 1
pos += n
continue
}
if (state === 'ff' && b !== 0xff) {
return next(new Error('expected 0xff, received: ' + hexb(b)))
} else if (state === 'ff') {
state = 'code'
} else if (state === 'code') {
offset = 0
if (b === 0x00) { // data
state = 'data'
j = i + 1
} else if (b === 0xd8) { // SOI
started = true
state = 'ff'
this.push({ type: 'SOI', start: pos-1, end: pos+1 })
} else if (b === 0xe0) { // JF{IF,XX}-APP0
state = 'app0'
} else if (b === 0xda) { // SOS
state = 'sos'
} else if (b === 0xd9) { // EOI
state = 'eoi'
} else if (b === 0xe1) { // APP1
state = 'app1'
} else if (b === 0xe2) { // APP2
state = 'app2'
} else if (b === 0xdb) { // DQT
state = 'dqt'
} else if (b === 0xc4) { // DHT
state = 'dht'
} else if (b === 0xdd) { // DRI
state = 'dri'
} else if (b === 0xc0) { // SOF
state = 'sof'
} else if (b === 0xda) { // SOS
state = 'sos'
} else if (b === 0xfe) { // ???
state = '0xfe'
} else {
return next(new Error('unknown code: ' + hexb(b)))
}
} else if (state === 'app0') {
if (offset === 0) s1 = b
else if (offset === 1) s2 = b
else if (offset === 2 && b !== 0x4a) {
return next(new Error('in app0 expected 0x4a, received: ' + hexb(b)))
} else if (offset === 3 && b !== 0x46) {
return next(new Error('in app0 expected 0x46, received: ' + hexb(b)))
} else if (offset === 4 && b === 0x49) {
state = 'jfif-app0'
offset = -1
} else if (offset === 4 && b === 0x58) {
state = 'jfxx-app0'
offset = -1
} else if (offset >= 4) {
return next(new Error(
'in app0 expected 0x49 or 0x58, received: ' + hexb(b)))
}
offset++
} else if (state === 'jfif-app0') {
if (++offset === 2) {
pending = s1*256 + s2 - 7
}
} else {
if (offset === 0) s1 = b
else if (offset === 1) s2 = b
if (++offset === 2) {
pending = s1*256 + s2 - 2
}
}
pos++
}
if (state === 'data') {
buffers.push(buf.slice(j, i))
}
if (pos > 2 && !started) {
return next(new Error('start of image not found'))
} else next()
}
function end (next) {
flushMarker.call(this, state, buffers)
next()
}
function flushMarker (state, buffers) {
var buf = buffers.length === 1 ? buffers[0] : Buffer.concat(buffers)
if (state === 'data') {
this.push({
type: 'DATA',
start: pos - 2,
end: pos + buf.length,
data: buf
})
} else if (state === 'jfif-app0') {
var units = 'unknown'
if (buf[2] === 0) units = 'aspect'
else if (buf[2] === 1) units = 'pixels per inch'
else if (buf[2] === 2) units = 'pixels per cm'
this.push({
type: 'JFIF',
start: pos - 9,
end: pos + buf.length,
version: buf[0] + '.' + buf[1], // major.minor
density: {
units: units,
x: buf.readUInt16BE(3),
y: buf.readUInt16BE(5)
},
thumbnail: {
width: buf[7],
height: buf[8],
data: buf.slice(9, 9+3*buf[7]*buf[8])
}
})
} else if (state === 'jfxx-app0') {
var thumb = {
format: 'unknown',
width: 0,
height: 0,
data: null
}
if (buf[0] === 0x10) {
thumb.format = 'JPEG'
thumb.data = buf.slice(1)
} else if (buf[0] === 0x11) {
thumb.format = 'PAL'
thumb.width = buf[1]
thumb.height = buf[2]
thumb.palette = buf.slice(3, 3+768)
thumb.data = buf.slice(3+768 + thumb.width*thumb.height)
} else if (buf[0] === 0x12) {
thumb.format = 'RGB'
thumb.width = buf[1]
thumb.height = buf[1]
thumb.data = 3*thumb.width*thumb.height
}
this.push({
type: 'JFXX',
start: pos - 9,
end: pos + buf.length,
thumbnail: thumb
})
} else if (state === 'app1') {
try {
var row = parseExif(buf)
row.type = 'EXIF'
row.start = pos - 2
row.end = pos + buf.length
this.push(row)
} catch(err) {
return this.emit('error', err)
}
} else if (state === 'app2') {
this.push({
type: 'FPXR',
start: pos - 2,
end: pos + buf.length
})
} else if (state === 'dqt') {
var tables = []
for (var i = 1; i < buf.length; i += 0x41) {
if (buf[i-1] !== dqtSeq++) {
return this.emit('error', new Error('unexpected DQT byte at ' +
(offset+i-1) + ' (' + i + '): ' + buf[i-1]))
}
tables.push(buf.slice(i, i+0x40))
}
this.push({
type: 'DQT',
start: pos - 2,
end: pos + buf.length,
tables: tables
})
} else if (state === 'dht') {
this.push({
type: 'DHT',
start: pos - 2,
end: pos + buf.length,
data: buf
})
} else if (state === 'dri') {
this.push({
type: 'DRI',
start: pos - 2,
end: pos + buf.length
})
} else if (state === 'sos') {
this.push({
type: 'SOS',
start: pos - 2,
end: pos + buf.length
})
return 'data'
} else if (state === 'sof') {
width = buf.readUInt16BE(3)
height = buf.readUInt16BE(1)
this.push({
type: 'SOF',
start: pos - 2,
end: pos + buf.length,
precision: buf[0],
width: width,
height: height,
H0: Math.floor(buf[7] / 16),
V0: buf[7] % 16
})
} else if (state === 'eoi') {
this.push({
type: 'EOI',
start: pos - 2,
end: pos + buf.length,
})
}
return 'ff'
}
}
function hexb (n) { return '0x'+(n<0x10?'0':'')+n.toString(16) }