geotiffimage.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910
  1. /** @module geotiffimage */
  2. import { getFloat16 } from '@petamoriken/float16';
  3. import getAttribute from 'xml-utils/get-attribute.js';
  4. import findTagsByName from 'xml-utils/find-tags-by-name.js';
  5. import { photometricInterpretations, ExtraSamplesValues } from './globals.js';
  6. import { fromWhiteIsZero, fromBlackIsZero, fromPalette, fromCMYK, fromYCbCr, fromCIELab } from './rgb.js';
  7. import { getDecoder } from './compression/index.js';
  8. import { resample, resampleInterleaved } from './resample.js';
  9. /**
  10. * @typedef {Object} ReadRasterOptions
  11. * @property {Array<number>} [window=whole window] the subset to read data from in pixels.
  12. * @property {Array<number>} [bbox=whole image] the subset to read data from in
  13. * geographical coordinates.
  14. * @property {Array<number>} [samples=all samples] the selection of samples to read from. Default is all samples.
  15. * @property {boolean} [interleave=false] whether the data shall be read
  16. * in one single array or separate
  17. * arrays.
  18. * @property {Pool} [pool=null] The optional decoder pool to use.
  19. * @property {number} [width] The desired width of the output. When the width is not the
  20. * same as the images, resampling will be performed.
  21. * @property {number} [height] The desired height of the output. When the width is not the
  22. * same as the images, resampling will be performed.
  23. * @property {string} [resampleMethod='nearest'] The desired resampling method.
  24. * @property {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
  25. * to be aborted
  26. * @property {number|number[]} [fillValue] The value to use for parts of the image
  27. * outside of the images extent. When multiple
  28. * samples are requested, an array of fill values
  29. * can be passed.
  30. */
  31. /** @typedef {import("./geotiff.js").TypedArray} TypedArray */
  32. /** @typedef {import("./geotiff.js").ReadRasterResult} ReadRasterResult */
  33. function sum(array, start, end) {
  34. let s = 0;
  35. for (let i = start; i < end; ++i) {
  36. s += array[i];
  37. }
  38. return s;
  39. }
  40. function arrayForType(format, bitsPerSample, size) {
  41. switch (format) {
  42. case 1: // unsigned integer data
  43. if (bitsPerSample <= 8) {
  44. return new Uint8Array(size);
  45. } else if (bitsPerSample <= 16) {
  46. return new Uint16Array(size);
  47. } else if (bitsPerSample <= 32) {
  48. return new Uint32Array(size);
  49. }
  50. break;
  51. case 2: // twos complement signed integer data
  52. if (bitsPerSample === 8) {
  53. return new Int8Array(size);
  54. } else if (bitsPerSample === 16) {
  55. return new Int16Array(size);
  56. } else if (bitsPerSample === 32) {
  57. return new Int32Array(size);
  58. }
  59. break;
  60. case 3: // floating point data
  61. switch (bitsPerSample) {
  62. case 16:
  63. case 32:
  64. return new Float32Array(size);
  65. case 64:
  66. return new Float64Array(size);
  67. default:
  68. break;
  69. }
  70. break;
  71. default:
  72. break;
  73. }
  74. throw Error('Unsupported data format/bitsPerSample');
  75. }
  76. function needsNormalization(format, bitsPerSample) {
  77. if ((format === 1 || format === 2) && bitsPerSample <= 32 && bitsPerSample % 8 === 0) {
  78. return false;
  79. } else if (format === 3 && (bitsPerSample === 16 || bitsPerSample === 32 || bitsPerSample === 64)) {
  80. return false;
  81. }
  82. return true;
  83. }
  84. function normalizeArray(inBuffer, format, planarConfiguration, samplesPerPixel, bitsPerSample, tileWidth, tileHeight) {
  85. // const inByteArray = new Uint8Array(inBuffer);
  86. const view = new DataView(inBuffer);
  87. const outSize = planarConfiguration === 2
  88. ? tileHeight * tileWidth
  89. : tileHeight * tileWidth * samplesPerPixel;
  90. const samplesToTransfer = planarConfiguration === 2
  91. ? 1 : samplesPerPixel;
  92. const outArray = arrayForType(format, bitsPerSample, outSize);
  93. // let pixel = 0;
  94. const bitMask = parseInt('1'.repeat(bitsPerSample), 2);
  95. if (format === 1) { // unsigned integer
  96. // translation of https://github.com/OSGeo/gdal/blob/master/gdal/frmts/gtiff/geotiff.cpp#L7337
  97. let pixelBitSkip;
  98. // let sampleBitOffset = 0;
  99. if (planarConfiguration === 1) {
  100. pixelBitSkip = samplesPerPixel * bitsPerSample;
  101. // sampleBitOffset = (samplesPerPixel - 1) * bitsPerSample;
  102. } else {
  103. pixelBitSkip = bitsPerSample;
  104. }
  105. // Bits per line rounds up to next byte boundary.
  106. let bitsPerLine = tileWidth * pixelBitSkip;
  107. if ((bitsPerLine & 7) !== 0) {
  108. bitsPerLine = (bitsPerLine + 7) & (~7);
  109. }
  110. for (let y = 0; y < tileHeight; ++y) {
  111. const lineBitOffset = y * bitsPerLine;
  112. for (let x = 0; x < tileWidth; ++x) {
  113. const pixelBitOffset = lineBitOffset + (x * samplesToTransfer * bitsPerSample);
  114. for (let i = 0; i < samplesToTransfer; ++i) {
  115. const bitOffset = pixelBitOffset + (i * bitsPerSample);
  116. const outIndex = (((y * tileWidth) + x) * samplesToTransfer) + i;
  117. const byteOffset = Math.floor(bitOffset / 8);
  118. const innerBitOffset = bitOffset % 8;
  119. if (innerBitOffset + bitsPerSample <= 8) {
  120. outArray[outIndex] = (view.getUint8(byteOffset) >> (8 - bitsPerSample) - innerBitOffset) & bitMask;
  121. } else if (innerBitOffset + bitsPerSample <= 16) {
  122. outArray[outIndex] = (view.getUint16(byteOffset) >> (16 - bitsPerSample) - innerBitOffset) & bitMask;
  123. } else if (innerBitOffset + bitsPerSample <= 24) {
  124. const raw = (view.getUint16(byteOffset) << 8) | (view.getUint8(byteOffset + 2));
  125. outArray[outIndex] = (raw >> (24 - bitsPerSample) - innerBitOffset) & bitMask;
  126. } else {
  127. outArray[outIndex] = (view.getUint32(byteOffset) >> (32 - bitsPerSample) - innerBitOffset) & bitMask;
  128. }
  129. // let outWord = 0;
  130. // for (let bit = 0; bit < bitsPerSample; ++bit) {
  131. // if (inByteArray[bitOffset >> 3]
  132. // & (0x80 >> (bitOffset & 7))) {
  133. // outWord |= (1 << (bitsPerSample - 1 - bit));
  134. // }
  135. // ++bitOffset;
  136. // }
  137. // outArray[outIndex] = outWord;
  138. // outArray[pixel] = outWord;
  139. // pixel += 1;
  140. }
  141. // bitOffset = bitOffset + pixelBitSkip - bitsPerSample;
  142. }
  143. }
  144. } else if (format === 3) { // floating point
  145. // Float16 is handled elsewhere
  146. // normalize 16/24 bit floats to 32 bit floats in the array
  147. // console.time();
  148. // if (bitsPerSample === 16) {
  149. // for (let byte = 0, outIndex = 0; byte < inBuffer.byteLength; byte += 2, ++outIndex) {
  150. // outArray[outIndex] = getFloat16(view, byte);
  151. // }
  152. // }
  153. // console.timeEnd()
  154. }
  155. return outArray.buffer;
  156. }
  157. /**
  158. * GeoTIFF sub-file image.
  159. */
  160. class GeoTIFFImage {
  161. /**
  162. * @constructor
  163. * @param {Object} fileDirectory The parsed file directory
  164. * @param {Object} geoKeys The parsed geo-keys
  165. * @param {DataView} dataView The DataView for the underlying file.
  166. * @param {Boolean} littleEndian Whether the file is encoded in little or big endian
  167. * @param {Boolean} cache Whether or not decoded tiles shall be cached
  168. * @param {import('./source/basesource').BaseSource} source The datasource to read from
  169. */
  170. constructor(fileDirectory, geoKeys, dataView, littleEndian, cache, source) {
  171. this.fileDirectory = fileDirectory;
  172. this.geoKeys = geoKeys;
  173. this.dataView = dataView;
  174. this.littleEndian = littleEndian;
  175. this.tiles = cache ? {} : null;
  176. this.isTiled = !fileDirectory.StripOffsets;
  177. const planarConfiguration = fileDirectory.PlanarConfiguration;
  178. this.planarConfiguration = (typeof planarConfiguration === 'undefined') ? 1 : planarConfiguration;
  179. if (this.planarConfiguration !== 1 && this.planarConfiguration !== 2) {
  180. throw new Error('Invalid planar configuration.');
  181. }
  182. this.source = source;
  183. }
  184. /**
  185. * Returns the associated parsed file directory.
  186. * @returns {Object} the parsed file directory
  187. */
  188. getFileDirectory() {
  189. return this.fileDirectory;
  190. }
  191. /**
  192. * Returns the associated parsed geo keys.
  193. * @returns {Object} the parsed geo keys
  194. */
  195. getGeoKeys() {
  196. return this.geoKeys;
  197. }
  198. /**
  199. * Returns the width of the image.
  200. * @returns {Number} the width of the image
  201. */
  202. getWidth() {
  203. return this.fileDirectory.ImageWidth;
  204. }
  205. /**
  206. * Returns the height of the image.
  207. * @returns {Number} the height of the image
  208. */
  209. getHeight() {
  210. return this.fileDirectory.ImageLength;
  211. }
  212. /**
  213. * Returns the number of samples per pixel.
  214. * @returns {Number} the number of samples per pixel
  215. */
  216. getSamplesPerPixel() {
  217. return typeof this.fileDirectory.SamplesPerPixel !== 'undefined'
  218. ? this.fileDirectory.SamplesPerPixel : 1;
  219. }
  220. /**
  221. * Returns the width of each tile.
  222. * @returns {Number} the width of each tile
  223. */
  224. getTileWidth() {
  225. return this.isTiled ? this.fileDirectory.TileWidth : this.getWidth();
  226. }
  227. /**
  228. * Returns the height of each tile.
  229. * @returns {Number} the height of each tile
  230. */
  231. getTileHeight() {
  232. if (this.isTiled) {
  233. return this.fileDirectory.TileLength;
  234. }
  235. if (typeof this.fileDirectory.RowsPerStrip !== 'undefined') {
  236. return Math.min(this.fileDirectory.RowsPerStrip, this.getHeight());
  237. }
  238. return this.getHeight();
  239. }
  240. getBlockWidth() {
  241. return this.getTileWidth();
  242. }
  243. getBlockHeight(y) {
  244. if (this.isTiled || (y + 1) * this.getTileHeight() <= this.getHeight()) {
  245. return this.getTileHeight();
  246. } else {
  247. return this.getHeight() - (y * this.getTileHeight());
  248. }
  249. }
  250. /**
  251. * Calculates the number of bytes for each pixel across all samples. Only full
  252. * bytes are supported, an exception is thrown when this is not the case.
  253. * @returns {Number} the bytes per pixel
  254. */
  255. getBytesPerPixel() {
  256. let bytes = 0;
  257. for (let i = 0; i < this.fileDirectory.BitsPerSample.length; ++i) {
  258. bytes += this.getSampleByteSize(i);
  259. }
  260. return bytes;
  261. }
  262. getSampleByteSize(i) {
  263. if (i >= this.fileDirectory.BitsPerSample.length) {
  264. throw new RangeError(`Sample index ${i} is out of range.`);
  265. }
  266. return Math.ceil(this.fileDirectory.BitsPerSample[i] / 8);
  267. }
  268. getReaderForSample(sampleIndex) {
  269. const format = this.fileDirectory.SampleFormat
  270. ? this.fileDirectory.SampleFormat[sampleIndex] : 1;
  271. const bitsPerSample = this.fileDirectory.BitsPerSample[sampleIndex];
  272. switch (format) {
  273. case 1: // unsigned integer data
  274. if (bitsPerSample <= 8) {
  275. return DataView.prototype.getUint8;
  276. } else if (bitsPerSample <= 16) {
  277. return DataView.prototype.getUint16;
  278. } else if (bitsPerSample <= 32) {
  279. return DataView.prototype.getUint32;
  280. }
  281. break;
  282. case 2: // twos complement signed integer data
  283. if (bitsPerSample <= 8) {
  284. return DataView.prototype.getInt8;
  285. } else if (bitsPerSample <= 16) {
  286. return DataView.prototype.getInt16;
  287. } else if (bitsPerSample <= 32) {
  288. return DataView.prototype.getInt32;
  289. }
  290. break;
  291. case 3:
  292. switch (bitsPerSample) {
  293. case 16:
  294. return function (offset, littleEndian) {
  295. return getFloat16(this, offset, littleEndian);
  296. };
  297. case 32:
  298. return DataView.prototype.getFloat32;
  299. case 64:
  300. return DataView.prototype.getFloat64;
  301. default:
  302. break;
  303. }
  304. break;
  305. default:
  306. break;
  307. }
  308. throw Error('Unsupported data format/bitsPerSample');
  309. }
  310. getSampleFormat(sampleIndex = 0) {
  311. return this.fileDirectory.SampleFormat
  312. ? this.fileDirectory.SampleFormat[sampleIndex] : 1;
  313. }
  314. getBitsPerSample(sampleIndex = 0) {
  315. return this.fileDirectory.BitsPerSample[sampleIndex];
  316. }
  317. getArrayForSample(sampleIndex, size) {
  318. const format = this.getSampleFormat(sampleIndex);
  319. const bitsPerSample = this.getBitsPerSample(sampleIndex);
  320. return arrayForType(format, bitsPerSample, size);
  321. }
  322. /**
  323. * Returns the decoded strip or tile.
  324. * @param {Number} x the strip or tile x-offset
  325. * @param {Number} y the tile y-offset (0 for stripped images)
  326. * @param {Number} sample the sample to get for separated samples
  327. * @param {import("./geotiff").Pool|import("./geotiff").BaseDecoder} poolOrDecoder the decoder or decoder pool
  328. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
  329. * to be aborted
  330. * @returns {Promise.<ArrayBuffer>}
  331. */
  332. async getTileOrStrip(x, y, sample, poolOrDecoder, signal) {
  333. const numTilesPerRow = Math.ceil(this.getWidth() / this.getTileWidth());
  334. const numTilesPerCol = Math.ceil(this.getHeight() / this.getTileHeight());
  335. let index;
  336. const { tiles } = this;
  337. if (this.planarConfiguration === 1) {
  338. index = (y * numTilesPerRow) + x;
  339. } else if (this.planarConfiguration === 2) {
  340. index = (sample * numTilesPerRow * numTilesPerCol) + (y * numTilesPerRow) + x;
  341. }
  342. let offset;
  343. let byteCount;
  344. if (this.isTiled) {
  345. offset = this.fileDirectory.TileOffsets[index];
  346. byteCount = this.fileDirectory.TileByteCounts[index];
  347. } else {
  348. offset = this.fileDirectory.StripOffsets[index];
  349. byteCount = this.fileDirectory.StripByteCounts[index];
  350. }
  351. const slice = (await this.source.fetch([{ offset, length: byteCount }], signal))[0];
  352. let request;
  353. if (tiles === null || !tiles[index]) {
  354. // resolve each request by potentially applying array normalization
  355. request = (async () => {
  356. let data = await poolOrDecoder.decode(this.fileDirectory, slice);
  357. const sampleFormat = this.getSampleFormat();
  358. const bitsPerSample = this.getBitsPerSample();
  359. if (needsNormalization(sampleFormat, bitsPerSample)) {
  360. data = normalizeArray(
  361. data,
  362. sampleFormat,
  363. this.planarConfiguration,
  364. this.getSamplesPerPixel(),
  365. bitsPerSample,
  366. this.getTileWidth(),
  367. this.getBlockHeight(y),
  368. );
  369. }
  370. return data;
  371. })();
  372. // set the cache
  373. if (tiles !== null) {
  374. tiles[index] = request;
  375. }
  376. } else {
  377. // get from the cache
  378. request = tiles[index];
  379. }
  380. // cache the tile request
  381. return { x, y, sample, data: await request };
  382. }
  383. /**
  384. * Internal read function.
  385. * @private
  386. * @param {Array} imageWindow The image window in pixel coordinates
  387. * @param {Array} samples The selected samples (0-based indices)
  388. * @param {TypedArray|TypedArray[]} valueArrays The array(s) to write into
  389. * @param {Boolean} interleave Whether or not to write in an interleaved manner
  390. * @param {import("./geotiff").Pool|AbstractDecoder} poolOrDecoder the decoder or decoder pool
  391. * @param {number} width the width of window to be read into
  392. * @param {number} height the height of window to be read into
  393. * @param {number} resampleMethod the resampling method to be used when interpolating
  394. * @param {AbortSignal} [signal] An AbortSignal that may be signalled if the request is
  395. * to be aborted
  396. * @returns {Promise<ReadRasterResult>}
  397. */
  398. async _readRaster(imageWindow, samples, valueArrays, interleave, poolOrDecoder, width,
  399. height, resampleMethod, signal) {
  400. const tileWidth = this.getTileWidth();
  401. const tileHeight = this.getTileHeight();
  402. const imageWidth = this.getWidth();
  403. const imageHeight = this.getHeight();
  404. const minXTile = Math.max(Math.floor(imageWindow[0] / tileWidth), 0);
  405. const maxXTile = Math.min(
  406. Math.ceil(imageWindow[2] / tileWidth),
  407. Math.ceil(imageWidth / tileWidth),
  408. );
  409. const minYTile = Math.max(Math.floor(imageWindow[1] / tileHeight), 0);
  410. const maxYTile = Math.min(
  411. Math.ceil(imageWindow[3] / tileHeight),
  412. Math.ceil(imageHeight / tileHeight),
  413. );
  414. const windowWidth = imageWindow[2] - imageWindow[0];
  415. let bytesPerPixel = this.getBytesPerPixel();
  416. const srcSampleOffsets = [];
  417. const sampleReaders = [];
  418. for (let i = 0; i < samples.length; ++i) {
  419. if (this.planarConfiguration === 1) {
  420. srcSampleOffsets.push(sum(this.fileDirectory.BitsPerSample, 0, samples[i]) / 8);
  421. } else {
  422. srcSampleOffsets.push(0);
  423. }
  424. sampleReaders.push(this.getReaderForSample(samples[i]));
  425. }
  426. const promises = [];
  427. const { littleEndian } = this;
  428. for (let yTile = minYTile; yTile < maxYTile; ++yTile) {
  429. for (let xTile = minXTile; xTile < maxXTile; ++xTile) {
  430. for (let sampleIndex = 0; sampleIndex < samples.length; ++sampleIndex) {
  431. const si = sampleIndex;
  432. const sample = samples[sampleIndex];
  433. if (this.planarConfiguration === 2) {
  434. bytesPerPixel = this.getSampleByteSize(sampleIndex);
  435. }
  436. const promise = this.getTileOrStrip(xTile, yTile, sample, poolOrDecoder, signal).then((tile) => {
  437. const buffer = tile.data;
  438. const dataView = new DataView(buffer);
  439. const blockHeight = this.getBlockHeight(tile.y);
  440. const firstLine = tile.y * tileHeight;
  441. const firstCol = tile.x * tileWidth;
  442. const lastLine = firstLine + blockHeight;
  443. const lastCol = (tile.x + 1) * tileWidth;
  444. const reader = sampleReaders[si];
  445. const ymax = Math.min(blockHeight, blockHeight - (lastLine - imageWindow[3]), imageHeight - firstLine);
  446. const xmax = Math.min(tileWidth, tileWidth - (lastCol - imageWindow[2]), imageWidth - firstCol);
  447. for (let y = Math.max(0, imageWindow[1] - firstLine); y < ymax; ++y) {
  448. for (let x = Math.max(0, imageWindow[0] - firstCol); x < xmax; ++x) {
  449. const pixelOffset = ((y * tileWidth) + x) * bytesPerPixel;
  450. const value = reader.call(
  451. dataView, pixelOffset + srcSampleOffsets[si], littleEndian,
  452. );
  453. let windowCoordinate;
  454. if (interleave) {
  455. windowCoordinate = ((y + firstLine - imageWindow[1]) * windowWidth * samples.length)
  456. + ((x + firstCol - imageWindow[0]) * samples.length)
  457. + si;
  458. valueArrays[windowCoordinate] = value;
  459. } else {
  460. windowCoordinate = (
  461. (y + firstLine - imageWindow[1]) * windowWidth
  462. ) + x + firstCol - imageWindow[0];
  463. valueArrays[si][windowCoordinate] = value;
  464. }
  465. }
  466. }
  467. });
  468. promises.push(promise);
  469. }
  470. }
  471. }
  472. await Promise.all(promises);
  473. if ((width && (imageWindow[2] - imageWindow[0]) !== width)
  474. || (height && (imageWindow[3] - imageWindow[1]) !== height)) {
  475. let resampled;
  476. if (interleave) {
  477. resampled = resampleInterleaved(
  478. valueArrays,
  479. imageWindow[2] - imageWindow[0],
  480. imageWindow[3] - imageWindow[1],
  481. width, height,
  482. samples.length,
  483. resampleMethod,
  484. );
  485. } else {
  486. resampled = resample(
  487. valueArrays,
  488. imageWindow[2] - imageWindow[0],
  489. imageWindow[3] - imageWindow[1],
  490. width, height,
  491. resampleMethod,
  492. );
  493. }
  494. resampled.width = width;
  495. resampled.height = height;
  496. return resampled;
  497. }
  498. valueArrays.width = width || imageWindow[2] - imageWindow[0];
  499. valueArrays.height = height || imageWindow[3] - imageWindow[1];
  500. return valueArrays;
  501. }
  502. /**
  503. * Reads raster data from the image. This function reads all selected samples
  504. * into separate arrays of the correct type for that sample or into a single
  505. * combined array when `interleave` is set. When provided, only a subset
  506. * of the raster is read for each sample.
  507. *
  508. * @param {ReadRasterOptions} [options={}] optional parameters
  509. * @returns {Promise<ReadRasterResult>} the decoded arrays as a promise
  510. */
  511. async readRasters({
  512. window: wnd, samples = [], interleave, pool = null,
  513. width, height, resampleMethod, fillValue, signal,
  514. } = {}) {
  515. const imageWindow = wnd || [0, 0, this.getWidth(), this.getHeight()];
  516. // check parameters
  517. if (imageWindow[0] > imageWindow[2] || imageWindow[1] > imageWindow[3]) {
  518. throw new Error('Invalid subsets');
  519. }
  520. const imageWindowWidth = imageWindow[2] - imageWindow[0];
  521. const imageWindowHeight = imageWindow[3] - imageWindow[1];
  522. const numPixels = imageWindowWidth * imageWindowHeight;
  523. const samplesPerPixel = this.getSamplesPerPixel();
  524. if (!samples || !samples.length) {
  525. for (let i = 0; i < samplesPerPixel; ++i) {
  526. samples.push(i);
  527. }
  528. } else {
  529. for (let i = 0; i < samples.length; ++i) {
  530. if (samples[i] >= samplesPerPixel) {
  531. return Promise.reject(new RangeError(`Invalid sample index '${samples[i]}'.`));
  532. }
  533. }
  534. }
  535. let valueArrays;
  536. if (interleave) {
  537. const format = this.fileDirectory.SampleFormat
  538. ? Math.max.apply(null, this.fileDirectory.SampleFormat) : 1;
  539. const bitsPerSample = Math.max.apply(null, this.fileDirectory.BitsPerSample);
  540. valueArrays = arrayForType(format, bitsPerSample, numPixels * samples.length);
  541. if (fillValue) {
  542. valueArrays.fill(fillValue);
  543. }
  544. } else {
  545. valueArrays = [];
  546. for (let i = 0; i < samples.length; ++i) {
  547. const valueArray = this.getArrayForSample(samples[i], numPixels);
  548. if (Array.isArray(fillValue) && i < fillValue.length) {
  549. valueArray.fill(fillValue[i]);
  550. } else if (fillValue && !Array.isArray(fillValue)) {
  551. valueArray.fill(fillValue);
  552. }
  553. valueArrays.push(valueArray);
  554. }
  555. }
  556. const poolOrDecoder = pool || await getDecoder(this.fileDirectory);
  557. const result = await this._readRaster(
  558. imageWindow, samples, valueArrays, interleave, poolOrDecoder, width, height, resampleMethod, signal,
  559. );
  560. return result;
  561. }
  562. /**
  563. * Reads raster data from the image as RGB. The result is always an
  564. * interleaved typed array.
  565. * Colorspaces other than RGB will be transformed to RGB, color maps expanded.
  566. * When no other method is applicable, the first sample is used to produce a
  567. * grayscale image.
  568. * When provided, only a subset of the raster is read for each sample.
  569. *
  570. * @param {Object} [options] optional parameters
  571. * @param {Array<number>} [options.window] the subset to read data from in pixels.
  572. * @param {boolean} [options.interleave=true] whether the data shall be read
  573. * in one single array or separate
  574. * arrays.
  575. * @param {import("./geotiff").Pool} [options.pool=null] The optional decoder pool to use.
  576. * @param {number} [options.width] The desired width of the output. When the width is no the
  577. * same as the images, resampling will be performed.
  578. * @param {number} [options.height] The desired height of the output. When the width is no the
  579. * same as the images, resampling will be performed.
  580. * @param {string} [options.resampleMethod='nearest'] The desired resampling method.
  581. * @param {boolean} [options.enableAlpha=false] Enable reading alpha channel if present.
  582. * @param {AbortSignal} [options.signal] An AbortSignal that may be signalled if the request is
  583. * to be aborted
  584. * @returns {Promise<ReadRasterResult>} the RGB array as a Promise
  585. */
  586. async readRGB({ window, interleave = true, pool = null, width, height,
  587. resampleMethod, enableAlpha = false, signal } = {}) {
  588. const imageWindow = window || [0, 0, this.getWidth(), this.getHeight()];
  589. // check parameters
  590. if (imageWindow[0] > imageWindow[2] || imageWindow[1] > imageWindow[3]) {
  591. throw new Error('Invalid subsets');
  592. }
  593. const pi = this.fileDirectory.PhotometricInterpretation;
  594. if (pi === photometricInterpretations.RGB) {
  595. let s = [0, 1, 2];
  596. if ((!(this.fileDirectory.ExtraSamples === ExtraSamplesValues.Unspecified)) && enableAlpha) {
  597. s = [];
  598. for (let i = 0; i < this.fileDirectory.BitsPerSample.length; i += 1) {
  599. s.push(i);
  600. }
  601. }
  602. return this.readRasters({
  603. window,
  604. interleave,
  605. samples: s,
  606. pool,
  607. width,
  608. height,
  609. resampleMethod,
  610. signal,
  611. });
  612. }
  613. let samples;
  614. switch (pi) {
  615. case photometricInterpretations.WhiteIsZero:
  616. case photometricInterpretations.BlackIsZero:
  617. case photometricInterpretations.Palette:
  618. samples = [0];
  619. break;
  620. case photometricInterpretations.CMYK:
  621. samples = [0, 1, 2, 3];
  622. break;
  623. case photometricInterpretations.YCbCr:
  624. case photometricInterpretations.CIELab:
  625. samples = [0, 1, 2];
  626. break;
  627. default:
  628. throw new Error('Invalid or unsupported photometric interpretation.');
  629. }
  630. const subOptions = {
  631. window: imageWindow,
  632. interleave: true,
  633. samples,
  634. pool,
  635. width,
  636. height,
  637. resampleMethod,
  638. signal,
  639. };
  640. const { fileDirectory } = this;
  641. const raster = await this.readRasters(subOptions);
  642. const max = 2 ** this.fileDirectory.BitsPerSample[0];
  643. let data;
  644. switch (pi) {
  645. case photometricInterpretations.WhiteIsZero:
  646. data = fromWhiteIsZero(raster, max);
  647. break;
  648. case photometricInterpretations.BlackIsZero:
  649. data = fromBlackIsZero(raster, max);
  650. break;
  651. case photometricInterpretations.Palette:
  652. data = fromPalette(raster, fileDirectory.ColorMap);
  653. break;
  654. case photometricInterpretations.CMYK:
  655. data = fromCMYK(raster);
  656. break;
  657. case photometricInterpretations.YCbCr:
  658. data = fromYCbCr(raster);
  659. break;
  660. case photometricInterpretations.CIELab:
  661. data = fromCIELab(raster);
  662. break;
  663. default:
  664. throw new Error('Unsupported photometric interpretation.');
  665. }
  666. // if non-interleaved data is requested, we must split the channels
  667. // into their respective arrays
  668. if (!interleave) {
  669. const red = new Uint8Array(data.length / 3);
  670. const green = new Uint8Array(data.length / 3);
  671. const blue = new Uint8Array(data.length / 3);
  672. for (let i = 0, j = 0; i < data.length; i += 3, ++j) {
  673. red[j] = data[i];
  674. green[j] = data[i + 1];
  675. blue[j] = data[i + 2];
  676. }
  677. data = [red, green, blue];
  678. }
  679. data.width = raster.width;
  680. data.height = raster.height;
  681. return data;
  682. }
  683. /**
  684. * Returns an array of tiepoints.
  685. * @returns {Object[]}
  686. */
  687. getTiePoints() {
  688. if (!this.fileDirectory.ModelTiepoint) {
  689. return [];
  690. }
  691. const tiePoints = [];
  692. for (let i = 0; i < this.fileDirectory.ModelTiepoint.length; i += 6) {
  693. tiePoints.push({
  694. i: this.fileDirectory.ModelTiepoint[i],
  695. j: this.fileDirectory.ModelTiepoint[i + 1],
  696. k: this.fileDirectory.ModelTiepoint[i + 2],
  697. x: this.fileDirectory.ModelTiepoint[i + 3],
  698. y: this.fileDirectory.ModelTiepoint[i + 4],
  699. z: this.fileDirectory.ModelTiepoint[i + 5],
  700. });
  701. }
  702. return tiePoints;
  703. }
  704. /**
  705. * Returns the parsed GDAL metadata items.
  706. *
  707. * If sample is passed to null, dataset-level metadata will be returned.
  708. * Otherwise only metadata specific to the provided sample will be returned.
  709. *
  710. * @param {number} [sample=null] The sample index.
  711. * @returns {Object}
  712. */
  713. getGDALMetadata(sample = null) {
  714. const metadata = {};
  715. if (!this.fileDirectory.GDAL_METADATA) {
  716. return null;
  717. }
  718. const string = this.fileDirectory.GDAL_METADATA;
  719. let items = findTagsByName(string, 'Item');
  720. if (sample === null) {
  721. items = items.filter((item) => getAttribute(item, 'sample') === undefined);
  722. } else {
  723. items = items.filter((item) => Number(getAttribute(item, 'sample')) === sample);
  724. }
  725. for (let i = 0; i < items.length; ++i) {
  726. const item = items[i];
  727. metadata[getAttribute(item, 'name')] = item.inner;
  728. }
  729. return metadata;
  730. }
  731. /**
  732. * Returns the GDAL nodata value
  733. * @returns {number|null}
  734. */
  735. getGDALNoData() {
  736. if (!this.fileDirectory.GDAL_NODATA) {
  737. return null;
  738. }
  739. const string = this.fileDirectory.GDAL_NODATA;
  740. return Number(string.substring(0, string.length - 1));
  741. }
  742. /**
  743. * Returns the image origin as a XYZ-vector. When the image has no affine
  744. * transformation, then an exception is thrown.
  745. * @returns {Array<number>} The origin as a vector
  746. */
  747. getOrigin() {
  748. const tiePoints = this.fileDirectory.ModelTiepoint;
  749. const modelTransformation = this.fileDirectory.ModelTransformation;
  750. if (tiePoints && tiePoints.length === 6) {
  751. return [
  752. tiePoints[3],
  753. tiePoints[4],
  754. tiePoints[5],
  755. ];
  756. }
  757. if (modelTransformation) {
  758. return [
  759. modelTransformation[3],
  760. modelTransformation[7],
  761. modelTransformation[11],
  762. ];
  763. }
  764. throw new Error('The image does not have an affine transformation.');
  765. }
  766. /**
  767. * Returns the image resolution as a XYZ-vector. When the image has no affine
  768. * transformation, then an exception is thrown.
  769. * @param {GeoTIFFImage} [referenceImage=null] A reference image to calculate the resolution from
  770. * in cases when the current image does not have the
  771. * required tags on its own.
  772. * @returns {Array<number>} The resolution as a vector
  773. */
  774. getResolution(referenceImage = null) {
  775. const modelPixelScale = this.fileDirectory.ModelPixelScale;
  776. const modelTransformation = this.fileDirectory.ModelTransformation;
  777. if (modelPixelScale) {
  778. return [
  779. modelPixelScale[0],
  780. -modelPixelScale[1],
  781. modelPixelScale[2],
  782. ];
  783. }
  784. if (modelTransformation) {
  785. return [
  786. modelTransformation[0],
  787. modelTransformation[5],
  788. modelTransformation[10],
  789. ];
  790. }
  791. if (referenceImage) {
  792. const [refResX, refResY, refResZ] = referenceImage.getResolution();
  793. return [
  794. refResX * referenceImage.getWidth() / this.getWidth(),
  795. refResY * referenceImage.getHeight() / this.getHeight(),
  796. refResZ * referenceImage.getWidth() / this.getWidth(),
  797. ];
  798. }
  799. throw new Error('The image does not have an affine transformation.');
  800. }
  801. /**
  802. * Returns whether or not the pixels of the image depict an area (or point).
  803. * @returns {Boolean} Whether the pixels are a point
  804. */
  805. pixelIsArea() {
  806. return this.geoKeys.GTRasterTypeGeoKey === 1;
  807. }
  808. /**
  809. * Returns the image bounding box as an array of 4 values: min-x, min-y,
  810. * max-x and max-y. When the image has no affine transformation, then an
  811. * exception is thrown.
  812. * @returns {Array<number>} The bounding box
  813. */
  814. getBoundingBox() {
  815. const origin = this.getOrigin();
  816. const resolution = this.getResolution();
  817. const x1 = origin[0];
  818. const y1 = origin[1];
  819. const x2 = x1 + (resolution[0] * this.getWidth());
  820. const y2 = y1 + (resolution[1] * this.getHeight());
  821. return [
  822. Math.min(x1, x2),
  823. Math.min(y1, y2),
  824. Math.max(x1, x2),
  825. Math.max(y1, y2),
  826. ];
  827. }
  828. }
  829. export default GeoTIFFImage;