Task 739: .TTC File Format
Task 739: .TTC File Format
1. List of Properties Intrinsic to the .TTC File Format
The .TTC (TrueType Collection) file format is designed to bundle multiple TrueType or OpenType fonts into a single file, allowing for shared tables to optimize storage. The properties intrinsic to the format, derived from its binary structure and independent of specific file system metadata (such as timestamps or permissions), are primarily the fields within the TTC header. These define the collection's organization and versioning. Based on the official specifications from Microsoft Typography and related documentation, the key properties are as follows:
- TTC Tag: A fixed 4-byte tag identifier, always set to 'ttcf' (0x74746366 in hexadecimal), confirming the file as a TrueType Collection.
- Major Version: A 2-byte unsigned integer (uint16) indicating the major version of the TTC header; typically 1 or 2.
- Minor Version: A 2-byte unsigned integer (uint16) indicating the minor version; typically 0.
- Number of Fonts: A 4-byte unsigned integer (uint32) specifying the count of fonts contained in the collection.
- Table Directory Offsets: An array of 4-byte offsets (Offset32, size equal to Number of Fonts), each pointing to the start of a font's Table Directory from the beginning of the file.
- DSIG Tag (conditional, present only if Major Version is 2): A 4-byte tag (uint32) indicating the presence of a digital signature table; set to 'DSIG' (0x44534947) if a signature exists, otherwise null (0).
- DSIG Length (conditional, present only if Major Version is 2): A 4-byte unsigned integer (uint32) specifying the length of the DSIG table in bytes; null if no signature.
- DSIG Offset (conditional, present only if Major Version is 2): A 4-byte unsigned integer (uint32) specifying the offset to the DSIG table from the file start; null if no signature.
These properties form the core header, followed by individual font Table Directories and shared or per-font tables. All offsets are relative to the file start, and tables must be 4-byte aligned.
2. Direct Download Links for .TTC Files
Two direct download links to publicly available .TTC files are provided below. These are sourced from reputable repositories and are free for download:
- https://raw.githubusercontent.com/prchann/fonts/main/Helvetica/Helvetica.ttc (Helvetica font collection)
- https://raw.githubusercontent.com/adobe-fonts/variable-font-collection-test/master/SourceHanVFProto.ttc (Source Han Variable Font Prototype collection)
3. HTML/JavaScript for Drag-and-Drop .TTC File Property Dump
The following is a self-contained HTML document with embedded JavaScript that enables a user to drag and drop a .TTC file. Upon dropping, it reads the file, decodes the header properties listed in section 1, and displays them on the screen. This implementation uses the FileReader API for binary parsing and assumes a modern web browser.
4. Python Class for .TTC File Handling
The following Python class can open a .TTC file, decode its header to read the properties, print them to the console, and write a new .TTC file with modified properties (e.g., for demonstration, it recreates the same structure). It uses built-in modules for binary handling.
import struct
import os
class TTCFileHandler:
def __init__(self, filepath):
self.filepath = filepath
self.ttc_tag = None
self.major_version = None
self.minor_version = None
self.num_fonts = None
self.table_offsets = []
self.dsig_tag = None
self.dsig_length = None
self.dsig_offset = None
self._decode()
def _decode(self):
with open(self.filepath, 'rb') as f:
data = f.read()
if len(data) < 12:
raise ValueError("Invalid TTC file: too short.")
self.ttc_tag = data[0:4].decode('ascii')
self.major_version, self.minor_version = struct.unpack('>HH', data[4:8])
self.num_fonts = struct.unpack('>I', data[8:12])[0]
offset = 12
self.table_offsets = []
for _ in range(self.num_fonts):
self.table_offsets.append(struct.unpack('>I', data[offset:offset+4])[0])
offset += 4
if self.major_version == 2:
if len(data) < offset + 12:
raise ValueError("Invalid TTC file: missing DSIG fields.")
self.dsig_tag = struct.unpack('>I', data[offset:offset+4])[0]
offset += 4
self.dsig_length = struct.unpack('>I', data[offset:offset+4])[0]
offset += 4
self.dsig_offset = struct.unpack('>I', data[offset:offset+4])[0]
def print_properties(self):
print(f"TTC Tag: {self.ttc_tag}")
print(f"Major Version: {self.major_version}")
print(f"Minor Version: {self.minor_version}")
print(f"Number of Fonts: {self.num_fonts}")
print(f"Table Directory Offsets: {self.table_offsets}")
if self.major_version == 2:
dsig_tag_str = 'DSIG' if self.dsig_tag else 'null'
print(f"DSIG Tag: {dsig_tag_str}")
print(f"DSIG Length: {self.dsig_length}")
print(f"DSIG Offset: {self.dsig_offset}")
def write(self, output_path):
with open(output_path, 'wb') as f:
f.write(self.ttc_tag.encode('ascii'))
f.write(struct.pack('>HH', self.major_version, self.minor_version))
f.write(struct.pack('>I', self.num_fonts))
for offset in self.table_offsets:
f.write(struct.pack('>I', offset))
if self.major_version == 2:
f.write(struct.pack('>I', self.dsig_tag))
f.write(struct.pack('>I', self.dsig_length))
f.write(struct.pack('>I', self.dsig_offset))
# Note: This writes only the header; full file recreation requires additional table data handling.
# Example usage:
# handler = TTCFileHandler('example.ttc')
# handler.print_properties()
# handler.write('output.ttc')
5. Java Class for .TTC File Handling
The following Java class can open a .TTC file, decode its header to read the properties, print them to the console, and write a new .TTC file with the same or modified properties. It uses RandomAccessFile for binary operations.
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
public class TTCFileHandler {
private String filepath;
private String ttcTag;
private short majorVersion;
private short minorVersion;
private int numFonts;
private long[] tableOffsets;
private long dsigTag;
private long dsigLength;
private long dsigOffset;
public TTCFileHandler(String filepath) throws IOException {
this.filepath = filepath;
decode();
}
private void decode() throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(new File(filepath), "r")) {
byte[] header = new byte[12];
raf.readFully(header);
ByteBuffer bb = ByteBuffer.wrap(header).order(ByteOrder.BIG_ENDIAN);
byte[] tagBytes = new byte[4];
bb.get(tagBytes);
ttcTag = new String(tagBytes);
majorVersion = bb.getShort();
minorVersion = bb.getShort();
numFonts = bb.getInt();
tableOffsets = new long[numFonts];
byte[] offsetsBytes = new byte[4 * numFonts];
raf.readFully(offsetsBytes);
bb = ByteBuffer.wrap(offsetsBytes).order(ByteOrder.BIG_ENDIAN);
for (int i = 0; i < numFonts; i++) {
tableOffsets[i] = bb.getInt() & 0xFFFFFFFFL; // Unsigned
}
if (majorVersion == 2) {
byte[] dsigBytes = new byte[12];
raf.readFully(dsigBytes);
bb = ByteBuffer.wrap(dsigBytes).order(ByteOrder.BIG_ENDIAN);
dsigTag = bb.getInt() & 0xFFFFFFFFL;
dsigLength = bb.getInt() & 0xFFFFFFFFL;
dsigOffset = bb.getInt() & 0xFFFFFFFFL;
}
}
}
public void printProperties() {
System.out.println("TTC Tag: " + ttcTag);
System.out.println("Major Version: " + majorVersion);
System.out.println("Minor Version: " + minorVersion);
System.out.println("Number of Fonts: " + numFonts);
System.out.println("Table Directory Offsets: " + Arrays.toString(tableOffsets));
if (majorVersion == 2) {
String dsigTagStr = (dsigTag != 0) ? "DSIG" : "null";
System.out.println("DSIG Tag: " + dsigTagStr);
System.out.println("DSIG Length: " + dsigLength);
System.out.println("DSIG Offset: " + dsigOffset);
}
}
public void write(String outputPath) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(new File(outputPath), "rw")) {
raf.write(ttcTag.getBytes());
ByteBuffer bb = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN);
bb.putShort(majorVersion).putShort(minorVersion).putInt(numFonts);
raf.write(bb.array());
bb = ByteBuffer.allocate(4 * numFonts).order(ByteOrder.BIG_ENDIAN);
for (long offset : tableOffsets) {
bb.putInt((int) offset);
}
raf.write(bb.array());
if (majorVersion == 2) {
bb = ByteBuffer.allocate(12).order(ByteOrder.BIG_ENDIAN);
bb.putInt((int) dsigTag).putInt((int) dsigLength).putInt((int) dsigOffset);
raf.write(bb.array());
}
}
// Note: This writes only the header; full file writing requires table data.
}
// Example usage:
// public static void main(String[] args) throws IOException {
// TTCFileHandler handler = new TTCFileHandler("example.ttc");
// handler.printProperties();
// handler.write("output.ttc");
// }
}
6. JavaScript Class for .TTC File Handling
The following JavaScript class can open a .TTC file (via File object), decode its header to read the properties, print them to the console, and write a new .TTC file as a Blob (which can be saved). It uses DataView for binary parsing.
class TTCFileHandler {
constructor(file) {
this.file = file;
this.ttcTag = null;
this.majorVersion = null;
this.minorVersion = null;
this.numFonts = null;
this.tableOffsets = [];
this.dsigTag = null;
this.dsigLength = null;
this.dsigOffset = null;
}
async decode() {
const arrayBuffer = await this.file.arrayBuffer();
const dataView = new DataView(arrayBuffer);
this.ttcTag = String.fromCharCode(dataView.getUint8(0), dataView.getUint8(1), dataView.getUint8(2), dataView.getUint8(3));
this.majorVersion = dataView.getUint16(4);
this.minorVersion = dataView.getUint16(6);
this.numFonts = dataView.getUint32(8);
let offset = 12;
this.tableOffsets = [];
for (let i = 0; i < this.numFonts; i++) {
this.tableOffsets.push(dataView.getUint32(offset));
offset += 4;
}
if (this.majorVersion === 2) {
this.dsigTag = dataView.getUint32(offset);
offset += 4;
this.dsigLength = dataView.getUint32(offset);
offset += 4;
this.dsigOffset = dataView.getUint32(offset);
}
}
printProperties() {
console.log(`TTC Tag: ${this.ttcTag}`);
console.log(`Major Version: ${this.majorVersion}`);
console.log(`Minor Version: ${this.minorVersion}`);
console.log(`Number of Fonts: ${this.numFonts}`);
console.log(`Table Directory Offsets: [${this.tableOffsets.join(', ')}]`);
if (this.majorVersion === 2) {
const dsigTagStr = this.dsigTag ? 'DSIG' : 'null';
console.log(`DSIG Tag: ${dsigTagStr}`);
console.log(`DSIG Length: ${this.dsigLength}`);
console.log(`DSIG Offset: ${this.dsigOffset}`);
}
}
write() {
const headerSize = 12 + (4 * this.numFonts) + (this.majorVersion === 2 ? 12 : 0);
const arrayBuffer = new ArrayBuffer(headerSize);
const dataView = new DataView(arrayBuffer);
for (let i = 0; i < 4; i++) {
dataView.setUint8(i, this.ttcTag.charCodeAt(i));
}
dataView.setUint16(4, this.majorVersion);
dataView.setUint16(6, this.minorVersion);
dataView.setUint32(8, this.numFonts);
let offset = 12;
for (let i = 0; i < this.numFonts; i++) {
dataView.setUint32(offset, this.tableOffsets[i]);
offset += 4;
}
if (this.majorVersion === 2) {
dataView.setUint32(offset, this.dsigTag);
offset += 4;
dataView.setUint32(offset, this.dsigLength);
offset += 4;
dataView.setUint32(offset, this.dsigOffset);
}
return new Blob([arrayBuffer], { type: 'application/octet-stream' });
// Note: This writes only the header; full writing requires table data.
}
}
// Example usage:
// const file = /* Obtain File object, e.g., from input */;
// const handler = new TTCFileHandler(file);
// await handler.decode();
// handler.printProperties();
// const blob = handler.write();
// // Save blob as file using URL.createObjectURL(blob) and <a> download.
7. C++ Class for .TTC File Handling
The following C++ class can open a .TTC file, decode its header to read the properties, print them to the console, and write a new .TTC file with the same or modified properties. It uses standard library for file I/O and assumes big-endian byte order (common for font files).
#include <iostream>
#include <fstream>
#include <vector>
#include <cstdint>
#include <cstring>
#include <endian.h> // For byte order conversion if needed; assume big-endian.
class TTCFileHandler {
private:
std::string filepath;
char ttcTag[5];
uint16_t majorVersion;
uint16_t minorVersion;
uint32_t numFonts;
std::vector<uint32_t> tableOffsets;
uint32_t dsigTag;
uint32_t dsigLength;
uint32_t dsigOffset;
public:
TTCFileHandler(const std::string& filepath) : filepath(filepath) {
ttcTag[4] = '\0';
decode();
}
void decode() {
std::ifstream file(filepath, std::ios::binary);
if (!file) {
throw std::runtime_error("Unable to open file.");
}
file.read(ttcTag, 4);
file.read(reinterpret_cast<char*>(&majorVersion), sizeof(uint16_t));
majorVersion = be16toh(majorVersion); // Convert if little-endian system.
file.read(reinterpret_cast<char*>(&minorVersion), sizeof(uint16_t));
minorVersion = be16toh(minorVersion);
file.read(reinterpret_cast<char*>(&numFonts), sizeof(uint32_t));
numFonts = be32toh(numFonts);
tableOffsets.resize(numFonts);
for (auto& offset : tableOffsets) {
file.read(reinterpret_cast<char*>(&offset), sizeof(uint32_t));
offset = be32toh(offset);
}
if (majorVersion == 2) {
file.read(reinterpret_cast<char*>(&dsigTag), sizeof(uint32_t));
dsigTag = be32toh(dsigTag);
file.read(reinterpret_cast<char*>(&dsigLength), sizeof(uint32_t));
dsigLength = be32toh(dsigLength);
file.read(reinterpret_cast<char*>(&dsigOffset), sizeof(uint32_t));
dsigOffset = be32toh(dsigOffset);
}
}
void printProperties() const {
std::cout << "TTC Tag: " << ttcTag << std::endl;
std::cout << "Major Version: " << majorVersion << std::endl;
std::cout << "Minor Version: " << minorVersion << std::endl;
std::cout << "Number of Fonts: " << numFonts << std::endl;
std::cout << "Table Directory Offsets: [";
for (size_t i = 0; i < tableOffsets.size(); ++i) {
std::cout << tableOffsets[i] << (i < tableOffsets.size() - 1 ? ", " : "");
}
std::cout << "]" << std::endl;
if (majorVersion == 2) {
std::string dsigTagStr = (dsigTag != 0) ? "DSIG" : "null";
std::cout << "DSIG Tag: " << dsigTagStr << std::endl;
std::cout << "DSIG Length: " << dsigLength << std::endl;
std::cout << "DSIG Offset: " << dsigOffset << std::endl;
}
}
void write(const std::string& outputPath) const {
std::ofstream file(outputPath, std::ios::binary);
if (!file) {
throw std::runtime_error("Unable to write file.");
}
file.write(ttcTag, 4);
uint16_t beMajor = htobe16(majorVersion);
file.write(reinterpret_cast<const char*>(&beMajor), sizeof(uint16_t));
uint16_t beMinor = htobe16(minorVersion);
file.write(reinterpret_cast<const char*>(&beMinor), sizeof(uint16_t));
uint32_t beNum = htobe32(numFonts);
file.write(reinterpret_cast<const char*>(&beNum), sizeof(uint32_t));
for (const auto& offset : tableOffsets) {
uint32_t beOffset = htobe32(offset);
file.write(reinterpret_cast<const char*>(&beOffset), sizeof(uint32_t));
}
if (majorVersion == 2) {
uint32_t beDsigTag = htobe32(dsigTag);
file.write(reinterpret_cast<const char*>(&beDsigTag), sizeof(uint32_t));
uint32_t beDsigLen = htobe32(dsigLength);
file.write(reinterpret_cast<const char*>(&beDsigLen), sizeof(uint32_t));
uint32_t beDsigOff = htobe32(dsigOffset);
file.write(reinterpret_cast<const char*>(&beDsigOff), sizeof(uint32_t));
}
// Note: This writes only the header; full writing requires table data.
}
};
// Example usage:
// int main() {
// try {
// TTCFileHandler handler("example.ttc");
// handler.printProperties();
// handler.write("output.ttc");
// } catch (const std::exception& e) {
// std::cerr << e.what() << std::endl;
// }
// return 0;
// }