cog-tiler-wasm 0.3.0 → 0.3.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.
package/cog-tiler.js CHANGED
@@ -25,6 +25,7 @@ import initTiler, { colorize, colormap_names } from "./cog_tiler_wasm.js";
25
25
  import proj4 from "proj4";
26
26
  import * as GeoTIFF from "geotiff";
27
27
  import geokeysToProj4 from "geotiff-geokeys-to-proj4";
28
+ import { sampleWindowBilinear } from "./sampling.js";
28
29
 
29
30
  const OS = 20037508.342789244; // Web Mercator half-extent (m)
30
31
  const TILE = 256; // output tile size (px)
@@ -224,22 +225,6 @@ function bilin(v00, v10, v01, v11, tx, ty) {
224
225
  return top + (bot - top) * ty;
225
226
  }
226
227
 
227
- // Bilinear sample of a row-major window at fractional (fc,fr). Returns NaN when
228
- // the cell center is outside the window so out-of-raster pixels stay transparent
229
- // (no edge smear); falls back to nearest at the window edge / next to nodata.
230
- function sampleWindowBilinear(buf, w, h, fc, fr) {
231
- const c0 = Math.floor(fc), r0 = Math.floor(fr);
232
- if (c0 < 0 || c0 >= w || r0 < 0 || r0 >= h) return NaN;
233
- const c1 = Math.min(c0 + 1, w - 1), r1 = Math.min(r0 + 1, h - 1);
234
- const v00 = buf[r0 * w + c0];
235
- if (Number.isNaN(v00)) return NaN;
236
- const v10 = buf[r0 * w + c1], v01 = buf[r1 * w + c0], v11 = buf[r1 * w + c1];
237
- if (Number.isNaN(v10) || Number.isNaN(v01) || Number.isNaN(v11)) return v00; // edge/nodata
238
- const tx = fc - c0, ty = fr - r0;
239
- const top = v00 + (v10 - v00) * tx, bot = v01 + (v11 - v01) * tx;
240
- return top + (bot - top) * ty;
241
- }
242
-
243
228
  // Build a byte reader from a URL string, a Blob/File, or an in-memory source
244
229
  // (ArrayBuffer, Uint8Array). `range(a,b)` yields bytes [a..b]; `openTiff()`
245
230
  // returns a geotiff.js GeoTIFF for reading the CRS / color table from the header.
@@ -290,6 +275,11 @@ async function makeReader(source) {
290
275
  export async function openCog(source) {
291
276
  await init();
292
277
  const { range, openTiff, label } = await makeReader(source);
278
+ // whitebox-wasm's streaming decoder currently materializes numeric tile
279
+ // samples as little-endian. Detect Motorola TIFFs up front so their pixels
280
+ // can be decoded by geotiff.js, which honors the header byte order.
281
+ const byteOrder = await range(0, 1);
282
+ const bigEndian = byteOrder[0] === 0x4d && byteOrder[1] === 0x4d;
293
283
  // Parse the COG header; grow the prefix and retry for large COGs whose IFDs
294
284
  // exceed 64 KB (many overviews / huge tile-offset arrays).
295
285
  let stream;
@@ -301,7 +291,7 @@ export async function openCog(source) {
301
291
  if (len >= 1 << 25) throw e; // give up past ~32 MB
302
292
  }
303
293
  }
304
- const gt = stream.geo_transform(); // [x0, px_w, rot, y0, rot, px_h]
294
+ let gt = stream.geo_transform(); // [x0, px_w, rot, y0, rot, px_h]
305
295
  const levels = JSON.parse(stream.levels_json());
306
296
  if (!Array.isArray(levels) || levels.length === 0) {
307
297
  throw new Error("levels_json() returned no levels");
@@ -312,12 +302,27 @@ export async function openCog(source) {
312
302
  // through geotiff.js instead (see _assembleWindow / point).
313
303
  const multiBand = levels[0].bands > 1;
314
304
  let tiff = null, img = null, planar = false;
315
- if (stream.epsg !== 3857 || multiBand) {
305
+ if (stream.epsg !== 3857 || multiBand || bigEndian) {
316
306
  tiff = await openTiff();
317
307
  img = await tiff.getImage();
318
308
  planar = multiBand && (await readTiffTag(img, "PlanarConfiguration")) === 2;
309
+ if (bigEndian && gt.length !== 6) {
310
+ const [x, y] = img.getOrigin();
311
+ const [pixelWidth, pixelHeight] = img.getResolution();
312
+ gt = [x, pixelWidth, 0, y, 0, -pixelHeight];
313
+ }
319
314
  }
320
- const base = { url: label, range, stream, levels, gt, nodata: stream.nodata, tiff, planar };
315
+ const base = {
316
+ url: label,
317
+ range,
318
+ stream,
319
+ levels,
320
+ gt,
321
+ nodata: stream.nodata,
322
+ tiff,
323
+ planar,
324
+ bigEndian,
325
+ };
321
326
 
322
327
  if (stream.epsg === 3857) {
323
328
  return new CogSource({
@@ -425,10 +430,11 @@ export class CogSource {
425
430
 
426
431
  // Fetch + decode band `band` (0-based) over a level pixel window into a
427
432
  // row-major buffer. Chunky COGs go through whitebox (cached, NaN for gaps);
428
- // planar (INTERLEAVE=BAND) COGs are read per-band via geotiff.js, which
429
- // whitebox's chunky-only streaming decoder can't address.
433
+ // Planar (INTERLEAVE=BAND) and big-endian COGs are read per-band via
434
+ // geotiff.js. Whitebox's streaming decoder cannot address planar tiles and
435
+ // currently interprets multi-byte tile samples as little-endian.
430
436
  async _assembleWindow(level, x, y, w, h, band = 0) {
431
- if (this.planar) {
437
+ if (this.planar || this.bigEndian) {
432
438
  const img = await this._tiffImage(level);
433
439
  const rasters = await img.readRasters({ window: [x, y, x + w, y + h], samples: [band] });
434
440
  return rasters[0]; // typed array, length w*h, row-major
@@ -569,7 +575,7 @@ export class CogSource {
569
575
  // RGB composite: bilinear-sample each band, rescale -> curve -> gamma.
570
576
  let ok = true;
571
577
  for (let k = 0; k < 3; k++) {
572
- const v = sampleWindowBilinear(bufs[k], ww, hh, fcol, frow);
578
+ const v = sampleWindowBilinear(bufs[k], ww, hh, fcol, frow, ndSet ? nodata : undefined);
573
579
  if (!isFinite(v) || (ndSet && v === nodata)) { ok = false; break; }
574
580
  const [mn, mx] = rescales[k] || rescales[0];
575
581
  const t = transferCurve(Math.max(0, Math.min(1, (v - mn) / ((mx - mn) || 1))), stretch, gamma);
@@ -579,7 +585,7 @@ export class CogSource {
579
585
  else { out[o] = out[o + 1] = out[o + 2] = 0; }
580
586
  } else {
581
587
  // Continuous single band: bilinear; colormap applied below.
582
- const v = sampleWindowBilinear(bufs[0], ww, hh, fcol, frow);
588
+ const v = sampleWindowBilinear(bufs[0], ww, hh, fcol, frow, ndSet ? nodata : undefined);
583
589
  if (!isFinite(v) || (ndSet && v === nodata)) continue;
584
590
  grid[py * outW + px] = v;
585
591
  }
Binary file
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "Qiusheng Wu <giswqs@gmail.com>"
6
6
  ],
7
7
  "description": "Serverless Cloud Optimized GeoTIFF (COG) dynamic tiling in WebAssembly. TiTiler-style XYZ tiles, no backend.",
8
- "version": "0.3.0",
8
+ "version": "0.3.2",
9
9
  "license": "MIT",
10
10
  "repository": {
11
11
  "type": "git",
@@ -16,6 +16,7 @@
16
16
  "cog_tiler_wasm.js",
17
17
  "cog_tiler_wasm.d.ts",
18
18
  "cog-tiler.js",
19
+ "sampling.js",
19
20
  "cog-tiler.d.ts",
20
21
  "README.md",
21
22
  "LICENSE"
package/sampling.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Pure resampling helpers, kept dependency-free so they can be unit-tested
3
+ * without loading the wasm/geotiff stack that `cog-tiler.js` pulls in.
4
+ */
5
+
6
+ // Bilinear sample of a row-major window at fractional (fc,fr). Returns NaN when
7
+ // the cell center is outside the window so out-of-raster pixels stay transparent
8
+ // (no edge smear); falls back to nearest at the window edge / next to nodata.
9
+ //
10
+ // `nodata` is the declared sentinel, when the dataset has one. It must be tested
11
+ // here rather than only on the result: interpolating across the boundary blends
12
+ // the sentinel into the value, and the product matches neither the sentinel nor
13
+ // NaN, so a caller's `v === nodata` check cannot recognize it. With a large
14
+ // negative sentinel (-9999 is the GDAL convention) the blend lands far below the
15
+ // rescale window, clamps to its low end, and paints a ring of the colormap's
16
+ // first color around every nodata boundary -- e.g. a blue border along a swath
17
+ // edge under `jet`. Treating a sentinel neighbor exactly like a NaN one keeps
18
+ // the boundary pixel at its own valid value instead.
19
+ export function sampleWindowBilinear(buf, w, h, fc, fr, nodata) {
20
+ const c0 = Math.floor(fc), r0 = Math.floor(fr);
21
+ if (c0 < 0 || c0 >= w || r0 < 0 || r0 >= h) return NaN;
22
+ const c1 = Math.min(c0 + 1, w - 1), r1 = Math.min(r0 + 1, h - 1);
23
+ // `nodata` is undefined when the dataset declares none; `v === undefined` is
24
+ // false for every numeric sample, so the test costs nothing in that case.
25
+ const isVoid = (v) => Number.isNaN(v) || v === nodata;
26
+ const v00 = buf[r0 * w + c0];
27
+ if (isVoid(v00)) return NaN;
28
+ const v10 = buf[r0 * w + c1], v01 = buf[r1 * w + c0], v11 = buf[r1 * w + c1];
29
+ if (isVoid(v10) || isVoid(v01) || isVoid(v11)) return v00; // edge/nodata
30
+ const tx = fc - c0, ty = fr - r0;
31
+ const top = v00 + (v10 - v00) * tx, bot = v01 + (v11 - v01) * tx;
32
+ return top + (bot - top) * ty;
33
+ }