viewinline 0.4.0__tar.gz → 0.5.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.
@@ -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.0
23
+ date-released: 2026-10-21
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.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
@@ -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
@@ -222,6 +228,7 @@ General:
222
228
  --display DISPLAY Resize only the displayed image (0.5=smaller, 2=bigger). Default: auto-fit to terminal.
223
229
  --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
230
  --export PATH Save the rendered image to PATH (.png/.jpg) and open it. Works with any display flag. Prints {"path": "..."} as the final line.
231
+ --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
232
 
226
233
  Raster:
227
234
  --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
@@ -189,6 +195,7 @@ General:
189
195
  --display DISPLAY Resize only the displayed image (0.5=smaller, 2=bigger). Default: auto-fit to terminal.
190
196
  --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
197
  --export PATH Save the rendered image to PATH (.png/.jpg) and open it. Works with any display flag. Prints {"path": "..."} as the final line.
198
+ --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
199
 
193
200
  Raster:
194
201
  --band BAND Band number to display (single raster), or slice number for NetCDF. (default: 1)
@@ -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.0 (2026-10-21)
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
@@ -136,6 +136,8 @@ General:
136
136
  - `--display DISPLAY` — resize only the displayed image (0.5 = smaller, 2 = bigger). Default: auto-fit to terminal.
137
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
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
+ - `--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`.
140
+
139
141
  Raster:
140
142
  - `--band BAND` — band number for a single raster, or slice number for NetCDF (default: 1).
141
143
  - `--bands BANDS` — display multiple bands as a grid. Accepts ranges (`30-40`), lists (`3,4,5`), or mixed (`1,5,10-15`).
@@ -236,6 +238,7 @@ Optional: `chafa` (terminal coverage; system binary), `duckdb` (filter/sort/SQL)
236
238
  - **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
239
  - **v0.1.5 (2026-02-13):** Fixed duplicate band-display prints; optimized uint8 normalization; minor perf.
238
240
  - **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.
241
+ - **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.
239
242
 
240
243
  ## Support & links
241
244
 
@@ -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.0"
8
8
  description = "Quick look geospatial viewer for the terminal, with inline image previews"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -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.0"
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,7 +1062,10 @@ 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
 
951
1071
  except Exception as e:
@@ -1102,6 +1222,8 @@ def render_netcdf_via_netcdf4(path, args):
1102
1222
  scale = max_dim / max(H, W)
1103
1223
  new_w, new_h = int(W * scale), int(H * scale)
1104
1224
  img = np.array(Image.fromarray(img).resize((new_w, new_h), Image.BILINEAR))
1225
+ if getattr(args, "grid", None):
1226
+ img = add_axis_and_grid(img, n_ticks=args.grid)
1105
1227
  show_image_auto(img, getattr(args, "display", None), is_vector=False)
1106
1228
  return
1107
1229
  except ValueError as e:
@@ -1177,8 +1299,11 @@ def render_netcdf_via_netcdf4(path, args):
1177
1299
  # (matches the width_pct logic in show_inline_image)
1178
1300
  display_pct = args.display if args.display is not None else 0.33
1179
1301
 
1180
- print(f"[VIEW] Rendered image size → {img.shape[1]}×{img.shape[0]}px (size={display_pct:.2f})")
1181
-
1302
+ print(f"[VIEW] Rendered image size → {img.shape[1]}×{img.shape[0]}px (size={display_pct:.2f})")
1303
+
1304
+ if getattr(args, "grid", None):
1305
+ img = add_axis_and_grid(img, n_ticks=args.grid)
1306
+
1182
1307
  show_image_auto(img, getattr(args, "display", None), is_vector=False)
1183
1308
 
1184
1309
  def render_raster(paths: list[str], args) -> None:
@@ -1255,6 +1380,7 @@ def render_raster(paths: list[str], args) -> None:
1255
1380
  # Continue with normal raster opening
1256
1381
  with rasterio.open(path) as ds:
1257
1382
  H, W = ds.height, ds.width
1383
+ grid_bounds = tuple(ds.bounds) if ds.crs is not None else None
1258
1384
  print(f"[DATA] Raster loaded: {os.path.basename(paths[0])} ({W}×{H})")
1259
1385
  band_count = ds.count
1260
1386
 
@@ -1372,6 +1498,7 @@ def render_raster(paths: list[str], args) -> None:
1372
1498
  for p in paths:
1373
1499
  with rasterio.open(p) as ds:
1374
1500
  bands.append(ds.read(1))
1501
+ grid_bounds = tuple(ds.bounds) if ds.crs is not None else None
1375
1502
  shapes = {b.shape for b in bands}
1376
1503
  if len(shapes) != 1:
1377
1504
  print("[ERROR] Raster sizes do not match.")
@@ -1396,6 +1523,10 @@ def render_raster(paths: list[str], args) -> None:
1396
1523
  img, scale = resize_to_terminal(img)
1397
1524
  print(f"[VIEW] Rendered image size → {img.shape[1]}×{img.shape[0]}px (size={scale:.2f})")
1398
1525
 
1526
+ if getattr(args, "grid", None):
1527
+ # img = add_axis_and_grid(img, n_ticks=args.grid)
1528
+ img = add_axis_and_grid(img, n_ticks=args.grid, bounds=grid_bounds)
1529
+
1399
1530
  # Use unified display
1400
1531
  show_image_auto(img, getattr(args, "display", None), is_vector=False)
1401
1532
 
@@ -1648,7 +1779,18 @@ def render_vector(path, args):
1648
1779
  # Figure setup
1649
1780
  fig, ax = plt.subplots(figsize=(6, 6), dpi=150, facecolor="gray")
1650
1781
  ax.set_facecolor("gray")
1651
- ax.set_axis_off()
1782
+ show_grid = getattr(args, "grid", None)
1783
+ if show_grid:
1784
+ # Keep axis on: real coordinate ticks (CRS units) plus a light grid,
1785
+ # instead of the pixel-based overlay used for raster images.
1786
+ ax.tick_params(colors="white", labelsize=6)
1787
+ for spine in ax.spines.values():
1788
+ spine.set_color("white")
1789
+ ax.locator_params(nbins=show_grid)
1790
+ ax.grid(True, color="white", alpha=0.3, linewidth=0.5)
1791
+ ax.ticklabel_format(style="plain", useOffset=False)
1792
+ else:
1793
+ ax.set_axis_off()
1652
1794
 
1653
1795
  # Determine colormap
1654
1796
  column = args.color_by if args.color_by in gdf.columns else None
@@ -1965,6 +2107,10 @@ def main() -> None:
1965
2107
 
1966
2108
  general.add_argument("--export", metavar="PATH", default=None,
1967
2109
  help="Also save the displayed image to PATH (.png/.jpg). Prints {\"path\": ...}.")
2110
+ general.add_argument(
2111
+ "--grid", nargs="?", type=int, const=5, default=None, metavar="N",
2112
+ help="Overlay coordinate axis ticks and a light grid on the image (N divisions, default 5)."
2113
+ )
1968
2114
 
1969
2115
  # Raster options
1970
2116
  raster = parser.add_argument_group("Raster")
@@ -62,6 +62,7 @@ Note: `--info` and `--export` report what a file *is* and what it *looks like*
62
62
  **Inspection (JSON output, no image):**
63
63
  - `--info` — print metadata + statistics as JSON, then exit (rasters and vectors; lists variables for NetCDF/HDF with `--subset N`)
64
64
  - `--export PATH` — save the rendered image to PATH (`.png`/`.jpg`); prints `{"path": "..."}`
65
+ - `--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
66
 
66
67
  **Raster Display:**
67
68
  - `--rgb R G B` — Specify band order for RGB (e.g., `--rgb 4 3 2`)
File without changes
File without changes
File without changes