crossbind
GitHub
← All libraries

libTIFF

TIFF image reader and writer.

Install

Runs in browsers; Node.js and edge runtimes use the same package. Built for wasm32, single-threaded and multi-threaded.

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 },
};
main.js
import { initNative, TIFFClientOpen } from '@crossbind/port-tiff/tiffio.h';
 
await initNative();

crossbind itself comes with your bundler plugin; Browser has Vite, Webpack, Rspack and Rollup.

TIFF in any browser, from fax pages to float elevation models

MDN lists Safari as the only browser that shows TIFF images by itself. These apps run libtiff, compiled by crossbind: they open multi-page, 16-bit and float TIFFs with their tags, turn page photos into one fax-grade archive, and compare every codec on scientific rasters. The first run downloads 2.4 MB of WebAssembly once; every app on this page shares it, and nothing is uploaded.

APP 01

Open any TIFF, in any browser

Scans, faxes, 16-bit microscope images, float elevation models and camera TIFFs, every page with its tags. MDN lists Safari as the only browser that shows TIFF images by itself (image file type guide); here libtiff decodes the file in the page, and greyscale data too deep for a screen is stretched to its own range.

The sample is a four-page TIFF the module writes itself: a CCITT Group 4 letter, a JPEG photo, 12-bit cells in 16-bit ZSTD tiles and a float elevation model with no-data. Your own file stays in this tab: it is mounted into the module's in-memory filesystem, never uploaded.

Open the sample, or a TIFF of your own, to see its pages and tags.
SHOW THE CODE
src/native/tiff_viewer.h
// src/native/tiff_viewer.h (excerpt): any page to RGBA, a band of rows at a time
TIFFRGBAImage image;
if (!TIFFRGBAImageBegin(&image, tif, 0, reason)) throw std::runtime_error(reason);
image.req_orientation = image.orientation; // rows as stored; the page applies Orientation
for (uint32_t top = 0; top < layout.height; top += band) {
const uint32_t rows = std::min(band, layout.height - top);
image.row_offset = static_cast<int>(top);
image.col_offset = 0;
if (!TIFFRGBAImageGet(&image, raster.data(), layout.width, rows)) { /* throw libtiff's error */ }
// each row is averaged into the smaller output as it arrives
}
TIFFRGBAImageEnd(&image);
main.js
const m = await initNative();
const [path] = await m.autoMountFiles([file], await m.getRandomPath('/memfs'));
const { pages } = JSON.parse(await m.TiffViewer.pages(path));
 
await m.FS.mkdirTree('/memfs/viewer');
const view = JSON.parse(await m.TiffViewer.render(path, 0, 1400, true, '/memfs/viewer/page.rgba'));
const rgba = await m.getFileBytes('/memfs/viewer/page.rgba');
const pixels = new Uint8ClampedArray(rgba.buffer, rgba.byteOffset, rgba.byteLength);
canvas.getContext('2d').putImageData(new ImageData(pixels, view.width, view.height), 0, 0);
APP 02

Turn page photos into one archival TIFF

Photograph a few pages, or use the two sample photos, and get one multi-page TIFF: each page is turned black and white with Otsu's threshold, then stored with CCITT Group 4, the coding of Group 4 fax. The two sample A4 pages take 19.6 KB in all, and nothing leaves this tab.

Your photos are decoded by this browser, scaled to at most 2,480 pixels on the longer side and never uploaded. Flat, evenly lit pages work best: there is no perspective correction.

Archive the sample pages, or your own photos, to see each page in black and white and what each codec makes of it.
SHOW THE CODE
src/native/scan_archive.h
// src/native/scan_archive.h (excerpt)
const int cut = threshold < 0 ? otsu(histogram, pixels) : std::min(threshold, 255);
// ... pixels at or below the cut become 1-bit black ...
tiffapps::Closer file(tiffapps::openFile(tifPath, index == 0 ? "w" : "a")); // "a" appends a page
TIFF* tif = file.get();
pageTags(tif, width, height, COMPRESSION_CCITTFAX4); // 1-bit, min-is-white, one strip
TIFFSetField(tif, TIFFTAG_SUBFILETYPE, FILETYPE_PAGE);
TIFFSetField(tif, TIFFTAG_PAGENUMBER, index, count);
TIFFSetField(tif, TIFFTAG_XRESOLUTION, static_cast<float>(dpi));
TIFFSetField(tif, TIFFTAG_YRESOLUTION, static_cast<float>(dpi));
TIFFSetField(tif, TIFFTAG_RESOLUTIONUNIT, RESUNIT_INCH);
writeRows(tif, bits, rowBytes, height);
main.js
const m = await initNative();
const dir = await m.getRandomPath('/memfs'); // a fresh directory for each run
// one page: the RGBA of a canvas the photo was drawn on
await m.FS.writeFile(`${dir}/page.rgba`, new Uint8Array(imageData.data.buffer));
const cut = JSON.parse(await m.ScanArchive.binarize(`${dir}/page.rgba`, width, height, -1, `${dir}/page.bits`));
await m.ScanArchive.addPage(`${dir}/archive.tif`, `${dir}/page.bits`, width, height, 200, 0, 1);
const tiff = await m.getFileBytes(`${dir}/archive.tif`);
APP 03

Compare every codec on 16-bit and float rasters

There is no best TIFF codec for scientific rasters, only a best one for each dataset. The module writes one raster with every codec in this build, reads each file back and compares every sample: here the floating-point predictor makes the elevation model 40% smaller with Deflate, while the horizontal predictor makes the noisy cells bigger.

Both rasters are generated in the module, so the sizes are the same on every machine. These are this data's sizes; other data will rank the codecs differently, and the same calls measure it. LERC can keep every sample, or trade a bounded error for size.

Pick a raster and compress it with every codec, to see sizes and which ones keep every sample.
SHOW THE CODE
src/native/compression_lab.h
// src/native/compression_lab.h (excerpt)
TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, kind == 0 ? 16 : 32);
TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, kind == 0 ? SAMPLEFORMAT_UINT : SAMPLEFORMAT_IEEEFP);
TIFFSetField(tif, TIFFTAG_COMPRESSION, compression);
if (predictor != PREDICTOR_NONE) TIFFSetField(tif, TIFFTAG_PREDICTOR, predictor);
if (compression == COMPRESSION_LERC) TIFFSetField(tif, TIFFTAG_LERC_MAXZERROR, maxZError);
// ... 256 x 256 tiles written, then read back with TIFFReadTile and compared sample by sample
main.js
const m = await initNative();
await m.FS.mkdirTree('/memfs/lab');
const lab = await new m.CompressionLab(1, 512); // 512 x 512 float32 elevation
// Deflate (8) with the floating-point predictor (3)
const run = JSON.parse(await lab.run(8, 3, 0, '/memfs/lab/deflate.tif'));
// run.bytes: 287494, run.maxError: 0 (every sample came back exactly)

Examples

Each one is a small C++ header and the JavaScript that calls it, with what it printed when the site was built. The JavaScript only tab does the same with libTIFF's own headers and no C++ file. The last is a whole C++ program, built for WASI.

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

A TIFF archiving tool

WASI has no JavaScript bindings: the program is a main() that links libtiff, built into one .wasm and run with wasmtime. This one rewrites every page of a TIFF losslessly, black-and-white pages as CCITT Group 4 and the others as Deflate with a predictor, the job a scan archive runs on each upload. The WASI build of libtiff has the codecs that need no library or only zlib: no JPEG, ZSTD or LERC.

crossbind.config.js
import tiffWasi from '@crossbind/port-tiff-wasi/crossbind.config.js';
 
export default {
general: { name: 'tiff-tool' },
dependencies: [tiffWasi],
paths: { config: import.meta.url },
};
src/native/main.cpp
// A command-line TIFF tool for WASI:
// tiff-tool sample <out.tif> writes a three-page, uncompressed TIFF to try it on
// tiff-tool archive <in.tif> <out.tif> rewrites every page losslessly: black-and-white pages as
// CCITT Group 4, the others as Deflate with a predictor
// tiff-tool info <file.tif> one line per page: size, samples, codec and stored bytes
#include <tiffio.h>
 
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
 
namespace {
 
using File = std::unique_ptr<TIFF, void (*)(TIFF*)>;
 
File open(const char* path, const char* mode) {
File tif(TIFFOpen(path, mode), TIFFClose);
if (!tif) std::fprintf(stderr, "cannot open %s\n", path);
return tif;
}
 
// Files are built in memory through TIFFClientOpen and saved with one fwrite. With wasi-sdk 34's
// wasm32-wasip2 and -wasip3 libc, a write that follows lseek(fd, 0, SEEK_END) lands at the old
// position; libtiff seeks that way before every page after the first, so a multi-page file written
// with TIFFOpen comes out corrupt.
class Output {
public:
Output() : tif(TIFFClientOpen("output", "w", this, read, write, seek, close, size, map, unmap), TIFFClose) {}
TIFF* get() const { return tif.get(); }
 
// Closes the TIFF, which writes its last directory, then saves the bytes.
bool save(const char* path) {
tif.reset();
std::unique_ptr<FILE, int (*)(FILE*)> file(std::fopen(path, "wb"), std::fclose);
return file && std::fwrite(bytes.data(), 1, bytes.size(), file.get()) == bytes.size();
}
 
private:
static tmsize_t read(thandle_t handle, void* buffer, tmsize_t count) {
Output* self = static_cast<Output*>(handle);
const size_t available = self->position < self->bytes.size() ? self->bytes.size() - self->position : 0;
const size_t take = std::min(available, static_cast<size_t>(count));
std::memcpy(buffer, self->bytes.data() + self->position, take);
self->position += take;
return static_cast<tmsize_t>(take);
}
static tmsize_t write(thandle_t handle, void* buffer, tmsize_t count) {
Output* self = static_cast<Output*>(handle);
if (self->bytes.size() < self->position + count) self->bytes.resize(self->position + count);
std::memcpy(&self->bytes[self->position], buffer, static_cast<size_t>(count));
self->position += count;
return count;
}
static toff_t seek(thandle_t handle, toff_t offset, int whence) {
Output* self = static_cast<Output*>(handle);
self->position = (whence == SEEK_END ? self->bytes.size() : whence == SEEK_CUR ? self->position : 0) + offset;
return self->position;
}
static int close(thandle_t) { return 0; }
static toff_t size(thandle_t handle) { return static_cast<Output*>(handle)->bytes.size(); }
static int map(thandle_t, void**, toff_t*) { return 0; }
static void unmap(thandle_t, void*, toff_t) {}
 
std::string bytes;
size_t position = 0;
std::unique_ptr<TIFF, void (*)(TIFF*)> tif;
};
 
long fileSize(const char* path) {
std::unique_ptr<FILE, int (*)(FILE*)> file(std::fopen(path, "rb"), std::fclose);
if (!file || std::fseek(file.get(), 0, SEEK_END) != 0) return -1;
return std::ftell(file.get());
}
 
std::string codecName(TIFF* tif) {
uint16_t compression = COMPRESSION_NONE;
TIFFGetFieldDefaulted(tif, TIFFTAG_COMPRESSION, &compression);
const TIFFCodec* codec = TIFFFindCODEC(compression);
return codec ? codec->name : "scheme " + std::to_string(compression);
}
 
// What the page's strips or tiles take in the file.
unsigned long long storedBytes(TIFF* tif) {
unsigned long long total = 0;
for (uint32_t strile = 0; strile < TIFFNumberOfStrips(tif); ++strile) total += TIFFGetStrileByteCount(tif, strile);
return total;
}
 
// Deterministic content: a text-like black-and-white page, a grey gradient and a colour gradient.
bool writeSample(const char* path) {
Output out;
TIFF* tif = out.get();
if (!tif) return false;
uint32_t state = 2026;
const auto random = [&state](uint32_t n) { return ((state = state * 1664525u + 1013904223u) >> 8) % n; };
for (int page = 0; page < 3; ++page) {
const uint32_t width = page == 0 ? 1240 : 800;
const uint32_t height = page == 0 ? 1754 : 600;
const uint16_t samples = page == 2 ? 3 : 1;
const uint16_t bits = page == 0 ? 1 : 8;
TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, width);
TIFFSetField(tif, TIFFTAG_IMAGELENGTH, height);
TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bits);
TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, samples);
TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, page == 0 ? PHOTOMETRIC_MINISWHITE : page == 1 ? PHOTOMETRIC_MINISBLACK : PHOTOMETRIC_RGB);
TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(tif, 0));
TIFFSetField(tif, TIFFTAG_PAGENUMBER, page, 3);
TIFFSetField(tif, TIFFTAG_PAGENAME, page == 0 ? "letter" : page == 1 ? "grey" : "colour");
std::vector<uint8_t> row(static_cast<size_t>(TIFFScanlineSize(tif)));
std::vector<uint8_t> ink(width, 0);
for (uint32_t y = 0; y < height; ++y) {
if (page == 0) {
// Lines of word-shaped blocks, 20 px tall every 36 px, inside 120 px margins.
if (y % 36 == 0) {
std::fill(ink.begin(), ink.end(), 0);
const bool text = y >= 120 && y < height - 120 && random(10) < 8;
for (uint32_t x = 120; text && x < width - 260;) {
const uint32_t word = 20 + 10 * random(13);
std::fill(ink.begin() + x, ink.begin() + x + word, 1);
x += word + 18;
}
}
std::fill(row.begin(), row.end(), 0);
if (y % 36 < 20) {
for (uint32_t x = 0; x < width; ++x) row[x / 8] |= static_cast<uint8_t>(ink[x] << (7 - x % 8));
}
} else {
for (uint32_t x = 0; x < width; ++x) {
if (page == 1) {
const int dx = static_cast<int>(x) - 400;
const int dy = static_cast<int>(y) - 300;
row[x] = static_cast<uint8_t>(255 - std::min(255, (dx * dx + dy * dy) / 1000));
} else {
row[x * 3] = static_cast<uint8_t>(x * 255 / (width - 1));
row[x * 3 + 1] = static_cast<uint8_t>(y * 255 / (height - 1));
row[x * 3 + 2] = 128;
}
}
}
if (TIFFWriteScanline(tif, row.data(), y, 0) < 0) return false;
}
if (!TIFFWriteDirectory(tif)) return false;
}
return out.save(path);
}
 
// Copies one stripped page into the next directory of `out`, recompressed.
bool archivePage(TIFF* in, TIFF* out, int page) {
uint32_t width = 0;
uint32_t height = 0;
uint16_t bits = 1;
uint16_t samples = 1;
uint16_t photometric = PHOTOMETRIC_MINISBLACK;
uint16_t format = SAMPLEFORMAT_UINT;
uint16_t planar = PLANARCONFIG_CONTIG;
TIFFGetField(in, TIFFTAG_IMAGEWIDTH, &width);
TIFFGetField(in, TIFFTAG_IMAGELENGTH, &height);
TIFFGetFieldDefaulted(in, TIFFTAG_BITSPERSAMPLE, &bits);
TIFFGetFieldDefaulted(in, TIFFTAG_SAMPLESPERPIXEL, &samples);
TIFFGetFieldDefaulted(in, TIFFTAG_SAMPLEFORMAT, &format);
TIFFGetFieldDefaulted(in, TIFFTAG_PLANARCONFIG, &planar);
TIFFGetField(in, TIFFTAG_PHOTOMETRIC, &photometric);
if (TIFFIsTiled(in) || planar != PLANARCONFIG_CONTIG) {
std::fprintf(stderr, "page %d: this tool copies pages stored in interleaved strips\n", page);
return false;
}
TIFFSetField(out, TIFFTAG_IMAGEWIDTH, width);
TIFFSetField(out, TIFFTAG_IMAGELENGTH, height);
TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, bits);
TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, samples);
TIFFSetField(out, TIFFTAG_SAMPLEFORMAT, format);
TIFFSetField(out, TIFFTAG_PHOTOMETRIC, photometric);
TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
uint16_t count = 0;
uint16_t* kinds = nullptr;
if (TIFFGetField(in, TIFFTAG_EXTRASAMPLES, &count, &kinds)) TIFFSetField(out, TIFFTAG_EXTRASAMPLES, count, kinds);
uint16_t *red = nullptr, *green = nullptr, *blue = nullptr;
if (TIFFGetField(in, TIFFTAG_COLORMAP, &red, &green, &blue)) TIFFSetField(out, TIFFTAG_COLORMAP, red, green, blue);
uint16_t number = 0, pages = 0;
if (TIFFGetField(in, TIFFTAG_PAGENUMBER, &number, &pages)) TIFFSetField(out, TIFFTAG_PAGENUMBER, number, pages);
char* name = nullptr;
if (TIFFGetField(in, TIFFTAG_PAGENAME, &name)) TIFFSetField(out, TIFFTAG_PAGENAME, name);
if (bits == 1 && samples == 1) {
TIFFSetField(out, TIFFTAG_COMPRESSION, COMPRESSION_CCITTFAX4);
TIFFSetField(out, TIFFTAG_ROWSPERSTRIP, height);
} else {
TIFFSetField(out, TIFFTAG_COMPRESSION, COMPRESSION_ADOBE_DEFLATE);
if (format == SAMPLEFORMAT_IEEEFP) {
TIFFSetField(out, TIFFTAG_PREDICTOR, PREDICTOR_FLOATINGPOINT);
} else if (bits == 8 || bits == 16 || bits == 32) {
TIFFSetField(out, TIFFTAG_PREDICTOR, PREDICTOR_HORIZONTAL);
}
TIFFSetField(out, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(out, 0));
}
std::vector<uint8_t> row(static_cast<size_t>(TIFFScanlineSize(in)));
for (uint32_t y = 0; y < height; ++y) {
if (TIFFReadScanline(in, row.data(), y, 0) < 0 || TIFFWriteScanline(out, row.data(), y, 0) < 0) {
std::fprintf(stderr, "page %d: could not copy row %u\n", page, y);
return false;
}
}
return TIFFWriteDirectory(out) == 1;
}
 
bool archive(const char* inPath, const char* outPath) {
File in = open(inPath, "r");
Output out;
if (!in || !out.get()) return false;
int page = 1;
do {
if (!archivePage(in.get(), out.get(), page)) return false;
page += 1;
} while (TIFFReadDirectory(in.get()));
return out.save(outPath);
}
 
bool info(const char* path) {
File tif = open(path, "r");
if (!tif) return false;
do {
uint32_t width = 0;
uint32_t height = 0;
uint16_t bits = 1;
uint16_t samples = 1;
char* name = nullptr;
TIFFGetField(tif.get(), TIFFTAG_IMAGEWIDTH, &width);
TIFFGetField(tif.get(), TIFFTAG_IMAGELENGTH, &height);
TIFFGetFieldDefaulted(tif.get(), TIFFTAG_BITSPERSAMPLE, &bits);
TIFFGetFieldDefaulted(tif.get(), TIFFTAG_SAMPLESPERPIXEL, &samples);
const bool named = TIFFGetField(tif.get(), TIFFTAG_PAGENAME, &name) == 1;
std::printf("page %u%s%s%s: %ux%u, %u x %u-bit, %s, %llu B\n", TIFFCurrentDirectory(tif.get()) + 1, named ? " \"" : "", named ? name : "", named ? "\"" : "", width, height,
samples, bits, codecName(tif.get()).c_str(), storedBytes(tif.get()));
} while (TIFFReadDirectory(tif.get()));
return true;
}
 
} // namespace
 
int main(int argc, char** argv) {
const std::string command = argc >= 2 ? argv[1] : "";
bool done = false;
if (command == "sample" && argc == 3) {
done = writeSample(argv[2]);
if (done) std::printf("wrote %s: 3 pages, %ld B\n", argv[2], fileSize(argv[2]));
} else if (command == "archive" && argc == 4) {
done = archive(argv[2], argv[3]);
if (done) std::printf("libtiff %s: %s %ld B -> %s %ld B\n", TIFFLIB_VERSION_STR_MAJ_MIN_MIC, argv[2], fileSize(argv[2]), argv[3], fileSize(argv[3]));
} else if (command == "info" && argc == 3) {
done = info(argv[2]);
} else {
std::fprintf(stderr, "usage: tiff-tool sample <out.tif>\n tiff-tool archive <in.tif> <out.tif>\n tiff-tool info <file.tif>\n");
return 2;
}
return done ? 0 : 1;
}
shell
npx crossbind build -p wasi -e wasi -b release
wasmtime run --dir=. .crossbind/build/tiff-tool-wasi-wasm32-st-release.wasm sample scan.tif
wasmtime run --dir=. .crossbind/build/tiff-tool-wasi-wasm32-st-release.wasm archive scan.tif archive.tif
wasmtime run --dir=. .crossbind/build/tiff-tool-wasi-wasm32-st-release.wasm info archive.tif
output
wrote scan.tif: 3 pages, 2194119 B
libtiff 4.7.2: scan.tif 2194119 B -> archive.tif 57777 B
page 1 "letter": 1240x1754, 1 x 1-bit, CCITT Group 4, 2971 B
page 2 "grey": 800x600, 1 x 8-bit, AdobeDeflate, 36879 B
page 3 "colour": 800x600, 3 x 8-bit, AdobeDeflate, 15296 B
Type to search every guide page and section.
↑↓ navigate↵ openesc close