@@ -1662,6 +1662,14 @@ def _read_cog_http(url: str, overview_level: int | None = None,
16621662 if arr .ndim == 3 and ifd .samples_per_pixel > 1 and band is not None :
16631663 arr = arr [:, :, band ]
16641664
1665+ # Apply Orientation tag (274) so HTTP reads return the same pixel
1666+ # order and transform as the local-file path. Only the full-read
1667+ # branch reaches here; the windowed-read branch is rejected above
1668+ # for non-default orientation. See issue #1717.
1669+ if ifd .orientation != 1 :
1670+ arr , geo_info = _apply_orientation_with_geo (
1671+ arr , geo_info , ifd .orientation )
1672+
16651673 return arr , geo_info
16661674
16671675
@@ -1948,6 +1956,69 @@ def _apply_orientation(arr: np.ndarray, orientation: int) -> np.ndarray:
19481956 )
19491957
19501958
1959+ def _apply_orientation_with_geo (
1960+ arr : np .ndarray , geo_info : GeoInfo , orientation : int ,
1961+ ) -> tuple [np .ndarray , GeoInfo ]:
1962+ """Apply Orientation tag to ``arr`` and update ``geo_info`` to match.
1963+
1964+ Shared helper used by the local-file and HTTP COG paths so both
1965+ return the same pixel order and transform for a given file. See
1966+ issue #1717 for the HTTP-path parity break this consolidates.
1967+ """
1968+ if orientation == 1 :
1969+ return arr , geo_info
1970+ # Use the *file* dimensions (before orientation) for the transform
1971+ # math below. After ``_apply_orientation`` the array shape may swap
1972+ # (orientations 5-8), so capture them now.
1973+ file_h = arr .shape [0 ]
1974+ file_w = arr .shape [1 ]
1975+ arr = _apply_orientation (arr , orientation )
1976+ t = geo_info .transform
1977+ if not geo_info .has_georef :
1978+ pass
1979+ elif orientation in (2 , 3 , 4 ):
1980+ if geo_info .raster_type == RASTER_PIXEL_IS_POINT :
1981+ x_shift = file_w - 1
1982+ y_shift = file_h - 1
1983+ else :
1984+ x_shift = file_w
1985+ y_shift = file_h
1986+ new_origin_x = t .origin_x
1987+ new_origin_y = t .origin_y
1988+ new_px_w = t .pixel_width
1989+ new_px_h = t .pixel_height
1990+ if orientation in (2 , 3 ): # x flipped
1991+ new_origin_x = t .origin_x + x_shift * t .pixel_width
1992+ new_px_w = - t .pixel_width
1993+ if orientation in (3 , 4 ): # y flipped
1994+ new_origin_y = t .origin_y + y_shift * t .pixel_height
1995+ new_px_h = - t .pixel_height
1996+ geo_info .transform = GeoTransform (
1997+ origin_x = new_origin_x ,
1998+ origin_y = new_origin_y ,
1999+ pixel_width = new_px_w ,
2000+ pixel_height = new_px_h ,
2001+ )
2002+ elif orientation in (5 , 6 , 7 , 8 ):
2003+ geo_info .transform = GeoTransform (
2004+ origin_x = t .origin_x ,
2005+ origin_y = t .origin_y ,
2006+ pixel_width = t .pixel_height ,
2007+ pixel_height = t .pixel_width ,
2008+ )
2009+ if (geo_info .crs_epsg is not None
2010+ or geo_info .crs_wkt is not None ):
2011+ import warnings
2012+ warnings .warn (
2013+ f"Orientation { orientation } swaps spatial axes on "
2014+ f"a georeferenced file; the returned coords are "
2015+ f"shape-correct but the geographic transform may "
2016+ f"need manual adjustment." ,
2017+ stacklevel = 2 ,
2018+ )
2019+ return arr , geo_info
2020+
2021+
19512022def read_to_array (source , * , window = None , overview_level : int | None = None ,
19522023 band : int | None = None ,
19532024 max_pixels : int = MAX_PIXELS_DEFAULT ,
@@ -2076,86 +2147,8 @@ def read_to_array(source, *, window=None, overview_level: int | None = None,
20762147 arr = arr [:, :, band ]
20772148
20782149 if orientation != 1 :
2079- # Use the *file* dimensions (before orientation) for the
2080- # transform-flip math below. After ``_apply_orientation`` the
2081- # array shape may swap (orientations 5-8), so capture them now.
2082- file_h = arr .shape [0 ]
2083- file_w = arr .shape [1 ]
2084- arr = _apply_orientation (arr , orientation )
2085- # The pixel buffer was just remapped; the transform that maps
2086- # display pixels back to geographic coordinates needs the
2087- # matching remap or the y/x coords still describe the file's
2088- # original layout.
2089- #
2090- # Orientations 2-4 are pure mirror flips: the array shape stays
2091- # the same, but the displayed origin moves to the opposite
2092- # edge along whichever axes were flipped. Update origin and
2093- # sign of the affected pixel scale so xarray coords land on
2094- # the right geographic positions.
2095- #
2096- # Orientations 5-8 swap rows and columns. Pixel sizes swap
2097- # axes so coord array lengths match the new shape. Signs are
2098- # preserved rather than coerced to north-up since some
2099- # legitimate files use a non-standard sign convention
2100- # (south-up, west-up). For 6/7/8 (rotations + flips, not a
2101- # pure transpose) the swap is geometrically inexact for
2102- # georef'd files: a strict implementation would also adjust
2103- # origin and re-sign per axis. Those files are vanishingly
2104- # rare in practice (TIFF Orientation 5-8 with a meaningful
2105- # ModelTransformation); warn so the user knows to verify.
2106- t = geo_info .transform
2107- # Only georeferenced files have a meaningful transform to flip.
2108- # Plain TIFFs with an Orientation tag but no GeoTIFF tags get
2109- # their pixel buffer remapped above; their default transform
2110- # is left untouched and the downstream consumer falls back to
2111- # integer pixel coords.
2112- if not geo_info .has_georef :
2113- pass
2114- elif orientation in (2 , 3 , 4 ):
2115- # PixelIsPoint tiepoints are at pixel centers, so the
2116- # opposite-edge pixel sits ``(N-1) * step`` away. PixelIsArea
2117- # tiepoints are at pixel edges, so the opposite edge is
2118- # ``N * step`` away. The two cases collapse to a single
2119- # formula below by switching the offset.
2120- if geo_info .raster_type == RASTER_PIXEL_IS_POINT :
2121- x_shift = file_w - 1
2122- y_shift = file_h - 1
2123- else :
2124- x_shift = file_w
2125- y_shift = file_h
2126- new_origin_x = t .origin_x
2127- new_origin_y = t .origin_y
2128- new_px_w = t .pixel_width
2129- new_px_h = t .pixel_height
2130- if orientation in (2 , 3 ): # x flipped
2131- new_origin_x = t .origin_x + x_shift * t .pixel_width
2132- new_px_w = - t .pixel_width
2133- if orientation in (3 , 4 ): # y flipped
2134- new_origin_y = t .origin_y + y_shift * t .pixel_height
2135- new_px_h = - t .pixel_height
2136- geo_info .transform = GeoTransform (
2137- origin_x = new_origin_x ,
2138- origin_y = new_origin_y ,
2139- pixel_width = new_px_w ,
2140- pixel_height = new_px_h ,
2141- )
2142- elif orientation in (5 , 6 , 7 , 8 ):
2143- geo_info .transform = GeoTransform (
2144- origin_x = t .origin_x ,
2145- origin_y = t .origin_y ,
2146- pixel_width = t .pixel_height ,
2147- pixel_height = t .pixel_width ,
2148- )
2149- if (geo_info .crs_epsg is not None
2150- or geo_info .crs_wkt is not None ):
2151- import warnings
2152- warnings .warn (
2153- f"Orientation { orientation } swaps spatial axes on "
2154- f"a georeferenced file; the returned coords are "
2155- f"shape-correct but the geographic transform may "
2156- f"need manual adjustment." ,
2157- stacklevel = 2 ,
2158- )
2150+ arr , geo_info = _apply_orientation_with_geo (
2151+ arr , geo_info , orientation )
21592152
21602153 # MinIsWhite (photometric=0): invert single-band grayscale values
21612154 if ifd .photometric == 0 and ifd .samples_per_pixel == 1 :
0 commit comments