little_exif-0.6.23/.cargo_vcs_info.json0000644000000001360000000000100134340ustar { "git": { "sha1": "2d6d73acea033630a1cf0056d23b9da81e0638c8" }, "path_in_vcs": "" }little_exif-0.6.23/.github/workflows/rust.yml000064400000000000000000000010441046102023000173400ustar 00000000000000name: Build & Test on: push: branches: [ "main" ] pull_request: branches: [ "main" ] env: CARGO_TERM_COLOR: always jobs: build_and_test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Output rust version for educational purposes run: rustup --version - name: Download resources run: cd heif_conformance_tests && chmod +x download.sh && ./download.sh && cd .. - name: Build run: cargo build --verbose - name: Run tests run: cargo test --workspace --verbose little_exif-0.6.23/Cargo.lock0000644000000054150000000000100114140ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "alloc-no-stdlib" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" dependencies = [ "alloc-no-stdlib", ] [[package]] name = "brotli" version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", "brotli-decompressor", ] [[package]] name = "brotli-decompressor" version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] [[package]] name = "crc" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ "crc-catalog", ] [[package]] name = "crc-catalog" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "little_exif" version = "0.6.23" dependencies = [ "brotli", "crc", "log", "miniz_oxide", "paste", "quick-xml", ] [[package]] name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "quick-xml" version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ "memchr", ] little_exif-0.6.23/Cargo.toml0000644000000042640000000000100114400ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.65" name = "little_exif" version = "0.6.23" authors = ["Tobias Prisching"] build = false exclude = [ ".cargo_vcs_info.json", ".DS_Store", "**/.DS_Store", ".gitignore", ".vscode/*", "additional-documentation/*", "Cargo.lock", "debug/*", "fuzz/*", "examples/*", "issue_tests/*", "heif_conformance_tests/*", "target/*", "tests/*", "vendor/*", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = """ The only pure Rust crate with true read *and* write support for EXIF data, available for PNG, JP(E)G, HEIF/HEIC/HIF/AVIF, JXL, TIFF, WebP - and soon even more! """ readme = "README.md" keywords = [ "metadata", "exif", "photo", "image", ] categories = ["multimedia::images"] license = "MIT OR Apache-2.0" repository = "https://github.com/TechnikTobi/little_exif" resolver = "3" [lib] name = "little_exif" path = "src/lib.rs" [dependencies.brotli] version = "8.0.1" [dependencies.crc] version = "3.3.0" [dependencies.log] version = "0.4" [dependencies.miniz_oxide] version = "0.8.8" [dependencies.paste] version = "1.0.15" [dependencies.quick-xml] version = "0.37.5" [lints.clippy] collapsible_else_if = "allow" doc_lazy_continuation = "allow" empty_line_after_doc_comments = "allow" from_over_into = "allow" identity_op = "allow" match_same_arms = "allow" modulo_one = "allow" needless_late_init = "allow" needless_range_loop = "allow" new_without_default = "allow" same_item_push = "allow" todo = "warn" unreachable = "warn" unused_io_amount = "allow" unwrap_used = "warn" upper_case_acronyms = "allow" [profile.release] debug = 2 debug-assertions = true overflow-checks = true little_exif-0.6.23/Cargo.toml.orig000064400000000000000000000040131046102023000151110ustar 00000000000000[package] name = "little_exif" version = "0.6.23" edition = "2021" rust-version = "1.65" description = """ The only pure Rust crate with true read *and* write support for EXIF data, available for PNG, JP(E)G, HEIF/HEIC/HIF/AVIF, JXL, TIFF, WebP - and soon even more! """ authors = ["Tobias Prisching"] license = "MIT OR Apache-2.0" repository = "https://github.com/TechnikTobi/little_exif" readme = "README.md" keywords = ["metadata", "exif", "photo", "image"] categories = ["multimedia::images"] exclude = [ ".cargo_vcs_info.json", ".DS_Store", "**/.DS_Store", ".gitignore", ".vscode/*", "additional-documentation/*", "Cargo.lock", "debug/*", "fuzz/*", "examples/*", "issue_tests/*", "heif_conformance_tests/*", "target/*", "tests/*", "vendor/*", ] [dependencies] crc = "3.3.0" paste = "1.0.15" miniz_oxide = "0.8.8" log = "0.4" quick-xml = "0.37.5" brotli = "8.0.1" [[test]] name = "tests" path = "tests/main.rs" # Used for fuzzer - fast iterations, with optimizations and debug info [profile.release] overflow-checks = true debug = true debug-assertions = true [workspace] members = [".", "issue_tests", "heif_conformance_tests", "fuzz"] resolver = "3" [lints.clippy] modulo_one = "allow" # used in macros from_over_into = "allow" identity_op = "allow" upper_case_acronyms = "allow" # IDAT etc. are uppercase by convention collapsible_else_if = "allow" new_without_default = "allow" doc_lazy_continuation = "allow" same_item_push = "allow" needless_late_init = "allow" empty_line_after_doc_comments = "allow" unused_io_amount = "allow" # TODO needless_range_loop = "allow" # TODO match_same_arms = "allow" # TODO # Clippy non-default rules unwrap_used = "warn" todo = "warn" unreachable = "warn" # Nice to have warnings #match_wildcard_for_single_variants = "warn" #unnecessary_wraps = "warn" # this can be fixed after fixing unwrap results #expect_used = "warn" # This is should will require to change public API #panic = "warn" # Needs some API changes #wildcard_imports = "warn" little_exif-0.6.23/LICENSE-APACHE000064400000000000000000000261221046102023000141530ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2022 Tobias Prisching Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. little_exif-0.6.23/LICENSE-MIT000064400000000000000000000020611046102023000136570ustar 00000000000000MIT License Copyright (c) 2022 Tobias Prisching Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. little_exif-0.6.23/README.md000064400000000000000000000077121046102023000135120ustar 00000000000000# little\_exif A little library for reading and writing EXIF data in pure Rust. [![Build & Test](https://github.com/TechnikTobi/little_exif/actions/workflows/rust.yml/badge.svg)](https://github.com/TechnikTobi/little_exif/actions/workflows/rust.yml)  [![version-badge][]][version]  [![license-badge][]][license]  [version-badge]: https://img.shields.io/crates/v/little_exif.svg [version]: https://crates.io/crates/little_exif [license-badge]: https://img.shields.io/crates/l/little_exif.svg [license]: https://github.com/TechnikTobi/little_exif#license ## Supported Formats - JPEG / JPG - JXL - HEIF / HEIC / HIF / AVIF - PNG - TIFF - WebP (only lossless and extended) Your required format is not listed here or you've run into a problem with a file that should be supported? Open up a new issue (ideally with an example image for reproduction in case of a problem) and I'll take a look! ## Example If the image is stored in a file, located at some given path: ```rust use little_exif::metadata::Metadata; use little_exif::exif_tag::ExifTag; let image_path = std::path::Path::new("image.png"); let mut metadata = Metadata::new_from_path(&image_path); metadata.set_tag( ExifTag::ImageDescription("Hello World!".to_string()) ); metadata.write_to_file(&image_path)?; ``` Alternatively, if the image is stored in a ```Vec``` variable: ```rust use little_exif::metadata::Metadata; use little_exif::exif_tag::ExifTag; use little_exif::filetype::FileExtension; let file_type = FileExtension::JPEG; let mut metadata = Metadata::new_from_vec(&image_vector, file_type); metadata.set_tag( ExifTag::ImageDescription("Hello World!".to_string()) ); metadata.write_to_vec(&mut image_vector, file_type)?; ``` ## Testing To run the tests from a specific file, use e.g. ```bash cargo test --test issue_000002 ``` To run a single test from that file use ```bash cargo test --test issue_000002 read_and_write_exif_data_1 ``` ## Logging This library uses the [`log`](https://crates.io/crates/log) crate for various levels of logging. ### Setup To enable this logging, you will need to add & initialize a logger implementation, such as [`env_logger`](https://docs.rs/env_logger/latest/env_logger/), but there are other loggers available, see the list on [available logging implementations](https://docs.rs/log/latest/log/#available-logging-implementations) in the [`log`](https://docs.rs/log/latest/log/) crate. Add the implementation to your crate: ```bash cargo add env_logger ``` Initialize the logger in your application: ```rust fn main() { env_logger::init(); // your little_exif code ... } ``` ### Usage In `env_logger`, you can view `little_exif`'s debug-level logs, for example, by setting the `RUST_LOG` env var: ```bash env RUST_LOG=debug cargo run ``` For other log levels, see [`env_logger`'s documentation](https://docs.rs/env_logger/latest/env_logger/) or the documentation of the logger of your choice. ## FAQ ### I tried writing the ImageDescription tag on a JPEG file, but it does not show up. Why? This could be due to the such called APP12 or APP13 segment stored in the JPEG, likely caused by editing the file using e.g. Photoshop. These segments may store data that image viewers also interpret as an ImageDescription, overriding the EXIF tag. Right now, little_exif can't edit these segments. As a workaround, the functions ```clear_app12_segment``` and ```clear_app13_segment``` can remove these areas from the JPEG: ```rust // File in a Vec Metadata::clear_app12_segment(&mut file_content, file_extension)?; Metadata::clear_app13_segment(&mut file_content, file_extension)?; // File at a given path Metadata::file_clear_app12_segment(&given_path)?; Metadata::file_clear_app13_segment(&given_path)?; ``` ## License Licensed under either - Apache License, Version 2.0 (See [LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) or - MIT License (See [LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. little_exif-0.6.23/clippy.toml000064400000000000000000000001571046102023000144240ustar 00000000000000msrv = "1.65.0" allow-indexing-slicing-in-tests = true allow-unwrap-in-tests = true allow-panic-in-tests = truelittle_exif-0.6.23/src/endian.rs000064400000000000000000000007311046102023000146200ustar 00000000000000// Copyright © 2024 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details #[derive(Clone, Debug, PartialEq)] pub enum Endian { Big, Little } impl Endian { pub(crate) fn header ( &self ) -> [u8; 8] { match *self { Endian::Little => [0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00], Endian::Big => [0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08], } } }little_exif-0.6.23/src/exif_tag/decode.rs000064400000000000000000000136241046102023000164000ustar 00000000000000// Copyright © 2024-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use crate::endian::Endian; use crate::exif_tag_format::RATIONAL64U; use crate::rational::*; use crate::general_file_io::io_error; use crate::ifd::ExifTagGroup; use crate::io_error_plain; use super::ExifTag; use super::ExifTagFormat; use super::U8conversion; use super::INT8U; use super::INT16U; use super::INT32U; #[allow(non_snake_case)] pub(crate) fn decode_tag_with_format_exceptions ( raw_tag: &ExifTag, format: ExifTagFormat, raw_data: &Vec, endian: &Endian, hex_tag: u16, group: &ExifTagGroup ) -> Result { if raw_tag.format().as_u16() != format.as_u16() { // The expected format and the given format in the file // do *not* match. Check special cases (e.g. INT16U -> INT32U) // If no special cases match, return an error match (raw_tag.format(), format) { // Expected for tag VS Decoded from data (ExifTagFormat::INT32U, ExifTagFormat::INT16U) => { let int16u_data = >::from_u8_vec(raw_data, endian); let int32u_data = int16u_data.into_iter().map(|x| x as u32).collect::>(); return raw_tag.set_value_to_int32u_vec(int32u_data).map_err( |e| io_error_plain!(Other, format!("Could not decode tag {:?}: {}", raw_tag, e)) ); }, (ExifTagFormat::INT32U, ExifTagFormat::INT8U) => { let int8u_data = >::from_u8_vec(raw_data, endian); let int32u_data = int8u_data.into_iter().map(|x| x as u32).collect::>(); return raw_tag.set_value_to_int32u_vec(int32u_data).map_err( |e| io_error_plain!(Other, format!("Could not decode tag {:?}: {}", raw_tag, e)) ); }, (ExifTagFormat::INT16U, ExifTagFormat::INT32U) => { // Not sure how to be more cautious in this case... let int32u_data = >::from_u8_vec(raw_data, endian); let int16u_data = int32u_data.into_iter().map(|x| x as u16).collect::>(); return raw_tag.set_value_to_int16u_vec(int16u_data).map_err( |e| io_error_plain!(Other, format!("Could not decode tag {:?}: {}", raw_tag, e)) ); }, (ExifTagFormat::INT16U, ExifTagFormat::INT8U) => { let int8u_data = >::from_u8_vec(raw_data, endian); let int16u_data = int8u_data.into_iter().map(|x| x as u16).collect::>(); return raw_tag.set_value_to_int16u_vec(int16u_data).map_err( |e| io_error_plain!(Other, format!("Could not decode tag {:?}: {}", raw_tag, e)) ); }, // See issue #74 (ExifTagFormat::INT8U, ExifTagFormat::INT16U) => { let int16u_data = >::from_u8_vec(raw_data, endian); let int8u_data = int16u_data.clone().into_iter().map(|x| x as u8).collect::>(); for (element_u16, element_u8) in int16u_data.iter().zip(int8u_data.iter()) { // Assert that the int16u data is within int8u range assert_eq!(*element_u16, *element_u8 as u16); } return raw_tag.set_value_to_int8u_vec(int8u_data).map_err( |e| io_error_plain!(Other, format!("Could not decode tag {:?}: {}", raw_tag, e)) ); }, (ExifTagFormat::INT8U, ExifTagFormat::STRING) => { if raw_tag.as_u16() == 0x0005 && // GPSAltitudeRef raw_tag.get_group() == ExifTagGroup::GPS { // The GPSAltitudeRef tag is a strange case. It is the only // GPS -Ref tag that is a INT8U, all others are STRINGs // with a length of two. // Some images store this as a string nevertheless. // So, we try to convert the string by taking its first // character. If it is 0x00 or 0x30 ("0") we set it to 0, // if it is 0x01 or 0x31 ("1") we set it to 1, and // otherwise we panic and tell the user to open a ticket. let first_char = raw_data[0]; let int8u_data = match first_char { 0x00 | 0x30 => vec![0u8], 0x01 | 0x31 => vec![1u8], _ => io_error!(InvalidData, "Problem while decoding GPSAltitudeRef. Please open a new issue for little_exif!")? }; return ExifTag::from_u16_with_data( 0x0005, &ExifTagFormat::INT8U, &int8u_data, endian, group ).map_err( |e| io_error_plain!(Other, format!("Could not decode tag {:?}: {}", raw_tag, e)) ); } else { return io_error!(Other, format!("Unknown tag for combination INT8U vs STRING while decoding: {:?}", raw_tag)); } }, // See issue #21 (ExifTagFormat::RATIONAL64S, ExifTagFormat::RATIONAL64U) => { let uR64_data = >::from_u8_vec(raw_data, endian); let iR64_data = uR64_data .into_iter().map(|x| x.into()).collect::>() .into_iter().map(|x| x.into()).collect::>(); return raw_tag.set_value_to_iR64_vec(iR64_data).map_err( |e| io_error_plain!(Other, format!("Could not decode tag {:?}: {}", raw_tag, e)) ); } // See issue #63 (ExifTagFormat::UNDEF, ExifTagFormat::STRING) => { if raw_tag.as_u16() == 0x001b && // GPSProcessingMethod raw_tag.get_group() == ExifTagGroup::GPS { return raw_tag.set_value_to_undef(raw_data.clone()).map_err( |e| io_error_plain!(Other, format!("Could not decode tag {:?}: {}", raw_tag, e)) ); } else { return io_error!(Other, format!("Unknown tag for combination UNDEF vs STRING while decoding: {:?}", raw_tag)); } } _ => { return io_error!(Other, format!("Illegal format for known tag! Tag: {:?} Expected: {:?} Got: {:?}", raw_tag, raw_tag.format(), format)); }, }; } else { // Format is as expected; set the data by replacing the tag return ExifTag::from_u16_with_data( hex_tag, &format, raw_data, endian, group ).map_err( |e| io_error_plain!(Other, format!("Could not decode tag {:?}: {}", raw_tag, e)) ); } }little_exif-0.6.23/src/exif_tag/mod.rs000064400000000000000000001034021046102023000157260ustar 00000000000000// Copyright © 2024 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details pub(crate) mod decode; pub(super) mod set_value_to; use paste::paste; use crate::endian::Endian; use crate::u8conversion::*; use crate::exif_tag_format::*; use crate::ifd::ExifTagGroup; #[allow(non_camel_case_types)] #[derive(PartialEq)] pub enum TagType { VALUE, IFD_OFFSET(ExifTagGroup), DATA_OFFSET(Vec) } macro_rules! build_tag_enum { ( $( ( $tag:ident, $hex_value:expr, $format_enum:ident, $component_number:expr, $writable:expr, $group:ident ) ),* ) => { /// These are the currently supported tags by little_exif. /// Note that for tags that are unknown at the moment a fallback /// solution is provided using the `Unknown...` variants. #[derive(PartialEq, Debug, Clone)] pub enum ExifTag { $( $tag(paste!{[<$format_enum>]}), )* StripOffsets( Vec::, Vec::>), StripByteCounts( Vec::, ), ThumbnailOffset( Vec::, Vec::), ThumbnailLength( Vec:: ), UnknownINT8U( INT8U, u16, ExifTagGroup), UnknownSTRING( STRING, u16, ExifTagGroup), UnknownINT16U( INT16U, u16, ExifTagGroup), UnknownINT32U( INT32U, u16, ExifTagGroup), UnknownRATIONAL64U( RATIONAL64U, u16, ExifTagGroup), UnknownINT8S( INT8S, u16, ExifTagGroup), UnknownUNDEF( UNDEF, u16, ExifTagGroup), UnknownINT16S( INT16S, u16, ExifTagGroup), UnknownINT32S( INT32S, u16, ExifTagGroup), UnknownRATIONAL64S( RATIONAL64S, u16, ExifTagGroup), UnknownFLOAT( FLOAT, u16, ExifTagGroup), UnknownDOUBLE( DOUBLE, u16, ExifTagGroup), } impl ExifTag { /// Gets the hex value of an EXIF tag pub fn as_u16 ( &self ) -> u16 { match *self { $( ExifTag::$tag(_) => $hex_value, )* ExifTag::StripOffsets( _, _, ) => 0x0111, ExifTag::StripByteCounts( _, ) => 0x0117, ExifTag::ThumbnailOffset( _, _, ) => 0x0201, ExifTag::ThumbnailLength( _, ) => 0x0202, ExifTag::UnknownINT8U( _, tag, _) => tag, ExifTag::UnknownSTRING( _, tag, _) => tag, ExifTag::UnknownINT16U( _, tag, _) => tag, ExifTag::UnknownINT32U( _, tag, _) => tag, ExifTag::UnknownRATIONAL64U( _, tag, _) => tag, ExifTag::UnknownINT8S( _, tag, _) => tag, ExifTag::UnknownUNDEF( _, tag, _) => tag, ExifTag::UnknownINT16S( _, tag, _) => tag, ExifTag::UnknownINT32S( _, tag, _) => tag, ExifTag::UnknownRATIONAL64S( _, tag, _) => tag, ExifTag::UnknownFLOAT( _, tag, _) => tag, ExifTag::UnknownDOUBLE( _, tag, _) => tag, } } /// Gets the tag for a given hex value. /// The tag is initialized with new, empty data. /// If the hex value is unknown, an error is returned. /// /// # Examples /// ```no_run /// use little_exif::exif_tag::ExifTag; /// use little_exif::ifd::ExifTagGroup; /// /// let tag = ExifTag::from_u16(0x010e, &ExifTagGroup::GENERIC).unwrap(); /// ``` pub fn from_u16 ( hex_value: u16, group: &ExifTagGroup ) -> Result { match (hex_value, group) { $( ($hex_value, ExifTagGroup::$group) => Ok(ExifTag::$tag(]}>::new())), )* (0x0111, _) => Ok(ExifTag::StripOffsets( Vec::new(), Vec::new())), (0x0117, _) => Ok(ExifTag::StripByteCounts(Vec::new(), )), (0x0201, _) => Ok(ExifTag::ThumbnailOffset(Vec::new(), Vec::new())), (0x0202, _) => Ok(ExifTag::ThumbnailLength(Vec::new(), )), _ => Err(String::from("Invalid hex value for EXIF tag - Use 'Unknown...' instead")), } } /// Gets the tag for a given hex value. /// The tag is initialized using the given raw data by converting it /// to the appropriate format. /// If the hex value is unknown, the other parameters are used to /// generate an appropriate unknown tag for the specified format. /// /// # Examples /// ```no_run /// use little_exif::endian::Endian; /// use little_exif::exif_tag::ExifTag; /// use little_exif::ifd::ExifTagGroup; /// use little_exif::exif_tag_format::ExifTagFormat; /// /// let tag = ExifTag::from_u16_with_data( /// 0x0113, // An unknown tag hex value /// &ExifTagFormat::INT8U, /// &vec![1u8], /// &Endian::Little, /// &ExifTagGroup::GENERIC /// ); /// ``` pub fn from_u16_with_data ( hex_value: u16, format: &ExifTagFormat, raw_data: &Vec, endian: &Endian, group: &ExifTagGroup, ) -> Result { match (hex_value, group) { $( ($hex_value, ExifTagGroup::$group) => Ok(ExifTag::$tag( ]} as U8conversion]}>>::from_u8_vec(raw_data, endian) )), )* (0x0111, _) => Ok(ExifTag::StripOffsets( >::from_u8_vec(&raw_data, endian), Vec::new())), (0x0117, _) => Ok(ExifTag::StripByteCounts(>::from_u8_vec(&raw_data, endian), )), (0x0201, _) => Ok(ExifTag::ThumbnailOffset(>::from_u8_vec(&raw_data, endian), Vec::new())), (0x0202, _) => Ok(ExifTag::ThumbnailLength(>::from_u8_vec(&raw_data, endian), )), _ => { // In this case, the given hex_value represents a tag that is unknown match *format { ExifTagFormat::INT8U => Ok(ExifTag::UnknownINT8U( >::from_u8_vec(raw_data, endian), hex_value, *group)), ExifTagFormat::STRING => Ok(ExifTag::UnknownSTRING( >::from_u8_vec(raw_data, endian), hex_value, *group)), ExifTagFormat::INT16U => Ok(ExifTag::UnknownINT16U( >::from_u8_vec(raw_data, endian), hex_value, *group)), ExifTagFormat::INT32U => Ok(ExifTag::UnknownINT32U( >::from_u8_vec(raw_data, endian), hex_value, *group)), ExifTagFormat::RATIONAL64U => Ok(ExifTag::UnknownRATIONAL64U(>::from_u8_vec(raw_data, endian), hex_value, *group)), ExifTagFormat::INT8S => Ok(ExifTag::UnknownINT8S( >::from_u8_vec(raw_data, endian), hex_value, *group)), ExifTagFormat::UNDEF => Ok(ExifTag::UnknownUNDEF( >::from_u8_vec(raw_data, endian), hex_value, *group)), ExifTagFormat::INT16S => Ok(ExifTag::UnknownINT16S( >::from_u8_vec(raw_data, endian), hex_value, *group)), ExifTagFormat::INT32S => Ok(ExifTag::UnknownINT32S( >::from_u8_vec(raw_data, endian), hex_value, *group)), ExifTagFormat::RATIONAL64S => Ok(ExifTag::UnknownRATIONAL64S(>::from_u8_vec(raw_data, endian), hex_value, *group)), ExifTagFormat::FLOAT => Ok(ExifTag::UnknownFLOAT( >::from_u8_vec(raw_data, endian), hex_value, *group)), ExifTagFormat::DOUBLE => Ok(ExifTag::UnknownDOUBLE( >::from_u8_vec(raw_data, endian), hex_value, *group)), } }, } } /// Gives information about whether the data stored in the tag can /// be written to file. /// Needed e.g. for Offset tags where the given value is useless /// and needs to be computed during the write process. /// /// # Examples /// ```no_run /// use little_exif::exif_tag::ExifTag; /// /// let writable = ExifTag::ImageDescription(String::new()); /// let not_writable = ExifTag::ExifOffset(vec![1u32]); /// /// assert_eq!(writable.is_writable(), true); /// assert_eq!(not_writable.is_writable(), false); /// ``` pub fn is_writable ( &self ) -> bool { match *self { $( ExifTag::$tag(_) => $writable, )* _ => true, } } /// Checks if the tag is known to little_exif or not /// Note that in the future the value returned by this function /// for a specific tag might change as the number of known tags /// gets increased pub fn is_unknown ( &self ) -> bool { match *self { ExifTag::UnknownINT8U( _, _, _) | ExifTag::UnknownSTRING( _, _, _) | ExifTag::UnknownINT16U( _, _, _) | ExifTag::UnknownINT32U( _, _, _) | ExifTag::UnknownRATIONAL64U( _, _, _) | ExifTag::UnknownINT8S( _, _, _) | ExifTag::UnknownUNDEF( _, _, _) | ExifTag::UnknownINT16S( _, _, _) | ExifTag::UnknownINT32S( _, _, _) | ExifTag::UnknownRATIONAL64S( _, _, _) | ExifTag::UnknownFLOAT( _, _, _) | ExifTag::UnknownDOUBLE( _, _, _) => true, _ => false } } /// Checks if the usage of an unknown tag for the tags hex value is /// justified or not pub fn unknown_is_justified ( &self ) -> bool { if self.is_unknown() { if let Ok(_) = Self::from_u16(self.as_u16(), &self.get_group()) { return false; } } return true; } /// Gets the group (i.e. IFD) the tag belongs to. /// Note that this is still somewhat problematic, as for some tags /// the value of this function is hard to determine. pub fn get_group ( &self ) -> ExifTagGroup { match *self { $( ExifTag::$tag(_) => ExifTagGroup::$group, )* ExifTag::StripOffsets( _, _ ) => ExifTagGroup::GENERIC, ExifTag::StripByteCounts( _, ) => ExifTagGroup::GENERIC, ExifTag::ThumbnailOffset( _, _ ) => ExifTagGroup::GENERIC, ExifTag::ThumbnailLength( _, ) => ExifTagGroup::GENERIC, ExifTag::UnknownINT8U( _, _, group) => group, ExifTag::UnknownSTRING( _, _, group) => group, ExifTag::UnknownINT16U( _, _, group) => group, ExifTag::UnknownINT32U( _, _, group) => group, ExifTag::UnknownRATIONAL64U( _, _, group) => group, ExifTag::UnknownINT8S( _, _, group) => group, ExifTag::UnknownUNDEF( _, _, group) => group, ExifTag::UnknownINT16S( _, _, group) => group, ExifTag::UnknownINT32S( _, _, group) => group, ExifTag::UnknownRATIONAL64S( _, _, group) => group, ExifTag::UnknownFLOAT( _, _, group) => group, ExifTag::UnknownDOUBLE( _, _, group) => group, } } /// Gets the format of the data for a tag (e.g. `STRING`, `INT8U`, ...) pub fn format ( &self ) -> ExifTagFormat { match *self { $( ExifTag::$tag(_) => ExifTagFormat::$format_enum, )* ExifTag::StripOffsets( _, _ ) => ExifTagFormat::INT32U, ExifTag::StripByteCounts( _, ) => ExifTagFormat::INT32U, ExifTag::ThumbnailOffset( _, _ ) => ExifTagFormat::INT32U, ExifTag::ThumbnailLength( _, ) => ExifTagFormat::INT32U, ExifTag::UnknownINT8U( _, _, _) => ExifTagFormat::INT8U, ExifTag::UnknownSTRING( _, _, _) => ExifTagFormat::STRING, ExifTag::UnknownINT16U( _, _, _) => ExifTagFormat::INT16U, ExifTag::UnknownINT32U( _, _, _) => ExifTagFormat::INT32U, ExifTag::UnknownRATIONAL64U( _, _, _) => ExifTagFormat::RATIONAL64U, ExifTag::UnknownINT8S( _, _, _) => ExifTagFormat::INT8S, ExifTag::UnknownUNDEF( _, _, _) => ExifTagFormat::UNDEF, ExifTag::UnknownINT16S( _, _, _) => ExifTagFormat::INT16S, ExifTag::UnknownINT32S( _, _, _) => ExifTagFormat::INT32S, ExifTag::UnknownRATIONAL64S( _, _, _) => ExifTagFormat::RATIONAL64S, ExifTag::UnknownFLOAT( _, _, _) => ExifTagFormat::FLOAT, ExifTag::UnknownDOUBLE( _, _, _) => ExifTagFormat::DOUBLE, } } /// Gets the number of components for the tag. This might be /// predefined by the specifications for some tags (e.g. /// `BitsPerSample` has to have 3 components of type `INT16U`), for /// other tags this depends on the actual data (e.g. most - but not /// all - `STRING` format type tags). /// Note that for `STRING` format type tags this includes the NUL /// terminator (which gets written automatically and should not be /// provided by the user). pub fn number_of_components ( &self ) -> u32 { match self { $( ExifTag::$tag(value) => { // First, handle strings as special cases if self.is_string() { return value.len() as u32 + 1; } // Next, prefer the length of the value vector in // case there already is data stored in the tag if value.len() > 0 { return value.len() as u32; } // Check if the value has a predefined number of components if $component_number.is_some() { return $component_number.unwrap() as u32; } // Otherwise, return 0 return 0; }, )* ExifTag::StripOffsets( _, value ) => value.len() as u32, ExifTag::StripByteCounts(value, ) => value.len() as u32, ExifTag::ThumbnailOffset( _, _ ) => 1, ExifTag::ThumbnailLength( _, ) => 1, ExifTag::UnknownINT8U( value, _, _) => value.len() as u32, ExifTag::UnknownSTRING( value, _, _) => value.len() as u32 + 1, ExifTag::UnknownINT16U( value, _, _) => value.len() as u32, ExifTag::UnknownINT32U( value, _, _) => value.len() as u32, ExifTag::UnknownRATIONAL64U( value, _, _) => value.len() as u32, ExifTag::UnknownINT8S( value, _, _) => value.len() as u32, ExifTag::UnknownUNDEF( value, _, _) => value.len() as u32, ExifTag::UnknownINT16S( value, _, _) => value.len() as u32, ExifTag::UnknownINT32S( value, _, _) => value.len() as u32, ExifTag::UnknownRATIONAL64S( value, _, _) => value.len() as u32, ExifTag::UnknownFLOAT( value, _, _) => value.len() as u32, ExifTag::UnknownDOUBLE( value, _, _) => value.len() as u32, } } /// Checks if the format type of the tag is `STRING`. /// Needed for generating the EXIF data to know whether to add a /// NUL terminator at the end pub fn is_string ( &self ) -> bool { match *self { $( ExifTag::$tag(_) => (ExifTagFormat::$format_enum == ExifTagFormat::STRING), )* ExifTag::UnknownSTRING(_, _, _) => true, _ => false, } } /// Gets the value stored in the tag as an u8 vector, using the /// given endianness for conversion. pub fn value_as_u8_vec ( &self, endian: &Endian ) -> Vec { match self { $( ExifTag::$tag(value) => value.to_u8_vec(endian), )* ExifTag::StripOffsets( _, _ ) => Vec::new(), ExifTag::StripByteCounts( byte_counts, ) => byte_counts.to_u8_vec(endian), ExifTag::ThumbnailOffset( _, _ ) => Vec::new(), ExifTag::ThumbnailLength( length_data ) => length_data.to_u8_vec(endian), ExifTag::UnknownINT8U( value, _, _) => value.to_u8_vec(endian), ExifTag::UnknownSTRING( value, _, _) => value.to_u8_vec(endian), ExifTag::UnknownINT16U( value, _, _) => value.to_u8_vec(endian), ExifTag::UnknownINT32U( value, _, _) => value.to_u8_vec(endian), ExifTag::UnknownRATIONAL64U( value, _, _) => value.to_u8_vec(endian), ExifTag::UnknownINT8S( value, _, _) => value.to_u8_vec(endian), ExifTag::UnknownUNDEF( value, _, _) => value.to_u8_vec(endian), ExifTag::UnknownINT16S( value, _, _) => value.to_u8_vec(endian), ExifTag::UnknownINT32S( value, _, _) => value.to_u8_vec(endian), ExifTag::UnknownRATIONAL64S( value, _, _) => value.to_u8_vec(endian), ExifTag::UnknownFLOAT( value, _, _) => value.to_u8_vec(endian), ExifTag::UnknownDOUBLE( value, _, _) => value.to_u8_vec(endian), } } } }; } // This is just a small subset of the available EXIF tags // Will be expanded in the future // // Note regarding non-writable tags: Apart from // - StripOffsets // - StripByteCounts // - Opto-ElectricConvFactor (OECF) // - SpatialFrequencyResponse // - DeviceSettingDescription // none of them are part of the EXIF 2.32 specification // (Source: https://exiftool.org/TagNames/EXIF.html ) build_tag_enum![ // Tag Tag ID Format Nr. Components Writable Group (GPSVersionID, 0x0000, INT8U, Some::(4), true, GPS), (GPSLatitudeRef, 0x0001, STRING, Some::(2), true, GPS), (GPSLatitude, 0x0002, RATIONAL64U, Some::(3), true, GPS), (GPSLongitudeRef, 0x0003, STRING, Some::(2), true, GPS), (GPSLongitude, 0x0004, RATIONAL64U, Some::(3), true, GPS), (GPSAltitudeRef, 0x0005, INT8U, Some::(1), true, GPS), (GPSAltitude, 0x0006, RATIONAL64U, Some::(1), true, GPS), (GPSTimeStamp, 0x0007, RATIONAL64U, Some::(3), true, GPS), (GPSSatellites, 0x0008, STRING, None::, true, GPS), (GPSStatus, 0x0009, STRING, Some::(2), true, GPS), (GPSMeasureMode, 0x000a, STRING, Some::(2), true, GPS), (GPSDOP, 0x000b, RATIONAL64U, Some::(1), true, GPS), (GPSSpeedRef, 0x000c, STRING, Some::(2), true, GPS), (GPSSpeed, 0x000d, RATIONAL64U, Some::(1), true, GPS), (GPSTrackRef, 0x000e, STRING, Some::(2), true, GPS), (GPSTrack, 0x000f, RATIONAL64U, Some::(1), true, GPS), (GPSImgDirectionRef, 0x0010, STRING, Some::(2), true, GPS), (GPSImgDirection, 0x0011, RATIONAL64U, Some::(1), true, GPS), (GPSMapDatum, 0x0012, STRING, None::, true, GPS), (GPSDestLatitudeRef, 0x0013, STRING, Some::(2), true, GPS), (GPSDestLatitude, 0x0014, RATIONAL64U, Some::(3), true, GPS), (GPSDestLongitudeRef, 0x0015, STRING, Some::(2), true, GPS), (GPSDestLongitude, 0x0016, RATIONAL64U, Some::(3), true, GPS), (GPSDestBearingRef, 0x0017, STRING, Some::(2), true, GPS), (GPSDestBearing, 0x0018, RATIONAL64U, Some::(1), true, GPS), (GPSDestDistanceRef, 0x0019, STRING, Some::(2), true, GPS), (GPSDestDistance, 0x001a, RATIONAL64U, Some::(1), true, GPS), (GPSProcessingMethod, 0x001b, UNDEF, None::, true, GPS), (GPSAreaInformation, 0x001c, UNDEF, None::, true, GPS), (GPSDateStamp, 0x001d, STRING, Some::(11), true, GPS), (GPSDifferential, 0x001e, INT16U, Some::(1), true, GPS), (GPSHPositioningError, 0x001f, RATIONAL64U, Some::(1), true, GPS), // Tag Tag ID Format Nr. Components Writable Group Required by bilevel grayscale palette-color full-color (InteroperabilityIndex, 0x0001, STRING, Some::(4), true, INTEROP), (InteroperabilityVersion, 0x0002, UNDEF, None::, true, INTEROP), (ImageWidth, 0x0100, INT32U, Some::(1), true, GENERIC), // Not EXIF but TIFF x x x x (ImageHeight, 0x0101, INT32U, Some::(1), true, GENERIC), // Not EXIF but TIFF x x x x (BitsPerSample, 0x0102, INT16U, Some::(3), true, GENERIC), // Not EXIF but TIFF x x x (Compression, 0x0103, INT16U, Some::(1), true, GENERIC), // Not EXIF but TIFF x x x x (PhotometricInterpretation, 0x0106, INT16U, Some::(1), true, GENERIC), // Not EXIF but TIFF x x x x (CellWidth, 0x0108, INT16U, Some::(1), true, GENERIC), // Not EXIF but TIFF (CellHeight, 0x0109, INT16U, Some::(1), true, GENERIC), // Not EXIF but TIFF (ImageDescription, 0x010e, STRING, None::, true, GENERIC), (Make, 0x010f, STRING, None::, true, GENERIC), (Model, 0x0110, STRING, None::, true, GENERIC), // (StripOffsets, 0x0111, INT32U, None::, false, GENERIC), // Not EXIF but TIFF x x x x (Orientation, 0x0112, INT16U, Some::(1), true, GENERIC), (SamplesPerPixel, 0x0115, INT16U, Some::(1), true, GENERIC), // Not EXIF but TIFF x (RowsPerStrip, 0x0116, INT32U, Some::(1), true, GENERIC), // Not EXIF but TIFF x x x x // (StripByteCounts, 0x0117, INT32U, None::, false, GENERIC), // Not EXIF but TIFF x x x x (XResolution, 0x011a, RATIONAL64U, Some::(1), true, GENERIC), // Not EXIF but TIFF x x x x (YResolution, 0x011b, RATIONAL64U, Some::(1), true, GENERIC), // Not EXIF but TIFF x x x x (PlanarConfiguration, 0x011c, INT16U, Some::(1), true, GENERIC), (ResolutionUnit, 0x0128, INT16U, Some::(1), true, GENERIC), // Not EXIF but TIFF x x x x (TransferFunction, 0x012d, INT16U, Some::(3), true, GENERIC), (Software, 0x0131, STRING, None::, true, GENERIC), (ModifyDate, 0x0132, STRING, Some::(20), true, GENERIC), (Artist, 0x013b, STRING, None::, true, GENERIC), // Not EXIF but TIFF (WhitePoint, 0x013e, RATIONAL64U, Some::(2), true, GENERIC), (PrimaryChromaticities, 0x013f, RATIONAL64U, Some::(6), true, GENERIC), (ColorMap, 0x0140, INT16U, None::, true, GENERIC), // Not EXIF but TIFF x // End of TIFF only tags (?) // (ThumbnailOffset, 0x0201, INT32U, Some::(1), true, GENERIC), // (ThumbnailLength, 0x0202, INT32U, Some::(1), true, GENERIC), (YCbCrCoefficients, 0x0211, RATIONAL64U, Some::(3), true, GENERIC), (YCbCrSubSampling, 0x0212, INT16U, Some::(2), true, GENERIC), (YCbCrPositioning, 0x0213, INT16U, Some::(1), true, GENERIC), (ReferenceBlackWhite, 0x0214, RATIONAL64U, Some::(6), true, GENERIC), (Copyright, 0x8298, STRING, None::, true, GENERIC), (ExposureTime, 0x829a, RATIONAL64U, Some::(1), true, EXIF), (FNumber, 0x829d, RATIONAL64U, Some::(1), true, EXIF), (ExifOffset, 0x8769, INT32U, Some::(1), false, GENERIC), (ExposureProgram, 0x8822, INT16U, Some::(1), true, EXIF), (SpectralSensitivity, 0x8824, STRING, None::, true, EXIF), (GPSInfo, 0x8825, INT32U, Some::(1), true, GENERIC), // -> GPS Tags: https://exiftool.org/TagNames/GPS.html (ISO, 0x8827, INT16U, None::, true, EXIF), (OECF, 0x8828, UNDEF, None::, false, EXIF), (SensitivityType, 0x8830, INT16U, Some::(1), true, EXIF), (StandardOutputSensitivity, 0x8831, INT32U, Some::(1), true, EXIF), (RecommendedExposureIndex, 0x8832, INT32U, Some::(1), true, EXIF), (ISOSpeed, 0x8833, INT32U, Some::(1), true, EXIF), (ISOSpeedLatitudeyyy, 0x8834, INT32U, Some::(1), true, EXIF), (ISOSpeedLatitudezzz, 0x8835, INT32U, Some::(1), true, EXIF), (ExifVersion, 0x9000, UNDEF, Some::(4), true, EXIF), // 4 ASCII chars but without NULL Terminator (DateTimeOriginal, 0x9003, STRING, Some::(20), true, EXIF), (CreateDate, 0x9004, STRING, Some::(20), true, EXIF), (OffsetTime, 0x9010, STRING, None::, true, EXIF), (OffsetTimeOriginal, 0x9011, STRING, None::, true, EXIF), (OffsetTimeDigitized, 0x9012, STRING, None::, true, EXIF), (ComponentsConfiguration, 0x9101, UNDEF, None::, true, EXIF), (CompressedBitsPerPixel, 0x9102, RATIONAL64U, Some::(1), true, EXIF), (ShutterSpeedValue, 0x9201, RATIONAL64S, Some::(1), true, EXIF), (ApertureValue, 0x9202, RATIONAL64U, Some::(1), true, EXIF), (BrightnessValue, 0x9203, RATIONAL64S, Some::(1), true, EXIF), (ExposureCompensation, 0x9204, RATIONAL64S, Some::(1), true, EXIF), (MaxApertureValue, 0x9205, RATIONAL64U, Some::(1), true, EXIF), (SubjectDistance, 0x9206, RATIONAL64U, Some::(1), true, EXIF), (MeteringMode, 0x9207, INT16U, Some::(1), true, EXIF), (LightSource, 0x9208, INT16U, Some::(1), true, EXIF), // -> EXIF LightSource Values: https://exiftool.org/TagNames/EXIF.html#LightSource (Flash, 0x9209, INT16U, Some::(1), true, EXIF), // -> EXIF Flash Values: https://exiftool.org/TagNames/EXIF.html#Flash (FocalLength, 0x920a, RATIONAL64U, Some::(1), true, EXIF), (SubjectArea, 0x9214, INT16U, Some::(4), true, EXIF), (MakerNote, 0x927c, UNDEF, None::, true, EXIF), (UserComment, 0x9286, UNDEF, None::, true, EXIF), // First 8 bytes describe the character code (e.g. "JIS" for Japanese characters) (SubSecTime, 0x9290, STRING, None::, true, EXIF), (SubSecTimeOriginal, 0x9291, STRING, None::, true, EXIF), (SubSecTimeDigitized, 0x9292, STRING, None::, true, EXIF), (AmbientTemperature, 0x9400, RATIONAL64S, Some::(1), true, EXIF), (Humidity, 0x9401, RATIONAL64U, Some::(1), true, EXIF), (Pressure, 0x9402, RATIONAL64U, Some::(1), true, EXIF), (WaterDepth, 0x9403, RATIONAL64S, Some::(1), true, EXIF), (Acceleration, 0x9404, RATIONAL64U, Some::(1), true, EXIF), (CameraElevationAngle, 0x9405, RATIONAL64S, Some::(1), true, EXIF), (FlashpixVersion, 0xa000, UNDEF, Some::(4), true, EXIF), (ColorSpace, 0xa001, INT16U, Some::(1), true, EXIF), (ExifImageWidth, 0xa002, INT32U, Some::(1), true, EXIF), (ExifImageHeight, 0xa003, INT32U, Some::(1), true, EXIF), (RelatedSoundFile, 0xa004, STRING, None::, true, EXIF), (InteropOffset, 0xa005, INT32U, Some::(1), true, EXIF), (FlashEnergy, 0xa20b, RATIONAL64U, Some::(1), true, EXIF), (SpatialFrequencyResponse, 0xa20c, INT16U, Some::(1), false, EXIF), (FocalPlaneXResolution, 0xa20e, RATIONAL64U, Some::(1), true, EXIF), (FocalPlaneYResolution, 0xa20f, RATIONAL64U, Some::(1), true, EXIF), (FocalPlaneResolutionUnit, 0xa210, INT16U, Some::(1), true, EXIF), (SubjectLocation, 0xa214, INT16U, Some::(1), true, EXIF), (ExposureIndex, 0xa215, RATIONAL64U, Some::(1), true, EXIF), (SensingMethod, 0xa217, INT16U, Some::(1), true, EXIF), (FileSource, 0xa300, UNDEF, None::, true, EXIF), (SceneType, 0xa301, UNDEF, None::, true, EXIF), (CFAPattern, 0xa302, UNDEF, None::, true, EXIF), (CustomRendered, 0xa401, INT16U, Some::(1), true, EXIF), (ExposureMode, 0xa402, INT16U, Some::(1), true, EXIF), (WhiteBalance, 0xa403, INT16U, Some::(1), true, EXIF), (DigitalZoomRatio, 0xa404, RATIONAL64U, Some::(1), true, EXIF), (FocalLengthIn35mmFormat, 0xa405, INT16U, Some::(1), true, EXIF), (SceneCaptureType, 0xa406, INT16U, Some::(1), true, EXIF), (GainControl, 0xa407, INT16U, Some::(1), true, EXIF), (Contrast, 0xa408, INT16U, Some::(1), true, EXIF), (Saturation, 0xa409, INT16U, Some::(1), true, EXIF), (Sharpness, 0xa40a, INT16U, Some::(1), true, EXIF), (DeviceSettingDescription, 0xa40b, UNDEF, None::, false, EXIF), (SubjectDistanceRange, 0xa40c, INT16U, Some::(1), true, EXIF), (ImageUniqueID, 0xa420, STRING, None::, true, EXIF), (OwnerName, 0xa430, STRING, None::, true, EXIF), (SerialNumber, 0xa431, STRING, None::, true, EXIF), (LensInfo, 0xa432, RATIONAL64U, Some::(4), true, EXIF), (LensMake, 0xa433, STRING, None::, true, EXIF), (LensModel, 0xa434, STRING, None::, true, EXIF), (LensSerialNumber, 0xa435, STRING, None::, true, EXIF), (CompositeImage, 0xa460, INT16U, Some::(1), true, EXIF), (CompositeImageCount, 0xa461, INT16U, Some::(2), true, EXIF), (CompositeImageExposureTimes, 0xa462, UNDEF, None::, true, EXIF), (Gamma, 0xa500, RATIONAL64U, Some::(1), true, EXIF) ]; impl ExifTag { /// Tells us what type of tag this is. The majority of tags is /// simply for storing values (either within the 4 bytes of an IFD /// entry or at some offset position). The other two types are /// - IFD Offsets: For representing the offset to a SubIFD (e.g. EXIF). /// Needed for generating the exif data for writing, as the value stored /// in the tag variables is useless because it needs to be computed /// during the writing process. /// - Data Offsets: They are somewhat similar to the case of value tags /// where the value is stored at an offset position. This offset position /// is either in the data pub fn get_tag_type ( &self ) -> TagType { match self { ExifTag::ExifOffset(_) => TagType::IFD_OFFSET(ExifTagGroup::EXIF), ExifTag::GPSInfo(_) => TagType::IFD_OFFSET(ExifTagGroup::GPS), ExifTag::InteropOffset(_) => TagType::IFD_OFFSET(ExifTagGroup::INTEROP), ExifTag::StripOffsets( offset_data, _) => TagType::DATA_OFFSET(offset_data.clone()), ExifTag::StripByteCounts(byte_counts, ) => TagType::DATA_OFFSET(byte_counts.clone()), ExifTag::ThumbnailOffset(offset_data, _) => TagType::DATA_OFFSET(offset_data.clone()), ExifTag::ThumbnailLength(length_data ) => TagType::DATA_OFFSET(length_data.clone()), _ => TagType::VALUE } } }little_exif-0.6.23/src/exif_tag/set_value_to.rs000064400000000000000000000030361046102023000176420ustar 00000000000000// Copyright © 2024 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use crate::endian::Endian; use crate::rational::iR64; use crate::u8conversion::*; use super::ExifTag; use super::ExifTagFormat; macro_rules! build_set_function { ( $( ( $function_name:ident, $rust_type:ty, $format_type:ident ) ),* ) => { impl ExifTag { /// This helps with handling tags that come in a different format /// than expected and, one converted, setting the data in the /// format that little_exif expects it to be. #[allow(non_snake_case)] pub(crate) fn $( $function_name )* ( &self, data: $($rust_type)* ) -> Result { match self.format() { $(ExifTagFormat::$format_type)* => { let endian = Endian::Little; let raw_data = data.to_u8_vec(&endian); return Self::from_u16_with_data( self.as_u16(), &$(ExifTagFormat::$format_type)*, &raw_data, &endian, &self.get_group(), ); } _ => Err(format!("Not a {:?} compatible tag!", $(ExifTagFormat::$format_type)*)) } } } } } build_set_function![(set_value_to_int8u_vec, Vec, INT8U)]; build_set_function![(set_value_to_int16u_vec, Vec, INT16U)]; build_set_function![(set_value_to_int32u_vec, Vec, INT32U)]; build_set_function![(set_value_to_iR64_vec, Vec, RATIONAL64S)]; build_set_function![(set_value_to_undef, Vec, UNDEF)];little_exif-0.6.23/src/exif_tag_format.rs000064400000000000000000000055341046102023000165260ustar 00000000000000// Copyright © 2024 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use crate::rational::*; pub type INT8U = Vec; pub type STRING = String; pub type INT16U = Vec; pub type INT32U = Vec; pub type RATIONAL64U = Vec; pub type INT8S = Vec; pub type UNDEF = Vec; // got no better idea for this atm pub type INT16S = Vec; pub type INT32S = Vec; pub type RATIONAL64S = Vec; pub type FLOAT = Vec; pub type DOUBLE = Vec; #[derive(Clone, Debug, PartialEq, Copy)] pub enum ExifTagFormat { INT8U, // unsigned byte int8u STRING, // ascii string string INT16U, // unsigned short int16u INT32U, // unsigned long int32u RATIONAL64U, // unsigned rational rational64u INT8S, // signed byte int8s UNDEF, // undefined undef INT16S, // signed short int16s INT32S, // signed long int32s RATIONAL64S, // signed rational rational64s FLOAT, // single float float DOUBLE // double float double } impl ExifTagFormat { pub fn as_u16 ( &self ) -> u16 { match *self { ExifTagFormat::INT8U => 0x0001, ExifTagFormat::STRING => 0x0002, ExifTagFormat::INT16U => 0x0003, ExifTagFormat::INT32U => 0x0004, ExifTagFormat::RATIONAL64U => 0x0005, ExifTagFormat::INT8S => 0x0006, ExifTagFormat::UNDEF => 0x0007, ExifTagFormat::INT16S => 0x0008, ExifTagFormat::INT32S => 0x0009, ExifTagFormat::RATIONAL64S => 0x000a, ExifTagFormat::FLOAT => 0x000b, ExifTagFormat::DOUBLE => 0x000c, } } pub fn from_u16 ( hex_code: u16 ) -> Option { match hex_code { 0x0001 => Some(ExifTagFormat::INT8U), 0x0002 => Some(ExifTagFormat::STRING), 0x0003 => Some(ExifTagFormat::INT16U), 0x0004 => Some(ExifTagFormat::INT32U), 0x0005 => Some(ExifTagFormat::RATIONAL64U), 0x0006 => Some(ExifTagFormat::INT8S), 0x0007 => Some(ExifTagFormat::UNDEF), 0x0008 => Some(ExifTagFormat::INT16S), 0x0009 => Some(ExifTagFormat::INT32S), 0x000a => Some(ExifTagFormat::RATIONAL64S), 0x000b => Some(ExifTagFormat::FLOAT), 0x000c => Some(ExifTagFormat::DOUBLE), _ => None, } } pub fn bytes_per_component ( &self ) -> u32 { match self.as_u16() { 0x0001 => 1, 0x0002 => 1, 0x0003 => 2, 0x0004 => 4, 0x0005 => 8, 0x0006 => 1, 0x0007 => 1, 0x0008 => 2, 0x0009 => 4, 0x000a => 8, 0x000b => 4, 0x000c => 8, _ => panic!("Invalid format value for ExifTagFormat!"), } } } little_exif-0.6.23/src/filetype.rs000064400000000000000000000121371046102023000152060ustar 00000000000000// Copyright © 2025 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io; use std::io::ErrorKind; use std::io::Read; use std::io::Seek; use std::path::Path; use std::str::FromStr; use crate::general_file_io::*; #[derive(Clone, Copy, Debug, PartialEq)] #[allow(non_snake_case, non_camel_case_types)] pub enum FileExtension { PNG {as_zTXt_chunk: bool}, JPEG, JXL, NAKED_JXL, // A JXL codestream without any data TIFF, WEBP, HEIF, } impl FileExtension { pub fn auto_detect ( cursor: &mut T ) -> Option { // Read first few bytes (32 bytes because I don't know any better) let mut buffer = [0; 32]; let Ok(n) = cursor.read(&mut buffer) else { return None; }; if n < 4 { return None; } match buffer { // PNG [0x89, 0x50, 0x4E, 0x47, ..] => { return Some(FileExtension::PNG { as_zTXt_chunk: true }); } // JP(E)G [0xFF, 0xD8, ..] => { return Some(FileExtension::JPEG); } // TIFF, little endian [0x49, 0x49, 0x2A, 0x00, ..] => { return Some(FileExtension::TIFF); } // TIFF, big endian [0x4D, 0x4D, 0x00, 0x2A, ..] => { return Some(FileExtension::TIFF); } // WebP [0x52, 0x49, 0x46, 0x46, _, _, _, _, 0x57, 0x45, 0x42, 0x50, ..] => { return Some(FileExtension::WEBP); } // A "naked" JXL codestream that can't hold metadata // See: https://www.loc.gov/preservation/digital/formats/fdd/fdd000538.shtml [0xFF, 0x0A, ..] => { return Some(FileExtension::NAKED_JXL); } // JXL (in ISO_BMFF container) // In this case, the JXL file starts with the JXL signature box // 4 bytes for length J X L space more stuff [0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A, ..] => { return Some(FileExtension::JXL); } // HEIC/HEIF/AVIF // length f t y p [_, _, _, _, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63, ..] // heic | [_, _, _, _, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x66, ..] // heif | [_, _, _, _, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66, ..] // avif => { return Some(FileExtension::HEIF) } // TODO: Other HEIF formats, e.g. ftypmif1, see also: // https://www.loc.gov/preservation/digital/formats/fdd/fdd000526.shtml _ => { return None; } }; } } impl FromStr for FileExtension { type Err = std::io::Error; fn from_str ( input: &str ) -> Result { match input.to_lowercase().as_str() { "heif" | "hif" | "heic" | "avif" => Ok(FileExtension::HEIF), "jpeg" | "jpg" => Ok(FileExtension::JPEG), "jxl" => Ok(FileExtension::JXL), "png" => Ok(FileExtension::PNG { as_zTXt_chunk: true}), "tiff" | "tif" => Ok(FileExtension::TIFF), "webp" => Ok(FileExtension::WEBP), _ => io_error!(Unsupported, format!("Unknown file type: {}", input)), } } } pub fn get_file_type ( path: &Path ) -> Result { if !path.try_exists()? { return io_error!(Other, "File does not exist!"); } let raw_file_type_str = path.extension() .ok_or(io::Error::new(ErrorKind::Other, "Can't get file extension!"))?; let file_type_str = raw_file_type_str.to_str() .ok_or(io::Error::new(ErrorKind::Other, "Can't convert extension!"))?; FileExtension::from_str(file_type_str.to_lowercase().as_str()).map_err(|e| { io::Error::new( ErrorKind::Unsupported, format!("Unsupported file type: {file_type_str} - {e}"), ) } ) } #[cfg(test)] mod tests { use super::*; #[test] fn str_parse() { let table = vec![ ("png", FileExtension::PNG { as_zTXt_chunk: true }), ("jpg", FileExtension::JPEG), ("jpeg", FileExtension::JPEG), ("jxl", FileExtension::JXL), ("tif", FileExtension::TIFF), ("tiff", FileExtension::TIFF), ("webp", FileExtension::WEBP), ]; for (input, expected) in table { let result = FileExtension::from_str(input); assert!(result.is_ok(), "Failed to parse '{}'", input); assert_eq!(result.unwrap(), expected, "Parsed value mismatch for '{}'", input); } } }little_exif-0.6.23/src/general_file_io.rs000064400000000000000000000027371046102023000164750ustar 00000000000000// Copyright © 2024 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details pub(crate) const NEWLINE: u8 = 0x0a; pub(crate) const SPACE: u8 = 0x20; pub(crate) const EXIF: [u8; 4] = [0x45, 0x78, 0x69, 0x66]; pub(crate) const EXIF_HEADER: [u8; 6] = [0x45, 0x78, 0x69, 0x66, 0x00, 0x00]; pub(crate) const LITTLE_ENDIAN_INFO: [u8; 4] = [0x49, 0x49, 0x2a, 0x00]; pub(crate) const BIG_ENDIAN_INFO: [u8; 4] = [0x4d, 0x4d, 0x00, 0x2a]; #[macro_export] macro_rules! io_error { ($kind:ident, $message:expr) => { Err(std::io::Error::new( std::io::ErrorKind::$kind, $message )) }; } #[macro_export] macro_rules! io_error_plain { ($kind:ident, $message:expr) => { std::io::Error::new( std::io::ErrorKind::$kind, $message ) }; } use std::fs::File; use std::fs::OpenOptions; use std::path::Path; pub(crate) fn open_read_file ( path: &Path ) -> Result { if !path.exists() { return io_error!(NotFound, "Can't open file - File does not exist!"); } OpenOptions::new() .read(true) .write(false) .open(path) } pub(crate) fn open_write_file ( path: &Path ) -> Result { if !path.exists() { return io_error!(NotFound, "Can't open file - File does not exist!"); } OpenOptions::new() .read(true) .write(true) .open(path) } pub(crate) use io_error;little_exif-0.6.23/src/heif/box_header.rs000064400000000000000000000151141046102023000163760ustar 00000000000000// Copyright © 2025-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::Read; use std::io::Seek; use crate::endian::Endian; use crate::io_error; use crate::u8conversion::U8conversion; use crate::u8conversion::to_u8_vec_macro; use crate::util::read_16_bytes; use crate::util::read_1_bytes; use crate::util::read_3_bytes; use crate::util::read_4_bytes; use crate::util::read_be_u32; use crate::util::read_be_u64; use super::box_type::BoxType; #[derive(Clone, Debug)] pub struct BoxHeader { box_size: u64, largesize: bool, box_type: BoxType, header_size: u64, // not sure if needed; u64 for convenience version: Option, // only if box type uses full headers flags: Option<[u8; 3]>, // only if box type uses full headers } impl BoxHeader { /// Creates a new, empty box header for an exif info entry box /// To be used to create a new, empty box for storing exif data that gets /// inserted into a file that previously did not have this box but requires /// one now to store metadata. /// See [create_new_item_info_entry](super::boxes::item_info::ItemInfoBox::create_new_item_info_entry) pub(crate) fn new_exif_info_entry_box_header () -> Self { // Default values based around an empty box Self { box_size: 21, largesize: false, box_type: BoxType::infe, header_size: 12, version: Some(2), flags: Some([0, 0, 1]), } } pub(crate) fn new_simple_box_header () -> Self { Self { box_size: 8, largesize: false, box_type: BoxType::unknown { box_type: "tobi".to_owned() }, header_size: 8, version: None, flags: None, } } pub(crate) fn new_full_box_header () -> Self { Self { box_size: 12, largesize: false, box_type: BoxType::unknown { box_type: "tobi".to_owned() }, header_size: 12, version: Some(0), flags: Some([0, 0, 0]), } } pub(super) fn read_box_header ( cursor: &mut T ) -> Result { // Read in the size let box_size = read_be_u32(cursor)?; // Read in the box type let box_type = BoxType::from_4_bytes(read_4_bytes(cursor)?); let mut header = Self { box_size: box_size as u64, largesize: false, box_type: box_type.clone(), header_size: 8, version: None, flags: None, }; if box_type.extends_fullbox() { header.version = Some(read_1_bytes(cursor)?[0]); header.flags = Some(read_3_bytes(cursor)?); // Adjust header size information header.header_size += 4; } // Uses largesize box size if header.box_size == 1 { header.box_size = read_be_u64(cursor)?; header.largesize = true; // Can't process boxes that are truly larger than u32::MAX on // 32-bit systems if usize::BITS == u32::BITS && header.box_size > u32::MAX as u64 { return io_error!( Unsupported, format!( "Box size {} exceeds maximum supported size on 32-bit systems", header.box_size ) ); } { } // Adjust header size information header.header_size += 8; } if let BoxType::uuid { usertype: _ } = header.box_type { let new_usertype = read_16_bytes(cursor)?; header.box_type = BoxType::uuid { usertype: new_usertype }; // Adjust header size information header.header_size += 16; } return Ok(header); } pub(super) fn get_box_size ( &self ) -> u64 { return self.box_size; } pub(super) fn get_box_type ( &self ) -> BoxType { return self.box_type.clone(); } pub(super) fn get_header_size ( &self ) -> u64 { return self.header_size; } pub(super) fn set_box_size ( &mut self, new_size: u64 ) { self.box_size = new_size; } pub(super) fn set_box_type_via_string ( &mut self, new_type: &str ) { self.box_type = BoxType::from_4_bytes( match new_type.as_bytes().try_into() { Ok(arr) => arr, Err(_) => panic!("Invalid box type string length"), } ); } pub(super) fn get_version ( &self ) -> u8 { return self.version.expect("BoxHeader: version is not set"); } pub(super) fn set_version ( &mut self, new_version: Option ) { self.version = new_version; } pub(super) fn serialize ( &self ) -> Vec { let mut serialized = Vec::new(); // Serialize box size - Part 1 if self.largesize { serialized.extend(to_u8_vec_macro!(u32, &1, &Endian::Big).iter()); } else { serialized.extend(to_u8_vec_macro!(u32, &(self.box_size as u32), &Endian::Big).iter()); } // Serialize box type - Part 1 serialized.extend(self.box_type.to_4_bytes()); // Serialize version and flags (if present) if self.box_type.extends_fullbox() { serialized.push(self.version.expect("BoxHeader: version is not set when serializing")); for flag in self.flags.expect("BoxHeader: flags not set when serializing") { serialized.push(flag); } } // Serialize box size - Part 2 if self.largesize { serialized.extend(to_u8_vec_macro!(u64, &(self.box_size as u64), &Endian::Big).iter()); } // Serialize box type - Part 2 if let BoxType::uuid { usertype } = self.box_type { for byte in usertype { serialized.push(byte); } } return serialized; } }little_exif-0.6.23/src/heif/box_type.rs000064400000000000000000000242661046102023000161370ustar 00000000000000// Copyright © 2025-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details #[allow(non_camel_case_types)] #[derive(Clone, Debug, PartialEq)] pub enum BoxType { ftyp, meta, hdlr, dinf, pitm, iinf, infe, iloc, iref, iprp, ipco, ipma, mdat, idat, pdin, mvhd, tkhd, mdhd, nmhd, elng, stsd, stdp, stts, ctts, cslg, stss, stsh, sdtp, elst, url , urn , dref, stsz, stz2, stsc, stco, co64, padb, subs, saiz, saio, mehd, trex, mfhd, tfhd, trun, tfra, mfro, tfdt, leva, trep, assp, sbgp, sgpd, cprt, tsel, kind, xml , bxml, ipro, mere, schm, fiin, fpar, fecr, gitn, fire, stri, stsg, stvi, sidx, ssix, prft, srpp, vmhd, smhd, srat, chnl, dmix, ludt, txtC, uri , uriI, hmhd, sthd, uuid { usertype: [u8; 16] }, unknown { box_type: String } } impl BoxType { pub(super) fn from_4_bytes ( bytes: [u8; 4] ) -> BoxType { let box_type_str = std::str::from_utf8(&bytes).unwrap_or(""); match box_type_str { "ftyp" => BoxType::ftyp, "meta" => BoxType::meta, "hdlr" => BoxType::hdlr, "dinf" => BoxType::dinf, "pitm" => BoxType::pitm, "iinf" => BoxType::iinf, "infe" => BoxType::infe, "iloc" => BoxType::iloc, "iref" => BoxType::iref, "iprp" => BoxType::iprp, "ipco" => BoxType::ipco, "ipma" => BoxType::ipma, "mdat" => BoxType::mdat, "idat" => BoxType::idat, "pdin" => BoxType::pdin, "mvhd" => BoxType::mvhd, "tkhd" => BoxType::tkhd, "mdhd" => BoxType::mdhd, "nmhd" => BoxType::nmhd, "elng" => BoxType::elng, "stsd" => BoxType::stsd, "stdp" => BoxType::stdp, "stts" => BoxType::stts, "ctts" => BoxType::ctts, "cslg" => BoxType::cslg, "stss" => BoxType::stss, "stsh" => BoxType::stsh, "sdtp" => BoxType::sdtp, "elst" => BoxType::elst, "url " => BoxType::url , "urn " => BoxType::urn , "dref" => BoxType::dref, "stsz" => BoxType::stsz, "stz2" => BoxType::stz2, "stsc" => BoxType::stsc, "stco" => BoxType::stco, "co64" => BoxType::co64, "padb" => BoxType::padb, "subs" => BoxType::subs, "saiz" => BoxType::saiz, "saio" => BoxType::saio, "mehd" => BoxType::mehd, "trex" => BoxType::trex, "mfhd" => BoxType::mfhd, "tfhd" => BoxType::tfhd, "trun" => BoxType::trun, "tfra" => BoxType::tfra, "mfro" => BoxType::mfro, "tfdt" => BoxType::tfdt, "leva" => BoxType::leva, "trep" => BoxType::trep, "assp" => BoxType::assp, "sbgp" => BoxType::sbgp, "sgpd" => BoxType::sgpd, "cprt" => BoxType::cprt, "tsel" => BoxType::tsel, "kind" => BoxType::kind, "xml " => BoxType::xml , "bxml" => BoxType::bxml, "ipro" => BoxType::ipro, "mere" => BoxType::mere, "schm" => BoxType::schm, "fiin" => BoxType::fiin, "fpar" => BoxType::fpar, "fecr" => BoxType::fecr, "gitn" => BoxType::gitn, "fire" => BoxType::fire, "stri" => BoxType::stri, "stsg" => BoxType::stsg, "stvi" => BoxType::stvi, "sidx" => BoxType::sidx, "ssix" => BoxType::ssix, "prft" => BoxType::prft, "srpp" => BoxType::srpp, "vmhd" => BoxType::vmhd, "smhd" => BoxType::smhd, "srat" => BoxType::srat, "chnl" => BoxType::chnl, "dmix" => BoxType::dmix, "ludt" => BoxType::ludt, "txtC" => BoxType::txtC, "uri " => BoxType::uri , "uriI" => BoxType::uriI, "hmhd" => BoxType::hmhd, "sthd" => BoxType::sthd, "uuid" => BoxType::uuid { usertype: [0u8; 16] }, _ => BoxType::unknown { box_type: String::from(box_type_str) } } } pub(super) fn to_4_bytes ( &self ) -> Vec { match self { BoxType::ftyp => "ftyp", BoxType::meta => "meta", BoxType::hdlr => "hdlr", BoxType::dinf => "dinf", BoxType::pitm => "pitm", BoxType::iinf => "iinf", BoxType::infe => "infe", BoxType::iloc => "iloc", BoxType::iref => "iref", BoxType::iprp => "iprp", BoxType::ipco => "ipco", BoxType::ipma => "ipma", BoxType::mdat => "mdat", BoxType::idat => "idat", BoxType::pdin => "pdin", BoxType::mvhd => "mvhd", BoxType::tkhd => "tkhd", BoxType::mdhd => "mdhd", BoxType::nmhd => "nmhd", BoxType::elng => "elng", BoxType::stsd => "stsd", BoxType::stdp => "stdp", BoxType::stts => "stts", BoxType::ctts => "ctts", BoxType::cslg => "cslg", BoxType::stss => "stss", BoxType::stsh => "stsh", BoxType::sdtp => "sdtp", BoxType::elst => "elst", BoxType::url => "url ", BoxType::urn => "urn ", BoxType::dref => "dref", BoxType::stsz => "stsz", BoxType::stz2 => "stz2", BoxType::stsc => "stsc", BoxType::stco => "stco", BoxType::co64 => "co64", BoxType::padb => "padb", BoxType::subs => "subs", BoxType::saiz => "saiz", BoxType::saio => "saio", BoxType::mehd => "mehd", BoxType::trex => "trex", BoxType::mfhd => "mfhd", BoxType::tfhd => "tfhd", BoxType::trun => "trun", BoxType::tfra => "tfra", BoxType::mfro => "mfro", BoxType::tfdt => "tfdt", BoxType::leva => "leva", BoxType::trep => "trep", BoxType::assp => "assp", BoxType::sbgp => "sbgp", BoxType::sgpd => "sgpd", BoxType::cprt => "cprt", BoxType::tsel => "tsel", BoxType::kind => "kind", BoxType::xml => "xml ", BoxType::bxml => "bxml", BoxType::ipro => "ipro", BoxType::mere => "mere", BoxType::schm => "schm", BoxType::fiin => "fiin", BoxType::fpar => "fpar", BoxType::fecr => "fecr", BoxType::gitn => "gitn", BoxType::fire => "fire", BoxType::stri => "stri", BoxType::stsg => "stsg", BoxType::stvi => "stvi", BoxType::sidx => "sidx", BoxType::ssix => "ssix", BoxType::prft => "prft", BoxType::srpp => "srpp", BoxType::vmhd => "vmhd", BoxType::smhd => "smhd", BoxType::srat => "srat", BoxType::chnl => "chnl", BoxType::dmix => "dmix", BoxType::ludt => "ludt", BoxType::txtC => "txtC", BoxType::uri => "uri ", BoxType::uriI => "uriI", BoxType::hmhd => "hmhd", BoxType::sthd => "sthd", BoxType::uuid { usertype: _ } => "uuid", BoxType::unknown { box_type } => box_type }.as_bytes().to_vec() } pub(super) fn extends_fullbox ( &self ) -> bool { matches!(self, BoxType::meta | BoxType::hdlr | BoxType::iinf | BoxType::infe | BoxType::iloc | BoxType::pitm | BoxType::iref | BoxType::pdin | BoxType::mvhd | BoxType::tkhd | BoxType::mdhd | BoxType::nmhd | BoxType::elng | BoxType::stsd | BoxType::stdp | BoxType::stts | BoxType::ctts | BoxType::cslg | BoxType::stss | BoxType::stsh | BoxType::sdtp | BoxType::elst | BoxType::url | BoxType::urn | BoxType::dref | BoxType::stsz | BoxType::stz2 | BoxType::stsc | BoxType::stco | BoxType::co64 | BoxType::padb | BoxType::subs | BoxType::saiz | BoxType::saio | BoxType::mehd | BoxType::trex | BoxType::mfhd | BoxType::tfhd | BoxType::trun | BoxType::tfra | BoxType::mfro | BoxType::tfdt | BoxType::leva | BoxType::trep | BoxType::assp | BoxType::sbgp | BoxType::sgpd | BoxType::cprt | BoxType::tsel | BoxType::kind | BoxType::xml | BoxType::bxml | BoxType::ipro | BoxType::mere | BoxType::schm | BoxType::fiin | BoxType::fpar | BoxType::fecr | BoxType::gitn | BoxType::fire | BoxType::stri | BoxType::stsg | BoxType::stvi | BoxType::sidx | BoxType::ssix | BoxType::prft | BoxType::srpp | BoxType::vmhd | BoxType::smhd | BoxType::srat | BoxType::chnl | BoxType::dmix | BoxType::ludt | BoxType::txtC | BoxType::uri | BoxType::uriI | BoxType::hmhd | BoxType::sthd ) } }little_exif-0.6.23/src/heif/boxes/iso.rs000064400000000000000000000056031046102023000162120ustar 00000000000000// Copyright © 2025-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::Read; use std::io::Seek; use crate::heif::box_header::BoxHeader; use crate::heif::boxes::GenericIsoBox; use crate::heif::boxes::ParsableIsoBox; #[allow(dead_code)] #[derive(Clone)] pub struct IsoBox { header: BoxHeader, data: Vec, } impl IsoBox { fn construct_from_cursor_unboxed ( cursor: &mut T, header: BoxHeader ) -> Result { log::trace!("Constructing generic ISO box for type {:?}", header.get_box_type()); // Check if this box is the last in the file // See also: ISO/IEC 14496-12:2015, § 4.2 if header.get_box_size() == 0 { let mut buffer = Vec::new(); cursor.read_to_end(&mut buffer)?; return Ok(IsoBox { header: header, data: buffer }); } let Some(data_left_to_read) = header.get_box_size().checked_sub(header.get_header_size()) else { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, format!( "Box size {} is smaller than header size {} for box type {:?}", header.get_box_size(), header.get_header_size(), header.get_box_type() ) )); }; let mut buffer: Vec = Vec::new(); // This may cause an out of memory error, but won't panic like vec![] buffer.try_reserve_exact(data_left_to_read as usize)?; // Can't use read_exact here because the name buffer we read into is // still size 0 (only has reserved capacity!) cursor.take(data_left_to_read as u64).read_to_end(&mut buffer)?; return Ok(IsoBox { header: header, data: buffer }); } } impl ParsableIsoBox for IsoBox { fn construct_from_cursor ( cursor: &mut T, header: BoxHeader ) -> Result, std::io::Error> { return Ok(Box::new(IsoBox::construct_from_cursor_unboxed( cursor, header )?)); } } impl GenericIsoBox for IsoBox { fn serialize ( &self ) -> Vec { let mut serialized = self.header.serialize(); serialized.extend(&self.data); return serialized; } fn as_any (& self) -> & dyn std::any::Any { self } fn as_any_mut (&mut self) -> &mut dyn std::any::Any { self } fn get_header (& self) -> & BoxHeader { & self.header } fn get_header_mut (&mut self) -> &mut BoxHeader { &mut self.header } } little_exif-0.6.23/src/heif/boxes/item_info.rs000064400000000000000000000174021046102023000173710ustar 00000000000000// Copyright © 2025-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::Read; use std::io::Seek; use crate::endian::Endian; use crate::general_file_io::io_error; use crate::u8conversion::U8conversion; use crate::u8conversion::to_u8_vec_macro; use crate::util::read_be_u16; use crate::util::read_be_u32; use crate::util::read_null_terminated_string; use crate::heif::box_header::BoxHeader; use crate::heif::boxes::GenericIsoBox; use crate::heif::boxes::ParsableIsoBox; // - infe // 00000015: size of 0x15 bytes (including the 0x04 bytes of the size field itself) // 696E6665: byte representation of `infe` // 02: version 2 // 000001: 24 bits of flags // 0019: item ID (16 bits) // 0000: item protection index (16 bits) // 6876633100: item name, a null terminated string, here: "hvc1" // theoretically, after this point there would be two other strings, the // content_type and the optional content_encoding, however, the practical // examples did *not* have any of this #[allow(dead_code)] #[derive(Debug)] pub struct ItemInfoEntryBox { pub(self) header: BoxHeader, pub(crate) item_id: u16, pub(crate) item_protection_index: u16, pub(crate) item_name: String, pub(crate) additional_data: Vec, } // - iinf // 00000603: size of 0x603 bytes (including the 0x04 bytes of the size field itself) // 69696E66: byte representation of `iinf` // 00000000: version (here: 0) and 24 bits of flags // 0041: number of item info entries, here 0x41 = 65 // 0000001569: start of first info entry #[allow(dead_code)] #[derive(Debug)] pub struct ItemInfoBox { pub(self) header: BoxHeader, pub(crate) item_count: usize, pub(crate) items: Vec } impl ItemInfoEntryBox { fn construct_from_cursor_unboxed ( cursor: &mut T, header: BoxHeader ) -> Result { let item_id = read_be_u16(cursor)?; let item_protection_index = read_be_u16(cursor)?; let item_name = read_null_terminated_string(cursor)?; // Determine how much data is left for this entry let data_read_so_far = header.get_header_size() + 2 // item_id + 2 // item_protection_index + item_name.len() as u64 + 1; // string len + null terminator if data_read_so_far > header.get_box_size() { return io_error!( InvalidData, format!( "ItemInfoEntryBox data read so far ({}) exceeds box size ({})", data_read_so_far, header.get_box_size() ) ); } let data_left_to_read = header.get_box_size() - data_read_so_far; let mut additional_data: Vec = Vec::new(); additional_data.try_reserve_exact(data_left_to_read as usize)?; cursor.take(data_left_to_read as u64).read_to_end(&mut additional_data)?; log::trace!("Successfully read in ItemInfoEntryBox with ID: {item_id}, Name: {item_name}"); return Ok(ItemInfoEntryBox { header, item_id, item_protection_index, item_name, additional_data, }); } } impl ParsableIsoBox for ItemInfoEntryBox { fn construct_from_cursor ( cursor: &mut T, header: BoxHeader ) -> Result, std::io::Error> { return Ok(Box::new(ItemInfoEntryBox::construct_from_cursor_unboxed( cursor, header )?)); } } impl ItemInfoBox { pub fn get_exif_item ( &self ) -> Option<&ItemInfoEntryBox> { return self.items.iter() .find(|item| item.item_name == "Exif") } /// Creates a new item in this item information box and returns by how many /// bytes this box got longer pub(crate) fn create_new_item_info_entry ( &mut self, iloc_id: u32, name: &str, ) -> u64 { self.items.push(ItemInfoEntryBox { header: BoxHeader::new_exif_info_entry_box_header(), item_id: iloc_id as u16, item_protection_index: 0, item_name: name.to_string(), additional_data: Vec::new() } ); self.item_count += 1; // Due to the addition of a new item, the size in the header needs to // be adjusted as well // TODO: make this more efficient by only computing how much memory is // needed, not by actually serializing (and thus, allocating memory) let old_box_size = self.header.get_box_size(); let new_box_size = self.serialize().len() as u64; self.header.set_box_size(new_box_size); return new_box_size - old_box_size; } } impl ParsableIsoBox for ItemInfoBox { fn construct_from_cursor ( cursor: &mut T, header: BoxHeader ) -> Result, std::io::Error> { // See: ISO/IEC 14496-12:2015, § 8.11.6.2 let item_count = if header.get_version() == 0 { read_be_u16(cursor)? as usize } else { read_be_u32(cursor)? as usize }; let mut items = Vec::new(); for _ in 0..item_count { let header = BoxHeader::read_box_header(cursor)?; items.push(ItemInfoEntryBox::construct_from_cursor_unboxed( cursor, header )?); } return Ok(Box::new(ItemInfoBox { header, item_count, items })); } } impl GenericIsoBox for ItemInfoEntryBox { fn serialize ( &self ) -> Vec { let mut serialized = self.header.serialize(); serialized.extend(to_u8_vec_macro!(u16, &self.item_id, &Endian::Big).iter()); serialized.extend(to_u8_vec_macro!(u16, &self.item_protection_index, &Endian::Big).iter()); serialized.extend(self.item_name.bytes()); serialized.push(0x00); // null terminator for item name string serialized.extend(&self.additional_data); return serialized; } fn as_any (& self) -> & dyn std::any::Any { self } fn as_any_mut (&mut self) -> &mut dyn std::any::Any { self } fn get_header (& self) -> & BoxHeader { & self.header } fn get_header_mut (&mut self) -> &mut BoxHeader { &mut self.header } } impl GenericIsoBox for ItemInfoBox { fn serialize ( &self ) -> Vec { let mut serialized = self.header.serialize(); if self.header.get_version() == 0 { serialized.extend(to_u8_vec_macro!(u16, &(self.item_count as u16), &Endian::Big).iter()); } else { serialized.extend(to_u8_vec_macro!(u32, &(self.item_count as u32), &Endian::Big).iter()); } for item in &self.items { serialized.extend(item.serialize()); } return serialized; } fn as_any (& self) -> & dyn std::any::Any { self } fn as_any_mut (&mut self) -> &mut dyn std::any::Any { self } fn get_header (& self) -> & BoxHeader { & self.header } fn get_header_mut (&mut self) -> &mut BoxHeader { &mut self.header } }little_exif-0.6.23/src/heif/boxes/item_location.rs000064400000000000000000000412771046102023000202550ustar 00000000000000// Copyright © 2025-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::Read; use std::io::Seek; use crate::io_error_plain; use crate::endian::Endian; use crate::general_file_io::io_error; use crate::u8conversion::U8conversion; use crate::u8conversion::to_u8_vec_macro; use crate::util::read_be_u16; use crate::util::read_be_u32; use crate::util::read_be_u64; use crate::heif::box_header::BoxHeader; use crate::heif::boxes::GenericIsoBox; use crate::heif::boxes::ParsableIsoBox; #[allow(dead_code)] pub(crate) struct ItemLocationBox { pub(self) header: BoxHeader, pub(crate) offset_size: u8, // actually u4 pub(crate) length_size: u8, // actually u4 pub(crate) base_offset_size: u8, // actually u4 pub(crate) index_size: u8, // actually u4, // only available if version == 1 || 2, otherwise these 4 bytes are // handled as `reserved` pub(crate) item_count: u32, // only if version == 2, if version < 2 this is u16 pub(crate) items: Vec } #[derive(Debug, PartialEq)] pub(crate) enum ItemConstructionMethod { FILE = 0, IDAT = 1, ITEM = 2, } #[allow(dead_code)] #[derive(Debug)] pub(crate) struct ItemLocationEntry { pub(crate) item_id: u32, // only if version == 2, if version < 2 this is u16 pub(crate) reserved_and_construction_method: u16, // first 12 bits are reserved, the other 4 are construction method: // - 0: file // - 1: idat // - 2: item // only present if version == 1 || 2 pub(crate) data_reference_index: u16, pub(crate) base_offset: u64, // actual size depends on value of base_offset_size * 8 pub(crate) extent_count: u16, // must be equal or greater 1 pub(crate) extents: Vec, } #[allow(dead_code)] #[derive(Debug)] pub(crate) struct ItemLocationEntryExtentEntry { pub(crate) extent_index: Option, // only if (version == 1 || 2) && index_size>0 // actual size depends on index_size * 8 pub(crate) extent_offset: u64, // actual size depends on offset_size * 8 pub(crate) extent_length: u64, // actual size depends on length_size * 8 } /* 0001: item_id 0000: reserved and construction method 0000: data ref index // as base offset size is zero, no bytes for base offset 0001: extent count // as index size is also zero, no bytes for extent index 00004841: extent offset 0000052D: extent length */ impl ItemLocationEntryExtentEntry { fn read_from_cursor ( cursor: &mut T, header: &BoxHeader, offset_size: u8, length_size: u8, index_size: u8, ) -> Result { let extent_index = if (header.get_version() == 1 || header.get_version() == 2) && index_size > 0 { match index_size { 4 => Some(read_be_u32(cursor)? as u64), 8 => Some(read_be_u64(cursor)?), _ => return io_error!(Other, format!("Invalid index_size: {}!", index_size)) } } else { None }; let extent_offset = match offset_size { 0 => 0, 4 => read_be_u32(cursor)? as u64, 8 => read_be_u64(cursor)?, _ => return io_error!(Other, format!("Invalid offset_size: {}!", offset_size)) }; let extent_length = match length_size { 0 => 0, 4 => read_be_u32(cursor)? as u64, 8 => read_be_u64(cursor)?, _ => return io_error!(Other, format!("Invalid length_size: {}!", length_size)) }; return Ok(Self{extent_index, extent_offset, extent_length}); } } impl ItemLocationEntry { fn read_from_cursor ( cursor: &mut T, header: &BoxHeader, offset_size: u8, length_size: u8, base_offset_size: u8, index_size: u8, ) -> Result { let item_id = match header.get_version() { 0 | 1 => read_be_u16(cursor)? as u32, 2 => read_be_u32(cursor)?, _ => return io_error!(Other, "Invalid version for ItemLocationEntry decode!".to_string()) }; let reserved_and_construction_method = if (header.get_version() == 1) || (header.get_version() == 2) { read_be_u16(cursor)? } else { 0 }; let data_reference_index = read_be_u16(cursor)?; let base_offset = match base_offset_size { 0 => 0, 4 => read_be_u32(cursor)? as u64, 8 => read_be_u64(cursor)?, _ => return io_error!(Other, "Invalid base_offset_size!".to_string()) }; let extent_count = read_be_u16(cursor)?; let mut extents = Vec::new(); for _ in 0..extent_count { extents.push(ItemLocationEntryExtentEntry::read_from_cursor( cursor, header, offset_size, length_size, index_size )?); } let entry = Self { item_id, reserved_and_construction_method, data_reference_index, base_offset, extent_count, extents }; log::trace!("Read in ItemLocationEntry: {:?}", entry); return Ok(entry); } pub(crate) fn get_construction_method ( &self ) -> ItemConstructionMethod { return match self.reserved_and_construction_method as u8 & 0x0f { 0 => ItemConstructionMethod::FILE, 1 => ItemConstructionMethod::IDAT, 2 => ItemConstructionMethod::ITEM, _ => panic!("Unknown item construction method!") }; } /* pub(crate) fn get_size ( &self, parent: &ItemLocationBox ) -> usize { let mut size = 0usize; // item_id if parent.get_header().get_version() == 2 { size += 4; } else { size += 2; } // reserved_and_construction_method if parent.get_header().get_version() == 1 || parent.get_header().get_version() == 2 { size += 2; } // data_reference_index size += 2; // base_offset size += parent.base_offset_size as usize; // extent_count size += 2; // extents for extent in &self.extents { size += extent.get_size( parent.offset_size, parent.length_size, parent.index_size ); } return size; } */ } impl ItemLocationBox { fn construct_from_cursor_unboxed ( cursor: &mut T, header: BoxHeader ) -> Result { let temp = read_be_u16(cursor)?; let (offset_size, length_size, base_offset_size) = ( (temp >> 12 & 0x0f) as u8, (temp >> 8 & 0x0f) as u8, (temp >> 4 & 0x0f) as u8 ); let index_size = match header.get_version() { 1 | 2 => temp as u8 & 0x0f, _ => 0, }; let item_count = match header.get_version() { 0 | 1 => read_be_u16(cursor)? as u32, 2 => read_be_u32(cursor)?, _ => return io_error!(Other, "Invalid version for ItemLocationBox decode!".to_string()) }; let mut items = Vec::new(); for _ in 0..item_count { items.push(ItemLocationEntry::read_from_cursor( cursor, &header, offset_size, length_size, base_offset_size, index_size )?); } return Ok(ItemLocationBox { header, offset_size, length_size, base_offset_size, index_size, item_count, items }); } pub(crate) fn get_item_location_entry ( &self, item_id: u16 ) -> Result<&ItemLocationEntry, std::io::Error> { self.items.iter() .find(|item| item.item_id == item_id as u32) .ok_or( io_error_plain!( Other, format!("ItemLocationEntry with item_id {} not found!", item_id) ) ) } // Returns the ID of the new entry and by how many bytes this box got longer pub(crate) fn create_new_item_location_entry ( &mut self, data_start: u64, data_length: u64 ) -> (u32, u64) { // Determine largest iloc ID so far let old_largest_id = self.items .iter() .map(|x| x.item_id) .max() .unwrap_or(0); self.items.push(ItemLocationEntry { item_id: old_largest_id + 1, reserved_and_construction_method: 0, data_reference_index: 0, base_offset: 0, extent_count: 1, extents: vec![ ItemLocationEntryExtentEntry { extent_index: Some(0), extent_offset: data_start, extent_length: data_length, } ] } ); self.item_count += 1; // Due to the addition of a new item, the size in the header needs to // be adjusted as well // TODO: make this more efficient by only computing how much memory is // needed, not by actually serializing (and thus, allocating memory) let old_box_size = self.header.get_box_size(); let new_box_size = self.serialize().len() as u64; self.header.set_box_size(new_box_size); return ( self.items.last_mut().expect("No items present after insertion").item_id, new_box_size - old_box_size ); } pub(crate) fn add_to_extents ( &mut self, value: i64 ) { for item in &mut self.items { if item.get_construction_method() == ItemConstructionMethod::IDAT { // In this case the offset information is relative to the // position of an idat box -> not affected by change in length // of another box continue; } if item.get_construction_method() == ItemConstructionMethod::ITEM { // Offset is relative to another item's extent // Also nothing to do here (for now...) continue; } if item.data_reference_index != 0 { // A value other than 0 implies that this extent refers to // another file, not this one, so we can also skip this // See ISO/IEC 14496-12:2015 § 8.11.3.1, p. 78 continue; } // For now, just add the value to the extent offsets. // This may be problematic in case the offset points to a location // before the iloc or iinf boxes, so changing their length won't // affect that offset value for extent in &mut item.extents { extent.extent_offset = (extent.extent_offset as i64 + value) as u64; } } } } impl ParsableIsoBox for ItemLocationBox { fn construct_from_cursor ( cursor: &mut T, header: BoxHeader ) -> Result, std::io::Error> { return Ok(Box::new(ItemLocationBox::construct_from_cursor_unboxed( cursor, header )?)); } } impl GenericIsoBox for ItemLocationBox { fn serialize ( &self ) -> Vec { let mut serialized = self.header.serialize(); // let (offset_size, length_size, base_offset_size) = // ((temp >> 12) as u8, (temp >> 8 & 0x0f) as u8, (temp >> 4 & 0x0f) as u8); #[allow(clippy::double_parens)] let temp = 0u16 + ((self.offset_size as u16) << 12) + ((self.length_size as u16) << 8) + ((self.base_offset_size as u16) << 4) + ((self.index_size as u16) << 0) ; serialized.extend(to_u8_vec_macro!(u16, &temp, &Endian::Big).iter()); match self.header.get_version() { 0 | 1 => serialized.extend(to_u8_vec_macro!(u16, &(self.item_count as u16), &Endian::Big).iter()), 2 => serialized.extend(to_u8_vec_macro!(u32, & self.item_count, &Endian::Big).iter()), _ => panic!("Invalid version for ItemLocationBox serialize!") }; for item in &self.items { match self.header.get_version() { 0 | 1 => serialized.extend(to_u8_vec_macro!(u16, &(item.item_id as u16), &Endian::Big).iter()), 2 => serialized.extend(to_u8_vec_macro!(u32, & item.item_id, &Endian::Big).iter()), _ => panic!("Invalid version for ItemLocationBox serialize!") }; if (self.header.get_version() == 1) || (self.header.get_version() == 2) { serialized.extend(to_u8_vec_macro!(u16, &item.reserved_and_construction_method, &Endian::Big).iter()); } serialized.extend(to_u8_vec_macro!(u16, &item.data_reference_index, &Endian::Big).iter()); match self.base_offset_size { 0 => (), 4 => serialized.extend(to_u8_vec_macro!(u32, &(item.base_offset as u32), &Endian::Big).iter()), 8 => serialized.extend(to_u8_vec_macro!(u64, & item.base_offset, &Endian::Big).iter()), _ => panic!("Invalid base_offset_size!") }; serialized.extend(to_u8_vec_macro!(u16, &item.extent_count, &Endian::Big).iter()); for extent in &item.extents { if (self.header.get_version() == 1 || self.header.get_version() == 2) && self.index_size > 0 { match self.index_size { 4 => { let idx: u32 = extent.extent_index.expect("Extent index missing") as u32; serialized.extend(to_u8_vec_macro!(u32, &idx, &Endian::Big).iter()); }, 8 => { let idx: u64 = extent.extent_index.expect("Extent index missing"); serialized.extend(to_u8_vec_macro!(u64, &idx, &Endian::Big).iter()); }, _ => panic!("Invalid index_size!") } } match self.offset_size { 0 => (), 4 => serialized.extend(to_u8_vec_macro!(u32, &(extent.extent_offset as u32), &Endian::Big).iter()), 8 => serialized.extend(to_u8_vec_macro!(u64, & extent.extent_offset, &Endian::Big).iter()), _ => panic!("Invalid offset_size!") }; match self.length_size { 0 => (), 4 => serialized.extend(to_u8_vec_macro!(u32, &(extent.extent_length as u32), &Endian::Big).iter()), 8 => serialized.extend(to_u8_vec_macro!(u64, & extent.extent_length, &Endian::Big).iter()), _ => panic!("Invalid length_size!") }; } } return serialized; } fn as_any (& self) -> & dyn std::any::Any { self } fn as_any_mut (&mut self) -> &mut dyn std::any::Any { self } fn get_header (& self) -> & BoxHeader { & self.header } fn get_header_mut (&mut self) -> &mut BoxHeader { &mut self.header } } little_exif-0.6.23/src/heif/boxes/item_reference.rs000064400000000000000000000170311046102023000203720ustar 00000000000000// Copyright © 2025-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::Read; use std::io::Seek; use crate::endian::Endian; use crate::u8conversion::U8conversion; use crate::u8conversion::to_u8_vec_macro; use crate::util::read_be_u16; use crate::util::read_be_u32; use crate::heif::box_header::BoxHeader; use crate::heif::boxes::GenericIsoBox; use crate::heif::boxes::ParsableIsoBox; use crate::io_error; #[allow(non_snake_case)] #[derive(Debug)] pub struct SingleItemTypeReferenceBox { pub(self) header: BoxHeader, pub(self) is_large: bool, pub(crate) from_item_ID: u32, pub(crate) reference_count: u16, pub(crate) to_item_ID: Vec, } #[derive(Debug)] pub struct ItemReferenceBox { pub(self) header: BoxHeader, pub(crate) references: Vec, } impl SingleItemTypeReferenceBox { #[allow(non_snake_case)] fn construct_from_cursor_unboxed ( cursor: &mut T, iref_header: &BoxHeader, ) -> Result { let header = BoxHeader::read_box_header(cursor)?; let mut to_item_ID = Vec::new(); // Depending on the version stored in the header of the iref box, // the references are either 'normal' (version == 0) or "large" // (version == 1), see ISO/IEC 14496-12:2015 § 8.11.12.2 let is_large = if iref_header.get_version() == 0 { false } else if iref_header.get_version() == 1 { true } else { return io_error!(InvalidData, "Expected either version == 0 or version == 1 for iref box! Please create a new ticket at https://github.com/TechnikTobi/little_exif with an example image file"); }; let from_item_ID = if is_large { read_be_u32(cursor)? } else { read_be_u16(cursor)? as u32 }; let reference_count = read_be_u16(cursor)?; for _ in 0..reference_count { to_item_ID.push( if is_large { read_be_u32(cursor)? } else { read_be_u16(cursor)? as u32 } ); } return Ok(SingleItemTypeReferenceBox { header, is_large, from_item_ID, reference_count, to_item_ID } ); } } impl ItemReferenceBox { pub(super) fn new () -> Self { let mut header = BoxHeader::new_full_box_header(); header.set_box_type_via_string("iref"); header.set_version(Some(1)); return ItemReferenceBox { header, references: Vec::new() }; } fn construct_from_cursor_unboxed ( cursor: &mut T, header: BoxHeader ) -> Result { let mut bytes_read = 0; let mut references = Vec::new(); if header.get_box_size() < header.get_header_size() { return io_error!(InvalidData, "Box size is smaller than header size for iref box"); } while bytes_read < header.get_box_size() - header.get_header_size() { let next_reference = SingleItemTypeReferenceBox::construct_from_cursor_unboxed( cursor, &header )?; bytes_read += next_reference.get_header().get_box_size(); references.push(next_reference); } return Ok(ItemReferenceBox { header, references }); } #[allow(non_snake_case)] pub(crate) fn create_new_single_item_reference_box ( &mut self, reference_type: &str, from_item_ID: u32, to_item_ID: Vec ) -> u64 { // Determine is_large and the box size let is_large = self.header.get_version() >= 1; let ID_size = if is_large { 4 } else { 2 }; let box_size = 0 + 8 // header + ID_size // from_item_id + 2 // reference_count + to_item_ID.len() as u64 * ID_size // to_item_id ; let mut new_reference_header = BoxHeader::new_simple_box_header(); new_reference_header.set_box_type_via_string(reference_type); new_reference_header.set_box_size(box_size); let singe_item_reference_box = SingleItemTypeReferenceBox { header: new_reference_header, is_large, from_item_ID, reference_count: to_item_ID.len() as u16, to_item_ID, }; self.references.push(singe_item_reference_box); let old_iref_box_size = self.header.get_box_size(); let new_iref_box_size = old_iref_box_size + box_size; self.header.set_box_size(new_iref_box_size); return box_size; } } impl ParsableIsoBox for ItemReferenceBox { fn construct_from_cursor ( cursor: &mut T, header: BoxHeader ) -> Result, std::io::Error> { return Ok(Box::new(ItemReferenceBox::construct_from_cursor_unboxed( cursor, header )?)); } } impl GenericIsoBox for SingleItemTypeReferenceBox { #[allow(non_snake_case)] fn serialize ( &self ) -> Vec { let mut serialized = self.header.serialize(); // from_item_ID if self.is_large { serialized.extend(to_u8_vec_macro!(u32, &self.from_item_ID, &Endian::Big).iter()); } else { serialized.extend(to_u8_vec_macro!(u16, &(self.from_item_ID as u16), &Endian::Big).iter()); } // reference_count serialized.extend(to_u8_vec_macro!(u16, &self.reference_count, &Endian::Big).iter()); // to_item_ID for to_item_ID_entry in &self.to_item_ID { if self.is_large { serialized.extend(to_u8_vec_macro!(u32, to_item_ID_entry, &Endian::Big).iter()); } else { serialized.extend(to_u8_vec_macro!(u16, &(*to_item_ID_entry as u16), &Endian::Big).iter()); } } return serialized; } fn as_any (& self) -> & dyn std::any::Any { self } fn as_any_mut (&mut self) -> &mut dyn std::any::Any { self } fn get_header (& self) -> & BoxHeader { & self.header } fn get_header_mut (&mut self) -> &mut BoxHeader { &mut self.header } } impl GenericIsoBox for ItemReferenceBox { fn serialize ( &self ) -> Vec { let mut serialized = self.header.serialize(); for reference in &self.references { serialized.extend(reference.serialize()); } return serialized; } fn as_any (& self) -> & dyn std::any::Any { self } fn as_any_mut (&mut self) -> &mut dyn std::any::Any { self } fn get_header (& self) -> & BoxHeader { & self.header } fn get_header_mut (&mut self) -> &mut BoxHeader { &mut self.header } } little_exif-0.6.23/src/heif/boxes/meta.rs000064400000000000000000000247141046102023000163520ustar 00000000000000// Copyright © 2025-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::Cursor; use std::io::Read; use std::io::Seek; use crate::endian::Endian; use crate::general_file_io::io_error; use crate::u8conversion::U8conversion; use crate::u8conversion::to_u8_vec_macro; use crate::util::read_be_u32; use crate::heif::box_type::BoxType; use crate::heif::box_header::BoxHeader; use crate::heif::boxes::GenericIsoBox; use crate::heif::boxes::ParsableIsoBox; use crate::heif::boxes::item_info::ItemInfoBox; use crate::heif::boxes::item_location::ItemLocationBox; use crate::heif::boxes::item_reference::ItemReferenceBox; use super::read_box_based_on_header; #[allow(dead_code)] pub struct MetaBox { header: BoxHeader, handler_box: HandlerBox, // primary_item_box: Option, // pitm // data_info_box: Option, // dinf // item_loc_box: Option, // iloc // item_protect_box: Option, // ipro // item_info_box: Option, // iinf // ipmp_control_box: Option, // ipmc // item_ref_box: Option, // iref // item_data_box: Option, // idat pub(crate) other_boxes: Vec>, } impl MetaBox { pub(crate) fn get_item_info_box ( &self ) -> Result<&ItemInfoBox, std::io::Error> { match self.other_boxes.iter().find(|b| b.get_header().get_box_type() == BoxType::iinf) { Some(b) => match b.as_any().downcast_ref::() { Some(unboxed) => Ok(unboxed), None => io_error!( InvalidData, "Found iinf box but could not downcast to ItemInfoBox" ), }, None => io_error!( NotFound, "No iinf box found in MetaBox" ), } } pub(crate) fn get_item_location_box ( &self ) -> Result<&ItemLocationBox, std::io::Error> { match self.other_boxes.iter().find(|b| b.get_header().get_box_type() == BoxType::iloc) { Some(b) => match b.as_any().downcast_ref::() { Some(unboxed) => Ok(unboxed), None => io_error!( InvalidData, "Found iloc box but could not downcast to ItemLocationBox" ), }, None => io_error!( NotFound, "No iloc box found in MetaBox" ), } } pub(crate) fn get_item_location_box_mut ( &mut self ) -> Result<&mut ItemLocationBox, std::io::Error> { match self.other_boxes.iter_mut().find(|b| b.get_header().get_box_type() == BoxType::iloc) { Some(b) => match b.as_any_mut().downcast_mut::() { Some(unboxed) => Ok(unboxed), None => io_error!( InvalidData, "Found iloc box but could not downcast to ItemLocationBox" ), }, None => io_error!( NotFound, "No iloc box found in MetaBox" ), } } pub(crate) fn get_item_reference_box ( &self ) -> Option<&ItemReferenceBox> { if let Some(found_box) = self.other_boxes.iter() .find(|b| b.get_header().get_box_type() == BoxType::iref) { return found_box.as_any().downcast_ref::(); } return None; } pub(crate) fn create_new_item_reference_box_if_none_exists_yet ( &mut self ) -> u64 { if self.get_item_reference_box().is_some() { return 0; } let new_iref_box = ItemReferenceBox::new(); let new_iref_box_size = new_iref_box.get_header().get_box_size(); let index = self.other_boxes .iter() .position(|x| x.get_header().get_box_type() == BoxType::iinf) .expect("Could not find iinf box to insert iref before"); self.other_boxes.insert(index, Box::new(new_iref_box)); return new_iref_box_size; } } impl ParsableIsoBox for MetaBox { fn construct_from_cursor ( cursor: &mut T, header: BoxHeader ) -> Result, std::io::Error> { if header.get_box_size() < header.get_header_size() { return io_error!( InvalidData, format!( "MetaBox has invalid size: box size {} is smaller than header size {}", header.get_box_size(), header.get_header_size() ) ); } if header.get_box_size() < header.get_header_size() { return io_error!( InvalidData, format!( "MetaBox has invalid size: box size {} is too small", header.get_box_size() ) ); } // Read in the remaining bytes for this box let remaining_bytes = header.get_box_size() - header.get_header_size(); let mut meta_box_bytes: Vec = Vec::new(); meta_box_bytes.try_reserve_exact(remaining_bytes as usize)?; cursor.take(remaining_bytes as u64).read_to_end(&mut meta_box_bytes)?; // Construct local cursor for these bytes let mut local_cursor = Cursor::new(meta_box_bytes); // Read in the mandatory handler box let handler_box_header = BoxHeader::read_box_header(&mut local_cursor)?; let handler_box = HandlerBox::construct_from_cursor_unboxed( &mut local_cursor, handler_box_header )?; // Read in other boxes let mut other_boxes = Vec::new(); while local_cursor.position() < remaining_bytes as u64 { let sub_header = BoxHeader::read_box_header(&mut local_cursor)?; let sub_box = read_box_based_on_header( &mut local_cursor, sub_header )?; other_boxes.push(sub_box); } return Ok(Box::new(MetaBox { header, handler_box, other_boxes, })); } } #[allow(dead_code)] pub struct HandlerBox { header: BoxHeader, pre_defined: u32, handler_type: u32, reserved: [u32; 3], name: Vec // UTF-8 string, don't bother decoding } impl HandlerBox { fn construct_from_cursor_unboxed ( cursor: &mut T, header: BoxHeader ) -> Result { let pre_defined = read_be_u32(cursor)?; let handler_type = read_be_u32(cursor)?; let reserved = [ read_be_u32(cursor)?, read_be_u32(cursor)?, read_be_u32(cursor)? ]; // Check that there is enough data left to read the box name if header.get_box_size() < header.get_header_size() + 4 + 4 + 12 { return io_error!( InvalidData, format!( "HandlerBox has invalid size: box size {} is too small to contain mandatory name field", header.get_box_size() ) ); } // Check that the remaining data is not unreasonably large // This threshold is somewhat arbitrary if header.get_box_size() > (u32::MAX/16) as u64 { return io_error!( Unsupported, format!( "HandlerBox size {} exceeds maximum supported size ({})", header.get_box_size(), (u32::MAX/16) ) ); } let number_of_bytes_that_form_the_name = header.get_box_size() as u64 - header.get_header_size() as u64 // header - 4 // pre_defined - 4 // handler_type - 12 // reserved ; let mut name: Vec = Vec::new(); // This may cause an out of memory error, but won't panic like vec![] name.try_reserve_exact(number_of_bytes_that_form_the_name as usize)?; // Can't use read_exact here because the name buffer we read into is // still size 0 (only has reserved capacity!) cursor .take(number_of_bytes_that_form_the_name as u64) .read_to_end(&mut name)?; return Ok(HandlerBox { header, pre_defined, handler_type, reserved, name, }); } } impl GenericIsoBox for MetaBox { fn serialize ( &self ) -> Vec { let mut serialized = self.header.serialize(); serialized.extend(self.handler_box.serialize()); for sub_box in &self.other_boxes { serialized.extend(sub_box.serialize()); } return serialized; } fn as_any (& self) -> & dyn std::any::Any { self } fn as_any_mut (&mut self) -> &mut dyn std::any::Any { self } fn get_header (& self) -> & BoxHeader { & self.header } fn get_header_mut (&mut self) -> &mut BoxHeader { &mut self.header } } impl GenericIsoBox for HandlerBox { fn serialize ( &self ) -> Vec { let mut serialized = self.header.serialize(); serialized.extend(to_u8_vec_macro!(u32, &self.pre_defined, &Endian::Big).iter()); serialized.extend(to_u8_vec_macro!(u32, &self.handler_type, &Endian::Big).iter()); for value in &self.reserved { serialized.extend(to_u8_vec_macro!(u32, &value, &Endian::Big).iter()); } serialized.extend(&self.name); return serialized; } fn as_any (& self) -> & dyn std::any::Any { self } fn as_any_mut (&mut self) -> &mut dyn std::any::Any { self } fn get_header (& self) -> & BoxHeader { & self.header } fn get_header_mut (&mut self) -> &mut BoxHeader { &mut self.header } }little_exif-0.6.23/src/heif/boxes/mod.rs000064400000000000000000000037761046102023000162100ustar 00000000000000// Copyright © 2025 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::Read; use std::io::Seek; use crate::heif::boxes::item_reference::ItemReferenceBox; use super::box_type::BoxType; use super::box_header::BoxHeader; pub(super) mod iso; pub(super) mod meta; pub(super) mod item_info; pub(super) mod item_location; pub(super) mod item_reference; use iso::IsoBox; use meta::MetaBox; use item_info::ItemInfoBox; use item_location::ItemLocationBox; #[allow(dead_code)] pub trait GenericIsoBox { fn as_any (& self) -> & dyn std::any::Any; fn as_any_mut (&mut self) -> &mut dyn std::any::Any; fn get_header (& self) -> & BoxHeader; fn get_header_mut (&mut self) -> &mut BoxHeader; fn serialize (& self) -> Vec; } pub trait ParsableIsoBox: GenericIsoBox { fn construct_from_cursor ( cursor: &mut T, header: BoxHeader ) -> Result, std::io::Error>; } pub(super) fn read_box_based_on_header ( cursor: &mut T, header: BoxHeader ) -> Result, std::io::Error> { return match header.get_box_type() { BoxType::meta => MetaBox:: construct_from_cursor(cursor, header), BoxType::iinf => ItemInfoBox:: construct_from_cursor(cursor, header), BoxType::iloc => ItemLocationBox:: construct_from_cursor(cursor, header), BoxType::iref => ItemReferenceBox::construct_from_cursor(cursor, header), _ => IsoBox:: construct_from_cursor(cursor, header) }; } pub(super) fn read_next_box ( cursor: &mut T, ) -> Result, std::io::Error> { let header = BoxHeader::read_box_header(cursor)?; log::trace!("Read in next HEIF box - Success! Header: {:?}", header); return read_box_based_on_header(cursor, header); }little_exif-0.6.23/src/heif/container.rs000064400000000000000000000565031046102023000162670ustar 00000000000000// Copyright © 2025-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::Cursor; use std::io::Read; use std::io::Seek; use crate::general_file_io::io_error; use crate::general_file_io::EXIF_HEADER; use crate::heif::box_type::BoxType; use crate::heif::boxes::item_location::ItemConstructionMethod; use crate::heif::boxes::item_reference::ItemReferenceBox; use crate::heif::boxes::meta::MetaBox; use crate::heif::read_next_box; use crate::metadata::Metadata; use crate::util::insert_multiple_at; use crate::util::range_remove; use crate::util::read_be_u32; use super::boxes::GenericIsoBox; use super::boxes::item_info::ItemInfoBox; use super::boxes::item_location::ItemLocationBox; pub struct HeifContainer { boxes: Vec> } // General structure of ISO container // // ┏━━━━━━┓ // ┃ ftyp ┃ // ┣━━━━━━┫ // ┃ meta ┃ ─> ┏━━━━━━┓ // ┗━━━━━━┛ ┃ hdlr ┃ // ┣━━━━━━┫ // ┃ pitm ┃ // ┣━━━━━━┫ // ┃ iinf ┃ ─> ┏━━━━━━┳━━━━━━━━━┓ // ┗━━━━━━┛ ┃ infe ┃ ID|Exif ┃ // ┣━━━━━━╋━━━━━━━━━┫ // ┃ infe ┃ ID|XMP ┃ // ┗━━━━━━┻━━━━━━━━━┛ // // ┏━━━━━━┳━━━┳━━━━━━━━━━━━━━━┓┉┉┉┏━━━━━━━━━━━━━┓ // ┃ iloc ┃ n ┃ ID|from|lenID ┃ ┃ ID|from|len ┃ // ┗━━━━━━┻━━━┻━━━━━━━━━━━━━━━┛┉┉┉┗━━━━━━━━━━━━━┛ // ┌────────────────────┘ // │ ┏━━━━━━┓ // │ ┃ iref ┃ // │ ┣━━━━━━┫ // │ ┃ idat ┃ // │ ┣━━━━━━┫ // │ ┃ iprp ┃ ─> ┏━━━━━━┓ // │ ┗━━━━━━┛ ┃ ipco ┃ ─> ┏━━━━━━┓ // │ ┗━━━━━━┛ ┃ ispe ┃ // │ ┣━━━━━━╋━━━━━━━━━━━━━┓ // │ ┃ colr ┃ ICC profile ┃ // │ ┏━━━━━━┓ ┗━━━━━━┻━━━━━━━━━━━━━┛ // │ ┃ ipma ┃ // │ ┗━━━━━━┛ // └─>┏━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ II*\0 ... ┃ // ┣━━━━━━━━━━━━━━━┳━━━━━━━┛ // ┃ ( cursor: &mut T, ) -> Result { let mut boxes = Vec::new(); while let Ok(next_box) = read_next_box(cursor) { boxes.push(next_box); } return Ok(Self { boxes }) } fn get_meta_box ( &self ) -> Result<&MetaBox, std::io::Error> { match self.boxes.iter() .find(|b| b.get_header().get_box_type() == BoxType::meta) { Some(b) => match b.as_any().downcast_ref::() { Some(unboxed) => Ok(unboxed), None => io_error!(Other, "Found meta box but could not downcast to MetaBox"), }, None => io_error!(Other, "No meta box found in HEIF container"), } } fn get_meta_box_mut ( &mut self ) -> Result<&mut MetaBox, std::io::Error> { match self.boxes.iter_mut() .find(|b| b.get_header().get_box_type() == BoxType::meta) { Some(b) => match b.as_any_mut().downcast_mut::() { Some(unboxed) => Ok(unboxed), None => io_error!(Other, "Found meta box but could not downcast to MetaBox (mut)"), }, None => io_error!(Other, "No meta box found in HEIF container"), } } fn get_item_id_exif_data ( &self ) -> Result { if let Ok(meta) = self.get_meta_box() { if let Some(item) = meta.get_item_info_box()?.get_exif_item() { return Ok(item.item_id); } } return io_error!(Other, "No EXIF item found!"); } fn get_exif_data_pos_and_len ( &self, exif_item_id: u16, ) -> Result<(u64, u64), std::io::Error> { let exif_item = match self.get_meta_box() { Ok(meta) => meta.get_item_location_box()?.get_item_location_entry(exif_item_id)?, Err(e) => return Err(e), }; let exif_extents = &exif_item.extents; if exif_extents.len() != 1 { return io_error!(Other, "Expected exactly one EXIF extent info entry"); } match exif_item.get_construction_method() { super::boxes::item_location::ItemConstructionMethod::FILE => { if let Some(first) = exif_extents.first() { return Ok(( first.extent_offset + exif_item.base_offset, first.extent_length )); } else { return io_error!(Other, "Expected one EXIF extent"); } }, super::boxes::item_location::ItemConstructionMethod::IDAT => { return io_error!(Other, "HEIF: item constr. method 'IDAT' currently not supported. Please create a new ticket at https://github.com/TechnikTobi/little_exif with an example image file"); }, super::boxes::item_location::ItemConstructionMethod::ITEM => { return io_error!(Other, "HEIF: item constr. method 'ITEM' currently not supported. Please create a new ticket at https://github.com/TechnikTobi/little_exif with an example image file"); }, } } pub(super) fn get_exif_data ( &self, cursor: &mut T, ) -> Result, std::io::Error> { // Locate exif data let exif_item_id = self.get_item_id_exif_data()?; let (start, length) = self.get_exif_data_pos_and_len(exif_item_id)?; // Reset cursor to start of exif data cursor.seek(std::io::SeekFrom::Start(start))?; // Read in the first 4 bytes, which gives the offset to the start // of the TIFF header and seek to that let exif_tiff_header_offset = read_be_u32(cursor)? as usize; cursor.seek(std::io::SeekFrom::Current(exif_tiff_header_offset as i64))?; if length < 4 + exif_tiff_header_offset as u64 { return io_error!( InvalidData, format!( "EXIF data length ({}) is smaller than expected minimum size ({})", length, 4 + exif_tiff_header_offset as u64 ) ); } // Read in the remaining bytes let mut exif_buffer = vec![0u8; length as usize - 4 // the 4 bytes that store the offset - exif_tiff_header_offset // the actual offset ]; cursor.read_exact(&mut exif_buffer)?; // Stick a EXIF_HEADER in the front let mut full_exif_data = EXIF_HEADER.to_vec(); full_exif_data.append(&mut exif_buffer); return Ok(full_exif_data); } /// Constructs a new version of the exif data area of the HEIF file /// the i64 tells us the delta in bytes. If negative, the new area is /// shorter than the old one, positive if longer #[allow(unused_assignments)] fn construct_new_exif_data_area ( &self, cursor: &mut T, metadata: &Metadata, ) -> Result<(Vec, i64), std::io::Error> { // The buffer containing the new metadata that gets returned let mut new_exif_buffer; // Try to locate the old exif data. let exif_item_id = self.get_item_id_exif_data()?; // Determine the start and length of the previous exif data area let (start, length) = self.get_exif_data_pos_and_len(exif_item_id)?; // If the length is zero, we assume that this is a previously newly // created exif data area, which requires special handling. // If the length is non-zero, there has been exif data before: if length > 0 { // Reset cursor to start of exif data cursor.seek(std::io::SeekFrom::Start(start))?; // Read in all of this area let mut exif_buffer = vec![0u8; length as usize]; cursor.read_exact(&mut exif_buffer)?; // Decode the first 4 bytes, which tells us where to cut off the old // data and replace with the new one let mut local_cursor = Cursor::new(exif_buffer[0..4].to_vec()); let exif_tiff_header_offset = read_be_u32(&mut local_cursor)?; // Cut off data, starting at the old TIFF header and replace with new new_exif_buffer = exif_buffer[0..exif_tiff_header_offset as usize + 4].to_vec(); } else { // Create a new exif header, starting with an empty TIFF header. // new_exif_buffer = 0_u32.to_be_bytes().to_vec(); new_exif_buffer = [0x00, 0x00, 0x00, 0x06].to_vec(); new_exif_buffer.extend(EXIF_HEADER.to_vec()); } // Append the encoded metadata to the old/newly created TIFF header and // compute the delta in length if !metadata.get_ifds().is_empty() { new_exif_buffer.append(&mut metadata.encode()?); } let delta = new_exif_buffer.len() as i64 - length as i64; return Ok(( new_exif_buffer, delta )); } fn get_start_address_for_new_exif_area ( &self ) -> u64 { // Assumes that the new exif area that gets created should start at the // end of the mdat area let mut byte_count = 0; for b in &self.boxes { byte_count += b.get_header().get_box_size(); if b.get_header().get_box_type() == BoxType::mdat { return byte_count as u64; } } return u64::MAX; } pub(super) fn generic_write_metadata ( &mut self, file_buffer: &mut Vec, metadata: & Metadata ) -> Result<(), std::io::Error> { // Find out where old exif is located, needed to determine which iloc // entries need to be updated let mut id = self.get_item_id_exif_data(); // Check that the ID is okay - if not, there is no exif area yet and // we need to create one! if id.is_err() { // What we need to do at this point is // - Create a new item location entry that points to the EXIF data // - Create an item information entry that tells us that the iloc // entry points to EXIF data // - Create an item reference entry that links the EXIF data to the // image/iloc ID #1 -> is this always #1? // Where to put the new exif area let new_exif_start = self.get_start_address_for_new_exif_area(); // If there is no iref box yet, create one so we can find one, // and get the size delta of the new box for extents let mut iref_size_delta = self.get_meta_box_mut()?.create_new_item_reference_box_if_none_exists_yet(); // Acquire the item location, the item information and the item // reference boxes that are inside the meta box. For some reason, // this is not trivial - using e.g. get_item_location_box_mut() // does not work due to (according to the borrow checker) multiple // mutable usages of self let mut iloc_opt = None; let mut iinf_opt = None; let mut iref_opt = None; let meta_mut_ref = self.get_meta_box_mut()?; for other_box in &mut meta_mut_ref.other_boxes { if other_box.get_header().get_box_type() == BoxType::iloc { iloc_opt = other_box .as_any_mut() .downcast_mut::(); } else if other_box.get_header().get_box_type() == BoxType::iinf { iinf_opt = other_box .as_any_mut() .downcast_mut::(); } else if other_box.get_header().get_box_type() == BoxType::iref { iref_opt = other_box .as_any_mut() .downcast_mut::(); } } let iloc = match iloc_opt { Some(v) => v, None => return io_error!(Other, "iloc box should exist"), }; let iinf = match iinf_opt { Some(v) => v, None => return io_error!(Other, "iinf box should exist"), }; let iref = match iref_opt { Some(v) => v, None => return io_error!(Other, "iref box should exist"), }; // Note that the given `new_exif_start` value is based on old // length values (which change due to adding a new item to both the // iloc and iinf boxes) - but this does not matter as this will // be updated anyway later by `add_to_extents` // This way, we don't need any exception during the update procedure let (new_iloc_id, iloc_size_delta) = iloc.create_new_item_location_entry( new_exif_start, 0 ); let iinf_size_delta = iinf.create_new_item_info_entry( new_iloc_id, "Exif" ); iref_size_delta += iref.create_new_single_item_reference_box( "cdsc", // TODO: Check if this is always this type? new_iloc_id, vec![1] // TODO: Check if this is always item #1? ); // Fix the extents in the iloc box iloc.add_to_extents( (iloc_size_delta + iinf_size_delta + iref_size_delta) as i64 ); // Fix up the size of the meta box as well let new_box_size = self.get_meta_box()?.serialize().len() as u64; self.get_meta_box_mut()?.get_header_mut().set_box_size(new_box_size); // No change to the mdat data at this point as we set up the // iloc item so that the exif area currently has a length of zero // Now we have a valid exif area with an iloc ID! id = Ok(new_iloc_id as u16); } // Get position and length of current exif area let (old_exif_pos, old_exif_len) = match &id { Ok(idv) => self.get_exif_data_pos_and_len(*idv)?, Err(_) => (0, 0), }; // Get cursor for file let mut cursor = Cursor::new(file_buffer); // Construct new exif data area let (mut new_exif_area, delta) = self.construct_new_exif_data_area( &mut cursor, metadata )?; let meta_mut = self.get_meta_box_mut()?; for item in &mut meta_mut.get_item_location_box_mut()?.items { // First, check if any extent of this item has the same offset as // the old exif data area. In that case, there must be only one // extent - other cases can't be handled right now if item.extents.iter() .any(|extent| { item.base_offset + extent.extent_offset == old_exif_pos }) { assert!(item.extents.len() == 1, "Expect to have exactly one extent info for EXIF!"); // In case of the EXIF extent information we need to update // the length information, not the offset! let first_extent = match item.extents.first() { Some(f) => f, None => return io_error!(Other, "Expected one extent for EXIF"), }; let new_ext_len = ( first_extent.extent_length as i64 + delta ) as u64; match item.extents.first_mut() { Some(fm) => fm.extent_length = new_ext_len, None => return io_error!(Other, "Expected one extent for EXIF (mut)"), } continue; } if item.get_construction_method() == ItemConstructionMethod::IDAT { // In this case the offset information is relative to the // position of an idat box -> not affected by change in length // of another box continue; } if item.get_construction_method() == ItemConstructionMethod::ITEM { // Offset is relative to another item's extent // Also nothing to do here (for now...) continue; } if item.data_reference_index != 0 { // A value other than 0 implies that this extent refers to // another file, not this one, so we can also skip this // See ISO/IEC 14496-12:2015 § 8.11.3.1, p. 78 continue; } if item.base_offset > delta.unsigned_abs() { // Potentially modify the entire base offset // however, we can only do that if all complete offsets // point to an area after the exif data area // So we need to check that first: if item.extents.iter() .all(|extent| { item.base_offset + extent.extent_offset >= old_exif_pos }) { item.base_offset = (item.base_offset as i64 + delta) as u64; continue; } } // At this point we have no option left but to modify all // individual extent offsets for extent in &mut item.extents { let complete_offset = item.base_offset + extent.extent_offset; if complete_offset > old_exif_pos { extent.extent_offset = (extent.extent_offset as i64 + delta) as u64; } } } // Now we clear the vec and write the boxes to it // Keep track of how many bytes were written so we know when to // replace old exif data with new cursor.get_mut().clear(); let mut written_bytes = 0usize; let mut new_exif_written = false; let end_of_old_exif = (old_exif_pos + old_exif_len) as usize; for iso_box in &mut self.boxes { let mut serialized = iso_box.serialize(); // If this box encompasses the exif data area, update its size and // serialize it again // TODO: As this is not the cleanest approach (e.g. what if the // exif area is not in this top level box but some nested box? // -> requires update of size fields of all boxes "downward") some // other solution needs to be found for this // In the meantime, this should work for the majority of HEIFs if written_bytes + serialized.len() >= end_of_old_exif && !new_exif_written { let new_size = (iso_box.get_header().get_box_size() as i64 + delta) as u64; iso_box.get_header_mut().set_box_size(new_size); serialized = iso_box.serialize(); // Write the serialized box with the OLD exif data cursor.get_mut().extend(&serialized); // Remove old exif data range_remove( cursor.get_mut(), old_exif_pos as usize, (old_exif_pos + old_exif_len) as usize ); // Insert new exif data insert_multiple_at( cursor.get_mut(), old_exif_pos as usize, &mut new_exif_area ); new_exif_written = true; } else { // Just extend with the serialized box contents cursor.get_mut().extend(&serialized); } written_bytes += serialized.len(); } return Ok(()); } pub(super) fn generic_clear_metadata ( &mut self, file_buffer: &mut Vec, ) -> Result<(), std::io::Error> { // Instead of truly clearing the metadata, just write an empty // exif data area // Based on what the macOS shortcut is doing, only keeps the tags // 0x0112: Orientation // 0x011A: XResolution // 0x011B: YResolution // 0x0128: ResolutionUnit // Note: It is up for debate whether keeping this information is wanted // or not/this should write a truly empty exif area // Create cursor let mut cursor = Cursor::new(file_buffer); // Read original metadata let orig_metadata = Metadata::general_decoding_wrapper( self.get_exif_data(&mut cursor) )?; // Construct new metadata that only contains the above tags let mut new_metadata = Metadata::new(); // 0x0112: Orientation if let Some(tag) = orig_metadata.get_tag_by_hex(0x0112, None).next() { new_metadata.set_tag(tag.clone()); } // 0x011A: XResolution if let Some(tag) = orig_metadata.get_tag_by_hex(0x011A, None).next() { new_metadata.set_tag(tag.clone()); } // 0x011A: YResolution if let Some(tag) = orig_metadata.get_tag_by_hex(0x011B, None).next() { new_metadata.set_tag(tag.clone()); } // 0x0128: ResolutionUnit if let Some(tag) = orig_metadata.get_tag_by_hex(0x0128, None).next() { new_metadata.set_tag(tag.clone()); } return self.generic_write_metadata(cursor.get_mut(), &new_metadata); } } little_exif-0.6.23/src/heif/mod.rs000064400000000000000000000102251046102023000150530ustar 00000000000000// Copyright © 2025-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details // Note: While the standard 14496-12 (which defines the base ISO BMFF stuff // but with focus on video files) states that a `moov` box is *required* on // top level, the Image File Format standard 23008-12 tells us that files with // the brand `mif1` do *not* require such a box. mod box_type; mod box_header; mod boxes; mod container; use std::io::Cursor; use std::io::Read; use std::io::Seek; use std::io::Write; use std::path::Path; use crate::general_file_io::open_read_file; use crate::general_file_io::open_write_file; use crate::general_file_io::EXIF_HEADER; use crate::metadata::Metadata; use crate::heif::boxes::read_next_box; use crate::heif::container::HeifContainer; fn generic_read_metadata ( cursor: &mut T ) -> Result, std::io::Error> { let container = HeifContainer::construct_from_cursor_unboxed(cursor)?; return container.get_exif_data(cursor); } pub(crate) fn read_metadata ( file_buffer: &[u8] ) -> Result, std::io::Error> { let mut cursor = Cursor::new(file_buffer); return generic_read_metadata(&mut cursor); } pub(crate) fn file_read_metadata ( path: &Path ) -> Result, std::io::Error> { let mut file = open_read_file(path)?; return generic_read_metadata(&mut file); } pub(crate) fn write_metadata ( file_buffer: &mut Vec, metadata: &Metadata ) -> Result<(), std::io::Error> { let mut cursor = Cursor::new(file_buffer); let mut container = HeifContainer::construct_from_cursor_unboxed(&mut cursor)?; return container.generic_write_metadata(cursor.get_mut(), metadata); } pub(crate) fn file_write_metadata ( path: &Path, metadata: &Metadata ) -> Result<(), std::io::Error> { // Load the entire file into memory instead of performing multiple read, // seek and write operations let mut file = open_write_file(path)?; let mut file_buffer: Vec = Vec::new(); file.read_to_end(&mut file_buffer)?; let mut cursor = Cursor::new(file_buffer); let mut container = HeifContainer::construct_from_cursor_unboxed(&mut cursor)?; container.generic_write_metadata(cursor.get_mut(), metadata)?; // Seek back to start, write the file and adjust its length, possibly // truncating the file if new contents are shorter file.seek(std::io::SeekFrom::Start(0))?; file.write_all(cursor.get_ref())?; file.set_len(cursor.get_ref().len() as u64)?; return Ok(()); } /// Encodes the given metadata into a vector of bytes that can be used as /// an exif box in an HEIF file. pub(crate) fn as_u8_vec ( general_encoded_metadata: &[u8], ) -> Vec { let mut data_buffer: Vec = Vec::new(); // Length of the EXIF HEADER data_buffer.extend(vec![0u8, 0u8, 0u8, 6u8]); // Actual EXIF HEADER data_buffer.extend(EXIF_HEADER.iter()); // And the exif data itself data_buffer.extend(general_encoded_metadata.iter()); return data_buffer; } pub(crate) fn clear_metadata ( file_buffer: &mut Vec ) -> Result<(), std::io::Error> { let mut cursor = Cursor::new(file_buffer); let mut container = HeifContainer::construct_from_cursor_unboxed(&mut cursor)?; return container.generic_clear_metadata(cursor.get_mut()); } pub(crate) fn file_clear_metadata ( path: &Path ) -> Result<(), std::io::Error> { // Load the entire file into memory instead of performing multiple read, // seek and write operations let mut file = open_write_file(path)?; let mut file_buffer: Vec = Vec::new(); file.read_to_end(&mut file_buffer)?; let mut cursor = Cursor::new(file_buffer); let mut container = HeifContainer::construct_from_cursor_unboxed(&mut cursor)?; container.generic_clear_metadata(cursor.get_mut())?; // Seek back to start, write the file and adjust its length, possibly // truncating the file if new contents are shorter file.seek(std::io::SeekFrom::Start(0))?; file.write_all(cursor.get_ref())?; file.set_len(cursor.get_ref().len() as u64)?; return Ok(()); } little_exif-0.6.23/src/ifd/get.rs000064400000000000000000000024161046102023000147050ustar 00000000000000// Copyright © 2024 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use crate::exif_tag::ExifTag; use super::ExifTagGroup; use super::ImageFileDirectory; impl ImageFileDirectory { pub fn get_tags ( &self ) -> &Vec { return &self.tags; } pub fn get_generic_ifd_nr ( &self ) -> u32 { return self.belongs_to_generic_ifd_nr; } pub fn get_ifd_type ( &self ) -> ExifTagGroup { return self.ifd_type; } pub fn get_offset_tag_for_parent_ifd ( &self ) -> Option<(ExifTagGroup, ExifTag)> { match self.ifd_type { ExifTagGroup::GENERIC => None, ExifTagGroup::EXIF => Some((ExifTagGroup::GENERIC, ExifTag::ExifOffset( Vec::new()))), ExifTagGroup::GPS => Some((ExifTagGroup::GENERIC, ExifTag::GPSInfo( Vec::new()))), ExifTagGroup::INTEROP => Some((ExifTagGroup::EXIF, ExifTag::InteropOffset(Vec::new()))), } } pub fn get_ifd_type_for_offset_tag ( tag: &ExifTag ) -> Option { match tag { ExifTag::ExifOffset(_) => Some(ExifTagGroup::EXIF), ExifTag::GPSInfo(_) => Some(ExifTagGroup::GPS), ExifTag::InteropOffset(_) => Some(ExifTagGroup::INTEROP), _ => None } } }little_exif-0.6.23/src/ifd/mod.rs000064400000000000000000000653101046102023000147070ustar 00000000000000// Copyright © 2024-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details pub mod get; pub mod set; use std::io::Cursor; use std::io::Read; use std::io::Seek; use std::vec; use crate::endian::*; use crate::exif_tag::decode::decode_tag_with_format_exceptions; use crate::exif_tag::ExifTag; use crate::exif_tag::TagType; use crate::exif_tag_format::ExifTagFormat; use crate::general_file_io::io_error; use crate::metadata::Metadata; use crate::u8conversion::from_u8_vec_res_macro; use crate::u8conversion::to_u8_vec_macro; use crate::u8conversion::U8conversion; /// Useful constants for dealing with IFDs: The length of a single IFD entry is /// equal to 12 bytes, as the entry consists of the tags hex value (2 byte), /// the format (2 byte), the number of components (4 byte) and the value/offset /// section (4 byte). /// The four zeros tell us that this is the last IFD in its sequence and there /// is no link to another IFD const IFD_ENTRY_LENGTH: u32 = 12; const IFD_END_NO_LINK: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; /// The different types of Image File Directories (IFD). A generic IFD is one /// without further specialization, like e.g. IFD0. The generic IFDs start /// with IFD0, which is located via the offset at the start of the TIFF data. /// The next IFD (in this case: IFD1) is then located via the link offset at /// the end of IFD0. /// Other IFDs, like e.g. the ExifIFD, are linked via offset tags (in case of /// the ExifIFD offset: 0x8769) that are located in the respective generic IFD /// (most of them in IFD0). #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd)] #[allow(non_snake_case, non_camel_case_types)] pub enum ExifTagGroup { GENERIC, EXIF, INTEROP, // MAKERNOTES, // TODO: Decide what to do with maker notes stuff... GPS, } /// The value of `belongs_to_generic_ifd_nr` tells us what generic IFD this /// specific IFD belongs to, e.g. `0` would indicate that it belongs (or is) /// IFD0. #[derive(Clone, Debug)] pub struct ImageFileDirectory { tags: Vec, ifd_type: ExifTagGroup, belongs_to_generic_ifd_nr: u32, } impl ImageFileDirectory { pub fn new_with_tags ( tags: Vec, group: ExifTagGroup, nr: u32 ) -> Self { ImageFileDirectory { tags: tags, ifd_type: group, belongs_to_generic_ifd_nr: nr } } /// Sorts the tags according to their hex value /// See TIFF 6.0 Specification: "The entries in an IFD must be sorted in /// ascending order by Tag." (page 15/121) pub(crate) fn sort_tags ( &mut self ) { self.tags.sort_by_key(|tag| tag.as_u16()); } /// If everything goes Ok and there is enough data to unpack, this returns /// the offset to the next generic IFD that needs to be processed. pub(crate) fn decode_ifd ( data_cursor: &mut Cursor<&Vec>, data_begin_position: u64, // Stays the same for all calls to this function while decoding endian: & Endian, group: & ExifTagGroup, generic_ifd_nr: u32, // Reuse value for recursive calls; only gets incremented by caller insert_into: &mut Vec, // Stays the same for all calls to this function while decoding ) -> Result, std::io::Error> { //////////////////////////////////////////////////////////////////////// // PREPARATION // Backup the entry position where this IFD started let data_cursor_entry_position = data_cursor.position(); // Check if there is enough data to decode an IFD if (data_cursor.get_ref().len() as i64 - data_cursor_entry_position as i64) < 6i64 { return Ok(None); } // The first two bytes give us the number of entries in this IFD let mut number_of_entries_buffer = vec![0u8; 2]; data_cursor.read_exact(&mut number_of_entries_buffer)?; let number_of_entries = from_u8_vec_res_macro!(u16, &number_of_entries_buffer, endian)?; // Check that there is enough data to unpack let required = 0 + 2 + IFD_ENTRY_LENGTH as usize * number_of_entries as usize + IFD_END_NO_LINK.len(); let available = (0 + data_cursor.get_ref().len() as i64 - data_cursor_entry_position as i64) as usize; if required > available { return io_error!(Other, format!("Not enough data to decode IFD! Required: {} Available: {}", required, available)); } // Temporarily storing specific tags that have been decoded // This has to do with data offset tags that are interconnected with // other tags. // For example, for decoding the StripOffsets we also need the // StripByteCounts to know how many bytes each strip has let mut strip_tags: (Option, Option) = (None, None); let mut thumbnail_info: (Option, Option) = (None, None); // Others following here in the future... //////////////////////////////////////////////////////////////////////// // TAG-DECODING // Storing all tags while decoding let mut tags = Vec::new(); // loop through the entries - assumes that the value stored in // `number_of_entries` is correct for _ in 0..number_of_entries { // Read the entry into a buffer let mut entry_buffer = vec![0u8; IFD_ENTRY_LENGTH as usize]; data_cursor.read_exact(&mut entry_buffer)?; // Decode the first 8 bytes with the tag, format and component number let hex_tag = from_u8_vec_res_macro!(u16, &entry_buffer[0..2], endian)?; let hex_format = from_u8_vec_res_macro!(u16, &entry_buffer[2..4], endian)?; let hex_component_number = from_u8_vec_res_macro!(u32, &entry_buffer[4..8], endian)?; // Decode the format // TODO: What to do in case these two differ but the given format // can be casted into the expected one, e.g. R64U to R64S? let format; if let Some(decoded_format) = ExifTagFormat::from_u16(hex_format) { format = decoded_format; } else { return io_error!(Other, format!("Illegal format value: {}", hex_format)); } // Calculating the number of required bytes to determine if next // 4 bytes are data or an offset to data // Note: It is expected that the format here is "correct" in the // sense that it tells us whether or not an offset is used for the // data even if the given format in the image file is not the // right/default one for the currently processed tag according to // the exif specification. let Some(byte_count) = format.bytes_per_component().checked_mul(hex_component_number) else { return io_error!(Other, format!("Byte count overflow for tag 0x{:04x}!", hex_tag)); }; let raw_data; if byte_count > 4 { // Compute the offset let hex_offset = from_u8_vec_res_macro!(u32, &entry_buffer[8..12], endian)?; // Backup current position & go to offset position let backup_position = data_cursor.position(); data_cursor.set_position(data_begin_position); data_cursor.seek(std::io::SeekFrom::Current(hex_offset as i64))?; // Read the raw data let mut raw_data_buffer = vec![0u8; byte_count as usize]; data_cursor.read_exact(&mut raw_data_buffer)?; raw_data = raw_data_buffer.to_vec(); // Rewind the cursor to the start of the next entry data_cursor.set_position(backup_position); } else { // The 4 bytes are the actual data // Note: This may actually be *less* than 4 bytes! raw_data = entry_buffer[8..(8+byte_count as usize)].to_vec(); } // Try to get the tag via its hex value let tag_result = ExifTag::from_u16(hex_tag, group); // Start of by checking if this is an unknown tag if tag_result.is_err() { // Note: `from_u16_with_data` can NOT be called initially due // to some possible conversion of data needed, e.g. INT16U to // INT32U, which is not accounted for yet at this stage match ExifTag::from_u16_with_data( hex_tag, &format, &raw_data, endian, group ) { Ok(tag) => tags.push(tag), Err(e) => return io_error!( Other, format!( "Could not construct unknown tag 0x{:04x}: {}", hex_tag, e ) ), } continue; } // We should now be able to safely unwrap the tag let mut tag = match tag_result { Ok(tag) => tag, Err(e) => return io_error!(Other, e) }; // If this is an IFD offset tag, perform a recursive call if let TagType::IFD_OFFSET(subifd_group) = tag.get_tag_type() { // Compute the offset to the SubIFD and save the current position let offset = from_u8_vec_res_macro!(u32, &raw_data, endian)? as usize; let backup_position = data_cursor.position(); // Go to the SubIFD offset and decode that data_cursor.set_position(data_begin_position); data_cursor.seek(std::io::SeekFrom::Current(offset as i64))?; let subifd_decode_result = Self::decode_ifd( data_cursor, data_begin_position, endian, &subifd_group, generic_ifd_nr, insert_into, ); // Check that this actually worked if let Ok(_subifd_result) = subifd_decode_result { // Assert result, restore old cursor position & continue // Disabled assert as of issue #31 // The idea behind this assert was that, as we are decoding // a SubIFD, there shouldn't be a link after the last entry // to another IFD and those 4 bytes are expected to be zero // and we get a Ok(None) from the recursive call back. // assert_eq!(subifd_result, None); // However, it is possible that those 4 bytes don't exist // at all and they are part of some offset data, possibly // even from another IFD! // So, for now we just assume that `subifd_result` is not // of relevance until evidence suggests otherwise. data_cursor.set_position(backup_position); continue; } else if let Err(decode_err) = subifd_decode_result { return io_error!( Other, format!( "Could not decode SubIFD {subifd_group:?}:\n {decode_err:?}" ) ); } } // At this point we check if the format is actually what we expect // it to be and convert it if possible tag = decode_tag_with_format_exceptions( &tag, format, &raw_data, endian, hex_tag, group )?; // Now we have at least confirmed that the format is ok (or has // been corrected). Next, we need to differ between the two other // tag types: if let TagType::DATA_OFFSET(_) = tag.get_tag_type() { match tag { ExifTag::StripOffsets(_, _) => { strip_tags.0 = Some(tag); }, ExifTag::StripByteCounts(_) => { strip_tags.1 = Some(tag); }, ExifTag::ThumbnailOffset(_, _) => { thumbnail_info.0 = Some(tag); }, ExifTag::ThumbnailLength(_) => { thumbnail_info.1 = Some(tag); }, _ => () } // do NOT push these tags to the tags vector yet! } else // TagType::VALUE { // Simply push this tag onto the vector tags.push(tag); } } // end of for-loop //////////////////////////////////////////////////////////////////////// // POST TAG-DECODING // At this stage we have decoded the tags themselves. // However, the data offset tags need further processing (i.e. their // data needs to be read as well) if let (Some(strip_tags_0), Some(strip_tags_1)) = strip_tags { // 0 -> offsets // 1 -> byte counts if let ( TagType::DATA_OFFSET(offsets), TagType::DATA_OFFSET(byte_counts) ) = ( strip_tags_0.get_tag_type(), strip_tags_1.get_tag_type() ) { let backup_position = data_cursor.position(); let mut strip_data = Vec::new(); // Gather the data from the offsets for (offset, byte_count) in offsets.iter().zip(byte_counts.iter()) { data_cursor.set_position(data_begin_position); data_cursor.seek(std::io::SeekFrom::Current(*offset as i64))?; let mut data_buffer = vec![0u8; *byte_count as usize]; data_cursor.read_exact(&mut data_buffer)?; strip_data.push(data_buffer); } // Push StripOffset tag to tags vector tags.push(ExifTag::StripOffsets(Vec::new(), strip_data)); // Push StripByteCounts tag to tags vector tags.push(ExifTag::StripByteCounts(byte_counts)); // Restore backup position data_cursor.set_position(backup_position); } } if let (Some(thumbnail_info_0), Some(thumbnail_info_1)) = thumbnail_info { // 0 -> offset // 1 -> length if let ( TagType::DATA_OFFSET(offset), TagType::DATA_OFFSET(length) ) = ( thumbnail_info_0.get_tag_type(), thumbnail_info_1.get_tag_type() ) { let backup_position = data_cursor.position(); if offset.len() == 1 && length.len() == 1 { let mut thumbnail_data = vec![0u8; length[0] as usize]; // Gather the data at the offset data_cursor.set_position(data_begin_position); data_cursor.seek(std::io::SeekFrom::Current(offset[0] as i64))?; data_cursor.read_exact(&mut thumbnail_data)?; // Push ThumbnailOffset tag to tags vector tags.push(ExifTag::ThumbnailOffset(Vec::new(), thumbnail_data)); // Also push ThumbnailLength tag to tags vector tags.push(ExifTag::ThumbnailLength(length)); } else { log::warn!("Can't decode thumbnail! The ThumbnailOffset and ThumbnailLength tags are expected to contain exactly 1 INT32U value. However, they have {} and {} values.", offset.len(), length.len()); } // Restore backup position data_cursor.set_position(backup_position); } } // Other offset tags here in the future... // At this point we are done with decoding the tags of this IFD and its // associated SubIFDs! // Put the current IFD into the given, referenced vector insert_into.push(ImageFileDirectory { tags: tags, ifd_type: *group, belongs_to_generic_ifd_nr: generic_ifd_nr }); // Read in the link to the next IFD and check if its zero let mut next_ifd_link_buffer = vec![0u8; 4]; if data_cursor.read_exact(&mut next_ifd_link_buffer).is_err() { // Covers the case that this IFD is stored at the very end of the // file and its a SubIFD that has no link at all return Ok(None); } let link_is_zero = next_ifd_link_buffer.iter() .zip(IFD_END_NO_LINK.iter()) .filter(|&(read, constant)| read == constant) .count() == IFD_END_NO_LINK.len(); if link_is_zero { return Ok(None); } return Ok(Some(from_u8_vec_res_macro!(u32, &next_ifd_link_buffer, endian)?)); } /// Recursively encodes IFDs /// Returns /// - an index position where the 4 bytes for the link to the next IFD are located /// - the offset of the encoded IFD, to be used for linking to this IFD pub(crate) fn encode_ifd ( &self, data: &Metadata, ifds_with_offset_info_only: &mut Vec, encode_vec: &mut Vec, current_offset: &mut u32 ) -> Result<(u64, Vec), std::io::Error> { // Store all relevant tags (IFD tags + offset tags) in a temporary // location and sort them there let mut all_relevant_tags = self.tags.clone(); if let Some(ifd_with_offset_info_only) = ifds_with_offset_info_only .iter() .find(|ifd| ifd.get_generic_ifd_nr() == self.get_generic_ifd_nr() && ifd.get_ifd_type() == self.get_ifd_type() ) { all_relevant_tags.extend(ifd_with_offset_info_only.get_tags().iter().cloned()); } // Start writing this IFD by adding the number of entries let count_entries = all_relevant_tags.iter().filter( |tag| tag.is_writable() || matches!(tag.get_tag_type(), TagType::IFD_OFFSET(_) | TagType::DATA_OFFSET(_) ) ).count() as u16; encode_vec.extend(to_u8_vec_macro!(u16, &count_entries, &data.get_endian()).iter()); // Remember the current offset as this is needed to address this IFD // and link to it from other IFDs let ifd_offset = *current_offset; let ifd_offset_vec = to_u8_vec_macro!(u32, &ifd_offset, &data.get_endian()); // Advance offset address to the point after the entries and provide // offset area vector *current_offset += 0 + 2 // length of entry count section + IFD_ENTRY_LENGTH * count_entries as u32 + IFD_END_NO_LINK.len() as u32 ; let mut ifd_offset_area: Vec; // Ensure that offset data is aligned properly let alignment_count = (4 - *current_offset % 4) % 4; *current_offset += alignment_count; ifd_offset_area = vec![0u8; alignment_count as usize]; // Write directory entries to the vector for tag in &all_relevant_tags { // Skip tags that can't be written if !tag.is_writable() { // But don't skip tags that describe offsets to IFDs or Data! if let TagType::IFD_OFFSET(_) = tag.get_tag_type() {} else if let TagType::DATA_OFFSET(_) = tag.get_tag_type() {} else { continue; } } // Need to differentiate at this stage as we have to access e.g. the // StripOffsets that are stored in a local vec let value = match tag.get_tag_type() { TagType::VALUE => { tag.value_as_u8_vec(&data.get_endian()) }, TagType::DATA_OFFSET(_) => { match tag { ExifTag::StripOffsets(_, strip_data) => { let mut value = Vec::new(); for strip in strip_data { // Store the current offset where the strip is // pushed, push the strip and account for its length // in the offset variable value.extend( to_u8_vec_macro!(u32, ¤t_offset.clone(), &data.get_endian()) ); ifd_offset_area.extend(strip); *current_offset += strip.len() as u32; } value }, ExifTag::ThumbnailOffset(_, thumbnail_data) => { let value = to_u8_vec_macro!(u32, ¤t_offset.clone(), &data.get_endian()); ifd_offset_area.extend(thumbnail_data); *current_offset += thumbnail_data.len() as u32; value }, _ => tag.value_as_u8_vec(&data.get_endian()), } } TagType::IFD_OFFSET(_) => { if let Some(group) = Self::get_ifd_type_for_offset_tag(tag) { // Find that IFD in the parent struct and encode that if let Some(found_ifd) = data.get_ifds() .iter() .find(|ifd| ifd.get_generic_ifd_nr() == self.get_generic_ifd_nr() && ifd.get_ifd_type() == group ) { let (_, subifd_offset) = found_ifd.encode_ifd( data, ifds_with_offset_info_only, &mut ifd_offset_area, current_offset )?; subifd_offset } else { return io_error!(Other, format!("Could not find SubIFD {group:?} for offset tag 0x{:04x}!", tag.as_u16())); } } else { return io_error!(Other, format!("Could not determine SubIFD type for offset tag 0x{:04x}!", tag.as_u16())); } } }; // Re-align let alignment_count = (4 - *current_offset % 4) % 4; *current_offset += alignment_count; ifd_offset_area.extend(vec![0u8; alignment_count as usize]); // Add Tag & Data Format / 2 + 2 bytes encode_vec.extend(to_u8_vec_macro!(u16, &tag.as_u16(), &data.get_endian()).iter()); encode_vec.extend(to_u8_vec_macro!(u16, &tag.format().as_u16(), &data.get_endian()).iter()); // Add number of components / 4 bytes let number_of_components: u32 = tag.number_of_components(); encode_vec.extend(to_u8_vec_macro!(u32, &number_of_components, &data.get_endian()).iter()); // Optional string padding (i.e. string is shorter than it should be) let mut string_padding: Vec = Vec::new(); if tag.is_string() { string_padding = vec![ 0x00; number_of_components as usize - value.len() ]; } // Add offset or value / 4 bytes // Depending on the amount of data, either put it directly into // next 4 bytes or write an offset where the data can be found let byte_count: u32 = number_of_components * tag.format().bytes_per_component(); if byte_count > 4 { encode_vec.extend(to_u8_vec_macro!(u32, current_offset, &data.get_endian()).iter()); ifd_offset_area.extend(value.iter()); ifd_offset_area.extend(string_padding.iter()); *current_offset += byte_count; // Re-align let alignment_count = (4 - *current_offset % 4) % 4; *current_offset += alignment_count; ifd_offset_area.extend(vec![0u8; alignment_count as usize]); } else { let pre_length = encode_vec.len(); encode_vec.extend(value.iter()); encode_vec.extend(string_padding.iter()); let post_length = encode_vec.len(); // Make sure that this area is indeed *exactly* 4 bytes long for _ in 0..(4-(post_length - pre_length) ) { encode_vec.push(0x00); } } } // Write link and offset data encode_vec.extend(IFD_END_NO_LINK.iter()); encode_vec.extend(ifd_offset_area.iter()); return Ok(((ifd_offset + 2 + IFD_ENTRY_LENGTH * count_entries as u32) as u64, ifd_offset_vec)); } } little_exif-0.6.23/src/ifd/set.rs000064400000000000000000000024521046102023000147210ustar 00000000000000// Copyright © 2024-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use crate::exif_tag::ExifTag; use super::ImageFileDirectory; impl ImageFileDirectory { /// Sets the value of an image file directory. Checks if the group of the /// IFD and the default group of the tag match and prints a warning /// otherwise. /// If the tag already exists in the IFD, it is replaced by the given tag. /// All tags in the IFD are sorted after the insert. pub fn set_tag ( &mut self, input_tag: ExifTag, ) { if input_tag.get_group() != self.ifd_type { log::warn!("The tag {input_tag:?} is set in an IFD that has not a matching group."); } self.tags.retain(|tag| tag.as_u16() != input_tag.as_u16()); self.tags.push(input_tag); self.sort_tags(); } /// Removes a tag with a given hex value from the image file directory. /// If the tag is removed successfully, nothing happens. /// If no such tag exists, nothing happens. pub fn remove_tag ( &mut self, tag_hex: u16 ) { self.tags.retain(|tag| tag.as_u16() != tag_hex); self.sort_tags(); } } little_exif-0.6.23/src/jpg.rs000064400000000000000000000366061046102023000141540ustar 00000000000000// Copyright © 2022-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::fs::File; use std::io::BufReader; use std::io::Cursor; use std::io::Seek; use std::io::SeekFrom; use std::io::Read; use std::io::Write; use std::path::Path; use crate::endian::Endian; use crate::metadata::Metadata; use crate::u8conversion::*; use crate::general_file_io::*; pub(crate) const JPG_SIGNATURE: [u8; 2] = [0xff, 0xd8]; const JPG_MARKER_PREFIX: u8 = 0xff; const JPG_APP1_MARKER: u16 = 0xffe1; fn encode_metadata_jpg ( exif_vec: &[u8], ) -> Vec { // vector storing the data that will be returned let mut jpg_exif: Vec = Vec::new(); // Compute the length of the exif data (includes the two bytes of the // actual length field) let length = 2u16 + (EXIF_HEADER.len() as u16) + (exif_vec.len() as u16); // Start with the APP1 marker and the length of the data // Then copy the previously encoded EXIF data jpg_exif.extend(to_u8_vec_macro!(u16, &JPG_APP1_MARKER, &Endian::Big)); jpg_exif.extend(to_u8_vec_macro!(u16, &length, &Endian::Big)); jpg_exif.extend(EXIF_HEADER.iter()); jpg_exif.extend(exif_vec.iter()); return jpg_exif; } fn check_signature ( file_buffer: &[u8], ) -> Result<(), std::io::Error> { if !file_buffer.starts_with(&JPG_SIGNATURE) { return io_error!(InvalidData, "Can't open JPG file - Wrong signature!"); } // Signature is valid - can proceed using as JPG file return Ok(()); } fn file_check_signature ( path: &Path ) -> Result { let mut file = open_read_file(path)?; // Read & check the signature let mut signature_buffer = [0u8; 2]; let bytes_read = file.read(&mut signature_buffer)?; if bytes_read != 2 { return io_error!(InvalidData, "Can't open JPG file - Can't read signature!"); } check_signature(&signature_buffer)?; // Signature is valid - can proceed using the file as JPG file return Ok(file); } pub(crate) fn clear_metadata ( file_buffer: &mut Vec, ) -> Result<(), std::io::Error> { return clear_segment(file_buffer, 0xe1, Some(&EXIF_HEADER)); } pub(crate) fn clear_segment ( file_buffer: &mut Vec, segment_marker: u8, prefix_bytes: Option<&[u8]>, ) -> Result<(), std::io::Error> { check_signature(file_buffer)?; // Setup of variables necessary for going through the file let mut byte_buffer = [0u8; 1]; // A buffer for reading in a byte of data from the file let mut previous_byte_was_marker_prefix = false; // A boolean for remembering if the previous byte was a marker prefix (0xFF) let mut cursor = Cursor::new(file_buffer); // Skip 0xFFD8 at the start cursor.seek(SeekFrom::Current(2))?; loop { // Read next byte into buffer if let Err(e) = cursor.read_exact(&mut byte_buffer) { if e.kind() == std::io::ErrorKind::UnexpectedEof { // Reached end of file without encountering EOI marker 0xd9 // See issue #93 for examples where this happens return Ok(()); } else { return Err(e); } } if previous_byte_was_marker_prefix { // Check if this is the end of the file. In that case, the length // data can't be read and we need to return prematurely. if byte_buffer[0] == 0xd9 // EOI marker { // No more data to read in return Ok(()); } // Read in the length of the segment // (which follows immediately after the marker) let mut length_buffer = [0u8; 2]; cursor.read_exact(&mut length_buffer)?; // Decode the length to determine how much more data there is let length = from_u8_vec_res_macro!(u16, &length_buffer, &Endian::Big)?; let remaining_length = (length - 2) as usize; if byte_buffer[0] == segment_marker // Given marker, e.g. for APP1 { // Backup current position, account for the 4 bytes already read let backup_position = cursor.position() - 4; let mut do_not_delete_segment_override = false; // If we are given prefix bytes to check for, read them in if let Some(prefix) = prefix_bytes { let mut prefix_buffer = vec![0u8; prefix.len()]; cursor.read_exact(&mut prefix_buffer)?; cursor.seek(SeekFrom::Current(-(prefix_buffer.len() as i64)))?; // We are given a prefix but don't know yet if it matches // So set the override to true for now as we might have to // skip deleting this segment if we don't get a match do_not_delete_segment_override = true; // Only delete the segment if the prefix matches if prefix_buffer.as_slice() == prefix { do_not_delete_segment_override = false; } } // Skip the segment cursor.seek(SeekFrom::Current(remaining_length as i64))?; // Overwrite segment only if we are allowed to if !do_not_delete_segment_override { // Copy data from there onwards into a buffer let mut temp_buffer = Vec::new(); cursor.read_to_end(&mut temp_buffer)?; // Overwrite segment cursor.set_position(backup_position); cursor.write_all(&temp_buffer)?; // Cut off right-most bytes that are now duplicates due // to the previous shift-to-left operation let cutoff_index = 0 + backup_position as usize + temp_buffer.len(); cursor.get_mut().truncate(cutoff_index); // Seek to start of next segment cursor.set_position(backup_position); } } else if byte_buffer[0] == 0xda { // See `generic_read_metadata` cursor.seek(SeekFrom::Current(remaining_length as i64))?; if let Err(e) = skip_ecs(&mut cursor) { if e.kind() == std::io::ErrorKind::UnexpectedEof { // Again, same as the check above, we have reached end // of file without encountering EOI marker 0xd9 // See issue #93 for examples where this happens return Ok(()); } else { return Err(e); } } } else { // Skip this segment cursor.seek(SeekFrom::Current(remaining_length as i64))?; } previous_byte_was_marker_prefix = false; } else { previous_byte_was_marker_prefix = byte_buffer[0] == JPG_MARKER_PREFIX; } } } pub(crate) fn file_clear_segment ( path: &Path, segment_marker: u8, prefix_bytes: Option<&[u8]>, ) -> Result<(), std::io::Error> { // Load the entire file into memory instead of reading one byte at a time // to improve the overall speed // Thanks to Xuf3r for this improvement! let mut file_buffer: Vec = std::fs::read(path)?; // Clear the metadata in the APP1 segment from the file buffer clear_segment(&mut file_buffer, segment_marker, prefix_bytes)?; // Write the file // Possible to optimize further by returning the purged bytestream itself? let mut file = std::fs::OpenOptions::new().write(true).truncate(true).open(path)?; file.write_all(&file_buffer)?; return Ok(()); } pub(crate) fn file_clear_metadata ( path: &Path ) -> Result<(), std::io::Error> { return file_clear_segment(path, 0xe1, Some(&EXIF_HEADER)); } /// Provides the JPEG specific encoding result as vector of bytes to be used /// by the user (e.g. in combination with another library) pub(crate) fn as_u8_vec ( general_encoded_metadata: &[u8], ) -> Vec { encode_metadata_jpg(general_encoded_metadata) } pub(crate) fn write_metadata ( file_buffer: &mut Vec, metadata: &Metadata ) -> Result<(), std::io::Error> { // Remove old metadata clear_metadata(file_buffer)?; // Encode the data specifically for JPG let mut encoded_metadata = encode_metadata_jpg(&metadata.encode()?); // Insert the metadata right after the signature crate::util::insert_multiple_at(file_buffer, 2, &mut encoded_metadata); return Ok(()); } /// Writes the given generally encoded metadata to the JP(E)G image file at /// the specified path. /// Note that any previously stored metadata under the APP1 marker gets removed /// first before writing the "new" metadata. pub(crate) fn file_write_metadata ( path: &Path, metadata: &Metadata ) -> Result<(), std::io::Error> { // Load the entire file into memory instead of performing multiple read, // seek and write operations let mut file = open_write_file(path)?; let mut file_buffer: Vec = Vec::new(); file.read_to_end(&mut file_buffer)?; // Writes the metadata to the file_buffer vec // The called function handles the removal of old metadata and the JPG // specific encoding, so we pass only the generally encoded metadata here write_metadata(&mut file_buffer, metadata)?; // Seek back to start & write the file file.seek(SeekFrom::Start(0))?; file.write_all(&file_buffer)?; return Ok(()); } pub(crate) fn read_metadata ( file_buffer: &Vec ) -> Result, std::io::Error> { check_signature(file_buffer)?; let mut cursor = Cursor::new(file_buffer); // Skip signature cursor.set_position(2); return generic_read_metadata(&mut cursor); } pub(crate) fn file_read_metadata ( path: &Path ) -> Result, std::io::Error> { // Use a buffered reader to speed up operations, see issue #21 let mut buffered_file = BufReader::new(file_check_signature(path)?); return generic_read_metadata(&mut buffered_file); } /// Skips the entropy-coded segment (ECS) that is followed by a start of scan /// segment (SOS) and positions the cursor at the start of the next segment, /// i.e. a 0xFF byte that is followed by a marker that is NOT 0xD0-0xD7 or 0x00. /// Assumes that the given cursor is positioned at the start of the ECS fn skip_ecs ( cursor: &mut T ) -> Result<(), std::io::Error> { let mut byte_buffer = [0u8; 1]; // A buffer for reading in a byte of data from the file let mut previous_byte_was_marker_prefix = false; // A boolean for remembering if the previous byte was a marker prefix (0xFF) loop { // Read next byte into buffer cursor.read_exact(&mut byte_buffer)?; if previous_byte_was_marker_prefix { match byte_buffer[0] { 0xd0 | 0xd1 | 0xd2 | 0xd3 | 0xd4 | 0xd5 | 0xd6 | 0xd7 | 0x00 => { // Do nothing }, _ => { // Position back to where the 0xFF byte is located cursor.seek(SeekFrom::Current(-2))?; return Ok(()); }, } previous_byte_was_marker_prefix = false; } else { previous_byte_was_marker_prefix = byte_buffer[0] == JPG_MARKER_PREFIX; } } } fn generic_read_metadata ( cursor: &mut T ) -> Result, std::io::Error> { // Setup of variables necessary for going through the data let mut byte_buffer = [0u8; 1]; // A buffer for reading in a byte of data from the file let mut previous_byte_was_marker_prefix = false; // A boolean for remembering if the previous byte was a marker prefix (0xFF) loop { // Read next byte into buffer cursor.read_exact(&mut byte_buffer)?; if previous_byte_was_marker_prefix { // Check if this is the end of the file. In that case, the length // data can't be read and we need to return prematurely. // This is why this case can't be included in the match afterwards. if byte_buffer[0] == 0xd9 // EOI marker { // No more data to read in return io_error!(Other, "No EXIF data found!"); } // Read in the length of the segment // (which follows immediately after the marker) let mut length_buffer = [0u8; 2]; cursor.read_exact(&mut length_buffer)?; // Decode the length to determine how much more data there is let length = from_u8_vec_res_macro!(u16, &length_buffer, &Endian::Big)?; if length < 2 { return io_error!(InvalidData, "Mangled JPG data encountered!"); } let remaining_length = (length - 2) as usize; match byte_buffer[0] { 0xe1 => { // APP1 marker // Read in & return the remaining data let mut app1_buffer = vec![0u8; remaining_length]; cursor.read_exact(&mut app1_buffer)?; return Ok(app1_buffer); }, 0xda => { // SOS marker // The start of scan (SOS) segment is followed by a blob of // image data, the entropy-coded segment (ECS), which has no // information regarding its length (as it may easily be // bigger than the max segment length of 64kb) // So, we have to scan byte-for-byte at this point until // a marker prefix comes up that is NOT // - followed by a restart marker (D0 - D7) or // - a data FF (followed by 00) // So, start by skipping the SOS segment cursor.seek(SeekFrom::Current(remaining_length as i64))?; // And skip the ECS skip_ecs(cursor)?; } _ => { // Every other marker // Skip this segment cursor.seek(SeekFrom::Current(remaining_length as i64))?; }, } previous_byte_was_marker_prefix = false; } else { previous_byte_was_marker_prefix = byte_buffer[0] == JPG_MARKER_PREFIX; } } }little_exif-0.6.23/src/jxl.rs000064400000000000000000000334651046102023000141710ustar 00000000000000// Copyright © 2024-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::fs::File; use std::io::Cursor; use std::io::Read; use std::io::Seek; use std::io::SeekFrom; use std::io::Write; use std::path::Path; use crate::endian::Endian; use crate::metadata::Metadata; use crate::u8conversion::*; use crate::general_file_io::*; use crate::util::insert_multiple_at; use crate::util::range_remove; pub(crate) const JXL_SIGNATURE: [u8; 2] = [0xff, 0x0a]; pub(crate) const ISO_BMFF_JXL_SIGNATURE: [u8; 12] = [ 0x00, 0x00, 0x00, 0x0c, 0x4a, 0x58, 0x4c, 0x20, 0x0d, 0x0a, 0x87, 0x0a ]; pub(crate) const FTYP_BOX: [u8; 20] = [ 0x00, 0x00, 0x00, 0x14, // length of this box 0x66, 0x74, 0x79, 0x70, // "ftyp" 0x6a, 0x78, 0x6c, 0x20, // "jxl " 0x00, 0x00, 0x00, 0x00, // minor version 0x6a, 0x78, 0x6c, 0x20 // "jxl " - yes, again ]; pub(crate) const ISO_BMFF_EXIF_MINOR_VERSION: [u8; 4] = [0x00, 0x00, 0x00, 0x06]; pub(crate) const BROB_BOX: [u8; 4] = [0x62, 0x72, 0x6f, 0x62]; #[non_exhaustive] struct IsoBmffBoxType; impl IsoBmffBoxType { pub const EXIF: [u8; 4] = [0x45, 0x78, 0x69, 0x66]; // "Exif" pub const FTYP: [u8; 4] = [0x66, 0x74, 0x79, 0x70]; // "ftyp" pub const JXL: [u8; 4] = [0x4a, 0x58, 0x4c, 0x20]; // "JXL " pub const JXLC: [u8; 4] = [0x6a, 0x78, 0x6c, 0x63]; // "jxlc" } /// Checks if the given file buffer vector starts with the necessary bytes that /// indicate a JXL file in an ISO BMFF container /// These containers are divided into boxes, each consisting of /// - 4 bytes that give the box size n /// - 4 bytes that give the box type (e.g. "jxlc" for a JXL codestream) /// - n-8 bytes of data /// /// These 12 bytes are for checking the first box that is the same for all such /// stored JXL images fn starts_with_iso_bmff_signature ( file_buffer: &[u8], ) -> bool { file_buffer.starts_with(&ISO_BMFF_JXL_SIGNATURE) } /// There are two types of JXL image files: One are simply a JXL codestream, /// which start with the `JXL_SIGNATURE` bytes 0xFF0A. These can *not* store /// any metadata. /// The other type is contained within a ISO BMFF container and are able to /// include EXIF metadata. /// If this function returns true, the image needs to be converted first before /// it is able to hold any metadata fn starts_with_jxl_signature ( file_buffer: &[u8], ) -> bool { file_buffer.starts_with(&JXL_SIGNATURE) } fn check_signature ( file_buffer: &[u8], ) -> Result<(), std::io::Error> { if starts_with_jxl_signature(file_buffer) { return io_error!(Other, "Simple JXL codestream file - No metadata!"); } if !starts_with_iso_bmff_signature(file_buffer) { return io_error!(Other, "This isn't ISO BMFF JXL data!"); } return Ok(()); } fn file_check_signature ( path: &Path ) -> Result { let mut file = open_write_file(path)?; let mut signature_buffer = [0u8; 12]; let bytes_read = file.read(&mut signature_buffer)?; if bytes_read != 12 { return io_error!(InvalidData, "Can't open JXL file - Can't read signature!"); } check_signature(&signature_buffer)?; return Ok(file); } pub(crate) fn clear_metadata ( file_buffer: &mut Vec ) -> Result<(), std::io::Error> { check_signature(file_buffer)?; let mut position = 0; loop { if position >= file_buffer.len() { return Ok(()); } // Get the first 4 bytes at the current cursor position to determine // the length of the current box let length_buffer = file_buffer[position..position+4].to_vec(); let length = from_u8_vec_res_macro!(u32, &length_buffer, &Endian::Big)? as usize; // Next, read the box type let type_buffer = file_buffer[position+4..position+8].to_vec(); if box_contains_exif( &mut Cursor::new( file_buffer[position+8..position+12].to_vec() ), [type_buffer[0], type_buffer[1], type_buffer[2], type_buffer[3]] )? { range_remove(file_buffer, position, position+length); } else { // Not an EXIF box so skip it position += length; } } } pub(crate) fn file_clear_metadata ( path: &Path ) -> Result<(), std::io::Error> { let mut file = file_check_signature(path)?; let mut length_buffer = [0u8; 4]; let mut type_buffer = [0u8; 4]; loop { let position = file.stream_position()?; let old_file_length = file.metadata()?.len(); if position >= old_file_length { return Ok(()); } file.read_exact(&mut length_buffer)?; file.read_exact(&mut type_buffer)?; let length = from_u8_vec_res_macro!(u32, &length_buffer, &Endian::Big)? as usize; if box_contains_exif(&mut file, type_buffer)? { // Seek past the EXIF box ... file.seek(SeekFrom::Current((length-8) as i64))?; // ... copy everything from here onwards into a buffer ... let mut buffer = Vec::new(); file.read_to_end(&mut buffer)?; // ... seek back to the start of the EXIF box ... file.seek(std::io::SeekFrom::Start(position))?; // ... overwrite everything from here onward ... file.write_all(&buffer)?; file.seek(std::io::SeekFrom::Start(position))?; // ... and finally update the file size - otherwise there will be // duplicate bytes at the end! file.set_len(old_file_length - length as u64)?; } else { // Not an EXIF box so skip it assert_eq!(position+8, file.stream_position()?); file.seek(SeekFrom::Current((length-8) as i64))?; } } } fn check_brob_type_for_exif ( cursor: &mut T ) -> Result { // Check if the next for 4 bytes say 'Exif' let mut brob_type = [0u8; 4]; cursor.read_exact(&mut brob_type)?; // Seek back to position prior to brob type cursor.seek(SeekFrom::Current(-4))?; return Ok(brob_type == EXIF); } fn box_contains_exif ( cursor: &mut T, type_buffer: [u8; 4], ) -> Result { if type_buffer == EXIF { return Ok(true); } if type_buffer == BROB_BOX && check_brob_type_for_exif(cursor)? { return Ok(true); } return Ok(false); } pub(crate) fn read_metadata ( file_buffer: &Vec ) -> Result, std::io::Error> { check_signature(file_buffer)?; let mut cursor = Cursor::new(file_buffer); return generic_read_metadata(&mut cursor); } pub(crate) fn file_read_metadata ( path: &Path ) -> Result, std::io::Error> { let mut file = open_read_file(path)?; // Read first 12 bytes and check that we have a ISO BMFF file let mut first_12_bytes = [0u8; 12]; let bytes_read = file.read(&mut first_12_bytes)?; if bytes_read != 12 { return io_error!(InvalidData, "Can't open JXL file - Can't read & check ISO BMFF signature!"); } check_signature(&first_12_bytes)?; return generic_read_metadata(&mut file); } fn generic_read_metadata ( cursor: &mut T ) -> Result, std::io::Error> { loop { // Get the first 4 bytes at the current cursor position to determine // the length of the current box (and account for the 8 bytes of length // and box type) let mut length_buffer = [0u8; 4]; cursor.read_exact(&mut length_buffer)?; let length = from_u8_vec_res_macro!(u32, &length_buffer, &Endian::Big)?.checked_sub(8).ok_or( std::io::Error::new( std::io::ErrorKind::InvalidData, "Invalid box length found when reading JXL metadata!" ) )?; // Next, read the box type let mut type_buffer = [0u8; 4]; cursor.read_exact(&mut type_buffer)?; match type_buffer { EXIF => { // Skip the next 4 bytes (which contain the minor version???) cursor.seek(SeekFrom::Current(4))?; // `length-4` because of the previous relative seek operation let mut exif_buffer = vec![0u8; (length-4) as usize]; cursor.read_exact(&mut exif_buffer)?; return Ok(exif_buffer); }, BROB_BOX => { // -> Brotli encoded data let position = cursor.stream_position()? as usize; if check_brob_type_for_exif(cursor)? { // Skip the next 4 bytes (which contain the minor version???) cursor.seek(SeekFrom::Current(4))?; let mut compressed_exif_buffer = vec![ 0u8; (length-4) as usize ]; cursor.read_exact(&mut compressed_exif_buffer)?; let mut decompressed_exif_buffer = Vec::new(); match brotli::BrotliDecompress( &mut Cursor::new(compressed_exif_buffer), &mut decompressed_exif_buffer ) { Ok(_) => (), Err(e) => return Err(e) }; // Ignore the next 4 bytes (I guess for the same reason // as above - some sort of minor version?) return Ok(decompressed_exif_buffer[4..].to_vec()); } else { cursor.seek(SeekFrom::Start(position as u64 + length as u64))?; } } _ => { // Not an EXIF box so skip it cursor.seek(SeekFrom::Current(length as i64))?; } } } } fn encode_metadata_jxl ( exif_vec: &[u8], ) -> Vec { let exif_box_length = 0 // Length has to include + 4 // - the length field + IsoBmffBoxType::EXIF.len() as u32 // - the box type + ISO_BMFF_EXIF_MINOR_VERSION.len() as u32 // - the minor version + EXIF_HEADER.len() as u32 // - the exif header + exif_vec.len() as u32 // - the exif data ; let mut jxl_exif = Vec::new(); jxl_exif.extend(to_u8_vec_macro!(u32, &exif_box_length, &Endian::Big)); jxl_exif.extend(IsoBmffBoxType::EXIF); jxl_exif.extend(ISO_BMFF_EXIF_MINOR_VERSION); jxl_exif.extend(EXIF_HEADER.iter()); jxl_exif.extend(exif_vec.iter()); return jxl_exif; } fn find_insert_position ( file_buffer: &Vec ) -> Result { let mut cursor = Cursor::new(file_buffer); loop { // Get the first 4 bytes at the current cursor position to determine // the length of the current box (and account for the 8 bytes of length // and box type) let mut length_buffer = [0u8; 4]; cursor.read_exact(&mut length_buffer)?; let length = from_u8_vec_res_macro!(u32, &length_buffer, &Endian::Big)?.checked_sub(8).ok_or( std::io::Error::new( std::io::ErrorKind::InvalidData, "Invalid box length found when reading JXL metadata!" ) )? as usize; // Next, read the box type let mut type_buffer = [0u8; 4]; cursor.read_exact(&mut type_buffer)?; match type_buffer { IsoBmffBoxType::JXL | IsoBmffBoxType::FTYP => { // Place exif box after these boxes cursor.seek(SeekFrom::Current(length as i64))?; } _ => { return Ok(cursor.position() as usize - 8); } } } } pub(crate) fn write_metadata ( file_buffer: &mut Vec, metadata: &Metadata ) -> Result<(), std::io::Error> { if starts_with_jxl_signature(file_buffer) { // Need to modify the file_buffer first so that it is a ISO BMFF let mut new_file_buffer = Vec::new(); // Start of the new file new_file_buffer.extend(ISO_BMFF_JXL_SIGNATURE); new_file_buffer.extend(FTYP_BOX); // JXL codestream box // - length of box (including 4 bytes of length & type fields each) // - type field // - data let jxlc_box_length = file_buffer.len() as u32 + 8; new_file_buffer.extend(to_u8_vec_macro!(u32, &jxlc_box_length, &Endian::Big)); new_file_buffer.extend(IsoBmffBoxType::JXLC); new_file_buffer.append(file_buffer); // Replace file buffer *file_buffer = new_file_buffer; } // Remove old metadata clear_metadata(file_buffer)?; // Insert new metadata let mut encoded_metadata = encode_metadata_jxl(&metadata.encode()?); let insert_position = find_insert_position(file_buffer)?; insert_multiple_at(file_buffer, insert_position, &mut encoded_metadata); return Ok(()); } pub(crate) fn file_write_metadata ( path: &Path, metadata: &Metadata ) -> Result<(), std::io::Error> { // Load the entire file into memory instead of performing multiple read, // seek and write operations let mut file = open_write_file(path)?; let mut file_buffer: Vec = Vec::new(); file.read_to_end(&mut file_buffer)?; // Writes the metadata to the file_buffer vec // The called function handles the removal of old metadata and the JXL // specific encoding, so we pass only the generally encoded metadata here write_metadata(&mut file_buffer, metadata)?; // Seek back to start & write the file file.seek(SeekFrom::Start(0))?; file.write_all(&file_buffer)?; return Ok(()); }little_exif-0.6.23/src/lib.rs000064400000000000000000000026741046102023000141400ustar 00000000000000// Copyright © 2022-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details #![allow(clippy::needless_return)] #![allow(clippy::upper_case_acronyms)] #![allow(clippy::identity_op)] #![allow(clippy::redundant_field_names)] #![warn(clippy::unwrap_used)] #![allow(unused_parens)] //! A small crate for reading and writing (some) EXIF data, written entirely in Rust. Currently supports //! - JPEG / JPG //! - JXL //! - HEIF / HEIC / HIF / AVIF //! - PNG //! - TIFF //! - WebP (only lossless and extended) //! //! files and a few dozen tags in IFD0 and ExifIFD. //! Interaction is done via the [`Metadata`](metadata/struct.Metadata.html) //! struct and the [`ExifTag`](exif_tag/enum.ExifTag.html) enum. //! //! # Usage //! ## Write EXIF data //! ```no_run //! use little_exif::metadata::Metadata; //! use little_exif::exif_tag::ExifTag; //! //! let mut metadata = Metadata::new(); //! metadata.set_tag( //! ExifTag::ImageDescription("Hello World!".to_string()) //! ); //! metadata.write_to_file(std::path::Path::new("image.png")); //! ``` #![forbid(unsafe_code)] #![crate_type = "lib"] #![crate_name = "little_exif"] mod general_file_io; pub mod ifd; mod png; mod heif; mod jpg; mod jxl; mod tiff; mod webp; mod xmp; mod util; pub mod endian; pub mod rational; pub mod u8conversion; pub mod exif_tag; pub mod exif_tag_format; pub mod filetype; pub mod metadata;little_exif-0.6.23/src/metadata/edit.rs000064400000000000000000000031021046102023000160620ustar 00000000000000// Copyright © 2024 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use crate::exif_tag::ExifTag; use crate::ifd::ExifTagGroup; use super::Metadata; impl Metadata { /// Reduces the `Metadata` struct to the absolute minimum required for /// TIFF compliance without losing important data (see table in exif_tag.rs, /// strip and thumbnail data) which is all assumed to be in GENERIC IFDs. /// If this is not the case for one of your images, please open a new issue pub fn reduce_to_a_minimum ( &mut self ) { // Only keep GENERIC IFDs self.image_file_directories.retain(|ifd| ifd.get_ifd_type() == ExifTagGroup::GENERIC); // Remove tags in IFDs that are not important for ifd in &mut self.image_file_directories { let mut tags_to_be_removed = Vec::new(); for tag in ifd.get_tags() { match tag { ExifTag::StripOffsets(_, _) | ExifTag::StripByteCounts(_) | ExifTag::ThumbnailOffset(_, _) | ExifTag::ThumbnailLength(_) | ExifTag::ImageWidth(_) | ExifTag::ImageHeight(_) | ExifTag::BitsPerSample(_) | ExifTag::Compression(_) | ExifTag::PhotometricInterpretation(_) | ExifTag::SamplesPerPixel(_) | ExifTag::RowsPerStrip(_) | ExifTag::XResolution(_) | ExifTag::YResolution(_) | ExifTag::ResolutionUnit(_) | ExifTag::ColorMap(_) => (), _ => tags_to_be_removed.push(tag.clone()), } } for tag in tags_to_be_removed { ifd.remove_tag(tag.as_u16()); } } } }little_exif-0.6.23/src/metadata/get.rs000064400000000000000000000124001046102023000157150ustar 00000000000000// Copyright © 2024-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use crate::exif_tag::ExifTag; use crate::ifd::ExifTagGroup; use super::Endian; use super::ImageFileDirectory; use super::Metadata; impl Metadata { /// Gets the endianness of the metadata /// /// # Examples /// ```no_run /// use little_exif::metadata::Metadata; /// /// let metadata = Metadata::new_from_path(std::path::Path::new("image.png")).unwrap(); /// let tag_data = metadata.get_tag_by_hex(0x010e, None).next().unwrap().value_as_u8_vec(&metadata.get_endian()); /// ``` pub fn get_endian ( &self ) -> Endian { self.endian.clone() } /// Gets the image file directories stored in the struct pub fn get_ifds ( &self ) -> &Vec { &self.image_file_directories } /// Gets an image file directory that is of a specific group an is /// associated with a certain generic IFD number pub fn get_ifd ( &self, group: ExifTagGroup, generic_ifd_nr: u32, ) -> Option<&ImageFileDirectory> { self.image_file_directories.iter().find(|ifd| ifd.get_generic_ifd_nr() == generic_ifd_nr && ifd.get_ifd_type() == group ) } /// Gets the maximum generic ifd number that any of the struct's IFDs has pub fn get_max_generic_ifd_number ( &self ) -> u32 { if let Some(max_generic_ifd) = self.image_file_directories.iter() .filter(|ifd| ifd.get_ifd_type() == ExifTagGroup::GENERIC) .max_by(|ifd1, ifd2| ifd1.get_generic_ifd_nr().cmp(&ifd2.get_generic_ifd_nr())) { return max_generic_ifd.get_generic_ifd_nr(); } return 0; } /// Gets an image file directory that is of a specific group an is /// associated with a certain generic IFD number as a mutable reference. /// If the desired IFD does not exist yet it gets created. pub fn get_ifd_mut ( &mut self, group: ExifTagGroup, generic_ifd_nr: u32, ) -> &mut ImageFileDirectory { if !self.image_file_directories.iter().any(|ifd| ifd.get_generic_ifd_nr() == generic_ifd_nr && ifd.get_ifd_type() == group ) { self.create_ifd(group, generic_ifd_nr); } return self.image_file_directories.iter_mut().find(|ifd| ifd.get_generic_ifd_nr() == generic_ifd_nr && ifd.get_ifd_type() == group ).expect("Item should be already created above"); } } impl Metadata { pub fn get_tag ( &self, tag: &ExifTag ) -> GetTagIterator<'_> { return self.get_tag_by_hex(tag.as_u16(), Some(tag.get_group())); } /// Gets a tag from the metadata struct via the hex number and the group /// Note: While it is not necessary to provide the group, it may be needed /// in some cases as there are tags that have the same tag number, e.g. /// the `InteroperabilityVersion` and the `GPSLatitude` tags. pub fn get_tag_by_hex ( &self, hex: u16, group: Option, ) -> GetTagIterator<'_> { GetTagIterator { metadata: self, current_ifd_index: 0, current_tag_index: 0, tag_hex_value: hex, group: group, } } } pub struct GetTagIterator<'a> { metadata: &'a Metadata, current_ifd_index: usize, current_tag_index: usize, tag_hex_value: u16, group: Option, } impl<'a> Iterator for GetTagIterator<'a> { type Item = &'a ExifTag; fn next ( &mut self ) -> Option { while self.current_ifd_index < self.metadata.image_file_directories.len() { // First: Check the group, assuming it is provided if let Some(given_group) = self.group { if given_group != self.metadata.image_file_directories[self.current_ifd_index].get_ifd_type() { self.current_ifd_index += 1; continue; } } // Check the current tag // - If it is wrong, increment the tag index // - If we can't access that tag, increment IFD index and reset tag index if self.current_tag_index < self.metadata.image_file_directories[self.current_ifd_index].get_tags().len() { self.current_tag_index += 1; if self.metadata.image_file_directories[self.current_ifd_index].get_tags()[self.current_tag_index-1].as_u16() == self.tag_hex_value { return Some(&self.metadata.image_file_directories[self.current_ifd_index].get_tags()[self.current_tag_index-1]); } } else { self.current_tag_index = 0; self.current_ifd_index += 1; } } return None; } }little_exif-0.6.23/src/metadata/iterator.rs000064400000000000000000000022621046102023000167740ustar 00000000000000// Copyright © 2024 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use crate::exif_tag::ExifTag; use super::Metadata; impl<'a> IntoIterator for &'a Metadata { type Item = &'a ExifTag; type IntoIter = MetadataIterator<'a>; fn into_iter ( self ) -> Self::IntoIter { MetadataIterator { metadata: self, current_ifd_index: 0, current_tag_index: 0 } } } pub struct MetadataIterator<'a> { metadata: &'a Metadata, current_ifd_index: usize, current_tag_index: usize } impl<'a> Iterator for MetadataIterator<'a> { type Item = &'a ExifTag; fn next ( &mut self ) -> Option { while self.current_ifd_index < self.metadata.image_file_directories.len() { if self.current_tag_index < self.metadata.image_file_directories[self.current_ifd_index].get_tags().len() { self.current_tag_index += 1; return Some(&self.metadata.image_file_directories[self.current_ifd_index].get_tags()[self.current_tag_index-1]); } else { self.current_tag_index = 0; self.current_ifd_index += 1; } } return None; } }little_exif-0.6.23/src/metadata/metadata_io.rs000064400000000000000000000372741046102023000174250ustar 00000000000000// Copyright © 2024-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::Cursor; use std::path::Path; use crate::filetype::get_file_type; use crate::filetype::FileExtension; use crate::general_file_io::io_error; use crate::general_file_io::open_read_file; use crate::heif; use crate::jpg; use crate::jxl; use crate::png; use crate::tiff; use crate::webp; use super::Metadata; impl Metadata { /// Constructs a new `Metadata` object with the metadata from an image that is stored as a `Vec` /// - If unable to handle the file vector (e.g. unsupported file type, etc.), this (currently) panics. /// - If unable to decode the metadata, a new, empty object gets created and returned. /// # Examples /// ```no_run /// use std::fs; /// use little_exif::metadata::Metadata; /// use little_exif::filetype::FileExtension; /// /// let file_data = fs::read("image.jpg").unwrap(); /// let mut metadata: Metadata = Metadata::new_from_vec(&file_data, FileExtension::JPEG).unwrap(); /// ``` #[allow(unreachable_patterns)] pub fn new_from_vec ( file_buffer: &Vec, file_type: FileExtension ) -> Result { // First, try to determine the file type automatically let mut cursor = Cursor::new(file_buffer); let auto_detected_file_type = FileExtension::auto_detect(&mut cursor); if let Some(detected_type) = auto_detected_file_type { if file_type != detected_type { log::warn!( "The supplied file type information ({file_type:?}) and \ detected ({detected_type:?}) do NOT match!" ); } } else { log::warn!("Could not automatically detect file type!"); } let raw_pre_decode_general = match file_type { FileExtension::HEIF => heif::read_metadata(file_buffer), FileExtension::JPEG => jpg::read_metadata(file_buffer), FileExtension::JXL => jxl::read_metadata(file_buffer), FileExtension::PNG { as_zTXt_chunk: _ } => png::read_metadata(file_buffer), FileExtension::TIFF => tiff::vec::read_metadata(file_buffer), FileExtension::WEBP => webp::vec::read_metadata(file_buffer), _ => return io_error!( Other, format!( "Function 'new_from_vec' not yet implemented for \ {file_type:?}" ) ), }; return Self::general_decoding_wrapper(raw_pre_decode_general); } /// Constructs a new `Metadata` object with the metadata from the image at the specified path. /// - If unable to read the file (e.g. does not exist, unsupported file type, etc.), this (currently) panics. /// - If unable to decode the metadata, a new, empty object gets created and returned. /// /// # Examples /// ```no_run /// use little_exif::metadata::Metadata; /// /// let mut metadata: Metadata = Metadata::new_from_path(std::path::Path::new("image.png")).unwrap(); /// ``` #[allow(unreachable_patterns)] pub fn new_from_path ( path: &Path ) -> Result { // First, try to get the type based on the file extension let extension_based_file_type_result = get_file_type(path); let extension_based_file_type_opt = match extension_based_file_type_result { Ok(result) => Some(result), Err(error) => match error.kind() { std::io::ErrorKind::Unsupported => return Err(error), _ => None }, }; // Next, try to use auto detect let mut file = open_read_file(path)?; let content_based_file_type = FileExtension::auto_detect(&mut file); let mut extension_based_file_type: FileExtension; if let Some(extension_based) = extension_based_file_type_opt { extension_based_file_type = extension_based; } else { if let Some(content_based_file_type) = content_based_file_type { extension_based_file_type = content_based_file_type; } else { return io_error!( Other, "Could not determine file type when reading file!" ); } } if let Some(content_based_file_type) = content_based_file_type { if extension_based_file_type != content_based_file_type { log::warn!("File extension and file content yield different file type, content takes precedence"); extension_based_file_type = content_based_file_type; } } else { log::warn!("Could not determine file type based on content, fall back on file extension"); } let file_type = extension_based_file_type; // Call the file specific decoders as a starting point for obtaining // the raw EXIF data that gets further processed let raw_pre_decode_general = match file_type { FileExtension::HEIF => heif::file_read_metadata(path), FileExtension::JPEG => jpg::file_read_metadata(path), FileExtension::JXL => jxl::file_read_metadata(path), FileExtension::PNG { as_zTXt_chunk: _ } => png::file_read_metadata(path), FileExtension::TIFF => tiff::file::read_metadata(path), FileExtension::WEBP => webp::file::read_metadata(path), _ => return io_error!( Other, format!( "Function 'new_from_path' not yet implemented for {:?}", file_type ) ), }; return Self::general_decoding_wrapper(raw_pre_decode_general); } #[allow(unreachable_patterns)] pub fn clear_metadata ( file_buffer: &mut Vec, file_type: FileExtension ) -> Result<(), std::io::Error> { match file_type { FileExtension::HEIF => heif::clear_metadata(file_buffer), FileExtension::JPEG => jpg::clear_metadata(file_buffer), FileExtension::JXL => jxl::clear_metadata(file_buffer), FileExtension::PNG { as_zTXt_chunk: _ } => png::clear_metadata(file_buffer), FileExtension::TIFF => tiff::vec::clear_metadata(file_buffer), FileExtension::WEBP => webp::vec::clear_metadata(file_buffer), _ => return io_error!( Other, format!( "Function 'clear_metadata' not yet implemented for {:?}", file_type ) ), } } /// Clears the APP12 segment in a JPEG file that contains data resulting /// from exporting the file via Photoshop. This may be required in order /// for other software to see e.g. the ImageDescription written in the /// APP1 exif segment by little_exif #[allow(unreachable_patterns)] pub fn clear_app12_segment ( file_buffer: &mut Vec, file_type: FileExtension ) -> Result<(), std::io::Error> { match file_type { FileExtension::JPEG => jpg::clear_segment(file_buffer, 0xec, None), _ => return io_error!( Other, format!( "Function 'clear_app12_segment' not available for {:?} (only relevant for JPEG)", file_type ) ), } } /// Clears the APP13 segment in a JPEG file that contains data resulting /// from exporting the file via Photoshop. This may be required in order /// for other software to see e.g. the ImageDescription written in the /// APP1 exif segment by little_exif #[allow(unreachable_patterns)] pub fn clear_app13_segment ( file_buffer: &mut Vec, file_type: FileExtension ) -> Result<(), std::io::Error> { match file_type { FileExtension::JPEG => jpg::clear_segment(file_buffer, 0xed, None), _ => return io_error!( Other, format!( "Function 'clear_app13_segment' not available for {:?} (only relevant for JPEG)", file_type ) ), } } /// Clears the APP12 segment in a JPEG file that contains data resulting /// from exporting the file via Photoshop. This may be required in order /// for other software to see e.g. the ImageDescription written in the /// APP1 exif segment by little_exif #[allow(unreachable_patterns)] pub fn file_clear_app12_segment ( path: &Path ) -> Result<(), std::io::Error> { let file_type = get_file_type(path)?; match file_type { FileExtension::JPEG => jpg::file_clear_segment(path, 0xec, None), _ => return io_error!( Other, format!( "Function 'file_clear_app12_segment' not available for {:?} (only relevant for JPEG)", file_type ) ), } } /// Clears the APP13 segment in a JPEG file that contains data resulting /// from exporting the file via Photoshop. This may be required in order /// for other software to see e.g. the ImageDescription written in the /// APP1 exif segment by little_exif #[allow(unreachable_patterns)] pub fn file_clear_app13_segment ( path: &Path ) -> Result<(), std::io::Error> { let file_type = get_file_type(path)?; match file_type { FileExtension::JPEG => jpg::file_clear_segment(path, 0xed, None), _ => return io_error!( Other, format!( "Function 'file_clear_app13_segment' not available for {:?} (only relevant for JPEG)", file_type ) ), } } #[allow(unreachable_patterns)] pub fn file_clear_metadata ( path: &Path ) -> Result<(), std::io::Error> { let file_type = get_file_type(path)?; match file_type { FileExtension::HEIF => heif::file_clear_metadata(path), FileExtension::JPEG => jpg::file_clear_metadata(path), FileExtension::JXL => jxl::file_clear_metadata(path), FileExtension::PNG { as_zTXt_chunk: _ } => png::file_clear_metadata(path), FileExtension::TIFF => tiff::file::clear_metadata(path), FileExtension::WEBP => webp::file::clear_metadata(path), _ => return io_error!( Other, format!( "Function 'file_clear_metadata' not yet implemented for {:?}", file_type ) ), } } /// Converts the metadata into a file specific vector of bytes /// Only to be used in combination with some other library/code that is /// able to handle the specific file type. /// Simply writing this to a file often is not enough, e.g. with WebP you /// have to determine where to write this, update the file size information /// and so on - check file type specific implementations or documentation /// for further details #[allow(unreachable_patterns)] pub fn as_u8_vec ( &self, for_file_type: FileExtension ) -> Result, std::io::Error> { let general_encoded_metadata = self.encode()?; Ok(match for_file_type { FileExtension::PNG { as_zTXt_chunk } => png::as_u8_vec(&general_encoded_metadata, as_zTXt_chunk), FileExtension::JPEG => jpg::as_u8_vec(&general_encoded_metadata), FileExtension::WEBP => webp::as_u8_vec(&general_encoded_metadata), FileExtension::HEIF => heif::as_u8_vec(&general_encoded_metadata), _ => { io_error!( Other, format!( "Function 'as_u8_vec' not yet implemented for {:?}", for_file_type ) )? } }) } /// Writes the metadata to an image stored as a Vec /// For now, this only works for JPGs #[allow(unreachable_patterns)] pub fn write_to_vec ( &self, file_buffer: &mut Vec, file_type: FileExtension ) -> Result<(), std::io::Error> { match file_type { FileExtension::HEIF => heif::write_metadata(file_buffer, self), FileExtension::JPEG => jpg::write_metadata(file_buffer, self), FileExtension::JXL => jxl::write_metadata(file_buffer, self), FileExtension::PNG { as_zTXt_chunk: _ } => png::write_metadata(file_buffer, self), FileExtension::TIFF => tiff::vec::write_metadata(file_buffer, self), FileExtension::WEBP => webp::vec::write_metadata(file_buffer, self), _ => return io_error!( Other, format!( "Function 'file_clear_metadata' not yet implemented for {:?}", file_type ) ), } } /// Writes the metadata to the specified file. /// This could return an error for multiple reasons: /// - The file does not exist at the given path /// - Interpreting the given path fails /// - The file type is not supported #[allow(unreachable_patterns)] pub fn write_to_file ( &self, path: &Path ) -> Result<(), std::io::Error> { let file_type = get_file_type(path)?; match file_type { FileExtension::HEIF => heif::file_write_metadata(path, self), FileExtension::JPEG => jpg::file_write_metadata(path, self), FileExtension::JXL => jxl::file_write_metadata(path, self), FileExtension::PNG { as_zTXt_chunk: _ } => png::file_write_metadata(path, self), FileExtension::TIFF => tiff::file::write_metadata(path, self), FileExtension::WEBP => webp::file::write_metadata(path, self), _ => return io_error!( Other, format!( "Function 'write_to_file' not yet implemented for {:?}", file_type ) ), } } } little_exif-0.6.23/src/metadata/mod.rs000064400000000000000000000261341046102023000157260ustar 00000000000000// Copyright © 2024-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details pub mod metadata_io; pub mod iterator; pub mod edit; pub mod get; pub mod set; use std::io::Cursor; use std::io::Read; use std::io::Seek; use std::io::Write; use crate::endian::*; use crate::general_file_io::io_error; use crate::general_file_io::EXIF_HEADER; use crate::ifd::ExifTagGroup; use crate::ifd::ImageFileDirectory; use crate::u8conversion::from_u8_vec_res_macro; use crate::u8conversion::U8conversion; #[derive(Clone, Debug)] pub struct Metadata { endian: Endian, image_file_directories: Vec } impl Metadata { /// Constructs a new, empty `Metadata` object. /// /// This uses little endian notation by default. /// /// # Examples /// ```no_run /// use little_exif::metadata::Metadata; /// /// let mut metadata: Metadata = Metadata::new(); /// ``` pub fn new () -> Metadata { Metadata { endian: Endian::Little, image_file_directories: Vec::new() } } /// Creates an IFD in this struct if it does not exist yet. /// Also handles that parent IFDs are properly created if they don't exist /// yet but are required later on for the encoding process. pub fn create_ifd ( &mut self, ifd_type: ExifTagGroup, generic_ifd_nr: u32, ) { if self.get_ifd(ifd_type, generic_ifd_nr).is_some() { return; } let new_ifd = ImageFileDirectory::new_with_tags(Vec::new(), ifd_type, generic_ifd_nr); if let Some((parent_ifd_group, _)) = new_ifd.get_offset_tag_for_parent_ifd() { self.create_ifd(parent_ifd_group, generic_ifd_nr); } self.image_file_directories.push(new_ifd); self.sort_data(); } pub(crate) fn general_decoding_wrapper ( raw_pre_decode_general: Result, std::io::Error> ) -> Result { if let Ok(pre_decode_general) = raw_pre_decode_general { let mut pre_decode_cursor = Cursor::new(&pre_decode_general); let decoding_result = Self::decode(&mut pre_decode_cursor); if let Ok((endian, image_file_directories)) = decoding_result { let mut data = Metadata { endian, image_file_directories }; data.sort_data(); return Ok(data); } else if let Err(decode_error) = decoding_result { log::error!("Error during decoding (1): {decode_error}"); return Err(decode_error); } else { return io_error!(Other, "Unreachable code reached in Metadata::general_decoding_wrapper (1)"); } } else if let Err(decode_error) = raw_pre_decode_general { log::error!("Error during decoding (2): {decode_error:?}"); return Err(decode_error); } else { return io_error!(Other, "Unreachable code reached in Metadata::general_decoding_wrapper (2)"); } } /// Assumes that the data is sorted according to `sort_data` pub fn encode ( &self ) -> Result, std::io::Error> { // Prepare offset information let mut ifds_with_offset_info_only: Vec = Vec::new(); for ifd in self.image_file_directories.iter() { ifds_with_offset_info_only.push( ImageFileDirectory::new_with_tags( vec![], ifd.get_ifd_type(), ifd.get_generic_ifd_nr() ) ); } for ifd in self.image_file_directories.iter() { if let Some((parent_ifd_group, offset_tag)) = ifd.get_offset_tag_for_parent_ifd() { // Check if the parent IFD is already in the vector if let Some(parent_ifd) = ifds_with_offset_info_only .iter_mut() .find(|candidate_ifd| candidate_ifd.get_ifd_type() == parent_ifd_group && candidate_ifd.get_generic_ifd_nr() == ifd.get_generic_ifd_nr() ) { parent_ifd.set_tag(offset_tag); } else { // This *can* happen! For example, take a new Metadata // struct that is empty and insert a tag that belongs to // the Exif SubIFD. Then, IFD0 is still missing in self, // does *not* get inserted into `ifds_with_offset_info_only` // and can thus not be found in the if let above. ifds_with_offset_info_only.push( ImageFileDirectory::new_with_tags( vec![offset_tag], parent_ifd_group, ifd.get_generic_ifd_nr() ) ); } } } // Now traverse the IFDs, starting with the SubIFDs associated with // IFD0, then IFD0 itself. Next, SubIFDs for IFD1, IFD1 itself, and // so on up to IFD-n. let generic_ifd_count = self.get_max_generic_ifd_number(); let mut index_of_previous_ifds_link_section: Option = Some(4); let mut encode_vec = Vec::from(self.endian.header()); let mut current_offset = 8; for n in 0..=generic_ifd_count { let filter_result = self.image_file_directories.iter().filter(|ifd| ifd.get_generic_ifd_nr() == n && ifd.get_ifd_type() == ExifTagGroup::GENERIC ).collect::>(); assert!(filter_result.len() <= 1); let Some(last_ifd) = filter_result.last() else { continue; }; if let Ok((next_link_section, link_vec)) = last_ifd.encode_ifd( self, &mut ifds_with_offset_info_only, &mut encode_vec, &mut current_offset ) { if let Some(index) = index_of_previous_ifds_link_section { let mut cursor = Cursor::new(&mut encode_vec); cursor.set_position(index); cursor.write_all(&link_vec)?; } index_of_previous_ifds_link_section = Some(next_link_section); } } Ok(encode_vec) } fn sort_data ( &mut self ) { self.image_file_directories.sort_by( |a, b| if a.get_generic_ifd_nr() != b.get_generic_ifd_nr() { a.get_generic_ifd_nr().cmp(&b.get_generic_ifd_nr()) } else { if a.get_ifd_type() == b.get_ifd_type() { panic!("Should not have two different IFDs with same group & number!"); } if a.get_ifd_type() < b.get_ifd_type() { std::cmp::Ordering::Less } else { std::cmp::Ordering::Greater } } ); } fn decode ( data_cursor: &mut Cursor<&Vec> ) -> Result<(Endian, Vec), std::io::Error> { // Get the start position let mut data_start_position = data_cursor.position(); // Check if this starts with the Exif header let mut first_6_bytes = vec![0u8; 6]; data_cursor.read_exact(&mut first_6_bytes)?; let starts_with_exif_signature = (first_6_bytes == EXIF_HEADER); // If those 6 bytes are *not* "Exif " then we need to rewind as these // six bytes should then be the endian information and magic number // Otherwise the cursor should now be advanced to this area if !starts_with_exif_signature { data_cursor.seek(std::io::SeekFrom::Current(-(EXIF_HEADER.len() as i64)))?; } else { // Otherwise we need to adjust the start position data_start_position += EXIF_HEADER.len() as u64; } // Determine endian let mut endian_buffer = vec![0u8; 2]; data_cursor.read_exact(&mut endian_buffer)?; let endian = match endian_buffer[0..2] { [0x49, 0x49] => { Endian::Little }, [0x4d, 0x4d] => { Endian::Big }, [0x68, 0x74] => { return io_error!(Other, "Expected endian information, but found something that suspectedly is XMP data") } _ => { return io_error!(Other, format!("Illegal endian information: {:?}", endian_buffer)) } }; // Validate magic number let mut magic_number_buffer = vec![0u8; 2]; data_cursor.read_exact(&mut magic_number_buffer)?; if !( (endian == Endian::Little && magic_number_buffer == [0x2a, 0x00]) || (endian == Endian::Big && magic_number_buffer == [0x00, 0x2a]) ) { return io_error!(Other, "Could not verify magic number!"); } // Get offset to IFD0 let mut ifd0_offset_buffer = vec![0u8; 4]; data_cursor.read_exact(&mut ifd0_offset_buffer)?; let mut ifd_offset_option = Some(from_u8_vec_res_macro!(u32, &ifd0_offset_buffer, &endian)?); // Decode all the IFDs let mut ifds = Vec::new(); let mut generic_ifd_nr = 0; while let Some(ifd_offset) = ifd_offset_option { data_cursor.set_position(data_start_position); data_cursor.seek(std::io::SeekFrom::Current(ifd_offset as i64))?; let decode_result = ImageFileDirectory::decode_ifd( data_cursor, data_start_position, &endian, &ExifTagGroup::GENERIC, generic_ifd_nr, &mut ifds ); ifd_offset_option = decode_result?; generic_ifd_nr += 1; } return Ok((endian, ifds)); } } impl Default for Metadata { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use std::fs::read; use std::io::Cursor; use super::Metadata; #[test] fn new_test_1() -> Result<(), std::io::Error> { let image_data = read("tests/read_sample.tif").unwrap(); Metadata::decode(&mut Cursor::new(&image_data))?; Ok(()) } #[ignore] #[test] fn new_test_2() -> Result<(), std::io::Error> { // let image_data = read("tests/multi_page.tif").unwrap(); let image_data = read("tests/multi_page_mod.tif").unwrap(); let data = Metadata::decode(&mut Cursor::new(&image_data))?; for ifd in data.1 { println!("{:?}", ifd); } Ok(()) } } little_exif-0.6.23/src/metadata/set.rs000064400000000000000000000044611046102023000157410ustar 00000000000000// Copyright © 2024 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use crate::exif_tag::ExifTag; use crate::ifd::ExifTagGroup; use super::Metadata; impl Metadata { /// Sets the tag in the metadata struct. Tries to determine what IFD the /// tag belongs to and should be inserted into, starting with IFD0. /// If the tag should e.g. be inserted into IFD0's EXIF SubIFD and that does /// not exist yet, the SubIFD gets created instead of trying to use the /// EXIF SubIFD of IFD1. /// For more fine-control (e.g. when handling multi-page TIFFs) it is /// strongly advised to instead first get a mutable reference to the /// preferred IFD and calling `set_tag` on that one instead. pub fn set_tag ( &mut self, input_tag: ExifTag ) { self.get_ifd_mut(input_tag.get_group(), 0).set_tag(input_tag); } /// Removes a tag from the metadata struct, based on its hex value and /// associated group. If, for whatever reason, this tag appears in multiple /// IFDs, all instances will be removed, assuming the groups match. /// The count of calls on `remove_tag` gets returned. If this is zero, /// no removals were performed. #[allow(clippy::needless_pass_by_value)] pub fn remove_tag ( &mut self, remove_me: ExifTag ) -> usize { self.remove_tag_by_hex_group( remove_me.as_u16(), remove_me.get_group() ) } /// Removes a tag from the metadata struct, based on its hex value and /// associated group. If, for whatever reason, this tag appears in multiple /// IFDs, all instances will be removed, assuming the groups match. /// The count of calls on `remove_tag` gets returned. If this is zero, /// no removals were performed. #[allow(clippy::needless_pass_by_value)] pub fn remove_tag_by_hex_group ( &mut self, tag_hex: u16, tag_group: ExifTagGroup ) -> usize { let mut removed_count = 0; // Traverse all IFD numbers up to the max. known one for ifd_number in 0..=self.get_max_generic_ifd_number() { // Does this IFD exist? if self.get_ifd(tag_group, ifd_number).is_some() { // If so, get it as mutable and call remove_tag on it self.get_ifd_mut( tag_group, ifd_number ).remove_tag(tag_hex); removed_count += 1; } } return removed_count; } }little_exif-0.6.23/src/png/chunk.rs000064400000000000000000000066071046102023000152660ustar 00000000000000// Copyright © 2022-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details #[allow(non_camel_case_types, dead_code)] pub(crate) enum PngChunkOrdering { FIRST, BEFORE_IDAT, BEFORE_PLTE_AND_IDAT, AFTER_PLTE_BEFORE_IDAT, LAST, NONE } /// This macro builds the enum for the different type of PNG chunks macro_rules! build_png_chunk_type_enum { ( $( ( $tag:ident, $critical:expr, $multiple:expr, $ordering:ident ) ),* ) => { /// These are the different PNG chunk types currently known to /// little_exif. These might be expanded in the future if necessary. #[allow(non_camel_case_types)] pub(crate) enum PngChunk { UNKNOWN(String, u32), $( $tag(u32), )* } impl PngChunk { pub(crate) fn length ( &self ) -> u32 { match *self { PngChunk::UNKNOWN(_, length) => length, $( PngChunk::$tag( length) => length, )* } } pub(crate) fn as_string ( &self ) -> String { match self { PngChunk::UNKNOWN(name, _) => name.clone(), $( PngChunk::$tag(_) => String::from(stringify!($tag)), )* } } pub(crate) fn from_string ( string_name: &str, length: u32 ) -> Result { match string_name { $( stringify!($tag) => Ok(PngChunk::$tag(length)), )* _ => Err(PngChunk::UNKNOWN(string_name.to_string(), length)), } } } } } build_png_chunk_type_enum![ // Tag Critical Multiple Ordering (IHDR, true, false, FIRST), (PLTE, true, false, BEFORE_IDAT), (IDAT, true, true, NONE), (IEND, true, false, LAST), (cHRM, false, false, BEFORE_PLTE_AND_IDAT), (gAMA, false, false, BEFORE_PLTE_AND_IDAT), (cICP, false, false, BEFORE_PLTE_AND_IDAT), (iCCP, false, false, BEFORE_PLTE_AND_IDAT), (sBIT, false, false, BEFORE_PLTE_AND_IDAT), (sRGB, false, false, BEFORE_PLTE_AND_IDAT), (bKGD, false, false, AFTER_PLTE_BEFORE_IDAT), (hIST, false, false, AFTER_PLTE_BEFORE_IDAT), (tRNS, false, false, AFTER_PLTE_BEFORE_IDAT), (pHYs, false, false, BEFORE_IDAT), (sPLT, false, true, BEFORE_IDAT), (eXIf, false, false, NONE), // not sure if ordering is correct (tIME, false, false, NONE), (iTXt, false, true, NONE), (tEXt, false, true, NONE), (vpAg, false, false, NONE), (zTXt, false, true, NONE) ]; little_exif-0.6.23/src/png/mod.rs000064400000000000000000000655471046102023000147450ustar 00000000000000// Copyright © 2022-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details pub mod chunk; mod read; mod text; use std::collections::VecDeque; use std::fs::File; use std::io::Cursor; use std::io::Seek; use std::io::SeekFrom; use std::io::Read; use std::io::Write; use std::ops::Neg; use std::path::Path; use crc::Crc; use crc::CRC_32_ISO_HDLC; use miniz_oxide::deflate::compress_to_vec_zlib; use text::construct_similar_with_new_data; use text::get_data_from_text_chunk; use crate::general_file_io::io_error; use crate::general_file_io::open_read_file; use crate::general_file_io::EXIF_HEADER; use crate::general_file_io::LITTLE_ENDIAN_INFO; use crate::general_file_io::BIG_ENDIAN_INFO; use crate::general_file_io::NEWLINE; use crate::general_file_io::SPACE; use crate::metadata::Metadata; use crate::png::chunk::PngChunk; use crate::png::read::read_chunk_length; use crate::png::read::read_chunk_name; use crate::png::read::read_chunk_data; use crate::png::read::read_chunk_crc; use crate::png::text::get_keyword_from_text_chunk; use crate::xmp::remove_exif_from_xmp; use crate::util::range_remove; pub(crate) const PNG_SIGNATURE: [u8; 8] = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; pub(crate) const RAW_PROFILE_TYPE_EXIF: [u8; 21] = [ 0x52, 0x61, 0x77, 0x20, // Raw 0x70, 0x72, 0x6F, 0x66, 0x69, 0x6C, 0x65, 0x20, // profile 0x74, 0x79, 0x70, 0x65, 0x20, // type 0x65, 0x78, 0x69, 0x66, // exif ]; pub(crate) const XML_COM_ADOBE_XMP: [u8; 17] = [ 0x58, 0x4d, 0x4c, 0x3a, // XML: 0x63, 0x6f, 0x6d, 0x2e, // com. 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, // adobe. 0x78, 0x6d, 0x70, // xmp ]; // The bytes during encoding need to be encoded themselves: // A given byte (e.g. 0x30 for the char '0') has two values in the string of its hex representation ('3' and '0') // These two characters need to be encoded themselves (51 for '3', 48 for '0'), resulting in the final encoded // version of the EXIF data // Independent of endian as this does not affect the ordering of values WITHIN a byte fn encode_byte(byte: &u8) -> [u8; 2] { [ byte / 16 + (if byte / 16 < 10 {b'0'} else {b'a' - 10}), byte % 16 + (if byte % 16 < 10 {b'0'} else {b'a' - 10}) ] } fn check_signature ( file_buffer: &Vec ) -> Result>, std::io::Error> { if !file_buffer.starts_with(&PNG_SIGNATURE) { return io_error!(InvalidData, "Can't open PNG file - Wrong signature!"); } // Signature is valid - can proceed using the data as PNG file let mut cursor = Cursor::new(file_buffer); cursor.set_position(8); return Ok(cursor); } fn file_check_signature ( path: &Path ) -> Result { let mut file = open_read_file(path)?; // Read & check the signature let mut signature_buffer = [0u8; 8]; let bytes_read = file.read(&mut signature_buffer)?; if bytes_read != 8 { return io_error!(InvalidData, "Can't open PNG file - Can't read signature!"); } check_signature(&signature_buffer.to_vec())?; // Signature is valid - can proceed using the file as PNG file return Ok(file); } /// "Parses" the PNG by checking various properties: /// - Can the file be opened and is the signature valid? /// - Are the various chunks OK or not? For this, the local subroutine `get_next_chunk_descriptor` is used pub(crate) fn vec_parse_png ( file_buffer: &Vec ) -> Result, std::io::Error> { let mut cursor = check_signature(file_buffer)?; return generic_parse_png(&mut cursor); } /// "Parses" the PNG by checking various properties: /// - Can the file be opened and is the signature valid? /// - Are the various chunks OK or not? For this, the local subroutine `get_next_chunk_descriptor` is used pub(crate) fn file_parse_png ( path: &Path ) -> Result, std::io::Error> { let mut file = file_check_signature(path)?; return generic_parse_png(&mut file); } fn generic_parse_png ( cursor: &mut T ) -> Result, std::io::Error> { let mut chunks = Vec::new(); loop { let chunk_descriptor = get_next_chunk_descriptor(cursor)?; chunks.push(chunk_descriptor); if let Some(last_chunk) = chunks.last() { if last_chunk.as_string() == "IEND" { break; } } } return Ok(chunks); } // TODO: Check if this is also affected by endianness // Edit: Should... not? I guess? fn get_next_chunk_descriptor ( cursor: &mut T ) -> Result { // Read the start of the chunk, its data and CRC let chunk_length = read_chunk_length(cursor)?; let chunk_name = read_chunk_name(cursor)?; let chunk_data = read_chunk_data(cursor, chunk_length as usize)?; let chunk_crc = read_chunk_crc(cursor)?; // Compute CRC on chunk let mut crc_input = Vec::new(); crc_input.extend(chunk_name.bytes()); crc_input.extend(chunk_data.iter()); let crc_struct = Crc::::new(&CRC_32_ISO_HDLC); let checksum = crc_struct.checksum(&crc_input); for (i, crc_byte) in chunk_crc.iter().enumerate().take(4) { if ((checksum >> (8 * (3-i))) as u8) != *crc_byte { return io_error!(InvalidData, "Checksum check failed while reading PNG!"); } } // If validating the chunk using the CRC was successful, return its descriptor // Note: chunk_length does NOT include the +4 for the CRC area! let png_chunk_result = PngChunk::from_string( &chunk_name.clone(), chunk_length ); match png_chunk_result { Ok(png_chunk) => { return Ok(png_chunk) }, Err(e) => { log::warn!("Unknown PNG chunk name: {chunk_name}"); return Ok(e) } }; } pub(crate) fn read_metadata ( file_buffer: &Vec ) -> Result, std::io::Error> { // Parse the PNG - if this fails, the read fails as well let parse_png_result = vec_parse_png(file_buffer)?; // Parsed PNG is Ok to use - Open the file and go through the chunks let mut cursor = check_signature(file_buffer)?; return generic_read_metadata(&mut cursor, &parse_png_result); } pub(crate) fn file_read_metadata ( path: &Path ) -> Result, std::io::Error> { // Parse the PNG - if this fails, the read fails as well let parse_png_result = file_parse_png(path)?; // Parsed PNG is Ok to use - Open the file and go through the chunks let mut file = file_check_signature(path)?; return generic_read_metadata(&mut file, &parse_png_result); } #[allow(non_snake_case)] fn generic_read_metadata ( cursor: &mut T, parsed_png: &Vec ) -> Result, std::io::Error> { for chunk in parsed_png { match chunk.as_string().as_str() { "eXIf" => { // Can be directly decoded // Skip chunk length and type (4+4 Bytes) cursor.seek(std::io::SeekFrom::Current(4+4))?; // Read chunk data into buffer // No need to verify this using CRC as already done by parse_png(path) let eXIf_chunk_data = read_chunk_data( cursor, chunk.length() as usize )?; return Ok(eXIf_chunk_data); }, "tEXt" | "zTXt" | "iTXt" => { // More common & expected case // Skip chunk length and type (4+4 Bytes) cursor.seek(std::io::SeekFrom::Current(4))?; let chunk_name = read_chunk_name(cursor)?; // Read chunk data into buffer // No need to verify this using CRC as already done by // previously calling parse_png(path) let chunk_data = read_chunk_data( cursor, chunk.length() as usize )?; // Check that this chunk contains raw profile EXIF data let keyword = get_keyword_from_text_chunk(&chunk_data); let mut has_raw_profile_type_exif = false; if keyword.len() == RAW_PROFILE_TYPE_EXIF.len() { has_raw_profile_type_exif = keyword .bytes() .zip(RAW_PROFILE_TYPE_EXIF.iter()) .all(|(a,b)| a == *b); } if !has_raw_profile_type_exif { // Skip CRC from current (wrong) chunk and continue cursor.seek(std::io::SeekFrom::Current(4))?; continue; } let decompressed_data = get_data_from_text_chunk( chunk_name.as_str(), &chunk_data )?; return decode_metadata_png(&decompressed_data); } _ => { cursor.seek(std::io::SeekFrom::Current(chunk.length() as i64 + 12))?; continue; } }; } return io_error!(Other, "No metadata found!"); } // Clears existing metadata chunk from a png file // Gets called before writing any new metadata #[allow(non_snake_case)] pub(crate) fn file_clear_metadata ( path: &Path ) -> Result<(), std::io::Error> { // Load the entire file into memory instead of reading one byte at a time // to improve the overall speed let mut file_buffer: Vec = std::fs::read(path)?; // Clear the metadata via the buffer based function clear_metadata(&mut file_buffer)?; // Write the file // Possible to optimize further by returning the purged bytestream itself? let mut file = std::fs::OpenOptions::new() .write(true) .truncate(true) .open(path)?; file.write_all(&file_buffer)?; return Ok(()); } // Clears existing metadata chunk from a png file // Gets called before writing any new metadata #[allow(non_snake_case)] pub(crate) fn clear_metadata ( file_buffer: &mut Vec ) -> Result<(), std::io::Error> { // Parse the PNG - if this fails, the clear operation fails as well let parse_png_result = vec_parse_png(file_buffer)?; // Parsed PNG is Ok to use - Open the file and go through the chunks let mut cursor = Cursor::new(file_buffer); // Skip the PNG file header (8 bytes) let mut remove_start; cursor.seek(std::io::SeekFrom::Current(8))?; for chunk in &parse_png_result { // Where the chunk that we might want to remove starts remove_start = cursor.stream_position()? as usize; match chunk.as_string().as_str() { "eXIf" => { // Remove the entire chunk (done after the match) }, "iTXt" | "zTXt" | "tEXt" => { // Skip chunk length and type (4+4 Bytes) cursor.seek(std::io::SeekFrom::Current(4+4))?; // Read chunk data into buffer for checking that this is the // correct chunk to delete let chunk_data = read_chunk_data( &mut cursor, chunk.length() as usize )?; let keyword = get_keyword_from_text_chunk(&chunk_data); // Compare to the "Raw profile type exif" string constant let mut has_raw_profile_type_exif = false; if keyword.len() == RAW_PROFILE_TYPE_EXIF.len() { has_raw_profile_type_exif = keyword .bytes() .zip(RAW_PROFILE_TYPE_EXIF.iter()) .all(|(a,b)| a == *b); } // Compare to the "XML:com.adobe.xmp" string constant let mut has_xml_com_adobe_xmp = false; if keyword.len() == XML_COM_ADOBE_XMP.len() { has_xml_com_adobe_xmp = keyword .bytes() .zip(XML_COM_ADOBE_XMP.iter()) .all(|(a,b)| a == *b); } if has_xml_com_adobe_xmp { // Don't fully remove the chunk, only remove EXIF from XMP // To do that, reposition the cursor to the start of the // entire chunk cursor.seek(SeekFrom::Current((chunk.length() as i64).neg()))?; cursor.seek(SeekFrom::Current(-8))?; clear_exif_from_xmp_metadata(&mut cursor, &chunk_data)?; continue; } // If this is not the correct zTXt/iTXt chunk, // ignore it, skip its CRC and continue with next chunk if !has_raw_profile_type_exif { cursor.seek(SeekFrom::Current(4))?; continue; } }, _ => { // In any other case, skip this chunk and continue with the // next one after adjusting the cursor cursor.seek(std::io::SeekFrom::Current(12 + chunk.length() as i64))?; continue; } } // As we haven't continued to the next chunk in a previous match arm, // we have now established that we want to remove this chunk. cursor.set_position(remove_start as u64); remove_chunk_at(&mut cursor)?; } return Ok(()); } /// Removes the chunk that starts at the given position. /// After that, cursor is positioned at the start of the next chunk. fn remove_chunk_at ( cursor: &mut Cursor<&mut Vec>, ) -> Result<(), std::io::Error> { let chunk_start_position = cursor.position() as usize; let chunk_length = read_chunk_length(cursor)?; // Seek to the end of the chunk, with the 8 additional bytes due to the // name and CRC fields cursor.seek(SeekFrom::Current(chunk_length as i64 + 8))?; let chunk_end_position = cursor.position() as usize; range_remove( cursor.get_mut(), chunk_start_position, chunk_end_position ); // Set the position of the cursor to the original start position cursor.set_position(chunk_start_position as u64); return Ok(()); } fn clear_exif_from_xmp_metadata ( cursor: &mut Cursor<&mut Vec>, chunk_data: &[u8], ) -> Result<(), std::io::Error> { // Read the chunk name and seek back let _ = read_chunk_length(cursor)?; let chunk_name = read_chunk_name(cursor)?; cursor.seek(SeekFrom::Current(-8))?; // Clear the EXIF from the XMP data let text_chunk_data = get_data_from_text_chunk( chunk_name.as_str(), chunk_data )?; let clean_xmp_data = match remove_exif_from_xmp(&text_chunk_data) { Ok(data) => data, Err(e) => { return io_error!(Other, format!("Failed to remove EXIF from XMP data: {}", e) ); } }; // Construct new chunk data field let new_chunk_data = construct_similar_with_new_data( chunk_name.as_str(), chunk_data, &clean_xmp_data )?; // Replace chunk remove_chunk_at(cursor)?; return write_chunk(cursor, chunk_name.as_str(), &new_chunk_data); } pub(crate) fn write_metadata ( file_buffer: &mut Vec, metadata: &Metadata ) -> Result<(), std::io::Error> { // First clear the existing metadata // This also parses the PNG and checks its validity, so it is safe to // assume that is, in fact, a usable PNG file clear_metadata(file_buffer)?; // Parsed PNG is Ok to use - Create a cursor for writing let mut cursor = Cursor::new(file_buffer); // Call the generic write function return generic_write_metadata(&mut cursor, metadata); } pub(crate) fn file_write_metadata ( path: &Path, metadata: &Metadata ) -> Result<(), std::io::Error> { // First clear the existing metadata // This also parses the PNG and checks its validity, so it is safe to // assume that is, in fact, a usable PNG file // For that, load the entire file into memory let mut file_buffer: Vec = std::fs::read(path)?; // Clear old metadata and write new to buffer write_metadata(&mut file_buffer, metadata)?; // Write the file // Possible to optimize further by returning the purged bytestream itself? let mut file = std::fs::OpenOptions::new() .write(true) .truncate(true) .open(path)?; file.write_all(&file_buffer)?; return Ok(()); } /// Assumes the cursor to be positioned at the insert position #[allow(non_snake_case)] fn write_chunk ( cursor: &mut T, chunk_name: &str, chunk_data: &[u8], ) -> Result<(), std::io::Error> { // Create a new vec for computing the CRC let mut data = chunk_name.as_bytes().to_vec(); data.extend(chunk_data); // Compute CRC and append it to the data vector let crc_struct = Crc::::new(&CRC_32_ISO_HDLC); let checksum = crc_struct.checksum(&data); for i in 0..4 { data.push( (checksum >> (8 * (3-i))) as u8); } // Prepare writing: // - Backup cursor position // - Read everything from there onwards into a buffer // - Go back to insert position let backup_cursor_position = cursor.stream_position()?; let mut buffer = Vec::new(); cursor.read_to_end(&mut buffer)?; cursor.seek(SeekFrom::Start(backup_cursor_position))?; // Write length of the new chunk (which is 8 bytes shorter than `data`) let chunk_data_len = chunk_data.len() as u32; for i in 0..4 { let bytes_written = cursor.write(&[(chunk_data_len >> (8 * (3-i))) as u8])?; assert_eq!(bytes_written, 1); } // Write data of new chunk, remember that position, write remaining PNG // data and revert position so that cursor now points to the chunk right // after the one that has been written cursor.write_all(&data)?; let end_of_written_chunk_cursor_position = cursor.stream_position()?; cursor.write_all(&buffer)?; cursor.seek(SeekFrom::Start(end_of_written_chunk_cursor_position))?; return Ok(()); } #[allow(non_snake_case)] fn generic_write_metadata ( cursor: &mut T, metadata: &Metadata ) -> Result<(), std::io::Error> { cursor.seek(SeekFrom::Start(8))?; let mut IHDR_length = 0u32; if let Ok(chunks) = generic_parse_png(cursor) { IHDR_length = chunks[0].length(); } // Encode the data specifically for PNG and open the image file let encoded_metadata = encode_metadata_png(&metadata.encode()?); let seek_start = 0u64 // Skip ... + PNG_SIGNATURE.len() as u64 // PNG Signature + IHDR_length as u64 // IHDR data section + 12 ; // rest of IHDR chunk (length, type, CRC) // Build data of new chunk using zlib compression (level=8 -> default) let zTXt_chunk_data: Vec = construct_zTXt_chunk_data( &Vec::new(), &encoded_metadata ); // Seek to insert position and write the chunk cursor.seek(SeekFrom::Start(seek_start))?; return write_chunk(cursor, "zTXt", &zTXt_chunk_data); } fn encode_metadata_png ( exif_vec: &Vec ) -> Vec { // The size of the EXIF data area, consists of // - length of EXIF header (follows after ssss) // - length of exif_vec // - 1 for ssss itself (why not 4? idk) let ssss = ( EXIF_HEADER.len() as u32 + exif_vec.len() as u32 + 1 ).to_string(); // Construct final vector with the bytes as they will be sent to the encoder // \n e x i f \n let mut png_exif: Vec = vec![NEWLINE, 0x65, 0x78, 0x69, 0x66, NEWLINE]; // Write ssss, padded to 8 bytes with leading spaces png_exif.extend(vec![SPACE; 8 - ssss.len()]); png_exif.extend(ssss.as_bytes().to_vec().iter()); png_exif.push(NEWLINE); // Write EXIF header and previously constructed EXIF data as encoded bytes for byte in &EXIF_HEADER { png_exif.extend(encode_byte(byte).iter()); } for byte in exif_vec { png_exif.extend(encode_byte(byte).iter()); } // Write end of EXIF data - 2* 0x30 results in the String "00" for 0x00 png_exif.push(0x30); png_exif.push(0x30); png_exif.push(NEWLINE); return png_exif; } fn decode_metadata_png ( encoded_data: &Vec ) -> Result, std::io::Error> { let mut exif_all: VecDeque = VecDeque::new(); let mut opt_other_byte: Option = None; // This performs the reverse operation to encode_byte: // Two succeeding bytes represent the ASCII values of the digits of // a hex value, e.g. 0x31, 0x32 represent '1' and '2', so the resulting // hex value is 0x12, which gets pushed onto exif_all for byte in encoded_data { // Ignore newline characters if *byte == b'\n' { continue; } if opt_other_byte.is_none() { opt_other_byte = Some(*byte); continue; } let Some(other_byte) = opt_other_byte else { return io_error!(Other, "Mangled PNG EXIF data encountered during decoding!"); }; let value_string = "".to_owned() + &(other_byte as char).to_string() + &(*byte as char).to_string(); if let Ok(value) = u8::from_str_radix(value_string.trim(), 16) { exif_all.push_back(value); } opt_other_byte = None; } // Now remove the first element until the exif header or endian information // is found. // Store the popped elements to get the size information let mut exif_header_found = false; let mut endian_info_found = false; let mut pop_storage: Vec = Vec::new(); while !exif_header_found && !endian_info_found { let mut counter = 0; for header_value in &EXIF_HEADER { if *header_value != exif_all[counter] { break; } counter += 1; } exif_header_found = counter == EXIF_HEADER.len(); if exif_header_found { break; } counter = 0; // But what if the EXIF_HEADER is missing and we are directly starting // with the endian information? See issue #54 for endian_info in &LITTLE_ENDIAN_INFO { if *endian_info != exif_all[counter] { break; } counter += 1; } endian_info_found = counter == LITTLE_ENDIAN_INFO.len(); if endian_info_found { break; } // And the same check for big endian for endian_info in &BIG_ENDIAN_INFO { if *endian_info != exif_all[counter] { break; } counter += 1; } endian_info_found = counter == BIG_ENDIAN_INFO.len(); if endian_info_found { break; } if let Some(next_byte) = exif_all.pop_front() { pop_storage.push(next_byte); } else { return io_error!( InvalidData, "Can't decode PNG EXIF data - No EXIF header or endian information found!" ); } } // The exif header has been found // -> exif_all now starts with the exif header information // -> pop_storage has in its last 4 elements the size information // that will now get extracted // Consider this part optional as it might be removed in the future and // isn't strictly necessary and just for validating the data we get assert!(!pop_storage.is_empty()); // Using the encode_byte function re-encode the bytes regarding the size // information and construct its value using decimal based shifting // Example: 153 = 0 // + 5*10*10^(2*0) + 3*1*10^(2*0) // + 0*10*10^(2*1) + 1*1*10^(2*1) let mut given_exif_len = 0u64; for i in 0..std::cmp::min(4, pop_storage.len()) { let re_encoded_byte = encode_byte(&pop_storage[pop_storage.len() -1 -i]); let tens_place = match (re_encoded_byte[0] as char).to_string().parse::() { Ok(value) => value, Err(_) => return io_error!(Other, "Mangled EXIF size info"), }; let ones_place = match (re_encoded_byte[1] as char).to_string().parse::() { Ok(value) => value, Err(_) => return io_error!(Other, "Mangled EXIF size info"), }; // (2*i) is small and fits into u32 safely given_exif_len += tens_place * 10 * 10_u64.pow((2 * i) as u32); given_exif_len += ones_place * 1 * 10_u64.pow((2 * i) as u32); } assert_eq!(given_exif_len, exif_all.len() as u64); // End optional part return Ok(Vec::from(exif_all)); } /// Provides the PNG specific encoding result as vector of bytes to be used /// by the user (e.g. in combination with another library) #[allow(non_snake_case)] pub(crate) fn as_u8_vec ( general_encoded_metadata: &Vec, as_zTXt_chunk: bool ) -> Vec { let basic_png_encode_result = encode_metadata_png(general_encoded_metadata); if !as_zTXt_chunk { return basic_png_encode_result; } return construct_zTXt_chunk_data( &[0x7a, 0x54, 0x58, 0x74], &basic_png_encode_result ); } #[allow(non_snake_case)] fn construct_zTXt_chunk_data ( prefix: &[u8], basic_png_encode_result: &[u8], ) -> Vec { // For further information on this see paragraph 11.3.3.3 of the this // document: https://www.w3.org/TR/png/#11zTXt // Build data of new chunk using zlib compression (level=8 -> default) let mut zTXt_chunk_data: Vec = Vec::new(); // Optional prefix, needed by the `as_u8_vec` function zTXt_chunk_data.extend(prefix.iter()); // Exif Keyword zTXt_chunk_data.extend(RAW_PROFILE_TYPE_EXIF.iter()); // Null separator that signals the end of the keyword zTXt_chunk_data.push(0x00); // The compression method for the zTXt chunk, with 0 telling a reader to // use the standard deflate compression zTXt_chunk_data.push(0x00); // The actual data bytes, compressed using the deflate method zTXt_chunk_data.extend(compress_to_vec_zlib(basic_png_encode_result, 8).iter()); return zTXt_chunk_data; } #[cfg(test)] mod tests { #[test] fn parsing_test() { let chunks = crate::png::file_parse_png( std::path::Path::new("tests/png_parse_test_image.png") ).unwrap(); assert_eq!(chunks.len(), 3); } } little_exif-0.6.23/src/png/read.rs000064400000000000000000000036211046102023000150620ustar 00000000000000// Copyright © 2025-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::Read; use std::io::Seek; use crate::general_file_io::io_error; use crate::util::read_4_bytes; use crate::util::read_be_u32; /// Assumes the cursor to be positioned at the start of the chunk where the /// length field is located. /// The function call advances the cursor by 4 bytes, which is where the /// chunk type field is located. pub(super) fn read_chunk_length ( cursor: &mut T ) -> Result { return read_be_u32(cursor); } /// Assumes the cursor to be positioned at the start of the chunk's name field. /// The function call advances the cursor by 4 bytes, which is where the /// chunk data is located. pub(super) fn read_chunk_name ( cursor: &mut T ) -> Result { let field = read_4_bytes(cursor)?; let name = String::from_utf8(field.to_vec()).unwrap_or_default(); return Ok(name); } /// Assumes the cursor to be positioned at the start of the chunk data /// Advances the cursor to the start of the chunk's CRC field pub(super) fn read_chunk_data ( cursor: &mut T, chunk_length: usize, ) -> Result, std::io::Error> { let mut chunk_data_buffer = vec![0u8; chunk_length]; let bytes_read = cursor.read(&mut chunk_data_buffer)?; if bytes_read != chunk_length { return io_error!(Other, "Could not read chunk data"); } return Ok(chunk_data_buffer); } /// Assumes the cursor to be positioned at the start of the chunk CRC field /// Advances the cursor by 4 bytes (start of next chunk) pub(super) fn read_chunk_crc ( cursor: &mut T ) -> Result<[u8; 4], std::io::Error> { let field = read_4_bytes(cursor)?; return Ok(field); }little_exif-0.6.23/src/png/text.rs000064400000000000000000000212361046102023000151350ustar 00000000000000// Copyright © 2025-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use miniz_oxide::inflate::decompress_to_vec_zlib; use miniz_oxide::deflate::compress_to_vec_zlib; use crate::general_file_io::io_error; /// This gets the keyword of a $TEXT chunk. /// Fortunately, this is the same for tEXt, zTXt and iTXt, as they all /// start with a keyword that is followed by a NUL separator. pub(crate) fn get_keyword_from_text_chunk ( chunk_data: &[u8] ) -> String { let mut keyword_buffer = Vec::new(); for character in chunk_data { if *character == 0x00 { break; } keyword_buffer.push(*character); } return String::from_utf8_lossy(&keyword_buffer).into_owned() } pub(crate) fn get_data_from_text_chunk ( chunk_name: &str, chunk_data: &[u8], ) -> Result, std::io::Error> { // The keyword length is required in all cases for determining the start // of the actual data let keyword_length = get_keyword_from_text_chunk(chunk_data).len(); match chunk_name { "tEXt" => { // Find start of data // For this we take the keyword length and add 1 for the NUL byte let data_start = keyword_length + 1; return Ok(chunk_data[data_start..].to_vec()); }, "zTXt" => { // Find start of data // For this we take the keyword length and add 2 for the NUL byte // and the byte representing the compression method let data_start = keyword_length + 2; // Check compression method if chunk_data[keyword_length + 1] != 0x00 { return io_error!( Other, "Unknown compression method for zTXt!" ); } // Decode zlib data if let Ok(decompressed_data) = decompress_to_vec_zlib( &chunk_data[data_start..] ) { return Ok(decompressed_data); } else { return io_error!( Other, "Could not inflate compressed chunk data!" ); } }, "iTXt" => { // We need more than just the keyword length let ( compression_flag, compression_method, _keyword, language_tag, translated_keyword, ) = get_info_about_iTXt_chunk(chunk_data); let data_start = keyword_length // keyword + 3 // NUL, compression flag & method + language_tag.len() // language tag + 1 // NUL + translated_keyword.len() // translated keyword + 1; // NUL if compression_flag == 0x00 { // No compression, simply return the data return Ok(chunk_data[data_start..].to_vec()); } if compression_method != 0x00 { return io_error!( Other, "Unknown compression method for iTXt!" ); } // Decode zlib data if let Ok(decompressed_data) = decompress_to_vec_zlib( &chunk_data[data_start..] ) { return Ok(decompressed_data); } else { return io_error!( Other, "Could not inflate compressed chunk data!" ); } }, _ => { return io_error!(Other, "Unknown text chunk!"); } } } pub(crate) fn construct_similar_with_new_data ( chunk_name: &str, old_chunk_data: &[u8], new_data: &[u8], ) -> Result, std::io::Error> { // Note: data is just the text after the keyword an so on, while *chunk* // data describes the entire data field that includes the keyword, the // compression information and so on // The keyword and NUL will be needed in every cases: let keyword = get_keyword_from_text_chunk(old_chunk_data); let mut new_chunk_data = keyword .bytes() .collect::>(); new_chunk_data.push(0x00); match chunk_name { "tEXt" => { new_chunk_data.extend(new_data); }, "zTXt" => { // Check compression method if old_chunk_data[keyword.len() + 1] != 0x00 { return io_error!( Other, "Unknown compression method for zTXt!" ); } new_chunk_data.extend(compress_to_vec_zlib(new_data, 8).iter()); }, "iTXt" => { // We need more than just the keyword let ( compression_flag, compression_method, _keyword, language_tag, translated_keyword, ) = get_info_about_iTXt_chunk(old_chunk_data); // Push compression information new_chunk_data.push(compression_flag); new_chunk_data.push(compression_method); // Add the language tag and translated keyword new_chunk_data.extend(language_tag.bytes()); new_chunk_data.push(0x00); new_chunk_data.extend(translated_keyword.bytes()); new_chunk_data.push(0x00); // Check compression if compression_flag == 0x00 { // No compression, simply add the new data new_chunk_data.extend(new_data); } else { if compression_method != 0x00 { return io_error!( Other, "Unknown compression method for iTXt!" ); } new_chunk_data.extend( compress_to_vec_zlib(new_data, 8).iter() ); } }, _ => { return io_error!(Other, "Unknown text chunk!"); } } return Ok(new_chunk_data); } /// The iTXt chunk is in its structure more complex than e.g. tEXt. The data /// section consists of (from the specifications, see paragraph 11.3.3.4 of /// https://www.w3.org/TR/png ): /// - Keyword 1-79 bytes (character string) /// - Null separator 1 byte (null character) /// - Compression flag 1 byte /// - Compression method 1 byte /// - Language tag 0 or more bytes (character string) /// - Null separator 1 byte (null character) /// - Translated keyword 0 or more bytes /// - Null separator 1 byte (null character) /// - Text 0 or more bytes #[allow(non_snake_case)] fn get_info_about_iTXt_chunk ( chunk_data: &[u8] ) -> ( u8, // compression flag u8, // compression method String, // keyword String, // language tag String, // translated keyword ) { // Tells us where we currently are in the chunk data let mut chunk_counter = 0; // Buffers for the different attributes let mut keyword_buffer = Vec::new(); loop { if chunk_data[chunk_counter] != 0x00 { keyword_buffer.push(chunk_data[chunk_counter]); chunk_counter += 1; } else { break; } } let _null_separator_1 = chunk_data[chunk_counter + 0]; let compression_flag = chunk_data[chunk_counter + 1]; let compression_method = chunk_data[chunk_counter + 2]; chunk_counter += 3; let mut language_tag_buffer = Vec::new(); loop { if chunk_data[chunk_counter] != 0x00 { language_tag_buffer.push(chunk_data[chunk_counter]); chunk_counter += 1; } else { break; } } chunk_counter += 1; let mut translated_keyword_buffer = Vec::new(); loop { if chunk_data[chunk_counter] != 0x00 { translated_keyword_buffer.push(chunk_data[chunk_counter]); chunk_counter += 1; } else { break; } } return ( compression_flag, compression_method, String::from_utf8(keyword_buffer ).unwrap_or_default(), String::from_utf8(language_tag_buffer ).unwrap_or_default(), String::from_utf8(translated_keyword_buffer).unwrap_or_default() ); }little_exif-0.6.23/src/rational.rs000064400000000000000000000127161046102023000152010ustar 00000000000000// Copyright © 2024-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details // Based on: https://github.com/google/audio-to-tactile/blob/main/src/dsp/number_util.c const MAX_TERM_COUNT: usize = 42; const CONVERGENCE_TOLERANCE: f64 = 1e-9; #[allow(non_camel_case_types)] #[derive(Clone, Debug, PartialEq)] pub struct uR64 { pub nominator: u32, pub denominator: u32 } #[allow(non_camel_case_types)] #[derive(Clone, Debug, PartialEq)] pub struct iR64 { pub nominator: i32, pub denominator: i32 } fn greatest_common_divisor ( mut a: u32, mut b: u32 ) -> u32 { while b != 0 { let remainder = a % b; a = b; b = remainder; } return a; } fn add_next_fraction_term ( term: &u32, convergent: &uR64, previous_convergent: &uR64, ) -> uR64 { return uR64 { nominator: term * convergent.nominator + previous_convergent.nominator, denominator: term * convergent.denominator + previous_convergent.denominator }; } fn rational64s_to_f64 ( fraction: &iR64 ) -> f64 { fraction.nominator as f64 / fraction.denominator as f64 } fn rational64u_to_f64 ( fraction: &uR64 ) -> f64 { fraction.nominator as f64 / fraction.denominator as f64 } fn f64_to_rational64s ( real_number: f64, ) -> iR64 { let best_approximation = f64_to_rational64u(real_number); return iR64 { nominator: if real_number < 0.0 { 0-best_approximation.nominator as i32 } else { best_approximation.nominator as i32 }, denominator: best_approximation.denominator as i32 }; } fn f64_to_rational64u ( real_number: f64, ) -> uR64 { // Make sure that we are dealing with positive real numbers let real_number = real_number.abs(); // Check if we are given a NaN value if real_number.is_nan() { return uR64 { nominator: 0, denominator: 0}; } // Check if real number is too large for us to handle if real_number > u32::MAX as f64 - 0.5 { return uR64 { nominator: i32::MAX as u32, denominator: 1}; } let mut reciprocal_residual = real_number; let mut continued_fraction_term = real_number.floor(); let mut previous_convergent = uR64 { nominator: 1u32, denominator: 0u32 }; let mut convergent = uR64 { nominator: continued_fraction_term as u32, denominator: 1u32 }; let mut n = 0; for _ in 2..MAX_TERM_COUNT { // Basically the value after the decimal point let next_residual = reciprocal_residual - continued_fraction_term; // If the difference is smaller than our tolerance we can return the // current representation if next_residual.abs() <= CONVERGENCE_TOLERANCE { return convergent; } reciprocal_residual = 1.0 / next_residual; continued_fraction_term = reciprocal_residual.floor(); n = (i32::MAX as u32 - previous_convergent.denominator) / convergent.denominator; if convergent.nominator > 0 { n = std::cmp::min( (u32::MAX - previous_convergent.nominator) / convergent.nominator, n ); } if continued_fraction_term >= n as f64 { break; } let next_convergent = add_next_fraction_term(&(continued_fraction_term as u32), &convergent, &previous_convergent); previous_convergent = convergent; convergent = next_convergent; } let mut best_approximation = convergent.clone(); // Add a final term if a semiconvergent further improves the approximation let lower_bound = continued_fraction_term / 2.0; if n as f64 >= lower_bound { if n as f64 > continued_fraction_term { n = continued_fraction_term as u32; } let semiconvergent = add_next_fraction_term(&n, &convergent, &previous_convergent); if (n as f64 > lower_bound) || ( (real_number - rational64u_to_f64(&semiconvergent)).abs() < (real_number - rational64u_to_f64(&convergent)).abs() ) { best_approximation = semiconvergent; } } let gcd = greatest_common_divisor( best_approximation.nominator, best_approximation.denominator ); return uR64 { nominator: best_approximation.nominator / gcd, denominator: best_approximation.denominator / gcd }; } impl From for uR64 { fn from (val: f64) -> Self { f64_to_rational64u(val) } } impl From for iR64 { fn from (val: f64) -> Self { f64_to_rational64s(val) } } impl From for uR64 { fn from (val: u32) -> Self { f64_to_rational64u(val as f64) } } impl From for iR64 { fn from (val: u32) -> Self { f64_to_rational64s(val as f64) } } impl From for iR64 { fn from (val: i32) -> Self { f64_to_rational64s(val as f64) } } impl From for f64 { fn from (val: uR64) -> Self { rational64u_to_f64(&val) } } impl From for f64 { fn from (val: iR64) -> Self { rational64s_to_f64(&val) } } impl From for u32 { fn from (val: uR64) -> Self { rational64u_to_f64(&val) as u32 } } impl From for u32 { fn from (val: iR64) -> Self { rational64s_to_f64(&val) as u32 } } impl From for i32 { fn from (val: iR64) -> Self { rational64s_to_f64(&val) as i32 } }little_exif-0.6.23/src/tiff/file.rs000064400000000000000000000024731046102023000152360ustar 00000000000000// Copyright © 2024 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::BufReader; use std::io::BufWriter; use std::path::Path; use crate::general_file_io::open_read_file; use crate::general_file_io::open_write_file; use crate::metadata::Metadata; use super::generic_read_metadata; use super::generic_write_metadata; pub(crate) fn read_metadata ( path: &Path ) -> Result, std::io::Error> { let mut buffered_file = BufReader::new(open_read_file(path)?); return generic_read_metadata(&mut buffered_file); } pub(crate) fn clear_metadata ( path: &Path ) -> Result<(), std::io::Error> { // Read in the data let raw_data = generic_read_metadata(&mut BufReader::new(open_read_file(path)?)); let mut data = Metadata::general_decoding_wrapper(raw_data)?; // Remove all IFDs that aren't required data.reduce_to_a_minimum(); // Write the reduced data back to the backup cursor generic_write_metadata(&mut BufWriter::new(open_write_file(path)?), &data)?; return Ok(()); } pub(crate) fn write_metadata ( path: &Path, metadata: &Metadata ) -> Result<(), std::io::Error> { let mut buffered_file = BufWriter::new(open_write_file(path)?); return generic_write_metadata(&mut buffered_file, metadata); }little_exif-0.6.23/src/tiff/mod.rs000064400000000000000000000074501046102023000150760ustar 00000000000000// Copyright © 2024, 2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::Seek; use std::io::Read; use std::io::Write; use crate::general_file_io::EXIF_HEADER; use crate::io_error; use crate::metadata::Metadata; use crate::ifd::ExifTagGroup::*; pub mod file; pub mod vec; pub(crate) fn generic_write_metadata ( cursor: &mut T, metadata: &Metadata ) -> Result<(), std::io::Error> { // First, check for required tags check_for_required_tags(metadata)?; // Does *not* call generic_clear_metadata, as the entire tiff data gets // overwritten anyways cursor.write_all(&metadata.encode()?)?; return Ok(()); } fn check_for_required_tags ( metadata: &Metadata ) -> Result<(), std::io::Error> { // First, check tags that are *definitely* required for TIFF compliance // ImageWidth: 0x0100 if metadata.get_tag_by_hex(0x0100, Some(GENERIC)).count() == 0 { return io_error!(NotFound, "TIFF requires ImageWidth (0x0100) tag!"); } // ImageHeight: 0x0101 if metadata.get_tag_by_hex(0x0101, Some(GENERIC)).count() == 0 { return io_error!(NotFound, "TIFF requires ImageHeight (0x0101) tag!"); } // Compression: 0x0103 if metadata.get_tag_by_hex(0x0103, Some(GENERIC)).count() == 0 { return io_error!(NotFound, "TIFF requires Compression (0x0103) tag!"); } // PhotometricInterpretation: 0x0106 if metadata.get_tag_by_hex(0x0106, Some(GENERIC)).count() == 0 { return io_error!(NotFound, "TIFF requires PhotometricInterpretation (0x0106) tag!"); } // StripOffsets: 0x0111 if metadata.get_tag_by_hex(0x0111, Some(GENERIC)).count() == 0 { return io_error!(NotFound, "TIFF requires StripOffsets (0x0111) tag!"); } // RowsPerStrip: 0x0116 if metadata.get_tag_by_hex(0x0116, Some(GENERIC)).count() == 0 { return io_error!(NotFound, "TIFF requires RowsPerStrip (0x0116) tag!"); } // StripByteCounts: 0x0117 if metadata.get_tag_by_hex(0x0117, Some(GENERIC)).count() == 0 { return io_error!(NotFound, "TIFF requires StripByteCounts (0x0117) tag!"); } // XResolution: 0x011A if metadata.get_tag_by_hex(0x011A, Some(GENERIC)).count() == 0 { return io_error!(NotFound, "TIFF requires XResolution (0x011A) tag!"); } // YResolution: 0x011B if metadata.get_tag_by_hex(0x011B, Some(GENERIC)).count() == 0 { return io_error!(NotFound, "TIFF requires YResolution (0x011B) tag!"); } // ResolutionUnit: 0x0128 if metadata.get_tag_by_hex(0x0128, Some(GENERIC)).count() == 0 { return io_error!(NotFound, "TIFF requires ResolutionUnit (0x0128) tag!"); } // Now check for tags that are required only by some TIFF variants // BitsPerSample: 0x0102 if metadata.get_tag_by_hex(0x0102, Some(GENERIC)).count() == 0 { log::warn!("All TIFF variants (except for bilevel graphics) require BitsPerSample (0x0102) tag!"); } // SamplesPerPixel: 0x0115 if metadata.get_tag_by_hex(0x0115, Some(GENERIC)).count() == 0 { log::warn!("Full-Color TIFFs require SamplesPerPixel (0x0115) tag!"); } // ColorMap: 0x0140 if metadata.get_tag_by_hex(0x0140, Some(GENERIC)).count() == 0 { log::warn!("Palette-Color TIFFs require ColorMap (0x0140) tag!"); } return Ok(()) } fn generic_read_metadata ( cursor: &mut T ) -> Result, std::io::Error> { let mut tiff_with_exif_header = Vec::new(); tiff_with_exif_header.extend(EXIF_HEADER); let mut buffer = Vec::new(); cursor.read_to_end(&mut buffer)?; tiff_with_exif_header.append(&mut buffer); return Ok(tiff_with_exif_header); } little_exif-0.6.23/src/tiff/vec.rs000064400000000000000000000023651046102023000150740ustar 00000000000000// Copyright © 2024 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::Cursor; use crate::metadata::Metadata; use super::generic_read_metadata; use super::generic_write_metadata; pub(crate) fn read_metadata ( file_buffer: &Vec ) -> Result, std::io::Error> { let mut cursor = Cursor::new(file_buffer); return generic_read_metadata(&mut cursor); } pub(crate) fn clear_metadata ( file_buffer: &mut Vec ) -> Result<(), std::io::Error> { // Create cursor let mut cursor = Cursor::new(file_buffer); let cursor_start_pos = cursor.position(); // Read in the data let raw_data = generic_read_metadata(&mut cursor); let mut data = Metadata::general_decoding_wrapper(raw_data)?; // Remove all IFDs that aren't required data.reduce_to_a_minimum(); // Write the reduced data back to the backup cursor cursor.set_position(cursor_start_pos); return generic_write_metadata(&mut cursor, &data); } pub(crate) fn write_metadata ( file_buffer: &mut Vec, metadata: &Metadata ) -> Result<(), std::io::Error> { let mut cursor = Cursor::new(file_buffer); return generic_write_metadata(&mut cursor, metadata); }little_exif-0.6.23/src/u8conversion.rs000064400000000000000000000176731046102023000160410ustar 00000000000000// Copyright © 2024-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io; use paste::paste; use crate::endian::Endian; use crate::rational::*; pub trait U8conversion { fn to_u8_vec ( &self, endian: &Endian, ) -> Vec; fn from_u8_vec ( u8_vec: &[u8], endian: &Endian ) -> T { match Self::from_u8_vec_res(u8_vec, endian) { Ok(value) => value, Err(_) => panic!("from_u8_vec: Mangled EXIF data encountered!") } } fn from_u8_vec_res ( u8_vec: &[u8], endian: &Endian ) -> Result; } macro_rules! build_u8conversion { ( $type:ty, $number_of_bytes:expr ) => { impl U8conversion<$type> for $type { fn to_u8_vec ( &self, endian: &Endian ) -> Vec { match *endian { Endian::Little => self.to_le_bytes().to_vec(), Endian::Big => self.to_be_bytes().to_vec(), } } fn from_u8_vec_res ( u8_vec: &[u8], endian: &Endian ) -> Result<$type, io::Error> { if u8_vec.len() != $number_of_bytes { return Err(io::Error::new( io::ErrorKind::InvalidData, "from_u8_vec_res: Mangled EXIF data encountered!" )); } let res = match *endian { Endian::Little => { ]}>::from_le_bytes( u8_vec[0..$number_of_bytes] .try_into() .map_err(|_| io::Error::new( io::ErrorKind::InvalidData, "from_u8_vec_res: Mangled EXIF data encountered!" ))? ) }, Endian::Big => { ]}>::from_be_bytes( u8_vec[0..$number_of_bytes] .try_into() .map_err(|_| io::Error::new( io::ErrorKind::InvalidData, "from_u8_vec_res: Mangled EXIF data encountered!" ))? ) }, }; Ok(res) } } } } build_u8conversion![u8, 1]; build_u8conversion![i8, 1]; build_u8conversion![u16, 2]; build_u8conversion![i16, 2]; build_u8conversion![u32, 4]; build_u8conversion![i32, 4]; build_u8conversion![u64, 8]; build_u8conversion![i64, 8]; build_u8conversion![f32, 4]; build_u8conversion![f64, 8]; impl U8conversion for String { fn to_u8_vec ( &self, _endian: &Endian ) -> Vec { let mut u8_vec = self.as_bytes().to_vec(); u8_vec.push(0x00_u8); return u8_vec; } fn from_u8_vec_res ( u8_vec: &[u8], _endian: &Endian ) -> Result { if let Ok(utf8_decode_result) = String::from_utf8(u8_vec.to_owned()) { // Drop null characters at the end return Ok(utf8_decode_result.trim_end_matches('\0').to_string()); } let mut result = String::new(); for byte in u8_vec { if *byte > 0 { result.push(*byte as char); } } Ok(result) } } impl U8conversion for uR64 { fn to_u8_vec ( &self, endian: &Endian ) -> Vec { let mut u8_vec = self.nominator.to_u8_vec(endian); u8_vec.extend(self.denominator.to_u8_vec(endian)); return u8_vec; } fn from_u8_vec_res ( u8_vec: &[u8], endian: &Endian ) -> Result { if u8_vec.len() != 8 { return Err(io::Error::new(io::ErrorKind::InvalidData, "from_u8_vec_res: Mangled EXIF data encountered!")); } let nominator = from_u8_vec_res_macro!(u32, &u8_vec[0..4], endian)?; let denominator = from_u8_vec_res_macro!(u32, &u8_vec[4..8], endian)?; Ok(uR64 { nominator, denominator }) } } impl U8conversion for iR64 { fn to_u8_vec ( &self, endian: &Endian ) -> Vec { let mut u8_vec = self.nominator.to_u8_vec(endian); u8_vec.extend(self.denominator.to_u8_vec(endian)); return u8_vec; } fn from_u8_vec_res ( u8_vec: &[u8], endian: &Endian ) -> Result { if u8_vec.len() != 8 { return Err(io::Error::new(io::ErrorKind::InvalidData, "from_u8_vec_res: Mangled EXIF data encountered!")); } let nominator = from_u8_vec_res_macro!(i32, &u8_vec[0..4], endian)?; let denominator = from_u8_vec_res_macro!(i32, &u8_vec[4..8], endian)?; Ok(iR64 { nominator, denominator }) } } macro_rules! build_vec_u8conversion { ( $type:ty, $number_of_bytes:expr ) => { impl U8conversion> for Vec<$type> { fn to_u8_vec ( &self, endian: &Endian ) -> Vec { let mut u8_vec = Vec::new(); for value in self { u8_vec.extend(<$type as U8conversion<$type>>::to_u8_vec(value, endian).iter()); } return u8_vec; } fn from_u8_vec_res ( u8_vec: &[u8], endian: &Endian ) -> Result, io::Error> { // The following "clippy allows" is for the case where we // we configure the conversion for 1-byte types like u8 or i8 // where the modulo operation will always return 0. #[allow(clippy::modulo_one)] if u8_vec.len() % $number_of_bytes != 0 { return Err(io::Error::new(io::ErrorKind::InvalidData, "from_u8_vec_res (Vec): Mangled EXIF data encountered!")); } let mut result: Vec<$type> = Vec::new(); for i in 0..(u8_vec.len() / $number_of_bytes) { result.push( <$type>::from_u8_vec( &u8_vec[(0 + i*$number_of_bytes)..((i+1)*$number_of_bytes)].to_vec(), endian ) as $type); } Ok(result) } } } } build_vec_u8conversion![u8, 1]; build_vec_u8conversion![i8, 1]; build_vec_u8conversion![u16, 2]; build_vec_u8conversion![i16, 2]; build_vec_u8conversion![u32, 4]; build_vec_u8conversion![i32, 4]; build_vec_u8conversion![u64, 8]; build_vec_u8conversion![i64, 8]; build_vec_u8conversion![f32, 4]; build_vec_u8conversion![f64, 8]; build_vec_u8conversion![uR64, 8]; build_vec_u8conversion![iR64, 8]; macro_rules! to_u8_vec_macro { ($type:ty, $value:expr, $endian:expr) => { <$type as U8conversion<$type>>::to_u8_vec($value, $endian) }; } macro_rules! from_u8_vec_res_macro { ($type:ty, $value:expr, $endian:expr) => { <$type as U8conversion<$type>>::from_u8_vec_res($value, $endian) } } pub(crate) use to_u8_vec_macro; pub(crate) use from_u8_vec_res_macro; little_exif-0.6.23/src/util.rs000064400000000000000000000236321046102023000143440ustar 00000000000000// Copyright © 2024-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::Read; use std::io::Seek; use crate::general_file_io::io_error; /// Reads in the next 1 bytes, starting at the current position of the cursor. /// The function call advances the cursor by 1 bytes. pub(crate) fn read_1_bytes ( cursor: &mut T ) -> Result<[u8; 1], std::io::Error> { // Read in the 1 bytes let mut field = [0u8; 1]; let bytes_read = cursor.read(&mut field)?; // Check that indeed 1 bytes were read if bytes_read != 1 { return io_error!(Other, "Could not read the next 1 bytes!"); } return Ok(field); } /// Reads in the next 2 bytes, starting at the current position of the cursor. /// The function call advances the cursor by 2 bytes. pub(crate) fn read_2_bytes ( cursor: &mut T ) -> Result<[u8; 2], std::io::Error> { // Read in the 2 bytes let mut field = [0u8; 2]; let bytes_read = cursor.read(&mut field)?; // Check that indeed 2 bytes were read if bytes_read != 2 { return io_error!(Other, "Could not read the next 2 bytes!"); } return Ok(field); } /// Reads in the next 3 bytes, starting at the current position of the cursor. /// The function call advances the cursor by 3 bytes. pub(crate) fn read_3_bytes ( cursor: &mut T ) -> Result<[u8; 3], std::io::Error> { // Read in the 3 bytes let mut field = [0u8; 3]; let bytes_read = cursor.read(&mut field)?; // Check that indeed 3 bytes were read if bytes_read != 3 { return io_error!(Other, "Could not read the next 3 bytes!"); } return Ok(field); } /// Reads in the next 4 bytes, starting at the current position of the cursor. /// The function call advances the cursor by 4 bytes. pub(crate) fn read_4_bytes ( cursor: &mut T ) -> Result<[u8; 4], std::io::Error> { // Read in the 4 bytes let mut field = [0u8; 4]; let bytes_read = cursor.read(&mut field)?; // Check that indeed 4 bytes were read if bytes_read != 4 { return io_error!(Other, "Could not read the next 4 bytes!"); } return Ok(field); } /// Reads in the next 8 bytes, starting at the current position of the cursor. /// The function call advances the cursor by 8 bytes. pub(crate) fn read_8_bytes ( cursor: &mut T ) -> Result<[u8; 8], std::io::Error> { // Read in the 8 bytes let mut field = [0u8; 8]; let bytes_read = cursor.read(&mut field)?; // Check that indeed 8 bytes were read if bytes_read != 8 { return io_error!(Other, "Could not read the next 8 bytes!"); } return Ok(field); } /// Reads in the next 16 bytes, starting at the current position of the cursor. /// The function call advances the cursor by 16 bytes. pub(crate) fn read_16_bytes ( cursor: &mut T ) -> Result<[u8; 16], std::io::Error> { // Read in the 16 bytes let mut field = [0u8; 16]; let bytes_read = cursor.read(&mut field)?; // Check that indeed 16 bytes were read if bytes_read != 16 { return io_error!(Other, "Could not read the next 16 bytes!"); } return Ok(field); } /// Reads in a u16 in big endian format at the current cursor position /// The function call advances the cursor by 2 bytes. pub(crate) fn read_be_u16 ( cursor: &mut T ) -> Result { let bytes = read_2_bytes(cursor)?; return Ok(bytes[0] as u16 * 256 + bytes[1] as u16); } /// Reads in a u32 in big endian format at the current cursor position /// The function call advances the cursor by 4 bytes. pub(crate) fn read_be_u32 ( cursor: &mut T ) -> Result { let bytes = read_4_bytes(cursor)?; let mut value = 0u32; for byte in bytes { value = value * 256 + byte as u32; } return Ok(value); } /// Reads in a u64 in big endian format at the current cursor position /// The function call advances the cursor by 8 bytes. pub(crate) fn read_be_u64 ( cursor: &mut T ) -> Result { let bytes = read_8_bytes(cursor)?; let mut value = 0u64; for byte in bytes { value = value * 256 + byte as u64; } return Ok(value); } pub(crate) fn read_null_terminated_string ( cursor: &mut T ) -> Result { let mut string_buffer = Vec::new(); let mut character_buffer = read_1_bytes(cursor)?; while character_buffer[0] != 0x00 { string_buffer.push(character_buffer[0]); character_buffer = read_1_bytes(cursor)?; } return String::from_utf8(string_buffer).map_err( |_e| std::io::Error::new ( std::io::ErrorKind::InvalidData, "Could not convert byte data to UTF-8 string!" ) ); } /// Inserts a slice into a vector at a given offset, shifting elements /// starting at the offset towards the end. /// Returns 0 (zero) if the operation was successful, non-zero if the offset /// is larger than the current length of the destination vector. In the latter /// case, everything stays untouched. pub(crate) fn insert_multiple_at ( vec_dst: &mut Vec, offset: usize, vec_src: &mut Vec, ) -> usize where T: Copy { match (vec_dst.len(), vec_src.len()) { (_, 0) => 0, (current_len, _) => { // If this is true we return at this point as this would cause a // "gap" between existing and new vector contents if current_len < offset { return std::cmp::max(1, current_len); } // Reserve without over-allocation space needed for new elements vec_dst.reserve_exact(vec_src.len()); let mut temp = vec_dst.split_off(offset); vec_dst.append(vec_src); vec_dst.append(&mut temp); return 0; }, } } /// Removes a section in the middle of a vector. The element at index `start` /// is where the removal starts, up to the element prior to at index `end` /// The element originally positioned at `end` will survive. pub(crate) fn range_remove ( vec: &mut Vec, start: usize, end: usize ) where T: Copy { // Invalid input, nothing to do here if start > end { return; } let old_vec_len = vec.len(); // Simply truncating is sufficient in this case if end >= old_vec_len { vec.truncate(start); return; } // Otherwise, move the elements starting at end over to the left for (dst_offset, src_index) in (end..old_vec_len).enumerate() { vec[start + dst_offset] = vec[src_index]; } // Resize the vector to cut off any residue from the shifting operations let new_vec_len = old_vec_len - (end - start); vec.truncate(new_vec_len); } /* /// Inserts a slice into a vector at a given offset, shifting elements /// starting at the offset towards the end. /// Returns 0 (zero) if the operation was successful, non-zero if the offset /// is larger than the current length of the destination vector. In the latter /// case, everything stays untouched. pub(crate) fn insert_multiple_at ( vec_dst: &mut Vec, offset: usize, vec_src: &mut [T] ) -> usize where T: Copy { match (vec_dst.len(), vec_src.len()) { (_, 0) => 0, (current_len, _) => { // Elements that need to be moved to make way for the new ones let move_count = current_len - offset; // If this is less than 0 we return at this point as this would // cause a "gap" between existing and new vector contents // (move_count is usize and thus can't be less than 0) if current_len < offset { return std::cmp::max(1, current_len); } // Reserve without over-allocation space needed for new elements vec_dst.reserve_exact(vec_src.len()); unsafe { // Pointer to the first location where vec_src elements will // be placed. // Called `src` at this stage as previously it has to serve // as source for elements that require to be copied to the // right to make way let src = vec_dst.as_mut_ptr().offset(offset as isize); // Set the new length of the vector after the operation vec_dst.set_len(current_len + vec_src.len()); // Check if there are any elements that require to be moved if move_count > 0 { let dst = src.offset(vec_src.len() as isize); std::ptr::copy( src, // Source pointer dst, // Destination pointer move_count // How many elements to copy ); } // Copy the new elements at the new "free" locations // The previous source pointer `src` becomes the destination of // the new elements to be inserted // In contrast to the previous copy we can here be sure that // the source and destination don't overlap std::ptr::copy_nonoverlapping( vec_src.as_mut_ptr(), // Source pointer src, // Destination pointer vec_src.len() // How many elements to copy (here: ALL) ); } return 0; }, } } */little_exif-0.6.23/src/webp/file.rs000064400000000000000000000602271046102023000152440ustar 00000000000000// Copyright © 2024-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::fs::File; use std::io::Read; use std::io::Write; use std::io::Seek; use std::io::SeekFrom; use std::path::Path; use crate::endian::*; use crate::metadata::Metadata; use crate::u8conversion::*; use crate::general_file_io::*; use super::riff_chunk::RiffChunk; use super::riff_chunk::RiffChunkDescriptor; use super::*; /// A WebP file starts as follows /// - The RIFF signature: ASCII characters "R", "I", "F", "F" -> 4 bytes /// - The file size starting at offset 8 -> 4 bytes /// - The WEBP signature: ASCII characters "W", "E", "B", "P" -> 4 bytes /// /// This function checks these 3 sections and their correctness after making /// sure that the file actually exists and can be opened. /// Finally, the file struct is returned for further processing fn check_signature ( path: &Path ) -> Result { let mut file = open_write_file(path)?; // Get the first 12 bytes that are required for the following checks let mut first_12_bytes = [0u8; 12]; let bytes_read = file.read(&mut first_12_bytes)?; if bytes_read != 12 { return io_error!(InvalidData, "Can't open WebP file - Can't read & check signature!"); } let first_12_bytes_vec = first_12_bytes.to_vec(); // Perform checks check_riff_signature(&first_12_bytes_vec )?; check_byte_count( &first_12_bytes_vec, Some(&file))?; check_webp_signature(&first_12_bytes_vec )?; // Signature is valid - can proceed using the file as WebP file return Ok(file); } /// Gets the next RIFF chunk, starting at the current file cursor /// Advances the cursor to the start of the next chunk fn get_next_chunk ( file: &mut T ) -> Result { // Read the start of the chunk let mut chunk_start = [0u8; 8]; let mut bytes_read = file.read(&mut chunk_start)?; // Check that indeed 8 bytes were read if bytes_read != 8 { return io_error!(UnexpectedEof, "Could not read start of chunk"); } // Construct name of chunk and its length let chunk_name = String::from_utf8(chunk_start[0..4].to_vec()); let mut chunk_length = from_u8_vec_res_macro!(u32, &chunk_start[4..8], &Endian::Little)?; // Account for the possible padding byte chunk_length += chunk_length % 2; // Read RIFF chunk data let mut chunk_data_buffer = vec![0u8; chunk_length as usize]; bytes_read = file.read(&mut chunk_data_buffer)?; if bytes_read != chunk_length as usize { return io_error!( Other, format!("Could not read RIFF chunk data! Expected {chunk_length} bytes but read {bytes_read}") ); } if let Ok(parsed_chunk_name) = chunk_name { return Ok(RiffChunk::new( parsed_chunk_name as String, chunk_length as usize, chunk_data_buffer as Vec )); } else { return io_error!(Other, "Could not parse RIFF fourCC chunk name!"); } } /// Gets a descriptor of the next RIFF chunk, starting at the current file /// cursor position. Advances the cursor to the start of the next chunk /// Relies on `get_next_chunk` by basically calling that function and throwing /// away the actual payload fn get_next_chunk_descriptor ( file: &mut T ) -> Result { let next_chunk_result = get_next_chunk(file)?; return Ok(next_chunk_result.descriptor()); } /// "Parses" the WebP file by checking various properties: /// - Can the file be opened and is the signature valid, including the file size? /// - Are the chunks and their size descriptions OK? Relies on the local subroutine `get_next_chunk_descriptor` pub(crate) fn parse_webp ( path: &Path ) -> Result, std::io::Error> { let mut file = check_signature(path)?; let mut chunks = Vec::new(); // The amount of data we expect to read while parsing the chunks let expected_length = file.metadata()?.len(); // How much data we have parsed so far. // Starts with 12 bytes: // - 4 bytes for RIFF signature // - 4 bytes for file size // - 4 bytes for WEBP signature // These bytes are already read in by the `check_signature` subroutine let mut parsed_length = 12u64; loop { let next_chunk_descriptor_result = get_next_chunk_descriptor(&mut file); if let Ok(chunk_descriptor) = next_chunk_descriptor_result { // The parsed length increases by the length of the chunk's // header (4 byte) + it's size section (4 byte) and the payload // size, which is noted by the aforementioned size section parsed_length += 4u64 + 4u64 + chunk_descriptor.len() as u64; // Add the chunk descriptor chunks.push(chunk_descriptor); if parsed_length == expected_length { // In this case we don't expect any more data to be in the file break; } } else { // This is the case when the read of the next chunk descriptor // fails due to not being able to fetch 8 bytes for the header and // chunk size information, indicating that there is no further data // in the file and we are done with parsing. // If the subroutine fails due to other reasons, the error gets // propagated further. if let Err(e) = next_chunk_descriptor_result { if e.kind() == std::io::ErrorKind::UnexpectedEof { break; } else { return Err(e); } } } } return Ok(chunks); } fn check_exif_in_file ( path: &Path ) -> Result<(File, Vec), std::io::Error> { // Parse the WebP file - if this fails, we surely can't read any metadata let parsed_webp_result = parse_webp(path)?; // Next, check if this is an Extended File Format WebP file // In this case, the first Chunk SHOULD have the type "VP8X" // Otherwise, the file is either invalid ("VP8X" at wrong location) or a // Simple File Format WebP file which don't contain any EXIF metadata. if let Some(first_chunk) = parsed_webp_result.first() { // Compare the chunk descriptor header. if first_chunk.header().to_lowercase() != VP8X_HEADER.to_lowercase() { return io_error!( Other, format!("Expected first chunk of WebP file to be of type 'VP8X' but instead got {}!", first_chunk.header()) ); } } else { return io_error!(Other, "Could not read first chunk descriptor of WebP file!"); } // Finally, check the flag by opening up the file and reading the data of // the VP8X chunk // Regarding the seek: // - RIFF + file size + WEBP -> 12 byte // - VP8X header -> 4 byte // - VP8X chunk size -> 4 byte let mut file = check_signature(path)?; let mut flag_buffer = vec![0u8; 4usize]; file.seek(SeekFrom::Start(12u64 + 4u64 + 4u64))?; if file.read(&mut flag_buffer)? != 4 { return io_error!(Other, "Could not read flags of VP8X chunk!"); } // Check the 5th bit of the 32 bit flag_buffer. // For further details see the Extended File Format section at // https://developers.google.com/speed/webp/docs/riff_container#extended_file_format if flag_buffer[0] & 0x08 != 0x08 { return io_error!(Other, "No EXIF chunk according to VP8X flags!"); } return Ok((file, parsed_webp_result)); } /// Reads the raw EXIF data from the WebP file. Note that if the file contains /// multiple such chunks, the first one is returned and the others get ignored. pub(crate) fn read_metadata ( path: &Path ) -> Result, std::io::Error> { // Check the file signature, parse it, check that it has a VP8X chunk and // the EXIF flag is set there let (mut file, parse_webp_result) = check_exif_in_file(path)?; // At this point we have established that the file has to contain an EXIF // chunk at some point. So, now we need to find & return it // Start by seeking to the start of the first chunk and visiting chunk after // chunk via checking the type and seeking again to the next chunk via the // size information file.seek(SeekFrom::Start(12u64))?; let mut header_buffer = vec![0u8; 4usize]; let mut chunk_index = 0usize; loop { // Read the chunk type into the buffer if file.read(&mut header_buffer)? != 4 { return io_error!(Other, "Could not read chunk type while traversing WebP file!"); } let chunk_type = String::from_u8_vec(&header_buffer.to_vec(), &Endian::Little); // Check that this is still the type that we expect from the previous // parsing over the file // TODO: Maybe remove this part? let Some(chunk_at_index) = parse_webp_result.get(chunk_index) else { return io_error!( Other, format!("Could not get chunk descriptor at index {} while traversing WebP file!", chunk_index) ); }; let expected_chunk_type = chunk_at_index.header(); if chunk_type != expected_chunk_type { return io_error!( Other, format!("Got unexpected chunk type! Expected {} but got {}", expected_chunk_type, chunk_type ) ); } // Get the size of this chunk from the previous parsing process and skip // the 4 bytes regarding the size let chunk_size = chunk_at_index.len(); file.seek(std::io::SeekFrom::Current(4))?; if chunk_type.to_lowercase() == EXIF_CHUNK_HEADER.to_lowercase() { // Read the EXIF chunk's data into a buffer let mut payload_buffer = vec![0u8; chunk_size]; let bytes_read = file.read(&mut payload_buffer)?; if bytes_read != chunk_size { return io_error!( Other, format!("Could not read entire EXIF chunk data! Expected {chunk_size} bytes but read {bytes_read}") ); } // Add the 6 bytes of the EXIF_HEADER as Prefix for the generic EXIF // data parser that is called on the result of this read function // Otherwise the result would directly start with the Endianness // information, leading to a failed EXIF header signature check in // the function `decode_metadata_general` let mut raw_exif_data = EXIF_HEADER.to_vec(); raw_exif_data.append(&mut payload_buffer); return Ok(raw_exif_data); } else { // Skip the entire chunk file.seek(std::io::SeekFrom::Current(chunk_size as i64))?; // Note that we have to seek another byte in case the chunk is of // uneven size to account for the padding byte that must be included file.seek(std::io::SeekFrom::Current(chunk_size as i64 % 2))?; } // Update for next loop iteration chunk_index += 1; } } fn update_file_size_information ( file: &mut File, delta: i32 ) -> Result<(), std::io::Error> { // Note from the documentation: // As the size of any chunk is even, the size given by the RIFF header is also even. // Update the file size information, first by reading in the current value... file.seek(SeekFrom::Start(4))?; let mut file_size_buffer = [0u8; 4]; // ...converting it to u32 representation... let bytes_read = file.read(&mut file_size_buffer)?; if bytes_read != 4 { return io_error!(Other, "Could not read file size information from WebP file!"); } let old_file_size = from_u8_vec_res_macro!(u32, &file_size_buffer, &Endian::Little)?; // ...adding the delta byte count (and performing some checks)... if delta < 0 { assert!(old_file_size as i32 > delta); } let new_file_size = (old_file_size as i32 + delta) as u32; assert!(old_file_size % 2 == 0); assert!(new_file_size % 2 == 0); // ...and writing back to file... file.seek(SeekFrom::Start(4))?; file.write_all(&to_u8_vec_macro!(u32, &new_file_size, &Endian::Little))?; Ok(()) } fn convert_to_extended_format ( file: &mut File ) -> Result<(), std::io::Error> { // Start by getting the first chunk of the WebP file file.seek(SeekFrom::Start(12))?; let first_chunk = get_next_chunk(file)?; // Find out what simple type of WebP file we are dealing with let (width, height) = match first_chunk.descriptor().header().as_str() { "VP8 " => get_dimension_info_from_vp8_chunk(first_chunk.payload()), "VP8L" => get_dimension_info_from_vp8l_chunk(first_chunk.payload()), _ => io_error!(Other, "Expected either 'VP8 ' or 'VP8L' chunk for conversion!") }?; let width_vec = to_u8_vec_macro!(u32, &width, &Endian::Little); let height_vec = to_u8_vec_macro!(u32, &height, &Endian::Little); let mut vp8x_chunk = vec![ 0x56, 0x50, 0x38, 0x58, // ASCII chars "V", "P", "8", "X" -> 4 byte 0x0A, 0x00, 0x00, 0x00, // size of this chunk (32 + 24 + 24 bit = 10 byte) -> 4 byte 0x00, 0x00, 0x00, 0x00, // Flags and reserved area -> 4 byte ]; // Add the two 24 bits for width and height information for byte in width_vec.iter().take(3) { vp8x_chunk.push(*byte); } for byte in height_vec.iter().take(3) { vp8x_chunk.push(*byte); } // Write the VP8X chunk, first by reading the file (except for the header) // into a buffer... let mut buffer = Vec::new(); file.seek(SeekFrom::Start(12u64))?; file.read_to_end(&mut buffer)?; // ...actually writing the VP8X chunk data... file.seek(SeekFrom::Start(12u64))?; if file.write(&vp8x_chunk)? != vp8x_chunk.len() { return io_error!(Other, "Could not write entire VP8X chunk data!"); } // ...and writing back the file contents if file.write(&buffer)? != buffer.len() { return io_error!(Other, "Could not write back entire WebP file data!"); } // Finally, update the file size information update_file_size_information(file, 18)?; Ok(()) } fn get_dimension_info_from_vp8l_chunk ( payload: &[u8], ) -> Result<(u32, u32), std::io::Error> { // Get the 4 bytes containing the dimension information // (although we only need 28 bits) // Starting at byte 1 instead of 0 due to the 0x2F byte // See: https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification#3_riff_header let width_height_info_buffer = payload[1..5].to_vec(); // Convert to a single u32 number for bit-mask operations let width_height_info = from_u8_vec_res_macro!(u32, &width_height_info_buffer, &Endian::Little)?; let mut width = 0; let mut height = 0; // Get the first 14 bit to construct the width for bit_index in 0..14 { width |= ((width_height_info >> (27 - bit_index)) & 0x01) << (13 - (bit_index % 14)); } // Get the next 14 bit to construct the height for bit_index in 14..28 { height |= ((width_height_info >> (27 - bit_index)) & 0x01) << (13 - (bit_index % 14)); } return Ok((width, height)); } fn set_exif_flag ( path: &Path, exif_flag_value: bool ) -> Result<(), std::io::Error> { // Parse the WebP file - if this fails, we surely can't read any metadata let parsed_webp_result = parse_webp(path)?; // Open the file for further processing let mut file = check_signature(path)?; // Next, check if this is an Extended File Format WebP file // In this case, the first Chunk SHOULD have the type "VP8X" // Otherwise we have to create the VP8X chunk! if let Some(first_chunk) = parsed_webp_result.first() { // Compare the chunk descriptor header and call chunk creator if required if first_chunk.header().to_lowercase() != VP8X_HEADER.to_lowercase() { convert_to_extended_format(&mut file)?; } } else { return io_error!(Other, "Could not read first chunk descriptor of WebP file!"); } // At this point we know that we have a VP8X chunk at the expected location // So, read in the flags and set the EXIF flag according to the given bool let mut flag_buffer = vec![0u8; 4usize]; file.seek(SeekFrom::Start(12u64 + 4u64 + 4u64))?; if file.read(&mut flag_buffer)? != 4 { return io_error!(Other, "Could not read flags of VP8X chunk!"); } // Mask the old flag by either or-ing with 1 at the EXIF flag position for // setting it to true, or and-ing with 1 everywhere but the EXIF flag pos // to set it to false flag_buffer[0] = if exif_flag_value { flag_buffer[0] | 0x08 } else { flag_buffer[0] & 0b11110111 }; // Write flag buffer back to the file file.seek(SeekFrom::Start(12u64 + 4u64 + 4u64))?; file.write_all(&flag_buffer)?; Ok(()) } pub(crate) fn clear_metadata ( path: &Path ) -> Result<(), std::io::Error> { // Check the file signature, parse it, check that it has a VP8X chunk and // the EXIF flag is set there let (mut file, parse_webp_result) = match check_exif_in_file(path) { Ok((file, parse_webp_result)) => (file, parse_webp_result), Err(e) => { match e.to_string().as_str() { "No EXIF chunk according to VP8X flags!" => return Ok(()), "Expected first chunk of WebP file to be of type 'VP8X' but instead got VP8L!" => return Ok(()), "Expected first chunk of WebP file to be of type 'VP8X' but instead got VP8 !" => return Ok(()), _ => return Err(e) } } }; // Compute a delta of how much the file size information has to change let mut delta = 0i32; // Skip the WEBP signature file.seek(std::io::SeekFrom::Current(4i64))?; for parsed_chunk in parse_webp_result { // At the start of each iteration, the file cursor is at the start of // the fourCC section of a chunk // Compute how many bytes this chunk has let parsed_chunk_byte_count = 4u64 // fourCC section of EXIF chunk + 4u64 // size information of EXIF chunk + parsed_chunk.len() as u64 // actual size of EXIF chunk data + parsed_chunk.len() as u64 % 2 // accounting for possible padding byte ; // Not an EXIF chunk, seek to next one and continue if parsed_chunk.header().to_lowercase() != EXIF_CHUNK_HEADER.to_lowercase() { file.seek(std::io::SeekFrom::Current(parsed_chunk_byte_count as i64))?; continue; } // Get the current size of the file in bytes let old_file_byte_count = file.metadata()?.len(); // Get a backup of the current cursor position let exif_chunk_start_cursor_position = SeekFrom::Start(file.stream_position()?); // Skip the EXIF chunk ... file.seek(std::io::SeekFrom::Current(parsed_chunk_byte_count as i64))?; // ...and copy everything afterwards into a buffer... let mut buffer = Vec::new(); file.read_to_end(&mut buffer)?; // ...and seek back to where the EXIF chunk is located... file.seek(exif_chunk_start_cursor_position)?; // ...and overwrite the EXIF chunk... file.write_all(&buffer)?; // ...and finally update the size of the file file.set_len(old_file_byte_count - parsed_chunk_byte_count)?; // Additionally, update the size information that gets written to the // file header after this loop delta -= parsed_chunk_byte_count as i32; } // Update file size information update_file_size_information(&mut file, delta)?; // Set the flags in the VP8X chunk. First, read in the current flags set_exif_flag(path, false)?; return Ok(()); } /// Writes the given generally encoded metadata to the WebP image file at /// the specified path. /// Note that *all* previously stored EXIF metadata gets removed first before /// writing the "new" metadata. pub(crate) fn write_metadata ( path: &Path, metadata: &Metadata ) -> Result<(), std::io::Error> { // Clear the metadata from the file and return if this results in an error clear_metadata(path)?; // Encode the general metadata format to WebP specifications let encoded_metadata = encode_metadata_webp(&metadata.encode()?); // Open the file... let mut file = check_signature(path)?; // ...and find a location where to put the EXIF chunk // This is done by requesting a chunk descriptor as long as we find a chunk // that is both known and should be located *before* the EXIF chunk let pre_exif_chunks = [ "VP8X", "VP8", "VP8L", "ICCP", "ANIM" ]; loop { // Request a chunk descriptor. If this fails, check the error // Depending on its type, either continue normally or return it let chunk_descriptor_result = get_next_chunk_descriptor(&mut file); match chunk_descriptor_result { Ok(chunk_descriptor) => { let mut chunk_type_found_in_pre_exif_chunks = false; // Check header of chunk descriptor against any of the known chunks // that should come before the EXIF chunk for pre_exif_chunk in &pre_exif_chunks { chunk_type_found_in_pre_exif_chunks |= pre_exif_chunk.to_lowercase() == chunk_descriptor.header().to_lowercase(); } if !chunk_type_found_in_pre_exif_chunks { break; } }, Err(e) => { match e.kind() { std::io::ErrorKind::UnexpectedEof => break, // No further chunks, place EXIF chunk here _ => return Err(e) } } } } // Next, read remaining file into a buffer... let current_file_cursor = SeekFrom::Start(file.stream_position()?); let mut read_buffer = Vec::new(); file.read_to_end(&mut read_buffer)?; // ...and write the EXIF chunk at the previously found location... file.seek(current_file_cursor)?; file.write_all(&encoded_metadata)?; // ...and writing back the remaining file content file.write_all(&read_buffer)?; // Update the file size information by adding the byte count of the EXIF chunk // (Note: Due to the WebP specific encoding function, this vector already // contains the EXIF header characters and size information, as well as the // possible padding byte. Therefore, simply taking the length of this // vector takes their byte count also into account and no further values // need to be added) update_file_size_information(&mut file, encoded_metadata.len() as i32)?; // Finally, set the EXIF flag set_exif_flag(path, true)?; return Ok(()); } #[cfg(test)] mod tests { use std::fs::copy; use std::fs::remove_file; use std::path::Path; #[test] fn clear_metadata() -> Result<(), std::io::Error> { // Remove file from previous run and replace it with fresh copy if let Err(error) = remove_file("tests/read_sample_no_exif.webp") { println!("{}", error); } copy("tests/read_sample.webp", "tests/read_sample_no_exif.webp")?; // Clear the metadata crate::webp::file::clear_metadata(Path::new("tests/read_sample_no_exif.webp"))?; Ok(()) } } little_exif-0.6.23/src/webp/mod.rs000064400000000000000000000117551046102023000151060ustar 00000000000000// Copyright © 2024, 2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details pub mod file; pub mod vec; mod riff_chunk; pub(crate) const RIFF_SIGNATURE: [u8; 4] = [0x52, 0x49, 0x46, 0x46]; pub(crate) const WEBP_SIGNATURE: [u8; 4] = [0x57, 0x45, 0x42, 0x50]; pub(crate) const VP8X_HEADER: &str = "VP8X"; pub(crate) const EXIF_CHUNK_HEADER: &str = "EXIF"; use std::fs::File; use crate::endian::Endian; use crate::general_file_io::io_error; use crate::io_error_plain; use crate::u8conversion::from_u8_vec_res_macro; use crate::u8conversion::to_u8_vec_macro; use crate::u8conversion::U8conversion; fn check_riff_signature ( file_buffer: &[u8], ) -> Result<(), std::io::Error> { let bytes_to_check = match file_buffer.get(0..4) { Some(bytes) => bytes, None => { return io_error!(InvalidData, "Can't open WebP file - File too small to contain RIFF signature!"); } }; if bytes_to_check != RIFF_SIGNATURE { return io_error!( InvalidData, format!("Can't open WebP file - Expected RIFF signature but found {}!", from_u8_vec_res_macro!(String, bytes_to_check, &Endian::Big)?) ); } return Ok(()); } fn check_webp_signature ( file_buffer: &[u8], ) -> Result<(), std::io::Error> { let Some(buffer_to_check) = file_buffer.get(8..12) else { return io_error!(InvalidData, "Can't open WebP file - File too small to contain WEBP signature!"); }; if buffer_to_check != WEBP_SIGNATURE { return io_error!( InvalidData, format!("Can't open WebP file - Expected WEBP signature but found {}!", from_u8_vec_res_macro!(String, buffer_to_check, &Endian::Big)?) ); } return Ok(()); } fn check_byte_count ( file_buffer: &[u8], opt_file: Option<&File>, ) -> Result<(), std::io::Error> { let byte_count = from_u8_vec_res_macro!( u32, &file_buffer[4..8], &Endian::Little )?.checked_add(8).ok_or( io_error_plain!(InvalidData, "Can't open WebP file - Byte count in RIFF header is too large!") )?; if let Some(file) = opt_file { if file.metadata()?.len() != byte_count as u64 { return io_error!(InvalidData, "Can't open WebP file - Promised byte count does not correspond with file size!"); } } else if file_buffer.len() != byte_count as usize { return io_error!(InvalidData, format!("Can't handle WebP file buffer - Promised byte count {} does not correspond with file buffer length {}!", byte_count, file_buffer.len())); } return Ok(()); } fn encode_metadata_webp ( exif_vec: &[u8], ) -> Vec { // Vector storing the data that will be returned let mut webp_exif: Vec = Vec::new(); // Compute the length of the exif data chunk // This does NOT include the fourCC and size information of that chunk // Also does NOT include the padding byte, i.e. this value may be odd! let length = exif_vec.len() as u32; // Start with the fourCC chunk head and the size information. // Then copy the previously encoded EXIF data webp_exif.extend([0x45, 0x58, 0x49, 0x46]); webp_exif.extend(to_u8_vec_macro!(u32, &length, &Endian::Little)); webp_exif.extend(exif_vec.iter()); // Add the padding byte if required if length % 2 != 0 { webp_exif.extend([0x00]); } return webp_exif; } /// Provides the WebP specific encoding result as vector of bytes to be used /// by the user (e.g. in combination with another library) pub(crate) fn as_u8_vec ( general_encoded_metadata: &[u8], ) -> Vec { encode_metadata_webp(general_encoded_metadata) } fn get_dimension_info_from_vp8_chunk ( payload: &[u8] ) -> Result<(u32, u32), std::io::Error> { // Get the bytes containing the VP8 frame header info // See: // VP8 Chunk: https://developers.google.com/speed/webp/docs/riff_container#simple_file_format_lossy // VP8 Data Format https://datatracker.ietf.org/doc/html/rfc6386#section-9.1 // Parsing function function vp8_parse_frame_header: https://datatracker.ietf.org/doc/html/rfc6386#section-20.4 let header_magic = payload[3..=5].to_vec(); if header_magic.len() != 3 || !matches!(header_magic.as_slice(), &[0x9d, 0x01, 0x2a]) { return io_error!(Other, "Invalid VP8 Frame Header Magic"); } let header_width_bytes = payload[6..=7].to_vec(); let header_height_bytes = payload[8..=9].to_vec(); let width_info = from_u8_vec_res_macro!(u16, &header_width_bytes, &Endian::Little)?; let height_info = from_u8_vec_res_macro!(u16, &header_height_bytes, &Endian::Little)?; // zero out the top 2 bits of each of the dimensions (scaling factor bits) let bitmask_14 = (1 << 14) - 1; let width = width_info & bitmask_14; let height = height_info & bitmask_14; return Ok((width as u32 -1, height as u32 -1)); }little_exif-0.6.23/src/webp/riff_chunk.rs000064400000000000000000000027631046102023000164440ustar 00000000000000// Copyright © 2024, 2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details #[allow(non_snake_case)] #[derive(Clone)] pub(crate) struct RiffChunkDescriptor { fourCC: String, // The 4 byte long header at the start of the chunk size: usize, // Chunk size WITHOUT the 8 bytes for the header and size section } impl RiffChunkDescriptor { #[allow(non_snake_case)] pub fn new ( fourCC: String, size: usize ) -> RiffChunkDescriptor { RiffChunkDescriptor { fourCC: fourCC, size: size } } pub fn len ( &self ) -> usize { self.size } pub fn header ( &self ) -> String { self.fourCC.clone() } } pub(crate) struct RiffChunk { descriptor: RiffChunkDescriptor, payload: Vec } impl RiffChunk { #[allow(non_snake_case)] pub fn new ( fourCC: String, size: usize, payload: Vec ) -> RiffChunk { RiffChunk { descriptor: RiffChunkDescriptor::new(fourCC, size), payload: payload } } pub fn descriptor ( &self ) -> RiffChunkDescriptor { self.descriptor.clone() } pub fn payload ( &self ) -> &Vec { &self.payload } } little_exif-0.6.23/src/webp/vec.rs000064400000000000000000000543451046102023000151060ustar 00000000000000// Copyright © 2024-2026 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use std::io::Cursor; use std::io::Read; use std::io::Seek; use std::io::Write; use crate::general_file_io::EXIF_HEADER; use crate::metadata::Metadata; use crate::util::insert_multiple_at; use crate::util::range_remove; use super::riff_chunk::RiffChunk; use super::riff_chunk::RiffChunkDescriptor; use super::*; /// A WebP file starts as follows /// - The RIFF signature: ASCII characters "R", "I", "F", "F" -> 4 bytes /// - The file size starting at offset 8 -> 4 bytes /// - The WEBP signature: ASCII characters "W", "E", "B", "P" -> 4 bytes /// /// This individually checks these bytes using the dedicated functions fn check_signature ( file_buffer: &Vec ) -> Result>, std::io::Error> { if file_buffer.len() < 12 { return io_error!(InvalidData, "Can't open WebP file - File too small to contain required signatures!"); } check_riff_signature(file_buffer )?; check_byte_count( file_buffer, None)?; check_webp_signature(file_buffer )?; let mut cursor = Cursor::new(file_buffer); cursor.set_position(12); return Ok(cursor); } /// Gets the next RIFF chunk, starting at the current file buffer cursor /// Advances the cursor to the start of the next chunk fn get_next_chunk ( cursor: &mut Cursor<&Vec> ) -> Result { // Read the start of the chunk let mut chunk_start = [0u8; 8]; let mut bytes_read = cursor.read(&mut chunk_start)?; // Check that indeed 8 bytes were read if bytes_read != 8 { return io_error!(UnexpectedEof, "Could not read start of chunk"); } // Construct name of chunk and its length let chunk_name = String::from_utf8(chunk_start[0..4].to_vec()); let mut chunk_length = from_u8_vec_res_macro!(u32, &chunk_start[4..8], &Endian::Little)?; // Account for the possible padding byte chunk_length += chunk_length % 2; // Read RIFF chunk data let mut chunk_data_buffer = vec![0u8; chunk_length as usize]; bytes_read = cursor.read(&mut chunk_data_buffer)?; if bytes_read != chunk_length as usize { return io_error!( Other, format!("Could not read RIFF chunk data! Expected {chunk_length} bytes but read {bytes_read}") ); } if let Ok(parsed_chunk_name) = chunk_name { return Ok(RiffChunk::new( parsed_chunk_name as String, chunk_length as usize, chunk_data_buffer as Vec )); } else { return io_error!(Other, "Could not parse RIFF fourCC chunk name!"); } } /// Gets a descriptor of the next RIFF chunk, starting at the current buffer /// cursor position. Advances the cursor to the start of the next chunk /// Relies on `get_next_chunk` by basically calling that function and throwing /// away the actual payload fn get_next_chunk_descriptor ( cursor: &mut Cursor<&Vec> ) -> Result { let next_chunk_result = get_next_chunk(cursor)?; return Ok(next_chunk_result.descriptor()); } /// "Parses" the WebP file by checking various properties: /// - Can the file be opened and is the signature valid, including the file size? /// - Are the chunks and their size descriptions OK? Relies on the local subroutine `get_next_chunk_descriptor` pub(crate) fn parse_webp ( file_buffer: &Vec ) -> Result, std::io::Error> { let mut cursor = check_signature(file_buffer)?; let mut chunks = Vec::new(); // The amount of data we expect to read while parsing the chunks let expected_length = file_buffer.len(); // How much data we have parsed so far. // Starts with 12 bytes: // - 4 bytes for RIFF signature // - 4 bytes for file size // - 4 bytes for WEBP signature // These bytes are already read in by the `check_signature` subroutine let mut parsed_length = 12; loop { let next_chunk_descriptor_result = get_next_chunk_descriptor(&mut cursor); if let Ok(chunk_descriptor) = next_chunk_descriptor_result { // The parsed length increases by the length of the chunk's // header (4 byte) + it's size section (4 byte) and the payload // size, which is noted by the aforementioned size section parsed_length += 4 + 4 + chunk_descriptor.len(); // Add the chunk descriptor chunks.push(chunk_descriptor); if parsed_length == expected_length { // In this case we don't expect any more data to be in the file break; } } else { // This is the case when the read of the next chunk descriptor // fails due to not being able to fetch 8 bytes for the header and // chunk size information, indicating that there is no further data // in the file and we are done with parsing. // If the subroutine fails due to other reasons, the error gets // propagated further. if let Err(e) = next_chunk_descriptor_result { if e.kind() == std::io::ErrorKind::UnexpectedEof { break; } else { return Err(e); } } } } return Ok(chunks); } fn check_exif_in_file ( file_buffer: &Vec ) -> Result<(Cursor<&Vec>, Vec), std::io::Error> { // Parse the WebP file - if this fails, we surely can't read any metadata let parsed_webp_result = parse_webp(file_buffer)?; // Next, check if this is an Extended File Format WebP file // In this case, the first Chunk SHOULD have the type "VP8X" // Otherwise, the file is either invalid ("VP8X" at wrong location) or a // Simple File Format WebP file which don't contain any EXIF metadata. if let Some(first_chunk) = parsed_webp_result.first() { // Compare the chunk descriptor header. if first_chunk.header().to_lowercase() != VP8X_HEADER.to_lowercase() { return io_error!( Other, format!("Expected first chunk of WebP file to be of type 'VP8X' but instead got {}!", first_chunk.header()) ); } } else { return io_error!(Other, "Could not read first chunk descriptor of WebP file!"); } // Finally, check the flag by opening up the file and reading the data of // the VP8X chunk // Regarding the seek: // - RIFF + file size + WEBP -> 12 byte // - VP8X header -> 4 byte // - VP8X chunk size -> 4 byte let mut cursor = check_signature(file_buffer)?; let mut flag_buffer = vec![0u8; 4usize]; cursor.set_position(12u64 + 4u64 + 4u64); if cursor.read(&mut flag_buffer)? != 4 { return io_error!(Other, "Could not read flags of VP8X chunk!"); } // Check the 5th bit of the 32 bit flag_buffer. // For further details see the Extended File Format section at // https://developers.google.com/speed/webp/docs/riff_container#extended_file_format if flag_buffer[0] & 0x08 != 0x08 { return io_error!(Other, "No EXIF chunk according to VP8X flags!"); } return Ok((cursor, parsed_webp_result)); } /// Reads the raw EXIF data from the WebP file. Note that if the file contains /// multiple such chunks, the first one is returned and the others get ignored. pub(crate) fn read_metadata ( file_buffer: &Vec ) -> Result, std::io::Error> { // Check the signature, parse it, check that it has a VP8X chunk and the // EXIF flag is set there let (mut cursor, parse_webp_result) = check_exif_in_file(file_buffer)?; // At this point we have established that the file has to contain an EXIF // chunk at some point. So, now we need to find & return it // Start by seeking to the start of the first chunk and visiting chunk after // chunk via checking the type and seeking again to the next chunk via the // size information cursor.set_position(12u64); let mut header_buffer = vec![0u8; 4usize]; let mut chunk_index = 0usize; loop { // Read the chunk type into the buffer if cursor.read(&mut header_buffer)? != 4 { return io_error!(Other, "Could not read chunk type while traversing WebP file!"); } let chunk_type = String::from_u8_vec(&header_buffer.to_vec(), &Endian::Little); // Check that this is still the type that we expect from the previous // parsing over the file // TODO: Maybe remove this part? let Some(chunk_at_index) = parse_webp_result.get(chunk_index) else { return io_error!( Other, format!("Could not get chunk descriptor at index {} while traversing WebP file!", chunk_index) ); }; let expected_chunk_type = chunk_at_index.header(); if chunk_type != expected_chunk_type { return io_error!( Other, format!("Got unexpected chunk type! Expected {} but got {}", expected_chunk_type, chunk_type) ); } // Get the size of this chunk from the previous parsing process and skip // the 4 bytes regarding the size let chunk_size = chunk_at_index.len(); cursor.seek(std::io::SeekFrom::Current(4))?; if chunk_type.to_lowercase() == EXIF_CHUNK_HEADER.to_lowercase() { // Read the EXIF chunk's data into a buffer let mut payload_buffer = vec![0u8; chunk_size]; let bytes_read = cursor.read(&mut payload_buffer)?; if bytes_read != chunk_size { return io_error!( Other, format!("Could not read entire EXIF chunk data! Expected {chunk_size} bytes but read {bytes_read}") ); } // Add the 6 bytes of the EXIF_HEADER as Prefix for the generic EXIF // data parser that is called on the result of this read function // Otherwise the result would directly start with the Endianness // information, leading to a failed EXIF header signature check in // the function `decode_metadata_general` let mut raw_exif_data = EXIF_HEADER.to_vec(); raw_exif_data.append(&mut payload_buffer); return Ok(raw_exif_data); } else { // Skip the entire chunk cursor.seek(std::io::SeekFrom::Current(chunk_size as i64))?; // Note that we have to seek another byte in case the chunk is of // uneven size to account for the padding byte that must be included cursor.seek(std::io::SeekFrom::Current(chunk_size as i64 % 2))?; } // Update for next loop iteration chunk_index += 1; } } fn update_file_size_information ( cursor: &mut Cursor<&mut Vec>, delta: i32 ) -> Result<(), std::io::Error> { // Note from the documentation: // As the size of any chunk is even, the size given by the RIFF header is also even. // Update the file size information, first by reading in the current value... let file_size_buffer = cursor.get_ref()[4..8].to_vec(); // ...converting it to u32 representation... let old_file_size = from_u8_vec_res_macro!(u32, &file_size_buffer, &Endian::Little)?; // ...adding the delta byte count (and performing some checks)... if delta < 0 { assert!(old_file_size as i32 > delta); } let new_file_size = (old_file_size as i32 + delta) as u32; assert!(old_file_size % 2 == 0); assert!(new_file_size % 2 == 0); // ...and writing back to file... cursor.set_position(4); cursor.write_all(&to_u8_vec_macro!(u32, &new_file_size, &Endian::Little))?; Ok(()) } fn convert_to_extended_format ( cursor: &mut Cursor<&mut Vec> ) -> Result<(), std::io::Error> { // Start by getting the first chunk of the WebP file // Clippy is a bit stupid here, the as_ref is required as otherwise the // read_cursor provided to get_next_chunk is of a mutable container. // This happens again in the function write_metadata further down. #[allow(clippy::useless_asref)] let mut read_cursor = Cursor::new(cursor.get_ref().as_ref()); read_cursor.set_position(12); let first_chunk = get_next_chunk(&mut read_cursor)?; // Find out what simple type of WebP file we are dealing with let (width, height) = match first_chunk.descriptor().header().as_str() { "VP8 " => {log::debug!("VP8 !"); io_error!(Other, "Conversion from Simple File Format with 'VP8' chunk to Extended File Format not yet implemented!") }, "VP8L" => get_dimension_info_from_vp8l_chunk(first_chunk.payload()), _ => io_error!(Other, format!("Expected either 'VP8 ' or 'VP8L' chunk for conversion but got {:?}!", first_chunk.descriptor().header().as_str())) }?; let width_vec = to_u8_vec_macro!(u32, &width, &Endian::Little); let height_vec = to_u8_vec_macro!(u32, &height, &Endian::Little); let mut vp8x_chunk = vec![ 0x56, 0x50, 0x38, 0x58, // ASCII chars "V", "P", "8", "X" -> 4 byte 0x0A, 0x00, 0x00, 0x00, // size of this chunk (32 + 24 + 24 bit = 10 byte) -> 4 byte 0x00, 0x00, 0x00, 0x00, // Flags and reserved area -> 4 byte ]; // Add the two 24 bits for width and height information for byte in width_vec.iter().take(3) { vp8x_chunk.push(*byte); } for byte in height_vec.iter().take(3) { vp8x_chunk.push(*byte); } // Write the VP8X chunk insert_multiple_at(cursor.get_mut(), 12, &mut vp8x_chunk); // Finally, update the file size information update_file_size_information(cursor, 18)?; Ok(()) } fn get_dimension_info_from_vp8l_chunk ( payload: &[u8], ) -> Result<(u32, u32), std::io::Error> { // Get the 4 bytes containing the dimension information // (although we only need 28 bits) // Starting at byte 1 instead of 0 due to the 0x2F byte // See: https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification#3_riff_header let width_height_info_buffer = payload[1..5].to_vec(); // Convert to a single u32 number for bit-mask operations let width_height_info = from_u8_vec_res_macro!(u32, &width_height_info_buffer, &Endian::Little)?; let mut width = 0; let mut height = 0; // Get the first 14 bit to construct the width for bit_index in 0..14 { width |= ((width_height_info >> (27 - bit_index)) & 0x01) << (13 - (bit_index % 14)); } // Get the next 14 bit to construct the height for bit_index in 14..28 { height |= ((width_height_info >> (27 - bit_index)) & 0x01) << (13 - (bit_index % 14)); } return Ok((width, height)); } fn set_exif_flag ( cursor: &mut Cursor<&mut Vec>, exif_flag_value: bool ) -> Result<(), std::io::Error> { // Parse the WebP file - if this fails, we surely can't read any metadata let parsed_webp_result = parse_webp(cursor.get_ref())?; // Next, check if this is an Extended File Format WebP file // In this case, the first Chunk SHOULD have the type "VP8X" // Otherwise we have to create the VP8X chunk! if let Some(first_chunk) = parsed_webp_result.first() { // Compare the chunk descriptor header and call chunk creator if required if first_chunk.header().to_lowercase() != VP8X_HEADER.to_lowercase() { convert_to_extended_format(cursor)?; } } else { return io_error!(Other, "Could not read first chunk descriptor of WebP file!"); } // At this point we know that we have a VP8X chunk at the expected location // Mask the old flag by either or-ing with 1 at the EXIF flag position for // setting it to true, or and-ing with 1 everywhere but the EXIF flag pos // to set it to false cursor.get_mut()[20] = if exif_flag_value { cursor.get_ref()[20] | 0x08 } else { cursor.get_ref()[20] & 0b11110111 }; Ok(()) } pub(crate) fn clear_metadata ( file_buffer: &mut Vec ) -> Result<(), std::io::Error> { // Check the file signature, parse it, check that it has a VP8X chunk and // the EXIF flag is set there let (mut cursor, parse_webp_result) = match check_exif_in_file(file_buffer) { Ok((_file, parse_webp_result)) => ( Cursor::new(file_buffer), parse_webp_result ), Err(e) => { match e.to_string().as_str() { "No EXIF chunk according to VP8X flags!" => return Ok(()), "Expected first chunk of WebP file to be of type 'VP8X' but instead got VP8L!" => return Ok(()), "Expected first chunk of WebP file to be of type 'VP8X' but instead got VP8 !" => return Ok(()), _ => return Err(e) } } }; // Compute a delta of how much the file size information has to change let mut delta = 0i32; // Skip the WEBP signature cursor.set_position(4); for parsed_chunk in parse_webp_result { // At the start of each iteration, the file cursor is at the start of // the fourCC section of a chunk // Compute how many bytes this chunk has let parsed_chunk_byte_count = 4u64 // fourCC section of EXIF chunk + 4u64 // size information of EXIF chunk + parsed_chunk.len() as u64 // actual size of EXIF chunk data + parsed_chunk.len() as u64 % 2 // accounting for possible padding byte ; // Not an EXIF chunk, seek to next one and continue if parsed_chunk.header().to_lowercase() != EXIF_CHUNK_HEADER.to_lowercase() { cursor.seek(std::io::SeekFrom::Current(parsed_chunk_byte_count as i64))?; continue; } // Remove the range containing the EXIF chunk let remove_start = cursor.position() as usize; let remove_end = remove_start + parsed_chunk_byte_count as usize; range_remove(cursor.get_mut(), remove_start, remove_end); // Additionally, update the size information that gets written to the // file header after this loop delta -= parsed_chunk_byte_count as i32; } // Update file size information update_file_size_information(&mut cursor, delta)?; // Set the flags in the VP8X chunk. First, read in the current flags set_exif_flag(&mut cursor, false)?; return Ok(()); } /// Writes the given generally encoded metadata to the WebP image file at /// the specified path. /// Note that *all* previously stored EXIF metadata gets removed first before /// writing the "new" metadata. pub(crate) fn write_metadata ( file_buffer: &mut Vec, metadata: &Metadata ) -> Result<(), std::io::Error> { // Clear the metadata from the file and return if this results in an error clear_metadata(file_buffer)?; // Encode the general metadata format to WebP specifications let mut encoded_metadata = encode_metadata_webp(&metadata.encode()?); let encoded_metadata_len = encoded_metadata.len() as i32; // Find a location where to put the EXIF chunk // This is done by requesting a chunk descriptor as long as we find a chunk // that is both known and should be located *before* the EXIF chunk let pre_exif_chunks = [ "VP8X", "VP8", "VP8L", "ICCP", "ANIM" ]; // Clippy wrongly needs this allowance, otherwise it states that this call // to as_ref does nothing. However, it *does* something, mainly // creating an immutable reference that gets passed as a copy to the // constructor of the cursor, so file_buffer is later on still available // when calling insert_multiple_at. // I guess there is a less messy solution to this? #[allow(clippy::useless_asref)] let mut read_cursor = Cursor::new(file_buffer.as_ref()); loop { // Request a chunk descriptor. If this fails, check the error // Depending on its type, either continue normally or return it let chunk_descriptor_result = get_next_chunk_descriptor(&mut read_cursor); match chunk_descriptor_result { Ok(chunk_descriptor) => { let mut chunk_type_found_in_pre_exif_chunks = false; // Check header of chunk descriptor against any of the known chunks // that should come before the EXIF chunk for pre_exif_chunk in &pre_exif_chunks { chunk_type_found_in_pre_exif_chunks |= pre_exif_chunk.to_lowercase() == chunk_descriptor.header().to_lowercase(); } if !chunk_type_found_in_pre_exif_chunks { break; } }, Err(e) => { match e.kind() { std::io::ErrorKind::UnexpectedEof => break, // No further chunks, place EXIF chunk here _ => return Err(e) } } } } // Write the EXIF chunk at the found location insert_multiple_at(file_buffer, read_cursor.position() as usize, &mut encoded_metadata); // Update the file size information by adding the byte count of the EXIF chunk // (Note: Due to the WebP specific encoding function, this vector already // contains the EXIF header characters and size information, as well as the // possible padding byte. Therefore, simply taking the length of this // vector takes their byte count also into account and no further values // need to be added) let mut write_cursor = Cursor::new(file_buffer); update_file_size_information(&mut write_cursor, encoded_metadata_len)?; // Finally, set the EXIF flag set_exif_flag(&mut write_cursor, true)?; return Ok(()); } little_exif-0.6.23/src/xmp.rs000064400000000000000000000102721046102023000141670ustar 00000000000000// Copyright © 2025 Tobias J. Prisching and CONTRIBUTORS // See https://github.com/TechnikTobi/little_exif#license for licensing details use quick_xml::events::BytesStart; use quick_xml::events::Event; use quick_xml::Reader; use quick_xml::Writer; use std::io::Cursor; /// Some images also contain XMP metadata, which in turn may include EXIF data /// that is simply a duplicate from e.g. the eXIf chunk in a PNG. /// This function takes in the raw XMP information and removes EXIF attributes, /// while maintaining other XMP information so that the result can be /// written back to the image data. pub(crate) fn remove_exif_from_xmp ( data: &[u8] ) -> Result, Box> { let mut reader = Reader::from_reader(data); let mut writer = Writer::new(Cursor::new(Vec::new())); // Needed by the reader let mut read_buffer = Vec::new(); // Needed for skipping stuff like // Hi\n let mut skip_depth = 0u32; let mut skip_next_nl = false; loop { // Read in the event let read_event = reader.read_event_into(&mut read_buffer); match read_event { Ok(Event::Start(ref event)) => { let event_name = String::from_utf8(event.name().0.to_vec())?; if event_name.starts_with("exif:") { skip_depth += 1; } else if skip_depth == 0 { writer.write_event(Event::Start(get_exif_filtered_event(event)?))?; } } Ok(Event::Empty(ref event)) => { let event_name = String::from_utf8(event.name().0.to_vec())?; if event_name.starts_with("exif:") { // do nothing } else if skip_depth == 0 { writer.write_event(Event::Empty(get_exif_filtered_event(event)?))?; } } Ok(Event::End(ref event)) => { if skip_depth > 0 { skip_depth -= 1; skip_next_nl = true; } else { writer.write_event(Event::End(event.clone()))?; } } Ok(Event::Eof) => { assert_eq!(skip_depth, 0); break; } Ok(Event::Text(ref event)) => { let event_string = String::from_utf8(event.to_vec())?; let characters = event_string.chars() .filter(|c| *c == '\n' || !c.is_whitespace()) .collect::>(); if characters == vec!['\n'] && skip_next_nl { skip_next_nl = false; } else if skip_depth == 0 { writer.write_event(Event::Text(event.clone()))?; } } Ok(other_event) => { if skip_depth == 0 { writer.write_event(other_event)?; } } Err(error_message) => { log::error!( "Error at position {}: {:?}", reader.buffer_position(), error_message ); break; } }; read_buffer.clear(); } return Ok(writer.into_inner().into_inner()); } fn get_exif_filtered_event<'a> ( event: &'a BytesStart<'a> ) -> Result, Box> { let mut new_event = BytesStart::new( std::str::from_utf8(event.name().0)? ); new_event.extend_attributes( event.attributes() .filter_map(Result::ok) .filter(|attribute| { if let Ok(key) = std::str::from_utf8( attribute.key.as_ref() ) { !key.starts_with("exif:") } else { true } } ), ); return Ok(new_event); }