@cogeotiff/core 7.2.1 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +5 -502
  2. package/README.md +23 -11
  3. package/build/__benchmark__/cog.read.benchmark.d.ts +0 -1
  4. package/build/__benchmark__/cog.read.benchmark.js +6 -4
  5. package/build/__benchmark__/cog.read.benchmark.js.map +1 -0
  6. package/build/__benchmark__/source.file.d.ts +9 -0
  7. package/build/__benchmark__/source.file.js +29 -0
  8. package/build/__benchmark__/source.file.js.map +1 -0
  9. package/build/__benchmark__/source.memory.d.ts +9 -0
  10. package/build/__benchmark__/source.memory.js +32 -0
  11. package/build/__benchmark__/source.memory.js.map +1 -0
  12. package/build/__test__/cog.image.test.d.ts +1 -2
  13. package/build/__test__/cog.image.test.js +82 -90
  14. package/build/__test__/cog.image.test.js.map +1 -0
  15. package/build/__test__/cog.read.test.d.ts +1 -2
  16. package/build/__test__/cog.read.test.js +37 -55
  17. package/build/__test__/cog.read.test.js.map +1 -0
  18. package/build/__test__/example.d.ts +1 -0
  19. package/build/__test__/example.js +27 -0
  20. package/build/__test__/example.js.map +1 -0
  21. package/build/cog.tiff.d.ts +22 -28
  22. package/build/cog.tiff.image.d.ts +11 -23
  23. package/build/cog.tiff.image.js +108 -62
  24. package/build/cog.tiff.image.js.map +1 -0
  25. package/build/cog.tiff.js +141 -97
  26. package/build/cog.tiff.js.map +1 -0
  27. package/build/const/index.d.ts +1 -2
  28. package/build/const/index.js +2 -2
  29. package/build/const/index.js.map +1 -0
  30. package/build/const/tiff.endian.d.ts +0 -1
  31. package/build/const/tiff.endian.js +1 -1
  32. package/build/const/tiff.endian.js.map +1 -0
  33. package/build/const/tiff.mime.d.ts +10 -11
  34. package/build/const/tiff.mime.js +22 -22
  35. package/build/const/tiff.mime.js.map +1 -0
  36. package/build/const/tiff.tag.id.d.ts +3 -3
  37. package/build/const/tiff.tag.id.js +4 -3
  38. package/build/const/tiff.tag.id.js.map +1 -0
  39. package/build/const/tiff.tag.value.d.ts +15 -16
  40. package/build/const/tiff.tag.value.js +16 -16
  41. package/build/const/tiff.tag.value.js.map +1 -0
  42. package/build/const/tiff.version.d.ts +0 -1
  43. package/build/const/tiff.version.js +1 -1
  44. package/build/const/tiff.version.js.map +1 -0
  45. package/build/index.d.ts +11 -6
  46. package/build/index.js +9 -5
  47. package/build/index.js.map +1 -0
  48. package/build/read/data.view.offset.d.ts +15 -0
  49. package/build/read/data.view.offset.js +19 -0
  50. package/build/read/data.view.offset.js.map +1 -0
  51. package/build/read/tiff.gdal.d.ts +9 -12
  52. package/build/read/tiff.gdal.js +22 -17
  53. package/build/read/tiff.gdal.js.map +1 -0
  54. package/build/read/tiff.ifd.config.d.ts +6 -4
  55. package/build/read/tiff.ifd.config.js +2 -2
  56. package/build/read/tiff.ifd.config.js.map +1 -0
  57. package/build/read/tiff.tag.d.ts +40 -20
  58. package/build/read/tiff.tag.factory.d.ts +16 -0
  59. package/build/read/tiff.tag.factory.js +130 -0
  60. package/build/read/tiff.tag.factory.js.map +1 -0
  61. package/build/read/tiff.tag.js +2 -37
  62. package/build/read/tiff.tag.js.map +1 -0
  63. package/build/read/tiff.value.reader.d.ts +2 -6
  64. package/build/read/tiff.value.reader.js +16 -54
  65. package/build/read/tiff.value.reader.js.map +1 -0
  66. package/build/source.d.ts +5 -0
  67. package/build/source.js +2 -0
  68. package/build/source.js.map +1 -0
  69. package/build/util/bytes.d.ts +17 -0
  70. package/build/util/bytes.js +42 -0
  71. package/build/util/bytes.js.map +1 -0
  72. package/build/util/util.hex.d.ts +2 -3
  73. package/build/util/util.hex.js +4 -5
  74. package/build/util/util.hex.js.map +1 -0
  75. package/build/vector.d.ts +0 -1
  76. package/build/vector.js +1 -1
  77. package/build/vector.js.map +1 -0
  78. package/package.json +25 -30
  79. package/src/__benchmark__/cog.read.benchmark.ts +12 -10
  80. package/src/__benchmark__/source.file.ts +23 -0
  81. package/src/__benchmark__/source.memory.ts +23 -0
  82. package/src/__test__/cog.image.test.ts +188 -197
  83. package/src/__test__/cog.read.test.ts +50 -72
  84. package/src/__test__/example.ts +31 -0
  85. package/src/cog.tiff.image.ts +456 -447
  86. package/src/cog.tiff.ts +142 -145
  87. package/src/const/index.ts +1 -1
  88. package/src/const/tiff.endian.ts +2 -2
  89. package/src/const/tiff.mime.ts +21 -21
  90. package/src/const/tiff.tag.id.ts +157 -156
  91. package/src/const/tiff.tag.value.ts +16 -16
  92. package/src/const/tiff.version.ts +11 -11
  93. package/src/index.ts +11 -5
  94. package/src/read/data.view.offset.ts +23 -0
  95. package/src/read/tiff.gdal.ts +61 -63
  96. package/src/read/tiff.ifd.config.ts +35 -31
  97. package/src/read/tiff.tag.factory.ts +163 -0
  98. package/src/read/tiff.tag.ts +40 -38
  99. package/src/read/tiff.value.reader.ts +25 -73
  100. package/src/source.ts +5 -0
  101. package/src/util/bytes.ts +44 -0
  102. package/src/util/util.hex.ts +5 -7
  103. package/src/vector.ts +5 -5
  104. package/tsconfig.json +8 -8
  105. package/build/__benchmark__/cog.read.benchmark.d.ts.map +0 -1
  106. package/build/__test__/cog.image.test.d.ts.map +0 -1
  107. package/build/__test__/cog.read.test.d.ts.map +0 -1
  108. package/build/cog.tiff.d.ts.map +0 -1
  109. package/build/cog.tiff.image.d.ts.map +0 -1
  110. package/build/const/index.d.ts.map +0 -1
  111. package/build/const/tiff.endian.d.ts.map +0 -1
  112. package/build/const/tiff.mime.d.ts.map +0 -1
  113. package/build/const/tiff.tag.id.d.ts.map +0 -1
  114. package/build/const/tiff.tag.value.d.ts.map +0 -1
  115. package/build/const/tiff.version.d.ts.map +0 -1
  116. package/build/index.d.ts.map +0 -1
  117. package/build/read/tag/__test__/tag.test.d.ts +0 -2
  118. package/build/read/tag/__test__/tag.test.d.ts.map +0 -1
  119. package/build/read/tag/__test__/tag.test.js +0 -23
  120. package/build/read/tag/tiff.tag.base.d.ts +0 -55
  121. package/build/read/tag/tiff.tag.base.d.ts.map +0 -1
  122. package/build/read/tag/tiff.tag.base.js +0 -79
  123. package/build/read/tag/tiff.tag.lazy.d.ts +0 -7
  124. package/build/read/tag/tiff.tag.lazy.d.ts.map +0 -1
  125. package/build/read/tag/tiff.tag.lazy.js +0 -18
  126. package/build/read/tag/tiff.tag.offset.d.ts +0 -21
  127. package/build/read/tag/tiff.tag.offset.d.ts.map +0 -1
  128. package/build/read/tag/tiff.tag.offset.js +0 -54
  129. package/build/read/tag/tiff.tag.static.d.ts +0 -8
  130. package/build/read/tag/tiff.tag.static.d.ts.map +0 -1
  131. package/build/read/tag/tiff.tag.static.js +0 -17
  132. package/build/read/tiff.gdal.d.ts.map +0 -1
  133. package/build/read/tiff.ifd.config.d.ts.map +0 -1
  134. package/build/read/tiff.tag.d.ts.map +0 -1
  135. package/build/read/tiff.value.reader.d.ts.map +0 -1
  136. package/build/source/cog.source.view.d.ts +0 -33
  137. package/build/source/cog.source.view.d.ts.map +0 -1
  138. package/build/source/cog.source.view.js +0 -65
  139. package/build/util/util.hex.d.ts.map +0 -1
  140. package/build/vector.d.ts.map +0 -1
  141. package/src/@types/ieee754.d.ts +0 -18
  142. package/src/read/tag/__test__/tag.test.ts +0 -27
  143. package/src/read/tag/tiff.tag.base.ts +0 -126
  144. package/src/read/tag/tiff.tag.lazy.ts +0 -17
  145. package/src/read/tag/tiff.tag.offset.ts +0 -61
  146. package/src/read/tag/tiff.tag.static.ts +0 -15
  147. package/src/source/cog.source.view.ts +0 -77
@@ -1,492 +1,501 @@
1
+ import { getUint } from './util/bytes.js';
1
2
  import { CogTiff } from './cog.tiff.js';
2
3
  import { TiffCompression, TiffMimeType } from './const/tiff.mime.js';
3
4
  import { TiffTag, TiffTagGeo } from './const/tiff.tag.id.js';
4
- import { CogTiffTagBase } from './read/tag/tiff.tag.base.js';
5
- import { CogTiffTagLazy } from './read/tag/tiff.tag.lazy.js';
6
- import { CogTiffTagOffset } from './read/tag/tiff.tag.offset.js';
7
- import { CogTiffTag } from './read/tiff.tag.js';
5
+ import { Tag, TagInline, TagOffset } from './read/tiff.tag.js';
8
6
  import { BoundingBox, Size } from './vector.js';
7
+ import { fetchLazy, getValueAt } from './read/tiff.tag.factory.js';
9
8
 
10
- /** Invalid EPSG code */
9
+ // /** Invalid EPSG code */
11
10
  export const InvalidProjectionCode = 32767;
12
11
 
13
12
  /**
14
13
  * Number of tiles used inside this image
15
14
  */
16
15
  export interface CogTiffImageTiledCount {
17
- /** Number of tiles on the x axis */
18
- x: number;
19
- /** Number of tiles on the y axis */
20
- y: number;
16
+ /** Number of tiles on the x axis */
17
+ x: number;
18
+ /** Number of tiles on the y axis */
19
+ y: number;
21
20
  }
22
21
 
22
+ /** Tags that are commonly accessed for geotiffs */
23
+ export const ImportantTags = new Set([
24
+ TiffTag.Compression,
25
+ TiffTag.ImageHeight,
26
+ TiffTag.ImageWidth,
27
+ TiffTag.ModelPixelScale,
28
+ TiffTag.ModelTiePoint,
29
+ TiffTag.ModelTransformation,
30
+ TiffTag.TileHeight,
31
+ TiffTag.TileWidth,
32
+ TiffTag.GeoKeyDirectory,
33
+ TiffTag.GeoAsciiParams,
34
+ TiffTag.GeoDoubleParams,
35
+ TiffTag.TileOffsets,
36
+ ]);
37
+
23
38
  /**
24
39
  * Size of a individual tile
25
40
  */
26
41
  export interface CogTiffImageTileSize {
27
- /** Tile width (pixels) */
28
- width: number;
29
- /** Tile height (pixels) */
30
- height: number;
42
+ /** Tile width (pixels) */
43
+ width: number;
44
+ /** Tile height (pixels) */
45
+ height: number;
31
46
  }
32
47
 
33
48
  export class CogTiffImage {
34
- /** All IFD tags that have been read for the image */
35
- tags: Map<TiffTag, CogTiffTagBase>;
36
-
37
- /** Id of the tif image, generally the image index inside the tif */
38
- id: number;
39
-
40
- tif: CogTiff;
41
-
42
- /** Has loadGeoTiffTags been called */
43
- private tagsGeoLoaded = false;
44
- /** Sub tags stored in TiffTag.GeoKeyDirectory */
45
- tagsGeo: Map<TiffTagGeo, string | number> = new Map();
46
-
47
- constructor(tif: CogTiff, id: number, tags: Map<TiffTag, CogTiffTagBase>) {
48
- this.tif = tif;
49
- this.id = id;
50
- this.tags = tags;
51
- }
52
-
53
- /**
54
- * Force loading of important tags if they have not already been loaded
55
- *
56
- * @param loadGeoTags Whether to load the GeoKeyDirectory and unpack it
57
- */
58
- async init(loadGeoTags = false): Promise<void> {
59
- const requiredTags = [
60
- this.fetch(TiffTag.Compression),
61
- this.fetch(TiffTag.ImageHeight),
62
- this.fetch(TiffTag.ImageWidth),
63
- this.fetch(TiffTag.ModelPixelScale),
64
- this.fetch(TiffTag.ModelTiePoint),
65
- this.fetch(TiffTag.ModelTransformation),
66
- this.fetch(TiffTag.TileHeight),
67
- this.fetch(TiffTag.TileWidth),
68
- ];
69
-
70
- if (loadGeoTags) {
71
- requiredTags.push(this.fetch(TiffTag.GeoKeyDirectory));
72
- requiredTags.push(this.fetch(TiffTag.GeoAsciiParams));
73
- requiredTags.push(this.fetch(TiffTag.GeoDoubleParams));
74
- }
75
-
76
- await Promise.all(requiredTags);
77
- if (loadGeoTags) {
78
- await this.loadGeoTiffTags();
79
- }
80
- }
81
-
82
- /**
83
- * Get the value of a TiffTag if it exists null otherwise
84
- */
85
- value<T>(tag: TiffTag): T | null {
86
- const sourceTag = this.tags.get(tag);
87
- if (sourceTag == null) return null;
88
- return sourceTag.value as T;
89
- }
90
-
91
- /**
92
- * Load and unpack the GeoKeyDirectory
93
- */
94
- async loadGeoTiffTags(): Promise<void> {
95
- // Already loaded
96
- if (this.tagsGeoLoaded) return;
97
- const sourceTag = this.tags.get(TiffTag.GeoKeyDirectory);
98
- if (sourceTag == null) {
99
- this.tagsGeoLoaded = true;
100
- return;
101
- }
102
- if (!sourceTag.isReady && sourceTag instanceof CogTiffTagLazy) {
103
- // Load all the required keys
104
- await Promise.all([
105
- this.fetch(TiffTag.GeoKeyDirectory),
106
- this.fetch(TiffTag.GeoAsciiParams),
107
- this.fetch(TiffTag.GeoDoubleParams),
108
- ]);
109
- }
110
- this.tagsGeoLoaded = true;
111
- if (sourceTag.value == null) return;
112
- const geoTags = sourceTag.value;
113
- if (!Array.isArray(geoTags)) throw new Error('Invalid geo tags found');
114
- for (let i = 4; i <= geoTags[3] * 4; i += 4) {
115
- const key = geoTags[i] as TiffTagGeo;
116
- const location = geoTags[i + 1];
117
-
118
- const offset = geoTags[i + 3];
119
-
120
- if (location === 0) {
121
- this.tagsGeo.set(key, offset);
122
- continue;
123
- }
124
- const tag = this.tags.get(location);
125
- if (tag == null || tag.value == null) continue;
126
- const count = geoTags[i + 2];
127
- if (Array.isArray(tag.value)) {
128
- this.tagsGeo.set(key, tag.value[offset + count - 1]);
129
- } else if (typeof tag.value === 'string') {
130
- this.tagsGeo.set(key, tag.value.substr(offset, offset + count - 1).trim());
131
- }
132
- }
133
- }
134
-
135
- /**
136
- * Get the associated GeoTiffTags
137
- */
138
- valueGeo(tag: TiffTagGeo): string | number | undefined {
139
- if (this.tagsGeoLoaded === false) throw new Error('loadGeoTiffTags() has not been called');
140
- return this.tagsGeo.get(tag);
141
- }
142
-
143
- /**
144
- * Load a tag, if it is not currently loaded, fetch the required data for the tag.
145
- * @param tag tag to fetch
146
- */
147
- public async fetch<T>(tag: TiffTag): Promise<T | null> {
148
- const sourceTag = this.tags.get(tag);
149
- if (sourceTag == null) return null;
150
- if (CogTiffTag.isLazy(sourceTag)) return sourceTag.fetch() as any;
151
- return sourceTag.value as T;
152
- }
153
-
154
- /**
155
- * Get the origin point for the image
156
- *
157
- * @returns origin point of the image
158
- */
159
- get origin(): [number, number, number] {
160
- const tiePoints: number[] | null = this.value<number[]>(TiffTag.ModelTiePoint);
161
- if (tiePoints != null && tiePoints.length === 6) {
162
- return [tiePoints[3], tiePoints[4], tiePoints[5]];
163
- }
164
-
165
- const modelTransformation = this.value<number[]>(TiffTag.ModelTransformation);
166
- if (modelTransformation != null) {
167
- return [modelTransformation[3], modelTransformation[7], modelTransformation[11]];
168
- }
169
-
170
- // If this is a sub image, use the origin from the top level image
171
- if (this.value(TiffTag.NewSubFileType) === 1 && this.id !== 0) {
172
- return this.tif.images[0].origin;
173
- }
174
-
175
- throw new Error('Image does not have a geo transformation.');
176
- }
177
-
178
- /** Is there enough geo information on this image to figure out where its actually located */
179
- get isGeoLocated(): boolean {
180
- const isImageLocated =
181
- this.value(TiffTag.ModelPixelScale) != null || this.value(TiffTag.ModelTransformation) != null;
182
- if (isImageLocated) return true;
183
- // If this is a sub image, use the isGeoLocated from the top level image
184
- if (this.value(TiffTag.NewSubFileType) === 1 && this.id !== 0) return this.tif.images[0].isGeoLocated;
185
- return false;
186
- }
187
-
188
- /**
189
- * Get the resolution of the image
190
- *
191
- * @returns [x,y,z] pixel scale
192
- */
193
- get resolution(): [number, number, number] {
194
- const modelPixelScale: number[] | null = this.value(TiffTag.ModelPixelScale);
195
- if (modelPixelScale != null) {
196
- return [modelPixelScale[0], -modelPixelScale[1], modelPixelScale[2]];
197
- }
198
- const modelTransformation: number[] | null = this.value(TiffTag.ModelTransformation);
199
- if (modelTransformation != null) {
200
- return [modelTransformation[0], modelTransformation[5], modelTransformation[10]];
201
- }
202
-
203
- // If this is a sub image, use the resolution from the top level image
204
- if (this.value(TiffTag.NewSubFileType) === 1 && this.id !== 0) {
205
- const firstImg = this.tif.images[0];
206
- const [resX, resY, resZ] = firstImg.resolution;
207
- const firstImgSize = firstImg.size;
208
- const imgSize = this.size;
209
- // scale resolution based on the size difference between the two images
210
- return [(resX * firstImgSize.width) / imgSize.width, (resY * firstImgSize.height) / imgSize.height, resZ];
211
- }
212
-
213
- throw new Error('Image does not have a geo transformation.');
49
+ /** All IFD tags that have been read for the image */
50
+ tags: Map<TiffTag, Tag>;
51
+
52
+ /** Id of the tif image, generally the image index inside the tif */
53
+ id: number;
54
+
55
+ tiff: CogTiff;
56
+
57
+ /** Has loadGeoTiffTags been called */
58
+ isGeoTagsLoaded = false;
59
+ /** Sub tags stored in TiffTag.GeoKeyDirectory */
60
+ tagsGeo: Map<TiffTagGeo, string | number> = new Map();
61
+
62
+ constructor(tiff: CogTiff, id: number, tags: Map<TiffTag, Tag>) {
63
+ this.tiff = tiff;
64
+ this.id = id;
65
+ this.tags = tags;
66
+ }
67
+
68
+ /**
69
+ * Force loading of important tags if they have not already been loaded
70
+ *
71
+ * @param loadGeoTags Whether to load the GeoKeyDirectory and unpack it
72
+ */
73
+ async init(loadGeoTags = true): Promise<void> {
74
+ const requiredTags = [
75
+ this.fetch(TiffTag.Compression),
76
+ this.fetch(TiffTag.ImageHeight),
77
+ this.fetch(TiffTag.ImageWidth),
78
+ this.fetch(TiffTag.ModelPixelScale),
79
+ this.fetch(TiffTag.ModelTiePoint),
80
+ this.fetch(TiffTag.ModelTransformation),
81
+ this.fetch(TiffTag.TileHeight),
82
+ this.fetch(TiffTag.TileWidth),
83
+ ];
84
+
85
+ if (loadGeoTags) {
86
+ requiredTags.push(this.fetch(TiffTag.GeoKeyDirectory));
87
+ requiredTags.push(this.fetch(TiffTag.GeoAsciiParams));
88
+ requiredTags.push(this.fetch(TiffTag.GeoDoubleParams));
214
89
  }
215
90
 
216
- /**
217
- * Bounding box of the image
218
- *
219
- * @returns [minX, minY, maxX, maxY] bounding box
220
- */
221
- get bbox(): [number, number, number, number] {
222
- const size = this.size;
223
- const origin = this.origin;
224
- const resolution = this.resolution;
225
-
226
- if (origin == null || size == null || resolution == null) {
227
- throw new Error('Unable to calculate bounding box');
228
- }
229
-
230
- const x1 = origin[0];
231
- const y1 = origin[1];
232
-
233
- const x2 = x1 + resolution[0] * size.width;
234
- const y2 = y1 + resolution[1] * size.height;
235
-
236
- return [Math.min(x1, x2), Math.min(y1, y2), Math.max(x1, x2), Math.max(y1, y2)];
91
+ await Promise.all(requiredTags);
92
+ if (loadGeoTags) await this.loadGeoTiffTags();
93
+ }
94
+
95
+ /**
96
+ * Get the value of a TiffTag if it exists null otherwise
97
+ */
98
+ value<T>(tag: TiffTag): T | null {
99
+ const sourceTag = this.tags.get(tag);
100
+ if (sourceTag == null) return null;
101
+ if (sourceTag.type === 'offset' && sourceTag.isLoaded === false) return null;
102
+ return sourceTag.value as T;
103
+ }
104
+
105
+ /**
106
+ * Load and unpack the GeoKeyDirectory
107
+ */
108
+ async loadGeoTiffTags(): Promise<void> {
109
+ // Already loaded
110
+ if (this.isGeoTagsLoaded) return;
111
+ const sourceTag = this.tags.get(TiffTag.GeoKeyDirectory);
112
+ if (sourceTag == null) {
113
+ this.isGeoTagsLoaded = true;
114
+ return;
237
115
  }
238
-
239
- /**
240
- * Get the compression used by the tile
241
- *
242
- * @see TiffCompression
243
- *
244
- * @returns Compression type eg webp
245
- */
246
- get compression(): TiffMimeType | null {
247
- const compression = this.value(TiffTag.Compression);
248
- if (compression == null || typeof compression !== 'number') {
249
- return null;
250
- }
251
- return TiffCompression[compression];
116
+ if (sourceTag.type === 'lazy' && sourceTag.value == null) {
117
+ // Load all the required keys
118
+ await Promise.all([
119
+ this.fetch(TiffTag.GeoKeyDirectory),
120
+ this.fetch(TiffTag.GeoAsciiParams),
121
+ this.fetch(TiffTag.GeoDoubleParams),
122
+ ]);
252
123
  }
253
-
254
- /**
255
- * Attempt to read the EPSG Code from TiffGeoTags
256
- *
257
- * @returns EPSG Code if it exists
258
- */
259
- get epsg(): number | null {
260
- const projection = this.valueGeo(TiffTagGeo.ProjectedCSTypeGeoKey) as number;
261
- if (projection === InvalidProjectionCode) return null;
262
- return projection;
124
+ this.isGeoTagsLoaded = true;
125
+ if (sourceTag.value == null) return;
126
+ const geoTags = sourceTag.value as Uint16Array;
127
+ if (typeof geoTags === 'number') throw new Error('Invalid geo tags found');
128
+ for (let i = 4; i <= geoTags[3] * 4; i += 4) {
129
+ const key = geoTags[i] as TiffTagGeo;
130
+ const location = geoTags[i + 1];
131
+
132
+ const offset = geoTags[i + 3];
133
+
134
+ if (location === 0) {
135
+ this.tagsGeo.set(key, offset);
136
+ continue;
137
+ }
138
+ const tag = this.tags.get(location);
139
+ if (tag == null || tag.value == null) continue;
140
+ const count = geoTags[i + 2];
141
+ if (Array.isArray(tag.value)) {
142
+ this.tagsGeo.set(key, tag.value[offset + count - 1]);
143
+ } else if (typeof tag.value === 'string') {
144
+ this.tagsGeo.set(key, tag.value.slice(offset, offset + count - 1).trim());
145
+ }
263
146
  }
264
-
265
- /**
266
- * Get the size of the image
267
- *
268
- * @returns Size in pixels
269
- */
270
- get size(): Size {
271
- return {
272
- width: this.value<number>(TiffTag.ImageWidth) as number,
273
- height: this.value<number>(TiffTag.ImageHeight) as number,
274
- };
147
+ }
148
+
149
+ /**
150
+ * Get the associated GeoTiffTags
151
+ */
152
+ valueGeo(tag: TiffTagGeo): string | number | undefined {
153
+ if (this.isGeoTagsLoaded === false) throw new Error('loadGeoTiffTags() has not been called');
154
+ return this.tagsGeo.get(tag);
155
+ }
156
+
157
+ /**
158
+ * Load a tag, if it is not currently loaded, fetch the required data for the tag.
159
+ * @param tag tag to fetch
160
+ */
161
+ public async fetch<T>(tag: TiffTag): Promise<T | null> {
162
+ const sourceTag = this.tags.get(tag);
163
+ if (sourceTag == null) return null;
164
+ if (sourceTag.type === 'inline') return sourceTag.value as unknown as T;
165
+ if (sourceTag.type === 'lazy') return fetchLazy(sourceTag, this.tiff) as unknown as T;
166
+ if (sourceTag.isLoaded) return sourceTag.value as unknown as T;
167
+ throw new Error('Cannot fetch:' + tag);
168
+ }
169
+
170
+ /**
171
+ * Get the origin point for the image
172
+ *
173
+ * @returns origin point of the image
174
+ */
175
+ get origin(): [number, number, number] {
176
+ const tiePoints: number[] | null = this.value<number[]>(TiffTag.ModelTiePoint);
177
+ if (tiePoints != null && tiePoints.length === 6) {
178
+ return [tiePoints[3], tiePoints[4], tiePoints[5]];
275
179
  }
276
180
 
277
- /**
278
- * Get the list of IFD tags that were read
279
- */
280
- get tagList(): string[] {
281
- return [...this.tags.keys()].map((c) => TiffTag[c]);
181
+ const modelTransformation = this.value<number[]>(TiffTag.ModelTransformation);
182
+ if (modelTransformation != null) {
183
+ return [modelTransformation[3], modelTransformation[7], modelTransformation[11]];
282
184
  }
283
185
 
284
- /**
285
- * Determine if this image is tiled
286
- */
287
- public isTiled(): boolean {
288
- return this.value(TiffTag.TileWidth) !== null;
186
+ // If this is a sub image, use the origin from the top level image
187
+ if (this.value(TiffTag.NewSubFileType) === 1 && this.id !== 0) {
188
+ return this.tiff.images[0].origin;
289
189
  }
290
190
 
291
- /**
292
- * Get size of individual tiles
293
- */
294
- get tileSize(): CogTiffImageTileSize {
295
- return {
296
- width: this.value<number>(TiffTag.TileWidth) as number,
297
- height: this.value<number>(TiffTag.TileHeight) as number,
298
- };
191
+ throw new Error('Image does not have a geo transformation.');
192
+ }
193
+
194
+ /** Is there enough geo information on this image to figure out where its actually located */
195
+ get isGeoLocated(): boolean {
196
+ const isImageLocated =
197
+ this.value(TiffTag.ModelPixelScale) != null || this.value(TiffTag.ModelTransformation) != null;
198
+ if (isImageLocated) return true;
199
+ // If this is a sub image, use the isGeoLocated from the top level image
200
+ if (this.value(TiffTag.NewSubFileType) === 1 && this.id !== 0) return this.tiff.images[0].isGeoLocated;
201
+ return false;
202
+ }
203
+
204
+ /**
205
+ * Get the resolution of the image
206
+ *
207
+ * @returns [x,y,z] pixel scale
208
+ */
209
+ get resolution(): [number, number, number] {
210
+ const modelPixelScale: number[] | null = this.value(TiffTag.ModelPixelScale);
211
+ if (modelPixelScale != null) {
212
+ return [modelPixelScale[0], -modelPixelScale[1], modelPixelScale[2]];
299
213
  }
300
-
301
- /**
302
- * Number of tiles used to create this image
303
- */
304
- get tileCount(): CogTiffImageTiledCount {
305
- const size = this.size;
306
- const tileSize = this.tileSize;
307
- const x = Math.ceil(size.width / tileSize.width);
308
- const y = Math.ceil(size.height / tileSize.height);
309
- return { x, y };
214
+ const modelTransformation: number[] | null = this.value(TiffTag.ModelTransformation);
215
+ if (modelTransformation != null) {
216
+ return [modelTransformation[0], modelTransformation[5], modelTransformation[10]];
310
217
  }
311
218
 
312
- /**
313
- * Get the pointer to where the tiles start in the Tiff file
314
- *
315
- * @remarks Used to read tiled tiffs
316
- *
317
- * @returns file offset to where the tiffs are stored
318
- */
319
- get tileOffset(): CogTiffTagOffset {
320
- const tileOffset = this.tags.get(TiffTag.TileOffsets) as CogTiffTagOffset;
321
- if (tileOffset == null) throw new Error('No tile offsets found');
322
- return tileOffset;
219
+ // If this is a sub image, use the resolution from the top level image
220
+ if (this.value(TiffTag.NewSubFileType) === 1 && this.id !== 0) {
221
+ const firstImg = this.tiff.images[0];
222
+ const [resX, resY, resZ] = firstImg.resolution;
223
+ const firstImgSize = firstImg.size;
224
+ const imgSize = this.size;
225
+ // scale resolution based on the size difference between the two images
226
+ return [(resX * firstImgSize.width) / imgSize.width, (resY * firstImgSize.height) / imgSize.height, resZ];
323
227
  }
324
228
 
325
- /**
326
- * Get the number of strip's inside this tiff
327
- *
328
- * @remarks Used to read striped tiffs
329
- *
330
- * @returns number of strips present
331
- */
332
- get stripCount(): number {
333
- const tileOffset = this.tags.get(TiffTag.StripByteCounts) as CogTiffTagOffset;
334
- if (tileOffset == null) return 0;
335
- return tileOffset.dataCount;
229
+ throw new Error('Image does not have a geo transformation.');
230
+ }
231
+
232
+ /**
233
+ * Bounding box of the image
234
+ *
235
+ * @returns [minX, minY, maxX, maxY] bounding box
236
+ */
237
+ get bbox(): [number, number, number, number] {
238
+ const size = this.size;
239
+ const origin = this.origin;
240
+ const resolution = this.resolution;
241
+
242
+ if (origin == null || size == null || resolution == null) {
243
+ throw new Error('Unable to calculate bounding box');
336
244
  }
337
245
 
338
- /**
339
- * Get a pointer to a specific tile inside the tiff file
340
- *
341
- * @param index tile index
342
- * @returns file offset of the specified tile
343
- */
344
- protected async getTileOffset(index: number): Promise<number> {
345
- const tileOffset = this.tileOffset;
346
- if (index < 0 || index > tileOffset.dataCount) {
347
- throw new Error(`Tile offset: ${index} out of range: 0 -> ${tileOffset.dataCount}`);
348
- }
349
-
350
- // Fetch only the part of the offsets that are needed
351
- return tileOffset.getValueAt(index);
246
+ const x1 = origin[0];
247
+ const y1 = origin[1];
248
+
249
+ const x2 = x1 + resolution[0] * size.width;
250
+ const y2 = y1 + resolution[1] * size.height;
251
+
252
+ return [Math.min(x1, x2), Math.min(y1, y2), Math.max(x1, x2), Math.max(y1, y2)];
253
+ }
254
+
255
+ /**
256
+ * Get the compression used by the tile
257
+ *
258
+ * @see TiffCompression
259
+ *
260
+ * @returns Compression type eg webp
261
+ */
262
+ get compression(): TiffMimeType | null {
263
+ const compression = this.value(TiffTag.Compression);
264
+ if (compression == null || typeof compression !== 'number') return null;
265
+ return TiffCompression[compression];
266
+ }
267
+
268
+ /**
269
+ * Attempt to read the EPSG Code from TiffGeoTags
270
+ *
271
+ * @returns EPSG Code if it exists
272
+ */
273
+ get epsg(): number | null {
274
+ const projection = this.valueGeo(TiffTagGeo.ProjectedCSTypeGeoKey) as number;
275
+ if (projection === InvalidProjectionCode) return null;
276
+ return projection;
277
+ }
278
+
279
+ /**
280
+ * Get the size of the image
281
+ *
282
+ * @returns Size in pixels
283
+ */
284
+ get size(): Size {
285
+ return {
286
+ width: this.value<number>(TiffTag.ImageWidth) as number,
287
+ height: this.value<number>(TiffTag.ImageHeight) as number,
288
+ };
289
+ }
290
+
291
+ /**
292
+ * Determine if this image is tiled
293
+ */
294
+ public isTiled(): boolean {
295
+ return this.value(TiffTag.TileWidth) !== null;
296
+ }
297
+
298
+ /**
299
+ * Get size of individual tiles
300
+ */
301
+ get tileSize(): CogTiffImageTileSize {
302
+ return {
303
+ width: this.value<number>(TiffTag.TileWidth) as number,
304
+ height: this.value<number>(TiffTag.TileHeight) as number,
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Number of tiles used to create this image
310
+ */
311
+ get tileCount(): CogTiffImageTiledCount {
312
+ const size = this.size;
313
+ const tileSize = this.tileSize;
314
+ const x = Math.ceil(size.width / tileSize.width);
315
+ const y = Math.ceil(size.height / tileSize.height);
316
+ return { x, y };
317
+ }
318
+
319
+ /**
320
+ * Get the pointer to where the tiles start in the Tiff file
321
+ *
322
+ * @remarks Used to read tiled tiffs
323
+ *
324
+ * @returns file offset to where the tiffs are stored
325
+ */
326
+ get tileOffset(): TagOffset {
327
+ const tileOffset = this.tags.get(TiffTag.TileOffsets) as TagOffset;
328
+ if (tileOffset == null) throw new Error('No tile offsets found');
329
+ return tileOffset;
330
+ }
331
+
332
+ /**
333
+ * Get the number of strip's inside this tiff
334
+ *
335
+ * @remarks Used to read striped tiffs
336
+ *
337
+ * @returns number of strips present
338
+ */
339
+ get stripCount(): number {
340
+ const tileOffset = this.tags.get(TiffTag.StripByteCounts) as TagOffset;
341
+ if (tileOffset == null) return 0;
342
+ return tileOffset.count;
343
+ }
344
+
345
+ // Clamp the bounds of the output image to the size of the image, as sometimes the edge tiles are not full tiles
346
+ getTileBounds(x: number, y: number): BoundingBox {
347
+ const { size, tileSize } = this;
348
+ const top = y * tileSize.height;
349
+ const left = x * tileSize.width;
350
+ const width = left + tileSize.width >= size.width ? size.width - left : tileSize.width;
351
+ const height = top + tileSize.height >= size.height ? size.height - top : tileSize.height;
352
+ return { x: left, y: top, width, height };
353
+ }
354
+
355
+ /**
356
+ * Read a strip into a uint8 array
357
+ *
358
+ * @param index Strip index to read
359
+ */
360
+ async getStrip(index: number): Promise<{ mimeType: TiffMimeType; bytes: ArrayBuffer } | null> {
361
+ if (this.isTiled()) throw new Error('Cannot read stripes, tiff is tiled: ' + index);
362
+
363
+ const byteCounts = this.tags.get(TiffTag.StripByteCounts) as TagOffset;
364
+ const offsets = this.tags.get(TiffTag.StripOffsets) as TagOffset;
365
+
366
+ if (index >= byteCounts.count) throw new Error('Cannot read strip, index out of bounds');
367
+
368
+ const [byteCount, offset] = await Promise.all([
369
+ getOffset(this.tiff, offsets, index),
370
+ getOffset(this.tiff, byteCounts, index),
371
+ ]);
372
+ return this.getBytes(byteCount, offset);
373
+ }
374
+
375
+ /** The jpeg header is stored in the IFD, read the JPEG header and adjust the byte array to include it */
376
+ private getJpegHeader(bytes: ArrayBuffer): ArrayBuffer {
377
+ // Both the JPEGTable and the Bytes with have the start of image and end of image markers
378
+ // StartOfImage 0xffd8 EndOfImage 0xffd9
379
+ const tables = this.value<number[]>(TiffTag.JPEGTables);
380
+ if (tables == null) throw new Error('Unable to find Jpeg header');
381
+
382
+ // Remove EndOfImage marker
383
+ const tableData = tables.slice(0, tables.length - 2);
384
+ const actualBytes = new Uint8Array(bytes.byteLength + tableData.length - 2);
385
+ actualBytes.set(tableData, 0);
386
+ actualBytes.set(new Uint8Array(bytes).slice(2), tableData.length);
387
+ return actualBytes;
388
+ }
389
+
390
+ /** Read image bytes at the given offset */
391
+ private async getBytes(
392
+ offset: number,
393
+ byteCount: number,
394
+ ): Promise<{ mimeType: TiffMimeType; bytes: ArrayBuffer } | null> {
395
+ const mimeType = this.compression;
396
+ if (mimeType == null) throw new Error('Unsupported compression: ' + this.value(TiffTag.Compression));
397
+ if (byteCount === 0) return null;
398
+
399
+ const bytes = await this.tiff.source.fetch(offset, byteCount);
400
+ if (bytes.byteLength < byteCount) {
401
+ throw new Error(`Failed to fetch bytes from offset:${offset} wanted:${byteCount} got:${bytes.byteLength}`);
352
402
  }
353
-
354
- // Clamp the bounds of the output image to the size of the image, as sometimes the edge tiles are not full tiles
355
- getTileBounds(x: number, y: number): BoundingBox {
356
- const { size, tileSize } = this;
357
- const top = y * tileSize.height;
358
- const left = x * tileSize.width;
359
- const width = left + tileSize.width >= size.width ? size.width - left : tileSize.width;
360
- const height = top + tileSize.height >= size.height ? size.height - top : tileSize.height;
361
- return { x: left, y: top, width, height };
403
+ if (this.compression === TiffMimeType.Jpeg) return { mimeType, bytes: this.getJpegHeader(bytes) };
404
+ return { mimeType, bytes };
405
+ }
406
+
407
+ /**
408
+ * Load the tile buffer, this works best with webp
409
+ *
410
+ * This will also apply the JPEG compression tables
411
+ *
412
+ * @param x Tile x offset
413
+ * @param y Tile y offset
414
+ */
415
+ async getTile(x: number, y: number): Promise<{ mimeType: TiffMimeType; bytes: ArrayBuffer } | null> {
416
+ const mimeType = this.compression;
417
+ const size = this.size;
418
+ const tiles = this.tileSize;
419
+
420
+ if (tiles == null) throw new Error('Tiff is not tiled');
421
+ if (mimeType == null) throw new Error('Unsupported compression: ' + this.value(TiffTag.Compression));
422
+
423
+ // TODO support GhostOptionTileOrder
424
+ const nyTiles = Math.ceil(size.height / tiles.height);
425
+ const nxTiles = Math.ceil(size.width / tiles.width);
426
+
427
+ if (x >= nxTiles || y >= nyTiles) {
428
+ throw new Error(`Tile index is outside of range x:${x} >= ${nxTiles} or y:${y} >= ${nyTiles}`);
362
429
  }
363
430
 
364
- /**
365
- * Read a strip into a uint8 array
366
- *
367
- * @param index Strip index to read
368
- */
369
- async getStrip(index: number): Promise<{ mimeType: TiffMimeType; bytes: Uint8Array } | null> {
370
- if (this.isTiled()) throw new Error('Cannot read stripes, tiff is tiled');
371
-
372
- const byteCounts = this.tags.get(TiffTag.StripByteCounts) as CogTiffTagOffset;
373
- const offsets = this.tags.get(TiffTag.StripOffsets) as CogTiffTagOffset;
374
-
375
- if (index >= byteCounts.dataCount) throw new Error('Cannot read strip, index out of bounds');
376
-
377
- const [byteCount, offset] = await Promise.all([offsets.getValueAt(index), byteCounts.getValueAt(index)]);
378
- return this.getBytes(byteCount, offset);
379
- }
380
-
381
- /** The jpeg header is stored in the IFD, read the JPEG header and adjust the byte array to include it */
382
- private getJpegHeader(bytes: Uint8Array): Uint8Array {
383
- // Both the JPEGTable and the Bytes with have the start of image and end of image markers
384
- // StartOfImage 0xffd8 EndOfImage 0xffd9
385
- const tables = this.value<number[]>(TiffTag.JPEGTables);
386
- if (tables == null) throw new Error('Unable to find Jpeg header');
387
-
388
- // Remove EndOfImage marker
389
- const tableData = tables.slice(0, tables.length - 2);
390
- const actualBytes = new Uint8Array(bytes.byteLength + tableData.length - 2);
391
- actualBytes.set(tableData, 0);
392
- actualBytes.set(bytes.slice(2), tableData.length);
393
- return actualBytes;
394
- }
395
-
396
- /** Read image bytes at the given offset */
397
- private async getBytes(
398
- offset: number,
399
- byteCount: number,
400
- ): Promise<{ mimeType: TiffMimeType; bytes: Uint8Array } | null> {
401
- const mimeType = this.compression;
402
- if (mimeType == null) throw new Error('Unsupported compression: ' + this.value(TiffTag.Compression));
403
- if (byteCount === 0) return null;
404
-
405
- await this.tif.source.loadBytes(offset, byteCount);
406
- const bytes = this.tif.source.bytes(offset, byteCount);
407
-
408
- if (this.compression === TiffMimeType.JPEG) {
409
- return { mimeType, bytes: this.getJpegHeader(bytes) };
410
- }
411
- return { mimeType, bytes };
431
+ const idx = y * nxTiles + x;
432
+ const totalTiles = nxTiles * nyTiles;
433
+ if (idx >= totalTiles) throw new Error(`Tile index is outside of tile range: ${idx} >= ${totalTiles}`);
434
+
435
+ const { offset, imageSize } = await this.getTileSize(idx);
436
+ // console.log({ x, y, offset, imageSize });
437
+
438
+ return this.getBytes(offset, imageSize);
439
+ }
440
+
441
+ /**
442
+ * Does this tile exist in the tiff and does it actually have a value
443
+ *
444
+ * Sparse tiffs can have a lot of empty tiles, this checks to see if the tile actually has data.
445
+ *
446
+ * @param x Tile x offset
447
+ * @param y Tile y offset
448
+ * @returns if the tile exists and has data
449
+ */
450
+ async hasTile(x: number, y: number): Promise<boolean> {
451
+ const tiles = this.tileSize;
452
+ const size = this.size;
453
+
454
+ if (tiles == null) throw new Error('Tiff is not tiled');
455
+
456
+ // TODO support GhostOptionTileOrder
457
+ const nyTiles = Math.ceil(size.height / tiles.height);
458
+ const nxTiles = Math.ceil(size.width / tiles.width);
459
+ if (x >= nxTiles || y >= nyTiles) return false;
460
+ const idx = y * nxTiles + x;
461
+ const ret = await this.getTileSize(idx);
462
+ return ret.offset > 0;
463
+ }
464
+
465
+ async getTileSize(index: number): Promise<{ offset: number; imageSize: number }> {
466
+ // GDAL optimizes tiles by storing the size of the tile in
467
+ // the few bytes leading up to the tile
468
+ const leaderBytes = this.tiff.options?.tileLeaderByteSize;
469
+ if (leaderBytes) {
470
+ const offset = await getOffset(this.tiff, this.tileOffset, index);
471
+ // Sparse COG no data found
472
+ if (offset === 0) return { offset: 0, imageSize: 0 };
473
+
474
+ // This fetch will generally load in the bytes needed for the image too
475
+ // provided the image size is less than the size of a chunk
476
+ const bytes = await this.tiff.source.fetch(offset - leaderBytes, leaderBytes);
477
+ return { offset, imageSize: getUint(new DataView(bytes), 0, leaderBytes, this.tiff.isLittleEndian) };
412
478
  }
413
479
 
414
- /**
415
- * Load the tile buffer, this works best with webp
416
- *
417
- * This will also apply the JPEG compression tables
418
- *
419
- * @param x Tile x offset
420
- * @param y Tile y offset
421
- */
422
- async getTile(x: number, y: number): Promise<{ mimeType: TiffMimeType; bytes: Uint8Array } | null> {
423
- const mimeType = this.compression;
424
- const size = this.size;
425
- const tiles = this.tileSize;
426
-
427
- if (tiles == null) throw new Error('Tiff is not tiled');
428
- if (mimeType == null) throw new Error('Unsupported compression: ' + this.value(TiffTag.Compression));
429
-
430
- // TODO support GhostOptionTileOrder
431
- const nyTiles = Math.ceil(size.height / tiles.height);
432
- const nxTiles = Math.ceil(size.width / tiles.width);
433
-
434
- if (x >= nxTiles || y >= nyTiles) {
435
- throw new Error(`Tile index is outside of range x:${x} >= ${nxTiles} or y:${y} >= ${nyTiles}`);
436
- }
437
-
438
- const idx = y * nxTiles + x;
439
- const totalTiles = nxTiles * nyTiles;
440
- if (idx >= totalTiles) throw new Error(`Tile index is outside of tile range: ${idx} >= ${totalTiles}`);
441
-
442
- const { offset, imageSize } = await this.getTileSize(idx);
443
- return this.getBytes(offset, imageSize);
444
- }
445
-
446
- /**
447
- * Does this tile exist in the tiff and does it actually have a value
448
- *
449
- * Sparse tiffs can have a lot of empty tiles, this checks to see if the tile actually has data.
450
- *
451
- * @param x Tile x offset
452
- * @param y Tile y offset
453
- * @returns if the tile exists and has data
454
- */
455
- async hasTile(x: number, y: number): Promise<boolean> {
456
- const tiles = this.tileSize;
457
- const size = this.size;
458
-
459
- if (tiles == null) throw new Error('Tiff is not tiled');
460
-
461
- // TODO support GhostOptionTileOrder
462
- const nyTiles = Math.ceil(size.height / tiles.height);
463
- const nxTiles = Math.ceil(size.width / tiles.width);
464
- if (x >= nxTiles || y >= nyTiles) return false;
465
- const idx = y * nxTiles + x;
466
- const ret = await this.getTileSize(idx);
467
- return ret.offset > 0;
468
- }
480
+ const byteCounts = this.tags.get(TiffTag.TileByteCounts) as TagOffset;
481
+ if (byteCounts == null) throw new Error('No tile byte counts found');
482
+ const [offset, imageSize] = await Promise.all([
483
+ getOffset(this.tiff, this.tileOffset, index),
484
+ getOffset(this.tiff, byteCounts, index),
485
+ ]);
486
+ return { offset, imageSize };
487
+ }
488
+ }
469
489
 
470
- protected async getTileSize(index: number): Promise<{ offset: number; imageSize: number }> {
471
- // GDAL optimizes tiles by storing the size of the tile in
472
- // the few bytes leading up to the tile
473
- const leaderBytes = this.tif.options.tileLeaderByteSize;
474
- if (leaderBytes) {
475
- const offset = await this.getTileOffset(index);
476
- // Sparse COG no data found
477
- if (offset === 0) return { offset: 0, imageSize: 0 };
478
-
479
- // This fetch will generally load in the bytes needed for the image too
480
- // provided the image size is less than the size of a chunk
481
- await this.tif.source.loadBytes(offset - leaderBytes, leaderBytes);
482
- return { offset, imageSize: this.tif.source.getUint(offset - leaderBytes, leaderBytes) };
483
- }
484
-
485
- const byteCounts = this.tags.get(TiffTag.TileByteCounts) as CogTiffTagOffset;
486
- if (byteCounts == null) {
487
- throw new Error('No tile byte counts found');
488
- }
489
- const [offset, imageSize] = await Promise.all([this.getTileOffset(index), byteCounts.getValueAt(index)]);
490
- return { offset, imageSize };
491
- }
490
+ function getOffset(
491
+ tiff: CogTiff,
492
+ x: TagOffset | TagInline<number | number[]>,
493
+ index: number,
494
+ ): number | Promise<number> {
495
+ if (index > x.count || index < 0) throw new Error('TagIndex: out of bounds ' + x.id + ' @ ' + index);
496
+ if (x.type === 'inline') {
497
+ if (Array.isArray(x.value)) return x.value[index] as number;
498
+ return x.value as number;
499
+ }
500
+ return getValueAt(tiff, x, index);
492
501
  }