webgeods webGeoDs
  • Home
  • Tools
    • All tools

    • Inspect
    • Validate
    • Check Topology
    • CRS

    • Raster
  • Articles
    • All articles

    • Inspect
    • Validate
    • Check Topology
    • CRS

    • Raster
  • About

Raster Inspector & Statistics

tool
raster
Upload a GeoTIFF and get an instant summary — dimensions, resolution, band count, CRS, NoData, and per-band min/max/mean. Free, runs entirely in your browser: the file never leaves your computer.
Published

September 6, 2026

Upload a .tif/.tiff file and get an instant summary of what’s actually in it — size, resolution, bands, coordinate reference system, and the value range of each band.

Tip

Private by design. Your file is processed entirely in your browser (Python via WebAssembly) — it’s never uploaded to any server, including ours. Nothing to configure, nothing to trust: the code never gets the chance to see your data.

mutable uploadStatus = WebGeoDS.Upload.defaultStatus
mutable uploadBusy = false
mutable uploadedFiles = null
uploadControl = WebGeoDS.Upload.createControl({
  label: "📁 Upload",
  onChange: (files) => { mutable uploadedFiles = files; }
})
{
  // Python-only tool, same reasoning as every other tool here: skip
  // writing to R's filesystem entirely so selecting a file doesn't
  // also boot webR in the background for nothing.
  mutable uploadBusy = true;
  mutable uploadStatus = "⌛ Uploading...";
  try {
    const result = await WebGeoDS.Upload.load(uploadedFiles, { languages: ["python"] });
    if (result.ok) {
      mutable uploadStatus = "⌛ Inspecting...";
      await autoInspect();
      mutable uploadStatus = result.message;
    } else {
      mutable uploadStatus = result.message;
    }
  } finally {
    mutable uploadBusy = false;
  }
}
uploadStatusEl = {
  const span = document.createElement("span");
  span.className = "webgeods-panel-status" + (uploadBusy ? " webgeods-btn-loading" : "");
  span.textContent = uploadStatus;
  return span;
}
sharedMap = {
  const map = new window.WebGeoDS.Map({ center: [12.5, 41.9], zoom: 4, height: "480px" });
  await map.ready();
  window.WebGeoDS.track?.("tool_loaded", { tool: "raster-inspector" });
  return map;
}

The code itself isn’t shown here — it’s a fixed, non-editable summary using rasterio underneath. Want to see the same statistics built up step by step, in Python and R? Read First Look at a Raster File.

// "Load example" writes a small synthetic raster directly, instead of
// decoding a pre-made GeoJSON string the way every vector tool's
// example does (see geospatial-file-inspector.qmd) — a GeoTIFF is a
// binary format, so the natural equivalent is a dedicated Python cell
// that builds one with `rasterio` on demand, not a string constant.
generateExample = async () => {
  await window.WebGeoDS.CodeCell.find("raster-generate-example-py").run();
}
loadExampleButton = {
  const button = document.createElement("button");
  button.className = "webgeods-panel-btn";
  button.dataset.variant = "outline";
  button.textContent = "📋 Load example";
  button.onclick = async () => {
    mutable uploadBusy = true;
    mutable uploadStatus = "⌛ Loading example...";
    try {
      await generateExample();
      mutable uploadStatus = "⌛ Inspecting...";
      await autoInspect();
      mutable uploadStatus = "✓ example data loaded — ready for all the cells below.";
    } finally {
      mutable uploadBusy = false;
    }
  };
  return button;
}
autoInspect = async () => {
  await window.WebGeoDS.CodeCell.find("raster-inspect-py").run();
}
FOOTPRINT_PAINT = ({
  "fill-color": "#42583c",
  "fill-opacity": 0.35,
  "fill-outline-color": "#2a2117"
})
mutable inspectSummary = null
pyInspect = WebGeoDS.getCellValue("raster-inspect-py", Generators)
{
  if (pyInspect && pyInspect.summary) {
    await sharedMap.setGeoJSON("raster-inspect-py", pyInspect.footprint, { paint: FOOTPRINT_PAINT });
    await sharedMap.fitToData(pyInspect.footprint);
    mutable inspectSummary = pyInspect.summary;
    window.WebGeoDS.track?.("validation_completed", { tool: "raster-inspector" });
  }
}
resetInspector = async () => {
  if (sharedMap.getGeoJSON("raster-inspect-py")) {
    await sharedMap.setGeoJSON("raster-inspect-py", { type: "FeatureCollection", features: [] });
  }
  mutable inspectSummary = null;
}
resetMapButton = {
  const button = document.createElement("button");
  button.className = "webgeods-panel-btn";
  button.dataset.variant = "outline";
  button.textContent = "🔄 Reset";
  button.onclick = () => resetInspector();
  return button;
}
// Downloads the computed summary itself (JSON) rather than a
// modified copy of the raster — unlike the vector tools, there's no
// natural "enriched" raster to hand back (the stats are page-level
// scalars, not a per-pixel value to overlay), so a machine-readable
// report of what's on screen is the closest equivalent.
downloadButton = {
  const button = document.createElement("button");
  button.className = "webgeods-panel-btn";
  button.dataset.variant = "outline";
  button.textContent = "⬇ Download";
  button.onclick = () => {
    const data = pyInspect?.summary;
    if (!data) return;
    const base = WebGeoDS.Upload.baseName(uploadedFiles);
    WebGeoDS.downloadBlob(
      JSON.stringify(data, null, 2),
      base ? `${base}-stats.json` : "raster-stats.json",
      "application/json",
      { tool: "raster-inspector" }
    );
  };
  return button;
}
controlPanelRow = {
  const row = document.createElement("div");
  row.className = "webgeods-panel-row";
  row.append(uploadControl, loadExampleButton, downloadButton, resetMapButton);
  return row;
}

First run loads the Python engine — expect 30-40 seconds. Instant after that, and again each time you come back to this page.

// Same reactive-on-a-dedicated-mutable pattern as
// geospatial-file-inspector.qmd's statCard, for the same reason:
// Reset has nothing to re-run, so reading straight from pyInspect
// would leave stale numbers on screen after clicking it.
statCard = {
  const s = inspectSummary;
  const box = document.createElement("div");
  box.className = "webgeods-stat-grid";

  const row = (label, value) => {
    const dt = document.createElement("div");
    dt.className = "webgeods-stat-label";
    dt.textContent = label;
    const dd = document.createElement("div");
    dd.className = "webgeods-stat-value";
    dd.textContent = value;
    box.append(dt, dd);
  };

  if (!s) {
    row("Dimensions", "—");
    row("—", "Upload a .tif/.tiff file or load the example above");
    return box;
  }

  const MAX_BANDS_SHOWN = 6;
  const bandLines = s.bandStats.slice(0, MAX_BANDS_SHOWN).map((b) =>
    b.min === null
      ? `Band ${b.band}: all NoData`
      : `Band ${b.band}: ${b.min} to ${b.max} (mean ${b.mean})`
  );
  if (s.bandStats.length > MAX_BANDS_SHOWN) {
    bandLines.push(`… and ${s.bandStats.length - MAX_BANDS_SHOWN} more band(s)`);
  }

  row("Dimensions", `${s.width} × ${s.height} px`);
  row("Bands", String(s.bandCount));
  row("CRS", s.crsWarning ? `${s.crs} ⚠️ ${s.crsWarning}` : s.crs);
  row("Resolution", s.resolution.join(" × "));
  row("Data type", s.dtype);
  row("NoData", s.nodata === null || s.nodata === undefined ? "None declared" : String(s.nodata));
  row("Bounds", s.bounds.join(", "));
  row("Value range", bandLines.join(" · "));

  return box;
}
sharedMapEl = sharedMap.element

This is the first tool in the Raster family. Read the companion article for the same statistics in Python and R, one cell at a time — a Calculator/Band Math tool is planned as the next step.