wbraster 0.1.4

A pure-Rust library for reading and writing raster GIS formats
Documentation
#[cfg(test)]
mod jpeg2000_byte_alignment_validation {
    use crate::formats::jpeg2000::read;
    use std::path::Path;

    // ── A5 external multiband fixture tests ───────────────────────────────────
    //
    // Fixture: crates/wbraster/tests/fixtures/rgb_8x8_lossless.jp2
    //
    // Generated by: scripts/gen_a5_fixtures.py (rasterio/OpenJPEG, no Rust dep).
    // Committed as a binary asset.  Exact pixel values verified at generation:
    //   Band 1 (index 0): all pixels = 100
    //   Band 2 (index 1): all pixels = 200
    //   Band 3 (index 2): ramp 0..63 (row-major 8×8)
    //
    // These tests exercise the native multicomponent decode path against a real
    // externally-encoded JP2 file without adding any C/OpenJPEG compile dependency.

    const RGB_8X8_FIXTURE: &[u8] = include_bytes!(
        "../../tests/fixtures/rgb_8x8_lossless.jp2"
    );
    const SENTINEL_STYLE_16X16_4B_FIXTURE: &[u8] = include_bytes!(
        "../../tests/fixtures/sentinel_style_16x16_4band_lossless.jp2"
    );
    const TILED_RGB_64X64_BLOCK32_FIXTURE: &[u8] = include_bytes!(
        "../../tests/fixtures/tiled_rgb_64x64_block32_lossless.jp2"
    );

    #[test]
    fn a5_external_rgb_fixture_metadata_parsed_correctly() {
        let jp2 = crate::formats::jpeg2000_core::reader::GeoJp2::from_bytes(RGB_8X8_FIXTURE)
            .expect("A5: should parse external rgb_8x8_lossless.jp2");
        assert_eq!(jp2.width(), 8, "A5: width");
        assert_eq!(jp2.height(), 8, "A5: height");
        assert_eq!(jp2.component_count(), 3, "A5: component count");
        assert_eq!(jp2.bits_per_sample(), 16, "A5: bits per sample");
        assert!(!jp2.is_signed(), "A5: image should be unsigned");
    }

    #[test]
    fn a5_external_rgb_fixture_band0_all_100() {
        let fixture_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/rgb_8x8_lossless.jp2");
        let ras = read(fixture_path).expect("A5: adapter read should decode fixture");
        assert_eq!(ras.bands, 3, "A5: expected 3 bands");
        assert_eq!(ras.rows, 8, "A5: expected 8 rows");
        assert_eq!(ras.cols, 8, "A5: expected 8 cols");

        for row in 0isize..8isize {
            for col in 0isize..8isize {
                let v = ras.get(0, row, col) as u16;
                assert_eq!(v, 100, "A5: band 0 mismatch at ({row},{col})");
            }
        }
    }

    #[test]
    fn a5_external_rgb_fixture_band1_all_200() {
        let fixture_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/rgb_8x8_lossless.jp2");
        let ras = read(fixture_path).expect("A5: adapter read should decode fixture");
        for row in 0isize..8isize {
            for col in 0isize..8isize {
                let v = ras.get(1, row, col) as u16;
                assert_eq!(v, 200, "A5: band 1 mismatch at ({row},{col})");
            }
        }
    }

    #[test]
    fn a5_external_rgb_fixture_band2_ramp() {
        let fixture_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/rgb_8x8_lossless.jp2");
        let ras = read(fixture_path).expect("A5: adapter read should decode fixture");
        for row in 0isize..8isize {
            for col in 0isize..8isize {
                let expected = (row * 8 + col) as u16;
                let got = ras.get(2, row, col) as u16;
                assert_eq!(got, expected, "A5: band 2 mismatch at ({row},{col})");
            }
        }
    }

    #[test]
    fn a5_sentinel_style_fixture_metadata_and_probes() {
        let jp2 = crate::formats::jpeg2000_core::reader::GeoJp2::from_bytes(SENTINEL_STYLE_16X16_4B_FIXTURE)
            .expect("A5: should parse sentinel-style fixture");
        assert_eq!(jp2.width(), 16, "A5 sentinel: width");
        assert_eq!(jp2.height(), 16, "A5 sentinel: height");
        assert_eq!(jp2.component_count(), 4, "A5 sentinel: component count");
        assert_eq!(jp2.bits_per_sample(), 16, "A5 sentinel: bits per sample");

        let fixture_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/sentinel_style_16x16_4band_lossless.jp2");
        let ras = read(fixture_path).expect("A5 sentinel: adapter read should decode fixture");
        assert_eq!(ras.bands, 4, "A5 sentinel: raster bands");
        assert_eq!(ras.rows, 16, "A5 sentinel: raster rows");
        assert_eq!(ras.cols, 16, "A5 sentinel: raster cols");

        // Pattern: band b has base (b+1)*1000 plus row-major offset.
        let probes = [
            (0isize, 0isize, 0isize, 1000u16),
            (0isize, 15isize, 15isize, 1255u16),
            (3isize, 0isize, 0isize, 4000u16),
            (3isize, 10isize, 5isize, 4165u16),
            (3isize, 0isize, 0isize, 4000u16),
            (3isize, 15isize, 15isize, 4255u16),
        ];
        for (band, row, col, expected) in probes {
            let got = ras.get(band, row, col) as u16;
            assert_eq!(got, expected, "A5 sentinel probe mismatch at band={band}, row={row}, col={col}");
        }
    }

    #[test]
    fn a4_sentinel_style_adapter_formula_and_ranges() {
        let fixture_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/sentinel_style_16x16_4band_lossless.jp2");
        let ras = read(fixture_path).expect("A4 sentinel: adapter read should decode fixture");

        assert_eq!(ras.bands, 4, "A4 sentinel: raster bands");
        assert_eq!(ras.rows, 16, "A4 sentinel: raster rows");
        assert_eq!(ras.cols, 16, "A4 sentinel: raster cols");

        let mut band_min = [u16::MAX; 4];
        let mut band_max = [0u16; 4];

        for band in 0isize..4isize {
            for row in 0isize..16isize {
                for col in 0isize..16isize {
                    let expected = ((band as u16 + 1) * 1000) + (row as u16 * 16) + col as u16;
                    let got = ras.get(band, row, col) as u16;
                    assert_eq!(got, expected, "A4 sentinel formula mismatch at band={band}, row={row}, col={col}");

                    let b = band as usize;
                    band_min[b] = band_min[b].min(got);
                    band_max[b] = band_max[b].max(got);
                }
            }
        }

        assert_eq!(band_min[0], 1000, "A4 sentinel: band 0 min mismatch");
        assert_eq!(band_max[0], 1255, "A4 sentinel: band 0 max mismatch");
        assert_eq!(band_min[1], 2000, "A4 sentinel: band 1 min mismatch");
        assert_eq!(band_max[1], 2255, "A4 sentinel: band 1 max mismatch");
        assert_eq!(band_min[2], 3000, "A4 sentinel: band 2 min mismatch");
        assert_eq!(band_max[2], 3255, "A4 sentinel: band 2 max mismatch");
        assert_eq!(band_min[3], 4000, "A4 sentinel: band 3 min mismatch");
        assert_eq!(band_max[3], 4255, "A4 sentinel: band 3 max mismatch");
    }

    #[test]
    fn a5_tiled_multicomponent_fixture_metadata_and_probes() {
        let jp2 = crate::formats::jpeg2000_core::reader::GeoJp2::from_bytes(TILED_RGB_64X64_BLOCK32_FIXTURE)
            .expect("A5: should parse tiled fixture");
        assert_eq!(jp2.width(), 64, "A5 tiled: width");
        assert_eq!(jp2.height(), 64, "A5 tiled: height");
        assert_eq!(jp2.component_count(), 3, "A5 tiled: component count");
        assert_eq!(jp2.bits_per_sample(), 16, "A5 tiled: bits per sample");

        let fixture_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/tiled_rgb_64x64_block32_lossless.jp2");
        let ras = read(fixture_path).expect("A5 tiled: adapter read should decode fixture");
        assert_eq!(ras.bands, 3, "A5 tiled: raster bands");
        assert_eq!(ras.rows, 64, "A5 tiled: raster rows");
        assert_eq!(ras.cols, 64, "A5 tiled: raster cols");

        // Band 0 = 100 + row, Band 1 = 200 + col, Band 2 = row*64 + col.
        let probes = [
            (0isize, 0isize, 0isize, 100u16),
            (0isize, 63isize, 63isize, 163u16),
            (1isize, 0isize, 0isize, 200u16),
            (1isize, 63isize, 63isize, 263u16),
            (2isize, 0isize, 0isize, 0u16),
            (2isize, 63isize, 63isize, 4095u16),
            (2isize, 17isize, 9isize, 1097u16),
        ];
        for (band, row, col, expected) in probes {
            let got = ras.get(band, row, col) as u16;
            assert_eq!(got, expected, "A5 tiled probe mismatch at band={band}, row={row}, col={col}");
        }
    }

    /// Test: Decode B03 JP2 and compare against reference PIL/imagecodecs output.
    /// This isolates the byte-alignment issue in packet header parsing.
    /// 
    /// Expected behavior:
    /// - native output should match PIL values for the first 256 pixels
    /// - PIL uses I;16 mode (16-bit unsigned) for proper B03 decoding
    #[test]
    fn test_b03_pixel_values_match_reference() {
        use crate::raster::RasterData;

        let b03_path = "/Users/johnlindsay/Documents/teaching/GEOG3420/W26/Labs/data/S2A_MSIL2A_20240727T161831_N0511_R040_T17TNJ_20240727T235645.SAFE/GRANULE/L2A_T17TNJ_A047513_20240727T161942/IMG_DATA/R10m/T17TNJ_20240727T161831_B03_10m.jp2";
        
        if !Path::new(b03_path).exists() {
            eprintln!("SKIP: B03 file not found at {}", b03_path);
            return;
        }

        // Reference values from PIL/imagecodecs (I;16 mode, first 256 pixels)
        // Extracted 2024-04-19 via: np.array(Image.open(b03_path), dtype=np.uint16).flat[:256]
        let reference_pixels: Vec<u16> = vec![
             3032,  3056,  3258,  3216,  3070,  3140,  3220,  3376,  3448,  3500,  3460,  3524,  3536,  3588,  3708,  3520,
             3170,  3112,  2974,  3094,  3144,  3132,  3022,  2876,  2990,  3034,  3012,  3140,  3066,  3036,  3184,  3238,
             3148,  3360,  3360,  3412,  3758,  3788,  3654,  3612,  3418,  3462,  3454,  3340,  3162,  3212,  3236,  3204,
             3340,  3478,  3352,  3468,  3676,  3610,  3294,  3230,  3392,  3248,  3396,  3540,  3508,  3456,  3448,  3476,
             3612,  3476,  3472,  3204,  3230,  3464,  3528,  4092,  5428,  5988,  5604,  4852,  4040,  3462,  3296,  3272,
             3264,  3272,  3268,  3226,  3364,  3722,  3500,  3506,  3368,  3322,  3354,  3412,  3328,  3204,  3210,  3230,
             3278,  3240,  3216,  3242,  3266,  3240,  3240,  3322,  3282,  3332,  3584,  3472,  3500,  3452,  3104,  3136,
             3278,  3230,  3186,  3258,  3038,  3072,  3172,  3140,  3180,  3230,  3296,  3286,  3408,  3418,  3618,  4204,
             4088,  4424,  4472,  4428,  4244,  3644,  3330,  3212,  3196,  3136,  3152,  3308,  3330,  3158,  3166,  3616,
             4060,  4136,  3972,  4732,  4816,  3432,  3374,  3296,  3268,  3228,  3324,  3224,  3160,  3264,  3164,  3436,
             3380,  3224,  3196,  3128,  3180,  3128,  3172,  3184,  3124,  3116,  3086,  3056,  2968,  2860,  2876,  2896,
             3072,  3136,  3156,  3162,  3078,  2988,  3180,  3232,  3196,  3138,  3196,  3094,  3130,  3284,  3236,  3288,
             3256,  3204,  3178,  3094,  3190,  3160,  3118,  3180,  4324,  4576,  3816,  4208,  3894,  3826,  5064,  4836,
             3782,  3268,  3078,  3284,  3368,  3480,  3476,  3410,  3418,  3430,  3552,  3500,  4400,  3512,  3376,  3354,
             3318,  3344,  3356,  3392,  3376,  3316,  3206,  3168,  3166,  3088,  3092,  3070,  3022,  3056,  3002,  3148,
             3080,  3080,  3064,  2992,  3008,  3012,  3028,  3000,  2938,  2940,  3074,  3058,  3024,  3100,  3102,  3034,
        ];

        // Read with our decoder
        match read(b03_path) {
            Ok(raster) => {
                let width = raster.cols;
                let height = raster.rows;
                eprintln!("[b03_validation] Decoded: {}x{}", width, height);

                // Extract first 256 pixels from the RasterData
                let native_pixels = match &raster.data {
                    RasterData::U16(vec) => &vec[0..256.min(vec.len())],
                    RasterData::I16(_vec) => {
                        eprintln!("ERROR: Got I16 data, expected U16");
                        return;
                    },
                    _other => {
                        eprintln!("ERROR: Got wrong data type, expected U16");
                        return;
                    }
                };

                if native_pixels.len() < 256 {
                    eprintln!("WARNING: Only {} pixels available, expected 256", native_pixels.len());
                }

                // Compare
                let mut mismatches = 0;
                let mut max_error = 0i32;
                for (i, (&native, &reference)) in native_pixels.iter().zip(reference_pixels.iter()).enumerate() {
                    let error = (native as i32 - reference as i32).abs();
                    if error > 50 {  // Allow tolerance for rounding/bit-depth differences
                        if mismatches < 10 {
                            eprintln!("[b03_validation] Pixel[{}]: native={} reference={} error={}", i, native, reference, error);
                        }
                        mismatches += 1;
                        max_error = max_error.max(error);
                    }
                }

                eprintln!("[b03_validation] Total mismatches (>50 error): {}/{}", mismatches, native_pixels.len());
                eprintln!("[b03_validation] Max error: {}", max_error);

                // For now, just log (don't assert) so we see the actual values
                if mismatches > 0 {
                    eprintln!("[b03_validation] MISMATCH DETECTED - decoder output differs from PIL reference");
                } else {
                    eprintln!("[b03_validation] PASS - pixels match PIL reference!");
                }
            }
            Err(e) => {
                eprintln!("ERROR decoding B03: {}", e);
            }
        }
    }

    /// Test: Specifically validate LL CB(0,0) block decoding.
    /// Extract raw fragment and decode in isolation.
    #[test]
    fn test_ll_codeblock_0_0_isolation() {
        let b03_path = "/Users/johnlindsay/Documents/teaching/GEOG3420/W26/Labs/data/S2A_MSIL2A_20240727T161831_N0511_R040_T17TNJ_20240727T235645.SAFE/GRANULE/L2A_T17TNJ_A047513_20240727T161942/IMG_DATA/R10m/T17TNJ_20240727T161831_B03_10m.jp2";

        if !Path::new(b03_path).exists() {
            eprintln!("SKIP: B03 file not found");
            return;
        }

        // Read the file and decode
        match read(b03_path) {
            Ok(raster) => {
                eprintln!("[ll_cb_isolation] Raster decoded successfully: {}x{}", raster.cols, raster.rows);
                // If we get here without panicking, basic structure is sound.
                // The actual coefficient values validation happens in test_b03_pixel_values_match_reference.
            }
            Err(e) => {
                eprintln!("ERROR: {}", e);
            }
        }
    }
}