image-comparator
Advanced tools
Comparing version 1.1.0 to 1.2.0
22
index.js
@@ -7,15 +7,25 @@ const { | ||
extractBitmapAndChannelCount, | ||
MIMES, | ||
} = require("./lib"); | ||
module.exports = { | ||
compare: async (buffer1, buffer2) => | ||
areBuffersEqual( | ||
compare: async (buffer1, buffer2) => { | ||
const isInterformatComparison = | ||
buffer1[0] === MIMES.WEBP && buffer1[0] !== buffer2[0]; | ||
if (isInterformatComparison) { | ||
throw new Error("Interformat webp comparison not supported"); | ||
} | ||
return areBuffersEqual( | ||
...(await Promise.all([compareImpl(buffer1), compareImpl(buffer2)])), | ||
BYTE_COMPARERS[buffer1.at(0)], | ||
BYTE_COMPARERS[buffer2.at(0)] | ||
), | ||
BYTE_COMPARERS[buffer1[0]], | ||
BYTE_COMPARERS[buffer2[0]], | ||
buffer1[0] | ||
); | ||
}, | ||
}; | ||
const compareImpl = async (buffer) => { | ||
const { width, height } = getDimensions[buffer.at(0)](buffer); | ||
const { width, height } = getDimensions[buffer[0]](buffer); | ||
@@ -22,0 +32,0 @@ const { channels, data: bitmap } = await extractBitmapAndChannelCount(buffer); |
@@ -1,2 +0,2 @@ | ||
const { HIT_THRESHOLD } = require("./enums"); | ||
const { HIT_THRESHOLD, MIMES } = require("./enums"); | ||
@@ -8,3 +8,4 @@ // https://stackoverflow.com/a/21554107 | ||
comparerOfBuffer1, | ||
comparerOfBuffer2 | ||
comparerOfBuffer2, | ||
mime | ||
) => { | ||
@@ -14,2 +15,18 @@ const view1 = new Int8Array(buffer1); | ||
if (mime === MIMES.WEBP) { | ||
const total = 16; | ||
let hits = 0; | ||
for (let i = 0; i < total; i++) { | ||
if ( | ||
comparerOfBuffer1(view1[i], view2[i]) || | ||
comparerOfBuffer2(view1[i], view2[i]) | ||
) { | ||
hits++; | ||
} | ||
} | ||
return hits / total >= 0.7; | ||
} | ||
if (buffer1.byteLength != buffer2.byteLength) { | ||
@@ -16,0 +33,0 @@ let hits = 0; |
const MIMES = { | ||
PNG: 0x89, | ||
JPG: 0xff, | ||
WEBP: 0x52, | ||
}; | ||
@@ -9,2 +10,3 @@ | ||
[MIMES.JPG]: (byte1, byte2) => Math.abs(byte1 - byte2) < 30, | ||
[MIMES.WEBP]: (byte1, byte2) => Math.abs(byte1 - byte2) < 30, | ||
}; | ||
@@ -11,0 +13,0 @@ |
@@ -17,3 +17,9 @@ const { | ||
const { toUTF8String, toHexString } = require("./formatters"); | ||
const { readUInt32BE, readUInt, readUInt16BE } = require("./readers"); | ||
const { | ||
readUInt32BE, | ||
readUInt, | ||
readUInt16BE, | ||
readInt16LE, | ||
readUInt24LE, | ||
} = require("./readers"); | ||
@@ -103,2 +109,27 @@ function isEXIF(input) { | ||
function calculateExtended(input) { | ||
return { | ||
height: 1 + readUInt24LE(input, 7), | ||
width: 1 + readUInt24LE(input, 4), | ||
}; | ||
} | ||
function calculateLossless(input) { | ||
return { | ||
height: | ||
1 + | ||
(((input[4] & 0xf) << 10) | (input[3] << 2) | ((input[2] & 0xc0) >> 6)), | ||
width: 1 + (((input[2] & 0x3f) << 8) | input[1]), | ||
}; | ||
} | ||
function calculateLossy(input) { | ||
// `& 0x3fff` returns the last 14 bits | ||
// TO-DO: include webp scaling in the calculations | ||
return { | ||
height: readInt16LE(input, 8) & 0x3fff, | ||
width: readInt16LE(input, 6) & 0x3fff, | ||
}; | ||
} | ||
const getDimensions = { | ||
@@ -158,2 +189,33 @@ // https://github.com/image-size/image-size/blob/08389eb1d6804485a3ae5964dfe827df885b0c54/lib/types/png.ts#L25 | ||
}, | ||
// https://github.com/image-size/image-size/blob/08389eb1d6804485a3ae5964dfe827df885b0c54/lib/types/webp.ts#L38 | ||
[MIMES.WEBP]: (input) => { | ||
const chunkHeader = toUTF8String(input, 12, 16); | ||
input = input.slice(20, 30); | ||
// Extended webp stream signature | ||
if (chunkHeader === "VP8X") { | ||
const extendedHeader = input[0]; | ||
const validStart = (extendedHeader & 0xc0) === 0; | ||
const validEnd = (extendedHeader & 0x01) === 0; | ||
if (validStart && validEnd) { | ||
return calculateExtended(input); | ||
} else { | ||
// TODO: breaking change | ||
throw new TypeError("Invalid WebP"); | ||
} | ||
} | ||
// Lossless webp stream signature | ||
if (chunkHeader === "VP8 " && input[0] !== 0x2f) { | ||
return calculateLossy(input); | ||
} | ||
// Lossy webp stream signature | ||
const signature = toHexString(input, 3, 6); | ||
if (chunkHeader === "VP8L" && signature !== "9d012a") { | ||
return calculateLossless(input); | ||
} | ||
throw new TypeError("Invalid WebP"); | ||
}, | ||
}; | ||
@@ -160,0 +222,0 @@ |
const Resize = require("./Resize"); | ||
const { MIMES } = require("./enums"); | ||
const parseJpg = require("./jpgparse"); | ||
const { WebPDecoder, WebPRiffParser } = require("./webpparse"); | ||
const { parse: parsePng } = require("./pngparse"); | ||
const parseJpg = require("./jpgparse"); | ||
const parseMapper = { | ||
[0x89]: (buffer) => | ||
[MIMES.PNG]: (buffer) => | ||
new Promise((resolve, reject) => { | ||
@@ -16,7 +18,24 @@ parsePng(buffer, (error, bitmap) => { | ||
}), | ||
[0xff]: parseJpg, | ||
[MIMES.JPG]: parseJpg, | ||
[MIMES.WEBP]: async (buffer) => { | ||
const { | ||
frames: [{ src_off: srcOff, src_size: srcSize }], | ||
} = WebPRiffParser(buffer, 0); | ||
const decoder = new WebPDecoder(); | ||
const dimensions = { width: [], height: [] }; | ||
return { | ||
data: decoder.WebPDecodeRGBA( | ||
buffer, | ||
srcOff, | ||
srcSize, | ||
dimensions.width, | ||
dimensions.height | ||
), | ||
channels: 4, | ||
}; | ||
}, | ||
}; | ||
const extractBitmapAndChannelCount = (buffer) => | ||
parseMapper[buffer.at(0)](buffer); | ||
const extractBitmapAndChannelCount = (buffer) => parseMapper[buffer[0]](buffer); | ||
@@ -23,0 +42,0 @@ module.exports = { |
@@ -12,2 +12,7 @@ // github.com/image-size/image-size/blob/08389eb1d6804485a3ae5964dfe827df885b0c54/lib/types/utils.ts | ||
const readInt16LE = (input, offset = 0) => { | ||
const val = input[offset] + input[offset + 1] * 2 ** 8; | ||
return val | ((val & (2 ** 15)) * 0x1fffe); | ||
}; | ||
const readUInt = (input, bits, offset, isBigEndian) => { | ||
@@ -20,6 +25,11 @@ offset = offset || 0; | ||
const readUInt24LE = (input, offset = 0) => | ||
input[offset] + input[offset + 1] * 2 ** 8 + input[offset + 2] * 2 ** 16; | ||
module.exports = { | ||
readUInt, | ||
readInt16LE, | ||
readUInt24LE, | ||
readUInt32BE, | ||
readUInt16BE, | ||
}; |
{ | ||
"name": "image-comparator", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"description": "Compares images by resizing without dependencies", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -62,2 +62,28 @@ const assert = require("node:assert"); | ||
describe("webp", () => { | ||
const commonJpgImagesPath = [...commonImagesPath, "webp"]; | ||
it("should return false for different webp images", async () => { | ||
const expected = false; | ||
const commonPathSegments = [...commonJpgImagesPath, "different"]; | ||
const image1 = readFileSync(join(...commonPathSegments, "image1.webp")); | ||
const image2 = readFileSync(join(...commonPathSegments, "image2.webp")); | ||
const actual = await compare(image1, image2); | ||
assert.strictEqual(actual, expected); | ||
}); | ||
it("should return true for same webp images", async () => { | ||
const expected = true; | ||
const commonPathSegments = [...commonJpgImagesPath, "same"]; | ||
const image1 = readFileSync(join(...commonPathSegments, "image1.webp")); | ||
const image2 = readFileSync(join(...commonPathSegments, "image2.webp")); | ||
const actual = await compare(image1, image2); | ||
assert.strictEqual(actual, expected); | ||
}); | ||
}); | ||
describe("mixins", () => { | ||
@@ -94,2 +120,16 @@ const commonPngImagesPath = [...commonImagesPath, "png"]; | ||
}); | ||
it("should throw on interformat webp comparison (not implemented)", async () => { | ||
const expected = Error; | ||
const jpgImage = readFileSync( | ||
join(...commonImagesPath, "webp", "same", "image1.webp") | ||
); | ||
const pngImage = readFileSync( | ||
join(...commonImagesPath, "mixins", "image.png") | ||
); | ||
const actual = () => compare(jpgImage, pngImage); | ||
assert.rejects(actual, expected); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 4 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
177675
30
3026
4