crossbind
GitHub

libTIFF for WebAssembly

v4.7.2WebAssembly

libTIFF 4.7.2 for browsers, Node.js and edge runtimes, precompiled for wasm32, single-threaded and multi-threaded as @crossbind/port-tiff-wasm.

npm install @crossbind/port-tiff-wasm@beta

Install

shell
npm install @crossbind/port-tiff-wasm@beta
crossbind.config.js
import tiffWasm from '@crossbind/port-tiff-wasm/crossbind.config.js';
 
export default {
dependencies: [tiffWasm],
paths: { config: import.meta.url },
};

crossbind itself arrives with your bundler plugin, or with a new project from npm create crossbind@beta; Bundlers has Vite, Webpack, Rspack and Rollup.

Usage

Each example runs here, in this tab, and prints what the site build checked.

Each example also has a JavaScript only tab: the same task with no C++ file, calling libTIFF's own headers from @crossbind/port-tiff directly. None of them work that way; each tab says what stops it.

Encode pixels as a TIFF and decode them back

The core of libtiff: TIFFSetField and TIFFWriteScanline write a page, TIFFGetField and TIFFReadRGBAImageOriented read it back as display pixels. TIFFStreamOpen keeps the file in memory.

src/native/tiff_codec.h
#pragma once
 
#include <tiffio.h>
#include <tiffio.hxx>
 
#include <cstdint>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
 
// Canvas pixels to a TIFF and back, in memory: TIFFStreamOpen gives libtiff a std::ostream or
// std::istream instead of a file. Bytes cross the binding as std::u16string, one code unit (0-255)
// per byte.
class Tiff {
public:
static std::string version() { return TIFFLIB_VERSION_STR_MAJ_MIN_MIC; }
 
// RGBA pixels, as ImageData holds them, to an 8-bit RGB TIFF (alpha is not stored). `compression`
// is a COMPRESSION_* code from tiff.h: 1 none, 5 LZW, 8 Deflate, 32773 PackBits, 50000 ZSTD.
static std::u16string encode(const std::u16string& rgba, int width, int height, int compression) {
if (width <= 0 || height <= 0 || rgba.size() != static_cast<size_t>(width) * height * 4) throw std::invalid_argument("rgba must hold width x height x 4 bytes");
if (!TIFFIsCODECConfigured(static_cast<uint16_t>(compression))) throw std::invalid_argument("compression " + std::to_string(compression) + " is not in this build");
std::vector<unsigned char> rgb(static_cast<size_t>(width) * height * 3);
for (size_t pixel = 0; pixel < rgb.size() / 3; ++pixel) {
for (size_t channel = 0; channel < 3; ++channel) {
const char16_t unit = rgba[pixel * 4 + channel];
if (unit > 0xFF) throw std::invalid_argument("not a byte string: a code unit is above 255");
rgb[pixel * 3 + channel] = static_cast<unsigned char>(unit);
}
}
std::ostringstream out;
TIFF* tif = TIFFStreamOpen("memory", &out);
if (!tif) throw std::runtime_error("libtiff could not open a stream for writing");
TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, width);
TIFFSetField(tif, TIFFTAG_IMAGELENGTH, height);
TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 3);
TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 8);
TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
TIFFSetField(tif, TIFFTAG_COMPRESSION, compression);
TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(tif, 0));
for (int y = 0; y < height; ++y) {
if (TIFFWriteScanline(tif, &rgb[static_cast<size_t>(y) * width * 3], static_cast<uint32_t>(y), 0) < 0) {
TIFFClose(tif);
throw std::runtime_error("libtiff could not write row " + std::to_string(y));
}
}
TIFFClose(tif);
const std::string bytes = out.str();
std::u16string units(bytes.size(), u'\0');
for (size_t i = 0; i < bytes.size(); ++i) units[i] = static_cast<unsigned char>(bytes[i]);
return units;
}
 
// The first page's size, samples and codec, and the page count.
static std::string describe(const std::u16string& tiff) {
std::istringstream in(fromUnits(tiff));
TIFF* tif = open(in);
uint32_t width = 0;
uint32_t height = 0;
uint16_t samples = 0;
uint16_t bits = 0;
uint16_t compression = 0;
TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &width);
TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &height);
TIFFGetFieldDefaulted(tif, TIFFTAG_SAMPLESPERPIXEL, &samples);
TIFFGetFieldDefaulted(tif, TIFFTAG_BITSPERSAMPLE, &bits);
TIFFGetFieldDefaulted(tif, TIFFTAG_COMPRESSION, &compression);
const unsigned pages = TIFFNumberOfDirectories(tif);
TIFFClose(tif);
const TIFFCodec* codec = TIFFFindCODEC(compression);
return std::to_string(width) + "x" + std::to_string(height) + ", " + std::to_string(samples) + " x " + std::to_string(bits) + "-bit samples, " +
(codec ? codec->name : "compression " + std::to_string(compression)) + ", " + std::to_string(pages) + (pages == 1 ? " page" : " pages");
}
 
// The first page as RGBA, top row first, whatever its bit depth, colour model, layout or codec.
static std::u16string decode(const std::u16string& tiff) {
std::istringstream in(fromUnits(tiff));
TIFF* tif = open(in);
uint32_t width = 0;
uint32_t height = 0;
TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &width);
TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &height);
if (static_cast<uint64_t>(width) * height > (64u << 20)) {
TIFFClose(tif);
throw std::runtime_error("more than 64 megapixels: decode it from a file instead");
}
std::vector<uint32_t> raster(static_cast<size_t>(width) * height);
const int ok = TIFFReadRGBAImageOriented(tif, width, height, raster.data(), ORIENTATION_TOPLEFT, 0);
TIFFClose(tif);
if (!ok) throw std::runtime_error("libtiff cannot convert this page to RGBA");
std::u16string rgba(raster.size() * 4, u'\0');
for (size_t i = 0; i < raster.size(); ++i) {
rgba[i * 4] = static_cast<char16_t>(TIFFGetR(raster[i]));
rgba[i * 4 + 1] = static_cast<char16_t>(TIFFGetG(raster[i]));
rgba[i * 4 + 2] = static_cast<char16_t>(TIFFGetB(raster[i]));
rgba[i * 4 + 3] = static_cast<char16_t>(TIFFGetA(raster[i]));
}
return rgba;
}
 
private:
static TIFF* open(std::istringstream& in) {
TIFF* tif = TIFFStreamOpen("memory", &in);
if (!tif) throw std::runtime_error("not a TIFF libtiff can read");
return tif;
}
 
static std::string fromUnits(const std::u16string& units) {
std::string bytes(units.size(), '\0');
for (size_t i = 0; i < units.size(); ++i) {
if (units[i] > 0xFF) throw std::invalid_argument("not a byte string: a code unit is above 255");
bytes[i] = static_cast<char>(units[i]);
}
return bytes;
}
};
main.js
import { initNative, Tiff } from './native/tiff_codec.h';
 
await initNative();
const width = 160;
const height = 120;
let rgba = ''; // one character per byte, in the order ImageData uses
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) rgba += String.fromCharCode(x, 64 + (y >> 5) * 32, 255 - x, 255);
}
const tiff = await Tiff.encode(rgba, width, height, 5); // 5 = COMPRESSION_LZW
console.log(`libtiff ${await Tiff.version()}: ${rgba.length} B of RGBA -> ${tiff.length} B, starts with ${tiff.slice(0, 3)}`);
console.log(await Tiff.describe(tiff));
console.log((await Tiff.decode(tiff)) === rgba);
PRINTSfirst run downloads 2.4 MB
libtiff 4.7.2: 76800 B of RGBA -> 27692 B, starts with II*
160x120, 3 x 8-bit samples, LZW, 1 page
true

Store several pages in one file

Scanners and fax software keep a document as one TIFF: every page is a directory closed with TIFFWriteDirectory, listed with TIFFReadDirectory and opened again with TIFFSetDirectory. These pages are 1-bit CCITT Group 4, the fax codec.

src/native/tiff_pages.h
#pragma once
 
#include <tiffio.h>
 
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <stdexcept>
#include <string>
#include <vector>
 
// A multi-page TIFF, the way scanners and fax software store documents: every page is its own
// directory, closed with TIFFWriteDirectory and found again with TIFFSetDirectory. The pages are
// black and white, stored as 1-bit CCITT Group 4. Bytes cross the binding as std::u16string, one
// code unit (0-255) per byte.
//
// The file lives in memory through TIFFClientOpen. TIFFStreamOpen's std::ostream will not do here:
// adding a page reads the previous one back, and libtiff cannot read from an ostream.
class TiffPages {
public:
TiffPages() : tif(openMemory(file, "w")) {}
 
~TiffPages() {
if (tif) TIFFClose(tif);
}
 
// Appends a page from 8-bit grey pixels, one byte each: below 128 is black.
void add(const std::u16string& grey, int width, int height, const std::string& name) {
if (!tif) throw std::logic_error("the document is already finished");
if (width <= 0 || height <= 0 || grey.size() != static_cast<size_t>(width) * height) throw std::invalid_argument("grey must hold width x height bytes");
TIFFSetField(tif, TIFFTAG_SUBFILETYPE, FILETYPE_PAGE);
TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, width);
TIFFSetField(tif, TIFFTAG_IMAGELENGTH, height);
TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 1);
TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1);
TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISWHITE);
TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_CCITTFAX4);
TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, height);
TIFFSetField(tif, TIFFTAG_PAGENUMBER, pages, 0); // 0: the page count is not known yet
TIFFSetField(tif, TIFFTAG_PAGENAME, name.c_str());
std::vector<unsigned char> row((static_cast<size_t>(width) + 7) / 8);
for (int y = 0; y < height; ++y) {
std::fill(row.begin(), row.end(), 0);
for (int x = 0; x < width; ++x) {
if (grey[static_cast<size_t>(y) * width + x] < 128) row[x / 8] |= static_cast<unsigned char>(0x80 >> (x % 8));
}
if (TIFFWriteScanline(tif, row.data(), static_cast<uint32_t>(y), 0) < 0) throw std::runtime_error("libtiff could not write row " + std::to_string(y));
}
if (!TIFFWriteDirectory(tif)) throw std::runtime_error("libtiff could not finish page " + std::to_string(pages + 1));
pages += 1;
}
 
// Closes the file and returns its bytes.
std::u16string finish() {
if (!tif) throw std::logic_error("the document is already finished");
TIFFClose(tif);
tif = nullptr;
std::u16string units(file.bytes.size(), u'\0');
for (size_t i = 0; i < file.bytes.size(); ++i) units[i] = static_cast<unsigned char>(file.bytes[i]);
return units;
}
 
// One line per page: its number, name, size and codec.
static std::string list(const std::u16string& tiff) {
MemoryFile source(tiff);
TIFF* in = openMemory(source, "r");
std::string lines;
do {
uint32_t width = 0;
uint32_t height = 0;
uint16_t compression = 0;
char* name = nullptr;
TIFFGetField(in, TIFFTAG_IMAGEWIDTH, &width);
TIFFGetField(in, TIFFTAG_IMAGELENGTH, &height);
TIFFGetFieldDefaulted(in, TIFFTAG_COMPRESSION, &compression);
const TIFFCodec* codec = TIFFFindCODEC(compression);
lines += "page " + std::to_string(TIFFCurrentDirectory(in) + 1) + " \"" + (TIFFGetField(in, TIFFTAG_PAGENAME, &name) ? name : "") + "\": " +
std::to_string(width) + "x" + std::to_string(height) + ", " + (codec ? codec->name : "?") + "\n";
} while (TIFFReadDirectory(in));
TIFFClose(in);
return lines;
}
 
// Page `index` (from 0) back as 8-bit grey, black 0 and white 255, read with TIFFReadScanline.
static std::u16string grey(const std::u16string& tiff, int index) {
MemoryFile source(tiff);
TIFF* in = openMemory(source, "r");
uint16_t bits = 0;
if (index < 0 || !TIFFSetDirectory(in, static_cast<tdir_t>(index)) || !TIFFGetField(in, TIFFTAG_BITSPERSAMPLE, &bits) || bits != 1) {
TIFFClose(in);
throw std::out_of_range("there is no black-and-white page " + std::to_string(index));
}
uint32_t width = 0;
uint32_t height = 0;
TIFFGetField(in, TIFFTAG_IMAGEWIDTH, &width);
TIFFGetField(in, TIFFTAG_IMAGELENGTH, &height);
std::vector<unsigned char> row(static_cast<size_t>(TIFFScanlineSize(in)));
std::u16string pixels(static_cast<size_t>(width) * height, u'\0');
for (uint32_t y = 0; y < height; ++y) {
if (TIFFReadScanline(in, row.data(), y, 0) < 0) {
TIFFClose(in);
throw std::runtime_error("libtiff could not read row " + std::to_string(y));
}
for (uint32_t x = 0; x < width; ++x) pixels[static_cast<size_t>(y) * width + x] = (row[x / 8] & (0x80 >> (x % 8))) ? 0 : 255;
}
TIFFClose(in);
return pixels;
}
 
private:
// A growable file in memory, and the read, write and seek procs libtiff calls on it.
struct MemoryFile {
std::string bytes;
uint64_t position = 0;
 
MemoryFile() = default;
explicit MemoryFile(const std::u16string& units) : bytes(units.size(), '\0') {
for (size_t i = 0; i < units.size(); ++i) {
if (units[i] > 0xFF) throw std::invalid_argument("not a byte string: a code unit is above 255");
bytes[i] = static_cast<char>(units[i]);
}
}
 
static tmsize_t read(thandle_t handle, void* buffer, tmsize_t size) {
MemoryFile* self = static_cast<MemoryFile*>(handle);
const uint64_t left = self->position < self->bytes.size() ? self->bytes.size() - self->position : 0;
const size_t count = static_cast<size_t>(std::min<uint64_t>(left, static_cast<uint64_t>(size)));
std::memcpy(buffer, self->bytes.data() + self->position, count);
self->position += count;
return static_cast<tmsize_t>(count);
}
static tmsize_t write(thandle_t handle, void* buffer, tmsize_t size) {
MemoryFile* self = static_cast<MemoryFile*>(handle);
if (self->bytes.size() < self->position + size) self->bytes.resize(static_cast<size_t>(self->position + size));
std::memcpy(&self->bytes[static_cast<size_t>(self->position)], buffer, static_cast<size_t>(size));
self->position += size;
return size;
}
static toff_t seek(thandle_t handle, toff_t offset, int whence) {
MemoryFile* self = static_cast<MemoryFile*>(handle);
const uint64_t base = whence == SEEK_CUR ? self->position : whence == SEEK_END ? self->bytes.size() : 0;
self->position = base + offset;
return self->position;
}
static toff_t size(thandle_t handle) { return static_cast<MemoryFile*>(handle)->bytes.size(); }
static int close(thandle_t) { return 0; }
static int map(thandle_t, void**, toff_t*) { return 0; }
static void unmap(thandle_t, void*, toff_t) {}
};
 
static TIFF* openMemory(MemoryFile& memory, const char* mode) {
TIFF* opened = TIFFClientOpen("memory", mode, &memory, MemoryFile::read, MemoryFile::write, MemoryFile::seek, MemoryFile::close, MemoryFile::size,
MemoryFile::map, MemoryFile::unmap);
if (!opened) throw std::runtime_error(*mode == 'w' ? "libtiff could not open a file in memory" : "not a TIFF libtiff can read");
return opened;
}
 
MemoryFile file;
TIFF* tif = nullptr;
int pages = 0;
};
main.js
import { initNative, TiffPages } from './native/tiff_pages.h';
 
await initNative();
const width = 240;
const height = 320;
const names = ['cover', 'summary', 'appendix'];
const pages = names.map((name, index) => {
let grey = ''; // one byte per pixel: a checkerboard whose squares widen page by page
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) grey += String.fromCharCode(((x >> (index + 2)) + (y >> 4)) % 2 ? 0 : 255);
}
return grey;
});
const scan = await new TiffPages();
for (const [index, name] of names.entries()) await scan.add(pages[index], width, height, name);
const tiff = await scan.finish();
 
console.log(`${names.length} pages in ${tiff.length} B`);
for (const line of (await TiffPages.list(tiff)).trimEnd().split('\n')) console.log(line);
console.log((await TiffPages.grey(tiff, 2)) === pages[2]);
PRINTSfirst run downloads 2.4 MB
3 pages in 6133 B
page 1 "cover": 240x320, CCITT Group 4
page 2 "summary": 240x320, CCITT Group 4
page 3 "appendix": 240x320, CCITT Group 4
true

Keep 32-bit float samples exact

Elevation, temperature and microscopy data are not 8-bit pictures. SAMPLEFORMAT_IEEEFP stores the real values, the floating-point predictor helps them compress and TIFFReadScanline reads them back; TIFFRGBAImageOK says why the RGBA reader cannot.

src/native/tiff_samples.h
#pragma once
 
#include <tiffio.h>
#include <tiffio.hxx>
 
#include <cstdint>
#include <cstring>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
 
// Scientific rasters keep their real values: one band of 32-bit floats, written and read a scanline
// at a time. The samples cross the binding as little-endian float32 bytes in a std::u16string, one
// code unit (0-255) per byte, which is what a Float32Array's buffer holds.
class TiffSamples {
public:
// `compression` is a COMPRESSION_* code; `predictor` 1 is none, 3 is the floating-point predictor,
// which Deflate, LZW and ZSTD compress better.
static std::u16string writeFloat32(const std::u16string& samples, int width, int height, int compression, int predictor) {
if (width <= 0 || height <= 0 || samples.size() != static_cast<size_t>(width) * height * 4) throw std::invalid_argument("samples must hold width x height x 4 bytes");
const std::string raw = fromUnits(samples);
std::ostringstream out;
TIFF* tif = TIFFStreamOpen("memory", &out);
if (!tif) throw std::runtime_error("libtiff could not open a stream for writing");
TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, width);
TIFFSetField(tif, TIFFTAG_IMAGELENGTH, height);
TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1);
TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 32);
TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP);
TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
TIFFSetField(tif, TIFFTAG_COMPRESSION, compression);
if (predictor != PREDICTOR_NONE) TIFFSetField(tif, TIFFTAG_PREDICTOR, predictor);
TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(tif, 0));
std::vector<float> row(static_cast<size_t>(width));
for (int y = 0; y < height; ++y) {
std::memcpy(row.data(), raw.data() + static_cast<size_t>(y) * width * 4, row.size() * 4);
if (TIFFWriteScanline(tif, row.data(), static_cast<uint32_t>(y), 0) < 0) {
TIFFClose(tif);
throw std::runtime_error("libtiff could not write row " + std::to_string(y));
}
}
TIFFClose(tif);
return toUnits(out.str());
}
 
// The samples back, row by row with TIFFReadScanline, as little-endian float32 bytes.
static std::u16string readFloat32(const std::u16string& tiff) {
std::istringstream in(fromUnits(tiff));
TIFF* tif = open(in);
uint32_t width = 0;
uint32_t height = 0;
uint16_t bits = 0;
uint16_t format = 0;
uint16_t samples = 0;
TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &width);
TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &height);
TIFFGetFieldDefaulted(tif, TIFFTAG_BITSPERSAMPLE, &bits);
TIFFGetFieldDefaulted(tif, TIFFTAG_SAMPLEFORMAT, &format);
TIFFGetFieldDefaulted(tif, TIFFTAG_SAMPLESPERPIXEL, &samples);
if (bits != 32 || format != SAMPLEFORMAT_IEEEFP || samples != 1 || TIFFIsTiled(tif)) {
TIFFClose(tif);
throw std::runtime_error("not a one-band float32 TIFF in strips");
}
std::string raw(static_cast<size_t>(width) * height * 4, '\0');
for (uint32_t y = 0; y < height; ++y) {
if (TIFFReadScanline(tif, &raw[static_cast<size_t>(y) * width * 4], y, 0) < 0) {
TIFFClose(tif);
throw std::runtime_error("libtiff could not read row " + std::to_string(y));
}
}
TIFFClose(tif);
return toUnits(raw);
}
 
// Whether TIFFReadRGBAImage could turn the image into display pixels, and if not, why not.
static std::string rgbaCheck(const std::u16string& tiff) {
std::istringstream in(fromUnits(tiff));
TIFF* tif = open(in);
char reason[1024] = "";
const bool ok = TIFFRGBAImageOK(tif, reason);
TIFFClose(tif);
return ok ? "TIFFReadRGBAImage can convert it" : reason;
}
 
private:
static TIFF* open(std::istringstream& in) {
TIFF* tif = TIFFStreamOpen("memory", &in);
if (!tif) throw std::runtime_error("not a TIFF libtiff can read");
return tif;
}
 
static std::u16string toUnits(const std::string& bytes) {
std::u16string units(bytes.size(), u'\0');
for (size_t i = 0; i < bytes.size(); ++i) units[i] = static_cast<unsigned char>(bytes[i]);
return units;
}
 
static std::string fromUnits(const std::u16string& units) {
std::string bytes(units.size(), '\0');
for (size_t i = 0; i < units.size(); ++i) {
if (units[i] > 0xFF) throw std::invalid_argument("not a byte string: a code unit is above 255");
bytes[i] = static_cast<char>(units[i]);
}
return bytes;
}
};
main.js
import { initNative, TiffSamples } from './native/tiff_samples.h';
 
await initNative();
const width = 256;
const height = 256;
// An elevation grid in metres: a smooth saddle in steps of 1/64 m, so float32 holds every value exactly.
const heights = new Float32Array(width * height);
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) heights[y * width + x] = 500 + ((x - 128) * (y - 96)) / 64;
}
let samples = ''; // the Float32Array's bytes, one character each
for (const byte of new Uint8Array(heights.buffer)) samples += String.fromCharCode(byte);
 
const plain = await TiffSamples.writeFloat32(samples, width, height, 8, 1); // 8 = Deflate, no predictor
const predicted = await TiffSamples.writeFloat32(samples, width, height, 8, 3); // floating-point predictor
console.log(`${samples.length} B of float32 -> Deflate ${plain.length} B, with predictor 3 ${predicted.length} B`);
console.log((await TiffSamples.readFloat32(predicted)) === samples);
console.log(await TiffSamples.rgbaCheck(predicted));
PRINTSfirst run downloads 2.4 MB
262144 B of float32 -> Deflate 123852 B, with predictor 3 9072 B
true
Sorry, can not handle images with 32-bit samples

Read one tile of a big image

Large TIFFs are cut into tiles so a viewer decodes only what it shows. TIFFWriteTile stores them, TIFFComputeTile finds the one under a pixel and TIFFReadTile decodes it alone.

src/native/tiff_tiles.h
#pragma once
 
#include <tiffio.h>
#include <tiffio.hxx>
 
#include <algorithm>
#include <cstdint>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
 
// Big images are stored in tiles, so a reader decodes only the part it shows: the layout of
// slide scanners, Cloud Optimized GeoTIFF and pyramidal TIFF. Bytes cross the binding as
// std::u16string, one code unit (0-255) per byte.
class TiffTiles {
public:
// RGBA pixels to an 8-bit RGB TIFF in square Deflate tiles; `tileSize` is a multiple of 16.
static std::u16string encode(const std::u16string& rgba, int width, int height, int tileSize) {
if (width <= 0 || height <= 0 || rgba.size() != static_cast<size_t>(width) * height * 4) throw std::invalid_argument("rgba must hold width x height x 4 bytes");
if (tileSize <= 0 || tileSize % 16 != 0) throw std::invalid_argument("tiles are a multiple of 16 pixels wide");
std::ostringstream out;
TIFF* tif = TIFFStreamOpen("memory", &out);
if (!tif) throw std::runtime_error("libtiff could not open a stream for writing");
TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, width);
TIFFSetField(tif, TIFFTAG_IMAGELENGTH, height);
TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 3);
TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 8);
TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_ADOBE_DEFLATE);
TIFFSetField(tif, TIFFTAG_TILEWIDTH, tileSize);
TIFFSetField(tif, TIFFTAG_TILELENGTH, tileSize);
std::vector<unsigned char> tile(static_cast<size_t>(TIFFTileSize(tif)));
for (int top = 0; top < height; top += tileSize) {
for (int left = 0; left < width; left += tileSize) {
std::fill(tile.begin(), tile.end(), 0); // edge tiles are padded
for (int y = top; y < std::min(top + tileSize, height); ++y) {
for (int x = left; x < std::min(left + tileSize, width); ++x) {
for (int channel = 0; channel < 3; ++channel) {
const char16_t unit = rgba[(static_cast<size_t>(y) * width + x) * 4 + channel];
if (unit > 0xFF) {
TIFFClose(tif);
throw std::invalid_argument("not a byte string: a code unit is above 255");
}
tile[(static_cast<size_t>(y - top) * tileSize + (x - left)) * 3 + channel] = static_cast<unsigned char>(unit);
}
}
}
if (TIFFWriteTile(tif, tile.data(), static_cast<uint32_t>(left), static_cast<uint32_t>(top), 0, 0) < 0) {
TIFFClose(tif);
throw std::runtime_error("libtiff could not write the tile at " + std::to_string(left) + "," + std::to_string(top));
}
}
}
TIFFClose(tif);
const std::string bytes = out.str();
std::u16string units(bytes.size(), u'\0');
for (size_t i = 0; i < bytes.size(); ++i) units[i] = static_cast<unsigned char>(bytes[i]);
return units;
}
 
// "WxH in N tiles of TxT"
static std::string layout(const std::u16string& tiff) {
std::istringstream in(fromUnits(tiff));
TIFF* tif = openTiled(in);
uint32_t width = 0;
uint32_t height = 0;
uint32_t tileWidth = 0;
uint32_t tileHeight = 0;
TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &width);
TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &height);
TIFFGetField(tif, TIFFTAG_TILEWIDTH, &tileWidth);
TIFFGetField(tif, TIFFTAG_TILELENGTH, &tileHeight);
const uint32_t tiles = TIFFNumberOfTiles(tif);
TIFFClose(tif);
return std::to_string(width) + "x" + std::to_string(height) + " in " + std::to_string(tiles) + " tiles of " + std::to_string(tileWidth) + "x" +
std::to_string(tileHeight);
}
 
// The number of the tile that holds pixel (x, y), counted row by row from 0.
static int tileIndex(const std::u16string& tiff, int x, int y) {
std::istringstream in(fromUnits(tiff));
TIFF* tif = openTiled(in);
const bool inside = x >= 0 && y >= 0 && TIFFCheckTile(tif, static_cast<uint32_t>(x), static_cast<uint32_t>(y), 0, 0);
const uint32_t index = inside ? TIFFComputeTile(tif, static_cast<uint32_t>(x), static_cast<uint32_t>(y), 0, 0) : 0;
TIFFClose(tif);
if (!inside) throw std::out_of_range("the pixel is outside the image");
return static_cast<int>(index);
}
 
// The RGB samples of the tile that holds pixel (x, y), decoded with TIFFReadTile; no other
// tile is read.
static std::u16string tile(const std::u16string& tiff, int x, int y) {
std::istringstream in(fromUnits(tiff));
TIFF* tif = openTiled(in);
if (x < 0 || y < 0 || !TIFFCheckTile(tif, static_cast<uint32_t>(x), static_cast<uint32_t>(y), 0, 0)) {
TIFFClose(tif);
throw std::out_of_range("the pixel is outside the image");
}
std::string samples(static_cast<size_t>(TIFFTileSize(tif)), '\0');
const tmsize_t read = TIFFReadTile(tif, &samples[0], static_cast<uint32_t>(x), static_cast<uint32_t>(y), 0, 0);
TIFFClose(tif);
if (read < 0) throw std::runtime_error("libtiff could not read the tile");
std::u16string units(samples.size(), u'\0');
for (size_t i = 0; i < samples.size(); ++i) units[i] = static_cast<unsigned char>(samples[i]);
return units;
}
 
private:
static TIFF* openTiled(std::istringstream& in) {
TIFF* tif = TIFFStreamOpen("memory", &in);
if (!tif) throw std::runtime_error("not a TIFF libtiff can read");
if (!TIFFIsTiled(tif)) {
TIFFClose(tif);
throw std::runtime_error("this TIFF is stored in strips, not tiles");
}
return tif;
}
 
static std::string fromUnits(const std::u16string& units) {
std::string bytes(units.size(), '\0');
for (size_t i = 0; i < units.size(); ++i) {
if (units[i] > 0xFF) throw std::invalid_argument("not a byte string: a code unit is above 255");
bytes[i] = static_cast<char>(units[i]);
}
return bytes;
}
};
main.js
import { initNative, TiffTiles } from './native/tiff_tiles.h';
 
await initNative();
const size = 512;
let rgba = '';
for (let y = 0; y < size; y += 1) {
for (let x = 0; x < size; x += 1) rgba += String.fromCharCode(x >> 1, y >> 1, (x + y) >> 2, 255);
}
const tiff = await TiffTiles.encode(rgba, size, size, 128);
console.log(`${await TiffTiles.layout(tiff)}, ${tiff.length} B`);
 
const [x, y] = [300, 200];
const tile = await TiffTiles.tile(tiff, x, y); // RGB, 128 x 128 x 3 bytes
const at = ((y % 128) * 128 + (x % 128)) * 3;
console.log(`pixel (${x}, ${y}) is in tile ${await TiffTiles.tileIndex(tiff, x, y)}, decoded alone: ${tile.length} B`);
console.log(`rgb(${[0, 1, 2].map((channel) => tile.charCodeAt(at + channel)).join(', ')})`);
PRINTSfirst run downloads 2.4 MB
512x512 in 16 tiles of 128x128, 239648 B
pixel (300, 200) is in tile 6, decoded alone: 49152 B
rgb(150, 100, 125)

Open a TIFF file and print its tags

Files reach C++ by path: m.autoMountFiles mounts what an <input type="file"> or a drop gives you, TIFFOpen reads it in place and TIFFPrintDirectory prints every tag, as the tiffinfo tool does.

src/native/tiff_file.h
#pragma once
 
#include <tiffio.h>
 
#include <cstdio>
#include <cstdlib>
#include <stdexcept>
#include <string>
 
// A TIFF by path, which is how files reach C++: mounted from an <input type="file"> in a browser,
// or from the app's storage on a phone. TIFFOpen reads only what each call needs, so the size of
// the file does not matter.
class TiffFile {
public:
static int pages(const std::string& path) {
TIFF* tif = open(path);
const int count = static_cast<int>(TIFFNumberOfDirectories(tif));
TIFFClose(tif);
return count;
}
 
// Page `index` (from 0) as TIFFPrintDirectory prints it, the report the tiffinfo tool shows.
static std::string directory(const std::string& path, int index) {
TIFF* tif = open(path);
if (index < 0 || !TIFFSetDirectory(tif, static_cast<tdir_t>(index))) {
TIFFClose(tif);
throw std::out_of_range("there is no page " + std::to_string(index));
}
char* text = nullptr;
size_t size = 0;
FILE* report = open_memstream(&text, &size);
if (!report) {
TIFFClose(tif);
throw std::runtime_error("could not open a memory stream");
}
TIFFPrintDirectory(tif, report, TIFFPRINT_NONE);
std::fclose(report);
TIFFClose(tif);
const std::string printed(text, size);
std::free(text);
return printed;
}
 
private:
static TIFF* open(const std::string& path) {
TIFF* tif = TIFFOpen(path.c_str(), "r");
if (!tif) throw std::runtime_error("not a TIFF libtiff can read: " + path);
return tif;
}
};
main.js
import { initNative } from './native/tiff_file.h';
 
const m = await initNative();
const { Tiff, TiffFile } = m;
// A File, as an <input type="file"> gives one; this one holds a TIFF made with the first example's class.
let rgba = '';
for (let i = 0; i < 64 * 48; i += 1) rgba += String.fromCharCode((i % 64) * 4, (i >> 6) * 5, 128, 255);
const tiff = await Tiff.encode(rgba, 64, 48, 8); // 8 = Deflate
const file = new File([Uint8Array.from(tiff, (c) => c.charCodeAt(0))], 'photo.tif', { type: 'image/tiff' });
 
const [path] = await m.autoMountFiles([file], await m.getRandomPath('/memfs')); // memory only, gone with the tab
console.log(`${file.name}: ${file.size} B, ${await TiffFile.pages(path)} page`);
for (const line of (await TiffFile.directory(path, 0)).trimEnd().split('\n')) console.log(line);
PRINTSfirst run downloads 2.4 MB
photo.tif: 6260 B, 1 page
TIFF Directory at offset 0x17e0 (6112)
  Image Width: 64 Image Length: 48
  Bits/Sample: 8
  Compression Scheme: AdobeDeflate
  Photometric Interpretation: RGB color
  Samples/Pixel: 3
  Rows/Strip: 42
  Planar Configuration: single image plane

What is different on WebAssembly

  • In a browser the module runs in a Worker by default (useWorker), so every call returns a promise: await calls and constructors alike.
  • The module has its own filesystem: m.FS writes files, m.getFileBytes reads them back and m.autoMountFiles mounts File objects from an <input type=file>. /memfs lives in memory; /opfs persists across reloads and needs the Worker. See Filesystem.
  • In Node.js, m.FS is the real disk, so use real paths there.
  • Multi-threaded builds (runtime: 'mt') need COOP and COEP headers in production. See Threading.

Other platforms

Facts on this page come from the port manifests in the repository and from what npm served on beta when the site was built. See the Libraries guide for the full consumer flow.

MORE LIBRARIES
cURLExpatGDALGEOSGeoTIFFiconvLERClibjpeg-turboOpenSSLPROJSpatiaLiteSQLiteWebPzlibZstandard
Type to search every guide page and section.
↑↓ navigate↵ openesc close