viewinline 0.3.2__tar.gz → 0.4.0__tar.gz

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.
@@ -0,0 +1,44 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it using the metadata from this file."
3
+ title: "viewinline"
4
+ abstract: >-
5
+ A tiny, non-interactive command-line viewer that displays rasters/photos,
6
+ vectors, and tabular data (CSV/Parquet) inline in the terminal — no GUI,
7
+ no X11, and no file downloads. Designed for quick visual inspection at the
8
+ command line, and especially useful on HPC systems and remote servers over
9
+ SSH, where images render on the local terminal.
10
+ type: software
11
+ authors:
12
+ - given-names: Keiko
13
+ family-names: Nomura
14
+ # orcid: "https://orcid.org/0000-0000-0000-0000" # optional: add if you have one
15
+ - given-names: Filip
16
+ family-names: Kral
17
+ # orcid: "https://orcid.org/0000-0000-0000-0000" # optional: add if you have one
18
+ repository-code: "https://github.com/nkeikon/inlineviewer"
19
+ url: "https://github.com/nkeikon/inlineviewer"
20
+ license: Apache-2.0
21
+ version: 0.3.2
22
+ date-released: 2026-06-06
23
+ # ---------------------------------------------------------------------------
24
+ # The DOI below is the VERSION DOI for v0.3.2 (a specific archived snapshot).
25
+ # To make citations always resolve to your LATEST release, replace it with the
26
+ # Zenodo CONCEPT / "all versions" DOI, shown on the record under "Cite all
27
+ # versions". The version DOI stays useful in `identifiers` for reproducibility.
28
+ # ---------------------------------------------------------------------------
29
+ doi: "10.5281/zenodo.20564266"
30
+ identifiers:
31
+ - type: doi
32
+ value: "10.5281/zenodo.20564266"
33
+ description: "Zenodo archive of version v0.3.2"
34
+ keywords:
35
+ - geospatial
36
+ - raster
37
+ - vector
38
+ - terminal
39
+ - command-line
40
+ - visualization
41
+ - GeoTIFF
42
+ - NetCDF
43
+ - GeoParquet
44
+ - remote-sensing
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: viewinline
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: Quick look geospatial viewer for the terminal, with inline image previews
5
5
  Project-URL: Homepage, https://github.com/nkeikon/viewinline
6
6
  Project-URL: Repository, https://github.com/nkeikon/viewinline
@@ -81,6 +81,11 @@ viewinline hyperspectral.nc --subset 22 --bands 10-54 --gallery 5x11
81
81
  viewinline path/to/vector.geojson
82
82
  viewinline boundaries.geoparquet --color-by population --colormap viridis
83
83
 
84
+ # Save to file
85
+ viewinline path/to/file.tif --export out.png # save the rendered image as PNG (or .jpg)
86
+ viewinline scene.tif --rgb 4 3 2 --export rgb.png
87
+ viewinline dem.tif --colormap terrain --display 1 --export dem.png # full-res with colormap
88
+
84
89
  # CSV and Parquet
85
90
  viewinline data.csv # preview rows and columns
86
91
  viewinline data.parquet --describe # summary statistics
@@ -134,6 +139,7 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
134
139
  - **NetCDF/HDF Support:** Display variables from NetCDF (.nc) and HDF5 (.h5, .hdf5) files with automatic nodata detection and multi-slice navigation
135
140
  - **Parquet/GeoParquet:** Render GeoParquet as vector maps or view as tabular data
136
141
  - **Tabular View for Vectors:** Use `--table` to access CSV-style operations (filter, sort, describe, hist) on any vector file
142
+ - **AI-agent inspection:** `--info` returns file metadata and statistics as JSON; `--export` saves a quick-look image for an agent to inspect. Designed for AI coding-agent workflows.
137
143
 
138
144
  ## Supported formats
139
145
  **Rasters**
@@ -180,6 +186,7 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
180
186
  - Variables with 4+ dimensions are not supported
181
187
  - For a complete variable list, use `ncdump -h file.nc` or `viewtif`
182
188
 
189
+
183
190
  ## Dependencies
184
191
 
185
192
  **Core dependencies** (installed automatically):
@@ -213,6 +220,8 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
213
220
  ```
214
221
  General:
215
222
  --display DISPLAY Resize only the displayed image (0.5=smaller, 2=bigger). Default: auto-fit to terminal.
223
+ --info Print file metadata and statistics as JSON, then exit (agent-facing). For NetCDF/HDF, lists variables; combine with --subset N to inspect one. Always returns JSON, including on error.
224
+ --export PATH Save the rendered image to PATH (.png/.jpg) and open it. Works with any display flag. Prints {"path": "..."} as the final line.
216
225
 
217
226
  Raster:
218
227
  --band BAND Band number to display (single raster), or slice number for NetCDF. (default: 1)
@@ -249,6 +258,95 @@ CSV and Parquet:
249
258
  --select COLUMNS Select specific columns (space separated). Example: --select Country City
250
259
  --sql QUERY Execute full DuckDB SQL query. Use 'data' as table name. Example: --sql "SELECT * FROM data WHERE Poverty > 40"
251
260
  ```
261
+ ## AI-agent inspection (`--info`, `--export`)
262
+
263
+ An optional command aimed at AI coding-agent workflows (Claude Code, Codex, Cursor, and similar). An agent can confirm its code *ran*, but not whether the geospatial file it produced is *sensible* — wrong CRS, unexpected dimensions, all-NoData, NaN/Inf values, or a flipped output. `--info` answers **"what did I create?"** as machine-readable JSON.
264
+
265
+ ```bash
266
+ viewinline result.tif --info # metadata + statistics as JSON, then exit
267
+ ```
268
+
269
+ It reports facts, not judgments: format, dimensions, bands, dtype, CRS, resolution, bounds, nodata, and per-band statistics. For NetCDF/HDF it lists variables; add `--subset N` to inspect one:
270
+
271
+ ```bash
272
+ viewinline data.nc --info # list variables
273
+ viewinline data.nc --subset 7 --info # inspect variable 7
274
+ viewinline scene.hdf --subset 1 --info # inspect HDF subdataset 1
275
+ ```
276
+
277
+ `--info` always returns JSON, including a `{"readable": false, "error": ...}` object on unreadable input, so an agent can always parse the result. Pairs well with `--export` (see Usage) when the agent wants to *see* the output too, not just read its metadata.
278
+
279
+ ### `--info` — structured inspection
280
+
281
+ Prints file metadata as JSON and exits (no image is drawn). Reports facts, not judgments — the agent interprets them in context.
282
+
283
+ ```bash
284
+ viewinline result.tif --info
285
+ ```
286
+ ```json
287
+ {
288
+ "format": "GTiff",
289
+ "filename": "result.tif",
290
+ "dimensions": [1001, 1001],
291
+ "bands": 3,
292
+ "dtype": "uint16",
293
+ "crs": "EPSG:32631",
294
+ "resolution": [10.0, 10.0],
295
+ "bounds": [590520.0, 5780620.0, 600530.0, 5790630.0],
296
+ "nodata": null,
297
+ "statistics": {
298
+ "method": "full",
299
+ "bands_total": 3,
300
+ "bands_reported": 3,
301
+ "per_band": [
302
+ {"band": 1, "min": 0.0, "max": 10964.0, "mean": 1009.32, "valid_fraction": 1.0, "naninf_fraction": 0.0}
303
+ ]
304
+ }
305
+ }
306
+ ```
307
+
308
+ For **NetCDF and HDF**, `--info` lists the file's variables/subdatasets; add `--subset N` to inspect one:
309
+
310
+ ```bash
311
+ viewinline data.nc --info # list variables
312
+ viewinline data.nc --subset 7 --info # inspect variable 7 (dims, dtype, units, stats)
313
+ viewinline scene.hdf --subset 1 --info # inspect HDF subdataset 1
314
+ ```
315
+
316
+ `--info` **always returns JSON**, including on failure. Unsupported or unreadable inputs return a structured error rather than crashing, so an agent can always parse the result:
317
+
318
+ ```json
319
+ {"error": "no directly-readable bands (file has subdatasets)", "readable": false, "subdataset_count": 22}
320
+ ```
321
+
322
+ Notes on the output:
323
+ - `crs` is an `EPSG:code` when one can be resolved, the full WKT string when the CRS has no EPSG code (e.g. MODIS Sinusoidal), or `null` when the file has no CRS.
324
+ - Statistics exclude NoData and non-finite pixels; `valid_fraction` and `naninf_fraction` report how much was excluded.
325
+ - For files with many bands, per-band stats are capped (a `bands_reported` < `bands_total` and a `note` indicate truncation — absence of a band's stats does **not** imply a problem).
326
+ - Large rasters are sampled for statistics (`"method": "sampled"`); small ones use every pixel (`"method": "full"`).
327
+
328
+ ### `--export` — visual inspection
329
+
330
+ Saves the rendered image to a file (PNG or JPEG, chosen by extension) so an agent can also inspect it with its vision capabilities, then prints the path as JSON:
331
+
332
+ ```bash
333
+ viewinline result.tif --export out.png
334
+ ```
335
+ ```json
336
+ {"path": "/abs/path/out.png"}
337
+ ```
338
+
339
+ `--export` works with **any display option** — the saved image is exactly what viewinline would render, after all flags are applied:
340
+
341
+ ```bash
342
+ viewinline scene.tif --rgb 4 3 2 --export rgb.png
343
+ viewinline dem.tif --colormap terrain --export dem.png
344
+ viewinline result.tif --display 1 --export full_res.png # full resolution instead of terminal-fit
345
+ ```
346
+
347
+ The exported image is a quick-look representation for catching problems metadata can't reveal — blank output, stripes, artifacts, holes, wrong orientation, unexpected extent, or bad color scaling — **not** a publication-quality rendering.
348
+
349
+ > **Scope:** `--info` and `--export` tell you what a file *is* and what it *looks like*. Neither claims the scientific result is *correct* — that judgment stays with the agent.
252
350
 
253
351
  ## Need help?
254
352
  NASA staff can ask questions about usage via the documentation-based assistant 'viewtif + viewgeom + viewinline Helper' via the ChatGSFC Agent Marketplace.
@@ -48,6 +48,11 @@ viewinline hyperspectral.nc --subset 22 --bands 10-54 --gallery 5x11
48
48
  viewinline path/to/vector.geojson
49
49
  viewinline boundaries.geoparquet --color-by population --colormap viridis
50
50
 
51
+ # Save to file
52
+ viewinline path/to/file.tif --export out.png # save the rendered image as PNG (or .jpg)
53
+ viewinline scene.tif --rgb 4 3 2 --export rgb.png
54
+ viewinline dem.tif --colormap terrain --display 1 --export dem.png # full-res with colormap
55
+
51
56
  # CSV and Parquet
52
57
  viewinline data.csv # preview rows and columns
53
58
  viewinline data.parquet --describe # summary statistics
@@ -101,6 +106,7 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
101
106
  - **NetCDF/HDF Support:** Display variables from NetCDF (.nc) and HDF5 (.h5, .hdf5) files with automatic nodata detection and multi-slice navigation
102
107
  - **Parquet/GeoParquet:** Render GeoParquet as vector maps or view as tabular data
103
108
  - **Tabular View for Vectors:** Use `--table` to access CSV-style operations (filter, sort, describe, hist) on any vector file
109
+ - **AI-agent inspection:** `--info` returns file metadata and statistics as JSON; `--export` saves a quick-look image for an agent to inspect. Designed for AI coding-agent workflows.
104
110
 
105
111
  ## Supported formats
106
112
  **Rasters**
@@ -147,6 +153,7 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
147
153
  - Variables with 4+ dimensions are not supported
148
154
  - For a complete variable list, use `ncdump -h file.nc` or `viewtif`
149
155
 
156
+
150
157
  ## Dependencies
151
158
 
152
159
  **Core dependencies** (installed automatically):
@@ -180,6 +187,8 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
180
187
  ```
181
188
  General:
182
189
  --display DISPLAY Resize only the displayed image (0.5=smaller, 2=bigger). Default: auto-fit to terminal.
190
+ --info Print file metadata and statistics as JSON, then exit (agent-facing). For NetCDF/HDF, lists variables; combine with --subset N to inspect one. Always returns JSON, including on error.
191
+ --export PATH Save the rendered image to PATH (.png/.jpg) and open it. Works with any display flag. Prints {"path": "..."} as the final line.
183
192
 
184
193
  Raster:
185
194
  --band BAND Band number to display (single raster), or slice number for NetCDF. (default: 1)
@@ -216,6 +225,95 @@ CSV and Parquet:
216
225
  --select COLUMNS Select specific columns (space separated). Example: --select Country City
217
226
  --sql QUERY Execute full DuckDB SQL query. Use 'data' as table name. Example: --sql "SELECT * FROM data WHERE Poverty > 40"
218
227
  ```
228
+ ## AI-agent inspection (`--info`, `--export`)
229
+
230
+ An optional command aimed at AI coding-agent workflows (Claude Code, Codex, Cursor, and similar). An agent can confirm its code *ran*, but not whether the geospatial file it produced is *sensible* — wrong CRS, unexpected dimensions, all-NoData, NaN/Inf values, or a flipped output. `--info` answers **"what did I create?"** as machine-readable JSON.
231
+
232
+ ```bash
233
+ viewinline result.tif --info # metadata + statistics as JSON, then exit
234
+ ```
235
+
236
+ It reports facts, not judgments: format, dimensions, bands, dtype, CRS, resolution, bounds, nodata, and per-band statistics. For NetCDF/HDF it lists variables; add `--subset N` to inspect one:
237
+
238
+ ```bash
239
+ viewinline data.nc --info # list variables
240
+ viewinline data.nc --subset 7 --info # inspect variable 7
241
+ viewinline scene.hdf --subset 1 --info # inspect HDF subdataset 1
242
+ ```
243
+
244
+ `--info` always returns JSON, including a `{"readable": false, "error": ...}` object on unreadable input, so an agent can always parse the result. Pairs well with `--export` (see Usage) when the agent wants to *see* the output too, not just read its metadata.
245
+
246
+ ### `--info` — structured inspection
247
+
248
+ Prints file metadata as JSON and exits (no image is drawn). Reports facts, not judgments — the agent interprets them in context.
249
+
250
+ ```bash
251
+ viewinline result.tif --info
252
+ ```
253
+ ```json
254
+ {
255
+ "format": "GTiff",
256
+ "filename": "result.tif",
257
+ "dimensions": [1001, 1001],
258
+ "bands": 3,
259
+ "dtype": "uint16",
260
+ "crs": "EPSG:32631",
261
+ "resolution": [10.0, 10.0],
262
+ "bounds": [590520.0, 5780620.0, 600530.0, 5790630.0],
263
+ "nodata": null,
264
+ "statistics": {
265
+ "method": "full",
266
+ "bands_total": 3,
267
+ "bands_reported": 3,
268
+ "per_band": [
269
+ {"band": 1, "min": 0.0, "max": 10964.0, "mean": 1009.32, "valid_fraction": 1.0, "naninf_fraction": 0.0}
270
+ ]
271
+ }
272
+ }
273
+ ```
274
+
275
+ For **NetCDF and HDF**, `--info` lists the file's variables/subdatasets; add `--subset N` to inspect one:
276
+
277
+ ```bash
278
+ viewinline data.nc --info # list variables
279
+ viewinline data.nc --subset 7 --info # inspect variable 7 (dims, dtype, units, stats)
280
+ viewinline scene.hdf --subset 1 --info # inspect HDF subdataset 1
281
+ ```
282
+
283
+ `--info` **always returns JSON**, including on failure. Unsupported or unreadable inputs return a structured error rather than crashing, so an agent can always parse the result:
284
+
285
+ ```json
286
+ {"error": "no directly-readable bands (file has subdatasets)", "readable": false, "subdataset_count": 22}
287
+ ```
288
+
289
+ Notes on the output:
290
+ - `crs` is an `EPSG:code` when one can be resolved, the full WKT string when the CRS has no EPSG code (e.g. MODIS Sinusoidal), or `null` when the file has no CRS.
291
+ - Statistics exclude NoData and non-finite pixels; `valid_fraction` and `naninf_fraction` report how much was excluded.
292
+ - For files with many bands, per-band stats are capped (a `bands_reported` < `bands_total` and a `note` indicate truncation — absence of a band's stats does **not** imply a problem).
293
+ - Large rasters are sampled for statistics (`"method": "sampled"`); small ones use every pixel (`"method": "full"`).
294
+
295
+ ### `--export` — visual inspection
296
+
297
+ Saves the rendered image to a file (PNG or JPEG, chosen by extension) so an agent can also inspect it with its vision capabilities, then prints the path as JSON:
298
+
299
+ ```bash
300
+ viewinline result.tif --export out.png
301
+ ```
302
+ ```json
303
+ {"path": "/abs/path/out.png"}
304
+ ```
305
+
306
+ `--export` works with **any display option** — the saved image is exactly what viewinline would render, after all flags are applied:
307
+
308
+ ```bash
309
+ viewinline scene.tif --rgb 4 3 2 --export rgb.png
310
+ viewinline dem.tif --colormap terrain --export dem.png
311
+ viewinline result.tif --display 1 --export full_res.png # full resolution instead of terminal-fit
312
+ ```
313
+
314
+ The exported image is a quick-look representation for catching problems metadata can't reveal — blank output, stripes, artifacts, holes, wrong orientation, unexpected extent, or bad color scaling — **not** a publication-quality rendering.
315
+
316
+ > **Scope:** `--info` and `--export` tell you what a file *is* and what it *looks like*. Neither claims the scientific result is *correct* — that judgment stays with the agent.
219
317
 
220
318
  ## Need help?
221
319
  NASA staff can ask questions about usage via the documentation-based assistant 'viewtif + viewgeom + viewinline Helper' via the ChatGSFC Agent Marketplace.
@@ -0,0 +1,199 @@
1
+ ---
2
+ name: viewinline
3
+ description: Terminal viewer for rasters, vectors, and tabular data. Use for quick visual inspection without leaving the shell—preview images in a folder gallery, inspect geospatial files after GDAL workflows, explore CSV data with histograms/scatter plots, or verify files before commit. Provides machine-readable `--info` (metadata + statistics as JSON) and `--export` (save the rendered image) for programmatically checking generated geospatial files. Non-interactive; everything controlled via command-line flags.
4
+ tags: [visualization, terminal, raster, vector, csv, geospatial, gdal, ml, inspection, workflow]
5
+ ---
6
+
7
+ # Viewinline Skill
8
+
9
+ Quick-look terminal viewer for geospatial and tabular data. Think of it as `ls` for visual files—designed for command-line workflows where you want to inspect data without leaving the terminal or opening a separate application.
10
+
11
+ Renders natively in iTerm2, WezTerm, Konsole, Rio, and Contour. Falls back to `chafa` (ASCII art with 24-bit color) in other terminals like kitty, Ghostty, Terminal.app, VS Code, and Linux terminals. Works over SSH without X11 forwarding or VNC.
12
+
13
+ ## When to Use
14
+
15
+ - **File browsing**: `find . -name "*.jpg" | xargs -n1 viewinline` to visually scan results
16
+ - **Gallery view**: `viewinline path/to/folder --gallery 4x3` to preview all images in a directory
17
+ - **Band inspection**: Display specific bands of a raster as a grid: `--bands 10-50` or `--bands 11,15,30,45`
18
+ - **Geospatial workflows**: Inspect raster/vector outputs after GDAL transformations
19
+ - **ML training data**: Review training image galleries without switching windows
20
+ - **Data exploration**: Quick CSV visualizations (histograms, scatter plots, summary stats)
21
+ - **Pre-commit checks**: Verify images, maps, and data files before pushing
22
+ - **Remote servers**: Works over SSH from your local terminal
23
+ - **Verify generated files (agent workflows)**: After writing a GeoTIFF/NetCDF/vector with GDAL, rasterio, or xarray, run `viewinline FILE --info` to check the result is sensible (correct CRS, dimensions, value range, not all-NoData/NaN) as JSON, and `viewinline FILE --export out.png` to visually inspect it
24
+
25
+ ## Supported Formats
26
+
27
+ **Rasters:** GeoTIFF, PNG, JPEG, NetCDF, HDF5, HDF4
28
+ **Vectors:** GeoJSON, Shapefile, GeoPackage, GeoParquet
29
+ **Tabular:** CSV, Parquet (with `pyarrow`)
30
+
31
+ ## Inspecting Files (JSON output)
32
+
33
+ For programmatic checks, two flags return machine-readable output instead of drawing an image. Useful after generating a file to confirm it's sensible before continuing.
34
+
35
+ - `--info` — print file metadata and statistics as JSON, then exit. Reports facts, not judgments (the caller interprets them).
36
+ - Rasters (GeoTIFF, NetCDF, HDF): format, dimensions, bands, dtype, CRS, resolution, bounds, nodata, per-band stats (min/max/mean, valid_fraction, naninf_fraction).
37
+ - Vectors (GeoJSON, Shapefile, GeoPackage, GeoParquet): feature count, geometry type, CRS, bounds, columns.
38
+ - For NetCDF/HDF: `--info` lists variables/subdatasets; add `--subset N` to inspect one.
39
+ - Always returns JSON, including `{"readable": false, "error": ...}` on unreadable input — safe to parse in all cases.
40
+ - `--export PATH` — save the rendered image to PATH (`.png`/`.jpg`) and print `{"path": "..."}`. Works with any display flag (`--rgb`, `--colormap`, `--band`, `--display`), saving exactly what would be drawn.
41
+
42
+ ```bash
43
+ # check a raster you just wrote
44
+ viewinline result.tif --info
45
+
46
+ # check a vector
47
+ viewinline boundaries.geojson --info
48
+
49
+ # NetCDF: list variables, then inspect one
50
+ viewinline data.nc --info
51
+ viewinline data.nc --subset 7 --info
52
+
53
+ # save a quick-look image to inspect visually
54
+ viewinline result.tif --export out.png
55
+ viewinline scene.tif --rgb 4 3 2 --export rgb.png
56
+ ```
57
+
58
+ Note: `--info` and `--export` report what a file *is* and what it *looks like* — not whether the scientific result is correct.
59
+
60
+ ## Core Flags
61
+
62
+ **Inspection (JSON output, no image):**
63
+ - `--info` — print metadata + statistics as JSON, then exit (rasters and vectors; lists variables for NetCDF/HDF with `--subset N`)
64
+ - `--export PATH` — save the rendered image to PATH (`.png`/`.jpg`); prints `{"path": "..."}`
65
+
66
+ **Raster Display:**
67
+ - `--rgb R G B` — Specify band order for RGB (e.g., `--rgb 4 3 2`)
68
+ - `--rgbfiles R.tif G.tif B.tif` — Create RGB composite from separate files
69
+ - `--band N` — Display specific band or NetCDF timestep
70
+ - `--bands RANGE` — Display multiple bands as a grid (e.g., `10-50` or `11,15,30,45`)
71
+ - `--gallery NxM` — Show all images in folder as thumbnails (e.g., `4x3` grid)
72
+ - `--colormap NAME` — Apply colormap (e.g., `plasma`, `viridis`, `terrain`)
73
+ - `--vmin VAL --vmax VAL` — Set min/max for scaling
74
+ - `--display SCALE` — Resize displayed output (0.5=smaller, 2=bigger)
75
+
76
+ **Vector Display:**
77
+ - `--color-by COLUMN` — Color features by attribute value
78
+ - `--colormap NAME` — Apply colormap to coloring
79
+ - `--width W` — Line width for boundaries
80
+ - `--edgecolor COLOR` — Edge color (hex or named)
81
+ - `--table` — Display as tabular data instead of rendering geometry
82
+
83
+ **Tabular Data (CSV/Parquet):**
84
+ - `--describe [COL]` — Summary statistics for all numeric columns or one
85
+ - `--hist [COL]` — Histograms for numeric columns
86
+ - `--scatter X Y` — Scatter plot of two columns
87
+ - `--where EXPR` — Filter rows (DuckDB required): `--where "year > 2010"`
88
+ - `--sort COL` — Sort by column (ascending); use `--desc` for descending
89
+ - `--limit N` — Limit output rows
90
+ - `--select COL1 COL2` — Choose specific columns to display
91
+ - `--sql QUERY` — Full DuckDB SQL (use `data` as table name)
92
+
93
+ **NetCDF/HDF:**
94
+ - `--subset N` — Select variable by index
95
+ - `--band N` or `--timestep N` — Select slice along time/band axis
96
+ - `--reduce DIM_NAME` — Override auto-detected band axis for non-standard dimensions
97
+
98
+ ## Examples
99
+
100
+ ### Image browsing
101
+ ```bash
102
+ # Browse all JPGs in directory
103
+ find . -name "*.jpg" | xargs viewinline
104
+
105
+ # Gallery of all images in a folder (4 columns × 3 rows)
106
+ viewinline path/to/images --gallery 4x3
107
+ ```
108
+
109
+ ### Raster workflows
110
+ ```bash
111
+ # Display single band
112
+ viewinline data.tif --band 2
113
+
114
+ # RGB composite from three bands
115
+ viewinline multiband.tif --rgb 4 3 2
116
+
117
+ # Create RGB from separate files
118
+ viewinline R.tif G.tif B.tif
119
+
120
+ # Display band range as gallery
121
+ viewinline hyperspectral.tif --bands 10-50 --gallery 5x5
122
+
123
+ # Apply colormap with min/max scaling
124
+ viewinline temp.nc --subset 1 --colormap plasma --vmin 273 --vmax 310
125
+ ```
126
+
127
+ ### GDAL verification
128
+ ```bash
129
+ # Reproject and check result
130
+ gdalwarp -t_srs EPSG:3857 input.tif temp.tif
131
+ viewinline temp.tif --colormap terrain
132
+
133
+ # Resample and verify
134
+ gdal_translate -outsize 50% 50% temp.tif output.tif
135
+ viewinline output.tif
136
+ ```
137
+
138
+ ### Vector inspection
139
+ ```bash
140
+ # View vector file
141
+ viewinline boundaries.geojson
142
+
143
+ # Color features by attribute, with colormap
144
+ viewinline boundaries.geoparquet --color-by population --colormap viridis
145
+
146
+ # View vector as tabular data
147
+ viewinline counties.shp --table
148
+ viewinline data.geoparquet --table --where "POP > 100000" --sort POP --desc
149
+ ```
150
+
151
+ ### Data exploration
152
+ ```bash
153
+ # Preview CSV
154
+ viewinline data.csv
155
+
156
+ # Summary statistics
157
+ viewinline data.parquet --describe
158
+
159
+ # Histograms for all numeric columns
160
+ viewinline data.csv --hist
161
+
162
+ # Scatter plot
163
+ viewinline data.csv --scatter area_km2 year
164
+
165
+ # Filter and sort
166
+ viewinline data.csv --where "year > 2010" --sort population --desc
167
+
168
+ # SQL query
169
+ viewinline data.csv --sql "SELECT * FROM data WHERE area > 100 ORDER BY year"
170
+
171
+ # Batch CSV inspection with xargs (use -I {} to control argument order)
172
+ find . -name "*.csv" | xargs -n1 -I {} viewinline {} --describe
173
+ ```
174
+
175
+ ### Verify generated files (agent workflows)
176
+ ```bash
177
+ # Reproject, then check the result is sensible as JSON
178
+ gdalwarp -t_srs EPSG:3857 input.tif output.tif
179
+ viewinline output.tif --info
180
+
181
+ # Confirm a written raster isn't all-NoData / has expected CRS and range
182
+ viewinline result.tif --info
183
+
184
+ # List a NetCDF's variables, then inspect the data variable
185
+ viewinline output.nc --info
186
+ viewinline output.nc --subset 7 --info
187
+
188
+ # Save a quick-look PNG to inspect visually
189
+ viewinline result.tif --export check.png
190
+ ```
191
+
192
+ ## Tips
193
+
194
+ - Use `--gallery` to quickly preview batches of training images or search results
195
+ - Pipe with `xargs`: `find . -newer file.txt | xargs viewinline` to inspect recent changes
196
+ - Add aliases: `alias check='viewinline --describe --hist'`
197
+ - Works in tmux if outer terminal is iTerm2; falls back to ASCII art in other terminals
198
+ - Install `chafa` for better coverage: `brew install chafa` (macOS), `apt install chafa` (Linux)
199
+ - SSH-friendly: images render on your local terminal, not the remote server
@@ -0,0 +1,20 @@
1
+ ```json
2
+ {
3
+ "$schema": "https://context7.com/schema/context7.json",
4
+ "projectTitle": "viewinline",
5
+ "description": "Terminal inline geospatial preview tool for rasters and vectors over SSH and HPC workflows, with machine-readable --info and --export for AI-agent file inspection.",
6
+ "excludeFolders": [
7
+ "tests",
8
+ "examples/large-data",
9
+ "build",
10
+ "dist"
11
+ ],
12
+ "rules": [
13
+ "Prefer SSH and terminal-based workflows in examples.",
14
+ "Use inline terminal image rendering instead of GUI workflows.",
15
+ "Highlight support for GeoTIFF, NetCDF, GeoJSON, and GeoParquet.",
16
+ "Emphasize compatibility limitations for terminal graphics protocols.",
17
+ "For programmatic or AI-agent use, note --info (JSON metadata) and --export (save rendered image)."
18
+ ]
19
+ }
20
+ ```
@@ -0,0 +1,244 @@
1
+ # viewinline
2
+
3
+ > A tiny, non-interactive command-line viewer that displays rasters/photos, vectors, and tabular data (CSV/Parquet) inline in your terminal — no GUI, no X11, no file downloads. Think of it as `ls` for geospatial files: built for quick visual inspection at the command line, not a replacement for QGIS/ArcGIS or analytical workflows. Especially useful on HPC systems and remote servers over SSH, where images render on your *local* terminal. Also provides agent-facing `--info` (metadata as JSON) and `--export` (save the rendered image) for AI coding-agent workflows.
4
+
5
+ - Package: `viewinline` (PyPI: https://pypi.org/project/viewinline/)
6
+ - Repo: https://github.com/nkeikon/inlineviewer
7
+ - Command name: `viewinline`
8
+ - Requires: Python 3.9+
9
+ - License: Apache-2.0 © 2026 Keiko Nomura
10
+ - Latest release: v0.4.0 (2026-06-06)
11
+ - Combines the core display logic of `viewtif` and `viewgeom`, but is **non-interactive**: no zoom/pan/colormap-switching on the fly. Everything is controlled via CLI options (e.g. `--display`, `--color-by`, `--colormap`).
12
+
13
+ ## Installation
14
+
15
+ ```
16
+ pip install viewinline
17
+ pip install --upgrade viewinline # upgrade
18
+ pip install --upgrade "viewinline[netcdf]" # with hyperspectral/NetCDF extras
19
+ ```
20
+
21
+ Optional extras (install only if needed):
22
+ - `duckdb` — required for `--where`, `--sort`, `--select`, `--limit`, `--sql`. `pip install duckdb`
23
+ - `pyarrow` — required for Parquet/GeoParquet. `pip install pyarrow`
24
+ - `h5py` — fallback for HDF5 if GDAL lacks HDF5 support (usually unnecessary). `pip install h5py`
25
+ - `chafa` — system binary (not a Python package); strongly recommended for terminal coverage beyond the native list. See "Terminals".
26
+
27
+ ## How display works (rendering engines)
28
+
29
+ - Uses the **iTerm2 inline image protocol (OSC 1337)** natively in supported terminals.
30
+ - Falls back to **`chafa`** elsewhere, which routes through each terminal's best protocol: real high-res images via the kitty graphics protocol or sixel in some terminals, and 24-bit colored block-art (ASCII-art) previews in others.
31
+ - Without `chafa`, terminals outside the native list show an info message instead of an image.
32
+ - Force the chafa path on any terminal with the env var `INLINE_VIEWER_ENGINE=chafa`.
33
+
34
+ ## Terminals
35
+
36
+ Native (no extra install, via OSC 1337): iTerm2 (macOS), WezTerm, Konsole (KDE), Rio, Contour.
37
+
38
+ Via `chafa` (recommended for everyone else):
39
+ - Real high-res images (kitty graphics protocol / sixel): kitty, Ghostty, foot.
40
+ - Colored block-art previews (24-bit color): Terminal.app, VS Code, GNOME Terminal, Alacritty, Warp, Hyper, most Linux terminals.
41
+
42
+ Install chafa once (system binary, works across all conda/virtualenv environments):
43
+ ```
44
+ brew install chafa # macOS
45
+ sudo apt install chafa # Debian/Ubuntu
46
+ sudo dnf install chafa # Fedora
47
+ scoop install chafa # Windows
48
+ ```
49
+
50
+ - **SSH/HPC:** Works over SSH when connecting from a compatible terminal. Images render on the *local* machine, not the remote server. No X11 forwarding or VNC required.
51
+ - **tmux/screen:** Full inline images work inside tmux only when the *outer* terminal is iTerm2 (or WezTerm/Konsole/Rio/Contour). With other outer terminals (kitty, Terminal.app, etc.), viewinline shows ASCII/block-art previews instead.
52
+ - **Windows:** For real images, use Windows Terminal (v1.22+) or WezTerm. Stock PuTTY can show block-art via chafa.
53
+
54
+ ### Terminal detection & image-support logic
55
+
56
+ viewinline decides how to render by detecting the terminal, then routing native-OSC-1337 terminals one way and everything else through chafa.
57
+
58
+ Detection checks these environment variables (in order): `TERM_PROGRAM`, `KONSOLE_VERSION`, `KONSOLE_PROFILE_NAME`, `VTE_VERSION`, `TERMINATOR_UUID`, `ALACRITTY_SOCKET`, `WEZTERM_EXECUTABLE`, `ITERM_SESSION_ID`, and `TERM`. If none are set, it falls back to inspecting the parent process name (via `ps`).
59
+
60
+ Override: setting `INLINE_VIEWER_ENGINE=chafa` forces the chafa path on any terminal, bypassing detection.
61
+
62
+ Important nuance — terminals NOT on the native OSC-1337 list still get images via chafa; "not native" does **not** mean "no images." chafa auto-detects the terminal and picks the best output:
63
+ - `xterm-kitty` (kitty) → real images via the kitty graphics protocol
64
+ - foot, Ghostty, and similar → may render real images via sixel or the kitty protocol, depending on chafa's detection
65
+ - most others (Terminal.app, VS Code, GNOME Terminal, Alacritty, Warp, etc.) → Unicode block-art preview with 24-bit color
66
+
67
+ Only terminals **without chafa installed** (and outside the native OSC-1337 set) see no rendering at all — just an info message.
68
+
69
+ Terminals routed through chafa rather than native OSC 1337 (these are the values matched during detection): `Apple_Terminal` (Terminal.app), `xterm-kitty` (kitty), `screen`/`screen-256color`, `tmux`/`tmux-256color` (the `TMUX` env var also signals tmux), `vscode` (VS Code integrated terminal), `alacritty`, `foot`, `ghostty`/`xterm-ghostty`, `WarpTerminal` (Warp), `Hyper`, `unknown`, `cygwin`, `rxvt`/`rxvt-unicode`/`rxvt-unicode-256color`, `st-256color` (suckless st), `gnome-terminal`, `xfce4-terminal`, `lxterminal`, `terminator`, `tilix`, `sakura`, `terminology`, `guake`, `tilda`, `deepin-terminal`, `eterm`, `putty`, and `Windows Terminal`. (Most Linux desktop terminals are VTE-based and lack OSC 1337.) Terminals using native OSC 1337 — iTerm2, WezTerm, Konsole, Rio, Contour — are deliberately absent from this list.
70
+
71
+ ## Supported formats
72
+
73
+ Rasters: GeoTIFF (.tif, .tiff); PNG, JPEG (.png, .jpg, .jpeg); NetCDF (.nc); HDF5 (.h5, .hdf5); HDF4 (.hdf, requires GDAL with HDF4 support); single-band or multi-band composites.
74
+
75
+ Vectors: GeoJSON (.geojson), Shapefile (.shp), GeoPackage (.gpkg), Parquet/GeoParquet (.parquet, .geoparquet).
76
+
77
+ Tabular: CSV (.csv), Parquet (.parquet, requires pyarrow). All CSV operations work on Parquet.
78
+
79
+ HDF/NetCDF notes:
80
+ - HDF5 (.h5/.hdf5): via rasterio if GDAL has HDF5 support (most installs).
81
+ - HDF4 (.hdf): requires GDAL compiled with HDF4 support (legacy MODIS / older NASA products).
82
+ - NetCDF (.nc): via rasterio (GDAL's NetCDF driver).
83
+ - viewinline lists only variables displayable as 2D or 3D arrays. 3D variables with time or known spatial dims are auto-handled (sliced along the non-spatial axis). Variables with 4+ dimensions are not supported. For a full variable list, use `ncdump -h file.nc` or `viewtif`.
84
+
85
+ ## Usage examples
86
+
87
+ Rasters:
88
+ ```
89
+ viewinline path/to/file.tif
90
+ viewinline R.tif G.tif B.tif # RGB composite (also via --rgbfiles)
91
+ viewinline path/to/multiband.tif --rgb 3 2 1
92
+ viewinline path/to/folder --gallery 4x3 # gallery of all images in a folder
93
+ viewinline path/to/hyperspectral.tif --bands 10-50 # gallery of selected bands (also --bands 11,15,30,45)
94
+ ```
95
+
96
+ NetCDF / HDF:
97
+ ```
98
+ viewinline file.nc # list variables
99
+ viewinline file.nc --subset 2 # display variable 2
100
+ viewinline file.nc --subset 1 --band 10 # variable 1, timestep 10 (--band or --timestep)
101
+ viewinline temp.nc --subset 1 --colormap plasma --vmin 273 --vmax 310
102
+ viewinline hyperspectral.nc --subset 1 --reduce NumberOfScanlines # override auto-detected axis
103
+ viewinline hyperspectral.nc --subset 22 --band 50
104
+ viewinline hyperspectral.nc --subset 22 --bands 10-54 --gallery 5x11
105
+ ```
106
+
107
+ Vectors:
108
+ ```
109
+ viewinline path/to/vector.geojson
110
+ viewinline boundaries.geoparquet --color-by population --colormap viridis
111
+ ```
112
+
113
+ CSV / Parquet:
114
+ ```
115
+ viewinline data.csv # preview rows and columns
116
+ viewinline data.parquet --describe # summary statistics
117
+ viewinline data.csv --hist # histograms for all numeric columns
118
+ viewinline data.csv --hist area_km2 # histogram for one column
119
+ viewinline data.csv --scatter X Y # scatter plot
120
+ viewinline data.csv --where "year > 2010" # filter rows (DuckDB)
121
+ viewinline data.csv --sort population --desc # sort rows
122
+ viewinline data.csv --sql "SELECT * FROM data WHERE area > 100 ORDER BY year" # full SQL; table name is 'data'
123
+ ```
124
+
125
+ Tabular view of vectors (`--table` unlocks CSV-style ops on any vector file):
126
+ ```
127
+ viewinline counties.shp --table
128
+ viewinline counties.shp --table --describe
129
+ viewinline counties.shp --table --unique STATE_NAME
130
+ viewinline data.geoparquet --table --where "POP > 100000" --sort POP --desc
131
+ ```
132
+
133
+ ## CLI options
134
+
135
+ General:
136
+ - `--display DISPLAY` — resize only the displayed image (0.5 = smaller, 2 = bigger). Default: auto-fit to terminal.
137
+ - `--info` — print file metadata as JSON, then exit (no image drawn). Works with rasters (GeoTIFF, NetCDF, HDF) and vectors (GeoJSON, Shapefile, GeoPackage, GeoParquet). For NetCDF/HDF, lists variables; combine with `--subset N` to inspect one. Always returns JSON, including a `{"readable": false, "error": ...}` object on unreadable input. Aimed at AI coding agents.
138
+ - `--export PATH` — save the rendered image to PATH (`.png`/`.jpg`, chosen by extension) and open it; prints `{"path": "..."}` as the final line. Works with any display flag (`--rgb`, `--colormap`, `--band`, `--display`), saving exactly what viewinline would render. Useful for saving quick-looks and for AI agents inspecting outputs visually.
139
+ Raster:
140
+ - `--band BAND` — band number for a single raster, or slice number for NetCDF (default: 1).
141
+ - `--bands BANDS` — display multiple bands as a grid. Accepts ranges (`30-40`), lists (`3,4,5`), or mixed (`1,5,10-15`).
142
+ - `--rgb R G B` — three band numbers for RGB display (e.g. `--rgb 4 3 2`); overrides default `1 2 3`. Accepts space- or comma-separated values (`--rgb 4 3 2` or `--rgb 4,3,2`).
143
+ - `--rgbfiles R G B` — three single-band rasters for an RGB composite (can also be given as positional args).
144
+ - `--timestep INTEGER` — alias for `--band` with NetCDF files.
145
+ - `--subset INTEGER` — variable index for NetCDF/HDF files (e.g. `--subset 1`).
146
+ - `--reduce DIM_NAME` — for 3D NetCDF variables, choose which dimension is the band/slider axis. Auto-detected if omitted.
147
+ - `--colormap` — apply colormap to single-band rasters; flag with no value → `terrain`.
148
+ - `--vmin VMIN` / `--vmax VMAX` — pixel value range for display scaling (each works independently).
149
+ - `--nodata NODATA` — override nodata value when dataset metadata is missing/incorrect.
150
+ - `--gallery [GRID]` — display all PNG/JPG/TIF images in a folder as thumbnails (e.g. `5x5`).
151
+
152
+ Vector:
153
+ - `--color-by COLUMN` — color features by a column.
154
+ - `--colormap` — apply colormap to vector coloring; flag with no value → `terrain`.
155
+ - `--width WIDTH` — line width for vector boundaries (default: 0.7).
156
+ - `--edgecolor COLOR` — edge color for outlines, hex or named (default: white).
157
+ - `--layer LAYER` — layer name for GeoPackage/multi-layer files, or variable name for NetCDF.
158
+ - `--table` — display a vector/parquet file as tabular data instead of rendering geometry.
159
+
160
+ CSV / Parquet:
161
+ - `--describe [COLUMN]` — summary statistics for all numeric columns or one named column.
162
+ - `--hist [COLUMN]` — histograms for all numeric columns or one named column.
163
+ - `--bins BINS` — number of histogram bins (with `--hist`; default: 20).
164
+ - `--scatter X Y` — scatter plot of two numeric columns.
165
+ - `--unique COLUMN` — unique values for a categorical column.
166
+ - `--where EXPR` — filter rows with a SQL WHERE clause (DuckDB). Example: `--where "year > 2010"`.
167
+ - `--sort COLUMN` — sort by column (ascending by default; use `--desc`).
168
+ - `--desc` — descending sort (with `--sort`).
169
+ - `--limit N` — limit number of rows shown.
170
+ - `--select COLUMNS` — select specific columns, space-separated. Example: `--select Country City`.
171
+ - `--sql QUERY` — full DuckDB SQL query; use `data` as the table name. Example: `--sql "SELECT * FROM data WHERE Poverty > 40"`.
172
+
173
+ Help text is grouped into sections: General, Raster, Vector, and Tabular.
174
+
175
+ ## AI-agent inspection (`--info`, `--export`)
176
+
177
+ Two commands for AI coding-agent workflows (Claude Code, Codex, Cursor, and similar): an agent can confirm its code *ran* but not whether the geospatial file it produced is *sensible* (wrong CRS, unexpected dimensions, all-NoData, NaN/Inf values, flipped output). These answer "what did I create?" and "what does it look like?" as machine-readable JSON. Both are deterministic and local — no LLM dependency, no external API calls.
178
+
179
+ `--info` reports facts, not judgments, choosing fields per format:
180
+ - Rasters (GeoTIFF, NetCDF, HDF): format, dimensions, bands, dtype, CRS, resolution, bounds, nodata, per-band statistics (min/max/mean, valid_fraction, naninf_fraction).
181
+ - Vectors (GeoJSON, Shapefile, GeoPackage, GeoParquet): feature count, geometry type, CRS, bounds, columns.
182
+
183
+ viewinline result.tif --info # raster metadata + stats as JSON
184
+ viewinline boundaries.geojson --info # vector: features, geometry, CRS, columns
185
+ viewinline data.nc --info # list NetCDF variables
186
+ viewinline data.nc --subset 7 --info # inspect variable 7 (dims, dtype, units, stats)
187
+ viewinline scene.hdf --subset 1 --info # inspect HDF subdataset 1
188
+
189
+ Output notes:
190
+ - `crs` is an `EPSG:code` when one can be resolved, WKT when the CRS has no EPSG code (e.g. MODIS Sinusoidal), or `null` when absent.
191
+ - Statistics exclude NoData and non-finite pixels; large rasters are sampled (`"method": "sampled"`), small ones read in full (`"method": "full"`).
192
+ - Files with many bands report a capped subset (`bands_reported` < `bands_total`, plus a `note`); missing per-band stats do not imply a problem.
193
+ - Errors return structured JSON (`{"readable": false, "error": ...}`) rather than crashing, so callers can always parse the result.
194
+
195
+ `--export` saves what viewinline would render to PNG/JPEG and prints `{"path": "..."}`:
196
+ viewinline result.tif --export out.png
197
+ viewinline scene.tif --rgb 4 3 2 --export rgb.png
198
+ viewinline dem.tif --colormap terrain --display 1 --export dem.png # full-res with colormap
199
+
200
+ Neither command claims the scientific result is *correct* — that judgment stays with the agent.
201
+
202
+ ## Behavior notes (current)
203
+
204
+ - Multi-band rasters (multispectral, hyperspectral, embeddings) are NOT auto-composited as RGB. Band 1 is shown in grayscale by default (consistent with NetCDF). Use `--rgb` to composite explicitly. `--band N` shows any single band N (including band 1).
205
+ - `--gallery` silently skips incompatible/non-image files instead of failing the whole run.
206
+ - In chafa/ASCII terminals, band labels and gallery filenames are printed as a text grid *after* the image (labels drawn on the canvas are unreadable there), so you can tell tiles/files apart.
207
+ - `--bands` tiles are labeled with their band number; default colormap for `--bands` is viridis (override with `--colormap`). Works with GeoTIFF and NetCDF.
208
+ - For large CSV or filtered results, viewinline prompts `Show first N or all? [first/all]`.
209
+ - `--vmin` and `--vmax` are independent — setting only one no longer falls back to auto-scaling on both ends.
210
+
211
+ ## Hyperspectral / non-standard NetCDF
212
+
213
+ viewinline can open NetCDF files that organize variables under hierarchical groups (e.g. `/radiometric_data/CalibratedRadianceData`), use non-standard dimension names (`NumberOfChannels`, `NumberOfScanlines`, etc.), or have malformed CF attributes (e.g. per-band `scale_factor` arrays that xarray's default decoding can't handle).
214
+
215
+ Band-axis detection for 3D variables is tiered: user override (`--reduce`) → standard convention (lat/lon detected) → smallest-dimension fallback. If auto-detection picks the wrong axis, set it explicitly:
216
+ ```
217
+ viewinline PICARDL1B.nc --subset 22 --band 50
218
+ viewinline file.nc --subset 1 --reduce DIM_NAME
219
+ ```
220
+ Tested on NASA PICARD L1B; the same pattern should apply to AVIRIS, EMIT, and similar instruments.
221
+
222
+ ## Dependencies
223
+
224
+ Core (installed automatically): `rasterio` (raster reading, includes GDAL), `geopandas` + `pyogrio` (vector reading), `matplotlib` (vector rendering), `Pillow` (image encoding), `numpy`, `pandas`.
225
+
226
+ Optional: `chafa` (terminal coverage; system binary), `duckdb` (filter/sort/SQL), `pyarrow` (Parquet/GeoParquet), `h5py` (HDF5 fallback).
227
+
228
+ ## Version history (selected)
229
+
230
+ - **v0.3.2 (2026-06-06):** Band labels and gallery filenames shown as a text grid after the image in chafa/ASCII terminals; `--gallery NxM` now works with `--bands` for NetCDF; `--rgb` works with NetCDF when used with `--subset`; `--vmin`/`--vmax` work independently; `--rgb` accepts comma-separated values; help text grouped into sections; large-CSV prompt changed to `Show first N or all? [first/all]`.
231
+ - **v0.3.1 (2026-06-02):** New `--bands` flag (gallery grid of multiple bands; ranges/lists/mixed; per-tile band-number labels; viridis default); `--gallery` now silently skips incompatible files; multi-band rasters no longer auto-composite as RGB (band 1 grayscale by default — use `--rgb`).
232
+ - **v0.3.0 (2026-05-15):** Hyperspectral NetCDF support (hierarchical groups, non-standard dimension names, malformed CF attributes); new `--reduce` flag; tiered dimension detection; fixed `--band 1` being overridden by RGB auto-composite on multi-band TIFFs; consistent `(downsampled)` label; `[netcdf]` install extra.
233
+ - **v0.2.3 (2026-05-13):** Broad terminal support via chafa (real high-res on kitty/foot; block-art elsewhere); fixed Apple_Terminal / `xterm-kitty` misdetection; reliable block-art inside tmux. chafa fallback contributed by @filipkral.
234
+ - **v0.2.2 (2026-04-22):** Added Zenodo DOI for citation.
235
+ - **v0.2.1 (2026-02-21):** NetCDF/HDF support (`--subset`, `--band`/`--timestep`, auto nodata, HDF4 via GDAL); Parquet/GeoParquet; `--table` for vectors; `--rgb 3 2 1` syntax + `--rgbfiles`; auto edge-color removal when coloring by column.
236
+ - **v0.2.0 (2026-02-17):** Inline-only display (OSC 1337); removed ANSI fallback and /tmp save; DuckDB integration for `--where`/`--sort`/`--select`/`--limit`/`--sql`; license switched MIT → Apache-2.0.
237
+ - **v0.1.5 (2026-02-13):** Fixed duplicate band-display prints; optimized uint8 normalization; minor perf.
238
+ - **v0.4.0 (2026-08-23):** New agent-facing inspection commands: `--info` (file metadata + statistics as JSON, for rasters and vectors; lists variables for NetCDF/HDF with `--subset`; always returns JSON including structured errors) and `--export` (save the rendered image to PNG/JPEG, works with any display flag). Existing viewer behavior unchanged.
239
+
240
+ ## Support & links
241
+
242
+ - NASA staff can ask usage questions via the documentation-based assistant "viewtif + viewgeom + viewinline Helper" in the ChatGSFC Agent Marketplace.
243
+ - YouTube demo playlist: https://www.youtube.com/playlist?list=PLP9MNCMgJIHj6FvahJ6Tembp1rCyhLtR4
244
+ - Releases: https://github.com/nkeikon/inlineviewer/releases
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "viewinline"
7
- version = "0.3.2"
7
+ version = "0.4.0"
8
8
  description = "Quick look geospatial viewer for the terminal, with inline image previews"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -24,6 +24,7 @@ from PIL import Image, ImageOps, ImageDraw, ImageFont
24
24
  from matplotlib import colormaps
25
25
  import matplotlib as mpl
26
26
  import subprocess
27
+ import json
27
28
 
28
29
  import warnings
29
30
 
@@ -31,7 +32,10 @@ warnings.filterwarnings("ignore", message="More than one layer found", category=
31
32
  warnings.filterwarnings("ignore", message="Dataset has no geotransform", category=UserWarning)
32
33
  warnings.filterwarnings("ignore", message="invalid scale_factor or add_offset attribute", category=UserWarning)
33
34
 
34
- __version__ = "0.3.2"
35
+ _EXPORT_PATH = None
36
+ _EXPORT_RESULT = None
37
+
38
+ __version__ = "0.4.0"
35
39
 
36
40
  AVAILABLE_COLORMAPS = [
37
41
  "viridis", "inferno", "magma", "plasma",
@@ -203,25 +207,42 @@ def show_inline_image(image_array: np.ndarray, display_scale = None, is_vector:
203
207
 
204
208
  sys.stdout.flush()
205
209
 
210
+ def _open_in_viewer(path: str) -> None:
211
+ """Open a file in the OS default viewer. Best-effort; ignores failures."""
212
+ try:
213
+ if sys.platform == "darwin":
214
+ subprocess.run(["open", path], check=False)
215
+ elif sys.platform.startswith("linux"):
216
+ subprocess.run(["xdg-open", path], check=False)
217
+ elif sys.platform.startswith("win"):
218
+ os.startfile(path)
219
+ except Exception:
220
+ pass
221
+
206
222
  def show_image_auto(img: np.ndarray, display_scale=None, is_vector: bool = False) -> None:
207
- """Render an image inline, with chafa fallback for non-iTerm2 terminals.
208
-
209
- Cascade:
210
- 1. If terminal supports OSC 1337 → emit iTerm2 inline image sequence.
211
- 2. Else if chafa is installed → pipe through chafa (which auto-detects
212
- and emits the terminal's native graphics protocol or block-art).
213
- 3. Else print an info message suggesting chafa installation.
214
-
215
- The branching happens inside show_inline_image(); this wrapper handles
216
- status messaging and exception safety.
217
- """
223
+ global _EXPORT_RESULT
224
+ # Export mode: save the image, open it in the OS viewer, skip inline render.
225
+ if _EXPORT_PATH:
226
+ try:
227
+ out = Image.fromarray(img)
228
+ ext = os.path.splitext(_EXPORT_PATH)[1].lower()
229
+ if ext in (".jpg", ".jpeg"):
230
+ out.convert("RGB").save(_EXPORT_PATH, quality=90)
231
+ else:
232
+ out.save(_EXPORT_PATH)
233
+ saved_path = os.path.abspath(_EXPORT_PATH)
234
+ _open_in_viewer(saved_path)
235
+ _EXPORT_RESULT = {"path": saved_path}
236
+ except Exception as e:
237
+ _EXPORT_RESULT = {"error": str(e)}
238
+ return
239
+
218
240
  try:
219
241
  show_inline_image(img, display_scale, is_vector)
220
242
  if _TERMINAL_SUPPORTS_IMAGES:
221
243
  print("[VIEW] Inline render complete")
222
244
  elif is_chafa_available():
223
245
  print("[VIEW] Inline render complete via chafa")
224
- # If neither path applies, show_inline_image already printed the info message
225
246
  except Exception as e:
226
247
  print(f"[ERROR] Failed to render image: {e}")
227
248
  import traceback
@@ -273,7 +294,255 @@ def parse_rgb(values: list[str]) -> list[int]:
273
294
  except ValueError:
274
295
  print("[WARN] --rgb requires exactly 3 band numbers. e.g. --rgb 4 3 2 or --rgb 4,3,2")
275
296
  return None
276
-
297
+
298
+ def print_raster_info(path: str, subset: int = None) -> int:
299
+ """Agent-facing: print JSON metadata + basic stats for a raster. Returns exit code."""
300
+ try:
301
+ import rasterio
302
+ from rasterio.enums import Resampling
303
+ except ImportError:
304
+ print(json.dumps({"error": "rasterio not installed", "readable": False}))
305
+ return 1
306
+
307
+ # Resolve subdataset selection for HDF/NetCDF containers
308
+ if subset is not None and path.lower().endswith((".nc", ".hdf", ".hdf5", ".h5")):
309
+ try:
310
+ with rasterio.open(path) as ds:
311
+ subs = ds.subdatasets
312
+ if subs:
313
+ if subset < 1 or subset > len(subs):
314
+ print(json.dumps({"error": f"--subset must be between 1 and {len(subs)}",
315
+ "readable": False, "subdataset_count": len(subs)}))
316
+ return 1
317
+ # path = subs[subset - 1]
318
+ path = subs[subset - 1]
319
+ subset_var = path.split(":")[-1]
320
+ except Exception as e:
321
+ print(json.dumps({"error": str(e), "readable": False}))
322
+ return 1
323
+
324
+ try:
325
+ with rasterio.open(path) as ds:
326
+ # Container with no directly-readable bands (e.g. HDF/NetCDF without --subset)
327
+ if ds.count == 0:
328
+ subs = ds.subdatasets
329
+ print(json.dumps({
330
+ "format": ds.driver,
331
+ "filename": os.path.basename(path),
332
+ "readable": False,
333
+ "error": "no directly-readable bands (file has subdatasets)",
334
+ "subdataset_count": len(subs),
335
+ "note": "Use --subset N to inspect a specific subdataset.",
336
+ }))
337
+ return 1
338
+
339
+ if ds.crs:
340
+ epsg = ds.crs.to_epsg() or ds.crs.to_epsg(confidence_threshold=20)
341
+ crs_str = f"EPSG:{epsg}" if epsg else ds.crs.to_string()
342
+ else:
343
+ crs_str = None
344
+
345
+ info = {
346
+ "format": ds.driver,
347
+ "filename": os.path.basename(path),
348
+ "dimensions": [ds.width, ds.height],
349
+ "bands": ds.count,
350
+ "dtype": ds.dtypes[0],
351
+ "crs": crs_str,
352
+ "resolution": [abs(ds.transform.a), abs(ds.transform.e)],
353
+ "bounds": [ds.bounds.left, ds.bounds.bottom, ds.bounds.right, ds.bounds.top],
354
+ "nodata": ds.nodata,
355
+ }
356
+
357
+ H, W = ds.height, ds.width
358
+ cap = 1024
359
+ sampled = max(H, W) > cap
360
+ if sampled:
361
+ s = cap / max(H, W)
362
+ out_shape = (max(1, int(H * s)), max(1, int(W * s)))
363
+
364
+ MAX_BANDS = 20
365
+ n_report = min(ds.count, MAX_BANDS)
366
+ per_band = []
367
+ for b in range(1, n_report + 1):
368
+ if sampled:
369
+ arr = ds.read(b, out_shape=out_shape,
370
+ resampling=Resampling.nearest).astype("float64")
371
+ else:
372
+ arr = ds.read(b).astype("float64")
373
+
374
+ valid = np.isfinite(arr)
375
+ if ds.nodata is not None:
376
+ valid &= (arr != ds.nodata)
377
+ n_total, n_valid = arr.size, int(valid.sum())
378
+
379
+ bs = {"band": b}
380
+ if n_valid:
381
+ v = arr[valid]
382
+ bs.update({"min": float(v.min()), "max": float(v.max()), "mean": float(v.mean())})
383
+ else:
384
+ bs.update({"min": None, "max": None, "mean": None})
385
+ bs["valid_fraction"] = round(n_valid / n_total, 4) if n_total else 0.0
386
+ bs["naninf_fraction"] = round(int((~np.isfinite(arr)).sum()) / n_total, 4) if n_total else 0.0
387
+ per_band.append(bs)
388
+
389
+ stats_block = {
390
+ "method": "sampled" if sampled else "full",
391
+ "bands_total": ds.count,
392
+ "bands_reported": n_report,
393
+ "per_band": per_band,
394
+ }
395
+ if n_report < ds.count:
396
+ stats_block["band_sampling"] = "first_n"
397
+ stats_block["note"] = (f"Showing bands 1-{n_report} of {ds.count}. "
398
+ f"Remaining bands not analyzed; absence of stats does not imply a problem.")
399
+ info["statistics"] = stats_block
400
+
401
+ try:
402
+ info["storage"] = {
403
+ "tiled": bool(ds.profile.get("tiled", False)),
404
+ "compression": ds.compression.value if ds.compression else None,
405
+ "overviews": ds.overviews(1),
406
+ }
407
+ except Exception:
408
+ pass
409
+
410
+ print(json.dumps(info))
411
+ return 0
412
+ except Exception as e:
413
+ print(json.dumps({"error": str(e), "readable": False}))
414
+ return 1
415
+
416
+ def print_netcdf_info(path: str, subset: int = None) -> int:
417
+ """Agent-facing: JSON metadata for a NetCDF file via netCDF4 (matches viewer's variable indexing)."""
418
+ try:
419
+ import netCDF4
420
+ except ImportError:
421
+ print(json.dumps({"error": "netCDF4 not installed", "readable": False}))
422
+ return 1
423
+ try:
424
+ nc = netCDF4.Dataset(path)
425
+ except Exception as e:
426
+ print(json.dumps({"error": str(e), "readable": False}))
427
+ return 1
428
+
429
+ # Same recursive enumeration the renderer uses, so indices match
430
+ def collect_vars(group, prefix=""):
431
+ out = []
432
+ for name, var in group.variables.items():
433
+ out.append((f"{prefix}{name}", var))
434
+ for sub_name, sub in group.groups.items():
435
+ out.extend(collect_vars(sub, f"{prefix}{sub_name}/"))
436
+ return out
437
+
438
+ all_vars = collect_vars(nc)
439
+ if not all_vars:
440
+ print(json.dumps({"error": "no variables found", "readable": False}))
441
+ nc.close()
442
+ return 1
443
+
444
+ # No --subset: list variables (mirrors the viewer's listing)
445
+ if subset is None:
446
+ variables = [{
447
+ "index": i,
448
+ "name": name,
449
+ "shape": list(var.shape),
450
+ "dtype": str(var.dtype),
451
+ "dimensions": list(var.dimensions),
452
+ } for i, (name, var) in enumerate(all_vars, 1)]
453
+ print(json.dumps({
454
+ "format": "netCDF",
455
+ "filename": os.path.basename(path),
456
+ "variable_count": len(all_vars),
457
+ "variables": variables,
458
+ "note": "Use --subset N to inspect a specific variable.",
459
+ }))
460
+ nc.close()
461
+ return 0
462
+
463
+ # --subset given: inspect that variable
464
+ if subset < 1 or subset > len(all_vars):
465
+ print(json.dumps({"error": f"--subset must be between 1 and {len(all_vars)}",
466
+ "readable": False, "variable_count": len(all_vars)}))
467
+ nc.close()
468
+ return 1
469
+
470
+ var_name, var = all_vars[subset - 1]
471
+ info = {
472
+ "format": "netCDF",
473
+ "filename": os.path.basename(path),
474
+ "subset": subset,
475
+ "variable": var_name,
476
+ "shape": list(var.shape),
477
+ "dtype": str(var.dtype),
478
+ "dimensions": list(var.dimensions),
479
+ }
480
+
481
+ # Attributes worth surfacing if present
482
+ for attr in ("units", "long_name", "standard_name"):
483
+ if hasattr(var, attr):
484
+ info[attr] = str(getattr(var, attr))
485
+
486
+ fill = getattr(var, "_FillValue", None)
487
+ info["fill_value"] = float(fill) if fill is not None else None
488
+
489
+ # Stats on a sample: read one slice if 3D+, or the whole thing if small 2D/1D
490
+ try:
491
+ if var.ndim >= 3:
492
+ # sample first slice along axis 0 to avoid loading the full cube
493
+ arr = np.asarray(var[0], dtype="float64")
494
+ stats_scope = f"first slice along '{var.dimensions[0]}' (of {var.shape[0]})"
495
+ else:
496
+ arr = np.asarray(var[:], dtype="float64")
497
+ stats_scope = "full variable"
498
+
499
+ valid = np.isfinite(arr)
500
+ if fill is not None:
501
+ valid &= (arr != fill)
502
+ n_total, n_valid = arr.size, int(valid.sum())
503
+ stats = {"scope": stats_scope}
504
+ if n_valid:
505
+ v = arr[valid]
506
+ stats.update({"min": float(v.min()), "max": float(v.max()), "mean": float(v.mean())})
507
+ else:
508
+ stats.update({"min": None, "max": None, "mean": None})
509
+ stats["valid_fraction"] = round(n_valid / n_total, 4) if n_total else 0.0
510
+ stats["naninf_fraction"] = round(int((~np.isfinite(arr)).sum()) / n_total, 4) if n_total else 0.0
511
+ info["statistics"] = stats
512
+ except Exception as e:
513
+ info["statistics"] = {"error": str(e)}
514
+
515
+ print(json.dumps(info))
516
+ nc.close()
517
+ return 0
518
+
519
+ def print_vector_info(path: str) -> int:
520
+ try:
521
+ import geopandas as gpd
522
+ except ImportError:
523
+ print(json.dumps({"error": "geopandas not installed", "readable": False}))
524
+ return 1
525
+ try:
526
+ if path.lower().endswith((".parquet", ".geoparquet")):
527
+ gdf = gpd.read_parquet(path)
528
+ else:
529
+ gdf = gpd.read_file(path)
530
+ b = gdf.total_bounds # [minx, miny, maxx, maxy]
531
+ info = {
532
+ "format": "vector",
533
+ "filename": os.path.basename(path),
534
+ "features": len(gdf),
535
+ "geometry_type": str(gdf.geom_type.iloc[0]) if len(gdf) else None,
536
+ "crs": (f"EPSG:{gdf.crs.to_epsg()}" if gdf.crs and gdf.crs.to_epsg()
537
+ else (gdf.crs.to_string() if gdf.crs else None)),
538
+ "bounds": [float(x) for x in b],
539
+ "columns": [c for c in gdf.columns if c != gdf.geometry.name],
540
+ }
541
+ print(json.dumps(info))
542
+ return 0
543
+ except Exception as e:
544
+ print(json.dumps({"error": str(e), "readable": False}))
545
+ return 1
277
546
  # ---------------------------------------------------------------------
278
547
  # CSV handling
279
548
  # ---------------------------------------------------------------------
@@ -1491,7 +1760,8 @@ def render_vector(path, args):
1491
1760
  # ---------------------------------------------------------------------
1492
1761
  import argparse
1493
1762
 
1494
- class SmartDefaults(argparse.ArgumentDefaultsHelpFormatter):
1763
+ class SmartDefaults(argparse.ArgumentDefaultsHelpFormatter,
1764
+ argparse.RawDescriptionHelpFormatter):
1495
1765
  """Show defaults only when meaningful (not None or SUPPRESS)."""
1496
1766
  def _get_help_string(self, action):
1497
1767
  if action.help and "%(default)" in action.help:
@@ -1662,8 +1932,16 @@ def main() -> None:
1662
1932
  "Supports rasters (.tif, .tiff, .png, .jpg, .jpeg), "
1663
1933
  "vectors (.shp, .geojson, .gpkg), and CSV preview.\n"
1664
1934
  "Sends iTerm2 inline image protocol — visible in compatible terminals."
1935
+ ),
1936
+ formatter_class=SmartDefaults,
1937
+ epilog=(
1938
+ "Agent interface (machine-readable JSON):\n"
1939
+ " viewinline result.tif --info # metadata + stats as JSON\n"
1940
+ " viewinline result.tif --export out.png # save PNG, print {\"path\": ...}\n"
1941
+ " viewinline data.nc --info # list NetCDF variables\n"
1942
+ " viewinline data.nc --subset 7 --info # inspect variable 7\n"
1943
+ " viewinline scene.hdf --subset 1 --info # inspect HDF subdataset 1\n"
1665
1944
  ),
1666
- formatter_class=SmartDefaults
1667
1945
  )
1668
1946
 
1669
1947
  # File input
@@ -1682,6 +1960,11 @@ def main() -> None:
1682
1960
  "--gallery", nargs="?", const="4x4", metavar="GRID",
1683
1961
  help="Display all image files in a folder as thumbnails (e.g., --gallery 5x4). Incompatible files are skipped."
1684
1962
  )
1963
+ general.add_argument("--info", action="store_true",
1964
+ help="Print JSON metadata (format, CRS, dims, stats) and exit. Agent-facing.")
1965
+
1966
+ general.add_argument("--export", metavar="PATH", default=None,
1967
+ help="Also save the displayed image to PATH (.png/.jpg). Prints {\"path\": ...}.")
1685
1968
 
1686
1969
  # Raster options
1687
1970
  raster = parser.add_argument_group("Raster")
@@ -1827,6 +2110,28 @@ def main() -> None:
1827
2110
 
1828
2111
  paths = args.paths
1829
2112
 
2113
+ if args.info:
2114
+ if len(paths) != 1:
2115
+ print(json.dumps({"error": "--info requires exactly one file"}))
2116
+ sys.exit(1)
2117
+ p = paths[0].lower()
2118
+ vector_exts = (".shp", ".geojson", ".json", ".gpkg", ".parquet", ".geoparquet")
2119
+ if p.endswith(".nc"):
2120
+ sys.exit(print_netcdf_info(paths[0], subset=args.subset))
2121
+ if p.endswith(vector_exts):
2122
+ sys.exit(print_vector_info(paths[0]))
2123
+ sys.exit(print_raster_info(paths[0], subset=args.subset))
2124
+
2125
+ if args.export:
2126
+ globals()["_EXPORT_PATH"] = args.export
2127
+ import atexit
2128
+ _real_stdout = sys.stdout
2129
+ sys.stdout = sys.stderr # send [DATA]/[INFO] logs to stderr, keep stdout clean
2130
+ def _emit_export_json(_out=_real_stdout):
2131
+ sys.stdout = _out
2132
+ print(json.dumps(_EXPORT_RESULT or {"error": "no image rendered"}))
2133
+ atexit.register(_emit_export_json)
2134
+
1830
2135
  # File routing
1831
2136
  raster_exts = (".png", ".jpg", ".jpeg", ".tif", ".tiff", ".nc", ".hdf", ".hdf5", ".h5")
1832
2137
  vector_exts = (".shp", ".geojson", ".json", ".gpkg", ".parquet", "geoparquet")
@@ -1910,4 +2215,4 @@ def main() -> None:
1910
2215
 
1911
2216
 
1912
2217
  if __name__ == "__main__":
1913
- main()
2218
+ main()
Binary file
File without changes
File without changes