| /* | |
| * Copyright 2026 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "rust/exif/FFI.h" | |
| #include "rust/exif/FFI.rs.h" | |
| #include "include/codec/SkEncodedOrigin.h" | |
| #include "include/private/SkExif.h" | |
| #include "tests/Test.h" | |
| #include "tools/Resources.h" | |
| #include <cmath> | |
| // Helpers: build minimal EXIF blobs (big- and little-endian) for unit tests. | |
| // Writes a 2-byte big-endian u16 into `buf` at `off`. | |
| static void write_u16_be(std::vector<uint8_t>& buf, size_t off, uint16_t v) { | |
| buf[off] = static_cast<uint8_t>(v >> 8); | |
| buf[off + 1] = static_cast<uint8_t>(v); | |
| } | |
| // Writes a 4-byte big-endian u32 into `buf` at `off`. | |
| static void write_u32_be(std::vector<uint8_t>& buf, size_t off, uint32_t v) { | |
| buf[off] = static_cast<uint8_t>(v >> 24); | |
| buf[off + 1] = static_cast<uint8_t>(v >> 16); | |
| buf[off + 2] = static_cast<uint8_t>(v >> 8); | |
| buf[off + 3] = static_cast<uint8_t>(v); | |
| } | |
| // Writes a 2-byte little-endian u16 into `buf` at `off`. | |
| static void write_u16_le(std::vector<uint8_t>& buf, size_t off, uint16_t v) { | |
| buf[off] = static_cast<uint8_t>(v); | |
| buf[off + 1] = static_cast<uint8_t>(v >> 8); | |
| } | |
| // Writes a 4-byte little-endian u32 into `buf` at `off`. | |
| static void write_u32_le(std::vector<uint8_t>& buf, size_t off, uint32_t v) { | |
| buf[off] = static_cast<uint8_t>(v); | |
| buf[off + 1] = static_cast<uint8_t>(v >> 8); | |
| buf[off + 2] = static_cast<uint8_t>(v >> 16); | |
| buf[off + 3] = static_cast<uint8_t>(v >> 24); | |
| } | |
| // Returns a TIFF header (8 bytes, big-endian) pointing to IFD at `ifd_offset`. | |
| static std::vector<uint8_t> tiff_header_be(uint32_t ifd_offset = 8) { | |
| return {'M', 'M', 0x00, 0x2a, | |
| static_cast<uint8_t>(ifd_offset >> 24), | |
| static_cast<uint8_t>(ifd_offset >> 16), | |
| static_cast<uint8_t>(ifd_offset >> 8), | |
| static_cast<uint8_t>(ifd_offset)}; | |
| } | |
| // Returns a TIFF header (8 bytes, little-endian) pointing to IFD at `ifd_offset`. | |
| static std::vector<uint8_t> tiff_header_le(uint32_t ifd_offset = 8) { | |
| return {'I', 'I', 0x2a, 0x00, | |
| static_cast<uint8_t>(ifd_offset), | |
| static_cast<uint8_t>(ifd_offset >> 8), | |
| static_cast<uint8_t>(ifd_offset >> 16), | |
| static_cast<uint8_t>(ifd_offset >> 24)}; | |
| } | |
| // Builds a single-IFD little-endian EXIF blob containing only an Orientation tag. | |
| static std::vector<uint8_t> make_orientation_exif_le(uint16_t orientation) { | |
| std::vector<uint8_t> buf(26, 0); | |
| auto hdr = tiff_header_le(8); | |
| std::copy(hdr.begin(), hdr.end(), buf.begin()); | |
| write_u16_le(buf, 8, 1); // IFD entry count = 1 | |
| write_u16_le(buf, 10, 0x0112); // tag: Orientation | |
| write_u16_le(buf, 12, 3); // type: SHORT | |
| write_u32_le(buf, 14, 1); // count | |
| write_u16_le(buf, 18, orientation); // value | |
| write_u32_le(buf, 22, 0); // next IFD offset | |
| return buf; | |
| } | |
| // Builds a single-IFD EXIF blob containing only a SHORT Orientation tag. | |
| static std::vector<uint8_t> make_orientation_exif_be(uint16_t orientation) { | |
| // Header (8) + entry count (2) + 1 entry (12) + next IFD offset (4) = 26 bytes | |
| std::vector<uint8_t> buf(26, 0); | |
| // TIFF header pointing to IFD at offset 8. | |
| auto hdr = tiff_header_be(8); | |
| std::copy(hdr.begin(), hdr.end(), buf.begin()); | |
| // IFD entry count = 1. | |
| write_u16_be(buf, 8, 1); | |
| // Entry: tag=0x0112 (Orientation), type=3 (SHORT), count=1, value=orientation. | |
| write_u16_be(buf, 10, 0x0112); // tag | |
| write_u16_be(buf, 12, 3); // type SHORT | |
| write_u32_be(buf, 14, 1); // count | |
| write_u16_be(buf, 18, orientation); // value (fits in 4 bytes inline, BE) | |
| // next IFD = 0 (no more IFDs). | |
| write_u32_be(buf, 22, 0); | |
| return buf; | |
| } | |
| // Builds a single-IFD EXIF blob with XResolution, YResolution, ResolutionUnit. | |
| static std::vector<uint8_t> make_resolution_exif_be(float xres, float yres, uint16_t unit) { | |
| // The RATIONAL values (8 bytes each) are stored after the IFD. | |
| // Header=8, count=2, entries=3×12=36, next_ifd=4 → data at offset 52. | |
| // XResolution → RATIONAL at offset 52, YResolution at 60. | |
| const uint32_t xres_off = 52, yres_off = 60; | |
| std::vector<uint8_t> buf(68, 0); | |
| auto hdr = tiff_header_be(8); | |
| std::copy(hdr.begin(), hdr.end(), buf.begin()); | |
| write_u16_be(buf, 8, 3); // 3 entries | |
| // Entry 0: ResolutionUnit (tag 0x0128, SHORT, 1, unit) | |
| write_u16_be(buf, 10, 0x0128); | |
| write_u16_be(buf, 12, 3); // SHORT | |
| write_u32_be(buf, 14, 1); | |
| write_u16_be(buf, 18, unit); | |
| // Entry 1: XResolution (tag 0x011a, RATIONAL, 1, offset) | |
| write_u16_be(buf, 22, 0x011a); | |
| write_u16_be(buf, 24, 5); // RATIONAL | |
| write_u32_be(buf, 26, 1); | |
| write_u32_be(buf, 30, xres_off); | |
| // Entry 2: YResolution (tag 0x011b, RATIONAL, 1, offset) | |
| write_u16_be(buf, 34, 0x011b); | |
| write_u16_be(buf, 36, 5); // RATIONAL | |
| write_u32_be(buf, 38, 1); | |
| write_u32_be(buf, 42, yres_off); | |
| // next IFD offset = 0 | |
| write_u32_be(buf, 46, 0); | |
| // XResolution rational: (xres * 1000) / 1000 → numerator/denominator | |
| auto xnum = static_cast<uint32_t>(xres * 1000.0f + 0.5f); | |
| write_u32_be(buf, xres_off, xnum); | |
| write_u32_be(buf, xres_off + 4, 1000); | |
| auto ynum = static_cast<uint32_t>(yres * 1000.0f + 0.5f); | |
| write_u32_be(buf, yres_off, ynum); | |
| write_u32_be(buf, yres_off + 4, 1000); | |
| return buf; | |
| } | |
| // Unit tests: parse_exif() + ToSkExifMetadata() on synthetic EXIF data. | |
| DEF_TEST(RustExif_empty_data_returns_false, r) { | |
| rust_exif::ExifMetadata meta; | |
| bool ok = rust_exif::parse_exif(rust::Slice<const uint8_t>(nullptr, 0), meta); | |
| REPORTER_ASSERT(r, !ok); | |
| } | |
| DEF_TEST(RustExif_invalid_data_returns_false, r) { | |
| rust_exif::ExifMetadata meta; | |
| const uint8_t garbage[] = {'n', 'o', 't', ' ', 'e', 'x', 'i', 'f'}; | |
| bool ok = rust_exif::parse_exif( | |
| rust::Slice<const uint8_t>(garbage, sizeof(garbage)), meta); | |
| REPORTER_ASSERT(r, !ok); | |
| } | |
| DEF_TEST(RustExif_orientation_parsing, r) { | |
| for (uint16_t orient = 1; orient <= 8; ++orient) { | |
| auto blob = make_orientation_exif_be(orient); | |
| rust_exif::ExifMetadata meta; | |
| bool ok = rust_exif::parse_exif( | |
| rust::Slice<const uint8_t>(blob.data(), blob.size()), meta); | |
| REPORTER_ASSERT(r, ok); | |
| REPORTER_ASSERT(r, meta.has_origin); | |
| REPORTER_ASSERT(r, meta.origin == static_cast<uint32_t>(orient)); | |
| // Verify ToSkExifMetadata maps it to the right SkEncodedOrigin. | |
| SkExif::Metadata sk_meta; | |
| rust_exif::ToSkExifMetadata(meta, &sk_meta); | |
| REPORTER_ASSERT(r, sk_meta.fOrigin.has_value()); | |
| REPORTER_ASSERT(r, sk_meta.fOrigin.value() == static_cast<SkEncodedOrigin>(orient)); | |
| } | |
| } | |
| DEF_TEST(RustExif_orientation_out_of_range_ignored, r) { | |
| // Orientation value 0 is invalid (EXIF spec says 1-8). | |
| auto blob = make_orientation_exif_be(0); | |
| rust_exif::ExifMetadata meta; | |
| rust_exif::parse_exif(rust::Slice<const uint8_t>(blob.data(), blob.size()), meta); | |
| REPORTER_ASSERT(r, !meta.has_origin); | |
| // Orientation value 9 is out of range. | |
| blob = make_orientation_exif_be(9); | |
| rust_exif::parse_exif(rust::Slice<const uint8_t>(blob.data(), blob.size()), meta); | |
| REPORTER_ASSERT(r, !meta.has_origin); | |
| } | |
| DEF_TEST(RustExif_little_endian_parsing, r) { | |
| // EXIF supports both big-endian ('M','M') and little-endian ('I','I') byte order. | |
| // Verify little-endian blobs are parsed correctly. | |
| for (uint16_t orient = 1; orient <= 8; ++orient) { | |
| auto blob = make_orientation_exif_le(orient); | |
| rust_exif::ExifMetadata meta; | |
| bool ok = rust_exif::parse_exif( | |
| rust::Slice<const uint8_t>(blob.data(), blob.size()), meta); | |
| REPORTER_ASSERT(r, ok); | |
| REPORTER_ASSERT(r, meta.has_origin); | |
| REPORTER_ASSERT(r, meta.origin == static_cast<uint32_t>(orient)); | |
| } | |
| } | |
| DEF_TEST(RustExif_resolution_parsing, r) { | |
| constexpr float kEpsilon = 0.0001f; | |
| auto blob = make_resolution_exif_be(72.0f, 72.0f, 2 /*inch*/); | |
| rust_exif::ExifMetadata meta; | |
| bool ok = rust_exif::parse_exif( | |
| rust::Slice<const uint8_t>(blob.data(), blob.size()), meta); | |
| REPORTER_ASSERT(r, ok); | |
| REPORTER_ASSERT(r, meta.has_resolution_unit); | |
| REPORTER_ASSERT(r, meta.resolution_unit == 2); | |
| REPORTER_ASSERT(r, meta.has_x_resolution); | |
| REPORTER_ASSERT(r, std::abs(meta.x_resolution - 72.0f) < kEpsilon); | |
| REPORTER_ASSERT(r, meta.has_y_resolution); | |
| REPORTER_ASSERT(r, std::abs(meta.y_resolution - 72.0f) < kEpsilon); | |
| // Verify ToSkExifMetadata conversion. | |
| SkExif::Metadata sk_meta; | |
| rust_exif::ToSkExifMetadata(meta, &sk_meta); | |
| REPORTER_ASSERT(r, sk_meta.fResolutionUnit.has_value()); | |
| REPORTER_ASSERT(r, sk_meta.fResolutionUnit.value() == 2); | |
| REPORTER_ASSERT(r, sk_meta.fXResolution.has_value()); | |
| REPORTER_ASSERT(r, std::abs(sk_meta.fXResolution.value() - 72.0f) < kEpsilon); | |
| REPORTER_ASSERT(r, sk_meta.fYResolution.has_value()); | |
| REPORTER_ASSERT(r, std::abs(sk_meta.fYResolution.value() - 72.0f) < kEpsilon); | |
| } | |
| // Builds a single-IFD EXIF blob with PixelXDimension and PixelYDimension tags. | |
| static std::vector<uint8_t> make_pixel_dimensions_exif_be(uint16_t width, uint16_t height) { | |
| // Header=8, count=2, entries=2×12=24, next_ifd=4 → 38 bytes. | |
| std::vector<uint8_t> buf(38, 0); | |
| auto hdr = tiff_header_be(8); | |
| std::copy(hdr.begin(), hdr.end(), buf.begin()); | |
| write_u16_be(buf, 8, 2); // 2 entries | |
| // Entry 0: PixelXDimension (tag 0xa002, SHORT, 1, width) | |
| write_u16_be(buf, 10, 0xa002); | |
| write_u16_be(buf, 12, 3); // SHORT | |
| write_u32_be(buf, 14, 1); | |
| write_u16_be(buf, 18, width); | |
| // Entry 1: PixelYDimension (tag 0xa003, SHORT, 1, height) | |
| write_u16_be(buf, 22, 0xa003); | |
| write_u16_be(buf, 24, 3); // SHORT | |
| write_u32_be(buf, 26, 1); | |
| write_u16_be(buf, 30, height); | |
| // next IFD = 0 | |
| write_u32_be(buf, 34, 0); | |
| return buf; | |
| } | |
| DEF_TEST(RustExif_pixel_dimensions_parsing, r) { | |
| auto blob = make_pixel_dimensions_exif_be(1920, 1080); | |
| rust_exif::ExifMetadata meta; | |
| bool ok = rust_exif::parse_exif( | |
| rust::Slice<const uint8_t>(blob.data(), blob.size()), meta); | |
| REPORTER_ASSERT(r, ok); | |
| REPORTER_ASSERT(r, meta.has_pixel_x_dimension); | |
| REPORTER_ASSERT(r, meta.pixel_x_dimension == 1920u); | |
| REPORTER_ASSERT(r, meta.has_pixel_y_dimension); | |
| REPORTER_ASSERT(r, meta.pixel_y_dimension == 1080u); | |
| SkExif::Metadata sk_meta; | |
| rust_exif::ToSkExifMetadata(meta, &sk_meta); | |
| REPORTER_ASSERT(r, sk_meta.fPixelXDimension.has_value()); | |
| REPORTER_ASSERT(r, sk_meta.fPixelXDimension.value() == 1920u); | |
| REPORTER_ASSERT(r, sk_meta.fPixelYDimension.has_value()); | |
| REPORTER_ASSERT(r, sk_meta.fPixelYDimension.value() == 1080u); | |
| } | |
| // Equivalence tests: compare Rust parser against SkExif::Parse on real files. | |
| static bool approx_eq_f(float a, float b, float eps) { | |
| return std::abs(a - b) < eps; | |
| } | |
| // Compares a Rust-parsed ExifMetadata (converted to SkExif::Metadata) against | |
| // the reference C++ SkExif::Parse result for the given resource EXIF file. | |
| static void check_equivalence(skiatest::Reporter* r, const char* resource_path) { | |
| sk_sp<SkData> data = GetResourceAsData(resource_path); | |
| if (!data) { | |
| INFOF(r, "Skipping equivalence test for %s: resource not available.", resource_path); | |
| return; | |
| } | |
| // C++ reference parse. | |
| SkExif::Metadata cpp_meta; | |
| SkExif::Parse(cpp_meta, data.get()); | |
| // Rust parse + conversion. | |
| rust_exif::ExifMetadata rust_raw; | |
| bool rust_ok = rust_exif::parse_exif( | |
| rust::Slice<const uint8_t>( | |
| reinterpret_cast<const uint8_t*>(data->data()), data->size()), | |
| rust_raw); | |
| SkExif::Metadata rust_meta; | |
| if (rust_ok) { | |
| rust_exif::ToSkExifMetadata(rust_raw, &rust_meta); | |
| } | |
| constexpr float kEpsilon = 0.0001f; | |
| // fOrigin | |
| REPORTER_ASSERT(r, | |
| cpp_meta.fOrigin.has_value() == rust_meta.fOrigin.has_value(), | |
| "[%s] fOrigin presence mismatch", resource_path); | |
| if (cpp_meta.fOrigin.has_value() && rust_meta.fOrigin.has_value()) { | |
| REPORTER_ASSERT(r, | |
| cpp_meta.fOrigin.value() == rust_meta.fOrigin.value(), | |
| "[%s] fOrigin value mismatch: cpp=%d rust=%d", | |
| resource_path, | |
| static_cast<int>(cpp_meta.fOrigin.value()), | |
| static_cast<int>(rust_meta.fOrigin.value())); | |
| } | |
| // fHdrHeadroom | |
| REPORTER_ASSERT(r, | |
| cpp_meta.fHdrHeadroom.has_value() == rust_meta.fHdrHeadroom.has_value(), | |
| "[%s] fHdrHeadroom presence mismatch", resource_path); | |
| if (cpp_meta.fHdrHeadroom.has_value() && rust_meta.fHdrHeadroom.has_value()) { | |
| REPORTER_ASSERT(r, | |
| approx_eq_f(cpp_meta.fHdrHeadroom.value(), | |
| rust_meta.fHdrHeadroom.value(), | |
| kEpsilon), | |
| "[%s] fHdrHeadroom value mismatch: cpp=%f rust=%f", | |
| resource_path, | |
| cpp_meta.fHdrHeadroom.value(), | |
| rust_meta.fHdrHeadroom.value()); | |
| } | |
| // fResolutionUnit | |
| REPORTER_ASSERT(r, | |
| cpp_meta.fResolutionUnit.has_value() == rust_meta.fResolutionUnit.has_value(), | |
| "[%s] fResolutionUnit presence mismatch", resource_path); | |
| if (cpp_meta.fResolutionUnit.has_value() && rust_meta.fResolutionUnit.has_value()) { | |
| REPORTER_ASSERT(r, | |
| cpp_meta.fResolutionUnit.value() == rust_meta.fResolutionUnit.value(), | |
| "[%s] fResolutionUnit value mismatch: cpp=%d rust=%d", | |
| resource_path, | |
| cpp_meta.fResolutionUnit.value(), | |
| rust_meta.fResolutionUnit.value()); | |
| } | |
| // fXResolution | |
| REPORTER_ASSERT(r, | |
| cpp_meta.fXResolution.has_value() == rust_meta.fXResolution.has_value(), | |
| "[%s] fXResolution presence mismatch", resource_path); | |
| if (cpp_meta.fXResolution.has_value() && rust_meta.fXResolution.has_value()) { | |
| REPORTER_ASSERT(r, | |
| approx_eq_f(cpp_meta.fXResolution.value(), | |
| rust_meta.fXResolution.value(), | |
| kEpsilon), | |
| "[%s] fXResolution value mismatch: cpp=%f rust=%f", | |
| resource_path, | |
| cpp_meta.fXResolution.value(), | |
| rust_meta.fXResolution.value()); | |
| } | |
| // fYResolution | |
| REPORTER_ASSERT(r, | |
| cpp_meta.fYResolution.has_value() == rust_meta.fYResolution.has_value(), | |
| "[%s] fYResolution presence mismatch", resource_path); | |
| if (cpp_meta.fYResolution.has_value() && rust_meta.fYResolution.has_value()) { | |
| REPORTER_ASSERT(r, | |
| approx_eq_f(cpp_meta.fYResolution.value(), | |
| rust_meta.fYResolution.value(), | |
| kEpsilon), | |
| "[%s] fYResolution value mismatch: cpp=%f rust=%f", | |
| resource_path, | |
| cpp_meta.fYResolution.value(), | |
| rust_meta.fYResolution.value()); | |
| } | |
| // fPixelXDimension | |
| REPORTER_ASSERT(r, | |
| cpp_meta.fPixelXDimension.has_value() == | |
| rust_meta.fPixelXDimension.has_value(), | |
| "[%s] fPixelXDimension presence mismatch", resource_path); | |
| if (cpp_meta.fPixelXDimension.has_value() && rust_meta.fPixelXDimension.has_value()) { | |
| REPORTER_ASSERT(r, | |
| cpp_meta.fPixelXDimension.value() == | |
| rust_meta.fPixelXDimension.value(), | |
| "[%s] fPixelXDimension value mismatch: cpp=%u rust=%u", | |
| resource_path, | |
| cpp_meta.fPixelXDimension.value(), | |
| rust_meta.fPixelXDimension.value()); | |
| } | |
| // fPixelYDimension | |
| REPORTER_ASSERT(r, | |
| cpp_meta.fPixelYDimension.has_value() == | |
| rust_meta.fPixelYDimension.has_value(), | |
| "[%s] fPixelYDimension presence mismatch", resource_path); | |
| if (cpp_meta.fPixelYDimension.has_value() && rust_meta.fPixelYDimension.has_value()) { | |
| REPORTER_ASSERT(r, | |
| cpp_meta.fPixelYDimension.value() == | |
| rust_meta.fPixelYDimension.value(), | |
| "[%s] fPixelYDimension value mismatch: cpp=%u rust=%u", | |
| resource_path, | |
| cpp_meta.fPixelYDimension.value(), | |
| rust_meta.fPixelYDimension.value()); | |
| } | |
| } | |
| DEF_TEST(RustExif_equivalence_with_resource_files, r) { | |
| // Test against the same EXIF files used in ExifTest.cpp. | |
| check_equivalence(r, "images/test0-hdr.exif"); | |
| check_equivalence(r, "images/test1-pixel32.exif"); | |
| check_equivalence(r, "images/test2-nonuniform.exif"); | |
| check_equivalence(r, "images/test3-little-endian.exif"); | |
| } |