Include script for image type detection based on magic numbers

Just wanted to share this little include file that provides a couple of functions that can be used to detect the main image types from a file based on the header (magic number) of the file.

inc_commonImage.opusscriptinstall (2.4 KB)

Standard use:

var header = readBytes(imagePath, 64);
var fmt = detectFormat(header);

The object returned has two properties:

  • fmt.format : string with the format name (e.g. JPEG, PNG, WEBP, ...)
  • fmy.ext : string with the common extension for that file type (e.g. .jpg, .png, .webp, ...)

Source code:

// commonImage
// (c) 2026 Stephane

// This is an include file script for Directory Opus.
// See https://www.gpsoft.com.au/endpoints/redirect.php?page=scripts for development information.



// Called by Directory Opus to initialize the include file script
function OnInitIncludeFile(initData)
{
	initData.name = "commonImage";
	initData.version = "1.0";
	initData.copyright = "(c) 2026 Stephane";
//	initData.url = "https://resource.dopus.com/c/buttons-scripts/16";
	initData.desc = "";
	initData.min_version = "13.0";
	initData.shared = true;
}

  // --- Helpers ---
function readBytes(filePath, maxBytes) {
	var file = DOpus.FSUtil.OpenFile(filePath);
	var data = DOpus.Create.Blob();
	var sizeToRead = Math.min(maxBytes, file.size.val);

	var sizeRead = file.Read(data, sizeToRead);
	// logger.debug("Read " + sizeRead + " bytes");
	var out = [];

	for (var i = 0; i < data.size; i++) {
		out.push(data(i));
		// logger.debug("#" + i + " => " + data(i));
	}

	file.Close();
	return out;
}

function bytesToString(bytes, start, length) {
	// Convert bytes to 8-bit ASCII strings (1 char = 1 byte).
	// Used to compare text magic numbers (e.g.: "RIFF", "WEBP", "8BPS").
	var s = "";
	var end = (typeof length === "number") ? (start + length) : bytes.length;
	for (var i = start; i < end && i < bytes.length; i++) {
		s += String.fromCharCode(bytes[i] & 0xFF);
	}
	return s;
}

function matchPrefix(bytes, arr) {
	// Compares an exact binary prefix : arr = [0xFF, 0xD8, ...]
	if (!bytes || bytes.length < arr.length) return false;
	for (var i = 0; i < arr.length; i++) {
		if ((bytes[i] & 0xFF) !== arr[i]) return false;
	}
	return true;
}

function equalsAt(bytes, offset, str) {
	// Compares an ASCII string at a given offset
	if (!bytes || bytes.length < offset + str.length) return false;
	for (var i = 0; i < str.length; i++) {
		if ((bytes[offset + i] & 0xFF) !== str.charCodeAt(i)) return false;
	}
	return true;
}

var magicNumbers = DOpus.Create.Map();
magicNumbers("JPEG")				 	= {header: [0xFF, 0xD8, 0xFF], ext: ".jpg" };
magicNumbers("JPEG") 					= {header: [0xFF, 0xD8, 0xFF], ext:".jpg" };
magicNumbers("PNG") 					= {header: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], ext:".png" };
magicNumbers("TIFF (Big-endian)") 		= {header: [0x4D, 0x4D, 0x00, 0x2A], ext:".tiff" };
magicNumbers("JPEG 2000 (.jp2)") 		= {header: [0x00,0x00,0x00,0x0C,0x6A,0x50,0x20,0x20,0x0D,0x0A,0x87,0x0A], ext:".jp2" };
magicNumbers("JPEG XL (bitstream)") 	= {header: [0xFF, 0x0A], ext:".jxl" };
magicNumbers("JPEG XL (container)") 	= {header: [0x00,0x00,0x00,0x0C,0x4A,0x58,0x4C,0x20,0x0D,0x0A,0x87,0x0A], ext:".jxl" };
magicNumbers("ICO (icône Windows)") 	= {header: [0x00,0x00,0x01,0x00], ext:".ico" };
magicNumbers("CUR (curseur Windows)") 	= {header: [0x00,0x00,0x02,0x00], ext:".cur" };

var stringHeaders = DOpus.Create.Map();
stringHeaders("GIF") = {start:0, length:6, match: "GIF87a", ext: ".gif"};
stringHeaders("GIF") = {start:0, length:6, match: "GIF89a", ext: ".gif"};
stringHeaders("BMP") = {start:0, length:2, match: "BM", ext: ".bmp"};
stringHeaders("PSD (Adobe Photoshop)") = {start:0, length:4, match: "8BPS", ext: ".psd"};
stringHeaders("XCF (GIMP)") = {start:0, length:8, match: "gimp xcf", ext: ".xcf"};

function detectFormat(bytes) {
	if (!bytes || bytes.length === 0) return { format: "Unknown (empty file)", ext: "" };

	// Helper for the return values
	function res(format, ext) { return { format: format, ext: ext }; }

	// All fixed header from the magicNumbers map
	for (var e = new Enumerator(magicNumbers); !e.atEnd(); e.moveNext()) {
		var format = e.item();
		var specs = magicNumbers(format);
		if (matchPrefix(bytes, specs.header))
			return res(format, specs.ext);
	}

	// TIFF (LE / BE)
	if (matchPrefix(bytes, [0x49, 0x49, 0x2A, 0x00])) {
	// Canon CR2 : "II*\x00\x10\x00\x00\x00CR"
		var isCR2 = (bytes.length >= 12 &&
		  bytes[4] === 0x10 && bytes[5] === 0x00 &&
		  bytes[6] === 0x00 && bytes[7] === 0x00 &&
		  bytes[8] === 0x43 && bytes[9] === 0x52);
		return isCR2 ? res("Canon CR2 (TIFF LE basé)", ".cr2") : res("TIFF (Little-endian)", ".tiff");
	}
	if (matchPrefix(bytes, [0x4D, 0x4D, 0x00, 0x2A])) {
	// NEF (Nikon) is often TIFF BE ; without further check, we return .tiff
		return res("TIFF (Big-endian)", ".tiff");
	}

	for (var e = new Enumerator(stringHeaders); !e.atEnd(); e.moveNext()) {
		var format = e.item();
		var specs = stringHeaders(format);
		if (bytesToString(bytes, specs.start, specs.length) === specs.match)
			return res(format, specs.ext);
	}

	// WEBP (RIFF + 'WEBP' at offset 8)
	if (bytesToString(bytes, 0, 4) === "RIFF" && bytesToString(bytes, 8, 4) === "WEBP") {
		return res("WEBP", ".webp");
	}

	// ISO BMFF: 'ftyp' at offset 4 -> HEIC/HEIF/AVIF, etc.
	if (bytes.length >= 12 && bytesToString(bytes, 4, 4) === "ftyp") {
		var major = bytesToString(bytes, 8, 4);
		var heifBrands = { "heic":1, "heix":1, "heim":1, "heis":1, "hevc":1, "mif1":1, "msf1":1 };
		var avifBrands = { "avif":1, "av01":1, "avis":1 };

		if (heifBrands[major]) {
		  // HEIC (HEVC) vs HEIF generic
			if (major === "heic" || major === "heix" || major === "heim" || major === "heis" || major === "hevc") {
				return res("HEIC (HEIF/ISO-BMFF)", ".heic");
			}
			return res("HEIF (ISO-BMFF)", ".heif");
		}
		if (avifBrands[major]) {
			return res("AVIF (ISO-BMFF)", ".avif");
		}
		// Other ISO-BMFF (probably not a standard image)
		return res("ISO-BMFF (ftyp: " + major + ")", "");
	}

	return res("Unknwon signature", "");
}
1 Like