Task 329: .JP2 File Format
Task 329: .JP2 File Format
File Format Specifications for .JP2
The .JP2 file format is defined in ISO/IEC 15444-1 (JPEG 2000 Part 1: Core coding system), which specifies a box-based container for JPEG 2000 compressed image data. It supports wavelet-based compression (lossy or lossless), metadata embedding, and scalability. The format uses a modular structure with nested boxes, each starting with a size and type field. Mandatory elements include a signature box, file type box, header superbox, and codestream box. Optional boxes allow for color profiles, resolution info, XML metadata, and more.
List of All Properties Intrinsic to the .JP2 File Format
Based on the box structure, the following are the key intrinsic properties (metadata and structural fields) embedded in the file format. These are derived from standard boxes and do not include filesystem attributes like creation date or permissions. Properties are grouped by box for clarity:
Signature Box ('jP '):
- Signature Value: Fixed sequence (e.g., 0x0D0A870A).
File Type Box ('ftyp'):
- Brand: Primary compatibility brand (e.g., 'jp2 ').
- Minor Version: Version number (usually 0).
- Compatibility List: List of compatible brands (e.g., ['jp2 ']).
JP2 Header Superbox ('jp2h'):
- Image Header Box ('ihdr'):
- Image Height: Unsigned integer (1 to 2^32-1).
- Image Width: Unsigned integer (1 to 2^32-1).
- Number of Components: Unsigned short (1 to 16384).
- Bits Per Component: Byte (0-37 for uniform bits-1, 255 if variable; MSB indicates signed if set).
- Compression Type: Byte (7 for JPEG 2000).
- Colorspace Unknown: Boolean flag (0=known, 1=unknown).
- Intellectual Property Present: Boolean flag (0=none, 1=present in 'jp2i' box).
- Colour Specification Box ('colr') (one or more):
- Method: Byte (1=enumerated colorspace, 2=restricted ICC profile, 3=any ICC profile, 4=vendor-specific).
- Precedence: Byte (priority for multiple 'colr' boxes).
- Approximation: Byte (color matching accuracy level).
- Enumerated Colorspace: Unsigned integer (if method=1; e.g., 0=greyscale, 16=sRGB, 17=sYCC).
- ICC Profile: Binary data (if method=2 or 3; length variable).
- Bits Per Component Box ('bpcc') (if variable bits):
- Bits Per Component List: Array of bytes (one per component, value = bits-1).
- Palette Box ('pclr') (optional):
- Number of Entries: Unsigned short (1 to 1024).
- Number of Palette Components: Byte (1 to 255).
- Bit Depths: Array of bytes (one per palette component, value = bits-1, MSB for signed).
- Palette Values: Signed/unsigned integers (variable size per entry).
- Component Mapping Box ('cmap') (optional, for palettes):
- Number of Mappings: Implied by box size.
- Component Index: Unsigned short per mapping.
- Mapping Type: Byte per mapping (0=direct, 1=palette).
- Palette Column: Byte per mapping (if type=1).
- Channel Definition Box ('cdef') (optional):
- Number of Channels: Unsigned short.
- Channel Index: Unsigned short per channel.
- Channel Type: Unsigned short per channel (0=color, 1=opacity, 2=premultiplied opacity, 65535=unspecified).
- Association: Unsigned short per channel (component or palette index).
- Resolution Superbox ('res ') (optional):
- Capture Resolution Box ('resc'):
- Vertical Numerator: Unsigned short.
- Vertical Denominator: Unsigned short.
- Horizontal Numerator: Unsigned short.
- Horizontal Denominator: Unsigned short.
- Vertical Exponent: Signed byte.
- Horizontal Exponent: Signed byte.
- Capture Resolution Vertical: Computed as (num/denom) * 10^exp pixels per meter.
- Capture Resolution Horizontal: Similar computation.
- Display Resolution Box ('resd'):
- Same fields as 'resc' but for default display resolution.
Contiguous Codestream Box ('jp2c'):
- Codestream Length: Derived from box size.
- Codestream Start Marker: Fixed (0xFF4F for SOC).
Optional Boxes:
- Intellectual Property Box ('jp2i'): Binary IP rights data.
- XML Box ('xml '): Well-formed XML string (e.g., for Exif/XMP metadata).
- UUID Box ('uuid'): 16-byte UUID + binary data.
- UUID Info Superbox ('uinf'):
- UUID List Box ('ulst'): Number of UUIDs + list of 16-byte UUIDs.
- Data Reference Box ('url '): Version + flag + URL string.
These properties represent the core and optional metadata intrinsic to the format's structure.
Two Direct Download Links for .JP2 Files
- https://raw.githubusercontent.com/openpreserve/jpylyzer-test-files/main/reference.jp2
- https://raw.githubusercontent.com/openpreserve/jpylyzer-test-files/main/kakadu61.jp2
Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .JP2 Property Dump
This is a self-contained HTML snippet with JavaScript that can be embedded in a Ghost blog post. It creates a drop zone for a .JP2 file, parses it using DataView, extracts the properties listed in part 1, and displays them on the screen in a pre-formatted text area.
Python Class for .JP2 Handling
This class opens a .JP2 file, decodes its boxes, reads the properties, prints them to console, and can write the parsed data back to a new file (copying the structure).
import struct
import os
class JP2Parser:
def __init__(self, filepath):
self.filepath = filepath
self.data = None
self.properties = {}
self.decode()
def decode(self):
with open(self.filepath, 'rb') as f:
self.data = f.read()
offset = 0
while offset < len(self.data):
size, = struct.unpack('>I', self.data[offset:offset+4])
offset += 4
type_ = self.data[offset:offset+4].decode('ascii', errors='ignore')
offset += 4
data_start = offset
if size == 1:
size, = struct.unpack('>Q', self.data[offset:offset+8])
offset += 8
data_start = offset
elif size == 0:
size = len(self.data) - (data_start - 8)
data_end = data_start + size - 8
if type_ == 'jP ':
self.properties['Signature Value'] = '0x{:X}'.format(struct.unpack('>I', self.data[offset:offset+4])[0])
elif type_ == 'ftyp':
self.properties['Brand'] = self.data[offset:offset+4].decode('ascii')
self.properties['Minor Version'] = struct.unpack('>I', self.data[offset+4:offset+8])[0]
self.properties['Compatibility List'] = []
for i in range(offset+8, data_end, 4):
self.properties['Compatibility List'].append(self.data[i:i+4].decode('ascii'))
elif type_ == 'jp2h':
# Recurse subboxes
sub_parser = JP2Parser('')
sub_parser.data = self.data[data_start:data_end]
sub_parser.decode()
self.properties.update(sub_parser.properties)
elif type_ == 'ihdr':
height, width = struct.unpack('>II', self.data[offset:offset+8])
nc, = struct.unpack('>H', self.data[offset+8:offset+10])
bpc, c, unkC, unkIPR = struct.unpack('>BBBB', self.data[offset+10:offset+14])
self.properties['Image Height'] = height
self.properties['Image Width'] = width
self.properties['Number of Components'] = nc
self.properties['Bits Per Component'] = bpc
self.properties['Compression Type'] = c
self.properties['Colorspace Unknown'] = unkC
self.properties['Intellectual Property Present'] = unkIPR
elif type_ == 'colr':
method, prec, approx = struct.unpack('>BBB', self.data[offset:offset+3])
self.properties['Color Method'] = method
self.properties['Color Precedence'] = prec
self.properties['Color Approximation'] = approx
if method == 1:
self.properties['Enumerated Colorspace'] = struct.unpack('>I', self.data[offset+3:offset+7])[0]
elif method in [2, 3]:
self.properties['ICC Profile Length'] = size - 11
elif type_ == 'res ':
# Recurse
sub_parser = JP2Parser('')
sub_parser.data = self.data[data_start:data_end]
sub_parser.decode()
self.properties.update(sub_parser.properties)
elif type_ in ['resc', 'resd']:
prefix = 'Capture' if type_ == 'resc' else 'Display'
vrn, vrd, hrn, hrd = struct.unpack('>HHHH', self.data[offset:offset+8])
vre, hre = struct.unpack('>bb', self.data[offset+8:offset+10])
self.properties[f'{prefix} Resolution Vertical'] = (vrn / vrd) * (10 ** vre)
self.properties[f'{prefix} Resolution Horizontal'] = (hrn / hrd) * (10 ** hre)
elif type_ == 'jp2c':
self.properties['Codestream Length'] = size - 8
elif type_ == 'xml ':
self.properties['XML Metadata'] = self.data[offset:data_end].decode('utf-8', errors='ignore')
# Add more parsers for pclr, cmap, cdef, etc., as needed
offset = data_end
self.print_properties()
def print_properties(self):
for key, value in self.properties.items():
print(f'{key}: {value}')
def write(self, output_path):
if self.data:
with open(output_path, 'wb') as f:
f.write(self.data)
print(f'File written to {output_path}')
# Usage: parser = JP2Parser('example.jp2'); parser.write('output.jp2')
Java Class for .JP2 Handling
This class opens a .JP2 file, decodes boxes using ByteBuffer, reads properties, prints to console, and can write the data to a new file.
import java.io.*;
import java.nio.*;
import java.util.*;
public class JP2Parser {
private String filepath;
private byte[] data;
private Map<String, Object> properties = new HashMap<>();
public JP2Parser(String filepath) {
this.filepath = filepath;
decode();
}
private void decode() {
try (FileInputStream fis = new FileInputStream(filepath)) {
data = fis.readAllBytes();
} catch (IOException e) {
e.printStackTrace();
return;
}
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
int offset = 0;
while (offset < data.length) {
long size = bb.getInt(offset) & 0xFFFFFFFFL;
offset += 4;
String type = new String(data, offset, 4);
offset += 4;
int dataStart = offset;
if (size == 1) {
size = bb.getLong(offset);
offset += 8;
dataStart = offset;
} else if (size == 0) {
size = data.length - (dataStart - 8);
}
int dataEnd = (int)(dataStart + size - 8);
if (type.equals("jP ")) {
properties.put("Signature Value", "0x" + Integer.toHexString(bb.getInt(offset)).toUpperCase());
} else if (type.equals("ftyp")) {
properties.put("Brand", new String(data, offset, 4));
properties.put("Minor Version", bb.getInt(offset + 4));
List<String> compat = new ArrayList<>();
for (int i = offset + 8; i < dataEnd; i += 4) {
compat.add(new String(data, i, 4));
}
properties.put("Compatibility List", compat);
} else if (type.equals("jp2h") || type.equals("res ")) {
// Recurse
byte[] subData = Arrays.copyOfRange(data, dataStart, dataEnd);
JP2Parser sub = new JP2Parser("");
sub.data = subData;
sub.decode();
properties.putAll(sub.properties);
} else if (type.equals("ihdr")) {
properties.put("Image Height", bb.getInt(offset));
properties.put("Image Width", bb.getInt(offset + 4));
properties.put("Number of Components", bb.getShort(offset + 8) & 0xFFFF);
properties.put("Bits Per Component", data[offset + 10] & 0xFF);
properties.put("Compression Type", data[offset + 11] & 0xFF);
properties.put("Colorspace Unknown", data[offset + 12] & 0xFF);
properties.put("Intellectual Property Present", data[offset + 13] & 0xFF);
} else if (type.equals("colr")) {
int method = data[offset] & 0xFF;
properties.put("Color Method", method);
properties.put("Color Precedence", data[offset + 1] & 0xFF);
properties.put("Color Approximation", data[offset + 2] & 0xFF);
if (method == 1) {
properties.put("Enumerated Colorspace", bb.getInt(offset + 3));
} else if (method == 2 || method == 3) {
properties.put("ICC Profile Length", (int)size - 11);
}
} else if (type.equals("resc") || type.equals("resd")) {
String prefix = type.equals("resc") ? "Capture" : "Display";
int vrn = bb.getShort(offset) & 0xFFFF, vrd = bb.getShort(offset + 2) & 0xFFFF;
int hrn = bb.getShort(offset + 4) & 0xFFFF, hrd = bb.getShort(offset + 6) & 0xFFFF;
byte vre = data[offset + 8], hre = data[offset + 9];
properties.put(prefix + " Resolution Vertical", (vrn / (double)vrd) * Math.pow(10, vre));
properties.put(prefix + " Resolution Horizontal", (hrn / (double)hrd) * Math.pow(10, hre));
} else if (type.equals("jp2c")) {
properties.put("Codestream Length", size - 8);
} else if (type.equals("xml ")) {
properties.put("XML Metadata", new String(data, offset, (int)size - 8));
} // Add more for other boxes
offset = dataEnd;
}
printProperties();
}
public void printProperties() {
properties.forEach((k, v) -> System.out.println(k + ": " + v));
}
public void write(String outputPath) {
if (data != null) {
try (FileOutputStream fos = new FileOutputStream(outputPath)) {
fos.write(data);
System.out.println("File written to " + outputPath);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// For sub-parsing without file
private JP2Parser(String dummy) {}
}
// Usage: new JP2Parser("example.jp2").write("output.jp2");
JavaScript Class for .JP2 Handling
This class (for Node.js) opens a .JP2 file, decodes with Buffer/DataView, reads properties, prints to console, and can write to a new file.
const fs = require('fs');
class JP2Parser {
constructor(filepath) {
this.filepath = filepath;
this.data = null;
this.properties = {};
this.decode();
}
decode() {
this.data = fs.readFileSync(this.filepath);
const view = new DataView(this.data.buffer);
let offset = 0;
while (offset < this.data.length) {
let size = view.getUint32(offset);
offset += 4;
const type = String.fromCharCode(view.getUint8(offset), view.getUint8(offset+1), view.getUint8(offset+2), view.getUint8(offset+3));
offset += 4;
let dataStart = offset;
if (size === 1) {
size = Number(view.getBigUint64(offset));
offset += 8;
dataStart = offset;
} else if (size === 0) {
size = this.data.length - (dataStart - 8);
}
const dataEnd = dataStart + size - 8;
if (type === 'jP ') {
this.properties['Signature Value'] = '0x' + view.getUint32(offset).toString(16).toUpperCase();
} else if (type === 'ftyp') {
this.properties['Brand'] = this.getString(offset, 4);
this.properties['Minor Version'] = view.getUint32(offset + 4);
this.properties['Compatibility List'] = [];
for (let i = offset + 8; i < dataEnd; i += 4) {
this.properties['Compatibility List'].push(this.getString(i, 4));
}
} else if (type === 'jp2h' || type === 'res ') {
// Recurse
const subView = new DataView(this.data.buffer.slice(dataStart, dataEnd));
const subParser = new JP2Parser('');
subParser.data = this.data.slice(dataStart, dataEnd);
subParser.decode(subView);
Object.assign(this.properties, subParser.properties);
} else if (type === 'ihdr') {
this.properties['Image Height'] = view.getUint32(offset);
this.properties['Image Width'] = view.getUint32(offset + 4);
this.properties['Number of Components'] = view.getUint16(offset + 8);
this.properties['Bits Per Component'] = view.getUint8(offset + 10);
this.properties['Compression Type'] = view.getUint8(offset + 11);
this.properties['Colorspace Unknown'] = view.getUint8(offset + 12);
this.properties['Intellectual Property Present'] = view.getUint8(offset + 13);
} else if (type === 'colr') {
const method = view.getUint8(offset);
this.properties['Color Method'] = method;
this.properties['Color Precedence'] = view.getUint8(offset + 1);
this.properties['Color Approximation'] = view.getUint8(offset + 2);
if (method === 1) {
this.properties['Enumerated Colorspace'] = view.getUint32(offset + 3);
} else if (method === 2 || method === 3) {
this.properties['ICC Profile Length'] = size - 11;
}
} else if (type === 'resc' || type === 'resd') {
const prefix = type === 'resc' ? 'Capture' : 'Display';
const vrn = view.getUint16(offset), vrd = view.getUint16(offset+2);
const hrn = view.getUint16(offset+4), hrd = view.getUint16(offset+6);
const vre = view.getInt8(offset+8), hre = view.getInt8(offset+9);
this.properties[`${prefix} Resolution Vertical`] = (vrn / vrd) * (10 ** vre);
this.properties[`${prefix} Resolution Horizontal`] = (hrn / hrd) * (10 ** hre);
} else if (type === 'jp2c') {
this.properties['Codestream Length'] = size - 8;
} else if (type === 'xml ') {
this.properties['XML Metadata'] = this.getString(offset, size - 8);
} // Add more
offset = dataEnd;
}
this.printProperties();
}
getString(start, len) {
let str = '';
const view = new DataView(this.data.buffer);
for (let i = 0; i < len; i++) str += String.fromCharCode(view.getUint8(start + i));
return str.trim();
}
printProperties() {
for (const [key, value] of Object.entries(this.properties)) {
console.log(`${key}: ${JSON.stringify(value)}`);
}
}
write(outputPath) {
if (this.data) {
fs.writeFileSync(outputPath, this.data);
console.log(`File written to ${outputPath}`);
}
}
}
// Usage: const parser = new JP2Parser('example.jp2'); parser.write('output.jp2');
C "Class" (Struct with Functions) for .JP2 Handling
This uses a struct and functions to open, decode, read/print properties, and write to a new file. Compile with a C compiler.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <math.h>
typedef struct {
char* filepath;
uint8_t* data;
size_t data_size;
// Use a simple array for properties (key-value pairs)
char* keys[100];
char* values[100];
int prop_count;
} JP2Parser;
void init_parser(JP2Parser* parser, const char* filepath) {
parser->filepath = strdup(filepath);
parser->data = NULL;
parser->prop_count = 0;
FILE* f = fopen(filepath, "rb");
if (!f) return;
fseek(f, 0, SEEK_END);
parser->data_size = ftell(f);
fseek(f, 0, SEEK_SET);
parser->data = malloc(parser->data_size);
fread(parser->data, 1, parser->data_size, f);
fclose(f);
}
uint32_t get_u32_be(uint8_t* buf) {
return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
}
uint64_t get_u64_be(uint8_t* buf) {
return ((uint64_t)buf[0] << 56) | ((uint64_t)buf[1] << 48) | ((uint64_t)buf[2] << 40) | ((uint64_t)buf[3] << 32) |
((uint64_t)buf[4] << 24) | ((uint64_t)buf[5] << 16) | ((uint64_t)buf[6] << 8) | buf[7];
}
void add_prop(JP2Parser* parser, const char* key, const char* value) {
parser->keys[parser->prop_count] = strdup(key);
parser->values[parser->prop_count] = strdup(value);
parser->prop_count++;
}
void decode(JP2Parser* parser, uint8_t* data, size_t size) {
size_t offset = 0;
while (offset < size) {
uint64_t box_size = get_u32_be(data + offset);
offset += 4;
char type[5] = {0};
memcpy(type, data + offset, 4);
offset += 4;
size_t data_start = offset;
if (box_size == 1) {
box_size = get_u64_be(data + offset);
offset += 8;
data_start = offset;
} else if (box_size == 0) {
box_size = size - (data_start - 8);
}
size_t data_end = data_start + box_size - 8;
char val_buf[256];
if (strcmp(type, "jP ") == 0) {
sprintf(val_buf, "0x%X", get_u32_be(data + offset));
add_prop(parser, "Signature Value", val_buf);
} else if (strcmp(type, "ftyp") == 0) {
memcpy(val_buf, data + offset, 4); val_buf[4] = 0;
add_prop(parser, "Brand", val_buf);
sprintf(val_buf, "%u", get_u32_be(data + offset + 4));
add_prop(parser, "Minor Version", val_buf);
// Compatibility list (simplified, first one)
memcpy(val_buf, data + offset + 8, 4); val_buf[4] = 0;
add_prop(parser, "Compatibility List", val_buf);
} else if (strcmp(type, "jp2h") == 0 || strcmp(type, "res ") == 0) {
// Recurse
decode(parser, data + data_start, box_size - 8);
} else if (strcmp(type, "ihdr") == 0) {
sprintf(val_buf, "%u", get_u32_be(data + offset));
add_prop(parser, "Image Height", val_buf);
sprintf(val_buf, "%u", get_u32_be(data + offset + 4));
add_prop(parser, "Image Width", val_buf);
sprintf(val_buf, "%u", (data[offset + 8] << 8) | data[offset + 9]);
add_prop(parser, "Number of Components", val_buf);
sprintf(val_buf, "%u", data[offset + 10]);
add_prop(parser, "Bits Per Component", val_buf);
sprintf(val_buf, "%u", data[offset + 11]);
add_prop(parser, "Compression Type", val_buf);
sprintf(val_buf, "%u", data[offset + 12]);
add_prop(parser, "Colorspace Unknown", val_buf);
sprintf(val_buf, "%u", data[offset + 13]);
add_prop(parser, "Intellectual Property Present", val_buf);
} else if (strcmp(type, "colr") == 0) {
sprintf(val_buf, "%u", data[offset]);
add_prop(parser, "Color Method", val_buf);
sprintf(val_buf, "%u", data[offset + 1]);
add_prop(parser, "Color Precedence", val_buf);
sprintf(val_buf, "%u", data[offset + 2]);
add_prop(parser, "Color Approximation", val_buf);
if (data[offset] == 1) {
sprintf(val_buf, "%u", get_u32_be(data + offset + 3));
add_prop(parser, "Enumerated Colorspace", val_buf);
} else if (data[offset] == 2 || data[offset] == 3) {
sprintf(val_buf, "%llu", box_size - 11);
add_prop(parser, "ICC Profile Length", val_buf);
}
} else if (strcmp(type, "resc") == 0 || strcmp(type, "resd") == 0) {
const char* prefix = (strcmp(type, "resc") == 0) ? "Capture" : "Display";
uint16_t vrn = (data[offset] << 8) | data[offset+1];
uint16_t vrd = (data[offset+2] << 8) | data[offset+3];
uint16_t hrn = (data[offset+4] << 8) | data[offset+5];
uint16_t hrd = (data[offset+6] << 8) | data[offset+7];
int8_t vre = *(int8_t*)(data + offset + 8);
int8_t hre = *(int8_t*)(data + offset + 9);
char key[50];
sprintf(key, "%s Resolution Vertical", prefix);
sprintf(val_buf, "%.6f", (vrn / (double)vrd) * pow(10, vre));
add_prop(parser, key, val_buf);
sprintf(key, "%s Resolution Horizontal", prefix);
sprintf(val_buf, "%.6f", (hrn / (double)hrd) * pow(10, hre));
add_prop(parser, key, val_buf);
} else if (strcmp(type, "jp2c") == 0) {
sprintf(val_buf, "%llu", box_size - 8);
add_prop(parser, "Codestream Length", val_buf);
} else if (strcmp(type, "xml ") == 0) {
char* xml = malloc(box_size - 7);
memcpy(xml, data + offset, box_size - 8);
xml[box_size - 8] = 0;
add_prop(parser, "XML Metadata", xml);
free(xml);
} // Add more
offset = data_end;
}
}
void print_properties(JP2Parser* parser) {
for (int i = 0; i < parser->prop_count; i++) {
printf("%s: %s\n", parser->keys[i], parser->values[i]);
}
}
void write(JP2Parser* parser, const char* output_path) {
if (parser->data) {
FILE* f = fopen(output_path, "wb");
fwrite(parser->data, 1, parser->data_size, f);
fclose(f);
printf("File written to %s\n", output_path);
}
}
void free_parser(JP2Parser* parser) {
free(parser->data);
free(parser->filepath);
for (int i = 0; i < parser->prop_count; i++) {
free(parser->keys[i]);
free(parser->values[i]);
}
}
int main() {
JP2Parser parser;
init_parser(&parser, "example.jp2");
decode(&parser, parser.data, parser.data_size);
print_properties(&parser);
write(&parser, "output.jp2");
free_parser(&parser);
return 0;
}