@cogeotiff/core 7.2.0 → 8.0.1

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 +3 -500
  2. package/README.md +28 -10
  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 +16 -28
  23. package/build/cog.tiff.image.js +147 -101
  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 +5 -5
  37. package/build/const/tiff.tag.id.js +155 -154
  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 +37 -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 -448
  86. package/src/cog.tiff.ts +143 -146
  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 +159 -158
  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 +38 -39
  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 -10
  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,500 @@
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
- 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';
4
+ import { TagId, TagGeoId } from './const/tiff.tag.id.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
+ TagId.Compression,
25
+ TagId.ImageHeight,
26
+ TagId.ImageWidth,
27
+ TagId.ModelPixelScale,
28
+ TagId.ModelTiePoint,
29
+ TagId.ModelTransformation,
30
+ TagId.TileHeight,
31
+ TagId.TileWidth,
32
+ TagId.GeoKeyDirectory,
33
+ TagId.GeoAsciiParams,
34
+ TagId.GeoDoubleParams,
35
+ TagId.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<TagId, 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<TagGeoId, string | number> = new Map();
61
+
62
+ constructor(tiff: CogTiff, id: number, tags: Map<TagId, 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(TagId.Compression),
76
+ this.fetch(TagId.ImageHeight),
77
+ this.fetch(TagId.ImageWidth),
78
+ this.fetch(TagId.ModelPixelScale),
79
+ this.fetch(TagId.ModelTiePoint),
80
+ this.fetch(TagId.ModelTransformation),
81
+ this.fetch(TagId.TileHeight),
82
+ this.fetch(TagId.TileWidth),
83
+ ];
84
+
85
+ if (loadGeoTags) {
86
+ requiredTags.push(this.fetch(TagId.GeoKeyDirectory));
87
+ requiredTags.push(this.fetch(TagId.GeoAsciiParams));
88
+ requiredTags.push(this.fetch(TagId.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: TagId): 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(TagId.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(TagId.GeoKeyDirectory),
120
+ this.fetch(TagId.GeoAsciiParams),
121
+ this.fetch(TagId.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 TagGeoId;
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: TagGeoId): 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: TagId): 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[]>(TagId.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[]>(TagId.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(TagId.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 = this.value(TagId.ModelPixelScale) != null || this.value(TagId.ModelTransformation) != null;
197
+ if (isImageLocated) return true;
198
+ // If this is a sub image, use the isGeoLocated from the top level image
199
+ if (this.value(TagId.NewSubFileType) === 1 && this.id !== 0) return this.tiff.images[0].isGeoLocated;
200
+ return false;
201
+ }
202
+
203
+ /**
204
+ * Get the resolution of the image
205
+ *
206
+ * @returns [x,y,z] pixel scale
207
+ */
208
+ get resolution(): [number, number, number] {
209
+ const modelPixelScale: number[] | null = this.value(TagId.ModelPixelScale);
210
+ if (modelPixelScale != null) {
211
+ return [modelPixelScale[0], -modelPixelScale[1], modelPixelScale[2]];
299
212
  }
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 };
213
+ const modelTransformation: number[] | null = this.value(TagId.ModelTransformation);
214
+ if (modelTransformation != null) {
215
+ return [modelTransformation[0], modelTransformation[5], modelTransformation[10]];
310
216
  }
311
217
 
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;
218
+ // If this is a sub image, use the resolution from the top level image
219
+ if (this.value(TagId.NewSubFileType) === 1 && this.id !== 0) {
220
+ const firstImg = this.tiff.images[0];
221
+ const [resX, resY, resZ] = firstImg.resolution;
222
+ const firstImgSize = firstImg.size;
223
+ const imgSize = this.size;
224
+ // scale resolution based on the size difference between the two images
225
+ return [(resX * firstImgSize.width) / imgSize.width, (resY * firstImgSize.height) / imgSize.height, resZ];
323
226
  }
324
227
 
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;
228
+ throw new Error('Image does not have a geo transformation.');
229
+ }
230
+
231
+ /**
232
+ * Bounding box of the image
233
+ *
234
+ * @returns [minX, minY, maxX, maxY] bounding box
235
+ */
236
+ get bbox(): [number, number, number, number] {
237
+ const size = this.size;
238
+ const origin = this.origin;
239
+ const resolution = this.resolution;
240
+
241
+ if (origin == null || size == null || resolution == null) {
242
+ throw new Error('Unable to calculate bounding box');
336
243
  }
337
244
 
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);
245
+ const x1 = origin[0];
246
+ const y1 = origin[1];
247
+
248
+ const x2 = x1 + resolution[0] * size.width;
249
+ const y2 = y1 + resolution[1] * size.height;
250
+
251
+ return [Math.min(x1, x2), Math.min(y1, y2), Math.max(x1, x2), Math.max(y1, y2)];
252
+ }
253
+
254
+ /**
255
+ * Get the compression used by the tile
256
+ *
257
+ * @see TiffCompression
258
+ *
259
+ * @returns Compression type eg webp
260
+ */
261
+ get compression(): TiffMimeType | null {
262
+ const compression = this.value(TagId.Compression);
263
+ if (compression == null || typeof compression !== 'number') return null;
264
+ return TiffCompression[compression];
265
+ }
266
+
267
+ /**
268
+ * Attempt to read the EPSG Code from TiffGeoTags
269
+ *
270
+ * @returns EPSG Code if it exists
271
+ */
272
+ get epsg(): number | null {
273
+ const projection = this.valueGeo(TagGeoId.ProjectedCSTypeGeoKey) as number;
274
+ if (projection === InvalidProjectionCode) return null;
275
+ return projection;
276
+ }
277
+
278
+ /**
279
+ * Get the size of the image
280
+ *
281
+ * @returns Size in pixels
282
+ */
283
+ get size(): Size {
284
+ return {
285
+ width: this.value<number>(TagId.ImageWidth) as number,
286
+ height: this.value<number>(TagId.ImageHeight) as number,
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Determine if this image is tiled
292
+ */
293
+ public isTiled(): boolean {
294
+ return this.value(TagId.TileWidth) !== null;
295
+ }
296
+
297
+ /**
298
+ * Get size of individual tiles
299
+ */
300
+ get tileSize(): CogTiffImageTileSize {
301
+ return {
302
+ width: this.value<number>(TagId.TileWidth) as number,
303
+ height: this.value<number>(TagId.TileHeight) as number,
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Number of tiles used to create this image
309
+ */
310
+ get tileCount(): CogTiffImageTiledCount {
311
+ const size = this.size;
312
+ const tileSize = this.tileSize;
313
+ const x = Math.ceil(size.width / tileSize.width);
314
+ const y = Math.ceil(size.height / tileSize.height);
315
+ return { x, y };
316
+ }
317
+
318
+ /**
319
+ * Get the pointer to where the tiles start in the Tiff file
320
+ *
321
+ * @remarks Used to read tiled tiffs
322
+ *
323
+ * @returns file offset to where the tiffs are stored
324
+ */
325
+ get tileOffset(): TagOffset {
326
+ const tileOffset = this.tags.get(TagId.TileOffsets) as TagOffset;
327
+ if (tileOffset == null) throw new Error('No tile offsets found');
328
+ return tileOffset;
329
+ }
330
+
331
+ /**
332
+ * Get the number of strip's inside this tiff
333
+ *
334
+ * @remarks Used to read striped tiffs
335
+ *
336
+ * @returns number of strips present
337
+ */
338
+ get stripCount(): number {
339
+ const tileOffset = this.tags.get(TagId.StripByteCounts) as TagOffset;
340
+ if (tileOffset == null) return 0;
341
+ return tileOffset.count;
342
+ }
343
+
344
+ // Clamp the bounds of the output image to the size of the image, as sometimes the edge tiles are not full tiles
345
+ getTileBounds(x: number, y: number): BoundingBox {
346
+ const { size, tileSize } = this;
347
+ const top = y * tileSize.height;
348
+ const left = x * tileSize.width;
349
+ const width = left + tileSize.width >= size.width ? size.width - left : tileSize.width;
350
+ const height = top + tileSize.height >= size.height ? size.height - top : tileSize.height;
351
+ return { x: left, y: top, width, height };
352
+ }
353
+
354
+ /**
355
+ * Read a strip into a uint8 array
356
+ *
357
+ * @param index Strip index to read
358
+ */
359
+ async getStrip(index: number): Promise<{ mimeType: TiffMimeType; bytes: ArrayBuffer } | null> {
360
+ if (this.isTiled()) throw new Error('Cannot read stripes, tiff is tiled: ' + index);
361
+
362
+ const byteCounts = this.tags.get(TagId.StripByteCounts) as TagOffset;
363
+ const offsets = this.tags.get(TagId.StripOffsets) as TagOffset;
364
+
365
+ if (index >= byteCounts.count) throw new Error('Cannot read strip, index out of bounds');
366
+
367
+ const [byteCount, offset] = await Promise.all([
368
+ getOffset(this.tiff, offsets, index),
369
+ getOffset(this.tiff, byteCounts, index),
370
+ ]);
371
+ return this.getBytes(byteCount, offset);
372
+ }
373
+
374
+ /** The jpeg header is stored in the IFD, read the JPEG header and adjust the byte array to include it */
375
+ private getJpegHeader(bytes: ArrayBuffer): ArrayBuffer {
376
+ // Both the JPEGTable and the Bytes with have the start of image and end of image markers
377
+ // StartOfImage 0xffd8 EndOfImage 0xffd9
378
+ const tables = this.value<number[]>(TagId.JPEGTables);
379
+ if (tables == null) throw new Error('Unable to find Jpeg header');
380
+
381
+ // Remove EndOfImage marker
382
+ const tableData = tables.slice(0, tables.length - 2);
383
+ const actualBytes = new Uint8Array(bytes.byteLength + tableData.length - 2);
384
+ actualBytes.set(tableData, 0);
385
+ actualBytes.set(new Uint8Array(bytes).slice(2), tableData.length);
386
+ return actualBytes;
387
+ }
388
+
389
+ /** Read image bytes at the given offset */
390
+ private async getBytes(
391
+ offset: number,
392
+ byteCount: number,
393
+ ): Promise<{ mimeType: TiffMimeType; bytes: ArrayBuffer } | null> {
394
+ const mimeType = this.compression;
395
+ if (mimeType == null) throw new Error('Unsupported compression: ' + this.value(TagId.Compression));
396
+ if (byteCount === 0) return null;
397
+
398
+ const bytes = await this.tiff.source.fetch(offset, byteCount);
399
+ if (bytes.byteLength < byteCount) {
400
+ throw new Error(`Failed to fetch bytes from offset:${offset} wanted:${byteCount} got:${bytes.byteLength}`);
352
401
  }
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 };
402
+ if (this.compression === TiffMimeType.Jpeg) return { mimeType, bytes: this.getJpegHeader(bytes) };
403
+ return { mimeType, bytes };
404
+ }
405
+
406
+ /**
407
+ * Load the tile buffer, this works best with webp
408
+ *
409
+ * This will also apply the JPEG compression tables
410
+ *
411
+ * @param x Tile x offset
412
+ * @param y Tile y offset
413
+ */
414
+ async getTile(x: number, y: number): Promise<{ mimeType: TiffMimeType; bytes: ArrayBuffer } | null> {
415
+ const mimeType = this.compression;
416
+ const size = this.size;
417
+ const tiles = this.tileSize;
418
+
419
+ if (tiles == null) throw new Error('Tiff is not tiled');
420
+ if (mimeType == null) throw new Error('Unsupported compression: ' + this.value(TagId.Compression));
421
+
422
+ // TODO support GhostOptionTileOrder
423
+ const nyTiles = Math.ceil(size.height / tiles.height);
424
+ const nxTiles = Math.ceil(size.width / tiles.width);
425
+
426
+ if (x >= nxTiles || y >= nyTiles) {
427
+ throw new Error(`Tile index is outside of range x:${x} >= ${nxTiles} or y:${y} >= ${nyTiles}`);
362
428
  }
363
429
 
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 };
430
+ const idx = y * nxTiles + x;
431
+ const totalTiles = nxTiles * nyTiles;
432
+ if (idx >= totalTiles) throw new Error(`Tile index is outside of tile range: ${idx} >= ${totalTiles}`);
433
+
434
+ const { offset, imageSize } = await this.getTileSize(idx);
435
+ // console.log({ x, y, offset, imageSize });
436
+
437
+ return this.getBytes(offset, imageSize);
438
+ }
439
+
440
+ /**
441
+ * Does this tile exist in the tiff and does it actually have a value
442
+ *
443
+ * Sparse tiffs can have a lot of empty tiles, this checks to see if the tile actually has data.
444
+ *
445
+ * @param x Tile x offset
446
+ * @param y Tile y offset
447
+ * @returns if the tile exists and has data
448
+ */
449
+ async hasTile(x: number, y: number): Promise<boolean> {
450
+ const tiles = this.tileSize;
451
+ const size = this.size;
452
+
453
+ if (tiles == null) throw new Error('Tiff is not tiled');
454
+
455
+ // TODO support GhostOptionTileOrder
456
+ const nyTiles = Math.ceil(size.height / tiles.height);
457
+ const nxTiles = Math.ceil(size.width / tiles.width);
458
+ if (x >= nxTiles || y >= nyTiles) return false;
459
+ const idx = y * nxTiles + x;
460
+ const ret = await this.getTileSize(idx);
461
+ return ret.offset > 0;
462
+ }
463
+
464
+ async getTileSize(index: number): Promise<{ offset: number; imageSize: number }> {
465
+ // GDAL optimizes tiles by storing the size of the tile in
466
+ // the few bytes leading up to the tile
467
+ const leaderBytes = this.tiff.options?.tileLeaderByteSize;
468
+ if (leaderBytes) {
469
+ const offset = await getOffset(this.tiff, this.tileOffset, index);
470
+ // Sparse COG no data found
471
+ if (offset === 0) return { offset: 0, imageSize: 0 };
472
+
473
+ // This fetch will generally load in the bytes needed for the image too
474
+ // provided the image size is less than the size of a chunk
475
+ const bytes = await this.tiff.source.fetch(offset - leaderBytes, leaderBytes);
476
+ return { offset, imageSize: getUint(new DataView(bytes), 0, leaderBytes, this.tiff.isLittleEndian) };
412
477
  }
413
478
 
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
- }
479
+ const byteCounts = this.tags.get(TagId.TileByteCounts) as TagOffset;
480
+ if (byteCounts == null) throw new Error('No tile byte counts found');
481
+ const [offset, imageSize] = await Promise.all([
482
+ getOffset(this.tiff, this.tileOffset, index),
483
+ getOffset(this.tiff, byteCounts, index),
484
+ ]);
485
+ return { offset, imageSize };
486
+ }
487
+ }
469
488
 
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
- }
489
+ function getOffset(
490
+ tiff: CogTiff,
491
+ x: TagOffset | TagInline<number | number[]>,
492
+ index: number,
493
+ ): number | Promise<number> {
494
+ if (index > x.count || index < 0) throw new Error('TagIndex: out of bounds ' + x.id + ' @ ' + index);
495
+ if (x.type === 'inline') {
496
+ if (Array.isArray(x.value)) return x.value[index] as number;
497
+ return x.value as number;
498
+ }
499
+ return getValueAt(tiff, x, index);
492
500
  }