viewinline 0.4.0__tar.gz → 0.5.1__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.5.1/.gitignore +5 -0
- {viewinline-0.4.0 → viewinline-0.5.1}/CITATION.cff +8 -7
- {viewinline-0.4.0 → viewinline-0.5.1}/PKG-INFO +17 -1
- {viewinline-0.4.0 → viewinline-0.5.1}/README.md +16 -0
- {viewinline-0.4.0 → viewinline-0.5.1}/context7.json +2 -3
- {viewinline-0.4.0 → viewinline-0.5.1}/llms.txt +16 -1
- {viewinline-0.4.0 → viewinline-0.5.1}/pyproject.toml +1 -1
- {viewinline-0.4.0 → viewinline-0.5.1/skills/viewinline}/SKILL.md +12 -2
- {viewinline-0.4.0 → viewinline-0.5.1}/src/viewinline/viewinline.py +162 -7
- viewinline-0.5.1/tests/test_missing_image.py +39 -0
- viewinline-0.4.0/.gitignore +0 -4
- {viewinline-0.4.0 → viewinline-0.5.1}/.github/FUNDING.yml +0 -0
- {viewinline-0.4.0 → viewinline-0.5.1}/LICENSE +0 -0
- {viewinline-0.4.0 → viewinline-0.5.1}/src/viewinline/__init__.py +0 -0
- {viewinline-0.4.0 → viewinline-0.5.1}/viewinline_gif1.gif +0 -0
- {viewinline-0.4.0 → viewinline-0.5.1}/viewinline_gif2.gif +0 -0
- {viewinline-0.4.0 → viewinline-0.5.1}/viewinline_gif2.jpg +0 -0
|
@@ -11,26 +11,27 @@ type: software
|
|
|
11
11
|
authors:
|
|
12
12
|
- given-names: Keiko
|
|
13
13
|
family-names: Nomura
|
|
14
|
-
|
|
14
|
+
orcid: "https://orcid.org/0000-0002-5218-3414"
|
|
15
15
|
- given-names: Filip
|
|
16
16
|
family-names: Kral
|
|
17
|
-
|
|
17
|
+
- given-names: Luis
|
|
18
|
+
family-names: Manrique Ruiz
|
|
18
19
|
repository-code: "https://github.com/nkeikon/inlineviewer"
|
|
19
20
|
url: "https://github.com/nkeikon/inlineviewer"
|
|
20
21
|
license: Apache-2.0
|
|
21
|
-
version: 0.
|
|
22
|
-
date-released: 2026-
|
|
22
|
+
version: 0.5.1
|
|
23
|
+
date-released: 2026-11-10
|
|
23
24
|
# ---------------------------------------------------------------------------
|
|
24
25
|
# The DOI below is the VERSION DOI for v0.3.2 (a specific archived snapshot).
|
|
25
26
|
# To make citations always resolve to your LATEST release, replace it with the
|
|
26
27
|
# Zenodo CONCEPT / "all versions" DOI, shown on the record under "Cite all
|
|
27
28
|
# versions". The version DOI stays useful in `identifiers` for reproducibility.
|
|
28
29
|
# ---------------------------------------------------------------------------
|
|
29
|
-
doi: "10.5281/zenodo.
|
|
30
|
+
doi: "10.5281/zenodo.19700943"
|
|
30
31
|
identifiers:
|
|
31
32
|
- type: doi
|
|
32
|
-
value: "10.5281/zenodo.
|
|
33
|
-
description: "Zenodo archive
|
|
33
|
+
value: "10.5281/zenodo.19700943"
|
|
34
|
+
description: "Zenodo archive"
|
|
34
35
|
keywords:
|
|
35
36
|
- geospatial
|
|
36
37
|
- raster
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: viewinline
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
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
|
|
@@ -86,6 +86,12 @@ viewinline path/to/file.tif --export out.png # save the rendered image as PNG
|
|
|
86
86
|
viewinline scene.tif --rgb 4 3 2 --export rgb.png
|
|
87
87
|
viewinline dem.tif --colormap terrain --display 1 --export dem.png # full-res with colormap
|
|
88
88
|
|
|
89
|
+
# Axis and grid overlay
|
|
90
|
+
viewinline path/to/file.tif --grid # ticks + grid in the raster's CRS units (5 divisions)
|
|
91
|
+
viewinline path/to/file.tif --grid 10 # 10 divisions instead of the default 5
|
|
92
|
+
viewinline boundaries.geoparquet --grid # ticks + grid in the vector's CRS units
|
|
93
|
+
viewinline photo.png --grid # pixel positions (no CRS)
|
|
94
|
+
|
|
89
95
|
# CSV and Parquet
|
|
90
96
|
viewinline data.csv # preview rows and columns
|
|
91
97
|
viewinline data.parquet --describe # summary statistics
|
|
@@ -103,6 +109,15 @@ viewinline counties.shp --table --unique STATE_NAME
|
|
|
103
109
|
viewinline data.geoparquet --table --where "POP > 100000" --sort POP --desc
|
|
104
110
|
```
|
|
105
111
|
|
|
112
|
+
## Use with Claude Code
|
|
113
|
+
|
|
114
|
+
viewinline includes a skill that lets Claude Code inspect files with `--info` and offer quick-look previews with `--export`. To install:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
mkdir -p ~/.claude/skills/viewinline
|
|
118
|
+
curl -o ~/.claude/skills/viewinline/SKILL.md https://raw.githubusercontent.com/nkeikon/inlineviewer/main/skills/viewinline/SKILL.md
|
|
119
|
+
```
|
|
120
|
+
|
|
106
121
|
## Compatible terminals
|
|
107
122
|
|
|
108
123
|
Native (no extra install required): images render via the iTerm2 inline image protocol on:
|
|
@@ -222,6 +237,7 @@ General:
|
|
|
222
237
|
--display DISPLAY Resize only the displayed image (0.5=smaller, 2=bigger). Default: auto-fit to terminal.
|
|
223
238
|
--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
239
|
--export PATH Save the rendered image to PATH (.png/.jpg) and open it. Works with any display flag. Prints {"path": "..."} as the final line.
|
|
240
|
+
--grid [N] Overlay axis ticks and a light grid (N divisions, default 5). Labels are in the file's CRS units (degrees or metres) for georeferenced rasters and vectors; pixel positions for PNG/JPG, NetCDF, and non-georeferenced files.
|
|
225
241
|
|
|
226
242
|
Raster:
|
|
227
243
|
--band BAND Band number to display (single raster), or slice number for NetCDF. (default: 1)
|
|
@@ -53,6 +53,12 @@ viewinline path/to/file.tif --export out.png # save the rendered image as PNG
|
|
|
53
53
|
viewinline scene.tif --rgb 4 3 2 --export rgb.png
|
|
54
54
|
viewinline dem.tif --colormap terrain --display 1 --export dem.png # full-res with colormap
|
|
55
55
|
|
|
56
|
+
# Axis and grid overlay
|
|
57
|
+
viewinline path/to/file.tif --grid # ticks + grid in the raster's CRS units (5 divisions)
|
|
58
|
+
viewinline path/to/file.tif --grid 10 # 10 divisions instead of the default 5
|
|
59
|
+
viewinline boundaries.geoparquet --grid # ticks + grid in the vector's CRS units
|
|
60
|
+
viewinline photo.png --grid # pixel positions (no CRS)
|
|
61
|
+
|
|
56
62
|
# CSV and Parquet
|
|
57
63
|
viewinline data.csv # preview rows and columns
|
|
58
64
|
viewinline data.parquet --describe # summary statistics
|
|
@@ -70,6 +76,15 @@ viewinline counties.shp --table --unique STATE_NAME
|
|
|
70
76
|
viewinline data.geoparquet --table --where "POP > 100000" --sort POP --desc
|
|
71
77
|
```
|
|
72
78
|
|
|
79
|
+
## Use with Claude Code
|
|
80
|
+
|
|
81
|
+
viewinline includes a skill that lets Claude Code inspect files with `--info` and offer quick-look previews with `--export`. To install:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
mkdir -p ~/.claude/skills/viewinline
|
|
85
|
+
curl -o ~/.claude/skills/viewinline/SKILL.md https://raw.githubusercontent.com/nkeikon/inlineviewer/main/skills/viewinline/SKILL.md
|
|
86
|
+
```
|
|
87
|
+
|
|
73
88
|
## Compatible terminals
|
|
74
89
|
|
|
75
90
|
Native (no extra install required): images render via the iTerm2 inline image protocol on:
|
|
@@ -189,6 +204,7 @@ General:
|
|
|
189
204
|
--display DISPLAY Resize only the displayed image (0.5=smaller, 2=bigger). Default: auto-fit to terminal.
|
|
190
205
|
--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
206
|
--export PATH Save the rendered image to PATH (.png/.jpg) and open it. Works with any display flag. Prints {"path": "..."} as the final line.
|
|
207
|
+
--grid [N] Overlay axis ticks and a light grid (N divisions, default 5). Labels are in the file's CRS units (degrees or metres) for georeferenced rasters and vectors; pixel positions for PNG/JPG, NetCDF, and non-georeferenced files.
|
|
192
208
|
|
|
193
209
|
Raster:
|
|
194
210
|
--band BAND Band number to display (single raster), or slice number for NetCDF. (default: 1)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
```json
|
|
2
1
|
{
|
|
3
2
|
"$schema": "https://context7.com/schema/context7.json",
|
|
4
3
|
"projectTitle": "viewinline",
|
|
@@ -14,7 +13,7 @@
|
|
|
14
13
|
"Use inline terminal image rendering instead of GUI workflows.",
|
|
15
14
|
"Highlight support for GeoTIFF, NetCDF, GeoJSON, and GeoParquet.",
|
|
16
15
|
"Emphasize compatibility limitations for terminal graphics protocols.",
|
|
17
|
-
"For programmatic or AI-agent use, note --info (JSON metadata) and --export (save rendered image)."
|
|
16
|
+
"For programmatic or AI-agent use, note --info (JSON metadata) and --export (save rendered image).",
|
|
17
|
+
"For Claude Code users, mention the viewinline skill at skills/viewinline/SKILL.md and how to install it."
|
|
18
18
|
]
|
|
19
19
|
}
|
|
20
|
-
```
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
- Command name: `viewinline`
|
|
8
8
|
- Requires: Python 3.9+
|
|
9
9
|
- License: Apache-2.0 © 2026 Keiko Nomura
|
|
10
|
-
- Latest release: v0.
|
|
10
|
+
- Latest release: v0.5.1 (2026-11-10)
|
|
11
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
12
|
|
|
13
13
|
## Installation
|
|
@@ -24,6 +24,17 @@ Optional extras (install only if needed):
|
|
|
24
24
|
- `h5py` — fallback for HDF5 if GDAL lacks HDF5 support (usually unnecessary). `pip install h5py`
|
|
25
25
|
- `chafa` — system binary (not a Python package); strongly recommended for terminal coverage beyond the native list. See "Terminals".
|
|
26
26
|
|
|
27
|
+
## Use with Claude Code
|
|
28
|
+
|
|
29
|
+
viewinline has a Claude Code skill. It is a separate install from the pip package:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
mkdir -p ~/.claude/skills/viewinline
|
|
33
|
+
curl -o ~/.claude/skills/viewinline/SKILL.md https://raw.githubusercontent.com/nkeikon/inlineviewer/main/skills/viewinline/SKILL.md
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Start a new Claude Code session after installing. The skill is at skills/viewinline/SKILL.md in the GitHub repository.
|
|
37
|
+
|
|
27
38
|
## How display works (rendering engines)
|
|
28
39
|
|
|
29
40
|
- Uses the **iTerm2 inline image protocol (OSC 1337)** natively in supported terminals.
|
|
@@ -136,6 +147,8 @@ General:
|
|
|
136
147
|
- `--display DISPLAY` — resize only the displayed image (0.5 = smaller, 2 = bigger). Default: auto-fit to terminal.
|
|
137
148
|
- `--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
149
|
- `--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.
|
|
150
|
+
- `--grid [N]` — overlay axis ticks and a light grid (N divisions, default 5). Labels are in the file's CRS units (degrees or metres) for georeferenced rasters and vectors; pixel positions for PNG/JPG, NetCDF, and non-georeferenced files. Works with `--export`.
|
|
151
|
+
|
|
139
152
|
Raster:
|
|
140
153
|
- `--band BAND` — band number for a single raster, or slice number for NetCDF (default: 1).
|
|
141
154
|
- `--bands BANDS` — display multiple bands as a grid. Accepts ranges (`30-40`), lists (`3,4,5`), or mixed (`1,5,10-15`).
|
|
@@ -236,6 +249,8 @@ Optional: `chafa` (terminal coverage; system binary), `duckdb` (filter/sort/SQL)
|
|
|
236
249
|
- **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
250
|
- **v0.1.5 (2026-02-13):** Fixed duplicate band-display prints; optimized uint8 normalization; minor perf.
|
|
238
251
|
- **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.
|
|
252
|
+
- **v0.5.0 (2026-10-21):** New `--grid [N]` flag: axis ticks and a light grid overlay. Georeferenced rasters and vectors are labelled in real CRS coordinates; photos, NetCDF, and non-georeferenced files in pixel positions. Grid contributed by @luismanriqueruiz.
|
|
253
|
+
- **v0.5.1 (2026-11-10):** Bug fix: viewinline now exits with code 1 (instead of 0) when an input file is missing or unreadable — PNG/JPG, GeoTIFF, vectors (.shp, .geojson, .gpkg), and NetCDF — so scripts and agents can detect the failure. Missing PNG/JPG fix contributed by @Qrytics (#8).
|
|
239
254
|
|
|
240
255
|
## Support & links
|
|
241
256
|
|
|
@@ -10,6 +10,15 @@ Quick-look terminal viewer for geospatial and tabular data. Think of it as `ls`
|
|
|
10
10
|
|
|
11
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
12
|
|
|
13
|
+
## Inside Claude Code
|
|
14
|
+
|
|
15
|
+
To show the user an image, use `--export`. It saves a PNG and opens it in the user's default image viewer, and you can read the same PNG. Ask before running it, e.g. "Want to see a quick-look of `ndvi.tif`?" If the user asks to see a file, run it directly. If they say to always or never show images, follow that for the rest of the session.
|
|
16
|
+
|
|
17
|
+
- Use `--info` freely for your own checks. It prints JSON and opens nothing.
|
|
18
|
+
- A plain `viewinline file.tif` run from the shell does not reach the user, because the output is captured as text. Don't claim you showed them an image that way.
|
|
19
|
+
- CSV and Parquet previews with more than 10 rows ask `[first/all]`. Pipe the answer in: `echo first | viewinline data.csv`.
|
|
20
|
+
- Delete exported PNGs once they're no longer needed, unless the user wants to keep them.
|
|
21
|
+
|
|
13
22
|
## When to Use
|
|
14
23
|
|
|
15
24
|
- **File browsing**: `find . -name "*.jpg" | xargs -n1 viewinline` to visually scan results
|
|
@@ -59,9 +68,10 @@ Note: `--info` and `--export` report what a file *is* and what it *looks like*
|
|
|
59
68
|
|
|
60
69
|
## Core Flags
|
|
61
70
|
|
|
62
|
-
**Inspection
|
|
71
|
+
**Inspection:**
|
|
63
72
|
- `--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`)
|
|
73
|
+
- `--export PATH` — save the rendered image to PATH (`.png`/`.jpg`), open it in the default image viewer, and print `{"path": "..."}`
|
|
74
|
+
- `--grid [N]` — overlay axis ticks and a light grid (N divisions, default 5). Labels are in the file's CRS units for georeferenced rasters and vectors, pixel positions otherwise. Combine with `--export` to save a quick-look with readable coordinates, e.g. to check a file covers the expected area.
|
|
65
75
|
|
|
66
76
|
**Raster Display:**
|
|
67
77
|
- `--rgb R G B` — Specify band order for RGB (e.g., `--rgb 4 3 2`)
|
|
@@ -35,7 +35,7 @@ warnings.filterwarnings("ignore", message="invalid scale_factor or add_offset at
|
|
|
35
35
|
_EXPORT_PATH = None
|
|
36
36
|
_EXPORT_RESULT = None
|
|
37
37
|
|
|
38
|
-
__version__ = "0.
|
|
38
|
+
__version__ = "0.5.1"
|
|
39
39
|
|
|
40
40
|
AVAILABLE_COLORMAPS = [
|
|
41
41
|
"viridis", "inferno", "magma", "plasma",
|
|
@@ -923,6 +923,123 @@ def normalize_to_uint8(band: np.ndarray, vmin=None, vmax=None, nodata=None) -> n
|
|
|
923
923
|
return (band * 255).astype(np.uint8)
|
|
924
924
|
|
|
925
925
|
|
|
926
|
+
# def add_axis_and_grid(img: np.ndarray, n_ticks: int = 5) -> np.ndarray:
|
|
927
|
+
# """Overlay pixel-coordinate axis ticks/labels and a light grid on an image.
|
|
928
|
+
|
|
929
|
+
# Operates on the final (already resized) array, right before display, so
|
|
930
|
+
# ticks reflect the pixel coordinates of the rendered image, not the
|
|
931
|
+
# source raster. Grid lines are alpha-blended so image content stays
|
|
932
|
+
# visible; ticks/labels live in an added margin along the left and
|
|
933
|
+
# bottom edges.
|
|
934
|
+
# """
|
|
935
|
+
# h, w = img.shape[:2]
|
|
936
|
+
# n_ticks = max(1, n_ticks)
|
|
937
|
+
|
|
938
|
+
# base = Image.fromarray(img).convert("RGBA")
|
|
939
|
+
# overlay = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
|
940
|
+
# odraw = ImageDraw.Draw(overlay)
|
|
941
|
+
# grid_color = (255, 255, 255, 110)
|
|
942
|
+
|
|
943
|
+
# xs = [min(int(round(i * w / n_ticks)), w - 1) for i in range(n_ticks + 1)]
|
|
944
|
+
# ys = [min(int(round(i * h / n_ticks)), h - 1) for i in range(n_ticks + 1)]
|
|
945
|
+
|
|
946
|
+
# for x in xs:
|
|
947
|
+
# odraw.line([(x, 0), (x, h - 1)], fill=grid_color, width=1)
|
|
948
|
+
# for y in ys:
|
|
949
|
+
# odraw.line([(0, y), (w - 1, y)], fill=grid_color, width=1)
|
|
950
|
+
|
|
951
|
+
# composed = Image.alpha_composite(base, overlay).convert("RGB")
|
|
952
|
+
|
|
953
|
+
# margin_l, margin_b, margin_r = 42, 22, 30
|
|
954
|
+
# canvas = Image.new("RGB", (w + margin_l + margin_r, h + margin_b), (24, 24, 24))
|
|
955
|
+
# canvas.paste(composed, (margin_l, 0))
|
|
956
|
+
# draw = ImageDraw.Draw(canvas)
|
|
957
|
+
|
|
958
|
+
# try:
|
|
959
|
+
# font = ImageFont.load_default()
|
|
960
|
+
# except Exception:
|
|
961
|
+
# font = None
|
|
962
|
+
|
|
963
|
+
# label_color = (255, 255, 0)
|
|
964
|
+
|
|
965
|
+
# for x in xs:
|
|
966
|
+
# px = margin_l + x
|
|
967
|
+
# draw.line([(px, h), (px, h + 3)], fill=label_color)
|
|
968
|
+
# draw.text((px + 2, h + 4), str(x), fill=label_color, font=font)
|
|
969
|
+
|
|
970
|
+
# for y in ys:
|
|
971
|
+
# label = str(y)
|
|
972
|
+
# tw = draw.textlength(label, font=font) if font else len(label) * 6
|
|
973
|
+
# draw.line([(margin_l - 3, y), (margin_l, y)], fill=label_color)
|
|
974
|
+
# draw.text((margin_l - 6 - tw, max(0, y - 5)), label, fill=label_color, font=font)
|
|
975
|
+
|
|
976
|
+
# return np.array(canvas)
|
|
977
|
+
|
|
978
|
+
def add_axis_and_grid(img: np.ndarray, n_ticks: int = 5, bounds=None) -> np.ndarray:
|
|
979
|
+
"""Overlay axis ticks/labels and a light grid on an image.
|
|
980
|
+
|
|
981
|
+
If ``bounds`` (left, bottom, right, top) is given, labels show real map
|
|
982
|
+
coordinates in the raster's CRS. Otherwise they show pixel positions of
|
|
983
|
+
the rendered image (used for plain photos and non-georeferenced files).
|
|
984
|
+
"""
|
|
985
|
+
h, w = img.shape[:2]
|
|
986
|
+
n_ticks = max(1, n_ticks)
|
|
987
|
+
fracs = [i / n_ticks for i in range(n_ticks + 1)]
|
|
988
|
+
|
|
989
|
+
xs = [min(int(round(f * w)), w - 1) for f in fracs]
|
|
990
|
+
ys = [min(int(round(f * h)), h - 1) for f in fracs]
|
|
991
|
+
|
|
992
|
+
if bounds is not None:
|
|
993
|
+
left, bottom, right, top = bounds
|
|
994
|
+
step = max(abs(right - left), abs(top - bottom)) / n_ticks
|
|
995
|
+
# Enough decimals to tell neighbouring ticks apart (0 for metres, ~2-4 for degrees)
|
|
996
|
+
decimals = 0 if step >= 10 else max(0, int(-np.floor(np.log10(step))) + 1)
|
|
997
|
+
x_labels = [f"{left + f * (right - left):.{decimals}f}" for f in fracs]
|
|
998
|
+
y_labels = [f"{top - f * (top - bottom):.{decimals}f}" for f in fracs]
|
|
999
|
+
else:
|
|
1000
|
+
x_labels = [str(x) for x in xs]
|
|
1001
|
+
y_labels = [str(y) for y in ys]
|
|
1002
|
+
|
|
1003
|
+
base = Image.fromarray(img).convert("RGBA")
|
|
1004
|
+
overlay = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
|
1005
|
+
odraw = ImageDraw.Draw(overlay)
|
|
1006
|
+
grid_color = (255, 255, 255, 110)
|
|
1007
|
+
for x in xs:
|
|
1008
|
+
odraw.line([(x, 0), (x, h - 1)], fill=grid_color, width=1)
|
|
1009
|
+
for y in ys:
|
|
1010
|
+
odraw.line([(0, y), (w - 1, y)], fill=grid_color, width=1)
|
|
1011
|
+
composed = Image.alpha_composite(base, overlay).convert("RGB")
|
|
1012
|
+
|
|
1013
|
+
try:
|
|
1014
|
+
font = ImageFont.load_default()
|
|
1015
|
+
except Exception:
|
|
1016
|
+
font = None
|
|
1017
|
+
probe = ImageDraw.Draw(Image.new("RGB", (1, 1)))
|
|
1018
|
+
|
|
1019
|
+
def text_w(s):
|
|
1020
|
+
return probe.textlength(s, font=font) if font else len(s) * 6
|
|
1021
|
+
|
|
1022
|
+
# Margins sized to the longest labels so nothing gets clipped
|
|
1023
|
+
margin_l = int(max(text_w(s) for s in y_labels)) + 10
|
|
1024
|
+
margin_r = int(text_w(x_labels[-1])) + 6
|
|
1025
|
+
margin_b = 22
|
|
1026
|
+
|
|
1027
|
+
canvas = Image.new("RGB", (w + margin_l + margin_r, h + margin_b), (24, 24, 24))
|
|
1028
|
+
canvas.paste(composed, (margin_l, 0))
|
|
1029
|
+
draw = ImageDraw.Draw(canvas)
|
|
1030
|
+
label_color = (255, 255, 0)
|
|
1031
|
+
|
|
1032
|
+
for x, label in zip(xs, x_labels):
|
|
1033
|
+
px = margin_l + x
|
|
1034
|
+
draw.line([(px, h), (px, h + 3)], fill=label_color)
|
|
1035
|
+
draw.text((px + 2, h + 4), label, fill=label_color, font=font)
|
|
1036
|
+
|
|
1037
|
+
for y, label in zip(ys, y_labels):
|
|
1038
|
+
draw.line([(margin_l - 3, y), (margin_l, y)], fill=label_color)
|
|
1039
|
+
draw.text((margin_l - 6 - text_w(label), max(0, y - 5)), label, fill=label_color, font=font)
|
|
1040
|
+
|
|
1041
|
+
return np.array(canvas)
|
|
1042
|
+
|
|
926
1043
|
def render_simple_image(filepath: str, args) -> None:
|
|
927
1044
|
"""Render a simple PNG/JPG image using PIL (no geospatial processing)."""
|
|
928
1045
|
try:
|
|
@@ -945,9 +1062,17 @@ def render_simple_image(filepath: str, args) -> None:
|
|
|
945
1062
|
else:
|
|
946
1063
|
img_array, scale = resize_to_terminal(img_array)
|
|
947
1064
|
print(f"[VIEW] Rendered image size → {img_array.shape[1]}×{img_array.shape[0]}px (size={scale:.2f})")
|
|
948
|
-
|
|
1065
|
+
|
|
1066
|
+
if getattr(args, "grid", None):
|
|
1067
|
+
img_array = add_axis_and_grid(img_array, n_ticks=args.grid)
|
|
1068
|
+
|
|
949
1069
|
show_image_auto(img_array, getattr(args, "display", None), is_vector=False)
|
|
950
1070
|
|
|
1071
|
+
except FileNotFoundError:
|
|
1072
|
+
# A missing file is not a damaged-but-readable image: let the
|
|
1073
|
+
# caller report it with a nonzero exit instead of falling back
|
|
1074
|
+
# to the rasterio path.
|
|
1075
|
+
raise
|
|
951
1076
|
except Exception as e:
|
|
952
1077
|
print(f"[ERROR] Failed to load image: {e}")
|
|
953
1078
|
|
|
@@ -967,7 +1092,7 @@ def render_netcdf_via_netcdf4(path, args):
|
|
|
967
1092
|
nc = netCDF4.Dataset(path)
|
|
968
1093
|
except Exception as e:
|
|
969
1094
|
print(f"[ERROR] Could not open NetCDF file: {e}")
|
|
970
|
-
|
|
1095
|
+
sys.exit(1)
|
|
971
1096
|
|
|
972
1097
|
# Recursively collect (path, variable) pairs across all groups
|
|
973
1098
|
def collect_vars(group, prefix=""):
|
|
@@ -1102,6 +1227,8 @@ def render_netcdf_via_netcdf4(path, args):
|
|
|
1102
1227
|
scale = max_dim / max(H, W)
|
|
1103
1228
|
new_w, new_h = int(W * scale), int(H * scale)
|
|
1104
1229
|
img = np.array(Image.fromarray(img).resize((new_w, new_h), Image.BILINEAR))
|
|
1230
|
+
if getattr(args, "grid", None):
|
|
1231
|
+
img = add_axis_and_grid(img, n_ticks=args.grid)
|
|
1105
1232
|
show_image_auto(img, getattr(args, "display", None), is_vector=False)
|
|
1106
1233
|
return
|
|
1107
1234
|
except ValueError as e:
|
|
@@ -1177,8 +1304,11 @@ def render_netcdf_via_netcdf4(path, args):
|
|
|
1177
1304
|
# (matches the width_pct logic in show_inline_image)
|
|
1178
1305
|
display_pct = args.display if args.display is not None else 0.33
|
|
1179
1306
|
|
|
1180
|
-
print(f"[VIEW] Rendered image size → {img.shape[1]}×{img.shape[0]}px (size={display_pct:.2f})")
|
|
1181
|
-
|
|
1307
|
+
print(f"[VIEW] Rendered image size → {img.shape[1]}×{img.shape[0]}px (size={display_pct:.2f})")
|
|
1308
|
+
|
|
1309
|
+
if getattr(args, "grid", None):
|
|
1310
|
+
img = add_axis_and_grid(img, n_ticks=args.grid)
|
|
1311
|
+
|
|
1182
1312
|
show_image_auto(img, getattr(args, "display", None), is_vector=False)
|
|
1183
1313
|
|
|
1184
1314
|
def render_raster(paths: list[str], args) -> None:
|
|
@@ -1255,6 +1385,7 @@ def render_raster(paths: list[str], args) -> None:
|
|
|
1255
1385
|
# Continue with normal raster opening
|
|
1256
1386
|
with rasterio.open(path) as ds:
|
|
1257
1387
|
H, W = ds.height, ds.width
|
|
1388
|
+
grid_bounds = tuple(ds.bounds) if ds.crs is not None else None
|
|
1258
1389
|
print(f"[DATA] Raster loaded: {os.path.basename(paths[0])} ({W}×{H})")
|
|
1259
1390
|
band_count = ds.count
|
|
1260
1391
|
|
|
@@ -1372,6 +1503,7 @@ def render_raster(paths: list[str], args) -> None:
|
|
|
1372
1503
|
for p in paths:
|
|
1373
1504
|
with rasterio.open(p) as ds:
|
|
1374
1505
|
bands.append(ds.read(1))
|
|
1506
|
+
grid_bounds = tuple(ds.bounds) if ds.crs is not None else None
|
|
1375
1507
|
shapes = {b.shape for b in bands}
|
|
1376
1508
|
if len(shapes) != 1:
|
|
1377
1509
|
print("[ERROR] Raster sizes do not match.")
|
|
@@ -1396,6 +1528,10 @@ def render_raster(paths: list[str], args) -> None:
|
|
|
1396
1528
|
img, scale = resize_to_terminal(img)
|
|
1397
1529
|
print(f"[VIEW] Rendered image size → {img.shape[1]}×{img.shape[0]}px (size={scale:.2f})")
|
|
1398
1530
|
|
|
1531
|
+
if getattr(args, "grid", None):
|
|
1532
|
+
# img = add_axis_and_grid(img, n_ticks=args.grid)
|
|
1533
|
+
img = add_axis_and_grid(img, n_ticks=args.grid, bounds=grid_bounds)
|
|
1534
|
+
|
|
1399
1535
|
# Use unified display
|
|
1400
1536
|
show_image_auto(img, getattr(args, "display", None), is_vector=False)
|
|
1401
1537
|
|
|
@@ -1405,6 +1541,7 @@ def render_raster(paths: list[str], args) -> None:
|
|
|
1405
1541
|
print("[INFO] viewinline only supports 2D or 3D NetCDF variables")
|
|
1406
1542
|
else:
|
|
1407
1543
|
print(f"[ERROR] Inline render failed: {e}")
|
|
1544
|
+
sys.exit(1)
|
|
1408
1545
|
|
|
1409
1546
|
|
|
1410
1547
|
def render_gallery(folder: str, grid: str = "4x4", display_scale=None, is_vector=False) -> None:
|
|
@@ -1619,7 +1756,7 @@ def render_vector(path, args):
|
|
|
1619
1756
|
return
|
|
1620
1757
|
except Exception as e:
|
|
1621
1758
|
print(f"[ERROR] Failed to read vector: {e}")
|
|
1622
|
-
|
|
1759
|
+
sys.exit(1)
|
|
1623
1760
|
|
|
1624
1761
|
# Detect non-geometry columns
|
|
1625
1762
|
all_cols = [c for c in gdf.columns if c != gdf.geometry.name]
|
|
@@ -1648,7 +1785,18 @@ def render_vector(path, args):
|
|
|
1648
1785
|
# Figure setup
|
|
1649
1786
|
fig, ax = plt.subplots(figsize=(6, 6), dpi=150, facecolor="gray")
|
|
1650
1787
|
ax.set_facecolor("gray")
|
|
1651
|
-
|
|
1788
|
+
show_grid = getattr(args, "grid", None)
|
|
1789
|
+
if show_grid:
|
|
1790
|
+
# Keep axis on: real coordinate ticks (CRS units) plus a light grid,
|
|
1791
|
+
# instead of the pixel-based overlay used for raster images.
|
|
1792
|
+
ax.tick_params(colors="white", labelsize=6)
|
|
1793
|
+
for spine in ax.spines.values():
|
|
1794
|
+
spine.set_color("white")
|
|
1795
|
+
ax.locator_params(nbins=show_grid)
|
|
1796
|
+
ax.grid(True, color="white", alpha=0.3, linewidth=0.5)
|
|
1797
|
+
ax.ticklabel_format(style="plain", useOffset=False)
|
|
1798
|
+
else:
|
|
1799
|
+
ax.set_axis_off()
|
|
1652
1800
|
|
|
1653
1801
|
# Determine colormap
|
|
1654
1802
|
column = args.color_by if args.color_by in gdf.columns else None
|
|
@@ -1965,6 +2113,10 @@ def main() -> None:
|
|
|
1965
2113
|
|
|
1966
2114
|
general.add_argument("--export", metavar="PATH", default=None,
|
|
1967
2115
|
help="Also save the displayed image to PATH (.png/.jpg). Prints {\"path\": ...}.")
|
|
2116
|
+
general.add_argument(
|
|
2117
|
+
"--grid", nargs="?", type=int, const=5, default=None, metavar="N",
|
|
2118
|
+
help="Overlay coordinate axis ticks and a light grid on the image (N divisions, default 5)."
|
|
2119
|
+
)
|
|
1968
2120
|
|
|
1969
2121
|
# Raster options
|
|
1970
2122
|
raster = parser.add_argument_group("Raster")
|
|
@@ -2146,6 +2298,9 @@ def main() -> None:
|
|
|
2146
2298
|
try:
|
|
2147
2299
|
render_simple_image(paths[0], args)
|
|
2148
2300
|
return
|
|
2301
|
+
except FileNotFoundError as e:
|
|
2302
|
+
print(f"[ERROR] Failed to read file: {e}")
|
|
2303
|
+
sys.exit(1)
|
|
2149
2304
|
except Exception:
|
|
2150
2305
|
pass
|
|
2151
2306
|
render_raster(paths, args)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""A missing PNG/JPG must exit nonzero, not report an error with exit 0."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from viewinline.viewinline import main
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _run(argv, capsys):
|
|
11
|
+
sys.argv = argv
|
|
12
|
+
with pytest.raises(SystemExit) as exc:
|
|
13
|
+
main()
|
|
14
|
+
out = capsys.readouterr().out
|
|
15
|
+
return exc.value.code, out
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_missing_png_exits_nonzero(capsys):
|
|
19
|
+
code, out = _run(["viewinline", "/nonexistent-xyz-440.png"], capsys)
|
|
20
|
+
assert code == 1
|
|
21
|
+
assert "[ERROR]" in out
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_missing_jpg_exits_nonzero(capsys):
|
|
25
|
+
code, out = _run(["viewinline", "/nonexistent-xyz-440.jpg"], capsys)
|
|
26
|
+
assert code == 1
|
|
27
|
+
assert "[ERROR]" in out
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.parametrize("path", [
|
|
31
|
+
"/nonexistent-xyz-440.tif",
|
|
32
|
+
"/nonexistent-xyz-440.shp",
|
|
33
|
+
"/nonexistent-xyz-440.geojson",
|
|
34
|
+
"/nonexistent-xyz-440.nc",
|
|
35
|
+
])
|
|
36
|
+
def test_missing_raster_or_vector_exits_nonzero(path, capsys):
|
|
37
|
+
code, out = _run(["viewinline", path], capsys)
|
|
38
|
+
assert code == 1
|
|
39
|
+
assert "[ERROR]" in out
|
viewinline-0.4.0/.gitignore
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|