geotiffwriter.js 11 KB

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