viewtif 0.1.8__py3-none-any.whl → 0.1.10__py3-none-any.whl

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.
viewtif/tif_viewer.py CHANGED
@@ -53,37 +53,52 @@ except Exception:
53
53
  HAVE_GEO = False
54
54
 
55
55
  def warn_if_large(tif_path, scale=1):
56
- """Warn and confirm before loading very large rasters (GeoTIFF, GDB, or HDF)."""
57
- from osgeo import gdal
56
+ """Warn and confirm before loading very large rasters (GeoTIFF, GDB, or HDF).
57
+ Works even if GDAL is not installed.
58
+ """
58
59
  import os
60
+ width = height = None
61
+ size_mb = None
59
62
 
63
+ # Try GDAL if available
60
64
  try:
65
+ from osgeo import gdal
61
66
  gdal.UseExceptions()
62
67
  info = gdal.Info(tif_path, format="json")
63
68
  width, height = info.get("size", [0, 0])
64
- total_pixels = (width * height) / (scale ** 2) # account for downsampling
65
- size_mb = None
66
- if os.path.exists(tif_path):
67
- size_mb = os.path.getsize(tif_path) / (1024 ** 2)
68
-
69
- # Only warn if the *effective* pixels remain large
70
- if total_pixels > 20_000_000 and scale <= 5:
71
- print(
72
- f"[WARN] Large raster detected ({width}×{height}, ~{total_pixels/1e6:.1f}M effective pixels"
73
- + (f", ~{size_mb:.1f} MB" if size_mb else "")
74
- + "). Loading may freeze. Consider rerunning with --scale (e.g. --scale 10)."
75
- )
76
- ans = input("Proceed anyway? [y/N]: ").strip().lower()
77
- if ans not in ("y", "yes"):
78
- print("Cancelled.")
79
- sys.exit(0)
69
+ except ImportError:
70
+ # Fallback if GDAL not installed
71
+ try:
72
+ import rasterio
73
+ with rasterio.open(tif_path) as src:
74
+ width, height = src.width, src.height
75
+ except Exception:
76
+ print("[WARN] Could not estimate raster size (no GDAL/rasterio). Skipping size check.")
77
+ return
80
78
  except Exception as e:
81
- print(f"[WARN] Could not pre-check raster size: {e}")
79
+ print(f"[WARN] Could not pre-check raster size with GDAL: {e}")
80
+ return
81
+
82
+ # File size
83
+ if os.path.exists(tif_path):
84
+ size_mb = os.path.getsize(tif_path) / (1024 ** 2)
85
+
86
+ total_pixels = (width * height) / (scale ** 2)
87
+ if total_pixels > 20_000_000 and scale <= 5:
88
+ msg = (
89
+ f"[WARN] Large raster detected ({width}×{height}, ~{total_pixels/1e6:.1f}M pixels"
90
+ + (f", ~{size_mb:.1f} MB" if size_mb else "")
91
+ + "). Loading may freeze. Consider --scale (e.g. --scale 10)."
92
+ )
93
+ print(msg)
94
+ ans = input("Proceed anyway? [y/N]: ").strip().lower()
95
+ if ans not in ("y", "yes"):
96
+ print("Cancelled.")
97
+ sys.exit(0)
82
98
 
83
99
  # -------------------------- QGraphicsView tweaks -------------------------- #
84
100
  class RasterView(QGraphicsView):
85
101
  def __init__(self, *args, **kwargs):
86
- import numpy as np
87
102
  super().__init__(*args, **kwargs)
88
103
  self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, False)
89
104
  self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
@@ -170,84 +185,116 @@ class TiffViewer(QMainWindow):
170
185
  self.tif_path = self.tif_path or (os.path.commonprefix([red, green, blue]) or red)
171
186
 
172
187
  elif tif_path:
173
- # --- Universal size check before loading ---
188
+ # ---------------- Handle File Geodatabase (.gdb) ---------------- #
189
+ if tif_path.lower().endswith(".gdb") and ":" not in tif_path:
190
+ import re, subprocess
191
+ gdb_path = tif_path # use full path to .gdb
192
+ try:
193
+ out = subprocess.check_output(["gdalinfo", "-norat", gdb_path], text=True)
194
+ rasters = re.findall(r"RASTER_DATASET=(\S+)", out)
195
+ if not rasters:
196
+ print(f"[WARN] No raster datasets found in {os.path.basename(gdb_path)}.")
197
+ sys.exit(0)
198
+ else:
199
+ print(f"Found {len(rasters)} raster dataset{'s' if len(rasters) > 1 else ''}:")
200
+ for i, r in enumerate(rasters):
201
+ print(f"[{i}] {r}")
202
+ print("\nUse one of these names to open. For example, to open the first raster:")
203
+ print(f'viewtif "OpenFileGDB:{gdb_path}:{rasters[0]}"')
204
+ sys.exit(0)
205
+ except subprocess.CalledProcessError as e:
206
+ print(f"[WARN] Could not inspect FileGDB: {e}")
207
+ sys.exit(0)
208
+
209
+ # --- Universal size check before loading ---
174
210
  warn_if_large(tif_path, scale=self._scale_arg)
211
+
175
212
  # --------------------- Detect HDF/HDF5 --------------------- #
176
213
  if tif_path.lower().endswith((".hdf", ".h5", ".hdf5")):
177
214
  try:
178
- from osgeo import gdal
179
- gdal.UseExceptions()
180
-
181
- ds = gdal.Open(tif_path)
182
- subs = ds.GetSubDatasets()
183
-
184
- if not subs:
185
- raise ValueError("No subdatasets found in HDF/HDF5 file.")
186
-
187
- print(f"Found {len(subs)} subdatasets in {os.path.basename(tif_path)}:")
188
- for i, (_, desc) in enumerate(subs):
189
- print(f"[{i}] {desc}")
190
-
191
- # Only list subsets if --subset not given
192
- if subset is None:
193
- print("\nUse --subset N to open a specific subdataset.")
194
- sys.exit(0)
215
+ # Try reading directly with Rasterio first (works for simple HDF layouts)
216
+ with rasterio.open(tif_path) as src:
217
+ print(f"Opened HDF with rasterio: {os.path.basename(tif_path)}")
218
+ arr = src.read().astype(np.float32)
219
+ arr = np.squeeze(arr)
220
+ if arr.ndim == 3:
221
+ arr = np.transpose(arr, (1, 2, 0))
222
+ elif arr.ndim == 2:
223
+ print("Single-band dataset.")
224
+ self.data = arr
225
+ self._transform = src.transform
226
+ self._crs = src.crs
227
+ self.band_count = arr.shape[2] if arr.ndim == 3 else 1
228
+ self.band_index = 0
229
+ self.vmin, self.vmax = np.nanmin(arr), np.nanmax(arr)
230
+ return # Skip GDAL path if Rasterio succeeded
231
+
232
+ except Exception as e:
233
+ print(f"Rasterio could not open HDF directly: {e}")
234
+ print("Falling back to GDAL...")
235
+
236
+ try:
237
+ from osgeo import gdal
238
+ gdal.UseExceptions()
239
+
240
+ ds = gdal.Open(tif_path)
241
+ subs = ds.GetSubDatasets()
242
+ if not subs:
243
+ raise ValueError("No subdatasets found in HDF/HDF5 file.")
244
+
245
+ print(f"Found {len(subs)} subdatasets in {os.path.basename(tif_path)}:")
246
+ for i, (_, desc) in enumerate(subs):
247
+ print(f"[{i}] {desc}")
248
+
249
+ if subset is None:
250
+ print("\nUse --subset N to open a specific subdataset.")
251
+ sys.exit(0)
252
+
253
+ if subset < 0 or subset >= len(subs):
254
+ raise ValueError(f"Invalid subset index {subset}. Valid range: 0–{len(subs)-1}")
255
+
256
+ sub_name, desc = subs[subset]
257
+ print(f"\nOpening subdataset [{subset}]: {desc}")
258
+ sub_ds = gdal.Open(sub_name)
259
+
260
+ arr = sub_ds.ReadAsArray().astype(np.float32)
261
+ arr = np.squeeze(arr)
262
+ if arr.ndim == 3:
263
+ arr = np.transpose(arr, (1, 2, 0))
264
+ elif arr.ndim == 2:
265
+ print("Single-band dataset.")
266
+ else:
267
+ raise ValueError(f"Unexpected array shape {arr.shape}")
268
+
269
+ # Downsample large arrays for responsiveness
270
+ h, w = arr.shape[:2]
271
+ if h * w > 4_000_000:
272
+ step = max(2, int((h * w / 4_000_000) ** 0.5))
273
+ arr = arr[::step, ::step] if arr.ndim == 2 else arr[::step, ::step, :]
274
+ print(f"⚠️ Large dataset preview: downsampled by {step}x")
275
+
276
+ # Assign
277
+ self.data = arr
278
+ self._transform = None
279
+ self._crs = None
280
+ self.band_count = arr.shape[2] if arr.ndim == 3 else 1
281
+ self.band_index = 0
282
+ self.vmin, self.vmax = np.nanmin(arr), np.nanmax(arr)
195
283
 
196
- # Validate subset index
197
- if subset < 0 or subset >= len(subs):
198
- raise ValueError(f"Invalid subset index {subset}. Valid range: 0–{len(subs)-1}")
199
-
200
- sub_name, desc = subs[subset]
201
- print(f"\nOpening subdataset [{subset}]: {desc}")
202
- sub_ds = gdal.Open(sub_name)
203
-
204
- # --- Read once ---
205
- arr = sub_ds.ReadAsArray().astype(np.float32)
206
- #print(f"Raw array shape from GDAL: {arr.shape} (ndim={arr.ndim})")
207
-
208
- # --- Normalize shape ---
209
- arr = np.squeeze(arr)
210
- if arr.ndim == 3:
211
- # Convert from (bands, rows, cols) → (rows, cols, bands)
212
- arr = np.transpose(arr, (1, 2, 0))
213
- #print(f"Transposed to {arr.shape} (rows, cols, bands)")
214
- elif arr.ndim == 2:
215
- print("Single-band dataset.")
216
- else:
217
- raise ValueError(f"Unexpected array shape {arr.shape}")
218
-
219
- # --- Downsample large arrays for responsiveness ---
220
- h, w = arr.shape[:2]
221
- if h * w > 4_000_000:
222
- step = max(2, int((h * w / 4_000_000) ** 0.5))
223
- arr = arr[::step, ::step] if arr.ndim == 2 else arr[::step, ::step, :]
224
- print(f"⚠️ Large dataset preview: downsampled by {step}x")
225
-
226
- # --- Final assignments ---
227
- self.data = arr
228
- self._transform = None
229
- self._crs = None
230
- self.band_count = arr.shape[2] if arr.ndim == 3 else 1
231
- self.band_index = 0
232
- self.vmin, self.vmax = np.nanmin(arr), np.nanmax(arr)
233
-
234
- if self.band_count > 1:
235
- print(f"This subdataset has {self.band_count} bands — switch with [ and ] keys.")
236
- else:
237
- print("This subdataset has 1 band.")
284
+ if self.band_count > 1:
285
+ print(f"This subdataset has {self.band_count} bands switch with [ and ] keys.")
286
+ else:
287
+ print("This subdataset has 1 band.")
238
288
 
239
- # --- If user specified --band, start there ---
240
- if self.band and self.band <= self.band_count:
241
- self.band_index = self.band - 1
242
- print(f"Opening band {self.band}/{self.band_count}")
243
- else:
244
- self.band_index = 0
289
+ if self.band and self.band <= self.band_count:
290
+ self.band_index = self.band - 1
291
+ print(f"Opening band {self.band}/{self.band_count}")
245
292
 
246
- except ImportError:
247
- raise RuntimeError(
248
- "HDF support requires GDAL.\n"
249
- "Install it first (e.g., brew install gdal && pip install GDAL)"
250
- )
293
+ except ImportError:
294
+ raise RuntimeError(
295
+ "HDF/HDF5 support requires GDAL (Python bindings).\n"
296
+ "Install it first (e.g., brew install gdal && pip install GDAL)"
297
+ )
251
298
 
252
299
  # --------------------- Regular GeoTIFF --------------------- #
253
300
  else:
@@ -467,8 +514,7 @@ class TiffViewer(QMainWindow):
467
514
  rgb = np.zeros_like(arr)
468
515
  if np.any(finite):
469
516
  # Global 2–98 percentile stretch across all bands (QGIS-like)
470
- global_min = np.nanpercentile(arr, 2)
471
- global_max = np.nanpercentile(arr, 98)
517
+ global_min, global_max = np.nanpercentile(arr, (2, 98))
472
518
  rng = max(global_max - global_min, 1e-12)
473
519
  norm = np.clip((arr - global_min) / rng, 0, 1)
474
520
  rgb = np.clip(norm * self.contrast, 0, 1)
@@ -620,8 +666,8 @@ class TiffViewer(QMainWindow):
620
666
  super().keyPressEvent(ev)
621
667
 
622
668
 
623
- # --------------------------------- CLI ----------------------------------- #
624
- def main():
669
+ # --------------------------------- Legacy argparse CLI (not used by default) ----------------------------------- #
670
+ def legacy_argparse_main():
625
671
  parser = argparse.ArgumentParser(description="TIFF viewer with RGB (2–98%) & shapefile overlays")
626
672
  parser.add_argument("tif_path", nargs="?", help="Path to TIFF (optional if --rgbfiles is used)")
627
673
  parser.add_argument("--scale", type=int, default=1, help="Downsample factor (1=full, 10=10x smaller)")
@@ -633,6 +679,8 @@ def main():
633
679
  parser.add_argument("--shp-width", type=float, default=1.5, help="Overlay line width (screen pixels). Default: 1.5")
634
680
  args = parser.parse_args()
635
681
 
682
+ from PySide6.QtCore import Qt
683
+ QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True)
636
684
  app = QApplication(sys.argv)
637
685
  win = TiffViewer(
638
686
  args.tif_path,
@@ -657,9 +705,12 @@ def run_viewer(
657
705
  shapefile=None,
658
706
  shp_color=None,
659
707
  shp_width=None,
660
- subset=None,
708
+ subset=None
661
709
  ):
710
+
662
711
  """Launch the TiffViewer app"""
712
+ from PySide6.QtCore import Qt
713
+ QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True)
663
714
  app = QApplication(sys.argv)
664
715
  win = TiffViewer(
665
716
  tif_path,
@@ -670,7 +721,7 @@ def run_viewer(
670
721
  shapefiles=shapefile,
671
722
  shp_color=shp_color,
672
723
  shp_width=shp_width,
673
- subset=subset,
724
+ subset=subset
674
725
  )
675
726
  win.show()
676
727
  sys.exit(app.exec())
@@ -678,16 +729,17 @@ def run_viewer(
678
729
  import click
679
730
 
680
731
  @click.command()
681
- @click.version_option("1.0.6", prog_name="viewtif")
732
+ @click.version_option("0.1.10", prog_name="viewtif")
682
733
  @click.argument("tif_path", required=False)
683
734
  @click.option("--band", default=1, show_default=True, type=int, help="Band number to display")
684
- @click.option("--scale", default=1.0, show_default=True, type=float, help="Scale factor for display")
735
+ @click.option("--scale", default=1.0, show_default=True, type=int, help="Scale factor for display")
685
736
  @click.option("--rgb", nargs=3, type=int, help="Three band numbers for RGB, e.g. --rgb 4 3 2")
686
737
  @click.option("--rgbfiles", nargs=3, type=str, help="Three single-band TIFFs for RGB, e.g. --rgbfiles B4.tif B3.tif B2.tif")
687
738
  @click.option("--shapefile", multiple=True, type=str, help="One or more shapefiles to overlay")
688
739
  @click.option("--shp-color", default="white", show_default=True, help="Overlay color (name or #RRGGBB).")
689
740
  @click.option("--shp-width", default=1.0, show_default=True, type=float, help="Overlay line width (screen pixels).")
690
741
  @click.option("--subset", default=None, type=int, help="Open specific subdataset index in .hdf/.h5 file")
742
+
691
743
  def main(tif_path, band, scale, rgb, rgbfiles, shapefile, shp_color, shp_width, subset):
692
744
  """Lightweight GeoTIFF viewer."""
693
745
  # --- Warn early if shapefile requested but geopandas missing ---
@@ -707,7 +759,7 @@ def main(tif_path, band, scale, rgb, rgbfiles, shapefile, shp_color, shp_width,
707
759
  shapefile=shapefile,
708
760
  shp_color=shp_color,
709
761
  shp_width=shp_width,
710
- subset=subset,
762
+ subset=subset
711
763
  )
712
764
 
713
765
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: viewtif
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: Lightweight GeoTIFF, HDF/HDF5, and Esri File Geodatabase (.gdb) viewer with shapefile overlay and large-raster safeguard.
5
5
  Project-URL: Homepage, https://github.com/nkeikon/tifviewer
6
6
  Project-URL: Source, https://github.com/nkeikon/tifviewer
@@ -27,6 +27,11 @@ A lightweight GeoTIFF viewer for quick visualization directly from the command l
27
27
 
28
28
  You can visualize single-band GeoTIFFs, RGB composites, and shapefile overlays in a simple Qt-based window.
29
29
 
30
+ ---
31
+ Latest stable release: v0.1.9 (PyPI)
32
+
33
+ Development branch: v0.2.0-dev (experimental, not released)
34
+
30
35
  ---
31
36
 
32
37
  ## Installation
@@ -87,12 +92,16 @@ viewtif AG100.v003.33.-107.0001.h5 --subset 1 --band 3
87
92
 
88
93
  ### Update in v1.0.7: File Geodatabase (.gdb) support
89
94
  `viewtif` can now open raster datasets stored inside Esri File Geodatabases (`.gdb`), using the GDAL `OpenFileGDB` driver.
95
+ When you open a .gdb directly, `viewtif` will list available raster datasets first, then you can choose one to view.
90
96
 
91
97
  ```bash
92
- # Example
98
+ # List available raster datasets
99
+ viewtif /path/to/geodatabase.gdb
100
+
101
+ # Open a specific raster
93
102
  viewtif "OpenFileGDB:/path/to/geodatabase.gdb:RasterName"
94
103
  ```
95
- > **Note:** Requires GDAL 3.7 or later with the OpenFileGDB driver enabled. The .gdb path and raster name must be separated by a colon (:).
104
+ > **Note:** Requires GDAL 3.7 or later with the OpenFileGDB driver enabled. If multiple raster datasets are present, viewtif lists them all and shows how to open each. The .gdb path and raster name must be separated by a colon (:).
96
105
 
97
106
  ### Update in v1.0.7: Large raster safeguard
98
107
  As of v1.0.7, `viewtif` automatically checks the raster size before loading.
@@ -134,4 +143,4 @@ This project is released under the MIT License.
134
143
 
135
144
  ## Contributors
136
145
  - [@HarshShinde0](https://github.com/HarshShinde0) — added mouse-wheel and trackpad zoom support
137
- - [@p-vdp](https://github.com/p-vdp) — added File Geodatabase (.gdb) raster support
146
+ - [@p-vdp](https://github.com/p-vdp) — added File Geodatabase (.gdb) raster support
@@ -0,0 +1,5 @@
1
+ viewtif/tif_viewer.py,sha256=6mTylUguRn56QaJDZiNLSVPyqNWTDW_HKRRoa9eqjKY,30960
2
+ viewtif-0.1.10.dist-info/METADATA,sha256=OTaQUPKmNLiL-L6Pvp5Sin3--KP6s3-MsNWQ7uJKWSk,6338
3
+ viewtif-0.1.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
4
+ viewtif-0.1.10.dist-info/entry_points.txt,sha256=NVEjlRyJ7R7hFPOVsZJio3Hl0VqlX7_oVfA7819XvHM,52
5
+ viewtif-0.1.10.dist-info/RECORD,,
@@ -1,5 +0,0 @@
1
- viewtif/tif_viewer.py,sha256=DR9yvZMfxQWbhVYGuFdrEvMkaV3glH2yeFTpG4D2WAs,28385
2
- viewtif-0.1.8.dist-info/METADATA,sha256=_AgokpNDSWK-MbgS2igM4pTYsD_bCfBSajtjrwFkWUo,5942
3
- viewtif-0.1.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
4
- viewtif-0.1.8.dist-info/entry_points.txt,sha256=NVEjlRyJ7R7hFPOVsZJio3Hl0VqlX7_oVfA7819XvHM,52
5
- viewtif-0.1.8.dist-info/RECORD,,