From bf68ff8ef016ef72cfc4593c7085398fe3aaf016 Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Wed, 19 Apr 2023 14:36:49 +0900 Subject: [PATCH] Import avif-parse 1.0.0 --- .cargo_vcs_info.json | 6 + Cargo.toml | 55 ++ Cargo.toml.orig | 45 + LICENSE | 373 ++++++++ README.md | 53 ++ av1-avif/LICENSE | 25 + av1-avif/README.md | 15 + av1-avif/testFiles/Link-U/README.md | 232 +++++ avif_parse.h | 47 + src/boxes.rs | 215 +++++ src/c_api.rs | 56 ++ src/lib.rs | 1290 +++++++++++++++++++++++++++ src/macros.rs | 12 + src/tests.rs | 26 + 14 files changed, 2450 insertions(+) create mode 100644 .cargo_vcs_info.json create mode 100644 Cargo.toml create mode 100644 Cargo.toml.orig create mode 100644 LICENSE create mode 100644 README.md create mode 100755 av1-avif/LICENSE create mode 100755 av1-avif/README.md create mode 100644 av1-avif/testFiles/Link-U/README.md create mode 100644 avif_parse.h create mode 100644 src/boxes.rs create mode 100644 src/c_api.rs create mode 100644 src/lib.rs create mode 100644 src/macros.rs create mode 100644 src/tests.rs diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..efd23f4 --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "2174e0e15647918cbbcb965ba142c285a6ef457f" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..52f98f6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,55 @@ +# 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" +name = "avif-parse" +version = "1.0.0" +authors = ["Ralph Giles ", "Matthew Gregan ", "Alfredo Yang ", "Jon Bauman ", "Kornel Lesiński "] +include = ["README.md", "Cargo.toml", "LICENSE", "/src/*.rs", "avif_parse.h"] +description = "Parser for AVIF image files" +documentation = "https://docs.rs/avif-parse/" +readme = "README.md" +keywords = ["demuxer", "image", "parser", "heif"] +categories = ["multimedia::images"] +license = "MPL-2.0" +repository = "https://github.com/kornelski/avif-parse" +resolver = "2" +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[lib] +crate-type = ["rlib", "staticlib"] +[dependencies.bitreader] +version = "0.3.5" + +[dependencies.byteorder] +version = "1.4.3" + +[dependencies.fallible_collections] +version = "0.4.4" +features = ["std_io"] + +[dependencies.log] +version = "0.4.14" + +[dependencies.static_assertions] +version = "1.1.0" +[dev-dependencies.env_logger] +version = "0.9.0" + +[dev-dependencies.walkdir] +version = "2.3.2" + +[features] +mp4parse_fallible = [] +[badges.maintenance] +status = "passively-maintained" diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..3b5a68a --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,45 @@ +[package] +name = "avif-parse" +version = "1.0.0" +authors = [ + "Ralph Giles ", + "Matthew Gregan ", + "Alfredo Yang ", + "Jon Bauman ", + "Kornel Lesiński ", +] +edition = "2021" +description = "Parser for AVIF image files" +documentation = "https://docs.rs/avif-parse/" +license = "MPL-2.0" +categories = ["multimedia::images"] +repository = "https://github.com/kornelski/avif-parse" +readme = "README.md" +include = ["README.md", "Cargo.toml", "LICENSE", "/src/*.rs", "avif_parse.h"] +keywords = ["demuxer", "image", "parser", "heif"] + +[lib] +crate-type = ["rlib", "staticlib"] + +[dependencies] +byteorder = "1.4.3" +bitreader = "0.3.5" +log = "0.4.14" +static_assertions = "1.1.0" +fallible_collections = { version = "0.4.4", features = ["std_io"] } + +[dev-dependencies] +env_logger = "0.9.0" +walkdir = "2.3.2" + +[features] +# Enable mp4parse_fallible to use fallible memory allocation rather than +# panicking on OOM. Note that this is only safe within Gecko where the system +# allocator has been globally overridden (see BMO 1457359). +mp4parse_fallible = [] + +[badges] +maintenance = { status = "passively-maintained" } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..14e2f77 --- /dev/null +++ b/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md new file mode 100644 index 0000000..94d278d --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# AVIF file structure parser (demuxer) + +Get AV1 payload and alpha channel metadata out of AVIF image files. The parser is a fork of Mozilla's MP4 parser used in Firefox, so it's designed to be robust and safely handle untrusted data. + +The parser is compatible with files supported by libavif, Chrome 85 and Firefox 81a. + +[API documentation](https://docs.rs/avif-parse/) + +This crate doesn't include an AV1 decoder. To display the pixels you will additionally need [dav1d](https://code.videolan.org/videolan/dav1d) or [libaom](//lib.rs/libaom-sys). + +## Usage from Rust + +It takes `io::Read`, so you can use any readable input, such as a byte slice (`vec.as_slice()`), or a `File` wrapped in `BufReader`, etc. + +```rust +let data = read_avif(&mut slice)?; +av1_decode(&data.primary_item)?; +if let Some(alpha) = &data.alpha_item { + av1_decode(alpha)?; +} +if data.premultiplied_alpha { + // after decoding, remember to divide R,G,B values by A +} +``` + +## Usage from C + +Install Rust 1.45 or later, preferably via [rustup](//rustup.rs), and run: + +```bash +cargo build --release +``` + +It will build `./target/release/libavif_parse.a` (or `avif_parse.lib` on Windows). Link it with your project. + +Cargo supports cross-compilation, so you can easily build it for other platforms (e.g. iOS). + +```c +#include "avif_parse.h" +avif_data_t data = avif_parse(file_data, file_length); + +if (data) { + av1_decode(data.primary_data, data.primary_size); + if (data.alpha_data) { + av1_decode(data.alpha_data, data.alpha_size); + } + if (data.premultiplied_alpha) { + // after decoding, remember to divide R,G,B values by A + } +} + +avif_data_free(data); +``` diff --git a/av1-avif/LICENSE b/av1-avif/LICENSE new file mode 100755 index 0000000..70a47fe --- /dev/null +++ b/av1-avif/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2017, AOMediaCodec +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/av1-avif/README.md b/av1-avif/README.md new file mode 100755 index 0000000..724e48f --- /dev/null +++ b/av1-avif/README.md @@ -0,0 +1,15 @@ +# av1-avif + +This document describes how to use ISO-BMFF structures to generate a HEIF/MIAF +compatible file that contains one or more still images encoded using AV1. + +The specification is written using a special syntax (mixing markup and markdown) +to enable generation of cross-references, syntax highlighting, ... +The file using this syntax is index.bs. + +index.bs is processed to produce an HTML version (index.html) by a tool +called Bikeshed (https://github.com/tabatkins/bikeshed). + +The repository contains both the input and the result of the Bikeshed processing. + +Pull requests should contain updates to both files index.html and index.bs, and possibly to other files (Bibliography biblio.json). diff --git a/av1-avif/testFiles/Link-U/README.md b/av1-avif/testFiles/Link-U/README.md new file mode 100644 index 0000000..243fa90 --- /dev/null +++ b/av1-avif/testFiles/Link-U/README.md @@ -0,0 +1,232 @@ +# AVIF Example files from [Link-U](https://www.link-u.co.jp/) + + - All files do not contain Exif metadata. + - All files are tagged as MIAF compatible. + - All files are tagged as compatible with the AVIF Baseline or Advanced Profile if possible. + - All images have the "reduced_still_picture_header" and "still_picture" flags set to 1 in the AV1 Sequence Header. + - All images are licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en). + +[Makefile](Makefile) describes how they were created. To generate files yourself, you have to install [cavif](https://github.com/link-u/cavif) and [davif](https://github.com/link-u/davif) + +Additional test images may to be added. Please check [our repositories](https://github.com/link-u/avif-sample-images) for them. + +## hato + +![hato.jpg](hato.jpg) + + - size: 3082x2048 + - Author: Kaede Fujisaki ([@ledyba](https://github.com/ledyba)) + - Retrieved from [her website](https://hexe.net/2017/11/27/12:27:02/). + +### AVIF version + +#### YUV 420 + +| profile | bit depth | Monochrome | CDEF | Loop Restoration | file | +|---------|-----------|------------|------|------------------|---------------------------------------------------------------------------| +| 0 | 8 | | NO | YES | [here](hato.profile0.8bpc.yuv420.no-cdef.avif) | +| 0 | 8 | YES | NO | YES | [here](hato.profile0.8bpc.yuv420.monochrome.no-cdef.avif) | +| 0 | 10 | | NO | NO | [here](hato.profile0.10bpc.yuv420.no-cdef.no-restoration.avif) | +| 0 | 10 | YES | NO | NO | [here](hato.profile0.10bpc.yuv420.monochrome.no-cdef.no-restoration.avif) | + +#### YUV422 + +| profile | bit depth | Monochrome | CDEF | Loop Restroation | file | +|---------|-----------|------------|------|------------------|---------------------------------------------------------------------------| +| 1 | 8 | | NO | YES | [here](hato.profile2.8bpc.yuv422.no-cdef.avif) | +| 1 | 8 | YES | NO | YES | [here](hato.profile2.8bpc.yuv422.monochrome.no-cdef.avif) | +| 1 | 10 | | NO | NO | [here](hato.profile2.10bpc.yuv422.no-cdef.no-restoration.avif) | +| 1 | 10 | YES | NO | NO | [here](hato.profile2.10bpc.yuv422.monochrome.no-cdef.no-restoration.avif) | +| 1 | 12 | | NO | NO | [here](hato.profile2.12bpc.yuv422.no-cdef.no-restoration.avif) | +| 1 | 12 | YES | NO | NO | [here](hato.profile2.12bpc.yuv422.monochrome.no-cdef.no-restoration.avif) | + +## Kimono - Transformation tests + +[kimono.jpg](kimono.jpg) + + - size: 722x1024 + - Authors: Momiji Jinzamomi([@momiji-san](https://github.com/momiji-san)) and Kaede Fujisaki ([@ledyba](https://github.com/ledyba)) + - Retrieved from [their website](https://hexe.net/2018/12/24/18:59:01/). + +Test images for rotation(`irot`), mirroring(`imir`), cropping(`clap`). + +All AVIF images are encoded in these settings: + + - Profile 0 + - YUV420 + - 8 bits per component + +### FYI: Transform operation order + +[MIAF](https://www.iso.org/standard/74417.html) defines the transform operation order(p.16): + +> These properties, if used, shall be indicated to be applied in the following order: +> clean aperture first, then rotation, then mirror. + +### Identity + +[kimono.avif](./kimono.avif) + +No operation is applied. + +### Rotation 90 + +[kimono.rotate90.avif](./kimono.rotate90.avif) + +[Encoded image is rotated at 90 degree in counter-clockwise](kimono.rotate90.png), and marked to rotate it 270 degree in counter-clockwise when displaying. Thus, resulted image is as the same as the original. + +### Rotation 270 + +[kimono.rotate270.avif](./kimono.rotate270.avif) + +[Encoded image is rotated at 270 degree in counter-clockwise](kimono.rotate270.png), and marked to rotate it 90 degree in counter-clockwise when displaying. Thus, resulted image is as the same as the original. + + +### Mirroring horizontally + +[kimono.mirror-horizontal.avif](./kimono.mirror-horizontal.avif) + +[Encoded image is mirrored horizontally](kimono.mirror-horizontal.png), and marked to mirror it horizontally again when displaying. Thus, resulted image is as the same as the original. + +### Mirroring vertically + +[kimono.mirror-vertical.avif](./kimono.mirror-vertical.avif) + +Vertical version. Same as above. + +### Mirroring vertically + Rotating at 90 degrees. + +[kimono.mirror-vertical.rotate270.avif](./kimono.mirror-vertical.rotate270.avif) + +[Encoded image is mirrored vertically, then rorated at 90 degree in clockwise](kimono.mirror-vertical.rotate270.png), and marked to rotate it at 90 degree in counter-clockwise and then mirror it vertically when displaying. + +Thus, resulted image is as the same as the original. + +### Cropping + +Displaying image will be cropped from the original image, using `CleanApertureBox`(See: ISO/IEC 14496-12:2015). + +Cropped under these condition: + + - cleanApertureWidthN: 385 + - cleanApertureWidthD: 1 + - cleanApertureHeightN: 330 + - cleanApertureHeightD: 1 + - horizOffN: 103 + - horizOffD: 1 + - vertOffN: -308 (This can be negative, as mensioned in ISO/IEC 14496-12:2015). + - vertOffD: 1 + +Resulted image should be: + +![kimono.crop.png](kimono.crop.png) + +### Cropping + Mirroring vertically + Rotating at 90 degrees. + +[Encoded image is mirrored vertically, then rorated at 90 degree in clockwise](kimono.mirror-vertical.rotate270.png), and marked to crop it first, rotate it at 90 degree in counter-clockwise, and then mirror it vertically. + +Cropping condition is: + +- cleanApertureWidthN: 330 +- cleanApertureWidthD: 1 +- cleanApertureHeightN: 385 +- cleanApertureHeightD: 1 +- horizOffN: -308 +- horizOffD: 1 +- vertOffN: 103 +- vertOffD: 1 + +Resulted image should be as the same as above. + +## Fox Parade - Odd dimensions images + +### Original + +[fox.jpg](fox.jpg) + + - size: 1204 x 800 + - Author: Kaede Fujisaki ([@ledyba](https://github.com/ledyba)) + - Retrieved from [her website](https://hexe.net/2017/12/02/16:33:53/). + +#### Odd-Width + + - [fox.odd-width.png](fox.odd-width.png) + - size: 1203 x 800 + +#### Odd-Height + + - [fox.odd-width.png](fox.odd-height.png) + - size: 1204 x 799 + +### AVIF version + +| profile | bit depth | pix fmt | Monochrome | odd width | odd height | file | +|---------|-----------|---------|------------|-----------|------------|------------------------------------------------------------------------| +| 0 | 8 | YUV420 | | | | [here](fox.profile0.8bpc.yuv420.avif) | +| 0 | 8 | YUV420 | | YES | | [here](fox.profile0.8bpc.yuv420.odd-width.avif) | +| 0 | 8 | YUV420 | | | YES | [here](fox.profile0.8bpc.yuv420.odd-height.avif) | +| 0 | 8 | YUV420 | | YES | YES | [here](fox.profile0.8bpc.yuv420.odd-width.odd-height.avif) | +| 0 | 8 | YUV420 | YES | | | [here](fox.profile0.8bpc.yuv420.monochrome.avif) | +| 0 | 8 | YUV420 | YES | YES | | [here](fox.profile0.8bpc.yuv420.monochrome.odd-width.avif) | +| 0 | 8 | YUV420 | YES | | YES | [here](fox.profile0.8bpc.yuv420.monochrome.odd-height.avif) | +| 0 | 8 | YUV420 | YES | YES | YES | [here](fox.profile0.8bpc.yuv420.monochrome.odd-width.odd-height.avif) | +| 0 | 10 | YUV420 | | | | [here](fox.profile0.10bpc.yuv420.avif) | +| 0 | 10 | YUV420 | | YES | | [here](fox.profile0.10bpc.yuv420.odd-width.avif) | +| 0 | 10 | YUV420 | | | YES | [here](fox.profile0.10bpc.yuv420.odd-height.avif) | +| 0 | 10 | YUV420 | | YES | YES | [here](fox.profile0.10bpc.yuv420.odd-width.odd-height.avif) | +| 0 | 10 | YUV420 | YES | | | [here](fox.profile0.10bpc.yuv420.monochrome.avif) | +| 0 | 10 | YUV420 | YES | YES | | [here](fox.profile0.10bpc.yuv420.monochrome.odd-width.avif) | +| 0 | 10 | YUV420 | YES | | YES | [here](fox.profile0.10bpc.yuv420.monochrome.odd-height.avif) | +| 0 | 10 | YUV420 | YES | YES | YES | [here](fox.profile0.10bpc.yuv420.monochrome.odd-width.odd-height.avif) | +| 2 | 12 | YUV420 | | | | [here](fox.profile2.12bpc.yuv420.avif) | +| 2 | 12 | YUV420 | | YES | | [here](fox.profile2.12bpc.yuv420.odd-width.avif) | +| 2 | 12 | YUV420 | | | YES | [here](fox.profile2.12bpc.yuv420.odd-height.avif) | +| 2 | 12 | YUV420 | | YES | YES | [here](fox.profile2.12bpc.yuv420.odd-width.odd-height.avif) | +| 2 | 12 | YUV420 | YES | | | [here](fox.profile2.12bpc.yuv420.monochrome.avif) | +| 2 | 12 | YUV420 | YES | YES | | [here](fox.profile2.12bpc.yuv420.monochrome.odd-width.avif) | +| 2 | 12 | YUV420 | YES | | YES | [here](fox.profile2.12bpc.yuv420.monochrome.odd-height.avif) | +| 2 | 12 | YUV420 | YES | YES | YES | [here](fox.profile2.12bpc.yuv420.monochrome.odd-width.odd-height.avif) | +| 2 | 8 | YUV422 | | | | [here](fox.profile2.8bpc.yuv422.avif) | +| 2 | 8 | YUV422 | | YES | | [here](fox.profile2.8bpc.yuv422.odd-width.avif) | +| 2 | 8 | YUV422 | | | YES | [here](fox.profile2.8bpc.yuv422.odd-height.avif) | +| 2 | 8 | YUV422 | | YES | YES | [here](fox.profile2.8bpc.yuv422.odd-width.odd-height.avif) | +| 2 | 8 | YUV422 | YES | | | [here](fox.profile2.8bpc.yuv422.monochrome.avif) | +| 2 | 8 | YUV422 | YES | YES | | [here](fox.profile2.8bpc.yuv422.monochrome.odd-width.avif) | +| 2 | 8 | YUV422 | YES | | YES | [here](fox.profile2.8bpc.yuv422.monochrome.odd-height.avif) | +| 2 | 8 | YUV422 | YES | YES | YES | [here](fox.profile2.8bpc.yuv422.monochrome.odd-width.odd-height.avif) | +| 2 | 10 | YUV422 | | | | [here](fox.profile2.10bpc.yuv422.avif) | +| 2 | 10 | YUV422 | | YES | | [here](fox.profile2.10bpc.yuv422.odd-width.avif) | +| 2 | 10 | YUV422 | | | YES | [here](fox.profile2.10bpc.yuv422.odd-height.avif) | +| 2 | 10 | YUV422 | | YES | YES | [here](fox.profile2.10bpc.yuv422.odd-width.odd-height.avif) | +| 2 | 10 | YUV422 | YES | | | [here](fox.profile2.10bpc.yuv422.monochrome.avif) | +| 2 | 10 | YUV422 | YES | YES | | [here](fox.profile2.10bpc.yuv422.monochrome.odd-width.avif) | +| 2 | 10 | YUV422 | YES | | YES | [here](fox.profile2.10bpc.yuv422.monochrome.odd-height.avif) | +| 2 | 10 | YUV422 | YES | YES | YES | [here](fox.profile2.10bpc.yuv422.monochrome.odd-width.odd-height.avif) | +| 2 | 12 | YUV422 | | | | [here](fox.profile2.12bpc.yuv422.avif) | +| 2 | 12 | YUV422 | | YES | | [here](fox.profile2.12bpc.yuv422.odd-width.avif) | +| 2 | 12 | YUV422 | | | YES | [here](fox.profile2.12bpc.yuv422.odd-height.avif) | +| 2 | 12 | YUV422 | | YES | YES | [here](fox.profile2.12bpc.yuv422.odd-width.odd-height.avif) | +| 2 | 12 | YUV422 | YES | | | [here](fox.profile2.12bpc.yuv422.monochrome.avif) | +| 2 | 12 | YUV422 | YES | YES | | [here](fox.profile2.12bpc.yuv422.monochrome.odd-width.avif) | +| 2 | 12 | YUV422 | YES | | YES | [here](fox.profile2.12bpc.yuv422.monochrome.odd-height.avif) | +| 2 | 12 | YUV422 | YES | YES | YES | [here](fox.profile2.12bpc.yuv422.monochrome.odd-width.odd-height.avif) | +| 1 | 8 | YUV444 | | | | [here](fox.profile1.8bpc.yuv444.avif) | +| 1 | 8 | YUV444 | | YES | | [here](fox.profile1.8bpc.yuv444.odd-width.avif) | +| 1 | 8 | YUV444 | | | YES | [here](fox.profile1.8bpc.yuv444.odd-height.avif) | +| 1 | 8 | YUV444 | | YES | YES | [here](fox.profile1.8bpc.yuv444.odd-width.odd-height.avif) | +| 1 | 8 | YUV444 | | | | [here](fox.profile1.8bpc.yuv444.avif) | +| 1 | 8 | YUV444 | | YES | | [here](fox.profile1.8bpc.yuv444.odd-width.avif) | +| 1 | 8 | YUV444 | | | YES | [here](fox.profile1.8bpc.yuv444.odd-height.avif) | +| 1 | 8 | YUV444 | | YES | YES | [here](fox.profile1.8bpc.yuv444.odd-width.odd-height.avif) | +| 1 | 10 | YUV444 | | | | [here](fox.profile1.10bpc.yuv444.avif) | +| 1 | 10 | YUV444 | | YES | | [here](fox.profile1.10bpc.yuv444.odd-width.avif) | +| 1 | 10 | YUV444 | | | YES | [here](fox.profile1.10bpc.yuv444.odd-height.avif) | +| 1 | 10 | YUV444 | | YES | YES | [here](fox.profile1.10bpc.yuv444.odd-width.odd-height.avif) | +| 2 | 12 | YUV444 | | | | [here](fox.profile2.12bpc.yuv444.avif) | +| 2 | 12 | YUV444 | | YES | | [here](fox.profile2.12bpc.yuv444.odd-width.avif) | +| 2 | 12 | YUV444 | | | YES | [here](fox.profile2.12bpc.yuv444.odd-height.avif) | +| 2 | 12 | YUV444 | | YES | YES | [here](fox.profile2.12bpc.yuv444.odd-width.odd-height.avif) | +| 2 | 12 | YUV444 | YES | | | [here](fox.profile2.12bpc.yuv444.monochrome.avif) | +| 2 | 12 | YUV444 | YES | YES | | [here](fox.profile2.12bpc.yuv444.monochrome.odd-width.avif) | +| 2 | 12 | YUV444 | YES | | YES | [here](fox.profile2.12bpc.yuv444.monochrome.odd-height.avif) | +| 2 | 12 | YUV444 | YES | YES | YES | [here](fox.profile2.12bpc.yuv444.monochrome.odd-width.odd-height.avif) | diff --git a/avif_parse.h b/avif_parse.h new file mode 100644 index 0000000..2865649 --- /dev/null +++ b/avif_parse.h @@ -0,0 +1,47 @@ +#include + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * Result of parsing an AVIF file. Contains AV1-compressed data. + */ +typedef struct avif_data_t { + /** + * AV1 data for color channels + */ + const unsigned char *primary_data; + size_t primary_size; + /** + * AV1 data for alpha channel (may be NULL if the image is fully opaque) + */ + const unsigned char *alpha_data; + size_t alpha_size; + /** + * 0 = normal, uncorrelated alpha channel + * 1 = premultiplied alpha. You must divide RGB values by alpha. + * + * ```c + * if (a != 0) {r = r * 255 / a} + * ``` + */ + char premultiplied_alpha; + void *reserved; +} avif_data_t; + +/** + * Parse AVIF image file and return results. Returns `NULL` if the file can't be parsed. + * + * Call `avif_data_free` on the result when done. + */ +const avif_data_t *avif_parse(const unsigned char *bytes, size_t bytes_len); + +/** + * Free all data related to `avif_data_t` + */ +void avif_data_free(const avif_data_t *data); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus diff --git a/src/boxes.rs b/src/boxes.rs new file mode 100644 index 0000000..3c64de6 --- /dev/null +++ b/src/boxes.rs @@ -0,0 +1,215 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +use std::fmt; + +// To ensure we don't use stdlib allocating types by accident +#[allow(dead_code)] +struct Vec; +#[allow(dead_code)] +struct Box; +#[allow(dead_code)] +struct HashMap; +#[allow(dead_code)] +struct String; + +macro_rules! box_database { + ($($(#[$attr:meta])* $boxenum:ident $boxtype:expr),*,) => { + #[derive(Clone, Copy, PartialEq)] + pub enum BoxType { + $($(#[$attr])* $boxenum),*, + UnknownBox(u32), + } + + impl From for BoxType { + fn from(t: u32) -> BoxType { + use self::BoxType::*; + match t { + $($(#[$attr])* $boxtype => $boxenum),*, + _ => UnknownBox(t), + } + } + } + + impl Into for BoxType { + fn into(self) -> u32 { + use self::BoxType::*; + match self { + $($(#[$attr])* $boxenum => $boxtype),*, + UnknownBox(t) => t, + } + } + } + + impl fmt::Debug for BoxType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let fourcc: FourCC = From::from(self.clone()); + fourcc.fmt(f) + } + } + } +} + +#[derive(Default, PartialEq, Clone)] +pub struct FourCC { + pub value: [u8; 4], +} + +impl From for FourCC { + fn from(number: u32) -> FourCC { + FourCC { + value: number.to_be_bytes(), + } + } +} + +impl From for FourCC { + fn from(t: BoxType) -> FourCC { + let box_num: u32 = Into::into(t); + From::from(box_num) + } +} + +impl From<[u8; 4]> for FourCC { + fn from(v: [u8; 4]) -> FourCC { + FourCC { value: v } + } +} + +impl fmt::Debug for FourCC { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match std::str::from_utf8(&self.value) { + Ok(s) => f.write_str(s), + Err(_) => self.value.fmt(f), + } + } +} + +impl fmt::Display for FourCC { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(std::str::from_utf8(&self.value).unwrap_or("null")) + } +} + +impl PartialEq<&[u8; 4]> for FourCC { + fn eq(&self, other: &&[u8; 4]) -> bool { + self.value.eq(*other) + } +} + +#[deny(unreachable_patterns)] +box_database!( + FileTypeBox 0x6674_7970, // "ftyp" + MediaDataBox 0x6d64_6174, // "mdat" + PrimaryItemBox 0x7069_746d, // "pitm" + ItemInfoBox 0x6969_6e66, // "iinf" + ItemInfoEntry 0x696e_6665, // "infe" + ItemLocationBox 0x696c_6f63, // "iloc" + MovieBox 0x6d6f_6f76, // "moov" + MovieHeaderBox 0x6d76_6864, // "mvhd" + TrackBox 0x7472_616b, // "trak" + TrackHeaderBox 0x746b_6864, // "tkhd" + EditBox 0x6564_7473, // "edts" + MediaBox 0x6d64_6961, // "mdia" + EditListBox 0x656c_7374, // "elst" + MediaHeaderBox 0x6d64_6864, // "mdhd" + HandlerBox 0x6864_6c72, // "hdlr" + MediaInformationBox 0x6d69_6e66, // "minf" + ImageReferenceBox 0x6972_6566, // "iref" + ImagePropertiesBox 0x6970_7270, // "iprp" + ItemPropertyContainerBox 0x6970_636f, // "ipco" + ItemPropertyAssociationBox 0x6970_6d61, // "ipma" + ColorInformationBox 0x636f_6c72, // "colr" + PixelInformationBox 0x7069_7869, // "pixi" + AuxiliaryTypeProperty 0x6175_7843, // "auxC" + SampleTableBox 0x7374_626c, // "stbl" + SampleDescriptionBox 0x7374_7364, // "stsd" + TimeToSampleBox 0x7374_7473, // "stts" + SampleToChunkBox 0x7374_7363, // "stsc" + SampleSizeBox 0x7374_737a, // "stsz" + ChunkOffsetBox 0x7374_636f, // "stco" + ChunkLargeOffsetBox 0x636f_3634, // "co64" + SyncSampleBox 0x7374_7373, // "stss" + AVCSampleEntry 0x6176_6331, // "avc1" + AVC3SampleEntry 0x6176_6333, // "avc3" - Need to check official name in spec. + AVCConfigurationBox 0x6176_6343, // "avcC" + MP4AudioSampleEntry 0x6d70_3461, // "mp4a" + MP4VideoSampleEntry 0x6d70_3476, // "mp4v" + ESDBox 0x6573_6473, // "esds" + VP8SampleEntry 0x7670_3038, // "vp08" + VP9SampleEntry 0x7670_3039, // "vp09" + VPCodecConfigurationBox 0x7670_6343, // "vpcC" + AV1SampleEntry 0x6176_3031, // "av01" + AV1CodecConfigurationBox 0x6176_3143, // "av1C" + FLACSampleEntry 0x664c_6143, // "fLaC" + FLACSpecificBox 0x6466_4c61, // "dfLa" + OpusSampleEntry 0x4f70_7573, // "Opus" + OpusSpecificBox 0x644f_7073, // "dOps" + ProtectedVisualSampleEntry 0x656e_6376, // "encv" - Need to check official name in spec. + ProtectedAudioSampleEntry 0x656e_6361, // "enca" - Need to check official name in spec. + MovieExtendsBox 0x6d76_6578, // "mvex" + MovieExtendsHeaderBox 0x6d65_6864, // "mehd" + QTWaveAtom 0x7761_7665, // "wave" - quicktime atom + ProtectionSystemSpecificHeaderBox 0x7073_7368, // "pssh" + SchemeInformationBox 0x7363_6869, // "schi" + TrackEncryptionBox 0x7465_6e63, // "tenc" + ProtectionSchemeInfoBox 0x7369_6e66, // "sinf" + OriginalFormatBox 0x6672_6d61, // "frma" + SchemeTypeBox 0x7363_686d, // "schm" + MP3AudioSampleEntry 0x2e6d_7033, // ".mp3" - from F4V. + CompositionOffsetBox 0x6374_7473, // "ctts" + LPCMAudioSampleEntry 0x6c70_636d, // "lpcm" - quicktime atom + ALACSpecificBox 0x616c_6163, // "alac" - Also used by ALACSampleEntry + UuidBox 0x7575_6964, // "uuid" + MetadataBox 0x6d65_7461, // "meta" + MetadataHeaderBox 0x6d68_6472, // "mhdr" + MetadataItemKeysBox 0x6b65_7973, // "keys" + MetadataItemListEntry 0x696c_7374, // "ilst" + MetadataItemDataEntry 0x6461_7461, // "data" + MetadataItemNameBox 0x6e61_6d65, // "name" + UserdataBox 0x7564_7461, // "udta" + AlbumEntry 0xa961_6c62, // "©alb" + ArtistEntry 0xa941_5254, // "©ART" + ArtistLowercaseEntry 0xa961_7274, // "©art" + AlbumArtistEntry 0x6141_5254, // "aART" + CommentEntry 0xa963_6d74, // "©cmt" + DateEntry 0xa964_6179, // "©day" + TitleEntry 0xa96e_616d, // "©nam" + CustomGenreEntry 0xa967_656e, // "©gen" + StandardGenreEntry 0x676e_7265, // "gnre" + TrackNumberEntry 0x7472_6b6e, // "trkn" + DiskNumberEntry 0x6469_736b, // "disk" + ComposerEntry 0xa977_7274, // "©wrt" + EncoderEntry 0xa974_6f6f, // "©too" + EncodedByEntry 0xa965_6e63, // "©enc" + TempoEntry 0x746d_706f, // "tmpo" + CopyrightEntry 0x6370_7274, // "cprt" + CompilationEntry 0x6370_696c, // "cpil" + CoverArtEntry 0x636f_7672, // "covr" + AdvisoryEntry 0x7274_6e67, // "rtng" + RatingEntry 0x7261_7465, // "rate" + GroupingEntry 0xa967_7270, // "©grp" + MediaTypeEntry 0x7374_696b, // "stik" + PodcastEntry 0x7063_7374, // "pcst" + CategoryEntry 0x6361_7467, // "catg" + KeywordEntry 0x6b65_7977, // "keyw" + PodcastUrlEntry 0x7075_726c, // "purl" + PodcastGuidEntry 0x6567_6964, // "egid" + DescriptionEntry 0x6465_7363, // "desc" + LongDescriptionEntry 0x6c64_6573, // "ldes" + LyricsEntry 0xa96c_7972, // "©lyr" + TVNetworkNameEntry 0x7476_6e6e, // "tvnn" + TVShowNameEntry 0x7476_7368, // "tvsh" + TVEpisodeNameEntry 0x7476_656e, // "tven" + TVSeasonNumberEntry 0x7476_736e, // "tvsn" + TVEpisodeNumberEntry 0x7476_6573, // "tves" + PurchaseDateEntry 0x7075_7264, // "purd" + GaplessPlaybackEntry 0x7067_6170, // "pgap" + OwnerEntry 0x6f77_6e72, // "ownr" + HDVideoEntry 0x6864_7664, // "hdvd" + SortNameEntry 0x736f_6e6d, // "sonm" + SortAlbumEntry 0x736f_616c, // "soal" + SortArtistEntry 0x736f_6172, // "soar" + SortAlbumArtistEntry 0x736f_6161, // "soaa" + SortComposerEntry 0x736f_636f, // "soco" +); diff --git a/src/c_api.rs b/src/c_api.rs new file mode 100644 index 0000000..50eb077 --- /dev/null +++ b/src/c_api.rs @@ -0,0 +1,56 @@ +use crate::AvifData as AvifDataRust; + +/// Result of parsing an AVIF file. Contains AV1-compressed data. +#[allow(bad_style)] +#[repr(C)] +pub struct avif_data_t { + /// AV1 data for color channels + pub primary_data: *const u8, + pub primary_size: usize, + /// AV1 data for alpha channel (may be NULL if the image is fully opaque) + pub alpha_data: *const u8, + pub alpha_size: usize, + /// 0 = normal, uncorrelated alpha channel + /// 1 = premultiplied alpha. You must divide RGB values by alpha. + /// + /// ```c + /// if (a != 0) {r = r * 255 / a} + /// ``` + pub premultiplied_alpha: u8, + _rusty_handle: *mut AvifDataRust, +} + +/// Parse AVIF image file and return results. Returns `NULL` if the file can't be parsed. +/// +/// Call `avif_data_free` on the result when done. +#[no_mangle] +pub unsafe extern "C" fn avif_parse(bytes: *const u8, bytes_len: usize) -> *const avif_data_t { + if bytes.is_null() || bytes_len == 0 { + return std::ptr::null(); + } + let mut data = std::slice::from_raw_parts(bytes, bytes_len); + match crate::read_avif(&mut data) { + Ok(data) => Box::into_raw(Box::new(avif_data_t { + primary_data: data.primary_item.as_ptr(), + primary_size: data.primary_item.len(), + alpha_data: data + .alpha_item + .as_ref() + .map_or(std::ptr::null(), |a| a.as_ptr()), + alpha_size: data.alpha_item.as_ref().map_or(0, |a| a.len()), + premultiplied_alpha: data.premultiplied_alpha as u8, + _rusty_handle: Box::into_raw(Box::new(data)), + })), + Err(_) => std::ptr::null(), + } +} + +/// Free all data related to `avif_data_t` +#[no_mangle] +pub unsafe extern "C" fn avif_data_free(data: *const avif_data_t) { + if data.is_null() { + return; + } + let _ = Box::from_raw((*data)._rusty_handle); + let _ = Box::from_raw(data as *mut avif_data_t); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..75d5665 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,1290 @@ +//! Module for parsing ISO Base Media Format aka video/mp4 streams. + +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#[macro_use] +extern crate log; + +use bitreader::BitReader; +use byteorder::ReadBytesExt; +use fallible_collections::TryClone; +use fallible_collections::TryRead; +use fallible_collections::TryReserveError; +use std::convert::{TryFrom, TryInto as _}; + +use std::io::{Read, Take}; +use std::ops::{Range, RangeFrom}; + +#[macro_use] +mod macros; + +mod boxes; +use crate::boxes::{BoxType, FourCC}; + +/// This crate can be used from C. +pub mod c_api; + +// Unit tests. +#[cfg(test)] +mod tests; + +// Arbitrary buffer size limit used for raw read_bufs on a box. +// const BUF_SIZE_LIMIT: u64 = 10 * 1024 * 1024; + +/// A trait to indicate a type can be infallibly converted to `u64`. +/// This should only be implemented for infallible conversions, so only unsigned types are valid. +trait ToU64 { + fn to_u64(self) -> u64; +} + +/// Statically verify that the platform `usize` can fit within a `u64`. +/// If the size won't fit on the given platform, this will fail at compile time, but if a type +/// which can fail TryInto is used, it may panic. +impl ToU64 for usize { + fn to_u64(self) -> u64 { + static_assertions::const_assert!( + std::mem::size_of::() <= std::mem::size_of::() + ); + self.try_into().expect("usize -> u64 conversion failed") + } +} + +/// A trait to indicate a type can be infallibly converted to `usize`. +/// This should only be implemented for infallible conversions, so only unsigned types are valid. +pub(crate) trait ToUsize { + fn to_usize(self) -> usize; +} + +/// Statically verify that the given type can fit within a `usize`. +/// If the size won't fit on the given platform, this will fail at compile time, but if a type +/// which can fail TryInto is used, it may panic. +macro_rules! impl_to_usize_from { + ( $from_type:ty ) => { + impl ToUsize for $from_type { + fn to_usize(self) -> usize { + static_assertions::const_assert!( + std::mem::size_of::<$from_type>() <= std::mem::size_of::() + ); + self.try_into().expect(concat!( + stringify!($from_type), + " -> usize conversion failed" + )) + } + } + }; +} + +impl_to_usize_from!(u8); +impl_to_usize_from!(u16); +impl_to_usize_from!(u32); + +/// Indicate the current offset (i.e., bytes already read) in a reader +trait Offset { + fn offset(&self) -> u64; +} + +/// Wraps a reader to track the current offset +struct OffsetReader<'a, T> { + reader: &'a mut T, + offset: u64, +} + +impl<'a, T> OffsetReader<'a, T> { + fn new(reader: &'a mut T) -> Self { + Self { reader, offset: 0 } + } +} + +impl<'a, T> Offset for OffsetReader<'a, T> { + fn offset(&self) -> u64 { + self.offset + } +} + +impl<'a, T: Read> Read for OffsetReader<'a, T> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let bytes_read = self.reader.read(buf)?; + self.offset = self + .offset + .checked_add(bytes_read.to_u64()) + .expect("total bytes read too large for offset type"); + Ok(bytes_read) + } +} + +pub type TryVec = fallible_collections::TryVec; +pub type TryString = fallible_collections::TryVec; +pub type TryHashMap = fallible_collections::TryHashMap; +pub type TryBox = fallible_collections::TryBox; + +// To ensure we don't use stdlib allocating types by accident +#[allow(dead_code)] +struct Vec; +#[allow(dead_code)] +struct Box; +#[allow(dead_code)] +struct HashMap; +#[allow(dead_code)] +struct String; + +/// Describes parser failures. +/// +/// This enum wraps the standard `io::Error` type, unified with +/// our own parser error states and those of crates we use. +#[derive(Debug)] +pub enum Error { + /// Parse error caused by corrupt or malformed data. + InvalidData(&'static str), + /// Parse error caused by limited parser support rather than invalid data. + Unsupported(&'static str), + /// Reflect `std::io::ErrorKind::UnexpectedEof` for short data. + UnexpectedEOF, + /// Propagate underlying errors from `std::io`. + Io(std::io::Error), + /// read_mp4 terminated without detecting a moov box. + NoMoov, + /// Out of memory + OutOfMemory, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(_: bitreader::BitReaderError) -> Error { + Error::InvalidData("invalid data") + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Error { + match err.kind() { + std::io::ErrorKind::UnexpectedEof => Error::UnexpectedEOF, + _ => Error::Io(err), + } + } +} + +impl From for Error { + fn from(_: std::string::FromUtf8Error) -> Error { + Error::InvalidData("invalid utf8") + } +} + +impl From for Error { + fn from(_: std::num::TryFromIntError) -> Error { + Error::Unsupported("integer conversion failed") + } +} + +impl From for std::io::Error { + fn from(err: Error) -> Self { + let kind = match err { + Error::InvalidData(_) => std::io::ErrorKind::InvalidData, + Error::UnexpectedEOF => std::io::ErrorKind::UnexpectedEof, + Error::Io(io_err) => return io_err, + _ => std::io::ErrorKind::Other, + }; + Self::new(kind, err) + } +} + +impl From for Error { + fn from(_: TryReserveError) -> Error { + Error::OutOfMemory + } +} + +/// Result shorthand using our Error enum. +pub type Result = std::result::Result; + +/// Basic ISO box structure. +/// +/// mp4 files are a sequence of possibly-nested 'box' structures. Each box +/// begins with a header describing the length of the box's data and a +/// four-byte box type which identifies the type of the box. Together these +/// are enough to interpret the contents of that section of the file. +/// +/// See ISO 14496-12:2015 § 4.2 +#[derive(Debug, Clone, Copy)] +struct BoxHeader { + /// Box type. + name: BoxType, + /// Size of the box in bytes. + size: u64, + /// Offset to the start of the contained data (or header size). + offset: u64, + /// Uuid for extended type. + uuid: Option<[u8; 16]>, +} + +impl BoxHeader { + const MIN_SIZE: u64 = 8; // 4-byte size + 4-byte type + const MIN_LARGE_SIZE: u64 = 16; // 4-byte size + 4-byte type + 16-byte size +} + +/// File type box 'ftyp'. +#[derive(Debug)] +struct FileTypeBox { + major_brand: FourCC, + minor_version: u32, + compatible_brands: TryVec, +} + +// Handler reference box 'hdlr' +#[derive(Debug)] +struct HandlerBox { + handler_type: FourCC, +} + +#[derive(Debug)] +pub(crate) struct AV1ConfigBox { + pub(crate) profile: u8, + pub(crate) level: u8, + pub(crate) tier: u8, + pub(crate) bit_depth: u8, + pub(crate) monochrome: bool, + pub(crate) chroma_subsampling_x: u8, + pub(crate) chroma_subsampling_y: u8, + pub(crate) chroma_sample_position: u8, + pub(crate) initial_presentation_delay_present: bool, + pub(crate) initial_presentation_delay_minus_one: u8, + pub(crate) config_obus: TryVec, +} + +#[derive(Debug, Default)] +pub struct AvifData { + /// AV1 data for the color channels. + /// + /// The collected data indicated by the `pitm` box, See ISO 14496-12:2015 § 8.11.4 + pub primary_item: TryVec, + /// AV1 data for alpha channel. + /// + /// Associated alpha channel for the primary item, if any + pub alpha_item: Option>, + /// If true, divide RGB values by the alpha value. + /// + /// See `prem` in MIAF § 7.3.5.2 + pub premultiplied_alpha: bool, +} + +struct AvifMeta { + item_references: TryVec, + properties: TryVec, + primary_item_id: u32, + iloc_items: TryVec, +} + +/// A Media Data Box +/// See ISO 14496-12:2015 § 8.1.1 +struct MediaDataBox { + /// Offset of `data` from the beginning of the file. See ConstructionMethod::File + offset: u64, + data: TryVec, +} + +impl MediaDataBox { + /// Check whether the beginning of `extent` is within the bounds of the `MediaDataBox`. + /// We assume extents to not cross box boundaries. If so, this will cause an error + /// in `read_extent`. + fn contains_extent(&self, extent: &ExtentRange) -> bool { + if self.offset <= extent.start() { + let start_offset = extent.start() - self.offset; + start_offset < self.data.len().to_u64() + } else { + false + } + } + + /// Check whether `extent` covers the `MediaDataBox` exactly. + fn matches_extent(&self, extent: &ExtentRange) -> bool { + if self.offset == extent.start() { + match extent { + ExtentRange::WithLength(range) => { + if let Some(end) = self.offset.checked_add(self.data.len().to_u64()) { + end == range.end + } else { + false + } + } + ExtentRange::ToEnd(_) => true, + } + } else { + false + } + } + + /// Copy the range specified by `extent` to the end of `buf` or return an error if the range + /// is not fully contained within `MediaDataBox`. + fn read_extent(&mut self, extent: &ExtentRange, buf: &mut TryVec) -> Result<()> { + let start_offset = extent + .start() + .checked_sub(self.offset) + .expect("mdat does not contain extent"); + let slice = match extent { + ExtentRange::WithLength(range) => { + let range_len = range + .end + .checked_sub(range.start) + .expect("range start > end"); + let end = start_offset + .checked_add(range_len) + .expect("extent end overflow"); + self.data.get(start_offset.try_into()?..end.try_into()?) + } + ExtentRange::ToEnd(_) => self.data.get(start_offset.try_into()?..), + }; + let slice = slice.ok_or(Error::InvalidData("extent crosses box boundary"))?; + buf.extend_from_slice(slice)?; + Ok(()) + } +} + +/// Used for 'infe' boxes within 'iinf' boxes +/// See ISO 14496-12:2015 § 8.11.6 +/// Only versions {2, 3} are supported +#[derive(Debug)] +struct ItemInfoEntry { + item_id: u32, + item_type: u32, +} + +/// See ISO 14496-12:2015 § 8.11.12 +#[derive(Debug)] +struct SingleItemTypeReferenceBox { + item_type: FourCC, + from_item_id: u32, + to_item_id: u32, +} + +/// Potential sizes (in bytes) of variable-sized fields of the 'iloc' box +/// See ISO 14496-12:2015 § 8.11.3 +#[derive(Debug)] +enum IlocFieldSize { + Zero, + Four, + Eight, +} + +impl IlocFieldSize { + fn to_bits(&self) -> u8 { + match self { + IlocFieldSize::Zero => 0, + IlocFieldSize::Four => 32, + IlocFieldSize::Eight => 64, + } + } +} + +impl TryFrom for IlocFieldSize { + type Error = Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Zero), + 4 => Ok(Self::Four), + 8 => Ok(Self::Eight), + _ => Err(Error::InvalidData("value must be in the set {0, 4, 8}")), + } + } +} + +#[derive(PartialEq)] +enum IlocVersion { + Zero, + One, + Two, +} + +impl TryFrom for IlocVersion { + type Error = Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Zero), + 1 => Ok(Self::One), + 2 => Ok(Self::Two), + _ => Err(Error::Unsupported("unsupported version in 'iloc' box")), + } + } +} + +/// Used for 'iloc' boxes +/// See ISO 14496-12:2015 § 8.11.3 +/// `base_offset` is omitted since it is integrated into the ranges in `extents` +/// `data_reference_index` is omitted, since only 0 (i.e., this file) is supported +#[derive(Debug)] +struct ItemLocationBoxItem { + item_id: u32, + construction_method: ConstructionMethod, + /// Unused for ConstructionMethod::Idat + extents: TryVec, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +enum ConstructionMethod { + File, + Idat, + #[allow(dead_code)] // TODO: see https://github.com/mozilla/mp4parse-rust/issues/196 + Item, +} + +/// `extent_index` is omitted since it's only used for ConstructionMethod::Item which +/// is currently not implemented. +#[derive(Clone, Debug)] +struct ItemLocationBoxExtent { + extent_range: ExtentRange, +} + +#[derive(Clone, Debug)] +enum ExtentRange { + WithLength(Range), + ToEnd(RangeFrom), +} + +impl ExtentRange { + fn start(&self) -> u64 { + match self { + Self::WithLength(r) => r.start, + Self::ToEnd(r) => r.start, + } + } +} + +/// See ISO 14496-12:2015 § 4.2 +struct BMFFBox<'a, T> { + head: BoxHeader, + content: Take<&'a mut T>, +} + +struct BoxIter<'a, T> { + src: &'a mut T, +} + +impl<'a, T: Read> BoxIter<'a, T> { + fn new(src: &mut T) -> BoxIter<'_, T> { + BoxIter { src } + } + + fn next_box(&mut self) -> Result>> { + let r = read_box_header(self.src); + match r { + Ok(h) => Ok(Some(BMFFBox { + head: h, + content: self.src.take(h.size - h.offset), + })), + Err(Error::UnexpectedEOF) => Ok(None), + Err(e) => Err(e), + } + } +} + +impl<'a, T: Read> Read for BMFFBox<'a, T> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.content.read(buf) + } +} + +impl<'a, T: Read> TryRead for BMFFBox<'a, T> { + fn try_read_to_end(&mut self, buf: &mut TryVec) -> std::io::Result { + fallible_collections::try_read_up_to(self, self.bytes_left(), buf) + } +} + +impl<'a, T: Offset> Offset for BMFFBox<'a, T> { + fn offset(&self) -> u64 { + self.content.get_ref().offset() + } +} + +impl<'a, T: Read> BMFFBox<'a, T> { + fn bytes_left(&self) -> u64 { + self.content.limit() + } + + fn get_header(&self) -> &BoxHeader { + &self.head + } + + fn box_iter<'b>(&'b mut self) -> BoxIter<'_, BMFFBox<'a, T>> { + BoxIter::new(self) + } +} + +impl<'a, T> Drop for BMFFBox<'a, T> { + fn drop(&mut self) { + if self.content.limit() > 0 { + let name: FourCC = From::from(self.head.name); + debug!("Dropping {} bytes in '{}'", self.content.limit(), name); + } + } +} + +/// Read and parse a box header. +/// +/// Call this first to determine the type of a particular mp4 box +/// and its length. Used internally for dispatching to specific +/// parsers for the internal content, or to get the length to +/// skip unknown or uninteresting boxes. +/// +/// See ISO 14496-12:2015 § 4.2 +fn read_box_header(src: &mut T) -> Result { + let size32 = be_u32(src)?; + let name = BoxType::from(be_u32(src)?); + let size = match size32 { + // valid only for top-level box and indicates it's the last box in the file. usually mdat. + 0 => return Err(Error::Unsupported("unknown sized box")), + 1 => { + let size64 = be_u64(src)?; + if size64 < BoxHeader::MIN_LARGE_SIZE { + return Err(Error::InvalidData("malformed wide size")); + } + size64 + } + _ => { + if u64::from(size32) < BoxHeader::MIN_SIZE { + return Err(Error::InvalidData("malformed size")); + } + u64::from(size32) + } + }; + let mut offset = match size32 { + 1 => BoxHeader::MIN_LARGE_SIZE, + _ => BoxHeader::MIN_SIZE, + }; + let uuid = if name == BoxType::UuidBox { + if size >= offset + 16 { + let mut buffer = [0u8; 16]; + let count = src.read(&mut buffer)?; + offset += count.to_u64(); + if count == 16 { + Some(buffer) + } else { + debug!("malformed uuid (short read), skipping"); + None + } + } else { + debug!("malformed uuid, skipping"); + None + } + } else { + None + }; + assert!(offset <= size); + Ok(BoxHeader { + name, + size, + offset, + uuid, + }) +} + +/// Parse the extra header fields for a full box. +fn read_fullbox_extra(src: &mut T) -> Result<(u8, u32)> { + let version = src.read_u8()?; + let flags_a = src.read_u8()?; + let flags_b = src.read_u8()?; + let flags_c = src.read_u8()?; + Ok(( + version, + u32::from(flags_a) << 16 | u32::from(flags_b) << 8 | u32::from(flags_c), + )) +} + +// Parse the extra fields for a full box whose flag fields must be zero. +fn read_fullbox_version_no_flags(src: &mut T) -> Result { + let (version, flags) = read_fullbox_extra(src)?; + + if flags != 0 { + return Err(Error::Unsupported("expected flags to be 0")); + } + + Ok(version) +} + +/// Skip over the entire contents of a box. +fn skip_box_content(src: &mut BMFFBox<'_, T>) -> Result<()> { + // Skip the contents of unknown chunks. + let to_skip = { + let header = src.get_header(); + debug!("{:?} (skipped)", header); + header + .size + .checked_sub(header.offset) + .expect("header offset > size") + }; + assert_eq!(to_skip, src.bytes_left()); + skip(src, to_skip) +} + +/// Skip over the remain data of a box. +fn skip_box_remain(src: &mut BMFFBox<'_, T>) -> Result<()> { + let remain = { + let header = src.get_header(); + let len = src.bytes_left(); + debug!("remain {} (skipped) in {:?}", len, header); + len + }; + skip(src, remain) +} + +/// Read the contents of an AVIF file +/// +/// Metadata is accumulated and returned in [`AvifData`] struct, +pub fn read_avif(f: &mut T) -> Result { + let mut f = OffsetReader::new(f); + + let mut iter = BoxIter::new(&mut f); + + // 'ftyp' box must occur first; see ISO 14496-12:2015 § 4.3.1 + if let Some(mut b) = iter.next_box()? { + if b.head.name == BoxType::FileTypeBox { + let ftyp = read_ftyp(&mut b)?; + if ftyp.major_brand != b"avif" { + return Err(Error::InvalidData("ftyp must be 'avif'")); + } + } else { + return Err(Error::InvalidData("'ftyp' box must occur first")); + } + } + + let mut meta = None; + let mut mdats = TryVec::new(); + + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::MetadataBox => { + if meta.is_some() { + return Err(Error::InvalidData( + "There should be zero or one meta boxes per ISO 14496-12:2015 § 8.11.1.1", + )); + } + meta = Some(read_avif_meta(&mut b)?); + } + BoxType::MediaDataBox => { + if b.bytes_left() > 0 { + let offset = b.offset(); + let data = b.read_into_try_vec()?; + mdats.push(MediaDataBox { offset, data })?; + } + } + _ => skip_box_content(&mut b)?, + } + + check_parser_state!(b.content); + } + + let meta = meta.ok_or(Error::InvalidData("missing meta"))?; + + let alpha_item_id = meta + .item_references + .iter() + // Auxiliary image for the primary image + .filter(|iref| { + iref.to_item_id == meta.primary_item_id + && iref.from_item_id != meta.primary_item_id + && iref.item_type == b"auxl" + }) + .map(|iref| iref.from_item_id) + // which has the alpha property + .find(|&item_id| { + meta.properties.iter().any(|prop| { + prop.item_id == item_id + && match &prop.property { + ItemProperty::AuxiliaryType(urn) => { + urn.aux_type.as_slice() + == "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha".as_bytes() + } + _ => false, + } + }) + }); + + let mut context = AvifData::default(); + context.premultiplied_alpha = alpha_item_id.map_or(false, |alpha_item_id| { + meta.item_references.iter().any(|iref| { + iref.from_item_id == meta.primary_item_id + && iref.to_item_id == alpha_item_id + && iref.item_type == b"prem" + }) + }); + + // load data of relevant items + for loc in meta.iloc_items.iter() { + let item_data = if loc.item_id == meta.primary_item_id { + &mut context.primary_item + } else if Some(loc.item_id) == alpha_item_id { + context.alpha_item.get_or_insert_with(TryVec::new) + } else { + continue; + }; + + if loc.construction_method != ConstructionMethod::File { + return Err(Error::Unsupported("unsupported construction_method")); + } + for extent in loc.extents.iter() { + let mut found = false; + // try to find an overlapping mdat + for mdat in mdats.iter_mut() { + if mdat.matches_extent(&extent.extent_range) { + item_data.append(&mut mdat.data)?; + found = true; + break; + } else if mdat.contains_extent(&extent.extent_range) { + mdat.read_extent(&extent.extent_range, item_data)?; + found = true; + break; + } + } + if !found { + return Err(Error::InvalidData( + "iloc contains an extent that is not in mdat", + )); + } + } + } + + Ok(context) +} + +/// Parse a metadata box in the context of an AVIF +/// Currently requires the primary item to be an av01 item type and generates +/// an error otherwise. +/// See ISO 14496-12:2015 § 8.11.1 +fn read_avif_meta(src: &mut BMFFBox<'_, T>) -> Result { + let version = read_fullbox_version_no_flags(src)?; + + if version != 0 { + return Err(Error::Unsupported("unsupported meta version")); + } + + let mut primary_item_id = None; + let mut item_infos = None; + let mut iloc_items = None; + let mut item_references = TryVec::new(); + let mut properties = TryVec::new(); + + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::ItemInfoBox => { + if item_infos.is_some() { + return Err(Error::InvalidData( + "There should be zero or one iinf boxes per ISO 14496-12:2015 § 8.11.6.1", + )); + } + item_infos = Some(read_iinf(&mut b)?); + } + BoxType::ItemLocationBox => { + if iloc_items.is_some() { + return Err(Error::InvalidData( + "There should be zero or one iloc boxes per ISO 14496-12:2015 § 8.11.3.1", + )); + } + iloc_items = Some(read_iloc(&mut b)?); + } + BoxType::PrimaryItemBox => { + if primary_item_id.is_some() { + return Err(Error::InvalidData( + "There should be zero or one iloc boxes per ISO 14496-12:2015 § 8.11.4.1", + )); + } + primary_item_id = Some(read_pitm(&mut b)?); + } + BoxType::ImageReferenceBox => { + item_references.append(&mut read_iref(&mut b)?)?; + } + BoxType::ImagePropertiesBox => { + properties = read_iprp(&mut b)?; + } + _ => skip_box_content(&mut b)?, + } + + check_parser_state!(b.content); + } + + let primary_item_id = primary_item_id.ok_or(Error::InvalidData( + "Required pitm box not present in meta box", + ))?; + + let item_infos = item_infos.ok_or(Error::InvalidData("iinf missing"))?; + + if let Some(item_info) = item_infos.iter().find(|x| x.item_id == primary_item_id) { + if &item_info.item_type.to_be_bytes() != b"av01" { + warn!("primary_item_id type: {}", U32BE(item_info.item_type)); + return Err(Error::InvalidData("primary_item_id type is not av01")); + } + } else { + return Err(Error::InvalidData( + "primary_item_id not present in iinf box", + )); + } + + Ok(AvifMeta { + properties, + item_references, + primary_item_id, + iloc_items: iloc_items.ok_or(Error::InvalidData("iloc missing"))?, + }) +} + +/// Parse a Primary Item Box +/// See ISO 14496-12:2015 § 8.11.4 +fn read_pitm(src: &mut BMFFBox<'_, T>) -> Result { + let version = read_fullbox_version_no_flags(src)?; + + let item_id = match version { + 0 => be_u16(src)?.into(), + 1 => be_u32(src)?, + _ => return Err(Error::Unsupported("unsupported pitm version")), + }; + + Ok(item_id) +} + +/// Parse an Item Information Box +/// See ISO 14496-12:2015 § 8.11.6 +fn read_iinf(src: &mut BMFFBox<'_, T>) -> Result> { + let version = read_fullbox_version_no_flags(src)?; + + match version { + 0 | 1 => (), + _ => return Err(Error::Unsupported("unsupported iinf version")), + } + + let entry_count = if version == 0 { + be_u16(src)?.to_usize() + } else { + be_u32(src)?.to_usize() + }; + let mut item_infos = TryVec::with_capacity(entry_count)?; + + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + if b.head.name != BoxType::ItemInfoEntry { + return Err(Error::InvalidData( + "iinf box should contain only infe boxes", + )); + } + + item_infos.push(read_infe(&mut b)?)?; + + check_parser_state!(b.content); + } + + Ok(item_infos) +} + +/// A simple wrapper to interpret a u32 as a 4-byte string in big-endian +/// order without requiring any allocation. +struct U32BE(u32); + +impl std::fmt::Display for U32BE { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match std::str::from_utf8(&self.0.to_be_bytes()) { + Ok(s) => f.write_str(s), + Err(_) => write!(f, "{:x?}", self.0), + } + } +} + +/// Parse an Item Info Entry +/// See ISO 14496-12:2015 § 8.11.6.2 +fn read_infe(src: &mut BMFFBox<'_, T>) -> Result { + // According to the standard, it seems the flags field should be 0, but + // at least one sample AVIF image has a nonzero value. + let (version, _) = read_fullbox_extra(src)?; + + // mif1 brand (see ISO 23008-12:2017 § 10.2.1) only requires v2 and 3 + let item_id = match version { + 2 => be_u16(src)?.into(), + 3 => be_u32(src)?, + _ => return Err(Error::Unsupported("unsupported version in 'infe' box")), + }; + + let item_protection_index = be_u16(src)?; + + if item_protection_index != 0 { + return Err(Error::Unsupported( + "protected items (infe.item_protection_index != 0) are not supported", + )); + } + + let item_type = be_u32(src)?; + debug!("infe item_id {} item_type: {}", item_id, U32BE(item_type)); + + // There are some additional fields here, but they're not of interest to us + skip_box_remain(src)?; + + Ok(ItemInfoEntry { item_id, item_type }) +} + +fn read_iref(src: &mut BMFFBox<'_, T>) -> Result> { + let mut item_references = TryVec::new(); + let version = read_fullbox_version_no_flags(src)?; + if version > 1 { + return Err(Error::Unsupported("iref version")); + } + + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + let from_item_id = if version == 0 { + be_u16(&mut b)?.into() + } else { + be_u32(&mut b)? + }; + let reference_count = be_u16(&mut b)?; + for _ in 0..reference_count { + let to_item_id = if version == 0 { + be_u16(&mut b)?.into() + } else { + be_u32(&mut b)? + }; + if from_item_id == to_item_id { + return Err(Error::InvalidData( + "from_item_id and to_item_id must be different", + )); + } + item_references.push(SingleItemTypeReferenceBox { + item_type: b.head.name.into(), + from_item_id, + to_item_id, + })?; + } + check_parser_state!(b.content); + } + Ok(item_references) +} + +fn read_iprp(src: &mut BMFFBox<'_, T>) -> Result> { + let mut iter = src.box_iter(); + let mut properties = TryVec::new(); + let mut associations = TryVec::new(); + + while let Some(mut b) = iter.next_box()? { + match b.head.name { + BoxType::ItemPropertyContainerBox => { + properties = read_ipco(&mut b)?; + } + BoxType::ItemPropertyAssociationBox => { + associations = read_ipma(&mut b)?; + } + _ => return Err(Error::InvalidData("unexpected ipco child")), + } + } + + let mut associated = TryVec::new(); + for a in associations { + let index = match a.property_index { + 0 => continue, + x => x as usize - 1, + }; + if let Some(prop) = properties.get(index) { + if *prop != ItemProperty::Unsupported { + associated.push(AssociatedProperty { + item_id: a.item_id, + property: prop.try_clone()?, + })?; + } + } + } + Ok(associated) +} + +#[derive(Debug, PartialEq)] +pub(crate) enum ItemProperty { + Channels(TryVec), + AuxiliaryType(AuxiliaryTypeProperty), + Unsupported, +} + +impl TryClone for ItemProperty { + fn try_clone(&self) -> Result { + Ok(match self { + Self::Channels(val) => Self::Channels(val.try_clone()?), + Self::AuxiliaryType(val) => Self::AuxiliaryType(val.try_clone()?), + Self::Unsupported => Self::Unsupported, + }) + } +} + +struct Association { + item_id: u32, + essential: bool, + property_index: u16, +} + +pub(crate) struct AssociatedProperty { + pub item_id: u32, + pub property: ItemProperty, +} + +fn read_ipma(src: &mut BMFFBox<'_, T>) -> Result> { + let (version, flags) = read_fullbox_extra(src)?; + + let mut associations = TryVec::new(); + + let entry_count = be_u32(src)?; + for _ in 0..entry_count { + let item_id = if version == 0 { + be_u16(src)?.into() + } else { + be_u32(src)? + }; + let association_count = src.read_u8()?; + for _ in 0..association_count { + let num_association_bytes = if flags & 1 == 1 { 2 } else { 1 }; + let association = src.take(num_association_bytes).read_into_try_vec()?; + let mut association = BitReader::new(association.as_slice()); + let essential = association.read_bool()?; + let property_index = association.read_u16(association.remaining().try_into()?)?; + associations.push(Association { + item_id, + essential, + property_index, + })?; + } + } + Ok(associations) +} + +fn read_ipco(src: &mut BMFFBox<'_, T>) -> Result> { + let mut properties = TryVec::new(); + + let mut iter = src.box_iter(); + while let Some(mut b) = iter.next_box()? { + // Must push for every property to have correct index for them + properties.push(match b.head.name { + BoxType::PixelInformationBox => ItemProperty::Channels(read_pixi(&mut b)?), + BoxType::AuxiliaryTypeProperty => ItemProperty::AuxiliaryType(read_auxc(&mut b)?), + _ => { + skip_box_remain(&mut b)?; + ItemProperty::Unsupported + } + })?; + } + Ok(properties) +} + +fn read_pixi(src: &mut BMFFBox<'_, T>) -> Result> { + let version = read_fullbox_version_no_flags(src)?; + if version != 0 { + return Err(Error::Unsupported("pixi version")); + } + + let num_channels = src.read_u8()?.into(); + let mut channels = TryVec::with_capacity(num_channels)?; + let num_channels_read = src.try_read_to_end(&mut channels)?; + + if num_channels_read != num_channels { + return Err(Error::InvalidData("invalid num_channels")); + } + + check_parser_state!(src.content); + Ok(channels) +} + +#[derive(Debug, PartialEq)] +pub struct AuxiliaryTypeProperty { + aux_type: TryString, + aux_subtype: TryString, +} + +impl TryClone for AuxiliaryTypeProperty { + fn try_clone(&self) -> Result { + Ok(AuxiliaryTypeProperty { + aux_type: self.aux_type.try_clone()?, + aux_subtype: self.aux_subtype.try_clone()?, + }) + } +} + +fn read_auxc(src: &mut BMFFBox<'_, T>) -> Result { + let version = read_fullbox_version_no_flags(src)?; + if version != 0 { + return Err(Error::Unsupported("auxC version")); + } + + let mut aux = TryString::new(); + src.try_read_to_end(&mut aux)?; + + let (aux_type, aux_subtype): (TryString, TryVec); + if let Some(nul_byte_pos) = aux.iter().position(|&b| b == b'\0') { + let (a, b) = aux.as_slice().split_at(nul_byte_pos); + aux_type = a.try_into()?; + aux_subtype = (&b[1..]).try_into()?; + } else { + aux_type = aux; + aux_subtype = TryVec::new(); + } + + Ok(AuxiliaryTypeProperty { + aux_type, + aux_subtype, + }) +} + +/// Parse an item location box inside a meta box +/// See ISO 14496-12:2015 § 8.11.3 +fn read_iloc(src: &mut BMFFBox<'_, T>) -> Result> { + let version: IlocVersion = read_fullbox_version_no_flags(src)?.try_into()?; + + let iloc = src.read_into_try_vec()?; + let mut iloc = BitReader::new(&iloc); + + let offset_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?; + let length_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?; + let base_offset_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?; + + let index_size: Option = match version { + IlocVersion::One | IlocVersion::Two => Some(iloc.read_u8(4)?.try_into()?), + IlocVersion::Zero => { + let _reserved = iloc.read_u8(4)?; + None + } + }; + + let item_count = match version { + IlocVersion::Zero | IlocVersion::One => iloc.read_u32(16)?, + IlocVersion::Two => iloc.read_u32(32)?, + }; + + let mut items = TryVec::with_capacity(item_count.to_usize())?; + + for _ in 0..item_count { + let item_id = match version { + IlocVersion::Zero | IlocVersion::One => iloc.read_u32(16)?, + IlocVersion::Two => iloc.read_u32(32)?, + }; + + // The spec isn't entirely clear how an `iloc` should be interpreted for version 0, + // which has no `construction_method` field. It does say: + // "For maximum compatibility, version 0 of this box should be used in preference to + // version 1 with `construction_method==0`, or version 2 when possible." + // We take this to imply version 0 can be interpreted as using file offsets. + let construction_method = match version { + IlocVersion::Zero => ConstructionMethod::File, + IlocVersion::One | IlocVersion::Two => { + let _reserved = iloc.read_u16(12)?; + match iloc.read_u16(4)? { + 0 => ConstructionMethod::File, + 1 => ConstructionMethod::Idat, + 2 => return Err(Error::Unsupported("construction_method 'item_offset' is not supported")), + _ => return Err(Error::InvalidData("construction_method is taken from the set 0, 1 or 2 per ISO 14496-12:2015 § 8.11.3.3")) + } + } + }; + + let data_reference_index = iloc.read_u16(16)?; + + if data_reference_index != 0 { + return Err(Error::Unsupported( + "external file references (iloc.data_reference_index != 0) are not supported", + )); + } + + let base_offset = iloc.read_u64(base_offset_size.to_bits())?; + let extent_count = iloc.read_u16(16)?; + + if extent_count < 1 { + return Err(Error::InvalidData( + "extent_count must have a value 1 or greater per ISO 14496-12:2015 § 8.11.3.3", + )); + } + + let mut extents = TryVec::with_capacity(extent_count.to_usize())?; + + for _ in 0..extent_count { + // Parsed but currently ignored, see `ItemLocationBoxExtent` + let _extent_index = match &index_size { + None | Some(IlocFieldSize::Zero) => None, + Some(index_size) => { + debug_assert!(version == IlocVersion::One || version == IlocVersion::Two); + Some(iloc.read_u64(index_size.to_bits())?) + } + }; + + // Per ISO 14496-12:2015 § 8.11.3.1: + // "If the offset is not identified (the field has a length of zero), then the + // beginning of the source (offset 0) is implied" + // This behavior will follow from BitReader::read_u64(0) -> 0. + let extent_offset = iloc.read_u64(offset_size.to_bits())?; + let extent_length = iloc.read_u64(length_size.to_bits())?; + + // "If the length is not specified, or specified as zero, then the entire length of + // the source is implied" (ibid) + let start = base_offset + .checked_add(extent_offset) + .ok_or(Error::InvalidData("offset calculation overflow"))?; + let extent_range = if extent_length == 0 { + ExtentRange::ToEnd(RangeFrom { start }) + } else { + let end = start + .checked_add(extent_length) + .ok_or(Error::InvalidData("end calculation overflow"))?; + ExtentRange::WithLength(Range { start, end }) + }; + + extents.push(ItemLocationBoxExtent { extent_range })?; + } + + items.push(ItemLocationBoxItem { + item_id, + construction_method, + extents, + })?; + } + + if iloc.remaining() == 0 { + Ok(items) + } else { + Err(Error::InvalidData("invalid iloc size")) + } +} + +/// Parse an ftyp box. +/// See ISO 14496-12:2015 § 4.3 +fn read_ftyp(src: &mut BMFFBox<'_, T>) -> Result { + let major = be_u32(src)?; + let minor = be_u32(src)?; + let bytes_left = src.bytes_left(); + if bytes_left % 4 != 0 { + return Err(Error::InvalidData("invalid ftyp size")); + } + // Is a brand_count of zero valid? + let brand_count = bytes_left / 4; + let mut brands = TryVec::with_capacity(brand_count.try_into()?)?; + for _ in 0..brand_count { + brands.push(be_u32(src)?.into())?; + } + Ok(FileTypeBox { + major_brand: From::from(major), + minor_version: minor, + compatible_brands: brands, + }) +} + +/// Skip a number of bytes that we don't care to parse. +fn skip(src: &mut T, bytes: u64) -> Result<()> { + std::io::copy(&mut src.take(bytes), &mut std::io::sink())?; + Ok(()) +} + +fn be_u16(src: &mut T) -> Result { + src.read_u16::().map_err(From::from) +} + +fn be_u32(src: &mut T) -> Result { + src.read_u32::().map_err(From::from) +} + +fn be_u64(src: &mut T) -> Result { + src.read_u64::().map_err(From::from) +} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..a893f7e --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,12 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +macro_rules! check_parser_state { + ( $src:expr ) => { + if $src.limit() > 0 { + debug!("bad parser state: {} content bytes left", $src.limit()); + return Err(Error::InvalidData("unread box content or bad parser sync")); + } + }; +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..4097df5 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,26 @@ +//! Module for parsing ISO Base Media Format aka video/mp4 streams. +//! Internal unit tests. + +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use fallible_collections::TryRead as _; + +use std::convert::TryInto as _; + +use std::io::Read as _; + +#[test] +fn read_to_end_() { + let mut src = b"1234567890".take(5); + let buf = src.read_into_try_vec().unwrap(); + assert_eq!(buf.len(), 5); + assert_eq!(buf, b"12345".as_ref()); +} + +#[test] +fn read_to_end_oom() { + let mut src = b"1234567890".take(std::usize::MAX.try_into().expect("usize < u64")); + assert!(src.read_into_try_vec().is_err()); +} -- 2.34.1