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.
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ .pytest_cache/
3
+ dist/
4
+ *.egg-info/
5
+ .DS_Store
@@ -11,26 +11,27 @@ type: software
11
11
  authors:
12
12
  - given-names: Keiko
13
13
  family-names: Nomura
14
- # orcid: "https://orcid.org/0000-0000-0000-0000" # optional: add if you have one
14
+ orcid: "https://orcid.org/0000-0002-5218-3414"
15
15
  - given-names: Filip
16
16
  family-names: Kral
17
- # orcid: "https://orcid.org/0000-0000-0000-0000" # optional: add if you have one
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.3.2
22
- date-released: 2026-06-06
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.20564266"
30
+ doi: "10.5281/zenodo.19700943"
30
31
  identifiers:
31
32
  - type: doi
32
- value: "10.5281/zenodo.20564266"
33
- description: "Zenodo archive of version v0.3.2"
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.4.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.4.0 (2026-06-06)
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "viewinline"
7
- version = "0.4.0"
7
+ version = "0.5.1"
8
8
  description = "Quick look geospatial viewer for the terminal, with inline image previews"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -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 (JSON output, no image):**
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`); prints `{"path": "..."}`
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.4.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
- return
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
- return
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
- ax.set_axis_off()
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
@@ -1,4 +0,0 @@
1
- dist/
2
- build/
3
- *.egg-info
4
- *.DS_Store
File without changes