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.
- viewinline-0.4.0/CITATION.cff +44 -0
- {viewinline-0.3.2 → viewinline-0.4.0}/PKG-INFO +100 -2
- {viewinline-0.3.2 → viewinline-0.4.0}/README.md +98 -0
- viewinline-0.4.0/SKILL.md +199 -0
- viewinline-0.4.0/context7.json +20 -0
- viewinline-0.4.0/llms.txt +244 -0
- {viewinline-0.3.2 → viewinline-0.4.0}/pyproject.toml +1 -1
- {viewinline-0.3.2 → viewinline-0.4.0}/src/viewinline/viewinline.py +322 -17
- viewinline-0.4.0/viewinline_gif2.jpg +0 -0
- {viewinline-0.3.2 → viewinline-0.4.0}/.github/FUNDING.yml +0 -0
- {viewinline-0.3.2 → viewinline-0.4.0}/.gitignore +0 -0
- {viewinline-0.3.2 → viewinline-0.4.0}/LICENSE +0 -0
- {viewinline-0.3.2 → viewinline-0.4.0}/src/viewinline/__init__.py +0 -0
- {viewinline-0.3.2 → viewinline-0.4.0}/viewinline_gif1.gif +0 -0
- {viewinline-0.3.2 → viewinline-0.4.0}/viewinline_gif2.gif +0 -0
|
@@ -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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: viewinline
|
|
3
|
-
Version: 0.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|