diff --git a/rasterio_generated/fixtures/float32_1band_deflate_block64_predictor3_big_endian.py b/rasterio_generated/fixtures/float32_1band_deflate_block64_predictor3_big_endian.py new file mode 100644 index 0000000..2d5a935 --- /dev/null +++ b/rasterio_generated/fixtures/float32_1band_deflate_block64_predictor3_big_endian.py @@ -0,0 +1,36 @@ +"""Generate a big endian (MM) float32 GeoTIFF with the floating point predictor.""" + +import tempfile +from pathlib import Path + +import numpy as np + +from rasterio_generated.write_utils import rewrite_big_endian, write_cog + +HERE = Path(__file__).parent + + +def generate_source(output_path: Path) -> None: + """Generate the little endian COG this fixture is rewritten from.""" + data = np.linspace(0.0, 1.0, 128 * 128, dtype=np.float32).reshape(128, 128) + + write_cog( + output_path, + data, + blocksize=64, + compress="DEFLATE", + predictor=3, + nodata_type=None, + ) + + +def generate(output_path: Path) -> None: + """Generate a 128x128 tiled float32 big endian GeoTIFF with DEFLATE and predictor 3. + + Predictor 3 splits each row into byte planes, most significant byte first, + whatever the file's byte order, so readers must not swap these samples. + """ + with tempfile.TemporaryDirectory() as tmp: + src_path = Path(tmp) / "source.tif" + generate_source(src_path) + rewrite_big_endian(src_path, output_path) diff --git a/rasterio_generated/fixtures/float32_1band_deflate_block64_predictor3_big_endian.tif b/rasterio_generated/fixtures/float32_1band_deflate_block64_predictor3_big_endian.tif new file mode 100644 index 0000000..4cae054 Binary files /dev/null and b/rasterio_generated/fixtures/float32_1band_deflate_block64_predictor3_big_endian.tif differ diff --git a/rasterio_generated/fixtures/float32_1band_deflate_block64_predictor3_big_endian_info.md b/rasterio_generated/fixtures/float32_1band_deflate_block64_predictor3_big_endian_info.md new file mode 100644 index 0000000..d025b63 --- /dev/null +++ b/rasterio_generated/fixtures/float32_1band_deflate_block64_predictor3_big_endian_info.md @@ -0,0 +1,47 @@ +``` +Driver: GTiff +File: rasterio_generated/fixtures/float32_1band_deflate_block64_predictor3_big_endian.tif +COG: True +Compression: DEFLATE +ColorSpace: None + +Profile + Width: 128 + Height: 128 + Bands: 1 + Tiled: True + Dtype: float32 + NoData: None + Alpha Band: False + Internal Mask: False + Interleave: BAND + ColorMap: False + ColorInterp: ('gray',) + Scales: (1.0,) + Offsets: (0.0,) + +Geo + Crs: EPSG:4326 + Origin: (0.0, 0.0) + Resolution: (0.01, -0.01) + BoundingBox: (0.0, -1.28, 1.28, 0.0) + MinZoom: 7 + MaxZoom: 7 + +Image Metadata + AREA_OR_POINT: Area + +Image Structure + LAYOUT: COG + COMPRESSION: DEFLATE + INTERLEAVE: BAND + PREDICTOR: 3 + +Band 1 + ColorInterp: gray + +IFD + Id Size BlockSize Decimation + 0 128x128 64x64 0 + 1 64x64 64x64 2 +``` diff --git a/rasterio_generated/fixtures/uint16_1band_lzw_block128_predictor2_big_endian.py b/rasterio_generated/fixtures/uint16_1band_lzw_block128_predictor2_big_endian.py new file mode 100644 index 0000000..891f78b --- /dev/null +++ b/rasterio_generated/fixtures/uint16_1band_lzw_block128_predictor2_big_endian.py @@ -0,0 +1,23 @@ +"""Generate a big endian (MM) copy of uint16_1band_lzw_block128_predictor2.""" + +import tempfile +from pathlib import Path + +from rasterio_generated.fixtures.uint16_1band_lzw_block128_predictor2 import ( + generate as generate_source, +) +from rasterio_generated.write_utils import rewrite_big_endian + +HERE = Path(__file__).parent + + +def generate(output_path: Path) -> None: + """Generate a 128x128 tiled uint16 big endian GeoTIFF with LZW and predictor 2. + + Readers must swap samples to native order before undoing horizontal + differencing, which adds neighbouring samples as integers. + """ + with tempfile.TemporaryDirectory() as tmp: + src_path = Path(tmp) / "source.tif" + generate_source(src_path) + rewrite_big_endian(src_path, output_path) diff --git a/rasterio_generated/fixtures/uint16_1band_lzw_block128_predictor2_big_endian.tif b/rasterio_generated/fixtures/uint16_1band_lzw_block128_predictor2_big_endian.tif new file mode 100644 index 0000000..031d557 Binary files /dev/null and b/rasterio_generated/fixtures/uint16_1band_lzw_block128_predictor2_big_endian.tif differ diff --git a/rasterio_generated/fixtures/uint16_1band_lzw_block128_predictor2_big_endian_info.md b/rasterio_generated/fixtures/uint16_1band_lzw_block128_predictor2_big_endian_info.md new file mode 100644 index 0000000..873b87f --- /dev/null +++ b/rasterio_generated/fixtures/uint16_1band_lzw_block128_predictor2_big_endian_info.md @@ -0,0 +1,47 @@ +``` +Driver: GTiff +File: rasterio_generated/fixtures/uint16_1band_lzw_block128_predictor2_big_endian.tif +COG: True +Compression: LZW +ColorSpace: None + +Profile + Width: 128 + Height: 128 + Bands: 1 + Tiled: True + Dtype: uint16 + NoData: 0.0 + Alpha Band: False + Internal Mask: False + Interleave: BAND + ColorMap: False + ColorInterp: ('gray',) + Scales: (1.0,) + Offsets: (0.0,) + +Geo + Crs: EPSG:4326 + Origin: (0.0, 0.0) + Resolution: (0.01, -0.01) + BoundingBox: (0.0, -1.28, 1.28, 0.0) + MinZoom: 7 + MaxZoom: 7 + +Image Metadata + AREA_OR_POINT: Area + +Image Structure + LAYOUT: COG + COMPRESSION: LZW + INTERLEAVE: BAND + PREDICTOR: 2 + +Band 1 + ColorInterp: gray + +IFD + Id Size BlockSize Decimation + 0 128x128 64x64 0 + 1 64x64 64x64 2 +``` diff --git a/rasterio_generated/write_utils.py b/rasterio_generated/write_utils.py index 4a04758..c0551db 100644 --- a/rasterio_generated/write_utils.py +++ b/rasterio_generated/write_utils.py @@ -181,3 +181,29 @@ def write_cog( # Copy to output path copy(mem, path, **cog_profile) + + +def rewrite_big_endian(src_path: Path, path: Path) -> None: + """Rewrite a COG as a big endian (MM) TIFF, keeping its layout. + + GDAL's COG driver has no ENDIANNESS option, so this copies through the GTiff + driver with COPY_SRC_OVERVIEWS, which keeps the COG layout: header and IFDs + first, then overview tiles, then full resolution tiles. Tiling, compression + and predictor are taken from the source. + """ + with rasterio.open(src_path) as src: + block_height, block_width = src.block_shapes[0] + image_structure = src.tags(ns="IMAGE_STRUCTURE") + creation_options = { + "driver": "GTiff", + "tiled": True, + "blockxsize": block_width, + "blockysize": block_height, + "compress": image_structure["COMPRESSION"], + "interleave": image_structure["INTERLEAVE"], + "endianness": "BIG", + } + if "PREDICTOR" in image_structure: + creation_options["predictor"] = image_structure["PREDICTOR"] + + copy(src, path, copy_src_overviews=True, **creation_options) diff --git a/tests/test_big_endian.py b/tests/test_big_endian.py new file mode 100644 index 0000000..1493c0d --- /dev/null +++ b/tests/test_big_endian.py @@ -0,0 +1,75 @@ +"""Check the big endian fixtures. + +GDAL's COG driver can't write big endian files, so these fixtures are rewritten +from a little endian COG through the GTiff driver. Check that they really are +big endian, carry the predictor they are named for on every IFD, and hold the +same pixels as their little endian source at every level. +""" + +from collections.abc import Callable +from pathlib import Path + +import numpy as np +import pytest +import rasterio +import tifffile + +from rasterio_generated.fixtures import ( + float32_1band_deflate_block64_predictor3_big_endian as float32_predictor3, +) +from rasterio_generated.fixtures import ( + uint16_1band_lzw_block128_predictor2 as uint16_predictor2, +) + +FIXTURES_DIR = Path(__file__).parent.parent / "rasterio_generated" / "fixtures" + +CASES = [ + ( + "uint16_1band_lzw_block128_predictor2_big_endian", + 2, + uint16_predictor2.generate, + ), + ( + "float32_1band_deflate_block64_predictor3_big_endian", + 3, + float32_predictor3.generate_source, + ), +] + + +def read_levels(path: Path) -> list[np.ndarray]: + """Read the full resolution image and every overview.""" + with rasterio.open(path) as src: + levels = [src.read()] + overview_count = len(src.overviews(1)) + for level in range(overview_count): + with rasterio.open(path, overview_level=level) as src: + levels.append(src.read()) + return levels + + +@pytest.mark.parametrize(("name", "predictor", "generate_source"), CASES) +def test_big_endian_fixture( + name: str, + predictor: int, + generate_source: Callable[[Path], None], + tmp_path: Path, +): + path = FIXTURES_DIR / f"{name}.tif" + assert path.read_bytes()[:2] == b"MM" + + with tifffile.TiffFile(path) as tif: + assert len(tif.pages) > 1 + assert [int(page.predictor) for page in tif.pages] == [predictor] * len( + tif.pages + ) + + source_path = tmp_path / "source.tif" + generate_source(source_path) + assert source_path.read_bytes()[:2] == b"II" + + actual = read_levels(path) + expected = read_levels(source_path) + assert len(actual) == len(expected) + for actual_level, expected_level in zip(actual, expected): + np.testing.assert_array_equal(actual_level, expected_level)