Skip to content

Commit

Permalink
Add exif support for webp
Browse files Browse the repository at this point in the history
  • Loading branch information
rlidwka committed Mar 10, 2021
1 parent b6460cd commit 196274a
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 80 deletions.
3 changes: 1 addition & 2 deletions known_issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

- images where metadata is encoded after image data are not supported (not recommended by standard)
- cropping transformation (`clap` box) is not supported (no supported in browsers yet)
- ??? exif orientation

### JPEG

Expand All @@ -14,4 +13,4 @@

### WEBP

- ??? exif orientation
- webp files will be downloaded and parsed until the end of the file
178 changes: 127 additions & 51 deletions lib/parse_stream/webp.js
Original file line number Diff line number Diff line change
@@ -1,99 +1,175 @@
'use strict';

/* eslint-disable no-bitwise */
/* eslint-disable no-use-before-define */

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_WEBPVP8 = str2arr('WEBPVP8');
var SIG_RIFF = str2arr('RIFF');
var SIG_WEBP = str2arr('WEBP');


function parseVP8(parser) {
parser._bytes(14, function (data) {
parser._skipBytes(Infinity);
function safeSkip(parser, count, callback) {
if (count === 0) { // parser._skipBytes throws error if count === 0
callback();
return;
}

if (data[7] !== 0x9D || data[8] !== 0x01 || data[9] !== 0x2A) {
// bad code block signature
parser.push(null);
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'
};
}

parser.push({
width: data.readUInt16LE(10) & 0x3FFF,
height: data.readUInt16LE(12) & 0x3FFF,
type: 'webp',
mime: 'image/webp',
wUnits: 'px',
hUnits: 'px'
safeSkip(parser, length - 10, function () {
sandbox.offset += length;
getWebpSize(parser, sandbox);
});

parser.push(null);
});
}


function parseVP8L(parser) {
parser._bytes(9, function (data) {
parser._skipBytes(Infinity);

if (data[4] !== 0x2F) {
// bad code block signature
parser.push(null);
return;
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'
};
}

var bits = data.readUInt32LE(5);
safeSkip(parser, length - 5, function () {
sandbox.offset += length;
getWebpSize(parser, sandbox);
});
});
}


parser.push({
width: (bits & 0x3FFF) + 1,
height: ((bits >> 14) & 0x3FFF) + 1,
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);
});
});
}

parser.push(null);

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 parseVP8X(parser) {
parser._bytes(14, function (data) {
function getWebpSize(parser, sandbox) {
if (sandbox.fileLength - 8 <= sandbox.offset) {
parser._skipBytes(Infinity);

parser.push({
// TODO: replace with `data.readUIntLE(8, 3) + 1`
// when 0.10 support is dropped
width: ((data[10] << 16) | (data[9] << 8) | data[8]) + 1,
height: ((data[13] << 16) | (data[12] << 8) | data[11]) + 1,
type: 'webp',
mime: 'image/webp',
wUnits: 'px',
hUnits: 'px'
});
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(16, function (data) {
parser._bytes(12, function (data) {

// check /^RIFF....WEBPVP8([ LX])$/ signature

if (sliceEq(data, 0, SIG_RIFF) && sliceEq(data, 8, SIG_WEBPVP8)) {
switch (data[15]) {
case 32/*' '*/: parseVP8(parser); return;
case 76/* L */: parseVP8L(parser); return;
case 88/* X */: parseVP8X(parser); return;
}
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);
Expand Down
76 changes: 53 additions & 23 deletions lib/parse_sync/webp.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,22 @@ 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_WEBPVP8 = str2arr('WEBPVP8');
var SIG_RIFF = str2arr('RIFF');
var SIG_WEBP = str2arr('WEBP');


function parseVP8(data) {
if (data.length < 16 + 14) return;

if (data[16 + 7] !== 0x9D || data[16 + 8] !== 0x01 || data[16 + 9] !== 0x2A) {
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, 16 + 10) & 0x3FFF,
height: readUInt16LE(data, 16 + 12) & 0x3FFF,
width: readUInt16LE(data, offset + 6) & 0x3FFF,
height: readUInt16LE(data, offset + 8) & 0x3FFF,
type: 'webp',
mime: 'image/webp',
wUnits: 'px',
Expand All @@ -32,12 +31,10 @@ function parseVP8(data) {
}


function parseVP8L(data) {
if (data.length < 16 + 9) return;

if (data[16 + 4] !== 0x2F) return;
function parseVP8L(data, offset) {
if (data[offset] !== 0x2F) return;

var bits = readUInt32LE(data, 16 + 5);
var bits = readUInt32LE(data, offset + 1);

return {
width: (bits & 0x3FFF) + 1,
Expand All @@ -50,14 +47,12 @@ function parseVP8L(data) {
}


function parseVP8X(data) {
if (data.length < 16 + 14) return;

function parseVP8X(data, offset) {
return {
// TODO: replace with `data.readUIntLE(8, 3) + 1`
// when 0.10 support is dropped
width: ((data[16 + 10] << 16) | (data[16 + 9] << 8) | data[16 + 8]) + 1,
height: ((data[16 + 13] << 16) | (data[16 + 12] << 8) | data[16 + 11]) + 1,
width: ((data[offset + 6] << 16) | (data[offset + 5] << 8) | data[offset + 4]) + 1,
height: ((data[offset + 9] << offset) | (data[offset + 8] << 8) | data[offset + 7]) + 1,
type: 'webp',
mime: 'image/webp',
wUnits: 'px',
Expand All @@ -70,11 +65,46 @@ module.exports = function (data) {
if (data.length < 16) return;

// check /^RIFF....WEBPVP8([ LX])$/ signature
if (sliceEq(data, 0, SIG_RIFF) && sliceEq(data, 8, SIG_WEBPVP8)) {
switch (data[15]) {
case 32/*' '*/: return parseVP8(data);
case 76/* L */: return parseVP8L(data);
case 88/* X */: return parseVP8X(data);
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 >= 9) {
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;
};
Binary file added test/fixtures/Exif5-1.5x.webp
Binary file not shown.
Loading

0 comments on commit 196274a

Please sign in to comment.