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
+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
}