Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -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
```
Original file line number Diff line number Diff line change
@@ -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)
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -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
```
26 changes: 26 additions & 0 deletions rasterio_generated/write_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
75 changes: 75 additions & 0 deletions tests/test_big_endian.py
Original file line number Diff line number Diff line change
@@ -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)
Loading