This commit is contained in:
Your Name
2026-08-11 17:41:36 +08:00
parent cfe4c82c90
commit 03fe4ddf9d
18771 changed files with 3617239 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
'use strict'
var Transform = require('stream').Transform
var streamParser = require('stream-parser')
function ParserStream () {
Transform.call(this, { readableObjectMode: true })
}
// Inherit from Transform
ParserStream.prototype = Object.create(Transform.prototype)
ParserStream.prototype.constructor = ParserStream
streamParser(ParserStream.prototype)
exports.ParserStream = ParserStream
exports.sliceEq = function (src, start, dest) {
for (var i = start, j = 0; j < dest.length;) {
if (src[i++] !== dest[j++]) return false
}
return true
}
exports.str2arr = function (str, format) {
var arr = []
var i = 0
if (format && format === 'hex') {
while (i < str.length) {
arr.push(parseInt(str.slice(i, i + 2), 16))
i += 2
}
} else {
for (; i < str.length; i++) {
arr.push(str.charCodeAt(i) & 0xFF)
}
}
return arr
}
exports.readUInt16LE = function (data, offset) {
return data[offset] | (data[offset + 1] << 8)
}
exports.readUInt16BE = function (data, offset) {
return (data[offset] << 8) | data[offset + 1]
}
exports.readInt16LE = function (data, offset) {
return (exports.readUInt16LE(data, offset) << 16) >> 16
}
exports.readInt16BE = function (data, offset) {
return (exports.readUInt16BE(data, offset) << 16) >> 16
}
exports.readUInt32LE = function (data, offset) {
return (data[offset] |
(data[offset + 1] << 8) |
(data[offset + 2] << 16)) +
data[offset + 3] * 0x1000000
}
exports.readUInt32BE = function (data, offset) {
return data[offset] * 0x1000000 +
((data[offset + 1] << 16) |
(data[offset + 2] << 8) |
data[offset + 3])
}
exports.readInt32LE = function (data, offset) {
return exports.readUInt32LE(data, offset) | 0
}
exports.readInt32BE = function (data, offset) {
return exports.readUInt32BE(data, offset) | 0
}
function ProbeError (message, code, statusCode) {
Error.call(this)
// Include stack trace in error object
if (Error.captureStackTrace) {
// Chrome and NodeJS
Error.captureStackTrace(this, this.constructor)
} else {
// FF, IE 10+ and Safari 6+. Fallback for others
this.stack = (new Error()).stack || ''
}
this.name = this.constructor.name
this.message = message
if (code) this.code = code
if (statusCode) this.statusCode = statusCode
}
// Inherit from Error
ProbeError.prototype = Object.create(Error.prototype)
ProbeError.prototype.constructor = ProbeError
exports.ProbeError = ProbeError
+266
View File
@@ -0,0 +1,266 @@
'use strict'
//
// Helpers
//
function error (message, code) {
var err = new Error(message)
err.code = code
return err
}
function utf8_decode (str) {
try {
return decodeURIComponent(escape(str))
} catch (_) {
return str
}
}
//
// Exif parser
//
// Input:
// - jpeg_bin: Uint8Array - jpeg file
// - exif_start: Number - start of TIFF header (after Exif\0\0)
// - exif_end: Number - end of Exif segment
// - on_entry: Number - callback
//
function ExifParser (jpeg_bin, exif_start, exif_end) {
// Uint8Array, exif without signature (which isn't included in offsets)
this.input = jpeg_bin.subarray(exif_start, exif_end)
// offset correction for `on_entry` callback
this.start = exif_start
// Check TIFF header (includes byte alignment and first IFD offset)
var sig = String.fromCharCode.apply(null, this.input.subarray(0, 4))
if (sig !== 'II\x2A\0' && sig !== 'MM\0\x2A') {
throw error('invalid TIFF signature', 'EBADDATA')
}
// true if motorola (big endian) byte alignment, false if intel
this.big_endian = sig[0] === 'M'
}
ExifParser.prototype.each = function (on_entry) {
// allow premature exit
this.aborted = false
var offset = this.read_uint32(4)
this.ifds_to_read = [{
id: 0,
offset: offset
}]
while (this.ifds_to_read.length > 0 && !this.aborted) {
var i = this.ifds_to_read.shift()
if (!i.offset) continue
this.scan_ifd(i.id, i.offset, on_entry)
}
}
ExifParser.prototype.read_uint16 = function (offset) {
var d = this.input
if (offset + 2 > d.length) throw error('unexpected EOF', 'EBADDATA')
return this.big_endian
? d[offset] * 0x100 + d[offset + 1]
: d[offset] + d[offset + 1] * 0x100
}
ExifParser.prototype.read_uint32 = function (offset) {
var d = this.input
if (offset + 4 > d.length) throw error('unexpected EOF', 'EBADDATA')
return this.big_endian
? d[offset] * 0x1000000 + d[offset + 1] * 0x10000 + d[offset + 2] * 0x100 + d[offset + 3]
: d[offset] + d[offset + 1] * 0x100 + d[offset + 2] * 0x10000 + d[offset + 3] * 0x1000000
}
ExifParser.prototype.is_subifd_link = function (ifd, tag) {
return (ifd === 0 && tag === 0x8769) || // SubIFD
(ifd === 0 && tag === 0x8825) || // GPS Info
(ifd === 0x8769 && tag === 0xA005) // Interop IFD
}
// Returns byte length of a single component of a given format
//
ExifParser.prototype.exif_format_length = function (format) {
switch (format) {
case 1: // byte
case 2: // ascii
case 6: // sbyte
case 7: // undefined
return 1
case 3: // short
case 8: // sshort
return 2
case 4: // long
case 9: // slong
case 11: // float
return 4
case 5: // rational
case 10: // srational
case 12: // double
return 8
default:
// unknown type
return 0
}
}
// Reads Exif data
//
ExifParser.prototype.exif_format_read = function (format, offset) {
var v
switch (format) {
case 1: // byte
case 2: // ascii
v = this.input[offset]
return v
case 6: // sbyte
v = this.input[offset]
return v | (v & 0x80) * 0x1fffffe
case 3: // short
v = this.read_uint16(offset)
return v
case 8: // sshort
v = this.read_uint16(offset)
return v | (v & 0x8000) * 0x1fffe
case 4: // long
v = this.read_uint32(offset)
return v
case 9: // slong
v = this.read_uint32(offset)
return v | 0
case 5: // rational
case 10: // srational
case 11: // float
case 12: // double
return null // not implemented
case 7: // undefined
return null // blob
default:
// unknown type
return null
}
}
ExifParser.prototype.scan_ifd = function (ifd_no, offset, on_entry) {
var entry_count = this.read_uint16(offset)
offset += 2
for (var i = 0; i < entry_count; i++) {
var tag = this.read_uint16(offset)
var format = this.read_uint16(offset + 2)
var count = this.read_uint32(offset + 4)
var comp_length = this.exif_format_length(format)
var data_length = count * comp_length
var data_offset = data_length <= 4 ? offset + 8 : this.read_uint32(offset + 8)
var is_subifd_link = false
if (data_offset + data_length > this.input.length) {
throw error('unexpected EOF', 'EBADDATA')
}
var value = []
var comp_offset = data_offset
for (var j = 0; j < count; j++, comp_offset += comp_length) {
var item = this.exif_format_read(format, comp_offset)
if (item === null) {
value = null
break
}
value.push(item)
}
if (Array.isArray(value) && format === 2) {
value = utf8_decode(String.fromCharCode.apply(null, value))
if (value && value[value.length - 1] === '\0') value = value.slice(0, -1)
}
if (this.is_subifd_link(ifd_no, tag)) {
if (Array.isArray(value) && Number.isInteger(value[0]) && value[0] > 0) {
this.ifds_to_read.push({
id: tag,
offset: value[0]
})
is_subifd_link = true
}
}
var entry = {
is_big_endian: this.big_endian,
ifd: ifd_no,
tag: tag,
format: format,
count: count,
entry_offset: offset + this.start,
data_length: data_length,
data_offset: data_offset + this.start,
value: value,
is_subifd_link: is_subifd_link
}
if (on_entry(entry) === false) {
this.aborted = true
return
}
offset += 12
}
if (ifd_no === 0) {
this.ifds_to_read.push({
id: 1,
offset: this.read_uint32(offset)
})
}
}
module.exports.ExifParser = ExifParser
// returns orientation stored in Exif (1-8), 0 if none was found, -1 if error
module.exports.get_orientation = function (data) {
var orientation = 0
try {
new ExifParser(data, 0, data.length).each(function (entry) {
if (entry.ifd === 0 && entry.tag === 0x112 && Array.isArray(entry.value)) {
orientation = entry.value[0]
return false
}
})
return orientation
} catch (err) {
return -1
}
}
+299
View File
@@ -0,0 +1,299 @@
// Utils used to parse miaf-based files (avif/heic/heif)
//
// ISO media file spec:
// https://web.archive.org/web/20180219054429/http://l.web.umkc.edu/lizhu/teaching/2016sp.video-communication/ref/mp4.pdf
//
// ISO image file format spec:
// https://standards.iso.org/ittf/PubliclyAvailableStandards/c066067_ISO_IEC_23008-12_2017.zip
//
'use strict'
var readUInt16BE = require('./common').readUInt16BE
var readUInt32BE = require('./common').readUInt32BE
/*
* interface Box {
* size: uint32; // if size == 0, box lasts until EOF
* boxtype: char[4];
* largesize?: uint64; // only if size == 1
* usertype?: char[16]; // only if boxtype == 'uuid'
* }
*/
function unbox (data, offset) {
if (data.length < 4 + offset) return null
var size = readUInt32BE(data, offset)
// size includes first 4 bytes (length)
if (data.length < size + offset || size < 8) return null
// if size === 1, real size is following uint64 (only for big boxes, not needed)
// if size === 0, real size is until the end of the file (only for big boxes, not needed)
return {
boxtype: String.fromCharCode.apply(null, data.slice(offset + 4, offset + 8)),
data: data.slice(offset + 8, offset + size),
end: offset + size
}
}
module.exports.unbox = unbox
// parses `meta` -> `iprp` -> `ipco` box, returns:
// {
// sizes: [ { width, height } ],
// transforms: [ { type, value } ]
// }
function scan_ipco (data, sandbox) {
var offset = 0
for (;;) {
var box = unbox(data, offset)
if (!box) break
switch (box.boxtype) {
case 'ispe':
sandbox.sizes.push({
width: readUInt32BE(box.data, 4),
height: readUInt32BE(box.data, 8)
})
break
case 'irot':
sandbox.transforms.push({
type: 'irot',
value: box.data[0] & 3
})
break
case 'imir':
sandbox.transforms.push({
type: 'imir',
value: box.data[0] & 1
})
break
}
offset = box.end
}
}
function readUIntBE (data, offset, size) {
var result = 0
for (var i = 0; i < size; i++) {
result = result * 256 + (data[offset + i] || 0)
}
return result
}
// parses `meta` -> `iloc` box
function scan_iloc (data, sandbox) {
var offset_size = (data[4] >> 4) & 0xF
var length_size = data[4] & 0xF
var base_offset_size = (data[5] >> 4) & 0xF
var item_count = readUInt16BE(data, 6)
var offset = 8
for (var i = 0; i < item_count; i++) {
var item_ID = readUInt16BE(data, offset)
offset += 2
var data_reference_index = readUInt16BE(data, offset)
offset += 2
var base_offset = readUIntBE(data, offset, base_offset_size)
offset += base_offset_size
var extent_count = readUInt16BE(data, offset)
offset += 2
if (data_reference_index === 0 && extent_count === 1) {
var first_extent_offset = readUIntBE(data, offset, offset_size)
var first_extent_length = readUIntBE(data, offset + offset_size, length_size)
sandbox.item_loc[item_ID] = { length: first_extent_length, offset: first_extent_offset + base_offset }
}
offset += extent_count * (offset_size + length_size)
}
}
// parses `meta` -> `iinf` box
function scan_iinf (data, sandbox) {
var item_count = readUInt16BE(data, 4)
var offset = 6
for (var i = 0; i < item_count; i++) {
var box = unbox(data, offset)
if (!box) break
if (box.boxtype === 'infe') {
var item_id = readUInt16BE(box.data, 4)
var item_name = ''
for (var pos = 8; pos < box.data.length && box.data[pos]; pos++) {
item_name += String.fromCharCode(box.data[pos])
}
sandbox.item_inf[item_name] = item_id
}
offset = box.end
}
}
// parses `meta` -> `iprp` box
function scan_iprp (data, sandbox) {
var offset = 0
for (;;) {
var box = unbox(data, offset)
if (!box) break
if (box.boxtype === 'ipco') scan_ipco(box.data, sandbox)
offset = box.end
}
}
// parses `meta` box
function scan_meta (data, sandbox) {
var offset = 4 // version + flags
for (;;) {
var box = unbox(data, offset)
if (!box) break
if (box.boxtype === 'iprp') scan_iprp(box.data, sandbox)
if (box.boxtype === 'iloc') scan_iloc(box.data, sandbox)
if (box.boxtype === 'iinf') scan_iinf(box.data, sandbox)
offset = box.end
}
}
// get image with largest single dimension as base
function getMaxSize (sizes) {
var maxWidthSize = sizes.reduce(function (a, b) {
return a.width > b.width || (a.width === b.width && a.height > b.height) ? a : b
})
var maxHeightSize = sizes.reduce(function (a, b) {
return a.height > b.height || (a.height === b.height && a.width > b.width) ? a : b
})
var maxSize
if (maxWidthSize.width > maxHeightSize.height ||
(maxWidthSize.width === maxHeightSize.height && maxWidthSize.height > maxHeightSize.width)) {
maxSize = maxWidthSize
} else {
maxSize = maxHeightSize
}
return maxSize
}
module.exports.readSizeFromMeta = function (data) {
var sandbox = {
sizes: [],
transforms: [],
item_inf: {},
item_loc: {}
}
scan_meta(data, sandbox)
if (!sandbox.sizes.length) return
var maxSize = getMaxSize(sandbox.sizes)
var orientation = 1
// convert imir/irot to exif orientation
sandbox.transforms.forEach(function (transform) {
var rotate_ccw = { 1: 6, 2: 5, 3: 8, 4: 7, 5: 4, 6: 3, 7: 2, 8: 1 }
var mirror_vert = { 1: 4, 2: 3, 3: 2, 4: 1, 5: 6, 6: 5, 7: 8, 8: 7 }
if (transform.type === 'imir') {
if (transform.value === 0) {
// vertical flip
orientation = mirror_vert[orientation]
} else {
// horizontal flip = vertical flip + 180 deg rotation
orientation = mirror_vert[orientation]
orientation = rotate_ccw[orientation]
orientation = rotate_ccw[orientation]
}
}
if (transform.type === 'irot') {
// counter-clockwise rotation 90 deg 0-3 times
for (var i = 0; i < transform.value; i++) {
orientation = rotate_ccw[orientation]
}
}
})
var exif_location = null
if (sandbox.item_inf.Exif) {
exif_location = sandbox.item_loc[sandbox.item_inf.Exif]
}
return {
width: maxSize.width,
height: maxSize.height,
orientation: sandbox.transforms.length ? orientation : null,
variants: sandbox.sizes,
exif_location: exif_location
}
}
module.exports.getMimeType = function (data) {
var brand = String.fromCharCode.apply(null, data.slice(0, 4))
var compat = {}
compat[brand] = true
for (var i = 8; i < data.length; i += 4) {
compat[String.fromCharCode.apply(null, data.slice(i, i + 4))] = true
}
// heic and avif are superset of miaf, so they should all list mif1 as compatible
if (!compat.mif1 && !compat.msf1 && !compat.miaf) return
if (brand === 'avif' || brand === 'avis' || brand === 'avio') {
// `.avifs` and `image/avif-sequence` are removed from spec, all files have single type
return { type: 'avif', mime: 'image/avif' }
}
// https://nokiatech.github.io/heif/technical.html
if (brand === 'heic' || brand === 'heix') {
return { type: 'heic', mime: 'image/heic' }
}
if (brand === 'hevc' || brand === 'hevx') {
return { type: 'heic', mime: 'image/heic-sequence' }
}
if (compat.avif || compat.avis) {
return { type: 'avif', mime: 'image/avif' }
}
if (compat.heic || compat.heix || compat.hevc || compat.hevx || compat.heis) {
if (compat.msf1) {
return { type: 'heif', mime: 'image/heif-sequence' }
}
return { type: 'heif', mime: 'image/heif' }
}
return { type: 'avif', mime: 'image/avif' }
}
+166
View File
@@ -0,0 +1,166 @@
// Utils used to parse miaf-based files (avif/heic/heif)
//
// - image collections are not supported (only last size is reported)
// - images with metadata encoded after image data are not supported
// - images without any `ispe` box are not supported
//
'use strict'
var ParserStream = require('../common').ParserStream
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var readUInt32BE = require('../common').readUInt32BE
var miaf = require('../miaf_utils')
var exif = require('../exif_utils')
var SIG_FTYP = str2arr('ftyp')
function safeSkip (parser, count, callback) {
if (count === 0) { // parser._skipBytes throws error if count === 0
callback()
return
}
parser._skipBytes(count, callback)
}
function readExifOrientation (parser, sandbox, on_orientation) {
if (!sandbox.exif_location || sandbox.exif_location.offset <= sandbox.offset) {
on_orientation(0)
return
}
parser._skipBytes(sandbox.exif_location.offset - sandbox.offset, function () {
sandbox.offset = sandbox.exif_location.offset
parser._bytes(4, function (data) {
sandbox.offset += 4
var sig_offset = readUInt32BE(data, 0)
safeSkip(parser, sig_offset, function () {
sandbox.offset += sig_offset
var byteCount = sandbox.exif_location.length - sig_offset - 4
if (byteCount <= 0) {
on_orientation(0)
return
}
parser._bytes(byteCount, function (exif_data) {
sandbox.offset += byteCount
on_orientation(exif.get_orientation(exif_data))
})
})
})
})
}
// sandbox is a storage for intermediate data retrieved from jpeg while parsing it
function readAvifSize (parser, sandbox) {
parser._bytes(8, function (data) {
sandbox.offset += 8
var size = readUInt32BE(data, 0) - 8
var type = String.fromCharCode.apply(null, data.slice(4, 8))
if (type === 'mdat') {
parser._skipBytes(Infinity)
parser.push(null)
return
}
if (size < 0) {
parser._skipBytes(Infinity)
parser.push(null)
return
}
if (type === 'meta' && size > 0) {
parser._bytes(size, function (data) {
sandbox.offset += size
var imgSize = miaf.readSizeFromMeta(data)
if (!imgSize) {
parser._skipBytes(Infinity)
parser.push(null)
return
}
var result = {
width: imgSize.width,
height: imgSize.height,
type: sandbox.fileType.type,
mime: sandbox.fileType.mime,
wUnits: 'px',
hUnits: 'px'
}
if (imgSize.variants.length > 1) {
result.variants = imgSize.variants
}
if (imgSize.orientation) {
result.orientation = imgSize.orientation
}
sandbox.exif_location = imgSize.exif_location
readExifOrientation(parser, sandbox, function (orientation) {
if (orientation > 0) result.orientation = orientation
parser._skipBytes(Infinity)
parser.push(result)
parser.push(null)
})
})
} else {
safeSkip(parser, size, function () {
sandbox.offset += size
readAvifSize(parser, sandbox)
})
}
})
}
module.exports = function () {
var parser = new ParserStream()
var sandbox = { offset: 0, fileType: null }
parser._bytes(8, function (data) {
sandbox.offset += 8
if (!sliceEq(data, 4, SIG_FTYP)) {
parser._skipBytes(Infinity)
parser.push(null)
return
}
var size = readUInt32BE(data, 0) - 8
if (size <= 0) {
parser._skipBytes(Infinity)
parser.push(null)
return
}
parser._bytes(size, function (data) {
sandbox.offset += size
sandbox.fileType = miaf.getMimeType(data)
if (!sandbox.fileType) {
parser._skipBytes(Infinity)
parser.push(null)
return
}
readAvifSize(parser, sandbox)
})
})
return parser
}
+53
View File
@@ -0,0 +1,53 @@
'use strict'
var ParserStream = require('../common').ParserStream
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var SIG_BM = str2arr('BM')
module.exports = function () {
var parser = new ParserStream()
parser._bytes(26, function (data) {
parser._skipBytes(Infinity)
if (!sliceEq(data, 0, SIG_BM)) {
parser.push(null)
return
}
var w, h
var headerSize = data.readUInt32LE(14)
if (headerSize === 12) {
// BMP v2 header
w = data.readInt16LE(18)
h = data.readInt16LE(20)
} else if (headerSize > 12) {
// BMP v3+ header
w = data.readInt32LE(18)
h = data.readInt32LE(22)
} else {
parser.push(null)
return
}
parser.push({
width: w,
// Height can be negative to indicate a top-down bitmap
height: Math.abs(h),
type: 'bmp',
mime: 'image/bmp',
wUnits: 'px',
hUnits: 'px'
})
parser.push(null)
})
return parser
}
+37
View File
@@ -0,0 +1,37 @@
'use strict'
var ParserStream = require('../common').ParserStream
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var SIG_GIF87a = str2arr('GIF87a')
var SIG_GIF89a = str2arr('GIF89a')
module.exports = function () {
var parser = new ParserStream()
parser._bytes(10, function (data) {
parser._skipBytes(Infinity)
if (!sliceEq(data, 0, SIG_GIF87a) && !sliceEq(data, 0, SIG_GIF89a)) {
parser.push(null)
return
}
parser.push({
width: data.readUInt16LE(6),
height: data.readUInt16LE(8),
type: 'gif',
mime: 'image/gif',
wUnits: 'px',
hUnits: 'px'
})
parser.push(null)
})
return parser
}
+56
View File
@@ -0,0 +1,56 @@
'use strict'
var ParserStream = require('../common').ParserStream
var HEADER = 0
var TYPE_ICO = 1
var INDEX_SIZE = 16
// Format specification:
// https://en.wikipedia.org/wiki/ICO_(file_format)#Icon_resource_structure
module.exports = function () {
var parser = new ParserStream()
parser._bytes(6, function (data) {
var header = data.readUInt16LE(0)
var type = data.readUInt16LE(2)
var numImages = data.readUInt16LE(4)
if (header !== HEADER || type !== TYPE_ICO || !numImages) {
parser._skipBytes(Infinity)
parser.push(null)
return
}
parser._bytes(numImages * INDEX_SIZE, function (indexData) {
parser._skipBytes(Infinity)
var variants = []
var maxSize = { width: 0, height: 0 }
for (var i = 0; i < numImages; i++) {
var width = indexData.readUInt8(INDEX_SIZE * i + 0) || 256
var height = indexData.readUInt8(INDEX_SIZE * i + 1) || 256
var size = { width: width, height: height }
variants.push(size)
if (width > maxSize.width || height > maxSize.height) {
maxSize = size
}
}
parser.push({
width: maxSize.width,
height: maxSize.height,
variants: variants,
type: 'ico',
mime: 'image/x-icon',
wUnits: 'px',
hUnits: 'px'
})
parser.push(null)
})
})
return parser
}
+148
View File
@@ -0,0 +1,148 @@
'use strict'
var ParserStream = require('../common').ParserStream
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var exif = require('../exif_utils')
var SIG_EXIF = str2arr('Exif\0\0')
// part of parseJpegMarker called after skipping initial FF
function parseJpegMarker_afterFF (parser, callback) {
parser._bytes(1, function (data) {
var code = data[0]
if (code === 0xFF) {
// padding byte, skip it
parseJpegMarker_afterFF(parser, callback)
return
}
// standalone markers, according to JPEG 1992,
// http://www.w3.org/Graphics/JPEG/itu-t81.pdf, see Table B.1
if ((code >= 0xD0 && code <= 0xD9) || code === 0x01) {
callback(code, 0)
return
}
// the rest of the unreserved markers
if (code >= 0xC0 && code <= 0xFE) {
parser._bytes(2, function (length) {
callback(code, length.readUInt16BE(0) - 2)
})
return
}
// unknown markers
callback()
})
}
function parseJpegMarker (parser, sandbox, callback) {
var start = sandbox.start
sandbox.start = false
parser._bytes(1, function (data) {
if (data[0] !== 0xFF) {
// not a JPEG marker
if (start) {
// expect JPEG file to start with `FFD8 FFE0`, `FFD8 FFE2` or `FFD8 FFE1`,
// don't allow garbage before second marker
callback()
} else {
// skip until we see 0xFF, see https://github.com/nodeca/probe-image-size/issues/68
parseJpegMarker(parser, sandbox, callback)
}
return
}
parseJpegMarker_afterFF(parser, callback)
})
}
// sandbox is a storage for intermediate data retrieved from jpeg while parsing it
function getJpegSize (parser, sandbox) {
parseJpegMarker(parser, sandbox, function (code, length) {
if (!code || length < 0) {
// invalid jpeg
parser._skipBytes(Infinity)
parser.push(null)
return
}
if (code === 0xD9 /* EOI */ || code === 0xDA /* SOS */) {
// end of the datastream
parser._skipBytes(Infinity)
parser.push(null)
return
}
// try to get orientation from Exif segment
if (code === 0xE1 && length >= 10) {
parser._bytes(length, function (data) {
if (sliceEq(data, 0, SIG_EXIF)) {
sandbox.orientation = exif.get_orientation(data.slice(6, 6 + length))
}
getJpegSize(parser, sandbox)
})
return
}
if (length <= 0) {
// e.g. empty comment
getJpegSize(parser, sandbox)
return
}
if (length >= 5 &&
(code >= 0xC0 && code <= 0xCF) &&
code !== 0xC4 && code !== 0xC8 && code !== 0xCC) {
parser._bytes(length, function (data) {
parser._skipBytes(Infinity)
var result = {
width: data.readUInt16BE(3),
height: data.readUInt16BE(1),
type: 'jpg',
mime: 'image/jpeg',
wUnits: 'px',
hUnits: 'px'
}
if (sandbox.orientation > 0) result.orientation = sandbox.orientation
parser.push(result)
parser.push(null)
})
return
}
parser._skipBytes(length, function () {
getJpegSize(parser, sandbox)
})
})
}
module.exports = function () {
var parser = new ParserStream()
parser._bytes(2, function (data) {
if (data[0] !== 0xFF || data[1] !== 0xD8) {
// first marker of the file MUST be 0xFFD8
parser._skipBytes(Infinity)
parser.push(null)
return
}
getJpegSize(parser, { start: true })
})
return parser
}
+44
View File
@@ -0,0 +1,44 @@
'use strict'
var ParserStream = require('../common').ParserStream
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var SIG_PNG = str2arr('\x89PNG\r\n\x1a\n')
var SIG_IHDR = str2arr('IHDR')
module.exports = function () {
var parser = new ParserStream()
parser._bytes(24, function (data) {
parser._skipBytes(Infinity)
// check PNG signature
if (!sliceEq(data, 0, SIG_PNG)) {
parser.push(null)
return
}
// check that first chunk is IHDR
if (!sliceEq(data, 12, SIG_IHDR)) {
parser.push(null)
return
}
parser.push({
width: data.readUInt32BE(16),
height: data.readUInt32BE(20),
type: 'png',
mime: 'image/png',
wUnits: 'px',
hUnits: 'px'
})
parser.push(null)
})
return parser
}
+40
View File
@@ -0,0 +1,40 @@
'use strict'
var ParserStream = require('../common').ParserStream
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var SIG_8BPS = str2arr('8BPS\x00\x01')
module.exports = function () {
var parser = new ParserStream()
parser._bytes(6, function (data) {
// signature + version
if (!sliceEq(data, 0, SIG_8BPS)) {
parser._skipBytes(Infinity)
parser.push(null)
return
}
parser._bytes(16, function (data) {
parser._skipBytes(Infinity)
parser.push({
width: data.readUInt32BE(12),
height: data.readUInt32BE(8),
type: 'psd',
mime: 'image/vnd.adobe.photoshop',
wUnits: 'px',
hUnits: 'px'
})
parser.push(null)
})
})
return parser
}
+217
View File
@@ -0,0 +1,217 @@
'use strict'
var Transform = require('stream').Transform
var STATE_IDENTIFY = 0 // look for '<'
var STATE_PARSE = 1 // extract width and height from svg tag
var STATE_IGNORE = 2 // we got all the data we want, skip the rest
// max size for pre-svg-tag comments plus svg tag itself
var MAX_DATA_LENGTH = 65536
// skip `<?` (comments), `<!` (directives, cdata, doctype),
// looking for `<svg>` or `<NAMESPACE:svg>`
var SVG_HEADER_RE = /<[-_.:a-zA-Z0-9][^>]*>/
// test if the top level element is svg + optional namespace,
// used to skip svg embedded in html
var SVG_TAG_RE = /^<([-_.:a-zA-Z0-9]+:)?svg\s/
var SVG_WIDTH_RE = /[^-]\bwidth="([^%]+?)"|[^-]\bwidth='([^%]+?)'/
var SVG_HEIGHT_RE = /\bheight="([^%]+?)"|\bheight='([^%]+?)'/
var SVG_VIEWBOX_RE = /\bview[bB]ox="(.+?)"|\bview[bB]ox='(.+?)'/
var SVG_UNITS_RE = /in$|mm$|cm$|pt$|pc$|px$|em$|ex$/
function isWhiteSpace (chr) {
return chr === 0x20 || chr === 0x09 || chr === 0x0D || chr === 0x0A
}
// Filter NaN, Infinity, < 0
function isFinitePositive (val) {
return typeof val === 'number' && isFinite(val) && val > 0
}
function svgAttrs (str) {
var width = str.match(SVG_WIDTH_RE)
var height = str.match(SVG_HEIGHT_RE)
var viewbox = str.match(SVG_VIEWBOX_RE)
return {
width: width && (width[1] || width[2]),
height: height && (height[1] || height[2]),
viewbox: viewbox && (viewbox[1] || viewbox[2])
}
}
function units (str) {
if (!SVG_UNITS_RE.test(str)) return 'px'
return str.match(SVG_UNITS_RE)[0]
}
function parseSvg (str) {
// get top level element
var svgTag = (str.match(SVG_HEADER_RE) || [''])[0]
// test if top level element is <svg>
if (!SVG_TAG_RE.test(svgTag)) return
var attrs = svgAttrs(svgTag)
var width = parseFloat(attrs.width)
var height = parseFloat(attrs.height)
// Extract from direct values
if (attrs.width && attrs.height) {
if (!isFinitePositive(width) || !isFinitePositive(height)) return
return {
width: width,
height: height,
type: 'svg',
mime: 'image/svg+xml',
wUnits: units(attrs.width),
hUnits: units(attrs.height)
}
}
// Extract from viewbox
var parts = (attrs.viewbox || '').split(' ')
var viewbox = {
width: parts[2],
height: parts[3]
}
var vbWidth = parseFloat(viewbox.width)
var vbHeight = parseFloat(viewbox.height)
if (!isFinitePositive(vbWidth) || !isFinitePositive(vbHeight)) return
if (units(viewbox.width) !== units(viewbox.height)) return
var ratio = vbWidth / vbHeight
if (attrs.width) {
if (!isFinitePositive(width)) return
return {
width: width,
height: width / ratio,
type: 'svg',
mime: 'image/svg+xml',
wUnits: units(attrs.width),
hUnits: units(attrs.width)
}
}
if (attrs.height) {
if (!isFinitePositive(height)) return
return {
width: height * ratio,
height: height,
type: 'svg',
mime: 'image/svg+xml',
wUnits: units(attrs.height),
hUnits: units(attrs.height)
}
}
return {
width: vbWidth,
height: vbHeight,
type: 'svg',
mime: 'image/svg+xml',
wUnits: units(viewbox.width),
hUnits: units(viewbox.height)
}
}
module.exports = function () {
var state = STATE_IDENTIFY
var data_len = 0
var str = ''
var buf = null // used to manage first chunk in IDENTIFY
var parser = new Transform({
readableObjectMode: true,
transform: function transform (chunk, encoding, next) {
switch (state) {
// identify step is needed to fail fast if the file isn't SVG
case STATE_IDENTIFY:
if (buf) {
// make sure that first chunk is at least 4 bytes (to do BOM skip later),
// last chunk was small
chunk = Buffer.concat([buf, chunk])
buf = null
}
if (data_len === 0 && chunk.length < 4) {
// make sure that first chunk is at least 4 bytes (to do BOM skip later),
// current chunk is small
buf = chunk
break
}
var i = 0
var max = chunk.length
// byte order mark, https://github.com/nodeca/probe-image-size/issues/57
if (data_len === 0 && chunk[0] === 0xEF && chunk[1] === 0xBB && chunk[2] === 0xBF) i = 3
while (i < max && isWhiteSpace(chunk[i])) i++
if (i >= max) {
data_len += chunk.length
if (data_len > MAX_DATA_LENGTH) {
state = STATE_IGNORE
parser.push(null)
}
} else if (chunk[i] === 0x3c /* < */) {
state = STATE_PARSE
return transform(chunk, encoding, next)
} else {
state = STATE_IGNORE
parser.push(null)
}
break
case STATE_PARSE:
str += chunk.toString()
var result = parseSvg(str)
if (result) {
state = STATE_IGNORE
parser.push(result)
parser.push(null)
break
}
data_len += chunk.length
if (data_len > MAX_DATA_LENGTH) {
state = STATE_IGNORE
parser.push(null)
}
break
}
next()
},
flush: function () {
state = STATE_IGNORE
parser.push(null)
}
})
return parser
}
+112
View File
@@ -0,0 +1,112 @@
'use strict'
var ParserStream = require('../common').ParserStream
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var SIG_1 = str2arr('II\x2A\0')
var SIG_2 = str2arr('MM\0\x2A')
function readUInt16 (buffer, offset, is_big_endian) {
return is_big_endian ? buffer.readUInt16BE(offset) : buffer.readUInt16LE(offset)
}
function readUInt32 (buffer, offset, is_big_endian) {
return is_big_endian ? buffer.readUInt32BE(offset) : buffer.readUInt32LE(offset)
}
function readIFDValue (data, data_offset, is_big_endian) {
var type = readUInt16(data, data_offset + 2, is_big_endian)
var values = readUInt32(data, data_offset + 4, is_big_endian)
if (values !== 1 || (type !== 3 && type !== 4)) {
return null
}
if (type === 3) {
return readUInt16(data, data_offset + 8, is_big_endian)
}
return readUInt32(data, data_offset + 8, is_big_endian)
}
module.exports = function () {
var parser = new ParserStream()
// read header
parser._bytes(8, function (data) {
// check TIFF signature
if (!sliceEq(data, 0, SIG_1) && !sliceEq(data, 0, SIG_2)) {
parser._skipBytes(Infinity)
parser.push(null)
return
}
var is_big_endian = (data[0] === 77 /* 'MM' */)
var count = readUInt32(data, 4, is_big_endian) - 8
if (count < 0) {
parser._skipBytes(Infinity)
parser.push(null)
return
}
function safeSkip (parser, count, callback) {
if (count === 0) { // parser._skipBytes throws error if count === 0
callback()
return
}
parser._skipBytes(count, callback)
}
// skip until IFD
safeSkip(parser, count, function () {
// read number of IFD entries
parser._bytes(2, function (data) {
var ifd_size = readUInt16(data, 0, is_big_endian) * 12
if (ifd_size <= 0) {
parser._skipBytes(Infinity)
parser.push(null)
return
}
// read all IFD entries
parser._bytes(ifd_size, function (data) {
parser._skipBytes(Infinity)
var i, width, height, tag
for (i = 0; i < ifd_size; i += 12) {
tag = readUInt16(data, i, is_big_endian)
if (tag === 256) {
width = readIFDValue(data, i, is_big_endian)
} else if (tag === 257) {
height = readIFDValue(data, i, is_big_endian)
}
}
if (width && height) {
parser.push({
width: width,
height: height,
type: 'tiff',
mime: 'image/tiff',
wUnits: 'px',
hUnits: 'px'
})
}
parser.push(null)
})
})
})
})
return parser
}
+177
View File
@@ -0,0 +1,177 @@
'use strict'
var ParserStream = require('../common').ParserStream
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var exif = require('../exif_utils')
var SIG_RIFF = str2arr('RIFF')
var SIG_WEBP = str2arr('WEBP')
function safeSkip (parser, count, callback) {
if (count === 0) { // parser._skipBytes throws error if count === 0
callback()
return
}
parser._skipBytes(count, callback)
}
function parseVP8 (parser, length, sandbox) {
parser._bytes(10, function (data) {
// check code block signature
if (data[3] === 0x9D && data[4] === 0x01 && data[5] === 0x2A) {
sandbox.result = sandbox.result || {
width: data.readUInt16LE(6) & 0x3FFF,
height: data.readUInt16LE(8) & 0x3FFF,
type: 'webp',
mime: 'image/webp',
wUnits: 'px',
hUnits: 'px'
}
}
safeSkip(parser, length - 10, function () {
sandbox.offset += length
getWebpSize(parser, sandbox)
})
})
}
function parseVP8L (parser, length, sandbox) {
parser._bytes(5, function (data) {
// check code block signature
if (data[0] === 0x2F) {
var bits = data.readUInt32LE(1)
sandbox.result = sandbox.result || {
width: (bits & 0x3FFF) + 1,
height: ((bits >> 14) & 0x3FFF) + 1,
type: 'webp',
mime: 'image/webp',
wUnits: 'px',
hUnits: 'px'
}
}
safeSkip(parser, length - 5, function () {
sandbox.offset += length
getWebpSize(parser, sandbox)
})
})
}
function parseVP8X (parser, length, sandbox) {
parser._bytes(10, function (data) {
sandbox.result = sandbox.result || {
// TODO: replace with `data.readUIntLE(8, 3) + 1`
// when 0.10 support is dropped
width: ((data[6] << 16) | (data[5] << 8) | data[4]) + 1,
height: ((data[9] << 16) | (data[8] << 8) | data[7]) + 1,
type: 'webp',
mime: 'image/webp',
wUnits: 'px',
hUnits: 'px'
}
safeSkip(parser, length - 10, function () {
sandbox.offset += length
getWebpSize(parser, sandbox)
})
})
}
function parseExif (parser, length, sandbox) {
parser._bytes(length, function (data) {
// exif is the last chunk we care about, stop after it
sandbox.offset = Infinity
sandbox.exif_orientation = exif.get_orientation(data)
getWebpSize(parser, sandbox)
})
}
function getWebpSize (parser, sandbox) {
if (sandbox.fileLength - 8 <= sandbox.offset) {
parser._skipBytes(Infinity)
if (sandbox.result) {
var result = sandbox.result
if (sandbox.exif_orientation > 0) {
result.orientation = sandbox.exif_orientation
}
parser.push(result)
}
parser.push(null)
return
}
parser._bytes(4 - sandbox.bufferedChunkHeader.length, function (data) {
sandbox.offset += 4 - sandbox.bufferedChunkHeader.length
var header = sandbox.bufferedChunkHeader + String.fromCharCode.apply(null, data)
// after each chunk of odd size there should be 0 byte of padding, skip those
header = header.replace(/^\0+/, '')
if (header.length < 4) {
sandbox.bufferedChunkHeader = header
getWebpSize(parser, sandbox)
return
}
sandbox.bufferedChunkHeader = ''
parser._bytes(4, function (data) {
sandbox.offset += 4
var length = data.readUInt32LE(0)
if (header === 'VP8 ' && length >= 10) {
parseVP8(parser, length, sandbox)
} else if (header === 'VP8L' && length >= 5) {
parseVP8L(parser, length, sandbox)
} else if (header === 'VP8X' && length >= 10) {
parseVP8X(parser, length, sandbox)
} else if (header === 'EXIF' && length >= 4) {
parseExif(parser, length, sandbox)
} else {
safeSkip(parser, length, function () {
sandbox.offset += length
getWebpSize(parser, sandbox)
})
}
})
})
}
module.exports = function () {
var parser = new ParserStream()
parser._bytes(12, function (data) {
// check /^RIFF....WEBPVP8([ LX])$/ signature
if (sliceEq(data, 0, SIG_RIFF) && sliceEq(data, 8, SIG_WEBP)) {
getWebpSize(parser, {
fileLength: data.readUInt32LE(4) + 8,
offset: 12,
exif_orientation: 0,
bufferedChunkHeader: '' // for dealing with padding
})
} else {
parser._skipBytes(Infinity)
parser.push(null)
}
})
return parser
}
+87
View File
@@ -0,0 +1,87 @@
// Utils used to parse miaf-based files (avif/heic/heif)
//
// - image collections are not supported (only last size is reported)
// - images with metadata encoded after image data are not supported
// - images without any `ispe` box are not supported
//
'use strict'
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var readUInt32BE = require('../common').readUInt32BE
var miaf = require('../miaf_utils')
var exif = require('../exif_utils')
var SIG_FTYP = str2arr('ftyp')
module.exports = function (data) {
// ISO media file (avif format) starts with ftyp box:
// 0000 0020 6674 7970 6176 6966
// (length) f t y p a v i f
//
if (!sliceEq(data, 4, SIG_FTYP)) return
var firstBox = miaf.unbox(data, 0)
if (!firstBox) return
var fileType = miaf.getMimeType(firstBox.data)
if (!fileType) return
var meta
var offset = firstBox.end
for (;;) {
var box = miaf.unbox(data, offset)
if (!box) break
offset = box.end
// mdat block SHOULD be last (but not strictly required),
// so it's unlikely that metadata is after it
if (box.boxtype === 'mdat') return
if (box.boxtype === 'meta') {
meta = box.data
break
}
}
if (!meta) return
var imgSize = miaf.readSizeFromMeta(meta)
if (!imgSize) return
var result = {
width: imgSize.width,
height: imgSize.height,
type: fileType.type,
mime: fileType.mime,
wUnits: 'px',
hUnits: 'px'
}
if (imgSize.variants.length > 1) {
result.variants = imgSize.variants
}
if (imgSize.orientation) {
result.orientation = imgSize.orientation
}
if (imgSize.exif_location &&
imgSize.exif_location.offset + imgSize.exif_location.length <= data.length) {
var sig_offset = readUInt32BE(data, imgSize.exif_location.offset)
var exif_data = data.slice(
imgSize.exif_location.offset + sig_offset + 4,
imgSize.exif_location.offset + imgSize.exif_location.length)
var orientation = exif.get_orientation(exif_data)
if (orientation > 0) result.orientation = orientation
}
return result
}
+44
View File
@@ -0,0 +1,44 @@
'use strict'
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var readInt16LE = require('../common').readInt16LE
var readInt32LE = require('../common').readInt32LE
var readUInt32LE = require('../common').readUInt32LE
var SIG_BM = str2arr('BM')
module.exports = function (data) {
if (data.length < 26) return
if (!sliceEq(data, 0, SIG_BM)) return
var h
var w
var headerSize = readUInt32LE(data, 14)
if (headerSize === 12) {
// BMP v2 header
w = readInt16LE(data, 18)
h = readInt16LE(data, 20)
} else if (headerSize > 12) {
// BMP v3+ header
w = readInt32LE(data, 18)
h = readInt32LE(data, 22)
} else {
// BPM v1 and other garbage (10 bytes usually)
return
}
return {
width: w,
// Height can be negative to indicate a top-down bitmap
height: Math.abs(h),
type: 'bmp',
mime: 'image/bmp',
wUnits: 'px',
hUnits: 'px'
}
}
+26
View File
@@ -0,0 +1,26 @@
'use strict'
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var readUInt16LE = require('../common').readUInt16LE
var SIG_GIF87a = str2arr('GIF87a')
var SIG_GIF89a = str2arr('GIF89a')
module.exports = function (data) {
if (data.length < 10) return
if (!sliceEq(data, 0, SIG_GIF87a) && !sliceEq(data, 0, SIG_GIF89a)) return
return {
width: readUInt16LE(data, 6),
height: readUInt16LE(data, 8),
type: 'gif',
mime: 'image/gif',
wUnits: 'px',
hUnits: 'px'
}
}
+46
View File
@@ -0,0 +1,46 @@
'use strict'
var readUInt16LE = require('../common').readUInt16LE
var HEADER = 0
var TYPE_ICO = 1
var INDEX_SIZE = 16
// Format specification:
// https://en.wikipedia.org/wiki/ICO_(file_format)#Icon_resource_structure
module.exports = function (data) {
var header = readUInt16LE(data, 0)
var type = readUInt16LE(data, 2)
var numImages = readUInt16LE(data, 4)
if (header !== HEADER || type !== TYPE_ICO || !numImages) {
return
}
if (data.length < 6 + numImages * INDEX_SIZE) return
var variants = []
var maxSize = { width: 0, height: 0 }
for (var i = 0; i < numImages; i++) {
var width = data[6 + INDEX_SIZE * i] || 256
var height = data[6 + INDEX_SIZE * i + 1] || 256
var size = { width: width, height: height }
variants.push(size)
if (width > maxSize.width || height > maxSize.height) {
maxSize = size
}
}
return {
width: maxSize.width,
height: maxSize.height,
variants: variants,
type: 'ico',
mime: 'image/x-icon',
wUnits: 'px',
hUnits: 'px'
}
}
+85
View File
@@ -0,0 +1,85 @@
'use strict'
var readUInt16BE = require('../common').readUInt16BE
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var exif = require('../exif_utils')
var SIG_EXIF = str2arr('Exif\0\0')
module.exports = function (data) {
if (data.length < 2) return
// first marker of the file MUST be 0xFFD8,
// following by either 0xFFE0, 0xFFE2 or 0xFFE3
if (data[0] !== 0xFF || data[1] !== 0xD8 || data[2] !== 0xFF) return
var offset = 2
for (;;) {
// skip until we see 0xFF, see https://github.com/nodeca/probe-image-size/issues/68
for (;;) {
if (data.length - offset < 2) return
if (data[offset++] === 0xFF) break
}
var code = data[offset++]
var length
// skip padding bytes
while (code === 0xFF) code = data[offset++]
// standalone markers, according to JPEG 1992,
// http://www.w3.org/Graphics/JPEG/itu-t81.pdf, see Table B.1
if ((code >= 0xD0 && code <= 0xD9) || code === 0x01) {
length = 0
} else if (code >= 0xC0 && code <= 0xFE) {
// the rest of the unreserved markers
if (data.length - offset < 2) return
length = readUInt16BE(data, offset) - 2
offset += 2
} else {
// unknown markers
return
}
if (code === 0xD9 /* EOI */ || code === 0xDA /* SOS */) {
// end of the datastream
return
}
var orientation
// try to get orientation from Exif segment
if (code === 0xE1 && length >= 10 && sliceEq(data, offset, SIG_EXIF)) {
orientation = exif.get_orientation(data.slice(offset + 6, offset + length))
}
if (length >= 5 &&
(code >= 0xC0 && code <= 0xCF) &&
code !== 0xC4 && code !== 0xC8 && code !== 0xCC) {
if (data.length - offset < length) return
var result = {
width: readUInt16BE(data, offset + 3),
height: readUInt16BE(data, offset + 1),
type: 'jpg',
mime: 'image/jpeg',
wUnits: 'px',
hUnits: 'px'
}
if (orientation > 0) {
result.orientation = orientation
}
return result
}
offset += length
}
}
+30
View File
@@ -0,0 +1,30 @@
'use strict'
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var readUInt32BE = require('../common').readUInt32BE
var SIG_PNG = str2arr('\x89PNG\r\n\x1a\n')
var SIG_IHDR = str2arr('IHDR')
module.exports = function (data) {
if (data.length < 24) return
// check PNG signature
if (!sliceEq(data, 0, SIG_PNG)) return
// check that first chunk is IHDR
if (!sliceEq(data, 12, SIG_IHDR)) return
return {
width: readUInt32BE(data, 16),
height: readUInt32BE(data, 20),
type: 'png',
mime: 'image/png',
wUnits: 'px',
hUnits: 'px'
}
}
+26
View File
@@ -0,0 +1,26 @@
'use strict'
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var readUInt32BE = require('../common').readUInt32BE
var SIG_8BPS = str2arr('8BPS\x00\x01')
module.exports = function (data) {
if (data.length < 6 + 16) return
// signature + version
if (!sliceEq(data, 0, SIG_8BPS)) return
return {
width: readUInt32BE(data, 6 + 12),
height: readUInt32BE(data, 6 + 8),
type: 'psd',
mime: 'image/vnd.adobe.photoshop',
wUnits: 'px',
hUnits: 'px'
}
}
+145
View File
@@ -0,0 +1,145 @@
'use strict'
function isWhiteSpace (chr) {
return chr === 0x20 || chr === 0x09 || chr === 0x0D || chr === 0x0A
}
// Filter NaN, Infinity, < 0
function isFinitePositive (val) {
return typeof val === 'number' && isFinite(val) && val > 0
}
function canBeSvg (buf) {
var i = 0
var max = buf.length
// byte order mark, https://github.com/nodeca/probe-image-size/issues/57
if (buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) i = 3
while (i < max && isWhiteSpace(buf[i])) i++
if (i === max) return false
return buf[i] === 0x3c /* < */
}
// skip `<?` (comments), `<!` (directives, cdata, doctype),
// looking for `<svg>` or `<NAMESPACE:svg>`
var SVG_HEADER_RE = /<[-_.:a-zA-Z0-9][^>]*>/
// test if the top level element is svg + optional namespace,
// used to skip svg embedded in html
var SVG_TAG_RE = /^<([-_.:a-zA-Z0-9]+:)?svg\s/
var SVG_WIDTH_RE = /[^-]\bwidth="([^%]+?)"|[^-]\bwidth='([^%]+?)'/
var SVG_HEIGHT_RE = /\bheight="([^%]+?)"|\bheight='([^%]+?)'/
var SVG_VIEWBOX_RE = /\bview[bB]ox="(.+?)"|\bview[bB]ox='(.+?)'/
var SVG_UNITS_RE = /in$|mm$|cm$|pt$|pc$|px$|em$|ex$/
function svgAttrs (str) {
var width = str.match(SVG_WIDTH_RE)
var height = str.match(SVG_HEIGHT_RE)
var viewbox = str.match(SVG_VIEWBOX_RE)
return {
width: width && (width[1] || width[2]),
height: height && (height[1] || height[2]),
viewbox: viewbox && (viewbox[1] || viewbox[2])
}
}
function units (str) {
if (!SVG_UNITS_RE.test(str)) return 'px'
return str.match(SVG_UNITS_RE)[0]
}
module.exports = function (data) {
if (!canBeSvg(data)) return
var str = ''
for (var i = 0; i < data.length; i++) {
// 1. We can't rely on buffer features
// 2. Don't care about UTF16 because ascii is enougth for our goals
str += String.fromCharCode(data[i])
}
// get top level element
var svgTag = (str.match(SVG_HEADER_RE) || [''])[0]
// test if top level element is <svg>
if (!SVG_TAG_RE.test(svgTag)) return
var attrs = svgAttrs(svgTag)
var width = parseFloat(attrs.width)
var height = parseFloat(attrs.height)
// Extract from direct values
if (attrs.width && attrs.height) {
if (!isFinitePositive(width) || !isFinitePositive(height)) return
return {
width: width,
height: height,
type: 'svg',
mime: 'image/svg+xml',
wUnits: units(attrs.width),
hUnits: units(attrs.height)
}
}
// Extract from viewbox
var parts = (attrs.viewbox || '').split(' ')
var viewbox = {
width: parts[2],
height: parts[3]
}
var vbWidth = parseFloat(viewbox.width)
var vbHeight = parseFloat(viewbox.height)
if (!isFinitePositive(vbWidth) || !isFinitePositive(vbHeight)) return
if (units(viewbox.width) !== units(viewbox.height)) return
var ratio = vbWidth / vbHeight
if (attrs.width) {
if (!isFinitePositive(width)) return
return {
width: width,
height: width / ratio,
type: 'svg',
mime: 'image/svg+xml',
wUnits: units(attrs.width),
hUnits: units(attrs.width)
}
}
if (attrs.height) {
if (!isFinitePositive(height)) return
return {
width: height * ratio,
height: height,
type: 'svg',
mime: 'image/svg+xml',
wUnits: units(attrs.height),
hUnits: units(attrs.height)
}
}
return {
width: vbWidth,
height: vbHeight,
type: 'svg',
mime: 'image/svg+xml',
wUnits: units(viewbox.width),
hUnits: units(viewbox.height)
}
}
+85
View File
@@ -0,0 +1,85 @@
'use strict'
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var readUInt16LE = require('../common').readUInt16LE
var readUInt16BE = require('../common').readUInt16BE
var readUInt32LE = require('../common').readUInt32LE
var readUInt32BE = require('../common').readUInt32BE
var SIG_1 = str2arr('II\x2A\0')
var SIG_2 = str2arr('MM\0\x2A')
function readUInt16 (buffer, offset, is_big_endian) {
return is_big_endian ? readUInt16BE(buffer, offset) : readUInt16LE(buffer, offset)
}
function readUInt32 (buffer, offset, is_big_endian) {
return is_big_endian ? readUInt32BE(buffer, offset) : readUInt32LE(buffer, offset)
}
function readIFDValue (data, data_offset, is_big_endian) {
var type = readUInt16(data, data_offset + 2, is_big_endian)
var values = readUInt32(data, data_offset + 4, is_big_endian)
if (values !== 1 || (type !== 3 && type !== 4)) return null
if (type === 3) {
return readUInt16(data, data_offset + 8, is_big_endian)
}
return readUInt32(data, data_offset + 8, is_big_endian)
}
module.exports = function (data) {
if (data.length < 8) return
// check TIFF signature
if (!sliceEq(data, 0, SIG_1) && !sliceEq(data, 0, SIG_2)) return
var is_big_endian = (data[0] === 77 /* 'MM' */)
var count = readUInt32(data, 4, is_big_endian) - 8
if (count < 0) return
// skip until IFD
var offset = count + 8
if (data.length - offset < 2) return
// read number of IFD entries
var ifd_size = readUInt16(data, offset + 0, is_big_endian) * 12
if (ifd_size <= 0) return
offset += 2
// read all IFD entries
if (data.length - offset < ifd_size) return
var i, width, height, tag
for (i = 0; i < ifd_size; i += 12) {
tag = readUInt16(data, offset + i, is_big_endian)
if (tag === 256) {
width = readIFDValue(data, offset + i, is_big_endian)
} else if (tag === 257) {
height = readIFDValue(data, offset + i, is_big_endian)
}
}
if (width && height) {
return {
width: width,
height: height,
type: 'tiff',
mime: 'image/tiff',
wUnits: 'px',
hUnits: 'px'
}
}
}
+108
View File
@@ -0,0 +1,108 @@
'use strict'
var str2arr = require('../common').str2arr
var sliceEq = require('../common').sliceEq
var readUInt16LE = require('../common').readUInt16LE
var readUInt32LE = require('../common').readUInt32LE
var exif = require('../exif_utils')
var SIG_RIFF = str2arr('RIFF')
var SIG_WEBP = str2arr('WEBP')
function parseVP8 (data, offset) {
if (data[offset + 3] !== 0x9D || data[offset + 4] !== 0x01 || data[offset + 5] !== 0x2A) {
// bad code block signature
return
}
return {
width: readUInt16LE(data, offset + 6) & 0x3FFF,
height: readUInt16LE(data, offset + 8) & 0x3FFF,
type: 'webp',
mime: 'image/webp',
wUnits: 'px',
hUnits: 'px'
}
}
function parseVP8L (data, offset) {
if (data[offset] !== 0x2F) return
var bits = readUInt32LE(data, offset + 1)
return {
width: (bits & 0x3FFF) + 1,
height: ((bits >> 14) & 0x3FFF) + 1,
type: 'webp',
mime: 'image/webp',
wUnits: 'px',
hUnits: 'px'
}
}
function parseVP8X (data, offset) {
return {
// TODO: replace with `data.readUIntLE(8, 3) + 1`
// when 0.10 support is dropped
width: ((data[offset + 6] << 16) | (data[offset + 5] << 8) | data[offset + 4]) + 1,
height: ((data[offset + 9] << 16) | (data[offset + 8] << 8) | data[offset + 7]) + 1,
type: 'webp',
mime: 'image/webp',
wUnits: 'px',
hUnits: 'px'
}
}
module.exports = function (data) {
if (data.length < 16) return
// check /^RIFF....WEBPVP8([ LX])$/ signature
if (!sliceEq(data, 0, SIG_RIFF) || !sliceEq(data, 8, SIG_WEBP)) return
var offset = 12
var result = null
var exif_orientation = 0
var fileLength = readUInt32LE(data, 4) + 8
if (fileLength > data.length) return
while (offset + 8 < fileLength) {
if (data[offset] === 0) {
// after each chunk of odd size there should be 0 byte of padding, skip those
offset++
continue
}
var header = String.fromCharCode.apply(null, data.slice(offset, offset + 4))
var length = readUInt32LE(data, offset + 4)
if (header === 'VP8 ' && length >= 10) {
result = result || parseVP8(data, offset + 8)
} else if (header === 'VP8L' && length >= 5) {
result = result || parseVP8L(data, offset + 8)
} else if (header === 'VP8X' && length >= 10) {
result = result || parseVP8X(data, offset + 8)
} else if (header === 'EXIF') {
exif_orientation = exif.get_orientation(data.slice(offset + 8, offset + 8 + length))
// exif is the last chunk we care about, stop after it
offset = Infinity
}
offset += 8 + length
}
if (!result) return
if (exif_orientation > 0) {
result.orientation = exif_orientation
}
return result
}
+14
View File
@@ -0,0 +1,14 @@
'use strict'
module.exports = {
avif: require('./parse_stream/avif'),
bmp: require('./parse_stream/bmp'),
gif: require('./parse_stream/gif'),
ico: require('./parse_stream/ico'),
jpeg: require('./parse_stream/jpeg'),
png: require('./parse_stream/png'),
psd: require('./parse_stream/psd'),
svg: require('./parse_stream/svg'),
tiff: require('./parse_stream/tiff'),
webp: require('./parse_stream/webp')
}
+15
View File
@@ -0,0 +1,15 @@
'use strict'
module.exports = {
avif: require('./parse_sync/avif'),
bmp: require('./parse_sync/bmp'),
gif: require('./parse_sync/gif'),
ico: require('./parse_sync/ico'),
jpeg: require('./parse_sync/jpeg'),
png: require('./parse_sync/png'),
psd: require('./parse_sync/psd'),
svg: require('./parse_sync/svg'),
tiff: require('./parse_sync/tiff'),
webp: require('./parse_sync/webp')
}