add geotiffJS library full git

This commit is contained in:
JS 2023-10-29 22:27:19 +01:00
parent 2e27417fc5
commit aef672986a
62 changed files with 21999 additions and 0 deletions

View file

@ -0,0 +1,20 @@
import { applyPredictor } from '../predictor.js';
export default class BaseDecoder {
async decode(fileDirectory, buffer) {
const decoded = await this.decodeBlock(buffer);
const predictor = fileDirectory.Predictor || 1;
if (predictor !== 1) {
const isTiled = !fileDirectory.StripOffsets;
const tileWidth = isTiled ? fileDirectory.TileWidth : fileDirectory.ImageWidth;
const tileHeight = isTiled ? fileDirectory.TileLength : (
fileDirectory.RowsPerStrip || fileDirectory.ImageLength
);
return applyPredictor(
decoded, predictor, tileWidth, tileHeight, fileDirectory.BitsPerSample,
fileDirectory.PlanarConfiguration,
);
}
return decoded;
}
}

View file

@ -0,0 +1,8 @@
import { inflate } from 'pako';
import BaseDecoder from './basedecoder.js';
export default class DeflateDecoder extends BaseDecoder {
decodeBlock(buffer) {
return inflate(new Uint8Array(buffer)).buffer;
}
}

View file

@ -0,0 +1,35 @@
const registry = new Map();
export function addDecoder(cases, importFn) {
if (!Array.isArray(cases)) {
cases = [cases]; // eslint-disable-line no-param-reassign
}
cases.forEach((c) => registry.set(c, importFn));
}
export async function getDecoder(fileDirectory) {
const importFn = registry.get(fileDirectory.Compression);
if (!importFn) {
throw new Error(`Unknown compression method identifier: ${fileDirectory.Compression}`);
}
const Decoder = await importFn();
return new Decoder(fileDirectory);
}
// Add default decoders to registry (end-user may override with other implementations)
addDecoder([undefined, 1], () => import('./raw.js').then((m) => m.default));
addDecoder(5, () => import('./lzw.js').then((m) => m.default));
addDecoder(6, () => {
throw new Error('old style JPEG compression is not supported.');
});
addDecoder(7, () => import('./jpeg.js').then((m) => m.default));
addDecoder([8, 32946], () => import('./deflate.js').then((m) => m.default));
addDecoder(32773, () => import('./packbits.js').then((m) => m.default));
addDecoder(34887, () => import('./lerc.js')
.then(async (m) => {
await m.zstd.init();
return m;
})
.then((m) => m.default),
);
addDecoder(50001, () => import('./webimage.js').then((m) => m.default));

View file

@ -0,0 +1,897 @@
import BaseDecoder from './basedecoder.js';
/* -*- tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/*
Copyright 2011 notmasteryet
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// - The JPEG specification can be found in the ITU CCITT Recommendation T.81
// (www.w3.org/Graphics/JPEG/itu-t81.pdf)
// - The JFIF specification can be found in the JPEG File Interchange Format
// (www.w3.org/Graphics/JPEG/jfif3.pdf)
// - The Adobe Application-Specific JPEG markers in the Supporting the DCT Filters
// in PostScript Level 2, Technical Note #5116
// (partners.adobe.com/public/developer/en/ps/sdk/5116.DCT_Filter.pdf)
const dctZigZag = new Int32Array([
0,
1, 8,
16, 9, 2,
3, 10, 17, 24,
32, 25, 18, 11, 4,
5, 12, 19, 26, 33, 40,
48, 41, 34, 27, 20, 13, 6,
7, 14, 21, 28, 35, 42, 49, 56,
57, 50, 43, 36, 29, 22, 15,
23, 30, 37, 44, 51, 58,
59, 52, 45, 38, 31,
39, 46, 53, 60,
61, 54, 47,
55, 62,
63,
]);
const dctCos1 = 4017; // cos(pi/16)
const dctSin1 = 799; // sin(pi/16)
const dctCos3 = 3406; // cos(3*pi/16)
const dctSin3 = 2276; // sin(3*pi/16)
const dctCos6 = 1567; // cos(6*pi/16)
const dctSin6 = 3784; // sin(6*pi/16)
const dctSqrt2 = 5793; // sqrt(2)
const dctSqrt1d2 = 2896;// sqrt(2) / 2
function buildHuffmanTable(codeLengths, values) {
let k = 0;
const code = [];
let length = 16;
while (length > 0 && !codeLengths[length - 1]) {
--length;
}
code.push({ children: [], index: 0 });
let p = code[0];
let q;
for (let i = 0; i < length; i++) {
for (let j = 0; j < codeLengths[i]; j++) {
p = code.pop();
p.children[p.index] = values[k];
while (p.index > 0) {
p = code.pop();
}
p.index++;
code.push(p);
while (code.length <= i) {
code.push(q = { children: [], index: 0 });
p.children[p.index] = q.children;
p = q;
}
k++;
}
if (i + 1 < length) {
// p here points to last code
code.push(q = { children: [], index: 0 });
p.children[p.index] = q.children;
p = q;
}
}
return code[0].children;
}
function decodeScan(data, initialOffset,
frame, components, resetInterval,
spectralStart, spectralEnd,
successivePrev, successive) {
const { mcusPerLine, progressive } = frame;
const startOffset = initialOffset;
let offset = initialOffset;
let bitsData = 0;
let bitsCount = 0;
function readBit() {
if (bitsCount > 0) {
bitsCount--;
return (bitsData >> bitsCount) & 1;
}
bitsData = data[offset++];
if (bitsData === 0xFF) {
const nextByte = data[offset++];
if (nextByte) {
throw new Error(`unexpected marker: ${((bitsData << 8) | nextByte).toString(16)}`);
}
// unstuff 0
}
bitsCount = 7;
return bitsData >>> 7;
}
function decodeHuffman(tree) {
let node = tree;
let bit;
while ((bit = readBit()) !== null) { // eslint-disable-line no-cond-assign
node = node[bit];
if (typeof node === 'number') {
return node;
}
if (typeof node !== 'object') {
throw new Error('invalid huffman sequence');
}
}
return null;
}
function receive(initialLength) {
let length = initialLength;
let n = 0;
while (length > 0) {
const bit = readBit();
if (bit === null) {
return undefined;
}
n = (n << 1) | bit;
--length;
}
return n;
}
function receiveAndExtend(length) {
const n = receive(length);
if (n >= 1 << (length - 1)) {
return n;
}
return n + (-1 << length) + 1;
}
function decodeBaseline(component, zz) {
const t = decodeHuffman(component.huffmanTableDC);
const diff = t === 0 ? 0 : receiveAndExtend(t);
component.pred += diff;
zz[0] = component.pred;
let k = 1;
while (k < 64) {
const rs = decodeHuffman(component.huffmanTableAC);
const s = rs & 15;
const r = rs >> 4;
if (s === 0) {
if (r < 15) {
break;
}
k += 16;
} else {
k += r;
const z = dctZigZag[k];
zz[z] = receiveAndExtend(s);
k++;
}
}
}
function decodeDCFirst(component, zz) {
const t = decodeHuffman(component.huffmanTableDC);
const diff = t === 0 ? 0 : (receiveAndExtend(t) << successive);
component.pred += diff;
zz[0] = component.pred;
}
function decodeDCSuccessive(component, zz) {
zz[0] |= readBit() << successive;
}
let eobrun = 0;
function decodeACFirst(component, zz) {
if (eobrun > 0) {
eobrun--;
return;
}
let k = spectralStart;
const e = spectralEnd;
while (k <= e) {
const rs = decodeHuffman(component.huffmanTableAC);
const s = rs & 15;
const r = rs >> 4;
if (s === 0) {
if (r < 15) {
eobrun = receive(r) + (1 << r) - 1;
break;
}
k += 16;
} else {
k += r;
const z = dctZigZag[k];
zz[z] = receiveAndExtend(s) * (1 << successive);
k++;
}
}
}
let successiveACState = 0;
let successiveACNextValue;
function decodeACSuccessive(component, zz) {
let k = spectralStart;
const e = spectralEnd;
let r = 0;
while (k <= e) {
const z = dctZigZag[k];
const direction = zz[z] < 0 ? -1 : 1;
switch (successiveACState) {
case 0: { // initial state
const rs = decodeHuffman(component.huffmanTableAC);
const s = rs & 15;
r = rs >> 4;
if (s === 0) {
if (r < 15) {
eobrun = receive(r) + (1 << r);
successiveACState = 4;
} else {
r = 16;
successiveACState = 1;
}
} else {
if (s !== 1) {
throw new Error('invalid ACn encoding');
}
successiveACNextValue = receiveAndExtend(s);
successiveACState = r ? 2 : 3;
}
continue; // eslint-disable-line no-continue
}
case 1: // skipping r zero items
case 2:
if (zz[z]) {
zz[z] += (readBit() << successive) * direction;
} else {
r--;
if (r === 0) {
successiveACState = successiveACState === 2 ? 3 : 0;
}
}
break;
case 3: // set value for a zero item
if (zz[z]) {
zz[z] += (readBit() << successive) * direction;
} else {
zz[z] = successiveACNextValue << successive;
successiveACState = 0;
}
break;
case 4: // eob
if (zz[z]) {
zz[z] += (readBit() << successive) * direction;
}
break;
default:
break;
}
k++;
}
if (successiveACState === 4) {
eobrun--;
if (eobrun === 0) {
successiveACState = 0;
}
}
}
function decodeMcu(component, decodeFunction, mcu, row, col) {
const mcuRow = (mcu / mcusPerLine) | 0;
const mcuCol = mcu % mcusPerLine;
const blockRow = (mcuRow * component.v) + row;
const blockCol = (mcuCol * component.h) + col;
decodeFunction(component, component.blocks[blockRow][blockCol]);
}
function decodeBlock(component, decodeFunction, mcu) {
const blockRow = (mcu / component.blocksPerLine) | 0;
const blockCol = mcu % component.blocksPerLine;
decodeFunction(component, component.blocks[blockRow][blockCol]);
}
const componentsLength = components.length;
let component;
let i;
let j;
let k;
let n;
let decodeFn;
if (progressive) {
if (spectralStart === 0) {
decodeFn = successivePrev === 0 ? decodeDCFirst : decodeDCSuccessive;
} else {
decodeFn = successivePrev === 0 ? decodeACFirst : decodeACSuccessive;
}
} else {
decodeFn = decodeBaseline;
}
let mcu = 0;
let marker;
let mcuExpected;
if (componentsLength === 1) {
mcuExpected = components[0].blocksPerLine * components[0].blocksPerColumn;
} else {
mcuExpected = mcusPerLine * frame.mcusPerColumn;
}
const usedResetInterval = resetInterval || mcuExpected;
while (mcu < mcuExpected) {
// reset interval stuff
for (i = 0; i < componentsLength; i++) {
components[i].pred = 0;
}
eobrun = 0;
if (componentsLength === 1) {
component = components[0];
for (n = 0; n < usedResetInterval; n++) {
decodeBlock(component, decodeFn, mcu);
mcu++;
}
} else {
for (n = 0; n < usedResetInterval; n++) {
for (i = 0; i < componentsLength; i++) {
component = components[i];
const { h, v } = component;
for (j = 0; j < v; j++) {
for (k = 0; k < h; k++) {
decodeMcu(component, decodeFn, mcu, j, k);
}
}
}
mcu++;
// If we've reached our expected MCU's, stop decoding
if (mcu === mcuExpected) {
break;
}
}
}
// find marker
bitsCount = 0;
marker = (data[offset] << 8) | data[offset + 1];
if (marker < 0xFF00) {
throw new Error('marker was not found');
}
if (marker >= 0xFFD0 && marker <= 0xFFD7) { // RSTx
offset += 2;
} else {
break;
}
}
return offset - startOffset;
}
function buildComponentData(frame, component) {
const lines = [];
const { blocksPerLine, blocksPerColumn } = component;
const samplesPerLine = blocksPerLine << 3;
const R = new Int32Array(64);
const r = new Uint8Array(64);
// A port of poppler's IDCT method which in turn is taken from:
// Christoph Loeffler, Adriaan Ligtenberg, George S. Moschytz,
// "Practical Fast 1-D DCT Algorithms with 11 Multiplications",
// IEEE Intl. Conf. on Acoustics, Speech & Signal Processing, 1989,
// 988-991.
function quantizeAndInverse(zz, dataOut, dataIn) {
const qt = component.quantizationTable;
let v0;
let v1;
let v2;
let v3;
let v4;
let v5;
let v6;
let v7;
let t;
const p = dataIn;
let i;
// dequant
for (i = 0; i < 64; i++) {
p[i] = zz[i] * qt[i];
}
// inverse DCT on rows
for (i = 0; i < 8; ++i) {
const row = 8 * i;
// check for all-zero AC coefficients
if (p[1 + row] === 0 && p[2 + row] === 0 && p[3 + row] === 0
&& p[4 + row] === 0 && p[5 + row] === 0 && p[6 + row] === 0
&& p[7 + row] === 0) {
t = ((dctSqrt2 * p[0 + row]) + 512) >> 10;
p[0 + row] = t;
p[1 + row] = t;
p[2 + row] = t;
p[3 + row] = t;
p[4 + row] = t;
p[5 + row] = t;
p[6 + row] = t;
p[7 + row] = t;
continue; // eslint-disable-line no-continue
}
// stage 4
v0 = ((dctSqrt2 * p[0 + row]) + 128) >> 8;
v1 = ((dctSqrt2 * p[4 + row]) + 128) >> 8;
v2 = p[2 + row];
v3 = p[6 + row];
v4 = ((dctSqrt1d2 * (p[1 + row] - p[7 + row])) + 128) >> 8;
v7 = ((dctSqrt1d2 * (p[1 + row] + p[7 + row])) + 128) >> 8;
v5 = p[3 + row] << 4;
v6 = p[5 + row] << 4;
// stage 3
t = (v0 - v1 + 1) >> 1;
v0 = (v0 + v1 + 1) >> 1;
v1 = t;
t = ((v2 * dctSin6) + (v3 * dctCos6) + 128) >> 8;
v2 = ((v2 * dctCos6) - (v3 * dctSin6) + 128) >> 8;
v3 = t;
t = (v4 - v6 + 1) >> 1;
v4 = (v4 + v6 + 1) >> 1;
v6 = t;
t = (v7 + v5 + 1) >> 1;
v5 = (v7 - v5 + 1) >> 1;
v7 = t;
// stage 2
t = (v0 - v3 + 1) >> 1;
v0 = (v0 + v3 + 1) >> 1;
v3 = t;
t = (v1 - v2 + 1) >> 1;
v1 = (v1 + v2 + 1) >> 1;
v2 = t;
t = ((v4 * dctSin3) + (v7 * dctCos3) + 2048) >> 12;
v4 = ((v4 * dctCos3) - (v7 * dctSin3) + 2048) >> 12;
v7 = t;
t = ((v5 * dctSin1) + (v6 * dctCos1) + 2048) >> 12;
v5 = ((v5 * dctCos1) - (v6 * dctSin1) + 2048) >> 12;
v6 = t;
// stage 1
p[0 + row] = v0 + v7;
p[7 + row] = v0 - v7;
p[1 + row] = v1 + v6;
p[6 + row] = v1 - v6;
p[2 + row] = v2 + v5;
p[5 + row] = v2 - v5;
p[3 + row] = v3 + v4;
p[4 + row] = v3 - v4;
}
// inverse DCT on columns
for (i = 0; i < 8; ++i) {
const col = i;
// check for all-zero AC coefficients
if (p[(1 * 8) + col] === 0 && p[(2 * 8) + col] === 0 && p[(3 * 8) + col] === 0
&& p[(4 * 8) + col] === 0 && p[(5 * 8) + col] === 0 && p[(6 * 8) + col] === 0
&& p[(7 * 8) + col] === 0) {
t = ((dctSqrt2 * dataIn[i + 0]) + 8192) >> 14;
p[(0 * 8) + col] = t;
p[(1 * 8) + col] = t;
p[(2 * 8) + col] = t;
p[(3 * 8) + col] = t;
p[(4 * 8) + col] = t;
p[(5 * 8) + col] = t;
p[(6 * 8) + col] = t;
p[(7 * 8) + col] = t;
continue; // eslint-disable-line no-continue
}
// stage 4
v0 = ((dctSqrt2 * p[(0 * 8) + col]) + 2048) >> 12;
v1 = ((dctSqrt2 * p[(4 * 8) + col]) + 2048) >> 12;
v2 = p[(2 * 8) + col];
v3 = p[(6 * 8) + col];
v4 = ((dctSqrt1d2 * (p[(1 * 8) + col] - p[(7 * 8) + col])) + 2048) >> 12;
v7 = ((dctSqrt1d2 * (p[(1 * 8) + col] + p[(7 * 8) + col])) + 2048) >> 12;
v5 = p[(3 * 8) + col];
v6 = p[(5 * 8) + col];
// stage 3
t = (v0 - v1 + 1) >> 1;
v0 = (v0 + v1 + 1) >> 1;
v1 = t;
t = ((v2 * dctSin6) + (v3 * dctCos6) + 2048) >> 12;
v2 = ((v2 * dctCos6) - (v3 * dctSin6) + 2048) >> 12;
v3 = t;
t = (v4 - v6 + 1) >> 1;
v4 = (v4 + v6 + 1) >> 1;
v6 = t;
t = (v7 + v5 + 1) >> 1;
v5 = (v7 - v5 + 1) >> 1;
v7 = t;
// stage 2
t = (v0 - v3 + 1) >> 1;
v0 = (v0 + v3 + 1) >> 1;
v3 = t;
t = (v1 - v2 + 1) >> 1;
v1 = (v1 + v2 + 1) >> 1;
v2 = t;
t = ((v4 * dctSin3) + (v7 * dctCos3) + 2048) >> 12;
v4 = ((v4 * dctCos3) - (v7 * dctSin3) + 2048) >> 12;
v7 = t;
t = ((v5 * dctSin1) + (v6 * dctCos1) + 2048) >> 12;
v5 = ((v5 * dctCos1) - (v6 * dctSin1) + 2048) >> 12;
v6 = t;
// stage 1
p[(0 * 8) + col] = v0 + v7;
p[(7 * 8) + col] = v0 - v7;
p[(1 * 8) + col] = v1 + v6;
p[(6 * 8) + col] = v1 - v6;
p[(2 * 8) + col] = v2 + v5;
p[(5 * 8) + col] = v2 - v5;
p[(3 * 8) + col] = v3 + v4;
p[(4 * 8) + col] = v3 - v4;
}
// convert to 8-bit integers
for (i = 0; i < 64; ++i) {
const sample = 128 + ((p[i] + 8) >> 4);
if (sample < 0) {
dataOut[i] = 0;
} else if (sample > 0XFF) {
dataOut[i] = 0xFF;
} else {
dataOut[i] = sample;
}
}
}
for (let blockRow = 0; blockRow < blocksPerColumn; blockRow++) {
const scanLine = blockRow << 3;
for (let i = 0; i < 8; i++) {
lines.push(new Uint8Array(samplesPerLine));
}
for (let blockCol = 0; blockCol < blocksPerLine; blockCol++) {
quantizeAndInverse(component.blocks[blockRow][blockCol], r, R);
let offset = 0;
const sample = blockCol << 3;
for (let j = 0; j < 8; j++) {
const line = lines[scanLine + j];
for (let i = 0; i < 8; i++) {
line[sample + i] = r[offset++];
}
}
}
}
return lines;
}
class JpegStreamReader {
constructor() {
this.jfif = null;
this.adobe = null;
this.quantizationTables = [];
this.huffmanTablesAC = [];
this.huffmanTablesDC = [];
this.resetFrames();
}
resetFrames() {
this.frames = [];
}
parse(data) {
let offset = 0;
// const { length } = data;
function readUint16() {
const value = (data[offset] << 8) | data[offset + 1];
offset += 2;
return value;
}
function readDataBlock() {
const length = readUint16();
const array = data.subarray(offset, offset + length - 2);
offset += array.length;
return array;
}
function prepareComponents(frame) {
let maxH = 0;
let maxV = 0;
let component;
let componentId;
for (componentId in frame.components) {
if (frame.components.hasOwnProperty(componentId)) {
component = frame.components[componentId];
if (maxH < component.h) {
maxH = component.h;
}
if (maxV < component.v) {
maxV = component.v;
}
}
}
const mcusPerLine = Math.ceil(frame.samplesPerLine / 8 / maxH);
const mcusPerColumn = Math.ceil(frame.scanLines / 8 / maxV);
for (componentId in frame.components) {
if (frame.components.hasOwnProperty(componentId)) {
component = frame.components[componentId];
const blocksPerLine = Math.ceil(Math.ceil(frame.samplesPerLine / 8) * component.h / maxH);
const blocksPerColumn = Math.ceil(Math.ceil(frame.scanLines / 8) * component.v / maxV);
const blocksPerLineForMcu = mcusPerLine * component.h;
const blocksPerColumnForMcu = mcusPerColumn * component.v;
const blocks = [];
for (let i = 0; i < blocksPerColumnForMcu; i++) {
const row = [];
for (let j = 0; j < blocksPerLineForMcu; j++) {
row.push(new Int32Array(64));
}
blocks.push(row);
}
component.blocksPerLine = blocksPerLine;
component.blocksPerColumn = blocksPerColumn;
component.blocks = blocks;
}
}
frame.maxH = maxH;
frame.maxV = maxV;
frame.mcusPerLine = mcusPerLine;
frame.mcusPerColumn = mcusPerColumn;
}
let fileMarker = readUint16();
if (fileMarker !== 0xFFD8) { // SOI (Start of Image)
throw new Error('SOI not found');
}
fileMarker = readUint16();
while (fileMarker !== 0xFFD9) { // EOI (End of image)
switch (fileMarker) {
case 0xFF00: break;
case 0xFFE0: // APP0 (Application Specific)
case 0xFFE1: // APP1
case 0xFFE2: // APP2
case 0xFFE3: // APP3
case 0xFFE4: // APP4
case 0xFFE5: // APP5
case 0xFFE6: // APP6
case 0xFFE7: // APP7
case 0xFFE8: // APP8
case 0xFFE9: // APP9
case 0xFFEA: // APP10
case 0xFFEB: // APP11
case 0xFFEC: // APP12
case 0xFFED: // APP13
case 0xFFEE: // APP14
case 0xFFEF: // APP15
case 0xFFFE: { // COM (Comment)
const appData = readDataBlock();
if (fileMarker === 0xFFE0) {
if (appData[0] === 0x4A && appData[1] === 0x46 && appData[2] === 0x49
&& appData[3] === 0x46 && appData[4] === 0) { // 'JFIF\x00'
this.jfif = {
version: { major: appData[5], minor: appData[6] },
densityUnits: appData[7],
xDensity: (appData[8] << 8) | appData[9],
yDensity: (appData[10] << 8) | appData[11],
thumbWidth: appData[12],
thumbHeight: appData[13],
thumbData: appData.subarray(14, 14 + (3 * appData[12] * appData[13])),
};
}
}
// TODO APP1 - Exif
if (fileMarker === 0xFFEE) {
if (appData[0] === 0x41 && appData[1] === 0x64 && appData[2] === 0x6F
&& appData[3] === 0x62 && appData[4] === 0x65 && appData[5] === 0) { // 'Adobe\x00'
this.adobe = {
version: appData[6],
flags0: (appData[7] << 8) | appData[8],
flags1: (appData[9] << 8) | appData[10],
transformCode: appData[11],
};
}
}
break;
}
case 0xFFDB: { // DQT (Define Quantization Tables)
const quantizationTablesLength = readUint16();
const quantizationTablesEnd = quantizationTablesLength + offset - 2;
while (offset < quantizationTablesEnd) {
const quantizationTableSpec = data[offset++];
const tableData = new Int32Array(64);
if ((quantizationTableSpec >> 4) === 0) { // 8 bit values
for (let j = 0; j < 64; j++) {
const z = dctZigZag[j];
tableData[z] = data[offset++];
}
} else if ((quantizationTableSpec >> 4) === 1) { // 16 bit
for (let j = 0; j < 64; j++) {
const z = dctZigZag[j];
tableData[z] = readUint16();
}
} else {
throw new Error('DQT: invalid table spec');
}
this.quantizationTables[quantizationTableSpec & 15] = tableData;
}
break;
}
case 0xFFC0: // SOF0 (Start of Frame, Baseline DCT)
case 0xFFC1: // SOF1 (Start of Frame, Extended DCT)
case 0xFFC2: { // SOF2 (Start of Frame, Progressive DCT)
readUint16(); // skip data length
const frame = {
extended: (fileMarker === 0xFFC1),
progressive: (fileMarker === 0xFFC2),
precision: data[offset++],
scanLines: readUint16(),
samplesPerLine: readUint16(),
components: {},
componentsOrder: [],
};
const componentsCount = data[offset++];
let componentId;
// let maxH = 0;
// let maxV = 0;
for (let i = 0; i < componentsCount; i++) {
componentId = data[offset];
const h = data[offset + 1] >> 4;
const v = data[offset + 1] & 15;
const qId = data[offset + 2];
frame.componentsOrder.push(componentId);
frame.components[componentId] = {
h,
v,
quantizationIdx: qId,
};
offset += 3;
}
prepareComponents(frame);
this.frames.push(frame);
break;
}
case 0xFFC4: { // DHT (Define Huffman Tables)
const huffmanLength = readUint16();
for (let i = 2; i < huffmanLength;) {
const huffmanTableSpec = data[offset++];
const codeLengths = new Uint8Array(16);
let codeLengthSum = 0;
for (let j = 0; j < 16; j++, offset++) {
codeLengths[j] = data[offset];
codeLengthSum += codeLengths[j];
}
const huffmanValues = new Uint8Array(codeLengthSum);
for (let j = 0; j < codeLengthSum; j++, offset++) {
huffmanValues[j] = data[offset];
}
i += 17 + codeLengthSum;
if ((huffmanTableSpec >> 4) === 0) {
this.huffmanTablesDC[huffmanTableSpec & 15] = buildHuffmanTable(
codeLengths, huffmanValues,
);
} else {
this.huffmanTablesAC[huffmanTableSpec & 15] = buildHuffmanTable(
codeLengths, huffmanValues,
);
}
}
break;
}
case 0xFFDD: // DRI (Define Restart Interval)
readUint16(); // skip data length
this.resetInterval = readUint16();
break;
case 0xFFDA: { // SOS (Start of Scan)
readUint16(); // skip length
const selectorsCount = data[offset++];
const components = [];
const frame = this.frames[0];
for (let i = 0; i < selectorsCount; i++) {
const component = frame.components[data[offset++]];
const tableSpec = data[offset++];
component.huffmanTableDC = this.huffmanTablesDC[tableSpec >> 4];
component.huffmanTableAC = this.huffmanTablesAC[tableSpec & 15];
components.push(component);
}
const spectralStart = data[offset++];
const spectralEnd = data[offset++];
const successiveApproximation = data[offset++];
const processed = decodeScan(data, offset,
frame, components, this.resetInterval,
spectralStart, spectralEnd,
successiveApproximation >> 4, successiveApproximation & 15);
offset += processed;
break;
}
case 0xFFFF: // Fill bytes
if (data[offset] !== 0xFF) { // Avoid skipping a valid marker.
offset--;
}
break;
default:
if (data[offset - 3] === 0xFF
&& data[offset - 2] >= 0xC0 && data[offset - 2] <= 0xFE) {
// could be incorrect encoding -- last 0xFF byte of the previous
// block was eaten by the encoder
offset -= 3;
break;
}
throw new Error(`unknown JPEG marker ${fileMarker.toString(16)}`);
}
fileMarker = readUint16();
}
}
getResult() {
const { frames } = this;
if (this.frames.length === 0) {
throw new Error('no frames were decoded');
} else if (this.frames.length > 1) {
console.warn('more than one frame is not supported');
}
// set each frame's components quantization table
for (let i = 0; i < this.frames.length; i++) {
const cp = this.frames[i].components;
for (const j of Object.keys(cp)) {
cp[j].quantizationTable = this.quantizationTables[cp[j].quantizationIdx];
delete cp[j].quantizationIdx;
}
}
const frame = frames[0];
const { components, componentsOrder } = frame;
const outComponents = [];
const width = frame.samplesPerLine;
const height = frame.scanLines;
for (let i = 0; i < componentsOrder.length; i++) {
const component = components[componentsOrder[i]];
outComponents.push({
lines: buildComponentData(frame, component),
scaleX: component.h / frame.maxH,
scaleY: component.v / frame.maxV,
});
}
const out = new Uint8Array(width * height * outComponents.length);
let oi = 0;
for (let y = 0; y < height; ++y) {
for (let x = 0; x < width; ++x) {
for (let i = 0; i < outComponents.length; ++i) {
const component = outComponents[i];
out[oi] = component.lines[0 | y * component.scaleY][0 | x * component.scaleX];
++oi;
}
}
}
return out;
}
}
export default class JpegDecoder extends BaseDecoder {
constructor(fileDirectory) {
super();
this.reader = new JpegStreamReader();
if (fileDirectory.JPEGTables) {
this.reader.parse(fileDirectory.JPEGTables);
}
}
decodeBlock(buffer) {
this.reader.resetFrames();
this.reader.parse(new Uint8Array(buffer));
return this.reader.getResult().buffer;
}
}

View file

@ -0,0 +1,37 @@
import { inflate } from 'pako';
import Lerc from 'lerc';
import { ZSTDDecoder } from 'zstddec';
import BaseDecoder from './basedecoder.js';
import { LercParameters, LercAddCompression } from '../globals.js';
export const zstd = new ZSTDDecoder();
export default class LercDecoder extends BaseDecoder {
constructor(fileDirectory) {
super();
this.planarConfiguration = typeof fileDirectory.PlanarConfiguration !== 'undefined' ? fileDirectory.PlanarConfiguration : 1;
this.samplesPerPixel = typeof fileDirectory.SamplesPerPixel !== 'undefined' ? fileDirectory.SamplesPerPixel : 1;
this.addCompression = fileDirectory.LercParameters[LercParameters.AddCompression];
}
decodeBlock(buffer) {
switch (this.addCompression) {
case LercAddCompression.None:
break;
case LercAddCompression.Deflate:
buffer = inflate(new Uint8Array(buffer)).buffer; // eslint-disable-line no-param-reassign, prefer-destructuring
break;
case LercAddCompression.Zstandard:
buffer = zstd.decode(new Uint8Array(buffer)).buffer; // eslint-disable-line no-param-reassign, prefer-destructuring
break;
default:
throw new Error(`Unsupported LERC additional compression method identifier: ${this.addCompression}`);
}
const lercResult = Lerc.decode(buffer, { returnPixelInterleavedDims: this.planarConfiguration === 1 });
const lercData = lercResult.pixels[0];
return lercData.buffer;
}
}

View file

@ -0,0 +1,131 @@
import BaseDecoder from './basedecoder.js';
const MIN_BITS = 9;
const CLEAR_CODE = 256; // clear code
const EOI_CODE = 257; // end of information
const MAX_BYTELENGTH = 12;
function getByte(array, position, length) {
const d = position % 8;
const a = Math.floor(position / 8);
const de = 8 - d;
const ef = (position + length) - ((a + 1) * 8);
let fg = (8 * (a + 2)) - (position + length);
const dg = ((a + 2) * 8) - position;
fg = Math.max(0, fg);
if (a >= array.length) {
console.warn('ran off the end of the buffer before finding EOI_CODE (end on input code)');
return EOI_CODE;
}
let chunk1 = array[a] & ((2 ** (8 - d)) - 1);
chunk1 <<= (length - de);
let chunks = chunk1;
if (a + 1 < array.length) {
let chunk2 = array[a + 1] >>> fg;
chunk2 <<= Math.max(0, (length - dg));
chunks += chunk2;
}
if (ef > 8 && a + 2 < array.length) {
const hi = ((a + 3) * 8) - (position + length);
const chunk3 = array[a + 2] >>> hi;
chunks += chunk3;
}
return chunks;
}
function appendReversed(dest, source) {
for (let i = source.length - 1; i >= 0; i--) {
dest.push(source[i]);
}
return dest;
}
function decompress(input) {
const dictionaryIndex = new Uint16Array(4093);
const dictionaryChar = new Uint8Array(4093);
for (let i = 0; i <= 257; i++) {
dictionaryIndex[i] = 4096;
dictionaryChar[i] = i;
}
let dictionaryLength = 258;
let byteLength = MIN_BITS;
let position = 0;
function initDictionary() {
dictionaryLength = 258;
byteLength = MIN_BITS;
}
function getNext(array) {
const byte = getByte(array, position, byteLength);
position += byteLength;
return byte;
}
function addToDictionary(i, c) {
dictionaryChar[dictionaryLength] = c;
dictionaryIndex[dictionaryLength] = i;
dictionaryLength++;
return dictionaryLength - 1;
}
function getDictionaryReversed(n) {
const rev = [];
for (let i = n; i !== 4096; i = dictionaryIndex[i]) {
rev.push(dictionaryChar[i]);
}
return rev;
}
const result = [];
initDictionary();
const array = new Uint8Array(input);
let code = getNext(array);
let oldCode;
while (code !== EOI_CODE) {
if (code === CLEAR_CODE) {
initDictionary();
code = getNext(array);
while (code === CLEAR_CODE) {
code = getNext(array);
}
if (code === EOI_CODE) {
break;
} else if (code > CLEAR_CODE) {
throw new Error(`corrupted code at scanline ${code}`);
} else {
const val = getDictionaryReversed(code);
appendReversed(result, val);
oldCode = code;
}
} else if (code < dictionaryLength) {
const val = getDictionaryReversed(code);
appendReversed(result, val);
addToDictionary(oldCode, val[val.length - 1]);
oldCode = code;
} else {
const oldVal = getDictionaryReversed(oldCode);
if (!oldVal) {
throw new Error(`Bogus entry. Not in dictionary, ${oldCode} / ${dictionaryLength}, position: ${position}`);
}
appendReversed(result, oldVal);
result.push(oldVal[oldVal.length - 1]);
addToDictionary(oldCode, oldVal[oldVal.length - 1]);
oldCode = code;
}
if (dictionaryLength + 1 >= (2 ** byteLength)) {
if (byteLength === MAX_BYTELENGTH) {
oldCode = undefined;
} else {
byteLength++;
}
}
code = getNext(array);
}
return new Uint8Array(result);
}
export default class LZWDecoder extends BaseDecoder {
decodeBlock(buffer) {
return decompress(buffer, false).buffer;
}
}

View file

@ -0,0 +1,26 @@
import BaseDecoder from './basedecoder.js';
export default class PackbitsDecoder extends BaseDecoder {
decodeBlock(buffer) {
const dataView = new DataView(buffer);
const out = [];
for (let i = 0; i < buffer.byteLength; ++i) {
let header = dataView.getInt8(i);
if (header < 0) {
const next = dataView.getUint8(i + 1);
header = -header;
for (let j = 0; j <= header; ++j) {
out.push(next);
}
i += 1;
} else {
for (let j = 0; j <= header; ++j) {
out.push(dataView.getUint8(i + j + 1));
}
i += header + 1;
}
}
return new Uint8Array(out).buffer;
}
}

View file

@ -0,0 +1,7 @@
import BaseDecoder from './basedecoder.js';
export default class RawDecoder extends BaseDecoder {
decodeBlock(buffer) {
return buffer;
}
}

View file

@ -0,0 +1,40 @@
import BaseDecoder from './basedecoder.js';
/**
* class WebImageDecoder
*
* This decoder uses the browsers image decoding facilities to read image
* formats like WebP when supported.
*/
export default class WebImageDecoder extends BaseDecoder {
constructor() {
super();
if (typeof createImageBitmap === 'undefined') {
throw new Error('Cannot decode WebImage as `createImageBitmap` is not available');
} else if (typeof document === 'undefined' && typeof OffscreenCanvas === 'undefined') {
throw new Error('Cannot decode WebImage as neither `document` nor `OffscreenCanvas` is not available');
}
}
async decode(fileDirectory, buffer) {
const blob = new Blob([buffer]);
const imageBitmap = await createImageBitmap(blob);
let canvas;
if (typeof document !== 'undefined') {
canvas = document.createElement('canvas');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
} else {
canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
}
const ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
// TODO: check how many samples per pixel we have, and return RGB/RGBA accordingly
// it seems like GDAL always encodes via RGBA which does not require a translation
return ctx.getImageData(0, 0, imageBitmap.width, imageBitmap.height).data.buffer;
}
}

View file

@ -0,0 +1,140 @@
export default class DataSlice {
constructor(arrayBuffer, sliceOffset, littleEndian, bigTiff) {
this._dataView = new DataView(arrayBuffer);
this._sliceOffset = sliceOffset;
this._littleEndian = littleEndian;
this._bigTiff = bigTiff;
}
get sliceOffset() {
return this._sliceOffset;
}
get sliceTop() {
return this._sliceOffset + this.buffer.byteLength;
}
get littleEndian() {
return this._littleEndian;
}
get bigTiff() {
return this._bigTiff;
}
get buffer() {
return this._dataView.buffer;
}
covers(offset, length) {
return this.sliceOffset <= offset && this.sliceTop >= offset + length;
}
readUint8(offset) {
return this._dataView.getUint8(
offset - this._sliceOffset, this._littleEndian,
);
}
readInt8(offset) {
return this._dataView.getInt8(
offset - this._sliceOffset, this._littleEndian,
);
}
readUint16(offset) {
return this._dataView.getUint16(
offset - this._sliceOffset, this._littleEndian,
);
}
readInt16(offset) {
return this._dataView.getInt16(
offset - this._sliceOffset, this._littleEndian,
);
}
readUint32(offset) {
return this._dataView.getUint32(
offset - this._sliceOffset, this._littleEndian,
);
}
readInt32(offset) {
return this._dataView.getInt32(
offset - this._sliceOffset, this._littleEndian,
);
}
readFloat32(offset) {
return this._dataView.getFloat32(
offset - this._sliceOffset, this._littleEndian,
);
}
readFloat64(offset) {
return this._dataView.getFloat64(
offset - this._sliceOffset, this._littleEndian,
);
}
readUint64(offset) {
const left = this.readUint32(offset);
const right = this.readUint32(offset + 4);
let combined;
if (this._littleEndian) {
combined = left + ((2 ** 32) * right);
if (!Number.isSafeInteger(combined)) {
throw new Error(
`${combined} exceeds MAX_SAFE_INTEGER. `
+ 'Precision may be lost. Please report if you get this message to https://github.com/geotiffjs/geotiff.js/issues',
);
}
return combined;
}
combined = ((2 ** 32) * left) + right;
if (!Number.isSafeInteger(combined)) {
throw new Error(
`${combined} exceeds MAX_SAFE_INTEGER. `
+ 'Precision may be lost. Please report if you get this message to https://github.com/geotiffjs/geotiff.js/issues',
);
}
return combined;
}
// adapted from https://stackoverflow.com/a/55338384/8060591
readInt64(offset) {
let value = 0;
const isNegative = (this._dataView.getUint8(offset + (this._littleEndian ? 7 : 0)) & 0x80)
> 0;
let carrying = true;
for (let i = 0; i < 8; i++) {
let byte = this._dataView.getUint8(
offset + (this._littleEndian ? i : 7 - i),
);
if (isNegative) {
if (carrying) {
if (byte !== 0x00) {
byte = ~(byte - 1) & 0xff;
carrying = false;
}
} else {
byte = ~byte & 0xff;
}
}
value += byte * (256 ** i);
}
if (isNegative) {
value = -value;
}
return value;
}
readOffset(offset) {
if (this._bigTiff) {
return this.readUint64(offset);
}
return this.readUint32(offset);
}
}

View file

@ -0,0 +1,97 @@
import { getFloat16 } from '@petamoriken/float16';
export default class DataView64 {
constructor(arrayBuffer) {
this._dataView = new DataView(arrayBuffer);
}
get buffer() {
return this._dataView.buffer;
}
getUint64(offset, littleEndian) {
const left = this.getUint32(offset, littleEndian);
const right = this.getUint32(offset + 4, littleEndian);
let combined;
if (littleEndian) {
combined = left + ((2 ** 32) * right);
if (!Number.isSafeInteger(combined)) {
throw new Error(
`${combined} exceeds MAX_SAFE_INTEGER. `
+ 'Precision may be lost. Please report if you get this message to https://github.com/geotiffjs/geotiff.js/issues',
);
}
return combined;
}
combined = ((2 ** 32) * left) + right;
if (!Number.isSafeInteger(combined)) {
throw new Error(
`${combined} exceeds MAX_SAFE_INTEGER. `
+ 'Precision may be lost. Please report if you get this message to https://github.com/geotiffjs/geotiff.js/issues',
);
}
return combined;
}
// adapted from https://stackoverflow.com/a/55338384/8060591
getInt64(offset, littleEndian) {
let value = 0;
const isNegative = (this._dataView.getUint8(offset + (littleEndian ? 7 : 0)) & 0x80) > 0;
let carrying = true;
for (let i = 0; i < 8; i++) {
let byte = this._dataView.getUint8(offset + (littleEndian ? i : 7 - i));
if (isNegative) {
if (carrying) {
if (byte !== 0x00) {
byte = ~(byte - 1) & 0xff;
carrying = false;
}
} else {
byte = ~byte & 0xff;
}
}
value += byte * (256 ** i);
}
if (isNegative) {
value = -value;
}
return value;
}
getUint8(offset, littleEndian) {
return this._dataView.getUint8(offset, littleEndian);
}
getInt8(offset, littleEndian) {
return this._dataView.getInt8(offset, littleEndian);
}
getUint16(offset, littleEndian) {
return this._dataView.getUint16(offset, littleEndian);
}
getInt16(offset, littleEndian) {
return this._dataView.getInt16(offset, littleEndian);
}
getUint32(offset, littleEndian) {
return this._dataView.getUint32(offset, littleEndian);
}
getInt32(offset, littleEndian) {
return this._dataView.getInt32(offset, littleEndian);
}
getFloat16(offset, littleEndian) {
return getFloat16(this._dataView, offset, littleEndian);
}
getFloat32(offset, littleEndian) {
return this._dataView.getFloat32(offset, littleEndian);
}
getFloat64(offset, littleEndian) {
return this._dataView.getFloat64(offset, littleEndian);
}
}

View file

@ -0,0 +1,774 @@
/** @module geotiff */
import GeoTIFFImage from './geotiffimage.js';
import DataView64 from './dataview64.js';
import DataSlice from './dataslice.js';
import Pool from './pool.js';
import { makeRemoteSource, makeCustomSource } from './source/remote.js';
import { makeBufferSource } from './source/arraybuffer.js';
import { makeFileReaderSource } from './source/filereader.js';
import { makeFileSource } from './source/file.js';
import { BaseClient, BaseResponse } from './source/client/base.js';
import { fieldTypes, fieldTagNames, arrayFields, geoKeyNames } from './globals.js';
import { writeGeotiff } from './geotiffwriter.js';
import * as globals from './globals.js';
import * as rgb from './rgb.js';
import { getDecoder, addDecoder } from './compression/index.js';
import { setLogger } from './logging.js';
export { globals };
export { rgb };
export { default as BaseDecoder } from './compression/basedecoder.js';
export { getDecoder, addDecoder };
export { setLogger };
/**
* @typedef {Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array | Float32Array | Float64Array}
* TypedArray
*/
/**
* @typedef {{ height:number, width: number }} Dimensions
*/
/**
* The autogenerated docs are a little confusing here. The effective type is:
*
* `TypedArray & { height: number; width: number}`
* @typedef {TypedArray & Dimensions} TypedArrayWithDimensions
*/
/**
* The autogenerated docs are a little confusing here. The effective type is:
*
* `TypedArray[] & { height: number; width: number}`
* @typedef {TypedArray[] & Dimensions} TypedArrayArrayWithDimensions
*/
/**
* The autogenerated docs are a little confusing here. The effective type is:
*
* `(TypedArray | TypedArray[]) & { height: number; width: number}`
* @typedef {TypedArrayWithDimensions | TypedArrayArrayWithDimensions} ReadRasterResult
*/
function getFieldTypeLength(fieldType) {
switch (fieldType) {
case fieldTypes.BYTE: case fieldTypes.ASCII: case fieldTypes.SBYTE: case fieldTypes.UNDEFINED:
return 1;
case fieldTypes.SHORT: case fieldTypes.SSHORT:
return 2;
case fieldTypes.LONG: case fieldTypes.SLONG: case fieldTypes.FLOAT: case fieldTypes.IFD:
return 4;
case fieldTypes.RATIONAL: case fieldTypes.SRATIONAL: case fieldTypes.DOUBLE:
case fieldTypes.LONG8: case fieldTypes.SLONG8: case fieldTypes.IFD8:
return 8;
default:
throw new RangeError(`Invalid field type: ${fieldType}`);
}
}
function parseGeoKeyDirectory(fileDirectory) {
const rawGeoKeyDirectory = fileDirectory.GeoKeyDirectory;
if (!rawGeoKeyDirectory) {
return null;
}
const geoKeyDirectory = {};
for (let i = 4; i <= rawGeoKeyDirectory[3] * 4; i += 4) {
const key = geoKeyNames[rawGeoKeyDirectory[i]];
const location = (rawGeoKeyDirectory[i + 1])
? (fieldTagNames[rawGeoKeyDirectory[i + 1]]) : null;
const count = rawGeoKeyDirectory[i + 2];
const offset = rawGeoKeyDirectory[i + 3];
let value = null;
if (!location) {
value = offset;
} else {
value = fileDirectory[location];
if (typeof value === 'undefined' || value === null) {
throw new Error(`Could not get value of geoKey '${key}'.`);
} else if (typeof value === 'string') {
value = value.substring(offset, offset + count - 1);
} else if (value.subarray) {
value = value.subarray(offset, offset + count);
if (count === 1) {
value = value[0];
}
}
}
geoKeyDirectory[key] = value;
}
return geoKeyDirectory;
}
function getValues(dataSlice, fieldType, count, offset) {
let values = null;
let readMethod = null;
const fieldTypeLength = getFieldTypeLength(fieldType);
switch (fieldType) {
case fieldTypes.BYTE: case fieldTypes.ASCII: case fieldTypes.UNDEFINED:
values = new Uint8Array(count); readMethod = dataSlice.readUint8;
break;
case fieldTypes.SBYTE:
values = new Int8Array(count); readMethod = dataSlice.readInt8;
break;
case fieldTypes.SHORT:
values = new Uint16Array(count); readMethod = dataSlice.readUint16;
break;
case fieldTypes.SSHORT:
values = new Int16Array(count); readMethod = dataSlice.readInt16;
break;
case fieldTypes.LONG: case fieldTypes.IFD:
values = new Uint32Array(count); readMethod = dataSlice.readUint32;
break;
case fieldTypes.SLONG:
values = new Int32Array(count); readMethod = dataSlice.readInt32;
break;
case fieldTypes.LONG8: case fieldTypes.IFD8:
values = new Array(count); readMethod = dataSlice.readUint64;
break;
case fieldTypes.SLONG8:
values = new Array(count); readMethod = dataSlice.readInt64;
break;
case fieldTypes.RATIONAL:
values = new Uint32Array(count * 2); readMethod = dataSlice.readUint32;
break;
case fieldTypes.SRATIONAL:
values = new Int32Array(count * 2); readMethod = dataSlice.readInt32;
break;
case fieldTypes.FLOAT:
values = new Float32Array(count); readMethod = dataSlice.readFloat32;
break;
case fieldTypes.DOUBLE:
values = new Float64Array(count); readMethod = dataSlice.readFloat64;
break;
default:
throw new RangeError(`Invalid field type: ${fieldType}`);
}
// normal fields
if (!(fieldType === fieldTypes.RATIONAL || fieldType === fieldTypes.SRATIONAL)) {
for (let i = 0; i < count; ++i) {
values[i] = readMethod.call(
dataSlice, offset + (i * fieldTypeLength),
);
}
} else { // RATIONAL or SRATIONAL
for (let i = 0; i < count; i += 2) {
values[i] = readMethod.call(
dataSlice, offset + (i * fieldTypeLength),
);
values[i + 1] = readMethod.call(
dataSlice, offset + ((i * fieldTypeLength) + 4),
);
}
}
if (fieldType === fieldTypes.ASCII) {
return new TextDecoder('utf-8').decode(values);
}
return values;
}
/**
* Data class to store the parsed file directory, geo key directory and
* offset to the next IFD
*/
class ImageFileDirectory {
constructor(fileDirectory, geoKeyDirectory, nextIFDByteOffset) {
this.fileDirectory = fileDirectory;
this.geoKeyDirectory = geoKeyDirectory;
this.nextIFDByteOffset = nextIFDByteOffset;
}
}
/**
* Error class for cases when an IFD index was requested, that does not exist
* in the file.
*/
class GeoTIFFImageIndexError extends Error {
constructor(index) {
super(`No image at index ${index}`);
this.index = index;
}
}
class GeoTIFFBase {
/**
* (experimental) Reads raster data from the best fitting image. This function uses
* the image with the lowest resolution that is still a higher resolution than the
* requested resolution.
* When specified, the `bbox` option is translated to the `window` option and the
* `resX` and `resY` to `width` and `height` respectively.
* Then, the [readRasters]{@link GeoTIFFImage#readRasters} method of the selected
* image is called and the result returned.
* @see GeoTIFFImage.readRasters
* @param {import('./geotiffimage').ReadRasterOptions} [options={}] optional parameters
* @returns {Promise<ReadRasterResult>} the decoded array(s), with `height` and `width`, as a promise
*/
async readRasters(options = {}) {
const { window: imageWindow, width, height } = options;
let { resX, resY, bbox } = options;
const firstImage = await this.getImage();
let usedImage = firstImage;
const imageCount = await this.getImageCount();
const imgBBox = firstImage.getBoundingBox();
if (imageWindow && bbox) {
throw new Error('Both "bbox" and "window" passed.');
}
// if width/height is passed, transform it to resolution
if (width || height) {
// if we have an image window (pixel coordinates), transform it to a BBox
// using the origin/resolution of the first image.
if (imageWindow) {
const [oX, oY] = firstImage.getOrigin();
const [rX, rY] = firstImage.getResolution();
bbox = [
oX + (imageWindow[0] * rX),
oY + (imageWindow[1] * rY),
oX + (imageWindow[2] * rX),
oY + (imageWindow[3] * rY),
];
}
// if we have a bbox (or calculated one)
const usedBBox = bbox || imgBBox;
if (width) {
if (resX) {
throw new Error('Both width and resX passed');
}
resX = (usedBBox[2] - usedBBox[0]) / width;
}
if (height) {
if (resY) {
throw new Error('Both width and resY passed');
}
resY = (usedBBox[3] - usedBBox[1]) / height;
}
}
// if resolution is set or calculated, try to get the image with the worst acceptable resolution
if (resX || resY) {
const allImages = [];
for (let i = 0; i < imageCount; ++i) {
const image = await this.getImage(i);
const { SubfileType: subfileType, NewSubfileType: newSubfileType } = image.fileDirectory;
if (i === 0 || subfileType === 2 || newSubfileType & 1) {
allImages.push(image);
}
}
allImages.sort((a, b) => a.getWidth() - b.getWidth());
for (let i = 0; i < allImages.length; ++i) {
const image = allImages[i];
const imgResX = (imgBBox[2] - imgBBox[0]) / image.getWidth();
const imgResY = (imgBBox[3] - imgBBox[1]) / image.getHeight();
usedImage = image;
if ((resX && resX > imgResX) || (resY && resY > imgResY)) {
break;
}
}
}
let wnd = imageWindow;
if (bbox) {
const [oX, oY] = firstImage.getOrigin();
const [imageResX, imageResY] = usedImage.getResolution(firstImage);
wnd = [
Math.round((bbox[0] - oX) / imageResX),
Math.round((bbox[1] - oY) / imageResY),
Math.round((bbox[2] - oX) / imageResX),
Math.round((bbox[3] - oY) / imageResY),
];
wnd = [
Math.min(wnd[0], wnd[2]),
Math.min(wnd[1], wnd[3]),
Math.max(wnd[0], wnd[2]),
Math.max(wnd[1], wnd[3]),
];
}
return usedImage.readRasters({ ...options, window: wnd });
}
}
/**
* @typedef {Object} GeoTIFFOptions
* @property {boolean} [cache=false] whether or not decoded tiles shall be cached.
*/
/**
* The abstraction for a whole GeoTIFF file.
* @augments GeoTIFFBase
*/
class GeoTIFF extends GeoTIFFBase {
/**
* @constructor
* @param {*} source The datasource to read from.
* @param {boolean} littleEndian Whether the image uses little endian.
* @param {boolean} bigTiff Whether the image uses bigTIFF conventions.
* @param {number} firstIFDOffset The numeric byte-offset from the start of the image
* to the first IFD.
* @param {GeoTIFFOptions} [options] further options.
*/
constructor(source, littleEndian, bigTiff, firstIFDOffset, options = {}) {
super();
this.source = source;
this.littleEndian = littleEndian;
this.bigTiff = bigTiff;
this.firstIFDOffset = firstIFDOffset;
this.cache = options.cache || false;
this.ifdRequests = [];
this.ghostValues = null;
}
async getSlice(offset, size) {
const fallbackSize = this.bigTiff ? 4048 : 1024;
return new DataSlice(
(await this.source.fetch([{
offset,
length: typeof size !== 'undefined' ? size : fallbackSize,
}]))[0],
offset,
this.littleEndian,
this.bigTiff,
);
}
/**
* Instructs to parse an image file directory at the given file offset.
* As there is no way to ensure that a location is indeed the start of an IFD,
* this function must be called with caution (e.g only using the IFD offsets from
* the headers or other IFDs).
* @param {number} offset the offset to parse the IFD at
* @returns {Promise<ImageFileDirectory>} the parsed IFD
*/
async parseFileDirectoryAt(offset) {
const entrySize = this.bigTiff ? 20 : 12;
const offsetSize = this.bigTiff ? 8 : 2;
let dataSlice = await this.getSlice(offset);
const numDirEntries = this.bigTiff
? dataSlice.readUint64(offset)
: dataSlice.readUint16(offset);
// if the slice does not cover the whole IFD, request a bigger slice, where the
// whole IFD fits: num of entries + n x tag length + offset to next IFD
const byteSize = (numDirEntries * entrySize) + (this.bigTiff ? 16 : 6);
if (!dataSlice.covers(offset, byteSize)) {
dataSlice = await this.getSlice(offset, byteSize);
}
const fileDirectory = {};
// loop over the IFD and create a file directory object
let i = offset + (this.bigTiff ? 8 : 2);
for (let entryCount = 0; entryCount < numDirEntries; i += entrySize, ++entryCount) {
const fieldTag = dataSlice.readUint16(i);
const fieldType = dataSlice.readUint16(i + 2);
const typeCount = this.bigTiff
? dataSlice.readUint64(i + 4)
: dataSlice.readUint32(i + 4);
let fieldValues;
let value;
const fieldTypeLength = getFieldTypeLength(fieldType);
const valueOffset = i + (this.bigTiff ? 12 : 8);
// check whether the value is directly encoded in the tag or refers to a
// different external byte range
if (fieldTypeLength * typeCount <= (this.bigTiff ? 8 : 4)) {
fieldValues = getValues(dataSlice, fieldType, typeCount, valueOffset);
} else {
// resolve the reference to the actual byte range
const actualOffset = dataSlice.readOffset(valueOffset);
const length = getFieldTypeLength(fieldType) * typeCount;
// check, whether we actually cover the referenced byte range; if not,
// request a new slice of bytes to read from it
if (dataSlice.covers(actualOffset, length)) {
fieldValues = getValues(dataSlice, fieldType, typeCount, actualOffset);
} else {
const fieldDataSlice = await this.getSlice(actualOffset, length);
fieldValues = getValues(fieldDataSlice, fieldType, typeCount, actualOffset);
}
}
// unpack single values from the array
if (typeCount === 1 && arrayFields.indexOf(fieldTag) === -1
&& !(fieldType === fieldTypes.RATIONAL || fieldType === fieldTypes.SRATIONAL)) {
value = fieldValues[0];
} else {
value = fieldValues;
}
// write the tags value to the file directly
fileDirectory[fieldTagNames[fieldTag]] = value;
}
const geoKeyDirectory = parseGeoKeyDirectory(fileDirectory);
const nextIFDByteOffset = dataSlice.readOffset(
offset + offsetSize + (entrySize * numDirEntries),
);
return new ImageFileDirectory(
fileDirectory,
geoKeyDirectory,
nextIFDByteOffset,
);
}
async requestIFD(index) {
// see if we already have that IFD index requested.
if (this.ifdRequests[index]) {
// attach to an already requested IFD
return this.ifdRequests[index];
} else if (index === 0) {
// special case for index 0
this.ifdRequests[index] = this.parseFileDirectoryAt(this.firstIFDOffset);
return this.ifdRequests[index];
} else if (!this.ifdRequests[index - 1]) {
// if the previous IFD was not yet loaded, load that one first
// this is the recursive call.
try {
this.ifdRequests[index - 1] = this.requestIFD(index - 1);
} catch (e) {
// if the previous one already was an index error, rethrow
// with the current index
if (e instanceof GeoTIFFImageIndexError) {
throw new GeoTIFFImageIndexError(index);
}
// rethrow anything else
throw e;
}
}
// if the previous IFD was loaded, we can finally fetch the one we are interested in.
// we need to wrap this in an IIFE, otherwise this.ifdRequests[index] would be delayed
this.ifdRequests[index] = (async () => {
const previousIfd = await this.ifdRequests[index - 1];
if (previousIfd.nextIFDByteOffset === 0) {
throw new GeoTIFFImageIndexError(index);
}
return this.parseFileDirectoryAt(previousIfd.nextIFDByteOffset);
})();
return this.ifdRequests[index];
}
/**
* Get the n-th internal subfile of an image. By default, the first is returned.
*
* @param {number} [index=0] the index of the image to return.
* @returns {Promise<GeoTIFFImage>} the image at the given index
*/
async getImage(index = 0) {
const ifd = await this.requestIFD(index);
return new GeoTIFFImage(
ifd.fileDirectory, ifd.geoKeyDirectory,
this.dataView, this.littleEndian, this.cache, this.source,
);
}
/**
* Returns the count of the internal subfiles.
*
* @returns {Promise<number>} the number of internal subfile images
*/
async getImageCount() {
let index = 0;
// loop until we run out of IFDs
let hasNext = true;
while (hasNext) {
try {
await this.requestIFD(index);
++index;
} catch (e) {
if (e instanceof GeoTIFFImageIndexError) {
hasNext = false;
} else {
throw e;
}
}
}
return index;
}
/**
* Get the values of the COG ghost area as a parsed map.
* See https://gdal.org/drivers/raster/cog.html#header-ghost-area for reference
* @returns {Promise<Object>} the parsed ghost area or null, if no such area was found
*/
async getGhostValues() {
const offset = this.bigTiff ? 16 : 8;
if (this.ghostValues) {
return this.ghostValues;
}
const detectionString = 'GDAL_STRUCTURAL_METADATA_SIZE=';
const heuristicAreaSize = detectionString.length + 100;
let slice = await this.getSlice(offset, heuristicAreaSize);
if (detectionString === getValues(slice, fieldTypes.ASCII, detectionString.length, offset)) {
const valuesString = getValues(slice, fieldTypes.ASCII, heuristicAreaSize, offset);
const firstLine = valuesString.split('\n')[0];
const metadataSize = Number(firstLine.split('=')[1].split(' ')[0]) + firstLine.length;
if (metadataSize > heuristicAreaSize) {
slice = await this.getSlice(offset, metadataSize);
}
const fullString = getValues(slice, fieldTypes.ASCII, metadataSize, offset);
this.ghostValues = {};
fullString
.split('\n')
.filter((line) => line.length > 0)
.map((line) => line.split('='))
.forEach(([key, value]) => {
this.ghostValues[key] = value;
});
}
return this.ghostValues;
}
/**
* Parse a (Geo)TIFF file from the given source.
*
* @param {*} source The source of data to parse from.
* @param {GeoTIFFOptions} [options] Additional options.
* @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
* to be aborted
*/
static async fromSource(source, options, signal) {
const headerData = (await source.fetch([{ offset: 0, length: 1024 }], signal))[0];
const dataView = new DataView64(headerData);
const BOM = dataView.getUint16(0, 0);
let littleEndian;
if (BOM === 0x4949) {
littleEndian = true;
} else if (BOM === 0x4D4D) {
littleEndian = false;
} else {
throw new TypeError('Invalid byte order value.');
}
const magicNumber = dataView.getUint16(2, littleEndian);
let bigTiff;
if (magicNumber === 42) {
bigTiff = false;
} else if (magicNumber === 43) {
bigTiff = true;
const offsetByteSize = dataView.getUint16(4, littleEndian);
if (offsetByteSize !== 8) {
throw new Error('Unsupported offset byte-size.');
}
} else {
throw new TypeError('Invalid magic number.');
}
const firstIFDOffset = bigTiff
? dataView.getUint64(8, littleEndian)
: dataView.getUint32(4, littleEndian);
return new GeoTIFF(source, littleEndian, bigTiff, firstIFDOffset, options);
}
/**
* Closes the underlying file buffer
* N.B. After the GeoTIFF has been completely processed it needs
* to be closed but only if it has been constructed from a file.
*/
close() {
if (typeof this.source.close === 'function') {
return this.source.close();
}
return false;
}
}
export { GeoTIFF };
export default GeoTIFF;
/**
* Wrapper for GeoTIFF files that have external overviews.
* @augments GeoTIFFBase
*/
class MultiGeoTIFF extends GeoTIFFBase {
/**
* Construct a new MultiGeoTIFF from a main and several overview files.
* @param {GeoTIFF} mainFile The main GeoTIFF file.
* @param {GeoTIFF[]} overviewFiles An array of overview files.
*/
constructor(mainFile, overviewFiles) {
super();
this.mainFile = mainFile;
this.overviewFiles = overviewFiles;
this.imageFiles = [mainFile].concat(overviewFiles);
this.fileDirectoriesPerFile = null;
this.fileDirectoriesPerFileParsing = null;
this.imageCount = null;
}
async parseFileDirectoriesPerFile() {
const requests = [this.mainFile.parseFileDirectoryAt(this.mainFile.firstIFDOffset)]
.concat(this.overviewFiles.map((file) => file.parseFileDirectoryAt(file.firstIFDOffset)));
this.fileDirectoriesPerFile = await Promise.all(requests);
return this.fileDirectoriesPerFile;
}
/**
* Get the n-th internal subfile of an image. By default, the first is returned.
*
* @param {number} [index=0] the index of the image to return.
* @returns {Promise<GeoTIFFImage>} the image at the given index
*/
async getImage(index = 0) {
await this.getImageCount();
await this.parseFileDirectoriesPerFile();
let visited = 0;
let relativeIndex = 0;
for (let i = 0; i < this.imageFiles.length; i++) {
const imageFile = this.imageFiles[i];
for (let ii = 0; ii < this.imageCounts[i]; ii++) {
if (index === visited) {
const ifd = await imageFile.requestIFD(relativeIndex);
return new GeoTIFFImage(
ifd.fileDirectory, ifd.geoKeyDirectory,
imageFile.dataView, imageFile.littleEndian, imageFile.cache, imageFile.source,
);
}
visited++;
relativeIndex++;
}
relativeIndex = 0;
}
throw new RangeError('Invalid image index');
}
/**
* Returns the count of the internal subfiles.
*
* @returns {Promise<number>} the number of internal subfile images
*/
async getImageCount() {
if (this.imageCount !== null) {
return this.imageCount;
}
const requests = [this.mainFile.getImageCount()]
.concat(this.overviewFiles.map((file) => file.getImageCount()));
this.imageCounts = await Promise.all(requests);
this.imageCount = this.imageCounts.reduce((count, ifds) => count + ifds, 0);
return this.imageCount;
}
}
export { MultiGeoTIFF };
/**
* Creates a new GeoTIFF from a remote URL.
* @param {string} url The URL to access the image from
* @param {object} [options] Additional options to pass to the source.
* See {@link makeRemoteSource} for details.
* @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
* to be aborted
* @returns {Promise<GeoTIFF>} The resulting GeoTIFF file.
*/
export async function fromUrl(url, options = {}, signal) {
return GeoTIFF.fromSource(makeRemoteSource(url, options), signal);
}
/**
* Creates a new GeoTIFF from a custom {@link BaseClient}.
* @param {BaseClient} client The client.
* @param {object} [options] Additional options to pass to the source.
* See {@link makeRemoteSource} for details.
* @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
* to be aborted
* @returns {Promise<GeoTIFF>} The resulting GeoTIFF file.
*/
export async function fromCustomClient(client, options = {}, signal) {
return GeoTIFF.fromSource(makeCustomSource(client, options), signal);
}
/**
* Construct a new GeoTIFF from an
* [ArrayBuffer]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer}.
* @param {ArrayBuffer} arrayBuffer The data to read the file from.
* @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
* to be aborted
* @returns {Promise<GeoTIFF>} The resulting GeoTIFF file.
*/
export async function fromArrayBuffer(arrayBuffer, signal) {
return GeoTIFF.fromSource(makeBufferSource(arrayBuffer), signal);
}
/**
* Construct a GeoTIFF from a local file path. This uses the node
* [filesystem API]{@link https://nodejs.org/api/fs.html} and is
* not available on browsers.
*
* N.B. After the GeoTIFF has been completely processed it needs
* to be closed but only if it has been constructed from a file.
* @param {string} path The file path to read from.
* @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
* to be aborted
* @returns {Promise<GeoTIFF>} The resulting GeoTIFF file.
*/
export async function fromFile(path, signal) {
return GeoTIFF.fromSource(makeFileSource(path), signal);
}
/**
* Construct a GeoTIFF from an HTML
* [Blob]{@link https://developer.mozilla.org/en-US/docs/Web/API/Blob} or
* [File]{@link https://developer.mozilla.org/en-US/docs/Web/API/File}
* object.
* @param {Blob|File} blob The Blob or File object to read from.
* @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
* to be aborted
* @returns {Promise<GeoTIFF>} The resulting GeoTIFF file.
*/
export async function fromBlob(blob, signal) {
return GeoTIFF.fromSource(makeFileReaderSource(blob), signal);
}
/**
* Construct a MultiGeoTIFF from the given URLs.
* @param {string} mainUrl The URL for the main file.
* @param {string[]} overviewUrls An array of URLs for the overview images.
* @param {Object} [options] Additional options to pass to the source.
* See [makeRemoteSource]{@link module:source.makeRemoteSource}
* for details.
* @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
* to be aborted
* @returns {Promise<MultiGeoTIFF>} The resulting MultiGeoTIFF file.
*/
export async function fromUrls(mainUrl, overviewUrls = [], options = {}, signal) {
const mainFile = await GeoTIFF.fromSource(makeRemoteSource(mainUrl, options), signal);
const overviewFiles = await Promise.all(
overviewUrls.map((url) => GeoTIFF.fromSource(makeRemoteSource(url, options))),
);
return new MultiGeoTIFF(mainFile, overviewFiles);
}
/**
* Main creating function for GeoTIFF files.
* @param {(Array)} array of pixel values
* @returns {metadata} metadata
*/
export function writeArrayBuffer(values, metadata) {
return writeGeotiff(values, metadata);
}
export { Pool };
export { GeoTIFFImage };
export { BaseClient, BaseResponse };

View file

@ -0,0 +1,945 @@
/** @module geotiffimage */
import { getFloat16 } from '@petamoriken/float16';
import getAttribute from 'xml-utils/get-attribute.js';
import findTagsByName from 'xml-utils/find-tags-by-name.js';
import { photometricInterpretations, ExtraSamplesValues } from './globals.js';
import { fromWhiteIsZero, fromBlackIsZero, fromPalette, fromCMYK, fromYCbCr, fromCIELab } from './rgb.js';
import { getDecoder } from './compression/index.js';
import { resample, resampleInterleaved } from './resample.js';
/**
* @typedef {Object} ReadRasterOptions
* @property {Array<number>} [window=whole window] the subset to read data from in pixels.
* @property {Array<number>} [bbox=whole image] the subset to read data from in
* geographical coordinates.
* @property {Array<number>} [samples=all samples] the selection of samples to read from. Default is all samples.
* @property {boolean} [interleave=false] whether the data shall be read
* in one single array or separate
* arrays.
* @property {Pool} [pool=null] The optional decoder pool to use.
* @property {number} [width] The desired width of the output. When the width is not the
* same as the images, resampling will be performed.
* @property {number} [height] The desired height of the output. When the width is not the
* same as the images, resampling will be performed.
* @property {string} [resampleMethod='nearest'] The desired resampling method.
* @property {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
* to be aborted
* @property {number|number[]} [fillValue] The value to use for parts of the image
* outside of the images extent. When multiple
* samples are requested, an array of fill values
* can be passed.
*/
/** @typedef {import("./geotiff.js").TypedArray} TypedArray */
/** @typedef {import("./geotiff.js").ReadRasterResult} ReadRasterResult */
function sum(array, start, end) {
let s = 0;
for (let i = start; i < end; ++i) {
s += array[i];
}
return s;
}
function arrayForType(format, bitsPerSample, size) {
switch (format) {
case 1: // unsigned integer data
if (bitsPerSample <= 8) {
return new Uint8Array(size);
} else if (bitsPerSample <= 16) {
return new Uint16Array(size);
} else if (bitsPerSample <= 32) {
return new Uint32Array(size);
}
break;
case 2: // twos complement signed integer data
if (bitsPerSample === 8) {
return new Int8Array(size);
} else if (bitsPerSample === 16) {
return new Int16Array(size);
} else if (bitsPerSample === 32) {
return new Int32Array(size);
}
break;
case 3: // floating point data
switch (bitsPerSample) {
case 16:
case 32:
return new Float32Array(size);
case 64:
return new Float64Array(size);
default:
break;
}
break;
default:
break;
}
throw Error('Unsupported data format/bitsPerSample');
}
function needsNormalization(format, bitsPerSample) {
if ((format === 1 || format === 2) && bitsPerSample <= 32 && bitsPerSample % 8 === 0) {
return false;
} else if (format === 3 && (bitsPerSample === 16 || bitsPerSample === 32 || bitsPerSample === 64)) {
return false;
}
return true;
}
function normalizeArray(inBuffer, format, planarConfiguration, samplesPerPixel, bitsPerSample, tileWidth, tileHeight) {
// const inByteArray = new Uint8Array(inBuffer);
const view = new DataView(inBuffer);
const outSize = planarConfiguration === 2
? tileHeight * tileWidth
: tileHeight * tileWidth * samplesPerPixel;
const samplesToTransfer = planarConfiguration === 2
? 1 : samplesPerPixel;
const outArray = arrayForType(format, bitsPerSample, outSize);
// let pixel = 0;
const bitMask = parseInt('1'.repeat(bitsPerSample), 2);
if (format === 1) { // unsigned integer
// translation of https://github.com/OSGeo/gdal/blob/master/gdal/frmts/gtiff/geotiff.cpp#L7337
let pixelBitSkip;
// let sampleBitOffset = 0;
if (planarConfiguration === 1) {
pixelBitSkip = samplesPerPixel * bitsPerSample;
// sampleBitOffset = (samplesPerPixel - 1) * bitsPerSample;
} else {
pixelBitSkip = bitsPerSample;
}
// Bits per line rounds up to next byte boundary.
let bitsPerLine = tileWidth * pixelBitSkip;
if ((bitsPerLine & 7) !== 0) {
bitsPerLine = (bitsPerLine + 7) & (~7);
}
for (let y = 0; y < tileHeight; ++y) {
const lineBitOffset = y * bitsPerLine;
for (let x = 0; x < tileWidth; ++x) {
const pixelBitOffset = lineBitOffset + (x * samplesToTransfer * bitsPerSample);
for (let i = 0; i < samplesToTransfer; ++i) {
const bitOffset = pixelBitOffset + (i * bitsPerSample);
const outIndex = (((y * tileWidth) + x) * samplesToTransfer) + i;
const byteOffset = Math.floor(bitOffset / 8);
const innerBitOffset = bitOffset % 8;
if (innerBitOffset + bitsPerSample <= 8) {
outArray[outIndex] = (view.getUint8(byteOffset) >> (8 - bitsPerSample) - innerBitOffset) & bitMask;
} else if (innerBitOffset + bitsPerSample <= 16) {
outArray[outIndex] = (view.getUint16(byteOffset) >> (16 - bitsPerSample) - innerBitOffset) & bitMask;
} else if (innerBitOffset + bitsPerSample <= 24) {
const raw = (view.getUint16(byteOffset) << 8) | (view.getUint8(byteOffset + 2));
outArray[outIndex] = (raw >> (24 - bitsPerSample) - innerBitOffset) & bitMask;
} else {
outArray[outIndex] = (view.getUint32(byteOffset) >> (32 - bitsPerSample) - innerBitOffset) & bitMask;
}
// let outWord = 0;
// for (let bit = 0; bit < bitsPerSample; ++bit) {
// if (inByteArray[bitOffset >> 3]
// & (0x80 >> (bitOffset & 7))) {
// outWord |= (1 << (bitsPerSample - 1 - bit));
// }
// ++bitOffset;
// }
// outArray[outIndex] = outWord;
// outArray[pixel] = outWord;
// pixel += 1;
}
// bitOffset = bitOffset + pixelBitSkip - bitsPerSample;
}
}
} else if (format === 3) { // floating point
// Float16 is handled elsewhere
// normalize 16/24 bit floats to 32 bit floats in the array
// console.time();
// if (bitsPerSample === 16) {
// for (let byte = 0, outIndex = 0; byte < inBuffer.byteLength; byte += 2, ++outIndex) {
// outArray[outIndex] = getFloat16(view, byte);
// }
// }
// console.timeEnd()
}
return outArray.buffer;
}
/**
* GeoTIFF sub-file image.
*/
class GeoTIFFImage {
/**
* @constructor
* @param {Object} fileDirectory The parsed file directory
* @param {Object} geoKeys The parsed geo-keys
* @param {DataView} dataView The DataView for the underlying file.
* @param {Boolean} littleEndian Whether the file is encoded in little or big endian
* @param {Boolean} cache Whether or not decoded tiles shall be cached
* @param {import('./source/basesource').BaseSource} source The datasource to read from
*/
constructor(fileDirectory, geoKeys, dataView, littleEndian, cache, source) {
this.fileDirectory = fileDirectory;
this.geoKeys = geoKeys;
this.dataView = dataView;
this.littleEndian = littleEndian;
this.tiles = cache ? {} : null;
this.isTiled = !fileDirectory.StripOffsets;
const planarConfiguration = fileDirectory.PlanarConfiguration;
this.planarConfiguration = (typeof planarConfiguration === 'undefined') ? 1 : planarConfiguration;
if (this.planarConfiguration !== 1 && this.planarConfiguration !== 2) {
throw new Error('Invalid planar configuration.');
}
this.source = source;
}
/**
* Returns the associated parsed file directory.
* @returns {Object} the parsed file directory
*/
getFileDirectory() {
return this.fileDirectory;
}
/**
* Returns the associated parsed geo keys.
* @returns {Object} the parsed geo keys
*/
getGeoKeys() {
return this.geoKeys;
}
/**
* Returns the width of the image.
* @returns {Number} the width of the image
*/
getWidth() {
return this.fileDirectory.ImageWidth;
}
/**
* Returns the height of the image.
* @returns {Number} the height of the image
*/
getHeight() {
return this.fileDirectory.ImageLength;
}
/**
* Returns the number of samples per pixel.
* @returns {Number} the number of samples per pixel
*/
getSamplesPerPixel() {
return typeof this.fileDirectory.SamplesPerPixel !== 'undefined'
? this.fileDirectory.SamplesPerPixel : 1;
}
/**
* Returns the width of each tile.
* @returns {Number} the width of each tile
*/
getTileWidth() {
return this.isTiled ? this.fileDirectory.TileWidth : this.getWidth();
}
/**
* Returns the height of each tile.
* @returns {Number} the height of each tile
*/
getTileHeight() {
if (this.isTiled) {
return this.fileDirectory.TileLength;
}
if (typeof this.fileDirectory.RowsPerStrip !== 'undefined') {
return Math.min(this.fileDirectory.RowsPerStrip, this.getHeight());
}
return this.getHeight();
}
getBlockWidth() {
return this.getTileWidth();
}
getBlockHeight(y) {
if (this.isTiled || (y + 1) * this.getTileHeight() <= this.getHeight()) {
return this.getTileHeight();
} else {
return this.getHeight() - (y * this.getTileHeight());
}
}
/**
* Calculates the number of bytes for each pixel across all samples. Only full
* bytes are supported, an exception is thrown when this is not the case.
* @returns {Number} the bytes per pixel
*/
getBytesPerPixel() {
let bytes = 0;
for (let i = 0; i < this.fileDirectory.BitsPerSample.length; ++i) {
bytes += this.getSampleByteSize(i);
}
return bytes;
}
getSampleByteSize(i) {
if (i >= this.fileDirectory.BitsPerSample.length) {
throw new RangeError(`Sample index ${i} is out of range.`);
}
return Math.ceil(this.fileDirectory.BitsPerSample[i] / 8);
}
getReaderForSample(sampleIndex) {
const format = this.fileDirectory.SampleFormat
? this.fileDirectory.SampleFormat[sampleIndex] : 1;
const bitsPerSample = this.fileDirectory.BitsPerSample[sampleIndex];
switch (format) {
case 1: // unsigned integer data
if (bitsPerSample <= 8) {
return DataView.prototype.getUint8;
} else if (bitsPerSample <= 16) {
return DataView.prototype.getUint16;
} else if (bitsPerSample <= 32) {
return DataView.prototype.getUint32;
}
break;
case 2: // twos complement signed integer data
if (bitsPerSample <= 8) {
return DataView.prototype.getInt8;
} else if (bitsPerSample <= 16) {
return DataView.prototype.getInt16;
} else if (bitsPerSample <= 32) {
return DataView.prototype.getInt32;
}
break;
case 3:
switch (bitsPerSample) {
case 16:
return function (offset, littleEndian) {
return getFloat16(this, offset, littleEndian);
};
case 32:
return DataView.prototype.getFloat32;
case 64:
return DataView.prototype.getFloat64;
default:
break;
}
break;
default:
break;
}
throw Error('Unsupported data format/bitsPerSample');
}
getSampleFormat(sampleIndex = 0) {
return this.fileDirectory.SampleFormat
? this.fileDirectory.SampleFormat[sampleIndex] : 1;
}
getBitsPerSample(sampleIndex = 0) {
return this.fileDirectory.BitsPerSample[sampleIndex];
}
getArrayForSample(sampleIndex, size) {
const format = this.getSampleFormat(sampleIndex);
const bitsPerSample = this.getBitsPerSample(sampleIndex);
return arrayForType(format, bitsPerSample, size);
}
/**
* Returns the decoded strip or tile.
* @param {Number} x the strip or tile x-offset
* @param {Number} y the tile y-offset (0 for stripped images)
* @param {Number} sample the sample to get for separated samples
* @param {import("./geotiff").Pool|import("./geotiff").BaseDecoder} poolOrDecoder the decoder or decoder pool
* @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
* to be aborted
* @returns {Promise.<ArrayBuffer>}
*/
async getTileOrStrip(x, y, sample, poolOrDecoder, signal) {
const numTilesPerRow = Math.ceil(this.getWidth() / this.getTileWidth());
const numTilesPerCol = Math.ceil(this.getHeight() / this.getTileHeight());
let index;
const { tiles } = this;
if (this.planarConfiguration === 1) {
index = (y * numTilesPerRow) + x;
} else if (this.planarConfiguration === 2) {
index = (sample * numTilesPerRow * numTilesPerCol) + (y * numTilesPerRow) + x;
}
let offset;
let byteCount;
if (this.isTiled) {
offset = this.fileDirectory.TileOffsets[index];
byteCount = this.fileDirectory.TileByteCounts[index];
} else {
offset = this.fileDirectory.StripOffsets[index];
byteCount = this.fileDirectory.StripByteCounts[index];
}
const slice = (await this.source.fetch([{ offset, length: byteCount }], signal))[0];
let request;
if (tiles === null || !tiles[index]) {
// resolve each request by potentially applying array normalization
request = (async () => {
let data = await poolOrDecoder.decode(this.fileDirectory, slice);
const sampleFormat = this.getSampleFormat();
const bitsPerSample = this.getBitsPerSample();
if (needsNormalization(sampleFormat, bitsPerSample)) {
data = normalizeArray(
data,
sampleFormat,
this.planarConfiguration,
this.getSamplesPerPixel(),
bitsPerSample,
this.getTileWidth(),
this.getBlockHeight(y),
);
}
return data;
})();
// set the cache
if (tiles !== null) {
tiles[index] = request;
}
} else {
// get from the cache
request = tiles[index];
}
// cache the tile request
return { x, y, sample, data: await request };
}
/**
* Internal read function.
* @private
* @param {Array} imageWindow The image window in pixel coordinates
* @param {Array} samples The selected samples (0-based indices)
* @param {TypedArray|TypedArray[]} valueArrays The array(s) to write into
* @param {Boolean} interleave Whether or not to write in an interleaved manner
* @param {import("./geotiff").Pool|AbstractDecoder} poolOrDecoder the decoder or decoder pool
* @param {number} width the width of window to be read into
* @param {number} height the height of window to be read into
* @param {number} resampleMethod the resampling method to be used when interpolating
* @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
* to be aborted
* @returns {Promise<ReadRasterResult>}
*/
async _readRaster(imageWindow, samples, valueArrays, interleave, poolOrDecoder, width,
height, resampleMethod, signal) {
const tileWidth = this.getTileWidth();
const tileHeight = this.getTileHeight();
const imageWidth = this.getWidth();
const imageHeight = this.getHeight();
const minXTile = Math.max(Math.floor(imageWindow[0] / tileWidth), 0);
const maxXTile = Math.min(
Math.ceil(imageWindow[2] / tileWidth),
Math.ceil(imageWidth / tileWidth),
);
const minYTile = Math.max(Math.floor(imageWindow[1] / tileHeight), 0);
const maxYTile = Math.min(
Math.ceil(imageWindow[3] / tileHeight),
Math.ceil(imageHeight / tileHeight),
);
const windowWidth = imageWindow[2] - imageWindow[0];
let bytesPerPixel = this.getBytesPerPixel();
const srcSampleOffsets = [];
const sampleReaders = [];
for (let i = 0; i < samples.length; ++i) {
if (this.planarConfiguration === 1) {
srcSampleOffsets.push(sum(this.fileDirectory.BitsPerSample, 0, samples[i]) / 8);
} else {
srcSampleOffsets.push(0);
}
sampleReaders.push(this.getReaderForSample(samples[i]));
}
const promises = [];
const { littleEndian } = this;
for (let yTile = minYTile; yTile < maxYTile; ++yTile) {
for (let xTile = minXTile; xTile < maxXTile; ++xTile) {
let getPromise;
if (this.planarConfiguration === 1) {
getPromise = this.getTileOrStrip(xTile, yTile, 0, poolOrDecoder, signal);
}
for (let sampleIndex = 0; sampleIndex < samples.length; ++sampleIndex) {
const si = sampleIndex;
const sample = samples[sampleIndex];
if (this.planarConfiguration === 2) {
bytesPerPixel = this.getSampleByteSize(sample);
getPromise = this.getTileOrStrip(xTile, yTile, sample, poolOrDecoder, signal);
}
const promise = getPromise.then((tile) => {
const buffer = tile.data;
const dataView = new DataView(buffer);
const blockHeight = this.getBlockHeight(tile.y);
const firstLine = tile.y * tileHeight;
const firstCol = tile.x * tileWidth;
const lastLine = firstLine + blockHeight;
const lastCol = (tile.x + 1) * tileWidth;
const reader = sampleReaders[si];
const ymax = Math.min(blockHeight, blockHeight - (lastLine - imageWindow[3]), imageHeight - firstLine);
const xmax = Math.min(tileWidth, tileWidth - (lastCol - imageWindow[2]), imageWidth - firstCol);
for (let y = Math.max(0, imageWindow[1] - firstLine); y < ymax; ++y) {
for (let x = Math.max(0, imageWindow[0] - firstCol); x < xmax; ++x) {
const pixelOffset = ((y * tileWidth) + x) * bytesPerPixel;
const value = reader.call(
dataView, pixelOffset + srcSampleOffsets[si], littleEndian,
);
let windowCoordinate;
if (interleave) {
windowCoordinate = ((y + firstLine - imageWindow[1]) * windowWidth * samples.length)
+ ((x + firstCol - imageWindow[0]) * samples.length)
+ si;
valueArrays[windowCoordinate] = value;
} else {
windowCoordinate = (
(y + firstLine - imageWindow[1]) * windowWidth
) + x + firstCol - imageWindow[0];
valueArrays[si][windowCoordinate] = value;
}
}
}
});
promises.push(promise);
}
}
}
await Promise.all(promises);
if ((width && (imageWindow[2] - imageWindow[0]) !== width)
|| (height && (imageWindow[3] - imageWindow[1]) !== height)) {
let resampled;
if (interleave) {
resampled = resampleInterleaved(
valueArrays,
imageWindow[2] - imageWindow[0],
imageWindow[3] - imageWindow[1],
width, height,
samples.length,
resampleMethod,
);
} else {
resampled = resample(
valueArrays,
imageWindow[2] - imageWindow[0],
imageWindow[3] - imageWindow[1],
width, height,
resampleMethod,
);
}
resampled.width = width;
resampled.height = height;
return resampled;
}
valueArrays.width = width || imageWindow[2] - imageWindow[0];
valueArrays.height = height || imageWindow[3] - imageWindow[1];
return valueArrays;
}
/**
* Reads raster data from the image. This function reads all selected samples
* into separate arrays of the correct type for that sample or into a single
* combined array when `interleave` is set. When provided, only a subset
* of the raster is read for each sample.
*
* @param {ReadRasterOptions} [options={}] optional parameters
* @returns {Promise<ReadRasterResult>} the decoded arrays as a promise
*/
async readRasters({
window: wnd, samples = [], interleave, pool = null,
width, height, resampleMethod, fillValue, signal,
} = {}) {
const imageWindow = wnd || [0, 0, this.getWidth(), this.getHeight()];
// check parameters
if (imageWindow[0] > imageWindow[2] || imageWindow[1] > imageWindow[3]) {
throw new Error('Invalid subsets');
}
const imageWindowWidth = imageWindow[2] - imageWindow[0];
const imageWindowHeight = imageWindow[3] - imageWindow[1];
const numPixels = imageWindowWidth * imageWindowHeight;
const samplesPerPixel = this.getSamplesPerPixel();
if (!samples || !samples.length) {
for (let i = 0; i < samplesPerPixel; ++i) {
samples.push(i);
}
} else {
for (let i = 0; i < samples.length; ++i) {
if (samples[i] >= samplesPerPixel) {
return Promise.reject(new RangeError(`Invalid sample index '${samples[i]}'.`));
}
}
}
let valueArrays;
if (interleave) {
const format = this.fileDirectory.SampleFormat
? Math.max.apply(null, this.fileDirectory.SampleFormat) : 1;
const bitsPerSample = Math.max.apply(null, this.fileDirectory.BitsPerSample);
valueArrays = arrayForType(format, bitsPerSample, numPixels * samples.length);
if (fillValue) {
valueArrays.fill(fillValue);
}
} else {
valueArrays = [];
for (let i = 0; i < samples.length; ++i) {
const valueArray = this.getArrayForSample(samples[i], numPixels);
if (Array.isArray(fillValue) && i < fillValue.length) {
valueArray.fill(fillValue[i]);
} else if (fillValue && !Array.isArray(fillValue)) {
valueArray.fill(fillValue);
}
valueArrays.push(valueArray);
}
}
const poolOrDecoder = pool || await getDecoder(this.fileDirectory);
const result = await this._readRaster(
imageWindow, samples, valueArrays, interleave, poolOrDecoder, width, height, resampleMethod, signal,
);
return result;
}
/**
* Reads raster data from the image as RGB. The result is always an
* interleaved typed array.
* Colorspaces other than RGB will be transformed to RGB, color maps expanded.
* When no other method is applicable, the first sample is used to produce a
* grayscale image.
* When provided, only a subset of the raster is read for each sample.
*
* @param {Object} [options] optional parameters
* @param {Array<number>} [options.window] the subset to read data from in pixels.
* @param {boolean} [options.interleave=true] whether the data shall be read
* in one single array or separate
* arrays.
* @param {import("./geotiff").Pool} [options.pool=null] The optional decoder pool to use.
* @param {number} [options.width] The desired width of the output. When the width is no the
* same as the images, resampling will be performed.
* @param {number} [options.height] The desired height of the output. When the width is no the
* same as the images, resampling will be performed.
* @param {string} [options.resampleMethod='nearest'] The desired resampling method.
* @param {boolean} [options.enableAlpha=false] Enable reading alpha channel if present.
* @param {AbortSignal} [options.signal] An AbortSignal that may be signalled if the request is
* to be aborted
* @returns {Promise<ReadRasterResult>} the RGB array as a Promise
*/
async readRGB({ window, interleave = true, pool = null, width, height,
resampleMethod, enableAlpha = false, signal } = {}) {
const imageWindow = window || [0, 0, this.getWidth(), this.getHeight()];
// check parameters
if (imageWindow[0] > imageWindow[2] || imageWindow[1] > imageWindow[3]) {
throw new Error('Invalid subsets');
}
const pi = this.fileDirectory.PhotometricInterpretation;
if (pi === photometricInterpretations.RGB) {
let s = [0, 1, 2];
if ((!(this.fileDirectory.ExtraSamples === ExtraSamplesValues.Unspecified)) && enableAlpha) {
s = [];
for (let i = 0; i < this.fileDirectory.BitsPerSample.length; i += 1) {
s.push(i);
}
}
return this.readRasters({
window,
interleave,
samples: s,
pool,
width,
height,
resampleMethod,
signal,
});
}
let samples;
switch (pi) {
case photometricInterpretations.WhiteIsZero:
case photometricInterpretations.BlackIsZero:
case photometricInterpretations.Palette:
samples = [0];
break;
case photometricInterpretations.CMYK:
samples = [0, 1, 2, 3];
break;
case photometricInterpretations.YCbCr:
case photometricInterpretations.CIELab:
samples = [0, 1, 2];
break;
default:
throw new Error('Invalid or unsupported photometric interpretation.');
}
const subOptions = {
window: imageWindow,
interleave: true,
samples,
pool,
width,
height,
resampleMethod,
signal,
};
const { fileDirectory } = this;
const raster = await this.readRasters(subOptions);
const max = 2 ** this.fileDirectory.BitsPerSample[0];
let data;
switch (pi) {
case photometricInterpretations.WhiteIsZero:
data = fromWhiteIsZero(raster, max);
break;
case photometricInterpretations.BlackIsZero:
data = fromBlackIsZero(raster, max);
break;
case photometricInterpretations.Palette:
data = fromPalette(raster, fileDirectory.ColorMap);
break;
case photometricInterpretations.CMYK:
data = fromCMYK(raster);
break;
case photometricInterpretations.YCbCr:
data = fromYCbCr(raster);
break;
case photometricInterpretations.CIELab:
data = fromCIELab(raster);
break;
default:
throw new Error('Unsupported photometric interpretation.');
}
// if non-interleaved data is requested, we must split the channels
// into their respective arrays
if (!interleave) {
const red = new Uint8Array(data.length / 3);
const green = new Uint8Array(data.length / 3);
const blue = new Uint8Array(data.length / 3);
for (let i = 0, j = 0; i < data.length; i += 3, ++j) {
red[j] = data[i];
green[j] = data[i + 1];
blue[j] = data[i + 2];
}
data = [red, green, blue];
}
data.width = raster.width;
data.height = raster.height;
return data;
}
/**
* Returns an array of tiepoints.
* @returns {Object[]}
*/
getTiePoints() {
if (!this.fileDirectory.ModelTiepoint) {
return [];
}
const tiePoints = [];
for (let i = 0; i < this.fileDirectory.ModelTiepoint.length; i += 6) {
tiePoints.push({
i: this.fileDirectory.ModelTiepoint[i],
j: this.fileDirectory.ModelTiepoint[i + 1],
k: this.fileDirectory.ModelTiepoint[i + 2],
x: this.fileDirectory.ModelTiepoint[i + 3],
y: this.fileDirectory.ModelTiepoint[i + 4],
z: this.fileDirectory.ModelTiepoint[i + 5],
});
}
return tiePoints;
}
/**
* Returns the parsed GDAL metadata items.
*
* If sample is passed to null, dataset-level metadata will be returned.
* Otherwise only metadata specific to the provided sample will be returned.
*
* @param {number} [sample=null] The sample index.
* @returns {Object}
*/
getGDALMetadata(sample = null) {
const metadata = {};
if (!this.fileDirectory.GDAL_METADATA) {
return null;
}
const string = this.fileDirectory.GDAL_METADATA;
let items = findTagsByName(string, 'Item');
if (sample === null) {
items = items.filter((item) => getAttribute(item, 'sample') === undefined);
} else {
items = items.filter((item) => Number(getAttribute(item, 'sample')) === sample);
}
for (let i = 0; i < items.length; ++i) {
const item = items[i];
metadata[getAttribute(item, 'name')] = item.inner;
}
return metadata;
}
/**
* Returns the GDAL nodata value
* @returns {number|null}
*/
getGDALNoData() {
if (!this.fileDirectory.GDAL_NODATA) {
return null;
}
const string = this.fileDirectory.GDAL_NODATA;
return Number(string.substring(0, string.length - 1));
}
/**
* Returns the image origin as a XYZ-vector. When the image has no affine
* transformation, then an exception is thrown.
* @returns {Array<number>} The origin as a vector
*/
getOrigin() {
const tiePoints = this.fileDirectory.ModelTiepoint;
const modelTransformation = this.fileDirectory.ModelTransformation;
if (tiePoints && tiePoints.length === 6) {
return [
tiePoints[3],
tiePoints[4],
tiePoints[5],
];
}
if (modelTransformation) {
return [
modelTransformation[3],
modelTransformation[7],
modelTransformation[11],
];
}
throw new Error('The image does not have an affine transformation.');
}
/**
* Returns the image resolution as a XYZ-vector. When the image has no affine
* transformation, then an exception is thrown.
* @param {GeoTIFFImage} [referenceImage=null] A reference image to calculate the resolution from
* in cases when the current image does not have the
* required tags on its own.
* @returns {Array<number>} The resolution as a vector
*/
getResolution(referenceImage = null) {
const modelPixelScale = this.fileDirectory.ModelPixelScale;
const modelTransformation = this.fileDirectory.ModelTransformation;
if (modelPixelScale) {
return [
modelPixelScale[0],
-modelPixelScale[1],
modelPixelScale[2],
];
}
if (modelTransformation) {
return [
modelTransformation[0],
-modelTransformation[5],
modelTransformation[10],
];
}
if (referenceImage) {
const [refResX, refResY, refResZ] = referenceImage.getResolution();
return [
refResX * referenceImage.getWidth() / this.getWidth(),
refResY * referenceImage.getHeight() / this.getHeight(),
refResZ * referenceImage.getWidth() / this.getWidth(),
];
}
throw new Error('The image does not have an affine transformation.');
}
/**
* Returns whether or not the pixels of the image depict an area (or point).
* @returns {Boolean} Whether the pixels are a point
*/
pixelIsArea() {
return this.geoKeys.GTRasterTypeGeoKey === 1;
}
/**
* Returns the image bounding box as an array of 4 values: min-x, min-y,
* max-x and max-y. When the image has no affine transformation, then an
* exception is thrown.
* @returns {Array<number>} The bounding box
*/
getBoundingBox() {
const height = this.getHeight();
const width = this.getWidth();
if (this.fileDirectory.ModelTransformation) {
// eslint-disable-next-line no-unused-vars
const [a, b, c, d, e, f, g, h] = this.fileDirectory.ModelTransformation;
const corners = [
[0, 0],
[0, height],
[width, 0],
[width, height],
];
const projected = corners.map(([I, J]) => [
d + (a * I) + (b * J),
h + (e * I) + (f * J),
]);
const xs = projected.map((pt) => pt[0]);
const ys = projected.map((pt) => pt[1]);
return [
Math.min(...xs),
Math.min(...ys),
Math.max(...xs),
Math.max(...ys),
];
} else {
const origin = this.getOrigin();
const resolution = this.getResolution();
const x1 = origin[0];
const y1 = origin[1];
const x2 = x1 + (resolution[0] * this.getWidth());
const y2 = y1 + (resolution[1] * this.getHeight());
return [
Math.min(x1, x2),
Math.min(y1, y2),
Math.max(x1, x2),
Math.max(y1, y2),
];
}
}
}
export default GeoTIFFImage;

View file

@ -0,0 +1,457 @@
/*
Some parts of this file are based on UTIF.js,
which was released under the MIT License.
You can view that here:
https://github.com/photopea/UTIF.js/blob/master/LICENSE
*/
import { fieldTagNames, fieldTagTypes, fieldTypeNames, geoKeyNames } from './globals.js';
import { assign, endsWith, forEach, invert, times } from './utils.js';
const tagName2Code = invert(fieldTagNames);
const geoKeyName2Code = invert(geoKeyNames);
const name2code = {};
assign(name2code, tagName2Code);
assign(name2code, geoKeyName2Code);
const typeName2byte = invert(fieldTypeNames);
// config variables
const numBytesInIfd = 1000;
const _binBE = {
nextZero: (data, o) => {
let oincr = o;
while (data[oincr] !== 0) {
oincr++;
}
return oincr;
},
readUshort: (buff, p) => {
return (buff[p] << 8) | buff[p + 1];
},
readShort: (buff, p) => {
const a = _binBE.ui8;
a[0] = buff[p + 1];
a[1] = buff[p + 0];
return _binBE.i16[0];
},
readInt: (buff, p) => {
const a = _binBE.ui8;
a[0] = buff[p + 3];
a[1] = buff[p + 2];
a[2] = buff[p + 1];
a[3] = buff[p + 0];
return _binBE.i32[0];
},
readUint: (buff, p) => {
const a = _binBE.ui8;
a[0] = buff[p + 3];
a[1] = buff[p + 2];
a[2] = buff[p + 1];
a[3] = buff[p + 0];
return _binBE.ui32[0];
},
readASCII: (buff, p, l) => {
return l.map((i) => String.fromCharCode(buff[p + i])).join('');
},
readFloat: (buff, p) => {
const a = _binBE.ui8;
times(4, (i) => {
a[i] = buff[p + 3 - i];
});
return _binBE.fl32[0];
},
readDouble: (buff, p) => {
const a = _binBE.ui8;
times(8, (i) => {
a[i] = buff[p + 7 - i];
});
return _binBE.fl64[0];
},
writeUshort: (buff, p, n) => {
buff[p] = (n >> 8) & 255;
buff[p + 1] = n & 255;
},
writeUint: (buff, p, n) => {
buff[p] = (n >> 24) & 255;
buff[p + 1] = (n >> 16) & 255;
buff[p + 2] = (n >> 8) & 255;
buff[p + 3] = (n >> 0) & 255;
},
writeASCII: (buff, p, s) => {
times(s.length, (i) => {
buff[p + i] = s.charCodeAt(i);
});
},
ui8: new Uint8Array(8),
};
_binBE.fl64 = new Float64Array(_binBE.ui8.buffer);
_binBE.writeDouble = (buff, p, n) => {
_binBE.fl64[0] = n;
times(8, (i) => {
buff[p + i] = _binBE.ui8[7 - i];
});
};
const _writeIFD = (bin, data, _offset, ifd) => {
let offset = _offset;
const keys = Object.keys(ifd).filter((key) => {
return key !== undefined && key !== null && key !== 'undefined';
});
bin.writeUshort(data, offset, keys.length);
offset += 2;
let eoff = offset + (12 * keys.length) + 4;
for (const key of keys) {
let tag = null;
if (typeof key === 'number') {
tag = key;
} else if (typeof key === 'string') {
tag = parseInt(key, 10);
}
const typeName = fieldTagTypes[tag];
const typeNum = typeName2byte[typeName];
if (typeName == null || typeName === undefined || typeof typeName === 'undefined') {
throw new Error(`unknown type of tag: ${tag}`);
}
let val = ifd[key];
if (val === undefined) {
throw new Error(`failed to get value for key ${key}`);
}
// ASCIIZ format with trailing 0 character
// http://www.fileformat.info/format/tiff/corion.htm
// https://stackoverflow.com/questions/7783044/whats-the-difference-between-asciiz-vs-ascii
if (typeName === 'ASCII' && typeof val === 'string' && endsWith(val, '\u0000') === false) {
val += '\u0000';
}
const num = val.length;
bin.writeUshort(data, offset, tag);
offset += 2;
bin.writeUshort(data, offset, typeNum);
offset += 2;
bin.writeUint(data, offset, num);
offset += 4;
let dlen = [-1, 1, 1, 2, 4, 8, 0, 0, 0, 0, 0, 0, 8][typeNum] * num;
let toff = offset;
if (dlen > 4) {
bin.writeUint(data, offset, eoff);
toff = eoff;
}
if (typeName === 'ASCII') {
bin.writeASCII(data, toff, val);
} else if (typeName === 'SHORT') {
times(num, (i) => {
bin.writeUshort(data, toff + (2 * i), val[i]);
});
} else if (typeName === 'LONG') {
times(num, (i) => {
bin.writeUint(data, toff + (4 * i), val[i]);
});
} else if (typeName === 'RATIONAL') {
times(num, (i) => {
bin.writeUint(data, toff + (8 * i), Math.round(val[i] * 10000));
bin.writeUint(data, toff + (8 * i) + 4, 10000);
});
} else if (typeName === 'DOUBLE') {
times(num, (i) => {
bin.writeDouble(data, toff + (8 * i), val[i]);
});
}
if (dlen > 4) {
dlen += (dlen & 1);
eoff += dlen;
}
offset += 4;
}
return [offset, eoff];
};
const encodeIfds = (ifds) => {
const data = new Uint8Array(numBytesInIfd);
let offset = 4;
const bin = _binBE;
// set big-endian byte-order
// https://en.wikipedia.org/wiki/TIFF#Byte_order
data[0] = 77;
data[1] = 77;
// set format-version number
// https://en.wikipedia.org/wiki/TIFF#Byte_order
data[3] = 42;
let ifdo = 8;
bin.writeUint(data, offset, ifdo);
offset += 4;
ifds.forEach((ifd, i) => {
const noffs = _writeIFD(bin, data, ifdo, ifd);
ifdo = noffs[1];
if (i < ifds.length - 1) {
bin.writeUint(data, noffs[0], ifdo);
}
});
if (data.slice) {
return data.slice(0, ifdo).buffer;
}
// node hasn't implemented slice on Uint8Array yet
const result = new Uint8Array(ifdo);
for (let i = 0; i < ifdo; i++) {
result[i] = data[i];
}
return result.buffer;
};
const encodeImage = (values, width, height, metadata) => {
if (height === undefined || height === null) {
throw new Error(`you passed into encodeImage a width of type ${height}`);
}
if (width === undefined || width === null) {
throw new Error(`you passed into encodeImage a width of type ${width}`);
}
const ifd = {
256: [width], // ImageWidth
257: [height], // ImageLength
273: [numBytesInIfd], // strips offset
278: [height], // RowsPerStrip
305: 'geotiff.js', // no array for ASCII(Z)
};
if (metadata) {
for (const i in metadata) {
if (metadata.hasOwnProperty(i)) {
ifd[i] = metadata[i];
}
}
}
const prfx = new Uint8Array(encodeIfds([ifd]));
const img = new Uint8Array(values);
const samplesPerPixel = ifd[277];
const data = new Uint8Array(numBytesInIfd + (width * height * samplesPerPixel));
times(prfx.length, (i) => {
data[i] = prfx[i];
});
forEach(img, (value, i) => {
data[numBytesInIfd + i] = value;
});
return data.buffer;
};
const convertToTids = (input) => {
const result = {};
for (const key in input) {
if (key !== 'StripOffsets') {
if (!name2code[key]) {
console.error(key, 'not in name2code:', Object.keys(name2code));
}
result[name2code[key]] = input[key];
}
}
return result;
};
const toArray = (input) => {
if (Array.isArray(input)) {
return input;
}
return [input];
};
const metadataDefaults = [
['Compression', 1], // no compression
['PlanarConfiguration', 1],
['ExtraSamples', 0],
];
export function writeGeotiff(data, metadata) {
const isFlattened = typeof data[0] === 'number';
let height;
let numBands;
let width;
let flattenedValues;
if (isFlattened) {
height = metadata.height || metadata.ImageLength;
width = metadata.width || metadata.ImageWidth;
numBands = data.length / (height * width);
flattenedValues = data;
} else {
numBands = data.length;
height = data[0].length;
width = data[0][0].length;
flattenedValues = [];
times(height, (rowIndex) => {
times(width, (columnIndex) => {
times(numBands, (bandIndex) => {
flattenedValues.push(data[bandIndex][rowIndex][columnIndex]);
});
});
});
}
metadata.ImageLength = height;
delete metadata.height;
metadata.ImageWidth = width;
delete metadata.width;
// consult https://www.loc.gov/preservation/digital/formats/content/tiff_tags.shtml
if (!metadata.BitsPerSample) {
metadata.BitsPerSample = times(numBands, () => 8);
}
metadataDefaults.forEach((tag) => {
const key = tag[0];
if (!metadata[key]) {
const value = tag[1];
metadata[key] = value;
}
});
// The color space of the image data.
// 1=black is zero and 2=RGB.
if (!metadata.PhotometricInterpretation) {
metadata.PhotometricInterpretation = metadata.BitsPerSample.length === 3 ? 2 : 1;
}
// The number of components per pixel.
if (!metadata.SamplesPerPixel) {
metadata.SamplesPerPixel = [numBands];
}
if (!metadata.StripByteCounts) {
// we are only writing one strip
metadata.StripByteCounts = [numBands * height * width];
}
if (!metadata.ModelPixelScale) {
// assumes raster takes up exactly the whole globe
metadata.ModelPixelScale = [360 / width, 180 / height, 0];
}
if (!metadata.SampleFormat) {
metadata.SampleFormat = times(numBands, () => 1);
}
// if didn't pass in projection information, assume the popular 4326 "geographic projection"
if (!metadata.hasOwnProperty('GeographicTypeGeoKey') && !metadata.hasOwnProperty('ProjectedCSTypeGeoKey')) {
metadata.GeographicTypeGeoKey = 4326;
metadata.ModelTiepoint = [0, 0, 0, -180, 90, 0]; // raster fits whole globe
metadata.GeogCitationGeoKey = 'WGS 84';
metadata.GTModelTypeGeoKey = 2;
}
const geoKeys = Object.keys(metadata)
.filter((key) => endsWith(key, 'GeoKey'))
.sort((a, b) => name2code[a] - name2code[b]);
if (!metadata.GeoAsciiParams) {
let geoAsciiParams = '';
geoKeys.forEach((name) => {
const code = Number(name2code[name]);
const tagType = fieldTagTypes[code];
if (tagType === 'ASCII') {
geoAsciiParams += `${metadata[name].toString()}\u0000`;
}
});
if (geoAsciiParams.length > 0) {
metadata.GeoAsciiParams = geoAsciiParams;
}
}
if (!metadata.GeoKeyDirectory) {
const NumberOfKeys = geoKeys.length;
const GeoKeyDirectory = [1, 1, 0, NumberOfKeys];
geoKeys.forEach((geoKey) => {
const KeyID = Number(name2code[geoKey]);
GeoKeyDirectory.push(KeyID);
let Count;
let TIFFTagLocation;
let valueOffset;
if (fieldTagTypes[KeyID] === 'SHORT') {
Count = 1;
TIFFTagLocation = 0;
valueOffset = metadata[geoKey];
} else if (geoKey === 'GeogCitationGeoKey') {
Count = metadata.GeoAsciiParams.length;
TIFFTagLocation = Number(name2code.GeoAsciiParams);
valueOffset = 0;
} else {
console.log(`[geotiff.js] couldn't get TIFFTagLocation for ${geoKey}`);
}
GeoKeyDirectory.push(TIFFTagLocation);
GeoKeyDirectory.push(Count);
GeoKeyDirectory.push(valueOffset);
});
metadata.GeoKeyDirectory = GeoKeyDirectory;
}
// delete GeoKeys from metadata, because stored in GeoKeyDirectory tag
for (const geoKey of geoKeys) {
if (metadata.hasOwnProperty(geoKey)) {
delete metadata[geoKey];
}
}
[
'Compression',
'ExtraSamples',
'GeographicTypeGeoKey',
'GTModelTypeGeoKey',
'GTRasterTypeGeoKey',
'ImageLength', // synonym of ImageHeight
'ImageWidth',
'Orientation',
'PhotometricInterpretation',
'ProjectedCSTypeGeoKey',
'PlanarConfiguration',
'ResolutionUnit',
'SamplesPerPixel',
'XPosition',
'YPosition',
'RowsPerStrip',
].forEach((name) => {
if (metadata[name]) {
metadata[name] = toArray(metadata[name]);
}
});
const encodedMetadata = convertToTids(metadata);
const outputImage = encodeImage(flattenedValues, width, height, encodedMetadata);
return outputImage;
}

View file

@ -0,0 +1,296 @@
export const fieldTagNames = {
// TIFF Baseline
0x013B: 'Artist',
0x0102: 'BitsPerSample',
0x0109: 'CellLength',
0x0108: 'CellWidth',
0x0140: 'ColorMap',
0x0103: 'Compression',
0x8298: 'Copyright',
0x0132: 'DateTime',
0x0152: 'ExtraSamples',
0x010A: 'FillOrder',
0x0121: 'FreeByteCounts',
0x0120: 'FreeOffsets',
0x0123: 'GrayResponseCurve',
0x0122: 'GrayResponseUnit',
0x013C: 'HostComputer',
0x010E: 'ImageDescription',
0x0101: 'ImageLength',
0x0100: 'ImageWidth',
0x010F: 'Make',
0x0119: 'MaxSampleValue',
0x0118: 'MinSampleValue',
0x0110: 'Model',
0x00FE: 'NewSubfileType',
0x0112: 'Orientation',
0x0106: 'PhotometricInterpretation',
0x011C: 'PlanarConfiguration',
0x0128: 'ResolutionUnit',
0x0116: 'RowsPerStrip',
0x0115: 'SamplesPerPixel',
0x0131: 'Software',
0x0117: 'StripByteCounts',
0x0111: 'StripOffsets',
0x00FF: 'SubfileType',
0x0107: 'Threshholding',
0x011A: 'XResolution',
0x011B: 'YResolution',
// TIFF Extended
0x0146: 'BadFaxLines',
0x0147: 'CleanFaxData',
0x0157: 'ClipPath',
0x0148: 'ConsecutiveBadFaxLines',
0x01B1: 'Decode',
0x01B2: 'DefaultImageColor',
0x010D: 'DocumentName',
0x0150: 'DotRange',
0x0141: 'HalftoneHints',
0x015A: 'Indexed',
0x015B: 'JPEGTables',
0x011D: 'PageName',
0x0129: 'PageNumber',
0x013D: 'Predictor',
0x013F: 'PrimaryChromaticities',
0x0214: 'ReferenceBlackWhite',
0x0153: 'SampleFormat',
0x0154: 'SMinSampleValue',
0x0155: 'SMaxSampleValue',
0x022F: 'StripRowCounts',
0x014A: 'SubIFDs',
0x0124: 'T4Options',
0x0125: 'T6Options',
0x0145: 'TileByteCounts',
0x0143: 'TileLength',
0x0144: 'TileOffsets',
0x0142: 'TileWidth',
0x012D: 'TransferFunction',
0x013E: 'WhitePoint',
0x0158: 'XClipPathUnits',
0x011E: 'XPosition',
0x0211: 'YCbCrCoefficients',
0x0213: 'YCbCrPositioning',
0x0212: 'YCbCrSubSampling',
0x0159: 'YClipPathUnits',
0x011F: 'YPosition',
// EXIF
0x9202: 'ApertureValue',
0xA001: 'ColorSpace',
0x9004: 'DateTimeDigitized',
0x9003: 'DateTimeOriginal',
0x8769: 'Exif IFD',
0x9000: 'ExifVersion',
0x829A: 'ExposureTime',
0xA300: 'FileSource',
0x9209: 'Flash',
0xA000: 'FlashpixVersion',
0x829D: 'FNumber',
0xA420: 'ImageUniqueID',
0x9208: 'LightSource',
0x927C: 'MakerNote',
0x9201: 'ShutterSpeedValue',
0x9286: 'UserComment',
// IPTC
0x83BB: 'IPTC',
// ICC
0x8773: 'ICC Profile',
// XMP
0x02BC: 'XMP',
// GDAL
0xA480: 'GDAL_METADATA',
0xA481: 'GDAL_NODATA',
// Photoshop
0x8649: 'Photoshop',
// GeoTiff
0x830E: 'ModelPixelScale',
0x8482: 'ModelTiepoint',
0x85D8: 'ModelTransformation',
0x87AF: 'GeoKeyDirectory',
0x87B0: 'GeoDoubleParams',
0x87B1: 'GeoAsciiParams',
// LERC
0xC5F2: 'LercParameters',
};
export const fieldTags = {};
for (const key in fieldTagNames) {
if (fieldTagNames.hasOwnProperty(key)) {
fieldTags[fieldTagNames[key]] = parseInt(key, 10);
}
}
export const fieldTagTypes = {
256: 'SHORT',
257: 'SHORT',
258: 'SHORT',
259: 'SHORT',
262: 'SHORT',
273: 'LONG',
274: 'SHORT',
277: 'SHORT',
278: 'LONG',
279: 'LONG',
282: 'RATIONAL',
283: 'RATIONAL',
284: 'SHORT',
286: 'SHORT',
287: 'RATIONAL',
296: 'SHORT',
297: 'SHORT',
305: 'ASCII',
306: 'ASCII',
338: 'SHORT',
339: 'SHORT',
513: 'LONG',
514: 'LONG',
1024: 'SHORT',
1025: 'SHORT',
2048: 'SHORT',
2049: 'ASCII',
3072: 'SHORT',
3073: 'ASCII',
33550: 'DOUBLE',
33922: 'DOUBLE',
34264: 'DOUBLE',
34665: 'LONG',
34735: 'SHORT',
34736: 'DOUBLE',
34737: 'ASCII',
42113: 'ASCII',
};
export const arrayFields = [
fieldTags.BitsPerSample,
fieldTags.ExtraSamples,
fieldTags.SampleFormat,
fieldTags.StripByteCounts,
fieldTags.StripOffsets,
fieldTags.StripRowCounts,
fieldTags.TileByteCounts,
fieldTags.TileOffsets,
fieldTags.SubIFDs,
];
export const fieldTypeNames = {
0x0001: 'BYTE',
0x0002: 'ASCII',
0x0003: 'SHORT',
0x0004: 'LONG',
0x0005: 'RATIONAL',
0x0006: 'SBYTE',
0x0007: 'UNDEFINED',
0x0008: 'SSHORT',
0x0009: 'SLONG',
0x000A: 'SRATIONAL',
0x000B: 'FLOAT',
0x000C: 'DOUBLE',
// IFD offset, suggested by https://owl.phy.queensu.ca/~phil/exiftool/standards.html
0x000D: 'IFD',
// introduced by BigTIFF
0x0010: 'LONG8',
0x0011: 'SLONG8',
0x0012: 'IFD8',
};
export const fieldTypes = {};
for (const key in fieldTypeNames) {
if (fieldTypeNames.hasOwnProperty(key)) {
fieldTypes[fieldTypeNames[key]] = parseInt(key, 10);
}
}
export const photometricInterpretations = {
WhiteIsZero: 0,
BlackIsZero: 1,
RGB: 2,
Palette: 3,
TransparencyMask: 4,
CMYK: 5,
YCbCr: 6,
CIELab: 8,
ICCLab: 9,
};
export const ExtraSamplesValues = {
Unspecified: 0,
Assocalpha: 1,
Unassalpha: 2,
};
export const LercParameters = {
Version: 0,
AddCompression: 1,
};
export const LercAddCompression = {
None: 0,
Deflate: 1,
Zstandard: 2,
};
export const geoKeyNames = {
1024: 'GTModelTypeGeoKey',
1025: 'GTRasterTypeGeoKey',
1026: 'GTCitationGeoKey',
2048: 'GeographicTypeGeoKey',
2049: 'GeogCitationGeoKey',
2050: 'GeogGeodeticDatumGeoKey',
2051: 'GeogPrimeMeridianGeoKey',
2052: 'GeogLinearUnitsGeoKey',
2053: 'GeogLinearUnitSizeGeoKey',
2054: 'GeogAngularUnitsGeoKey',
2055: 'GeogAngularUnitSizeGeoKey',
2056: 'GeogEllipsoidGeoKey',
2057: 'GeogSemiMajorAxisGeoKey',
2058: 'GeogSemiMinorAxisGeoKey',
2059: 'GeogInvFlatteningGeoKey',
2060: 'GeogAzimuthUnitsGeoKey',
2061: 'GeogPrimeMeridianLongGeoKey',
2062: 'GeogTOWGS84GeoKey',
3072: 'ProjectedCSTypeGeoKey',
3073: 'PCSCitationGeoKey',
3074: 'ProjectionGeoKey',
3075: 'ProjCoordTransGeoKey',
3076: 'ProjLinearUnitsGeoKey',
3077: 'ProjLinearUnitSizeGeoKey',
3078: 'ProjStdParallel1GeoKey',
3079: 'ProjStdParallel2GeoKey',
3080: 'ProjNatOriginLongGeoKey',
3081: 'ProjNatOriginLatGeoKey',
3082: 'ProjFalseEastingGeoKey',
3083: 'ProjFalseNorthingGeoKey',
3084: 'ProjFalseOriginLongGeoKey',
3085: 'ProjFalseOriginLatGeoKey',
3086: 'ProjFalseOriginEastingGeoKey',
3087: 'ProjFalseOriginNorthingGeoKey',
3088: 'ProjCenterLongGeoKey',
3089: 'ProjCenterLatGeoKey',
3090: 'ProjCenterEastingGeoKey',
3091: 'ProjCenterNorthingGeoKey',
3092: 'ProjScaleAtNatOriginGeoKey',
3093: 'ProjScaleAtCenterGeoKey',
3094: 'ProjAzimuthAngleGeoKey',
3095: 'ProjStraightVertPoleLongGeoKey',
3096: 'ProjRectifiedGridAngleGeoKey',
4096: 'VerticalCSTypeGeoKey',
4097: 'VerticalCitationGeoKey',
4098: 'VerticalDatumGeoKey',
4099: 'VerticalUnitsGeoKey',
};
export const geoKeys = {};
for (const key in geoKeyNames) {
if (geoKeyNames.hasOwnProperty(key)) {
geoKeys[geoKeyNames[key]] = parseInt(key, 10);
}
}

View file

@ -0,0 +1,56 @@
/**
* A no-op logger
*/
class DummyLogger {
log() {}
debug() {}
info() {}
warn() {}
error() {}
time() {}
timeEnd() {}
}
let LOGGER = new DummyLogger();
/**
*
* @param {object} logger the new logger. e.g `console`
*/
export function setLogger(logger = new DummyLogger()) {
LOGGER = logger;
}
export function debug(...args) {
return LOGGER.debug(...args);
}
export function log(...args) {
return LOGGER.log(...args);
}
export function info(...args) {
return LOGGER.info(...args);
}
export function warn(...args) {
return LOGGER.warn(...args);
}
export function error(...args) {
return LOGGER.error(...args);
}
export function time(...args) {
return LOGGER.time(...args);
}
export function timeEnd(...args) {
return LOGGER.timeEnd(...args);
}

View file

@ -0,0 +1,101 @@
import { getDecoder } from './compression/index.js';
const defaultPoolSize = typeof navigator !== 'undefined' ? (navigator.hardwareConcurrency || 2) : 2;
/**
* @module pool
*/
/**
* Pool for workers to decode chunks of the images.
*/
class Pool {
/**
* @constructor
* @param {Number} [size] The size of the pool. Defaults to the number of CPUs
* available. When this parameter is `null` or 0, then the
* decoding will be done in the main thread.
* @param {function(): Worker} [createWorker] A function that creates the decoder worker.
* Defaults to a worker with all decoders that ship with geotiff.js. The `createWorker()`
* function is expected to return a `Worker` compatible with Web Workers. For code that
* runs in Node, [web-worker](https://www.npmjs.com/package/web-worker) is a good choice.
*
* A worker that uses a custom lzw decoder would look like this `my-custom-worker.js` file:
* ```js
* import { addDecoder, getDecoder } from 'geotiff';
* addDecoder(5, () => import ('./my-custom-lzw').then((m) => m.default));
* self.addEventListener('message', async (e) => {
* const { id, fileDirectory, buffer } = e.data;
* const decoder = await getDecoder(fileDirectory);
* const decoded = await decoder.decode(fileDirectory, buffer);
* self.postMessage({ decoded, id }, [decoded]);
* });
* ```
* The way the above code is built into a worker by the `createWorker()` function
* depends on the used bundler. For most bundlers, something like this will work:
* ```js
* function createWorker() {
* return new Worker(new URL('./my-custom-worker.js', import.meta.url));
* }
* ```
*/
constructor(size = defaultPoolSize, createWorker) {
this.workers = null;
this._awaitingDecoder = null;
this.size = size;
this.messageId = 0;
if (size) {
this._awaitingDecoder = createWorker ? Promise.resolve(createWorker) : new Promise((resolve) => {
import('./worker/decoder.js').then((module) => {
resolve(module.create);
});
});
this._awaitingDecoder.then((create) => {
this._awaitingDecoder = null;
this.workers = [];
for (let i = 0; i < size; i++) {
this.workers.push({ worker: create(), idle: true });
}
});
}
}
/**
* Decode the given block of bytes with the set compression method.
* @param {ArrayBuffer} buffer the array buffer of bytes to decode.
* @returns {Promise<ArrayBuffer>} the decoded result as a `Promise`
*/
async decode(fileDirectory, buffer) {
if (this._awaitingDecoder) {
await this._awaitingDecoder;
}
return this.size === 0
? getDecoder(fileDirectory).then((decoder) => decoder.decode(fileDirectory, buffer))
: new Promise((resolve) => {
const worker = this.workers.find((candidate) => candidate.idle)
|| this.workers[Math.floor(Math.random() * this.size)];
worker.idle = false;
const id = this.messageId++;
const onMessage = (e) => {
if (e.data.id === id) {
worker.idle = true;
resolve(e.data.decoded);
worker.worker.removeEventListener('message', onMessage);
}
};
worker.worker.addEventListener('message', onMessage);
worker.worker.postMessage({ fileDirectory, buffer, id }, [buffer]);
});
}
destroy() {
if (this.workers) {
this.workers.forEach((worker) => {
worker.worker.terminate();
});
this.workers = null;
}
}
}
export default Pool;

View file

@ -0,0 +1,88 @@
function decodeRowAcc(row, stride) {
let length = row.length - stride;
let offset = 0;
do {
for (let i = stride; i > 0; i--) {
row[offset + stride] += row[offset];
offset++;
}
length -= stride;
} while (length > 0);
}
function decodeRowFloatingPoint(row, stride, bytesPerSample) {
let index = 0;
let count = row.length;
const wc = count / bytesPerSample;
while (count > stride) {
for (let i = stride; i > 0; --i) {
row[index + stride] += row[index];
++index;
}
count -= stride;
}
const copy = row.slice();
for (let i = 0; i < wc; ++i) {
for (let b = 0; b < bytesPerSample; ++b) {
row[(bytesPerSample * i) + b] = copy[((bytesPerSample - b - 1) * wc) + i];
}
}
}
export function applyPredictor(block, predictor, width, height, bitsPerSample,
planarConfiguration) {
if (!predictor || predictor === 1) {
return block;
}
for (let i = 0; i < bitsPerSample.length; ++i) {
if (bitsPerSample[i] % 8 !== 0) {
throw new Error('When decoding with predictor, only multiple of 8 bits are supported.');
}
if (bitsPerSample[i] !== bitsPerSample[0]) {
throw new Error('When decoding with predictor, all samples must have the same size.');
}
}
const bytesPerSample = bitsPerSample[0] / 8;
const stride = planarConfiguration === 2 ? 1 : bitsPerSample.length;
for (let i = 0; i < height; ++i) {
// Last strip will be truncated if height % stripHeight != 0
if (i * stride * width * bytesPerSample >= block.byteLength) {
break;
}
let row;
if (predictor === 2) { // horizontal prediction
switch (bitsPerSample[0]) {
case 8:
row = new Uint8Array(
block, i * stride * width * bytesPerSample, stride * width * bytesPerSample,
);
break;
case 16:
row = new Uint16Array(
block, i * stride * width * bytesPerSample, stride * width * bytesPerSample / 2,
);
break;
case 32:
row = new Uint32Array(
block, i * stride * width * bytesPerSample, stride * width * bytesPerSample / 4,
);
break;
default:
throw new Error(`Predictor 2 not allowed with ${bitsPerSample[0]} bits per sample.`);
}
decodeRowAcc(row, stride, bytesPerSample);
} else if (predictor === 3) { // horizontal floating point
row = new Uint8Array(
block, i * stride * width * bytesPerSample, stride * width * bytesPerSample,
);
decodeRowFloatingPoint(row, stride, bytesPerSample);
}
}
return block;
}

View file

@ -0,0 +1,211 @@
/**
* @module resample
*/
function copyNewSize(array, width, height, samplesPerPixel = 1) {
return new (Object.getPrototypeOf(array).constructor)(width * height * samplesPerPixel);
}
/**
* Resample the input arrays using nearest neighbor value selection.
* @param {TypedArray[]} valueArrays The input arrays to resample
* @param {number} inWidth The width of the input rasters
* @param {number} inHeight The height of the input rasters
* @param {number} outWidth The desired width of the output rasters
* @param {number} outHeight The desired height of the output rasters
* @returns {TypedArray[]} The resampled rasters
*/
export function resampleNearest(valueArrays, inWidth, inHeight, outWidth, outHeight) {
const relX = inWidth / outWidth;
const relY = inHeight / outHeight;
return valueArrays.map((array) => {
const newArray = copyNewSize(array, outWidth, outHeight);
for (let y = 0; y < outHeight; ++y) {
const cy = Math.min(Math.round(relY * y), inHeight - 1);
for (let x = 0; x < outWidth; ++x) {
const cx = Math.min(Math.round(relX * x), inWidth - 1);
const value = array[(cy * inWidth) + cx];
newArray[(y * outWidth) + x] = value;
}
}
return newArray;
});
}
// simple linear interpolation, code from:
// https://en.wikipedia.org/wiki/Linear_interpolation#Programming_language_support
function lerp(v0, v1, t) {
return ((1 - t) * v0) + (t * v1);
}
/**
* Resample the input arrays using bilinear interpolation.
* @param {TypedArray[]} valueArrays The input arrays to resample
* @param {number} inWidth The width of the input rasters
* @param {number} inHeight The height of the input rasters
* @param {number} outWidth The desired width of the output rasters
* @param {number} outHeight The desired height of the output rasters
* @returns {TypedArray[]} The resampled rasters
*/
export function resampleBilinear(valueArrays, inWidth, inHeight, outWidth, outHeight) {
const relX = inWidth / outWidth;
const relY = inHeight / outHeight;
return valueArrays.map((array) => {
const newArray = copyNewSize(array, outWidth, outHeight);
for (let y = 0; y < outHeight; ++y) {
const rawY = relY * y;
const yl = Math.floor(rawY);
const yh = Math.min(Math.ceil(rawY), (inHeight - 1));
for (let x = 0; x < outWidth; ++x) {
const rawX = relX * x;
const tx = rawX % 1;
const xl = Math.floor(rawX);
const xh = Math.min(Math.ceil(rawX), (inWidth - 1));
const ll = array[(yl * inWidth) + xl];
const hl = array[(yl * inWidth) + xh];
const lh = array[(yh * inWidth) + xl];
const hh = array[(yh * inWidth) + xh];
const value = lerp(
lerp(ll, hl, tx),
lerp(lh, hh, tx),
rawY % 1,
);
newArray[(y * outWidth) + x] = value;
}
}
return newArray;
});
}
/**
* Resample the input arrays using the selected resampling method.
* @param {TypedArray[]} valueArrays The input arrays to resample
* @param {number} inWidth The width of the input rasters
* @param {number} inHeight The height of the input rasters
* @param {number} outWidth The desired width of the output rasters
* @param {number} outHeight The desired height of the output rasters
* @param {string} [method = 'nearest'] The desired resampling method
* @returns {TypedArray[]} The resampled rasters
*/
export function resample(valueArrays, inWidth, inHeight, outWidth, outHeight, method = 'nearest') {
switch (method.toLowerCase()) {
case 'nearest':
return resampleNearest(valueArrays, inWidth, inHeight, outWidth, outHeight);
case 'bilinear':
case 'linear':
return resampleBilinear(valueArrays, inWidth, inHeight, outWidth, outHeight);
default:
throw new Error(`Unsupported resampling method: '${method}'`);
}
}
/**
* Resample the pixel interleaved input array using nearest neighbor value selection.
* @param {TypedArray} valueArrays The input arrays to resample
* @param {number} inWidth The width of the input rasters
* @param {number} inHeight The height of the input rasters
* @param {number} outWidth The desired width of the output rasters
* @param {number} outHeight The desired height of the output rasters
* @param {number} samples The number of samples per pixel for pixel
* interleaved data
* @returns {TypedArray} The resampled raster
*/
export function resampleNearestInterleaved(
valueArray, inWidth, inHeight, outWidth, outHeight, samples) {
const relX = inWidth / outWidth;
const relY = inHeight / outHeight;
const newArray = copyNewSize(valueArray, outWidth, outHeight, samples);
for (let y = 0; y < outHeight; ++y) {
const cy = Math.min(Math.round(relY * y), inHeight - 1);
for (let x = 0; x < outWidth; ++x) {
const cx = Math.min(Math.round(relX * x), inWidth - 1);
for (let i = 0; i < samples; ++i) {
const value = valueArray[(cy * inWidth * samples) + (cx * samples) + i];
newArray[(y * outWidth * samples) + (x * samples) + i] = value;
}
}
}
return newArray;
}
/**
* Resample the pixel interleaved input array using bilinear interpolation.
* @param {TypedArray} valueArrays The input arrays to resample
* @param {number} inWidth The width of the input rasters
* @param {number} inHeight The height of the input rasters
* @param {number} outWidth The desired width of the output rasters
* @param {number} outHeight The desired height of the output rasters
* @param {number} samples The number of samples per pixel for pixel
* interleaved data
* @returns {TypedArray} The resampled raster
*/
export function resampleBilinearInterleaved(
valueArray, inWidth, inHeight, outWidth, outHeight, samples) {
const relX = inWidth / outWidth;
const relY = inHeight / outHeight;
const newArray = copyNewSize(valueArray, outWidth, outHeight, samples);
for (let y = 0; y < outHeight; ++y) {
const rawY = relY * y;
const yl = Math.floor(rawY);
const yh = Math.min(Math.ceil(rawY), (inHeight - 1));
for (let x = 0; x < outWidth; ++x) {
const rawX = relX * x;
const tx = rawX % 1;
const xl = Math.floor(rawX);
const xh = Math.min(Math.ceil(rawX), (inWidth - 1));
for (let i = 0; i < samples; ++i) {
const ll = valueArray[(yl * inWidth * samples) + (xl * samples) + i];
const hl = valueArray[(yl * inWidth * samples) + (xh * samples) + i];
const lh = valueArray[(yh * inWidth * samples) + (xl * samples) + i];
const hh = valueArray[(yh * inWidth * samples) + (xh * samples) + i];
const value = lerp(
lerp(ll, hl, tx),
lerp(lh, hh, tx),
rawY % 1,
);
newArray[(y * outWidth * samples) + (x * samples) + i] = value;
}
}
}
return newArray;
}
/**
* Resample the pixel interleaved input array using the selected resampling method.
* @param {TypedArray} valueArray The input array to resample
* @param {number} inWidth The width of the input rasters
* @param {number} inHeight The height of the input rasters
* @param {number} outWidth The desired width of the output rasters
* @param {number} outHeight The desired height of the output rasters
* @param {number} samples The number of samples per pixel for pixel
* interleaved data
* @param {string} [method = 'nearest'] The desired resampling method
* @returns {TypedArray} The resampled rasters
*/
export function resampleInterleaved(valueArray, inWidth, inHeight, outWidth, outHeight, samples, method = 'nearest') {
switch (method.toLowerCase()) {
case 'nearest':
return resampleNearestInterleaved(
valueArray, inWidth, inHeight, outWidth, outHeight, samples,
);
case 'bilinear':
case 'linear':
return resampleBilinearInterleaved(
valueArray, inWidth, inHeight, outWidth, outHeight, samples,
);
default:
throw new Error(`Unsupported resampling method: '${method}'`);
}
}

View file

@ -0,0 +1,111 @@
export function fromWhiteIsZero(raster, max) {
const { width, height } = raster;
const rgbRaster = new Uint8Array(width * height * 3);
let value;
for (let i = 0, j = 0; i < raster.length; ++i, j += 3) {
value = 256 - (raster[i] / max * 256);
rgbRaster[j] = value;
rgbRaster[j + 1] = value;
rgbRaster[j + 2] = value;
}
return rgbRaster;
}
export function fromBlackIsZero(raster, max) {
const { width, height } = raster;
const rgbRaster = new Uint8Array(width * height * 3);
let value;
for (let i = 0, j = 0; i < raster.length; ++i, j += 3) {
value = raster[i] / max * 256;
rgbRaster[j] = value;
rgbRaster[j + 1] = value;
rgbRaster[j + 2] = value;
}
return rgbRaster;
}
export function fromPalette(raster, colorMap) {
const { width, height } = raster;
const rgbRaster = new Uint8Array(width * height * 3);
const greenOffset = colorMap.length / 3;
const blueOffset = colorMap.length / 3 * 2;
for (let i = 0, j = 0; i < raster.length; ++i, j += 3) {
const mapIndex = raster[i];
rgbRaster[j] = colorMap[mapIndex] / 65536 * 256;
rgbRaster[j + 1] = colorMap[mapIndex + greenOffset] / 65536 * 256;
rgbRaster[j + 2] = colorMap[mapIndex + blueOffset] / 65536 * 256;
}
return rgbRaster;
}
export function fromCMYK(cmykRaster) {
const { width, height } = cmykRaster;
const rgbRaster = new Uint8Array(width * height * 3);
for (let i = 0, j = 0; i < cmykRaster.length; i += 4, j += 3) {
const c = cmykRaster[i];
const m = cmykRaster[i + 1];
const y = cmykRaster[i + 2];
const k = cmykRaster[i + 3];
rgbRaster[j] = 255 * ((255 - c) / 256) * ((255 - k) / 256);
rgbRaster[j + 1] = 255 * ((255 - m) / 256) * ((255 - k) / 256);
rgbRaster[j + 2] = 255 * ((255 - y) / 256) * ((255 - k) / 256);
}
return rgbRaster;
}
export function fromYCbCr(yCbCrRaster) {
const { width, height } = yCbCrRaster;
const rgbRaster = new Uint8ClampedArray(width * height * 3);
for (let i = 0, j = 0; i < yCbCrRaster.length; i += 3, j += 3) {
const y = yCbCrRaster[i];
const cb = yCbCrRaster[i + 1];
const cr = yCbCrRaster[i + 2];
rgbRaster[j] = (y + (1.40200 * (cr - 0x80)));
rgbRaster[j + 1] = (y - (0.34414 * (cb - 0x80)) - (0.71414 * (cr - 0x80)));
rgbRaster[j + 2] = (y + (1.77200 * (cb - 0x80)));
}
return rgbRaster;
}
const Xn = 0.95047;
const Yn = 1.00000;
const Zn = 1.08883;
// from https://github.com/antimatter15/rgb-lab/blob/master/color.js
export function fromCIELab(cieLabRaster) {
const { width, height } = cieLabRaster;
const rgbRaster = new Uint8Array(width * height * 3);
for (let i = 0, j = 0; i < cieLabRaster.length; i += 3, j += 3) {
const L = cieLabRaster[i + 0];
const a_ = cieLabRaster[i + 1] << 24 >> 24; // conversion from uint8 to int8
const b_ = cieLabRaster[i + 2] << 24 >> 24; // same
let y = (L + 16) / 116;
let x = (a_ / 500) + y;
let z = y - (b_ / 200);
let r;
let g;
let b;
x = Xn * ((x * x * x > 0.008856) ? x * x * x : (x - (16 / 116)) / 7.787);
y = Yn * ((y * y * y > 0.008856) ? y * y * y : (y - (16 / 116)) / 7.787);
z = Zn * ((z * z * z > 0.008856) ? z * z * z : (z - (16 / 116)) / 7.787);
r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986);
g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415);
b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570);
r = (r > 0.0031308) ? ((1.055 * (r ** (1 / 2.4))) - 0.055) : 12.92 * r;
g = (g > 0.0031308) ? ((1.055 * (g ** (1 / 2.4))) - 0.055) : 12.92 * g;
b = (b > 0.0031308) ? ((1.055 * (b ** (1 / 2.4))) - 0.055) : 12.92 * b;
rgbRaster[j] = Math.max(0, Math.min(1, r)) * 255;
rgbRaster[j + 1] = Math.max(0, Math.min(1, g)) * 255;
rgbRaster[j + 2] = Math.max(0, Math.min(1, b)) * 255;
}
return rgbRaster;
}

View file

@ -0,0 +1,20 @@
import { BaseSource } from './basesource.js';
import { AbortError } from '../utils.js';
class ArrayBufferSource extends BaseSource {
constructor(arrayBuffer) {
super();
this.arrayBuffer = arrayBuffer;
}
fetchSlice(slice, signal) {
if (signal && signal.aborted) {
throw new AbortError('Request aborted');
}
return this.arrayBuffer.slice(slice.offset, slice.offset + slice.length);
}
}
export function makeBufferSource(arrayBuffer) {
return new ArrayBufferSource(arrayBuffer);
}

View file

@ -0,0 +1,38 @@
/**
* @typedef Slice
* @property {number} offset
* @property {number} length
*/
export class BaseSource {
/**
*
* @param {Slice[]} slices
* @returns {ArrayBuffer[]}
*/
async fetch(slices, signal = undefined) {
return Promise.all(
slices.map((slice) => this.fetchSlice(slice, signal)),
);
}
/**
*
* @param {Slice} slice
* @returns {ArrayBuffer}
*/
async fetchSlice(slice) {
throw new Error(`fetching of slice ${slice} not possible, not implemented`);
}
/**
* Returns the filesize if already determined and null otherwise
*/
get fileSize() {
return null;
}
async close() {
// no-op by default
}
}

View file

@ -0,0 +1,296 @@
import QuickLRU from 'quick-lru';
import { BaseSource } from './basesource.js';
import { AbortError, AggregateError, wait, zip } from '../utils.js';
class Block {
/**
*
* @param {number} offset
* @param {number} length
* @param {ArrayBuffer} [data]
*/
constructor(offset, length, data = null) {
this.offset = offset;
this.length = length;
this.data = data;
}
/**
* @returns {number} the top byte border
*/
get top() {
return this.offset + this.length;
}
}
class BlockGroup {
/**
*
* @param {number} offset
* @param {number} length
* @param {number[]} blockIds
*/
constructor(offset, length, blockIds) {
this.offset = offset;
this.length = length;
this.blockIds = blockIds;
}
}
export class BlockedSource extends BaseSource {
/**
*
* @param {BaseSource} source The underlying source that shall be blocked and cached
* @param {object} options
* @param {number} [options.blockSize]
* @param {number} [options.cacheSize]
*/
constructor(source, { blockSize = 65536, cacheSize = 100 } = {}) {
super();
this.source = source;
this.blockSize = blockSize;
this.blockCache = new QuickLRU({
maxSize: cacheSize,
onEviction: (blockId, block) => {
this.evictedBlocks.set(blockId, block);
},
});
/** @type {Map<number, Block>} */
this.evictedBlocks = new Map();
// mapping blockId -> Block instance
this.blockRequests = new Map();
// set of blockIds missing for the current requests
this.blockIdsToFetch = new Set();
this.abortedBlockIds = new Set();
}
get fileSize() {
return this.source.fileSize;
}
/**
*
* @param {import("./basesource").Slice[]} slices
*/
async fetch(slices, signal) {
const blockRequests = [];
const missingBlockIds = [];
const allBlockIds = [];
this.evictedBlocks.clear();
for (const { offset, length } of slices) {
let top = offset + length;
const { fileSize } = this;
if (fileSize !== null) {
top = Math.min(top, fileSize);
}
const firstBlockOffset = Math.floor(offset / this.blockSize) * this.blockSize;
for (let current = firstBlockOffset; current < top; current += this.blockSize) {
const blockId = Math.floor(current / this.blockSize);
if (!this.blockCache.has(blockId) && !this.blockRequests.has(blockId)) {
this.blockIdsToFetch.add(blockId);
missingBlockIds.push(blockId);
}
if (this.blockRequests.has(blockId)) {
blockRequests.push(this.blockRequests.get(blockId));
}
allBlockIds.push(blockId);
}
}
// allow additional block requests to accumulate
await wait();
this.fetchBlocks(signal);
// Gather all of the new requests that this fetch call is contributing to `fetch`.
const missingRequests = [];
for (const blockId of missingBlockIds) {
// The requested missing block could already be in the cache
// instead of having its request still be outstanding.
if (this.blockRequests.has(blockId)) {
missingRequests.push(this.blockRequests.get(blockId));
}
}
// Actually await all pending requests that are needed for this `fetch`.
await Promise.allSettled(blockRequests);
await Promise.allSettled(missingRequests);
// Perform retries if a block was interrupted by a previous signal
const abortedBlockRequests = [];
const abortedBlockIds = allBlockIds
.filter((id) => this.abortedBlockIds.has(id) || !this.blockCache.has(id));
abortedBlockIds.forEach((id) => this.blockIdsToFetch.add(id));
// start the retry of some blocks if required
if (abortedBlockIds.length > 0 && signal && !signal.aborted) {
this.fetchBlocks(null);
for (const blockId of abortedBlockIds) {
const block = this.blockRequests.get(blockId);
if (!block) {
throw new Error(`Block ${blockId} is not in the block requests`);
}
abortedBlockRequests.push(block);
}
await Promise.allSettled(abortedBlockRequests);
}
// throw an abort error
if (signal && signal.aborted) {
throw new AbortError('Request was aborted');
}
const blocks = allBlockIds.map((id) => this.blockCache.get(id) || this.evictedBlocks.get(id));
const failedBlocks = blocks.filter((i) => !i);
if (failedBlocks.length) {
throw new AggregateError(failedBlocks, 'Request failed');
}
// create a final Map, with all required blocks for this request to satisfy
const requiredBlocks = new Map(zip(allBlockIds, blocks));
// TODO: satisfy each slice
return this.readSliceData(slices, requiredBlocks);
}
/**
*
* @param {AbortSignal} signal
*/
fetchBlocks(signal) {
// check if we still need to
if (this.blockIdsToFetch.size > 0) {
const groups = this.groupBlocks(this.blockIdsToFetch);
// start requesting slices of data
const groupRequests = this.source.fetch(groups, signal);
for (let groupIndex = 0; groupIndex < groups.length; ++groupIndex) {
const group = groups[groupIndex];
for (const blockId of group.blockIds) {
// make an async IIFE for each block
this.blockRequests.set(blockId, (async () => {
try {
const response = (await groupRequests)[groupIndex];
const blockOffset = blockId * this.blockSize;
const o = blockOffset - response.offset;
const t = Math.min(o + this.blockSize, response.data.byteLength);
const data = response.data.slice(o, t);
const block = new Block(
blockOffset,
data.byteLength,
data,
blockId,
);
this.blockCache.set(blockId, block);
this.abortedBlockIds.delete(blockId);
} catch (err) {
if (err.name === 'AbortError') {
// store the signal here, we need it to determine later if an
// error was caused by this signal
err.signal = signal;
this.blockCache.delete(blockId);
this.abortedBlockIds.add(blockId);
} else {
throw err;
}
} finally {
this.blockRequests.delete(blockId);
}
})());
}
}
this.blockIdsToFetch.clear();
}
}
/**
*
* @param {Set} blockIds
* @returns {BlockGroup[]}
*/
groupBlocks(blockIds) {
const sortedBlockIds = Array.from(blockIds).sort((a, b) => a - b);
if (sortedBlockIds.length === 0) {
return [];
}
let current = [];
let lastBlockId = null;
const groups = [];
for (const blockId of sortedBlockIds) {
if (lastBlockId === null || lastBlockId + 1 === blockId) {
current.push(blockId);
lastBlockId = blockId;
} else {
groups.push(new BlockGroup(
current[0] * this.blockSize,
current.length * this.blockSize,
current,
));
current = [blockId];
lastBlockId = blockId;
}
}
groups.push(new BlockGroup(
current[0] * this.blockSize,
current.length * this.blockSize,
current,
));
return groups;
}
/**
*
* @param {import("./basesource").Slice[]} slices
* @param {Map} blocks
*/
readSliceData(slices, blocks) {
return slices.map((slice) => {
let top = slice.offset + slice.length;
if (this.fileSize !== null) {
top = Math.min(this.fileSize, top);
}
const blockIdLow = Math.floor(slice.offset / this.blockSize);
const blockIdHigh = Math.floor(top / this.blockSize);
const sliceData = new ArrayBuffer(slice.length);
const sliceView = new Uint8Array(sliceData);
for (let blockId = blockIdLow; blockId <= blockIdHigh; ++blockId) {
const block = blocks.get(blockId);
const delta = block.offset - slice.offset;
const topDelta = block.top - top;
let blockInnerOffset = 0;
let rangeInnerOffset = 0;
let usedBlockLength;
if (delta < 0) {
blockInnerOffset = -delta;
} else if (delta > 0) {
rangeInnerOffset = delta;
}
if (topDelta < 0) {
usedBlockLength = block.length - blockInnerOffset;
} else {
usedBlockLength = top - block.offset - blockInnerOffset;
}
const blockView = new Uint8Array(block.data, blockInnerOffset, usedBlockLength);
sliceView.set(blockView, rangeInnerOffset);
}
return sliceData;
});
}
}

View file

@ -0,0 +1,45 @@
export class BaseResponse {
/**
* Returns whether the response has an ok'ish status code
*/
get ok() {
return this.status >= 200 && this.status <= 299;
}
/**
* Returns the status code of the response
*/
get status() {
throw new Error('not implemented');
}
/**
* Returns the value of the specified header
* @param {string} headerName the header name
* @returns {string} the header value
*/
getHeader(headerName) { // eslint-disable-line no-unused-vars
throw new Error('not implemented');
}
/**
* @returns {ArrayBuffer} the response data of the request
*/
async getData() {
throw new Error('not implemented');
}
}
export class BaseClient {
constructor(url) {
this.url = url;
}
/**
* Send a request with the options
* @param {object} [options]
*/
async request({ headers, credentials, signal } = {}) { // eslint-disable-line no-unused-vars
throw new Error('request is not implemented');
}
}

View file

@ -0,0 +1,41 @@
import { BaseClient, BaseResponse } from './base.js';
class FetchResponse extends BaseResponse {
/**
* BaseResponse facade for fetch API Response
* @param {Response} response
*/
constructor(response) {
super();
this.response = response;
}
get status() {
return this.response.status;
}
getHeader(name) {
return this.response.headers.get(name);
}
async getData() {
const data = this.response.arrayBuffer
? await this.response.arrayBuffer()
: (await this.response.buffer()).buffer;
return data;
}
}
export class FetchClient extends BaseClient {
constructor(url, credentials) {
super(url);
this.credentials = credentials;
}
async request({ headers, credentials, signal } = {}) {
const response = await fetch(this.url, {
headers, credentials, signal,
});
return new FetchResponse(response);
}
}

View file

@ -0,0 +1,81 @@
import http from 'http';
import https from 'https';
import urlMod from 'url';
import { BaseClient, BaseResponse } from './base.js';
import { AbortError } from '../../utils.js';
class HttpResponse extends BaseResponse {
/**
* BaseResponse facade for node HTTP/HTTPS API Response
* @param {http.ServerResponse} response
*/
constructor(response, dataPromise) {
super();
this.response = response;
this.dataPromise = dataPromise;
}
get status() {
return this.response.statusCode;
}
getHeader(name) {
return this.response.headers[name];
}
async getData() {
const data = await this.dataPromise;
return data;
}
}
export class HttpClient extends BaseClient {
constructor(url) {
super(url);
this.parsedUrl = urlMod.parse(this.url);
this.httpApi = (this.parsedUrl.protocol === 'http:' ? http : https);
}
constructRequest(headers, signal) {
return new Promise((resolve, reject) => {
const request = this.httpApi.get(
{
...this.parsedUrl,
headers,
},
(response) => {
const dataPromise = new Promise((resolveData) => {
const chunks = [];
// collect chunks
response.on('data', (chunk) => {
chunks.push(chunk);
});
// concatenate all chunks and resolve the promise with the resulting buffer
response.on('end', () => {
const data = Buffer.concat(chunks).buffer;
resolveData(data);
});
response.on('error', reject);
});
resolve(new HttpResponse(response, dataPromise));
},
);
request.on('error', reject);
if (signal) {
if (signal.aborted) {
request.destroy(new AbortError('Request aborted'));
}
signal.addEventListener('abort', () => request.destroy(new AbortError('Request aborted')));
}
});
}
async request({ headers, signal } = {}) {
const response = await this.constructRequest(headers, signal);
return response;
}
}

View file

@ -0,0 +1,61 @@
import { BaseClient, BaseResponse } from './base.js';
import { AbortError } from '../../utils.js';
class XHRResponse extends BaseResponse {
/**
* BaseResponse facade for XMLHttpRequest
* @param {XMLHttpRequest} xhr
* @param {ArrayBuffer} data
*/
constructor(xhr, data) {
super();
this.xhr = xhr;
this.data = data;
}
get status() {
return this.xhr.status;
}
getHeader(name) {
return this.xhr.getResponseHeader(name);
}
async getData() {
return this.data;
}
}
export class XHRClient extends BaseClient {
constructRequest(headers, signal) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', this.url);
xhr.responseType = 'arraybuffer';
for (const [key, value] of Object.entries(headers)) {
xhr.setRequestHeader(key, value);
}
// hook signals
xhr.onload = () => {
const data = xhr.response;
resolve(new XHRResponse(xhr, data));
};
xhr.onerror = reject;
xhr.onabort = () => reject(new AbortError('Request aborted'));
xhr.send();
if (signal) {
if (signal.aborted) {
xhr.abort();
}
signal.addEventListener('abort', () => xhr.abort());
}
});
}
async request({ headers, signal } = {}) {
const response = await this.constructRequest(headers, signal);
return response;
}
}

View file

@ -0,0 +1,68 @@
import fs from 'fs';
import { BaseSource } from './basesource.js';
function closeAsync(fd) {
return new Promise((resolve, reject) => {
fs.close(fd, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
function openAsync(path, flags, mode = undefined) {
return new Promise((resolve, reject) => {
fs.open(path, flags, mode, (err, fd) => {
if (err) {
reject(err);
} else {
resolve(fd);
}
});
});
}
function readAsync(...args) {
return new Promise((resolve, reject) => {
fs.read(...args, (err, bytesRead, buffer) => {
if (err) {
reject(err);
} else {
resolve({ bytesRead, buffer });
}
});
});
}
class FileSource extends BaseSource {
constructor(path) {
super();
this.path = path;
this.openRequest = openAsync(path, 'r');
}
async fetchSlice(slice) {
// TODO: use `signal`
const fd = await this.openRequest;
const { buffer } = await readAsync(
fd,
Buffer.alloc(slice.length),
0,
slice.length,
slice.offset,
);
return buffer.buffer;
}
async close() {
const fd = await this.openRequest;
await closeAsync(fd);
}
}
export function makeFileSource(path) {
return new FileSource(path);
}

View file

@ -0,0 +1,32 @@
import { BaseSource } from './basesource.js';
class FileReaderSource extends BaseSource {
constructor(file) {
super();
this.file = file;
}
async fetchSlice(slice, signal) {
return new Promise((resolve, reject) => {
const blob = this.file.slice(slice.offset, slice.offset + slice.length);
const reader = new FileReader();
reader.onload = (event) => resolve(event.target.result);
reader.onerror = reject;
reader.onabort = reject;
reader.readAsArrayBuffer(blob);
if (signal) {
signal.addEventListener('abort', () => reader.abort());
}
});
}
}
/**
* Create a new source from a given file/blob.
* @param {Blob} file The file or blob to read from.
* @returns The constructed source
*/
export function makeFileReaderSource(file) {
return new FileReaderSource(file);
}

View file

@ -0,0 +1,145 @@
const CRLFCRLF = '\r\n\r\n';
/*
* Shim for 'Object.fromEntries'
*/
function itemsToObject(items) {
if (typeof Object.fromEntries !== 'undefined') {
return Object.fromEntries(items);
}
const obj = {};
for (const [key, value] of items) {
obj[key.toLowerCase()] = value;
}
return obj;
}
/**
* Parse HTTP headers from a given string.
* @param {String} text the text to parse the headers from
* @returns {Object} the parsed headers with lowercase keys
*/
function parseHeaders(text) {
const items = text
.split('\r\n')
.map((line) => {
const kv = line.split(':').map((str) => str.trim());
kv[0] = kv[0].toLowerCase();
return kv;
});
return itemsToObject(items);
}
/**
* Parse a 'Content-Type' header value to the content-type and parameters
* @param {String} rawContentType the raw string to parse from
* @returns {Object} the parsed content type with the fields: type and params
*/
export function parseContentType(rawContentType) {
const [type, ...rawParams] = rawContentType.split(';').map((s) => s.trim());
const paramsItems = rawParams.map((param) => param.split('='));
return { type, params: itemsToObject(paramsItems) };
}
/**
* Parse a 'Content-Range' header value to its start, end, and total parts
* @param {String} rawContentRange the raw string to parse from
* @returns {Object} the parsed parts
*/
export function parseContentRange(rawContentRange) {
let start;
let end;
let total;
if (rawContentRange) {
[, start, end, total] = rawContentRange.match(/bytes (\d+)-(\d+)\/(\d+)/);
start = parseInt(start, 10);
end = parseInt(end, 10);
total = parseInt(total, 10);
}
return { start, end, total };
}
/**
* Parses a list of byteranges from the given 'multipart/byteranges' HTTP response.
* Each item in the list has the following properties:
* - headers: the HTTP headers
* - data: the sliced ArrayBuffer for that specific part
* - offset: the offset of the byterange within its originating file
* - length: the length of the byterange
* @param {ArrayBuffer} responseArrayBuffer the response to be parsed and split
* @param {String} boundary the boundary string used to split the sections
* @returns {Object[]} the parsed byteranges
*/
export function parseByteRanges(responseArrayBuffer, boundary) {
let offset = null;
const decoder = new TextDecoder('ascii');
const out = [];
const startBoundary = `--${boundary}`;
const endBoundary = `${startBoundary}--`;
// search for the initial boundary, may be offset by some bytes
// TODO: more efficient to check for `--` in bytes directly
for (let i = 0; i < 10; ++i) {
const text = decoder.decode(
new Uint8Array(responseArrayBuffer, i, startBoundary.length),
);
if (text === startBoundary) {
offset = i;
}
}
if (offset === null) {
throw new Error('Could not find initial boundary');
}
while (offset < responseArrayBuffer.byteLength) {
const text = decoder.decode(
new Uint8Array(responseArrayBuffer, offset,
Math.min(startBoundary.length + 1024, responseArrayBuffer.byteLength - offset),
),
);
// break if we arrived at the end
if (text.length === 0 || text.startsWith(endBoundary)) {
break;
}
// assert that we are actually dealing with a byterange and are at the correct offset
if (!text.startsWith(startBoundary)) {
throw new Error('Part does not start with boundary');
}
// get a substring from where we read the headers
const innerText = text.substr(startBoundary.length + 2);
if (innerText.length === 0) {
break;
}
// find the double linebreak that denotes the end of the headers
const endOfHeaders = innerText.indexOf(CRLFCRLF);
// parse the headers to get the content range size
const headers = parseHeaders(innerText.substr(0, endOfHeaders));
const { start, end, total } = parseContentRange(headers['content-range']);
// calculate the length of the slice and the next offset
const startOfData = offset + startBoundary.length + endOfHeaders + CRLFCRLF.length;
const length = parseInt(end, 10) + 1 - parseInt(start, 10);
out.push({
headers,
data: responseArrayBuffer.slice(startOfData, startOfData + length),
offset: start,
length,
fileSize: total,
});
offset = startOfData + length + 4;
}
return out;
}

View file

@ -0,0 +1,196 @@
import { parseByteRanges, parseContentRange, parseContentType } from './httputils.js';
import { BaseSource } from './basesource.js';
import { BlockedSource } from './blockedsource.js';
import { FetchClient } from './client/fetch.js';
import { XHRClient } from './client/xhr.js';
import { HttpClient } from './client/http.js';
class RemoteSource extends BaseSource {
/**
*
* @param {BaseClient} client
* @param {object} headers
* @param {numbers} maxRanges
* @param {boolean} allowFullFile
*/
constructor(client, headers, maxRanges, allowFullFile) {
super();
this.client = client;
this.headers = headers;
this.maxRanges = maxRanges;
this.allowFullFile = allowFullFile;
this._fileSize = null;
}
/**
*
* @param {Slice[]} slices
*/
async fetch(slices, signal) {
// if we allow multi-ranges, split the incoming request into that many sub-requests
// and join them afterwards
if (this.maxRanges >= slices.length) {
return this.fetchSlices(slices, signal);
} else if (this.maxRanges > 0 && slices.length > 1) {
// TODO: split into multiple multi-range requests
// const subSlicesRequests = [];
// for (let i = 0; i < slices.length; i += this.maxRanges) {
// subSlicesRequests.push(
// this.fetchSlices(slices.slice(i, i + this.maxRanges), signal),
// );
// }
// return (await Promise.all(subSlicesRequests)).flat();
}
// otherwise make a single request for each slice
return Promise.all(
slices.map((slice) => this.fetchSlice(slice, signal)),
);
}
async fetchSlices(slices, signal) {
const response = await this.client.request({
headers: {
...this.headers,
Range: `bytes=${slices
.map(({ offset, length }) => `${offset}-${offset + length}`)
.join(',')
}`,
},
signal,
});
if (!response.ok) {
throw new Error('Error fetching data.');
} else if (response.status === 206) {
const { type, params } = parseContentType(response.getHeader('content-type'));
if (type === 'multipart/byteranges') {
const byteRanges = parseByteRanges(await response.getData(), params.boundary);
this._fileSize = byteRanges[0].fileSize || null;
return byteRanges;
}
const data = await response.getData();
const { start, end, total } = parseContentRange(response.getHeader('content-range'));
this._fileSize = total || null;
const first = [{
data,
offset: start,
length: end - start,
}];
if (slices.length > 1) {
// we requested more than one slice, but got only the first
// unfortunately, some HTTP Servers don't support multi-ranges
// and return only the first
// get the rest of the slices and fetch them iteratively
const others = await Promise.all(slices.slice(1).map((slice) => this.fetchSlice(slice, signal)));
return first.concat(others);
}
return first;
} else {
if (!this.allowFullFile) {
throw new Error('Server responded with full file');
}
const data = await response.getData();
this._fileSize = data.byteLength;
return [{
data,
offset: 0,
length: data.byteLength,
}];
}
}
async fetchSlice(slice, signal) {
const { offset, length } = slice;
const response = await this.client.request({
headers: {
...this.headers,
Range: `bytes=${offset}-${offset + length}`,
},
signal,
});
// check the response was okay and if the server actually understands range requests
if (!response.ok) {
throw new Error('Error fetching data.');
} else if (response.status === 206) {
const data = await response.getData();
const { total } = parseContentRange(response.getHeader('content-range'));
this._fileSize = total || null;
return {
data,
offset,
length,
};
} else {
if (!this.allowFullFile) {
throw new Error('Server responded with full file');
}
const data = await response.getData();
this._fileSize = data.byteLength;
return {
data,
offset: 0,
length: data.byteLength,
};
}
}
get fileSize() {
return this._fileSize;
}
}
function maybeWrapInBlockedSource(source, { blockSize, cacheSize }) {
if (blockSize === null) {
return source;
}
return new BlockedSource(source, { blockSize, cacheSize });
}
export function makeFetchSource(url, { headers = {}, credentials, maxRanges = 0, allowFullFile = false, ...blockOptions } = {}) {
const client = new FetchClient(url, credentials);
const source = new RemoteSource(client, headers, maxRanges, allowFullFile);
return maybeWrapInBlockedSource(source, blockOptions);
}
export function makeXHRSource(url, { headers = {}, maxRanges = 0, allowFullFile = false, ...blockOptions } = {}) {
const client = new XHRClient(url);
const source = new RemoteSource(client, headers, maxRanges, allowFullFile);
return maybeWrapInBlockedSource(source, blockOptions);
}
export function makeHttpSource(url, { headers = {}, maxRanges = 0, allowFullFile = false, ...blockOptions } = {}) {
const client = new HttpClient(url);
const source = new RemoteSource(client, headers, maxRanges, allowFullFile);
return maybeWrapInBlockedSource(source, blockOptions);
}
export function makeCustomSource(client, { headers = {}, maxRanges = 0, allowFullFile = false, ...blockOptions } = {}) {
const source = new RemoteSource(client, headers, maxRanges, allowFullFile);
return maybeWrapInBlockedSource(source, blockOptions);
}
/**
*
* @param {string} url
* @param {object} options
*/
export function makeRemoteSource(url, { forceXHR = false, ...clientOptions } = {}) {
if (typeof fetch === 'function' && !forceXHR) {
return makeFetchSource(url, clientOptions);
}
if (typeof XMLHttpRequest !== 'undefined') {
return makeXHRSource(url, clientOptions);
}
return makeHttpSource(url, clientOptions);
}

View file

@ -0,0 +1,158 @@
export function assign(target, source) {
for (const key in source) {
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
}
export function chunk(iterable, length) {
const results = [];
const lengthOfIterable = iterable.length;
for (let i = 0; i < lengthOfIterable; i += length) {
const chunked = [];
for (let ci = i; ci < i + length; ci++) {
chunked.push(iterable[ci]);
}
results.push(chunked);
}
return results;
}
export function endsWith(string, expectedEnding) {
if (string.length < expectedEnding.length) {
return false;
}
const actualEnding = string.substr(string.length - expectedEnding.length);
return actualEnding === expectedEnding;
}
export function forEach(iterable, func) {
const { length } = iterable;
for (let i = 0; i < length; i++) {
func(iterable[i], i);
}
}
export function invert(oldObj) {
const newObj = {};
for (const key in oldObj) {
if (oldObj.hasOwnProperty(key)) {
const value = oldObj[key];
newObj[value] = key;
}
}
return newObj;
}
export function range(n) {
const results = [];
for (let i = 0; i < n; i++) {
results.push(i);
}
return results;
}
export function times(numTimes, func) {
const results = [];
for (let i = 0; i < numTimes; i++) {
results.push(func(i));
}
return results;
}
export function toArray(iterable) {
const results = [];
const { length } = iterable;
for (let i = 0; i < length; i++) {
results.push(iterable[i]);
}
return results;
}
export function toArrayRecursively(input) {
if (input.length) {
return toArray(input).map(toArrayRecursively);
}
return input;
}
// copied from https://github.com/academia-de-codigo/parse-content-range-header/blob/master/index.js
export function parseContentRange(headerValue) {
if (!headerValue) {
return null;
}
if (typeof headerValue !== 'string') {
throw new Error('invalid argument');
}
const parseInt = (number) => Number.parseInt(number, 10);
// Check for presence of unit
let matches = headerValue.match(/^(\w*) /);
const unit = matches && matches[1];
// check for start-end/size header format
matches = headerValue.match(/(\d+)-(\d+)\/(\d+|\*)/);
if (matches) {
return {
unit,
first: parseInt(matches[1]),
last: parseInt(matches[2]),
length: matches[3] === '*' ? null : parseInt(matches[3]),
};
}
// check for size header format
matches = headerValue.match(/(\d+|\*)/);
if (matches) {
return {
unit,
first: null,
last: null,
length: matches[1] === '*' ? null : parseInt(matches[1]),
};
}
return null;
}
/*
* Promisified wrapper around 'setTimeout' to allow 'await'
*/
export async function wait(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
export function zip(a, b) {
const A = Array.isArray(a) ? a : Array.from(a);
const B = Array.isArray(b) ? b : Array.from(b);
return A.map((k, i) => [k, B[i]]);
}
// Based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
export class AbortError extends Error {
constructor(params) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(params);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, AbortError);
}
this.name = 'AbortError';
}
}
export class CustomAggregateError extends Error {
constructor(errors, message) {
super(message);
this.errors = errors;
this.message = message;
this.name = 'AggregateError';
}
}
export const AggregateError = CustomAggregateError;

View file

@ -0,0 +1,14 @@
/* global globalThis */
/* eslint-disable import/no-mutable-exports */
import { getDecoder } from '../compression/index.js';
const worker = globalThis;
worker.addEventListener('message', async (e) => {
const { id, fileDirectory, buffer } = e.data;
const decoder = await getDecoder(fileDirectory);
const decoded = await decoder.decode(fileDirectory, buffer);
worker.postMessage({ decoded, id }, [decoded]);
});
export let create;