Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
c52b438
feat(raster): cloud-optimised GeoTIFFs are raster layers read in place
Nov 2, 2026
d44edc1
fix(raster): address review of COGs read in place; wait for Retry-Aft…
Nov 2, 2026
f9a1afa
Merge remote-tracking branch 'origin/main' into feat/cog-as-raster
Nov 2, 2026
6cdb0e8
fix(raster): say a missing colour table when pixels land later; make …
Nov 2, 2026
37ff3e2
Merge branch 'main' into feat/cog-as-raster
siiinc Nov 2, 2026
a386d82
Merge branch 'main' into feat/cog-as-raster
siiinc Nov 2, 2026
991575c
fix(raster): second review round of COGs read in place
Nov 2, 2026
73a7789
Merge remote-tracking branch 'fork/feat/cog-as-raster' into feat/cog-…
Nov 2, 2026
ba55251
fix(raster): third review round of COGs read in place
Nov 2, 2026
e37d2b4
Merge remote-tracking branch 'origin/main' into feat/cog-as-raster
Nov 2, 2026
b5b6684
fix(raster): fourth review round of COGs read in place
Nov 2, 2026
2585cc4
fix(raster): fifth review round of COGs read in place
Nov 2, 2026
184cdea
Merge remote-tracking branch 'origin/main' into feat/cog-as-raster
Nov 3, 2026
22b22a8
fix(raster): check an in-place address as a tile layer's before readi…
Nov 3, 2026
2ca64af
fix(raster): the map says when an in-place layer draws less than the …
Nov 3, 2026
055128b
fix(raster): say a dropped overlay flag; refuse partial in-place expo…
Nov 3, 2026
93b4652
fix(raster): refuse a PNG export only for an in-place layer's own pen…
Nov 3, 2026
83d2846
fix(raster): three minor points from review
Nov 3, 2026
0e6c5c0
Merge remote-tracking branch 'origin/main' into feat/cog-as-raster
Nov 3, 2026
9fe6717
fix(raster): a tile's end saturates in assemble; a data cube's file s…
Nov 3, 2026
9df3209
fix(raster): a view across a 0–360° grid's seam reads the two strips …
Nov 3, 2026
d7d6675
fix(raster): a turned in-place layer zoomed in past its overview says…
Nov 3, 2026
62a98eb
Merge remote-tracking branch 'origin/main' into feat/cog-as-raster
Nov 3, 2026
4a81c8f
Merge remote-tracking branch 'origin/main' into feat/cog-as-raster
Nov 3, 2026
95983b4
fix(raster): review of the merge with main
Nov 3, 2026
a61e6a2
Merge branch 'main' into feat/cog-as-raster
siiinc Nov 3, 2026
21ce40d
fix(raster): review of the merge with main, second round
Nov 3, 2026
3497185
Merge branch 'main' into feat/cog-as-raster
siiinc Nov 3, 2026
241050e
Merge branch 'main' into feat/cog-as-raster
siiinc Nov 3, 2026
cfa6f9c
Merge branch 'main' into feat/cog-as-raster
siiinc Nov 3, 2026
2ce92bc
fix(raster): review of the merges with main, third round
Nov 3, 2026
5424f81
Merge branch 'main' into feat/cog-as-raster
siiinc Nov 3, 2026
3996a53
fix(tiles): review of the merges with main, fourth round
Nov 4, 2026
5793700
fix(tiles): a window asks for its tiles in batches
Nov 4, 2026
7c3809f
Merge remote-tracking branch 'origin/main' into feat/cog-as-raster
Nov 4, 2026
0f9e94d
Merge remote-tracking branch 'origin/main' into feat/cog-as-raster
Nov 4, 2026
767f99a
fix(engine): the desktop doesn't wait for a read in place, and a fail…
Nov 4, 2026
e7d4e9f
fix(tiles): a joint read the queue keeps dropping ends, and one wait …
Nov 4, 2026
e7d39da
Merge branch 'main' into feat/cog-as-raster
siiinc Nov 4, 2026
93a6553
fix(engine): a bind counts a raster read in place as reading until it…
Nov 4, 2026
4312d4f
Merge branch 'main' into feat/cog-as-raster
siiinc Nov 4, 2026
4cd9d68
fix(engine): COG tile layers open as rasters as the project opens onl…
Nov 4, 2026
c399906
Merge remote-tracking branch 'origin/main' into feat/cog-as-raster
Nov 4, 2026
9dd9241
fix(engine): a session that doesn't draw reads a failed overview agai…
Nov 5, 2026
d70174a
Merge remote-tracking branch 'origin/main' into feat/cog-as-raster
Nov 5, 2026
3aff85d
Merge branch 'main' into feat/cog-as-raster
siiinc Nov 5, 2026
5b0c6aa
Merge remote-tracking branch 'origin/main' into feat/cog-as-raster
Nov 6, 2026
fb449b1
fix(engine): a migrated COG tile layer keeps its connection, a palett…
Nov 6, 2026
54a24d7
fix(tiles, engine): every wait asked for has the map drawn again, a d…
Nov 6, 2026
b005938
Merge branch 'main' into feat/cog-as-raster
siiinc Nov 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions apps/rustgis-web/src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,17 @@ async fn unless_stalled(
Some(raced)
}

/// A refusal's text (`HTTP 503`), with how long the server asked to be left when it says so in a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nits, grouped (left for the release quality pass, as AGENTS.md asks):

  • The new refused function is inserted between fetch_bytes's doc comment (lines 573–576) and fetch_bytes itself. fetch_bytes now has no doc comment, and refused inherits the text about split_range and omit. Moving refused above that doc block fixes it. crates/tiles/src/cache.rs (around 1538–1539) has the same pattern: retry_after_header took over the doc comment of refused.
  • Unpaletted::new and photometric_patches take a wide_rgb flag that is always true (the only caller is reader, geotiff.rs:710). The false path is dead, so the parameter can go.
  • In add_in_place (crates/engine/src/cmd/layer.rs), r["layer"].as_u64().unwrap_or(0) names the wrong layer if the key is missing. Using and_then keeps the name unset instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three fixed rather than left: fetch_bytes and cache.rs's refused have their doc comments back above them; photometric_patches and Unpaletted::new lose the always-true flag; add_in_place names the layer through and_then, so a missing id leaves the name unset.

/// `Retry-After` header the browser lets the page read (its CORS rules must expose it).
fn refused(resp: &web_sys::Response) -> String {
let wait = resp.headers().get("retry-after").ok().flatten();
rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::failure::with_retry_after(
format!("HTTP {}", resp.status()),
resp.status(),
wait.as_deref(),
)
}

/// Downloads `url`, or the byte range it asks for (a PMTiles archive's part: see
/// `rustgis_tiles::cache::split_range`). A status in `omit` is an expected answer (a tile past a
/// map's edge): its failure isn't recorded, only noted once per server, as on the desktop. A
Expand Down Expand Up @@ -670,7 +681,7 @@ async fn fetch_bytes(url: &str, capture: Capture, omit: &'static [u16]) -> Resul
.map_err(err)?;
diagnostics.answered(resp.status());
if !resp.ok() {
return Err(format!("HTTP {}", resp.status()));
return Err(refused(&resp));
}
// The last bytes asked for and the whole file answered: fine when it is no longer.
let whole_tail = tail.filter(|_| resp.status() != 206);
Expand Down Expand Up @@ -793,7 +804,7 @@ async fn post_json(url: &str, body: &str, capture: Capture) -> Result<Vec<u8>, S
.map_err(err)?;
diagnostics.answered(resp.status());
if !resp.ok() {
return Err(format!("HTTP {}", resp.status()));
return Err(refused(&resp));
}
let buf = wasm_bindgen_futures::JsFuture::from(resp.array_buffer().map_err(err)?).await.map_err(err)?;
let bytes = js_sys::Uint8Array::new(&buf);
Expand Down Expand Up @@ -861,7 +872,7 @@ async fn head(url: &str, capture: Capture) -> Result<rustgis_ui_egui::rustgis_en
.map_err(err)?;
diagnostics.answered(resp.status());
if !resp.ok() {
return Err(format!("HTTP {}", resp.status()));
return Err(refused(&resp));
}
let header = |k: &str| resp.headers().get(k).ok().flatten();
Ok(cache::Head {
Expand Down
5 changes: 4 additions & 1 deletion apps/rustgis/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,11 +485,14 @@ fn main() -> eframe::Result {
.collect();
// Installed plugins, before the first session lists commands and tools.
let plugin_errors = rustgis_engine::cmd::plugins::load_installed();
let app = if args.iter().any(|a| a == "--sample") || files.is_empty() {
let mut app = if args.iter().any(|a| a == "--sample") || files.is_empty() {
RustGisApp::with_sample()
} else {
RustGisApp::new(Session::new(Project::default()))
};
// Commands run on the window's own thread: a raster read in place lands as the map draws
// rather than holding the window for a slow host.
app.session.in_place_wait = false;
let mut services = prefs::services();
// Autosave, in a recovery folder only the copy holding the run marker uses: a second copy
// running alongside would offer, or overwrite, the first one's unsaved work.
Expand Down
141 changes: 132 additions & 9 deletions crates/data/src/raster/cog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,9 @@ fn read_layout(r: Sparse) -> Result<Parse> {
// then is a colour table read, as the whole-file read does; a grey image's stray one isn't.
// The full image's word, which the window and the drawing read.
let palette_image = rd.first_is_palette();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-raising the palette / mixed-file thread with new evidence (correctness, never silent, confidence ~70% on the mismatch, ~50% on user impact).

The reply on the earlier thread says the palette and RGB-with-more-bands decisions were made per image (palette_ifds, rgb_ifds, tests a_tableless_palette_image_beside_grey_ones_says_so and a_file_mixing_grey_and_rgb_images_tells_them_apart_by_image). None of those names exist in this checkout; they appear only in the review JSON. The code still takes both decisions from the first image only: palette_image = rd.first_is_palette() here, wide_rgb = rd.first_is_wide_rgb() on the next line, and the same first-image gate in geotiff.rs (palette, wide_rgb around lines 1158 and 1204).

Failure scenarios:

  • A COG whose full image is grey or RGB, with a tableless palette overview. The overview is drawn grey with no notice.
  • A palette file whose first image has a table, with a genuinely grey overview and no ColorMap. The reader reports "palette image with no colour table" falsely.
  • A file with a grey full image and an RGB+NIR overview. The overview takes the full image's colour decision.

The decision needs to come from each IFD's own original PhotometricInterpretation, recorded in palette_patches, and not from the first image. A mixed-photometric test would pin it down.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that the reply described code that is no longer here: the per-image rule (palette_ifds, rgb_ifds and their tests) went when main was merged in. main (#1204) decides both from the full image (first_is_palette, and now first_is_wide_rgb for RGB images with more bands), on purpose and with its own tests (stray: a grey full image with a colour table anyway isn't a palette; no_table: a palette image without one is drawn grey and says so). The merge adopts that rule rather than carrying two, as the merge note on the PR says. On the three cases: a tableless palette overview under a grey or RGB full image draws grey without a notice, and a grey overview of a palette file says "no colour table": both are files whose overviews contradict their full image, which main's rule reads by the full image, as a window does; an RGB+NIR overview under a grey full image is read grey, by the same rule. If a per-image rule is wanted, it belongs with main's model (Layout.palette, lost) and its stray test, not in this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-raised with new evidence: the reply on the palette / mixed-file thread describes code that isn't in this head.

The reply says the decision is now per image (palette_ifds, rgb_ifds), with tests a_tableless_palette_image_beside_grey_ones_says_so and a_file_mixing_grey_and_rgb_images_tells_them_apart_by_image. Neither the names nor the tests exist in the repository or the diff. The head still takes both decisions from the full image: rd.first_is_palette() here, rd.first_is_wide_rgb() on the next line, and the same first-image gate in geotiff.rs (read_limited_with_nodata, around line 1064). The only mixed-file test is a_file_mixing_grey_and_rgb_images_goes_by_the_full_image.

Consequence, which AGENTS.md's never-silent rule rules out: when the chosen overview is a palette image with no ColorMap and the full image is grey or RGB, palette_image is false, so the overview is drawn grey (palette indices as grey values) with no notice.

Either implement the per-image rule the reply describes, with its tests, or correct the reply. Whichever is chosen, the tableless-overview case needs a notice or a test showing why it's silent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reply you quote is stale and stands corrected by the reply right under it (the geotiff.rs:1157 thread, 3 Nov) and by the first thread at this line: there is no per-image rule in this branch. Both decisions come from the full image, main's rule. The silent case is pinned by a_palette_overview_under_a_grey_full_image_is_read_grey_as_the_full_image_is (4096 px in 256 px tiles, classes + stray + no_table: the overview held is the 1024 px palette image without a table, read grey as the full image is, with no notice), whose comment says why: a file is what its full image says, a window over it is read the same way, and an overview contradicting its full image is the one thing the reader doesn't take over it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-raising the palette-overview thread on AGENTS.md's never-silent rule (confidence ~55%).

The reply says the full image decides the palette question, and that the test a_palette_overview_under_a_grey_full_image_is_read_grey_as_the_full_image_is pins the grey reading. That answers which reading is consistent, not the notice. The test asserts !s.notices.iter().any(|n| n.text.contains("colour table")), so an overview whose header says Palette, with no ColorMap, is drawn as grey indices with nothing said.

AGENTS.md: "A reader … that drops, truncates or approximates data says what, how much and where," and "Test both ways". Here the overview's own PhotometricInterpretation contradicts the full image, and the layer shows indices as grey values. The same case already gets a notice when the full image is palette ("drawn in grey, without its colour table"), so the silence is specific to this branch.

Suggested direction: keep the full-image rule for the look, but add a notice when the overview is palette-tagged (or its colour table is missing) and the full image isn't, naming the layer. The test should then assert the notice, not its absence.

// An RGB image with more bands (shown to the decoder as grey, see `Unpaletted`): every band,
// the first three red, green and blue.
let wide_rgb = rd.first_is_wide_rgb();
let mut dec = Decoder::new(rd).map_err(err)?.with_limits(limits());
let Some(geo) = super::geotiff::cog_geo(&mut dec)? else {
return Ok(Parse::NotTiled("a rotated image, or one without coordinates, is read whole".into()));
Expand All @@ -346,6 +349,9 @@ fn read_layout(r: Sparse) -> Result<Parse> {
(3, ColorHint::Rgb { alpha: false })
}
ColorType::RGBA(_) => (4, ColorHint::Rgb { alpha: true }),
ColorType::Multiband { num_samples, .. } if wide_rgb && num_samples >= 3 => {
(num_samples as usize, ColorHint::Rgb { alpha: super::geotiff::extra_alpha(&mut dec) })
}
ColorType::Multiband { num_samples, .. } => {
let color = super::geotiff::multiband_color(&mut dec, num_samples);
(num_samples.max(1) as usize, color)
Expand Down Expand Up @@ -613,6 +619,11 @@ pub struct Example {
pub int8: bool,
/// Stored bottom up: a positive Y pixel size, the first row the southernmost (at 45° N).
pub bottom_up: bool,
/// Its PhotometricInterpretation: 1 grey, 2 RGB (the bands past the third extra samples);
/// `classes` makes a palette image (3) whatever this says.
pub photometric: u16,
/// The first extra sample (the band past the colour ones) is unassociated alpha.
pub alpha: bool,
/// A palette image (one band of bytes) whose index `i` is the colour (i, 255 − i, 0), with
/// classes named in GDAL's metadata as the MapBiomas files name theirs (`CLASS_3`, `CLASS_15`).
pub classes: bool,
Expand All @@ -628,6 +639,11 @@ pub struct Example {
pub stray: bool,
/// With `classes`: a palette image with no colour table.
pub no_table: bool,
/// The full image's first tile left out, as GDAL's sparse files leave out tiles of no-data
/// (no bytes, no offset).
pub sparse: bool,
/// Without overviews: the full image alone (a file saved without them).
pub no_overviews: bool,
}

impl Default for Example {
Expand All @@ -639,12 +655,16 @@ impl Default for Example {
planar: false,
int8: false,
bottom_up: false,
photometric: 1,
alpha: false,
classes: false,
lossy: false,
wide: false,
huge: false,
stray: false,
no_table: false,
sparse: false,
no_overviews: false,
}
}
}
Expand All @@ -657,7 +677,7 @@ pub fn example_with(e: &Example) -> Vec<u8> {
let planes = if e.planar { nb } else { 1 };
let per_chunk = nb / planes;
let mut sizes = vec![size];
while *sizes.last().unwrap_or(&tile) > tile {
while !e.no_overviews && *sizes.last().unwrap_or(&tile) > tile {
sizes.push(sizes.last().unwrap_or(&tile).div_ceil(2));
}
let value = |x: u32, y: u32, b: u32| -> u8 {
Expand Down Expand Up @@ -695,6 +715,12 @@ pub fn example_with(e: &Example) -> Vec<u8> {
chunks.push(c);
}
}
if e.sparse
&& k == 1
&& let Some(first) = chunks.first_mut()
{
first.clear();
}
chunks
})
.collect();
Expand Down Expand Up @@ -737,16 +763,23 @@ pub fn example_with(e: &Example) -> Vec<u8> {
]),
),
(259, V::S(vec![1])),
(262, V::S(vec![if e.classes && !(e.stray && i == 0) { 3 } else { 1 }])),
(262, V::S(vec![if e.classes && !(e.stray && i == 0) { 3 } else { e.photometric }])),
(277, V::S(vec![nb as u16])),
(284, V::S(vec![if e.planar { 2 } else { 1 }])),
(322, V::L(vec![tile])),
(323, V::L(vec![tile])),
(324, V::L(vec![0; n])),
(325, V::L(vec![chunk_len; n])),
(325, V::L(images.get(i).map_or(vec![chunk_len; n], |c| c.iter().map(|c| c.len() as u32).collect()))),
];
if nb > 1 {
t.push((338, V::S(vec![0; nb as usize - 1])));
// The samples past the colour ones (three of an RGB image, else one): the first alpha
// when asked, the others undefined.
let colours = if e.photometric == 2 { 3 } else { 1 };
if nb as usize > colours {
let mut extra = vec![0u16; nb as usize - colours];
if e.alpha {
extra[0] = 2;
}
t.push((338, V::S(extra)));
}
if e.int8 || e.wide {
t.push((339, V::S(vec![2; nb as usize])));
Expand Down Expand Up @@ -802,14 +835,16 @@ pub fn example_with(e: &Example) -> Vec<u8> {
let mut data_at = at;
let mut out = vec![b'I', b'I', 42, 0];
out.extend_from_slice(&header_len.to_le_bytes());
for t in ifds.iter_mut() {
for (lvl, t) in ifds.iter_mut().enumerate() {
for (tag, v) in t.iter_mut() {
if *tag == 324
&& let V::L(offs) = v
{
for o in offs.iter_mut() {
*o = data_at as u32;
data_at += chunk_len as usize;
for (j, o) in offs.iter_mut().enumerate() {
// A chunk left out has no offset, as GDAL writes a sparse file.
let len = images.get(lvl).and_then(|c| c.get(j)).map_or(0, Vec::len);
*o = if len == 0 { 0 } else { data_at as u32 };
data_at += len;
}
}
}
Expand Down Expand Up @@ -1034,4 +1069,92 @@ pub(crate) mod tests {
}
let _ = layout(&Sparse::default().with(0, b"II*\0\xff\xff\xff\x7f".to_vec()));
}

#[test]
fn every_band_whatever_the_photometric_interpretation_says() {
let want = |b: u32| ((300 + 10 * b) % 256) as f32;
let e = Example { size: 300, tile: 128, ..Example::default() };
// Grey, as remote sensing images mostly are: eight bands (a WorldView multispectral image),
// interleaved or stored apart; a panchromatic one; a pan band with alpha.
for planar in [false, true] {
let (l, v) = pixel(&example_with(&Example { bands: 8, planar, ..e }), 150, 150);
assert_eq!((l.bands, &l.color), (8, &ColorHint::Gray));
assert_eq!(v, (0..8).map(want).collect::<Vec<_>>());
}
let (l, v) = pixel(&example_with(&Example { bands: 1, ..e }), 150, 150);
assert_eq!((l.bands, &l.color, v), (1, &ColorHint::Gray, vec![want(0)]));
let (l, v) = pixel(&example_with(&Example { bands: 2, alpha: true, ..e }), 150, 150);
assert_eq!((l.bands, &l.color, v), (2, &ColorHint::GrayAlpha, vec![want(0), want(1)]));
// RGB: three bands, with alpha, and with bands past the colour ones (RGB and near-infrared,
// seven bands), which the decoder alone leaves out or refuses: every band, the first three
// red, green and blue.
for (bands, alpha) in [(3, false), (4, true), (4, false), (7, false), (5, true)] {
for planar in [false, true] {
let file = example_with(&Example { bands, alpha, planar, photometric: 2, ..e });
let (l, v) = pixel(&file, 150, 150);
assert_eq!((l.bands, &l.color), (usize::from(bands), &ColorHint::Rgb { alpha }), "{bands} bands, alpha {alpha}");
assert_eq!(v, (0..u32::from(bands)).map(want).collect::<Vec<_>>(), "{bands} bands, alpha {alpha}, planar {planar}");
}
}
// A palette image: its values, drawn grey, with its colour table apart.
let (l, v) = pixel(&example_with(&Example { classes: true, ..e }), 150, 150);
let table = l.palette.as_ref().unwrap();
assert_eq!((&l.color, table.len(), table[44], v), (&ColorHint::Gray, 256, [44, 211, 0, 255], vec![want(0)]));
}

/// A GeoTIFF file is read as the same image read in place is: every band, whatever its
/// PhotometricInterpretation says, coloured alike.
#[test]
fn files_have_every_band_a_cog_has() {
let e = Example { size: 300, tile: 128, ..Example::default() };
for (bands, alpha, photometric) in [(8, false, 1), (2, true, 1), (4, true, 2), (4, false, 2), (7, false, 2), (5, true, 2)] {
for planar in [false, true] {
let file = example_with(&Example { bands, alpha, planar, photometric, ..e });
let (l, v) = pixel(&file, 150, 150);
let r = crate::raster::geotiff::read(std::io::Cursor::new(&file), "f").unwrap();
let read: Vec<f32> = (0..r.band_count()).map(|b| r.value(b, 150, 150).unwrap()).collect();
assert_eq!((r.band_count(), &r.color, read), (l.bands, &l.color, v), "{bands} bands, photometric {photometric}");
}
}
}

/// A file holding an RGB image with more bands and a grey overview: the full image's word
/// goes, whichever image is read, as a palette file's does.
#[test]
fn a_file_mixing_grey_and_rgb_images_goes_by_the_full_image() {
let e = Example { size: 300, tile: 128, bands: 4, photometric: 2, ..Example::default() };
let mut file = example_with(&e);
// The coarsest overview's PhotometricInterpretation (tag 262, one short) from RGB to grey.
let entry = [0x06, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00];
let at: Vec<usize> = (0..file.len().saturating_sub(entry.len())).filter(|&i| file[i..i + entry.len()] == entry).collect();
assert_eq!(at.len(), 3, "the full image and its overviews");
file[at[2] + 8] = 1;
let (l, _) = pixel(&file, 150, 150);
assert_eq!((l.bands, &l.color), (4, &ColorHint::Rgb { alpha: false }));
let r = crate::raster::geotiff::read(std::io::Cursor::new(&file), "mixed").unwrap();
assert_eq!((r.band_count(), &r.color), (4, &ColorHint::Rgb { alpha: false }));
let small = crate::raster::geotiff::read_limited(
crate::raster::geotiff::reader(std::io::Cursor::new(&file)).unwrap(),
"mixed",
30_000,
None,
)
.unwrap();
assert_eq!((small.width, small.band_count(), &small.color), (75, 4, &ColorHint::Rgb { alpha: false }));
}
/// Pixel (x, y)'s value in every band of an example's full image, read a tile at a time.
fn pixel(file: &[u8], x: u32, y: u32) -> (Box<Layout>, Vec<f32>) {
let (s, l) = layout_of(file);
let lv = &l.levels[0];
let tile = (y / lv.tile_height) * lv.across() + x / lv.tile_width;
let at = ((y % lv.tile_height) * lv.tile_width + x % lv.tile_width) as usize;
let values = (0..l.bands)
.map(|b| {
let (chunk, i) = l.chunk(lv, tile, b).unwrap();
let (o, n) = (lv.offsets[chunk as usize] as usize, lv.counts[chunk as usize] as usize);
decode_tile(&s, &l, 0, chunk, file[o..o + n].to_vec()).unwrap()[i][at]
})
.collect();
(l, values)
}
}
Loading
Loading