// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Utility which gives a best effort guess on whether a supplied image file's
* bytes represent an image with transparency.
*/
export function checkTransparency(buffer: ArrayBuffer): boolean {
const view = new DataView(buffer);
return isTransparentPNG(view) || isTransparentBMP(view) ||
isTransparentWebP(view);
}
/**
* Safely gets Uint8 value from DataView.
*
* Swallows potential RangeErrors, opting to return null in those cases.
*/
function getUint8FromView(view: DataView, offset: number): number|null {
try {
return view.getUint8(offset);
} catch {
return null;
}
}
/**
* Safely gets Uint16 value from DataView.
*
* Swallows potential RangeErrors, opting to return null in those cases.
*/
function getUint16FromView(view: DataView, offset: number): number|null {
try {
return view.getUint16(offset);
} catch {
return null;
}
}
/**
* Safely gets Uint32 value from DataView.
*
* Swallows potential RangeErrors, opting to return null in those cases.
*/
function getUint32FromView(view: DataView, offset: number): number|null {
try {
return view.getUint32(offset);
} catch {
return null;
}
}
/**
* Whether a DataView represents a PNG image.
*/
export function isPNG(view: DataView): boolean {
// 89 50 4E 47 is PNG magic number.
// Next four bytes should always be 0D 0A 1A 0A.
return getUint32FromView(view, 0) === 0x89504E47 &&
getUint32FromView(view, 4) === 0x0D0A1A0A;
}
function isTransparentPNG(view: DataView): boolean {
if (!isPNG(view)) {
return false;
}
// We know format field exists in the IHDR chunk. The chunk exists at
// offset 8 +8 bytes (size, name) +8 (depth) & +9 (type) = 25 byte offset.
const type = getUint8FromView(view, 25);
return type === 0x04 || type === 0x06; // grayscale + alpha or RGB + alpha
}
/**
* Whether a DataView represents a WebP image.
*/
export function isWebP(view: DataView): boolean {
// 52 49 46 46 || <ignore 4 bytes> || 57 45 42 50 is the WebP magic number.
// R I F F || <ignore 4 bytes> || W E B P in ASCII.
// https://developers.google.com/speed/webp/docs/riff_container#webp_file_header
return getUint32FromView(view, 0) === 0x52494646 &&
getUint32FromView(view, 8) === 0x57454250;
}
/**
* Whether a DataView represents a WebP image.
* Checks WebP format (VP8 | VP8L | VP8X). VP8 never has an alpha channel. We
* make an assumption that VP8X and VP8L always have alpha channels. That is
* not true, but does simplify the logic and for practical purposes, this is
* good enough of an assumption for us.
*/
function isTransparentWebP(view: DataView): boolean {
if (!isWebP(view)) {
return false;
}
// Fourteenth byte indicates WebP format. There are three variations of WebP
// encoding: VP8, VP8L, and VP8X.
//
// Offset: 12 13 14 15
// ASCII: V P 8 ? <- ? == " " || "L" || "X"
// Hex: 56 50 38 ? <- ? == 0x20 || 0x4C || 0x58
const format = getUint8FromView(view, 15);
// 58 indicates VP8X, 4C indicates VP8L.
return format === 0x58 || format === 0x4C;
}
/**
* Whether a DataView represents a BMP image.
*/
export function isBMP(view: DataView): boolean {
// 42 4D is the BMP magic number.
return getUint16FromView(view, 0) === 0x424D;
}
function isTransparentBMP(view: DataView): boolean {
if (!isBMP(view)) {
return false;
}
// Check the value of the bit count field, 2 bytes, 28 byte offset.
return getUint16FromView(view, 28) === 0x32;
}