viewtif 0.1.9__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)
@@ -190,84 +205,96 @@ class TiffViewer(QMainWindow):
190
205
  except subprocess.CalledProcessError as e:
191
206
  print(f"[WARN] Could not inspect FileGDB: {e}")
192
207
  sys.exit(0)
193
- # --- Universal size check before loading ---
208
+
209
+ # --- Universal size check before loading ---
194
210
  warn_if_large(tif_path, scale=self._scale_arg)
211
+
195
212
  # --------------------- Detect HDF/HDF5 --------------------- #
196
213
  if tif_path.lower().endswith((".hdf", ".h5", ".hdf5")):
197
214
  try:
198
- from osgeo import gdal
199
- gdal.UseExceptions()
200
-
201
- ds = gdal.Open(tif_path)
202
- subs = ds.GetSubDatasets()
203
-
204
- if not subs:
205
- raise ValueError("No subdatasets found in HDF/HDF5 file.")
206
-
207
- print(f"Found {len(subs)} subdatasets in {os.path.basename(tif_path)}:")
208
- for i, (_, desc) in enumerate(subs):
209
- print(f"[{i}] {desc}")
210
-
211
- # Only list subsets if --subset not given
212
- if subset is None:
213
- print("\nUse --subset N to open a specific subdataset.")
214
- 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)
215
283
 
216
- # Validate subset index
217
- if subset < 0 or subset >= len(subs):
218
- raise ValueError(f"Invalid subset index {subset}. Valid range: 0–{len(subs)-1}")
219
-
220
- sub_name, desc = subs[subset]
221
- print(f"\nOpening subdataset [{subset}]: {desc}")
222
- sub_ds = gdal.Open(sub_name)
223
-
224
- # --- Read once ---
225
- arr = sub_ds.ReadAsArray().astype(np.float32)
226
- #print(f"Raw array shape from GDAL: {arr.shape} (ndim={arr.ndim})")
227
-
228
- # --- Normalize shape ---
229
- arr = np.squeeze(arr)
230
- if arr.ndim == 3:
231
- # Convert from (bands, rows, cols) → (rows, cols, bands)
232
- arr = np.transpose(arr, (1, 2, 0))
233
- #print(f"Transposed to {arr.shape} (rows, cols, bands)")
234
- elif arr.ndim == 2:
235
- print("Single-band dataset.")
236
- else:
237
- raise ValueError(f"Unexpected array shape {arr.shape}")
238
-
239
- # --- Downsample large arrays for responsiveness ---
240
- h, w = arr.shape[:2]
241
- if h * w > 4_000_000:
242
- step = max(2, int((h * w / 4_000_000) ** 0.5))
243
- arr = arr[::step, ::step] if arr.ndim == 2 else arr[::step, ::step, :]
244
- print(f"⚠️ Large dataset preview: downsampled by {step}x")
245
-
246
- # --- Final assignments ---
247
- self.data = arr
248
- self._transform = None
249
- self._crs = None
250
- self.band_count = arr.shape[2] if arr.ndim == 3 else 1
251
- self.band_index = 0
252
- self.vmin, self.vmax = np.nanmin(arr), np.nanmax(arr)
253
-
254
- if self.band_count > 1:
255
- print(f"This subdataset has {self.band_count} bands — switch with [ and ] keys.")
256
- else:
257
- 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.")
258
288
 
259
- # --- If user specified --band, start there ---
260
- if self.band and self.band <= self.band_count:
261
- self.band_index = self.band - 1
262
- print(f"Opening band {self.band}/{self.band_count}")
263
- else:
264
- 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}")
265
292
 
266
- except ImportError:
267
- raise RuntimeError(
268
- "HDF support requires GDAL.\n"
269
- "Install it first (e.g., brew install gdal && pip install GDAL)"
270
- )
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
+ )
271
298
 
272
299
  # --------------------- Regular GeoTIFF --------------------- #
273
300
  else:
@@ -487,8 +514,7 @@ class TiffViewer(QMainWindow):
487
514
  rgb = np.zeros_like(arr)
488
515
  if np.any(finite):
489
516
  # Global 2–98 percentile stretch across all bands (QGIS-like)
490
- global_min = np.nanpercentile(arr, 2)
491
- global_max = np.nanpercentile(arr, 98)
517
+ global_min, global_max = np.nanpercentile(arr, (2, 98))
492
518
  rng = max(global_max - global_min, 1e-12)
493
519
  norm = np.clip((arr - global_min) / rng, 0, 1)
494
520
  rgb = np.clip(norm * self.contrast, 0, 1)
@@ -640,8 +666,8 @@ class TiffViewer(QMainWindow):
640
666
  super().keyPressEvent(ev)
641
667
 
642
668
 
643
- # --------------------------------- CLI ----------------------------------- #
644
- def main():
669
+ # --------------------------------- Legacy argparse CLI (not used by default) ----------------------------------- #
670
+ def legacy_argparse_main():
645
671
  parser = argparse.ArgumentParser(description="TIFF viewer with RGB (2–98%) & shapefile overlays")
646
672
  parser.add_argument("tif_path", nargs="?", help="Path to TIFF (optional if --rgbfiles is used)")
647
673
  parser.add_argument("--scale", type=int, default=1, help="Downsample factor (1=full, 10=10x smaller)")
@@ -653,6 +679,8 @@ def main():
653
679
  parser.add_argument("--shp-width", type=float, default=1.5, help="Overlay line width (screen pixels). Default: 1.5")
654
680
  args = parser.parse_args()
655
681
 
682
+ from PySide6.QtCore import Qt
683
+ QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True)
656
684
  app = QApplication(sys.argv)
657
685
  win = TiffViewer(
658
686
  args.tif_path,
@@ -677,9 +705,12 @@ def run_viewer(
677
705
  shapefile=None,
678
706
  shp_color=None,
679
707
  shp_width=None,
680
- subset=None,
708
+ subset=None
681
709
  ):
710
+
682
711
  """Launch the TiffViewer app"""
712
+ from PySide6.QtCore import Qt
713
+ QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True)
683
714
  app = QApplication(sys.argv)
684
715
  win = TiffViewer(
685
716
  tif_path,
@@ -690,7 +721,7 @@ def run_viewer(
690
721
  shapefiles=shapefile,
691
722
  shp_color=shp_color,
692
723
  shp_width=shp_width,
693
- subset=subset,
724
+ subset=subset
694
725
  )
695
726
  win.show()
696
727
  sys.exit(app.exec())
@@ -698,16 +729,17 @@ def run_viewer(
698
729
  import click
699
730
 
700
731
  @click.command()
701
- @click.version_option("1.0.9", prog_name="viewtif")
732
+ @click.version_option("0.1.10", prog_name="viewtif")
702
733
  @click.argument("tif_path", required=False)
703
734
  @click.option("--band", default=1, show_default=True, type=int, help="Band number to display")
704
- @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")
705
736
  @click.option("--rgb", nargs=3, type=int, help="Three band numbers for RGB, e.g. --rgb 4 3 2")
706
737
  @click.option("--rgbfiles", nargs=3, type=str, help="Three single-band TIFFs for RGB, e.g. --rgbfiles B4.tif B3.tif B2.tif")
707
738
  @click.option("--shapefile", multiple=True, type=str, help="One or more shapefiles to overlay")
708
739
  @click.option("--shp-color", default="white", show_default=True, help="Overlay color (name or #RRGGBB).")
709
740
  @click.option("--shp-width", default=1.0, show_default=True, type=float, help="Overlay line width (screen pixels).")
710
741
  @click.option("--subset", default=None, type=int, help="Open specific subdataset index in .hdf/.h5 file")
742
+
711
743
  def main(tif_path, band, scale, rgb, rgbfiles, shapefile, shp_color, shp_width, subset):
712
744
  """Lightweight GeoTIFF viewer."""
713
745
  # --- Warn early if shapefile requested but geopandas missing ---
@@ -727,7 +759,7 @@ def main(tif_path, band, scale, rgb, rgbfiles, shapefile, shp_color, shp_width,
727
759
  shapefile=shapefile,
728
760
  shp_color=shp_color,
729
761
  shp_width=shp_width,
730
- subset=subset,
762
+ subset=subset
731
763
  )
732
764
 
733
765
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: viewtif
3
- Version: 0.1.9
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,7 +92,7 @@ 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.
90
- When you open a .gdb directly, `viewtif`` will list available raster datasets first, then you can choose one to view.
95
+ When you open a .gdb directly, `viewtif` will list available raster datasets first, then you can choose one to view.
91
96
 
92
97
  ```bash
93
98
  # List available raster datasets
@@ -138,4 +143,4 @@ This project is released under the MIT License.
138
143
 
139
144
  ## Contributors
140
145
  - [@HarshShinde0](https://github.com/HarshShinde0) — added mouse-wheel and trackpad zoom support
141
- - [@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=huQInI8s-OxgoxqLPy0QzzRh4OTRZHOG2QE_0llK-Dw,29612
2
- viewtif-0.1.9.dist-info/METADATA,sha256=vKcAT60CbNgz5Ax-XahxEuhZhU4ZRS8tgb-Z_WVOM9Q,6234
3
- viewtif-0.1.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
4
- viewtif-0.1.9.dist-info/entry_points.txt,sha256=NVEjlRyJ7R7hFPOVsZJio3Hl0VqlX7_oVfA7819XvHM,52
5
- viewtif-0.1.9.dist-info/RECORD,,