@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.
- package/CHANGELOG.md +3 -500
- package/README.md +28 -10
- package/build/__benchmark__/cog.read.benchmark.d.ts +0 -1
- package/build/__benchmark__/cog.read.benchmark.js +6 -4
- package/build/__benchmark__/cog.read.benchmark.js.map +1 -0
- package/build/__benchmark__/source.file.d.ts +9 -0
- package/build/__benchmark__/source.file.js +29 -0
- package/build/__benchmark__/source.file.js.map +1 -0
- package/build/__benchmark__/source.memory.d.ts +9 -0
- package/build/__benchmark__/source.memory.js +32 -0
- package/build/__benchmark__/source.memory.js.map +1 -0
- package/build/__test__/cog.image.test.d.ts +1 -2
- package/build/__test__/cog.image.test.js +82 -90
- package/build/__test__/cog.image.test.js.map +1 -0
- package/build/__test__/cog.read.test.d.ts +1 -2
- package/build/__test__/cog.read.test.js +37 -55
- package/build/__test__/cog.read.test.js.map +1 -0
- package/build/__test__/example.d.ts +1 -0
- package/build/__test__/example.js +27 -0
- package/build/__test__/example.js.map +1 -0
- package/build/cog.tiff.d.ts +22 -28
- package/build/cog.tiff.image.d.ts +16 -28
- package/build/cog.tiff.image.js +147 -101
- package/build/cog.tiff.image.js.map +1 -0
- package/build/cog.tiff.js +141 -97
- package/build/cog.tiff.js.map +1 -0
- package/build/const/index.d.ts +1 -2
- package/build/const/index.js +2 -2
- package/build/const/index.js.map +1 -0
- package/build/const/tiff.endian.d.ts +0 -1
- package/build/const/tiff.endian.js +1 -1
- package/build/const/tiff.endian.js.map +1 -0
- package/build/const/tiff.mime.d.ts +10 -11
- package/build/const/tiff.mime.js +22 -22
- package/build/const/tiff.mime.js.map +1 -0
- package/build/const/tiff.tag.id.d.ts +5 -5
- package/build/const/tiff.tag.id.js +155 -154
- package/build/const/tiff.tag.id.js.map +1 -0
- package/build/const/tiff.tag.value.d.ts +15 -16
- package/build/const/tiff.tag.value.js +16 -16
- package/build/const/tiff.tag.value.js.map +1 -0
- package/build/const/tiff.version.d.ts +0 -1
- package/build/const/tiff.version.js +1 -1
- package/build/const/tiff.version.js.map +1 -0
- package/build/index.d.ts +11 -6
- package/build/index.js +9 -5
- package/build/index.js.map +1 -0
- package/build/read/data.view.offset.d.ts +15 -0
- package/build/read/data.view.offset.js +19 -0
- package/build/read/data.view.offset.js.map +1 -0
- package/build/read/tiff.gdal.d.ts +9 -12
- package/build/read/tiff.gdal.js +22 -17
- package/build/read/tiff.gdal.js.map +1 -0
- package/build/read/tiff.ifd.config.d.ts +6 -4
- package/build/read/tiff.ifd.config.js +2 -2
- package/build/read/tiff.ifd.config.js.map +1 -0
- package/build/read/tiff.tag.d.ts +37 -20
- package/build/read/tiff.tag.factory.d.ts +16 -0
- package/build/read/tiff.tag.factory.js +130 -0
- package/build/read/tiff.tag.factory.js.map +1 -0
- package/build/read/tiff.tag.js +2 -37
- package/build/read/tiff.tag.js.map +1 -0
- package/build/read/tiff.value.reader.d.ts +2 -6
- package/build/read/tiff.value.reader.js +16 -54
- package/build/read/tiff.value.reader.js.map +1 -0
- package/build/source.d.ts +5 -0
- package/build/source.js +2 -0
- package/build/source.js.map +1 -0
- package/build/util/bytes.d.ts +17 -0
- package/build/util/bytes.js +42 -0
- package/build/util/bytes.js.map +1 -0
- package/build/util/util.hex.d.ts +2 -3
- package/build/util/util.hex.js +4 -5
- package/build/util/util.hex.js.map +1 -0
- package/build/vector.d.ts +0 -1
- package/build/vector.js +1 -1
- package/build/vector.js.map +1 -0
- package/package.json +25 -30
- package/src/__benchmark__/cog.read.benchmark.ts +12 -10
- package/src/__benchmark__/source.file.ts +23 -0
- package/src/__benchmark__/source.memory.ts +23 -0
- package/src/__test__/cog.image.test.ts +188 -197
- package/src/__test__/cog.read.test.ts +50 -72
- package/src/__test__/example.ts +31 -0
- package/src/cog.tiff.image.ts +456 -448
- package/src/cog.tiff.ts +143 -146
- package/src/const/index.ts +1 -1
- package/src/const/tiff.endian.ts +2 -2
- package/src/const/tiff.mime.ts +21 -21
- package/src/const/tiff.tag.id.ts +159 -158
- package/src/const/tiff.tag.value.ts +16 -16
- package/src/const/tiff.version.ts +11 -11
- package/src/index.ts +11 -5
- package/src/read/data.view.offset.ts +23 -0
- package/src/read/tiff.gdal.ts +61 -63
- package/src/read/tiff.ifd.config.ts +35 -31
- package/src/read/tiff.tag.factory.ts +163 -0
- package/src/read/tiff.tag.ts +38 -39
- package/src/read/tiff.value.reader.ts +25 -73
- package/src/source.ts +5 -0
- package/src/util/bytes.ts +44 -0
- package/src/util/util.hex.ts +5 -7
- package/src/vector.ts +5 -5
- package/tsconfig.json +8 -10
- package/build/__benchmark__/cog.read.benchmark.d.ts.map +0 -1
- package/build/__test__/cog.image.test.d.ts.map +0 -1
- package/build/__test__/cog.read.test.d.ts.map +0 -1
- package/build/cog.tiff.d.ts.map +0 -1
- package/build/cog.tiff.image.d.ts.map +0 -1
- package/build/const/index.d.ts.map +0 -1
- package/build/const/tiff.endian.d.ts.map +0 -1
- package/build/const/tiff.mime.d.ts.map +0 -1
- package/build/const/tiff.tag.id.d.ts.map +0 -1
- package/build/const/tiff.tag.value.d.ts.map +0 -1
- package/build/const/tiff.version.d.ts.map +0 -1
- package/build/index.d.ts.map +0 -1
- package/build/read/tag/__test__/tag.test.d.ts +0 -2
- package/build/read/tag/__test__/tag.test.d.ts.map +0 -1
- package/build/read/tag/__test__/tag.test.js +0 -23
- package/build/read/tag/tiff.tag.base.d.ts +0 -55
- package/build/read/tag/tiff.tag.base.d.ts.map +0 -1
- package/build/read/tag/tiff.tag.base.js +0 -79
- package/build/read/tag/tiff.tag.lazy.d.ts +0 -7
- package/build/read/tag/tiff.tag.lazy.d.ts.map +0 -1
- package/build/read/tag/tiff.tag.lazy.js +0 -18
- package/build/read/tag/tiff.tag.offset.d.ts +0 -21
- package/build/read/tag/tiff.tag.offset.d.ts.map +0 -1
- package/build/read/tag/tiff.tag.offset.js +0 -54
- package/build/read/tag/tiff.tag.static.d.ts +0 -8
- package/build/read/tag/tiff.tag.static.d.ts.map +0 -1
- package/build/read/tag/tiff.tag.static.js +0 -17
- package/build/read/tiff.gdal.d.ts.map +0 -1
- package/build/read/tiff.ifd.config.d.ts.map +0 -1
- package/build/read/tiff.tag.d.ts.map +0 -1
- package/build/read/tiff.value.reader.d.ts.map +0 -1
- package/build/source/cog.source.view.d.ts +0 -33
- package/build/source/cog.source.view.d.ts.map +0 -1
- package/build/source/cog.source.view.js +0 -65
- package/build/util/util.hex.d.ts.map +0 -1
- package/build/vector.d.ts.map +0 -1
- package/src/@types/ieee754.d.ts +0 -18
- package/src/read/tag/__test__/tag.test.ts +0 -27
- package/src/read/tag/tiff.tag.base.ts +0 -126
- package/src/read/tag/tiff.tag.lazy.ts +0 -17
- package/src/read/tag/tiff.tag.offset.ts +0 -61
- package/src/read/tag/tiff.tag.static.ts +0 -15
- package/src/source/cog.source.view.ts +0 -77
package/src/cog.tiff.image.ts
CHANGED
|
@@ -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 {
|
|
4
|
-
import {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
/** Tile width (pixels) */
|
|
43
|
+
width: number;
|
|
44
|
+
/** Tile height (pixels) */
|
|
45
|
+
height: number;
|
|
31
46
|
}
|
|
32
47
|
|
|
33
48
|
export class CogTiffImage {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
}
|