geotiffwriter.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.writeGeotiff = void 0;
  4. /*
  5. Some parts of this file are based on UTIF.js,
  6. which was released under the MIT License.
  7. You can view that here:
  8. https://github.com/photopea/UTIF.js/blob/master/LICENSE
  9. */
  10. const globals_js_1 = require("./globals.js");
  11. const utils_js_1 = require("./utils.js");
  12. const tagName2Code = (0, utils_js_1.invert)(globals_js_1.fieldTagNames);
  13. const geoKeyName2Code = (0, utils_js_1.invert)(globals_js_1.geoKeyNames);
  14. const name2code = {};
  15. (0, utils_js_1.assign)(name2code, tagName2Code);
  16. (0, utils_js_1.assign)(name2code, geoKeyName2Code);
  17. const typeName2byte = (0, utils_js_1.invert)(globals_js_1.fieldTypeNames);
  18. // config variables
  19. const numBytesInIfd = 1000;
  20. const _binBE = {
  21. nextZero: (data, o) => {
  22. let oincr = o;
  23. while (data[oincr] !== 0) {
  24. oincr++;
  25. }
  26. return oincr;
  27. },
  28. readUshort: (buff, p) => {
  29. return (buff[p] << 8) | buff[p + 1];
  30. },
  31. readShort: (buff, p) => {
  32. const a = _binBE.ui8;
  33. a[0] = buff[p + 1];
  34. a[1] = buff[p + 0];
  35. return _binBE.i16[0];
  36. },
  37. readInt: (buff, p) => {
  38. const a = _binBE.ui8;
  39. a[0] = buff[p + 3];
  40. a[1] = buff[p + 2];
  41. a[2] = buff[p + 1];
  42. a[3] = buff[p + 0];
  43. return _binBE.i32[0];
  44. },
  45. readUint: (buff, p) => {
  46. const a = _binBE.ui8;
  47. a[0] = buff[p + 3];
  48. a[1] = buff[p + 2];
  49. a[2] = buff[p + 1];
  50. a[3] = buff[p + 0];
  51. return _binBE.ui32[0];
  52. },
  53. readASCII: (buff, p, l) => {
  54. return l.map((i) => String.fromCharCode(buff[p + i])).join('');
  55. },
  56. readFloat: (buff, p) => {
  57. const a = _binBE.ui8;
  58. (0, utils_js_1.times)(4, (i) => {
  59. a[i] = buff[p + 3 - i];
  60. });
  61. return _binBE.fl32[0];
  62. },
  63. readDouble: (buff, p) => {
  64. const a = _binBE.ui8;
  65. (0, utils_js_1.times)(8, (i) => {
  66. a[i] = buff[p + 7 - i];
  67. });
  68. return _binBE.fl64[0];
  69. },
  70. writeUshort: (buff, p, n) => {
  71. buff[p] = (n >> 8) & 255;
  72. buff[p + 1] = n & 255;
  73. },
  74. writeUint: (buff, p, n) => {
  75. buff[p] = (n >> 24) & 255;
  76. buff[p + 1] = (n >> 16) & 255;
  77. buff[p + 2] = (n >> 8) & 255;
  78. buff[p + 3] = (n >> 0) & 255;
  79. },
  80. writeASCII: (buff, p, s) => {
  81. (0, utils_js_1.times)(s.length, (i) => {
  82. buff[p + i] = s.charCodeAt(i);
  83. });
  84. },
  85. ui8: new Uint8Array(8),
  86. };
  87. _binBE.fl64 = new Float64Array(_binBE.ui8.buffer);
  88. _binBE.writeDouble = (buff, p, n) => {
  89. _binBE.fl64[0] = n;
  90. (0, utils_js_1.times)(8, (i) => {
  91. buff[p + i] = _binBE.ui8[7 - i];
  92. });
  93. };
  94. const _writeIFD = (bin, data, _offset, ifd) => {
  95. let offset = _offset;
  96. const keys = Object.keys(ifd).filter((key) => {
  97. return key !== undefined && key !== null && key !== 'undefined';
  98. });
  99. bin.writeUshort(data, offset, keys.length);
  100. offset += 2;
  101. let eoff = offset + (12 * keys.length) + 4;
  102. for (const key of keys) {
  103. let tag = null;
  104. if (typeof key === 'number') {
  105. tag = key;
  106. }
  107. else if (typeof key === 'string') {
  108. tag = parseInt(key, 10);
  109. }
  110. const typeName = globals_js_1.fieldTagTypes[tag];
  111. const typeNum = typeName2byte[typeName];
  112. if (typeName == null || typeName === undefined || typeof typeName === 'undefined') {
  113. throw new Error(`unknown type of tag: ${tag}`);
  114. }
  115. let val = ifd[key];
  116. if (val === undefined) {
  117. throw new Error(`failed to get value for key ${key}`);
  118. }
  119. // ASCIIZ format with trailing 0 character
  120. // http://www.fileformat.info/format/tiff/corion.htm
  121. // https://stackoverflow.com/questions/7783044/whats-the-difference-between-asciiz-vs-ascii
  122. if (typeName === 'ASCII' && typeof val === 'string' && (0, utils_js_1.endsWith)(val, '\u0000') === false) {
  123. val += '\u0000';
  124. }
  125. const num = val.length;
  126. bin.writeUshort(data, offset, tag);
  127. offset += 2;
  128. bin.writeUshort(data, offset, typeNum);
  129. offset += 2;
  130. bin.writeUint(data, offset, num);
  131. offset += 4;
  132. let dlen = [-1, 1, 1, 2, 4, 8, 0, 0, 0, 0, 0, 0, 8][typeNum] * num;
  133. let toff = offset;
  134. if (dlen > 4) {
  135. bin.writeUint(data, offset, eoff);
  136. toff = eoff;
  137. }
  138. if (typeName === 'ASCII') {
  139. bin.writeASCII(data, toff, val);
  140. }
  141. else if (typeName === 'SHORT') {
  142. (0, utils_js_1.times)(num, (i) => {
  143. bin.writeUshort(data, toff + (2 * i), val[i]);
  144. });
  145. }
  146. else if (typeName === 'LONG') {
  147. (0, utils_js_1.times)(num, (i) => {
  148. bin.writeUint(data, toff + (4 * i), val[i]);
  149. });
  150. }
  151. else if (typeName === 'RATIONAL') {
  152. (0, utils_js_1.times)(num, (i) => {
  153. bin.writeUint(data, toff + (8 * i), Math.round(val[i] * 10000));
  154. bin.writeUint(data, toff + (8 * i) + 4, 10000);
  155. });
  156. }
  157. else if (typeName === 'DOUBLE') {
  158. (0, utils_js_1.times)(num, (i) => {
  159. bin.writeDouble(data, toff + (8 * i), val[i]);
  160. });
  161. }
  162. if (dlen > 4) {
  163. dlen += (dlen & 1);
  164. eoff += dlen;
  165. }
  166. offset += 4;
  167. }
  168. return [offset, eoff];
  169. };
  170. const encodeIfds = (ifds) => {
  171. const data = new Uint8Array(numBytesInIfd);
  172. let offset = 4;
  173. const bin = _binBE;
  174. // set big-endian byte-order
  175. // https://en.wikipedia.org/wiki/TIFF#Byte_order
  176. data[0] = 77;
  177. data[1] = 77;
  178. // set format-version number
  179. // https://en.wikipedia.org/wiki/TIFF#Byte_order
  180. data[3] = 42;
  181. let ifdo = 8;
  182. bin.writeUint(data, offset, ifdo);
  183. offset += 4;
  184. ifds.forEach((ifd, i) => {
  185. const noffs = _writeIFD(bin, data, ifdo, ifd);
  186. ifdo = noffs[1];
  187. if (i < ifds.length - 1) {
  188. bin.writeUint(data, noffs[0], ifdo);
  189. }
  190. });
  191. if (data.slice) {
  192. return data.slice(0, ifdo).buffer;
  193. }
  194. // node hasn't implemented slice on Uint8Array yet
  195. const result = new Uint8Array(ifdo);
  196. for (let i = 0; i < ifdo; i++) {
  197. result[i] = data[i];
  198. }
  199. return result.buffer;
  200. };
  201. const encodeImage = (values, width, height, metadata) => {
  202. if (height === undefined || height === null) {
  203. throw new Error(`you passed into encodeImage a width of type ${height}`);
  204. }
  205. if (width === undefined || width === null) {
  206. throw new Error(`you passed into encodeImage a width of type ${width}`);
  207. }
  208. const ifd = {
  209. 256: [width],
  210. 257: [height],
  211. 273: [numBytesInIfd],
  212. 278: [height],
  213. 305: 'geotiff.js', // no array for ASCII(Z)
  214. };
  215. if (metadata) {
  216. for (const i in metadata) {
  217. if (metadata.hasOwnProperty(i)) {
  218. ifd[i] = metadata[i];
  219. }
  220. }
  221. }
  222. const prfx = new Uint8Array(encodeIfds([ifd]));
  223. const img = new Uint8Array(values);
  224. const samplesPerPixel = ifd[277];
  225. const data = new Uint8Array(numBytesInIfd + (width * height * samplesPerPixel));
  226. (0, utils_js_1.times)(prfx.length, (i) => {
  227. data[i] = prfx[i];
  228. });
  229. (0, utils_js_1.forEach)(img, (value, i) => {
  230. data[numBytesInIfd + i] = value;
  231. });
  232. return data.buffer;
  233. };
  234. const convertToTids = (input) => {
  235. const result = {};
  236. for (const key in input) {
  237. if (key !== 'StripOffsets') {
  238. if (!name2code[key]) {
  239. console.error(key, 'not in name2code:', Object.keys(name2code));
  240. }
  241. result[name2code[key]] = input[key];
  242. }
  243. }
  244. return result;
  245. };
  246. const toArray = (input) => {
  247. if (Array.isArray(input)) {
  248. return input;
  249. }
  250. return [input];
  251. };
  252. const metadataDefaults = [
  253. ['Compression', 1],
  254. ['PlanarConfiguration', 1],
  255. ['ExtraSamples', 0],
  256. ];
  257. function writeGeotiff(data, metadata) {
  258. const isFlattened = typeof data[0] === 'number';
  259. let height;
  260. let numBands;
  261. let width;
  262. let flattenedValues;
  263. if (isFlattened) {
  264. height = metadata.height || metadata.ImageLength;
  265. width = metadata.width || metadata.ImageWidth;
  266. numBands = data.length / (height * width);
  267. flattenedValues = data;
  268. }
  269. else {
  270. numBands = data.length;
  271. height = data[0].length;
  272. width = data[0][0].length;
  273. flattenedValues = [];
  274. (0, utils_js_1.times)(height, (rowIndex) => {
  275. (0, utils_js_1.times)(width, (columnIndex) => {
  276. (0, utils_js_1.times)(numBands, (bandIndex) => {
  277. flattenedValues.push(data[bandIndex][rowIndex][columnIndex]);
  278. });
  279. });
  280. });
  281. }
  282. metadata.ImageLength = height;
  283. delete metadata.height;
  284. metadata.ImageWidth = width;
  285. delete metadata.width;
  286. // consult https://www.loc.gov/preservation/digital/formats/content/tiff_tags.shtml
  287. if (!metadata.BitsPerSample) {
  288. metadata.BitsPerSample = (0, utils_js_1.times)(numBands, () => 8);
  289. }
  290. metadataDefaults.forEach((tag) => {
  291. const key = tag[0];
  292. if (!metadata[key]) {
  293. const value = tag[1];
  294. metadata[key] = value;
  295. }
  296. });
  297. // The color space of the image data.
  298. // 1=black is zero and 2=RGB.
  299. if (!metadata.PhotometricInterpretation) {
  300. metadata.PhotometricInterpretation = metadata.BitsPerSample.length === 3 ? 2 : 1;
  301. }
  302. // The number of components per pixel.
  303. if (!metadata.SamplesPerPixel) {
  304. metadata.SamplesPerPixel = [numBands];
  305. }
  306. if (!metadata.StripByteCounts) {
  307. // we are only writing one strip
  308. metadata.StripByteCounts = [numBands * height * width];
  309. }
  310. if (!metadata.ModelPixelScale) {
  311. // assumes raster takes up exactly the whole globe
  312. metadata.ModelPixelScale = [360 / width, 180 / height, 0];
  313. }
  314. if (!metadata.SampleFormat) {
  315. metadata.SampleFormat = (0, utils_js_1.times)(numBands, () => 1);
  316. }
  317. // if didn't pass in projection information, assume the popular 4326 "geographic projection"
  318. if (!metadata.hasOwnProperty('GeographicTypeGeoKey') && !metadata.hasOwnProperty('ProjectedCSTypeGeoKey')) {
  319. metadata.GeographicTypeGeoKey = 4326;
  320. metadata.ModelTiepoint = [0, 0, 0, -180, 90, 0]; // raster fits whole globe
  321. metadata.GeogCitationGeoKey = 'WGS 84';
  322. metadata.GTModelTypeGeoKey = 2;
  323. }
  324. const geoKeys = Object.keys(metadata)
  325. .filter((key) => (0, utils_js_1.endsWith)(key, 'GeoKey'))
  326. .sort((a, b) => name2code[a] - name2code[b]);
  327. if (!metadata.GeoAsciiParams) {
  328. let geoAsciiParams = '';
  329. geoKeys.forEach((name) => {
  330. const code = Number(name2code[name]);
  331. const tagType = globals_js_1.fieldTagTypes[code];
  332. if (tagType === 'ASCII') {
  333. geoAsciiParams += `${metadata[name].toString()}\u0000`;
  334. }
  335. });
  336. if (geoAsciiParams.length > 0) {
  337. metadata.GeoAsciiParams = geoAsciiParams;
  338. }
  339. }
  340. if (!metadata.GeoKeyDirectory) {
  341. const NumberOfKeys = geoKeys.length;
  342. const GeoKeyDirectory = [1, 1, 0, NumberOfKeys];
  343. geoKeys.forEach((geoKey) => {
  344. const KeyID = Number(name2code[geoKey]);
  345. GeoKeyDirectory.push(KeyID);
  346. let Count;
  347. let TIFFTagLocation;
  348. let valueOffset;
  349. if (globals_js_1.fieldTagTypes[KeyID] === 'SHORT') {
  350. Count = 1;
  351. TIFFTagLocation = 0;
  352. valueOffset = metadata[geoKey];
  353. }
  354. else if (geoKey === 'GeogCitationGeoKey') {
  355. Count = metadata.GeoAsciiParams.length;
  356. TIFFTagLocation = Number(name2code.GeoAsciiParams);
  357. valueOffset = 0;
  358. }
  359. else {
  360. console.log(`[geotiff.js] couldn't get TIFFTagLocation for ${geoKey}`);
  361. }
  362. GeoKeyDirectory.push(TIFFTagLocation);
  363. GeoKeyDirectory.push(Count);
  364. GeoKeyDirectory.push(valueOffset);
  365. });
  366. metadata.GeoKeyDirectory = GeoKeyDirectory;
  367. }
  368. // delete GeoKeys from metadata, because stored in GeoKeyDirectory tag
  369. for (const geoKey in geoKeys) {
  370. if (geoKeys.hasOwnProperty(geoKey)) {
  371. delete metadata[geoKey];
  372. }
  373. }
  374. [
  375. 'Compression',
  376. 'ExtraSamples',
  377. 'GeographicTypeGeoKey',
  378. 'GTModelTypeGeoKey',
  379. 'GTRasterTypeGeoKey',
  380. 'ImageLength',
  381. 'ImageWidth',
  382. 'Orientation',
  383. 'PhotometricInterpretation',
  384. 'ProjectedCSTypeGeoKey',
  385. 'PlanarConfiguration',
  386. 'ResolutionUnit',
  387. 'SamplesPerPixel',
  388. 'XPosition',
  389. 'YPosition',
  390. ].forEach((name) => {
  391. if (metadata[name]) {
  392. metadata[name] = toArray(metadata[name]);
  393. }
  394. });
  395. const encodedMetadata = convertToTids(metadata);
  396. const outputImage = encodeImage(flattenedValues, width, height, encodedMetadata);
  397. return outputImage;
  398. }
  399. exports.writeGeotiff = writeGeotiff;
  400. //# sourceMappingURL=geotiffwriter.js.map