├── LICENSE ├── README.md ├── docs ├── Static-Colour-Mapping-Metadata-lut3d-v1-rfc.md └── contributing.md └── lut3d_utils ├── __init__.py ├── __main__.py ├── data ├── hlg_bt2020_to_bt709_33x33x33.cube └── testsrc_1920x1080.mp4 ├── lut3d_util.py ├── lut3d_util_test.py └── mpeg ├── __init__.py ├── box.py ├── constants.py ├── container.py └── mpeg4_container.py /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tone Map Metadata (3D Look-Up-Table) Injector 2 | 3 | A tool for manipulating 4 | [production metadata](../docs/Static-Colour-Mapping-Metadata-lut3d-rfc.md), 5 | specifically tone map metadata (3D LUT), in MP4 and MOV files. It can be used to 6 | inject 3D LUT metadata into a file or validate metadata in an existing file. 7 | 8 | ## Usage 9 | 10 | [Python 3.11](https://www.python.org/downloads/) must be used to run the tool. 11 | From within the directory above `lut3d_utils`: 12 | 13 | #### Help 14 | 15 | ``` 16 | python lut3d_utils -h 17 | ``` 18 | 19 | Prints help and usage information. 20 | 21 | #### Inject 22 | 23 | ``` 24 | python lut3d_utils -inject_lut3d -i ${input_file} -o ${output_file} -l ${lut3d_file} -p COLOUR_PRIMARIES_BT709 -t COLOUR_TRANSFER_CHARACTERISTICS_GAMMA22 25 | ``` 26 | 27 | Loads a tone mapping (3D LUT) metadata from `lut3d_file` and inject it to 28 | `input_file`(.mov or .mp4). The specified `output_colour_primaries` and 29 | `output_colour_transfer_characteristics` are injected too. It saves the result 30 | to `output_file`. \ 31 | `input_file` and `output_file` must not be the same file. 32 | 33 | #### Examine 34 | 35 | ``` 36 | python lut3d_utils -retrieve_lut3d -i ${input} -l ${lut3d_file} 37 | ``` 38 | 39 | Checks if `input_file` contains 3D LUT metadata. If so, parses the metadata and 40 | prints it out. In addition, it saves the 3D LUT entries to `lut3d_file` as a 41 | ".cube" file. 42 | 43 | ## Running unit tests 44 | 45 | 46 | ``` 47 | pytest lut3d_utils/lut3d_util_test.py 48 | ``` 49 | 50 | -------------------------------------------------------------------------------- /docs/Static-Colour-Mapping-Metadata-lut3d-v1-rfc.md: -------------------------------------------------------------------------------- 1 | # Static Colour Mapping Metadata (3D Look-Up-Table) Version 1 RFC 2 | *This document describes a revised open metadata scheme by which MP4 (ISOBMFF) multimedia containers may accommodate colour tone mapping metadata (as production metadata) to map from an input R’G’B’ colour space to an output R’G’B’ colour space, including HDR to SDR video conversion. Comments are welcome by 3 | filing an issue on GitHub.* 4 | 5 | ------------------------------------------------------ 6 | 7 | ## Production Metadata in MP4 (ISOBMFF) 8 | Production metadata is the information that may be used in a production process and is not required for playback or delivery. 9 | The 3D Look-Up-Table (3D LUT) is a kind of production metadata that may be used in a production process to map between R’G’B’ colour spaces, including HDR to SDR video conversion. 10 | Production metadata is stored in a new box, `prmd`, defined in this RFC, in 11 | an MP4 (ISOBMFF) container. The metadata is applicable to individual video 12 | tracks in the container. In order to allow the production metadata to be associated to a `VisualSampleEntry`, 13 | the `prmr` box holds an identifier that is to be referred from the `VisualSampleEntry` to look up the 14 | correspoding production metadata. 15 | 16 | ### Production Metadata Box (prmd) 17 | #### Definition 18 | BoxType: `prmd` 19 | Container: UserData Box (`udta`) of the corresponding Track Box (`trak`) 20 | Mandatory: No 21 | Quantity: Zero or more 22 | 23 | Contains the production metadata which can be stored in an MP4 (ISOBMFF) container. The box is a child box of the track-level `UserDataBox` of the corresponding track. 24 | 25 | Different types of production metadata can be defined in the `ProductionMetadataBox`. Two types of metadata (`lut3` and `l3ur`) are defined in the current version of this specification, specifically for the static tone mapping metadata (3D LUT). Such metadata types may be used for colour space conversion in a production process. 26 | 27 | 28 | #### Syntax 29 | ``` 30 | aligned(8) class ProductionMetadataBox extends FullBox('prmd', 0, 0) { 31 | unsigned int(8) metadata_connection_uuid[16]; 32 | unsigned int(32) production_metadata_type; 33 | if (production_metadata_type == 'lut3') { 34 | unsigned int(8) lut_size; 35 | unsigned int(8) output_colour_primaries; 36 | unsigned int(8) output_transfer_characteristics; 37 | unsigned int(16) lut_value[lut_size][lut_size][lut_size][3]; 38 | } else if (production_metadata_type == 'l3ur') { 39 | utf8string lut3_uri; 40 | } 41 | } 42 | ``` 43 | 44 | #### Semantics 45 | 46 | - `metadata_connection_uuid` is a Universally Unique Identifiers (UUID) that is used to match with the same UUID held in a referent (e.g., `SampleEntryProductionMetadataReferenceBox`). 47 | A value of 0 for all elements of `metadata_connection_uuid` is valid but should be considered a placeholder and treated as absent. It shouldn't be used for look up from a referent. The value of 0 can be replaced with a non-zero UUID once known. 48 | - `production_metadata_type` is the type of contained production metadata that indicates different production metadata structures. Two types are defined in the above syntax. Unrecognized values of `production_metadata_type` shall be treated as the `ProductionMetadataBox` is absent. 49 | - `lut_size` is the size of the 1st, 2nd, and 3rd dimension of the 3D LUT (defined by `lut_value`). The size shall not be zero. 50 | - `output_colour_primaries` specifies the output colour primaries associated with the output R’G’B’ of the LUT specifying the CIE 1931 xy chromaticity coordinates of the white point and the red, green, and blue primaries. The `output_colour_primaries` uses the code points defined for ColourPrimaries from [ITU-T H.273](https://www.itu.int/rec/T-REC-H.273). 51 | - `output_transfer_characteristics` specifies the output transfer characteristics associated with the output R’G’B’ of the LUT specifying the nonlinear transfer function characteristics used to translate between RGB colour space values and Y´CbCr values. The `output_transfer_characteristics` uses the code points defined for TransferCharacteristics from [ITU-T H.273](https://www.itu.int/rec/T-REC-H.273). 52 | - `lut_value` contains the static 3D look-up-table (LUT) for colour mapping. The LUT maps an input R'G'B' colour to an output R′G′B′ colour (big-endian). For YCbCr input video, the input R'G'B' obtained by applying the MatrixCoefficients associated to the input video stream. 53 | The `lut_value` shall contain the table entries for the LUT from the minimum to the maximum input values, with the third component index changing fastest (i.e. `lut_value[r_i][g_i][b_i][c_i] = data[(r_i * n * n + g_i * n + b_i) * 3 + c_i]`). The 3D LUT has dimensions lut_size-by-lut_size-by-lut_size-by-3. 54 | To look up an input RGB (a vector with 3 elements in [0, 1]), which must be in full range, a conversion step could be needed to convert from the coded video format to RGB, subsequently RGB will be mapped to `[0, lut_size - 1]` (simply by multiplying RGB values by `lut_size - 1`) to find the entries. Since the mapped RGB (which is assumed to be in full range) is not always on a lattice point, the output values shall be interpolated, preferably using tetrahedral interpolation, as detailed in [Specification S-2014-006 Common LUT Format (CLF)](https://community.acescentral.com/uploads/short-url/iHX8xsDczlEg7l7OtIbJrbPvm4C.pdf)- Appendix B. To generate a 3D LUT, one way is to follow Section 5.2 of [Rep. ITU-R BT.2446-1](https://www.itu.int/dms_pub/itu-r/opb/rep/R-REP-BT.2446-1-2021-PDF-E.pdf). 55 | The `lut_value` is a 1.15 fixed-point value (16-bit fixed point number, of which 15 rightmost bits are fractional) in the range [0, 2.0) (Note: this representation allows for the lut_value representation to have headroom above 1.0 to avoid potential 1-crossing interpolation distortion issues). 56 | - `lut3_uri` is a Universal Resource Identifier (URI) that refers to a 3D LUT with the following representation (same as `lut3`) in a binary file: 57 | ``` 58 | struct LUT3 { 59 | unsigned int(8) lut_size; 60 | unsigned int(8) output_colour_primaries; 61 | unsigned int(8) output_transfer_characteristics; 62 | unsigned int(16) lut_value[lut_size][lut_size][lut_size][3]; 63 | } 64 | ``` 65 | ### Production Metadata Reference Box (prmr) 66 | #### Definition 67 | BoxType: `prmr` 68 | Container: VisualSampleEntry (e.g. `avc1`, `hev1`) 69 | Mandatory: No 70 | Quantity: Zero or more 71 | 72 | The`ProductionMetadataBox` should be accessible from the `VisualSampleEntry`. The new box `ProductionMetadataReferenceBox` holds an identifier used to look up production metadata associated with the particular `VisualSampleEntry`. This indirection allows the `VisualSampleEntry` to avoid containing potentially large production metadata and also to reference the same production metadata among multiple sample entries of the track. 73 | The `connection_uuid` holds a UUID that matches production metadata found elsewhere in the `TrackBox`.The production metadata associated with the `connection_uuid` shall be unique and the search for the match can end once a matching production metadata item is found. 74 | #### Syntax 75 | ``` 76 | aligned(8) class VisualSampleEntryProductionMetadataReferenceBox extends FullBox('prmr', 0, 0) { 77 | unsigned int(8) connection_uuid[16]; 78 | } 79 | ``` 80 | #### Semantics 81 | - `connection_uuid` is the Universally Unique Identifiers (UUID) that refers to production metadata associated with the `VisualSampleEntry`. This UUID is used to look up production metadata found elsewhere in the `TrackBox`. The production metadata associated with the `connection_uuid` shall be unique and the search for the match can end once a matching production metadata item is found. If the `connection_uuid` cannot be resolved to production metadata, the reference shall be treated as absent. The algorithm used for UUID generation shall be privacy preserving. 82 | The value of 0 for all elements of `connection_uuid` is a placeholder and valid which shall be treated as absent. This placeholder value can be replaced once a non-zero UUID is known. 83 | 84 | ### Example 85 | 86 | Here is an example box hierarchy for a file containing the PRMR/PRMD metadata: 87 | 88 | ``` 89 | [moov: Movie Box] 90 | [trak: Video Track Box] 91 | [mdia: Media Box] 92 | [minf: Media Information Box] 93 | [stbl: Sample Table Box] 94 | [stsd: Sample Table Sample Descriptor] 95 | [avc1: Advance Video Coding Box] 96 | [avcC: AVC Configuration Box] 97 | ... 98 | [prmr: Production Metadata Reference Box] 99 | connection_uuid = 0xe6a100fc94a140668d34dd98d671bae8 100 | ... 101 | ... 102 | [udta: User Data Box] 103 | ... 104 | [prmd: Production Metadata Box] 105 | metadata_connection_uuid = 0xe6a100fc94a140668d34dd98d671bae8 106 | production_metadata_type = 'lut3' 107 | lut_size = 33 108 | output_colour_primaries = 1 109 | output_transfer_characteristics = 1 110 | lut_value = ... 111 | ... 112 | ... 113 | ``` 114 | 115 | The associated input video track will have MatrixCoefficients and TransferCharacteristics 116 | associated to it (via metadata in the elementary stream or metadata in a color parameter 117 | atom, `colr`). The input video signal should be transformed to R'G'B' space by applying 118 | the input matrix coefficients as defined in [ITU-T H.273](https://www.itu.int/rec/T-REC-H.273). 119 | The LUT data in the `prmd` box defines the transfomration of this R'G'B' data to an output 120 | space defined by `output_colour_primaries` and `output_transfer_characteristics`. 121 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. 4 | 5 | ## Before you begin 6 | 7 | ### Sign our Contributor License Agreement 8 | 9 | Contributions to this project must be accompanied by a 10 | [Contributor License Agreement](https://cla.developers.google.com/about) (CLA). 11 | You (or your employer) retain the copyright to your contribution; this simply 12 | gives us permission to use and redistribute your contributions as part of the 13 | project. 14 | 15 | If you or your current employer have already signed the Google CLA (even if it 16 | was for a different project), you probably don't need to do it again. 17 | 18 | Visit to see your current agreements or to 19 | sign a new one. 20 | 21 | ### Review our Community Guidelines 22 | 23 | This project follows [Google's Open Source Community 24 | Guidelines](https://opensource.google/conduct/). 25 | 26 | ## Contribution process 27 | 28 | ### Code Reviews 29 | 30 | All submissions, including submissions by project members, require review. We 31 | use GitHub pull requests for this purpose. Consult 32 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 33 | information on using pull requests. -------------------------------------------------------------------------------- /lut3d_utils/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2023 Google LLC All rights reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | # Ensure the package is available on the current path or is installed. 19 | import os 20 | import sys 21 | 22 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 23 | 24 | __all__ = ["lut3d_util", "mpeg"] 25 | 26 | import lut3d_utils.lut3d_util 27 | import lut3d_utils.mpeg 28 | -------------------------------------------------------------------------------- /lut3d_utils/__main__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2023 Google LLC All rights reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """Tone Map Metadata (3D Look-Up-Table) Injector. 18 | 19 | Tool for examining and injecting tone mapping metadata in MP4/MOV files. 20 | """ 21 | 22 | import argparse 23 | import os 24 | import sys 25 | 26 | path = os.path.dirname(sys.modules[__name__].__file__) 27 | path = os.path.join(path, "..") 28 | sys.path.insert(0, path) 29 | 30 | from lut3d_utils import lut3d_util 31 | from lut3d_utils import mpeg 32 | 33 | 34 | def main(): 35 | """Main function for printing and injecting the tone mapping metadata (3D LUT).""" 36 | 37 | parser = argparse.ArgumentParser( 38 | usage=( 39 | "%(prog)s [options] \n\nIf inject_lut3d option is set, this tool" 40 | " loads a tone mapping (3D LUT) metadata from the lut3d file(.cube" 41 | " file) and inject to the input file (.mov or .mp4) if. The tool also" 42 | " takes the output colour primaries and the output colour transfer" 43 | " characteristics using the specified options. It finally saves the" 44 | " result to the output file. If retrieve_lut3d is set, this tool will" 45 | " parse the 3D LUT metadata from the input file and write it to the" 46 | " lut3d file(.cube file). \n\n Typical usage example to inject the" 47 | " lut3d:\n python lut3d_utils -inject_lut3d -i ${input_file} -o" 48 | " ${output_file} -l ${lut3d_file} -p COLOUR_PRIMARIES_BT709 -t" 49 | " COLOUR_TRANSFER_CHARACTERISTICS_GAMMA22\n\n The following command" 50 | " parses and prints lut3d from input_file and writes its entries to" 51 | " lut3d_file:\n python lut3d_utils -retrieve_lut3d -i ${input_file}" 52 | " -l ${lut3d_file}" 53 | ) 54 | ) 55 | parser.add_argument( 56 | "-inject_lut3d", 57 | "--inject_lut3d", 58 | action="store_true", 59 | help="Inject the tone mapping metadata (3D LUT) file if it's true.", 60 | ) 61 | parser.add_argument( 62 | "-retrieve_lut3d", 63 | "--retrieve_lut3d", 64 | action="store_true", 65 | help=( 66 | "Retrieve the tone mapping metadata (3D LUT) from the input file if" 67 | " it's true." 68 | ), 69 | ) 70 | parser.add_argument( 71 | "-l", 72 | "--lut3d_file", 73 | nargs="?", 74 | help="The tone mapping metadata (3D LUT) file", 75 | ) 76 | parser.add_argument( 77 | "-i", "--input_file", nargs="?", required=True, help="The input file" 78 | ) 79 | parser.add_argument("-o", "--output_file", nargs="?", help="The output file") 80 | parser.add_argument( 81 | "-p", 82 | "--output_colour_primaries", 83 | nargs="?", 84 | default="COLOUR_PRIMARIES_BT709", 85 | help="The output colour primaries", 86 | ) 87 | parser.add_argument( 88 | "-t", 89 | "--output_colour_transfer_characteristics", 90 | nargs="?", 91 | default="COLOUR_TRANSFER_CHARACTERISTICS_BT709", 92 | help="The output colour transfer characteristics function", 93 | ) 94 | 95 | args = parser.parse_args() 96 | if ( 97 | args.inject_lut3d is not None 98 | and args.inject_lut3d 99 | and args.retrieve_lut3d is not None 100 | and args.retrieve_lut3d 101 | ): 102 | print("Error: either inject_lut3d or retrieve_lut3d must be set.") 103 | return 104 | 105 | if args.retrieve_lut3d is not None and args.retrieve_lut3d: 106 | lut3d = lut3d_util.parse_lut3d_mpeg4(args.input_file) 107 | if not lut3d: 108 | print("Not found lut3d metadata in file: " + args.input_file) 109 | return 110 | print(f"{args.input_file} contains a valid lut3d:") 111 | lut3d.print() 112 | if args.lut3d_file: 113 | lut3d.write_to_cube_file(args.lut3d_file) 114 | return 115 | if args.inject_lut3d is None or not args.inject_lut3d: 116 | print("Error: either inject_lut3d or retrieve_lut3d must be set.") 117 | return 118 | 119 | if args.output_file is None: 120 | print("No output file to save the result!") 121 | return 122 | lut3d = lut3d_util.Lut3d( 123 | output_colour_primaries=mpeg.constants.ColourPrimaries[ 124 | args.output_colour_primaries 125 | ], 126 | output_colour_transfer_characteristics=mpeg.constants.ColourTransferCharacteristics[ 127 | args.output_colour_transfer_characteristics 128 | ], 129 | ) 130 | if not lut3d.read_from_cube_file(args.lut3d_file): 131 | print("Unable to read lut3d from the file.") 132 | return 133 | if not lut3d_util.inject_lut3d_mpeg4( 134 | args.input_file, args.output_file, lut3d 135 | ): 136 | print("Failed to inject the lut3d!") 137 | return 138 | 139 | return 140 | 141 | 142 | if __name__ == "__main__": 143 | main() 144 | -------------------------------------------------------------------------------- /lut3d_utils/data/testsrc_1920x1080.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/lut3d_utils/15fab25b1964bcc255619066ae0f1c56fda6d1d3/lut3d_utils/data/testsrc_1920x1080.mp4 -------------------------------------------------------------------------------- /lut3d_utils/lut3d_util.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2023 Google LLC All rights reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """Utilities for examining/injecting the tone mapping metadata (3D LUT) in MP4/MOV files.""" 18 | 19 | import binascii 20 | import io 21 | import os 22 | import struct 23 | import uuid 24 | 25 | from lut3d_utils import mpeg 26 | 27 | MPEG_FILE_EXTENSIONS = [".mp4", ".mov"] 28 | FIXED_POINT_FRACTIONAL_BITS = 15 29 | 30 | 31 | class Lut3d(object): 32 | """A class for storing/parsing the tone mapping metadata (3D LUT). 33 | 34 | Attributes: 35 | output_colour_primaries: the output colour primaries associated with the 36 | output RGB of the LUT specifying the CIE 1931 xy chromaticity coordinates 37 | of the white point and the red, green, and blue primaries. 38 | output_colour_transfer_characteristics: output transfer function associated 39 | with the output RGB of the LUT specifying the nonlinear transfer function 40 | coefficients used to translate between RGB colour space values and YCbCr 41 | values. 42 | connection_uuid: the UUID that is used to match with the same UUID held in a 43 | referent 44 | lut_size: the size of the 1st, 2nd, and 3rd dimension of the 3D LUT (defined 45 | by lut_value). 46 | lut_value: the static 3D look-up-table (LUT) for colour mapping. The LUT 47 | maps an input RGB colour to an output R′G′B′ colour (big-endian). The 48 | lut_value is the table entries for the LUT from the minimum to the maximum 49 | input values, with the third component index changing fastest (i.e. 50 | lut_value[(r_i * n * n + g_i * n + b_i) * 3 + c_i]). The 3D LUT has 51 | dimensions lut_size-by-lut_size-by-lut_size-by-3. 52 | """ 53 | 54 | def __init__( 55 | self, 56 | output_colour_primaries: mpeg.constants.ColourPrimaries = mpeg.constants.ColourPrimaries.COLOUR_PRIMARIES_UNSPECIFIED, 57 | output_colour_transfer_characteristics: mpeg.constants.ColourTransferCharacteristics = mpeg.constants.ColourTransferCharacteristics.COLOUR_TRANSFER_CHARACTERISTICS_UNSPECIFIED, 58 | connection_uuid: str = uuid.uuid4().bytes, 59 | ): 60 | self.output_colour_primaries = output_colour_primaries 61 | self.output_colour_transfer_characteristics = ( 62 | output_colour_transfer_characteristics 63 | ) 64 | self.connection_uuid = connection_uuid 65 | self.lut_size = 0 66 | self.lut_value = None 67 | 68 | 69 | def to_3d_index(self, i): 70 | """Convert the index into a 3d tuple with last dimension changing fastest.""" 71 | return ( 72 | (i // self.lut_size // self.lut_size), 73 | (i // self.lut_size) % self.lut_size, 74 | i % self.lut_size 75 | ) 76 | 77 | 78 | def shuffle_indices(self, i): 79 | """ 80 | Convert the 3d index i from last dimension changing fastest to first dimension 81 | changing fastest (or vice versa). 82 | """ 83 | inds = self.to_3d_index(i) 84 | return (inds[2] * self.lut_size + inds[1]) * self.lut_size + inds[0] 85 | 86 | 87 | def read_from_cube_file(self, src): 88 | """Reads the lut_value from a lut3d file(.cube). 89 | 90 | Args: 91 | src: the source file to read lut3d values. 92 | 93 | Returns: 94 | True if succeeds. Otherwise False will be returned. 95 | """ 96 | 97 | infile = os.path.abspath(src) 98 | try: 99 | in_fc = open(infile, "r") 100 | except ValueError: 101 | print( 102 | f"Error: {infile} does not exist or we do not have permission:" 103 | f" {ValueError.message}" 104 | ) 105 | return False 106 | lines = in_fc.readlines() 107 | if lines is None or not lines: 108 | print("Error: The file has no data to read!") 109 | return False 110 | 111 | title = None 112 | domain_min = None 113 | domain_max = None 114 | lut3d_size = -1 115 | data = [] 116 | # Parse keywords 117 | for line in lines: 118 | line = line.strip() 119 | if not line or line.startswith("#"): 120 | continue 121 | 122 | elements = line.split() 123 | if len(elements) != 3 and data: 124 | print( 125 | "Error: all keywords shall appear before any table or the data" 126 | f" vector size shall be 3! Line: {line}" 127 | ) 128 | return False 129 | if len(elements) != 3: 130 | if elements[0] == "TITLE" and title is None: 131 | title = " ".join(elements[1:])[1:-1] 132 | elif elements[0] == "DOMAIN_MIN" and domain_min is None: 133 | domain_min = [float(x) for x in elements[1:]] 134 | elif elements[0] == "DOMAIN_MAX" and domain_max is None: 135 | domain_max = [float(x) for x in elements[1:]] 136 | elif elements[0] == "LUT_1D_SIZE": 137 | print("Error: 1D LUT is not supported!") 138 | return False 139 | elif elements[0] == "LUT_3D_SIZE" and lut3d_size < 0: 140 | if len(elements) != 2: 141 | print(f"Error: LUT_3D_SIZE shall have only one param! Line: {line}") 142 | return False 143 | lut3d_size = int(elements[1]) 144 | if lut3d_size < 2 or lut3d_size > 256: 145 | print( 146 | "Error: LUT_3D_SIZE shall be an integer in the range of" 147 | f" [2,256]. Size: {lut3d_size}" 148 | ) 149 | return False 150 | else: 151 | print(f"Error: Unknow keyword or repeated keyword! Line: {line}") 152 | return False 153 | else: 154 | data.append([float(x) for x in elements]) 155 | 156 | if lut3d_size < 0: 157 | print("Error: There is no LUT_3D_SIZE in the file.") 158 | return False 159 | if (domain_min and len(domain_min) != 3) or ( 160 | domain_max and len(domain_max) != 3 161 | ): 162 | print( 163 | "Error: domain_min/domain_max shall be vector with 3 elements." 164 | f" domain_min: {domain_min}, domain_max: {domain_max}" 165 | ) 166 | return False 167 | if len(data) != lut3d_size**3: 168 | print( 169 | f"Error: The data size is not as expected. Expected: {lut3d_size}^3 =" 170 | f" {lut3d_size**3}, Actual: {len(data)}" 171 | ) 172 | return False 173 | self.lut_size = lut3d_size 174 | self.lut_value = [data[self.shuffle_indices(i)] 175 | for i in range(self.lut_size**3)] 176 | in_fc.close() 177 | return True 178 | 179 | def create_prmd_contents(self): 180 | """Creats a Production Metadata (PRMD) box's contents (binary) from the self attributes. 181 | 182 | Returns: 183 | A binary message. 184 | """ 185 | msg = io.BytesIO() 186 | msg.write(struct.pack(">I", 0)) # version and flags = (0, 0) 187 | msg.write(self.connection_uuid) # metadata_connection_uuid 188 | msg.write(b"lut3") # production_metadata_type 189 | msg.write(struct.pack(">B", self.lut_size)) 190 | msg.write(struct.pack(">B", self.output_colour_primaries.value)) 191 | msg.write( 192 | struct.pack(">B", self.output_colour_transfer_characteristics.value) 193 | ) 194 | mul = 1 << FIXED_POINT_FRACTIONAL_BITS 195 | lut_value_fixed_point = [ 196 | int(max(min(x, 1.9999), 0) * mul + 0.5) 197 | for rgb in self.lut_value 198 | for x in rgb 199 | ] 200 | msg.write( 201 | struct.pack( 202 | ">{}H".format(len(lut_value_fixed_point)), *lut_value_fixed_point 203 | ) 204 | ) 205 | return msg 206 | 207 | def read_from_prmd_contents(self, src): 208 | """Reads the atributes from a Production Metadata (PRMD) box's contents (binary). 209 | 210 | Args: 211 | src: a binary stream or message. 212 | 213 | Returns: 214 | True if succeeds. Otherwise False will be returned. 215 | """ 216 | src_size = len(src) 217 | if src_size < 20: 218 | print("Not sufficient data to read!") 219 | return False 220 | msg = io.BytesIO(src) 221 | version_and_flags = struct.unpack(">I", msg.read(4)) 222 | if version_and_flags[0] != 0: 223 | print(f"Invalid version and flags ({version_and_flags}), should be 0") 224 | return False 225 | self.connection_uuid = msg.read(16) 226 | metadata_type = msg.read(4) 227 | if metadata_type != b"lut3": 228 | print(f"Incorrect metadata type: {metadata_type}, unable to parse it!") 229 | return False 230 | if src_size - msg.tell() < 3: 231 | print("Not sufficient data to read!") 232 | return False 233 | self.lut_size = struct.unpack("B", msg.read(1))[0] 234 | self.output_colour_primaries = mpeg.constants.ColourPrimaries( 235 | struct.unpack("B", msg.read(1))[0] 236 | ) 237 | self.output_colour_transfer_characteristics = ( 238 | mpeg.constants.ColourTransferCharacteristics( 239 | struct.unpack("B", msg.read(1))[0] 240 | ) 241 | ) 242 | if src_size - msg.tell() < 3 * pow(self.lut_size, 3) * 2: 243 | print("Not sufficient data to read!") 244 | return False 245 | self.lut_value = [[]] * pow(self.lut_size, 3) 246 | denum = 1 << FIXED_POINT_FRACTIONAL_BITS 247 | for k in range(pow(self.lut_size, 3)): 248 | self.lut_value[k] = [ 249 | float(struct.unpack(">H", msg.read(2))[0]) / denum for c in range(0, 3)] 250 | return True 251 | 252 | def print(self): 253 | """Prints the attributes.""" 254 | print("Tone Map Metadata (3D LUT) {") 255 | print( 256 | " metadata_connection_uuid (hex): " 257 | + binascii.hexlify(self.connection_uuid).decode("utf-8") 258 | ) 259 | print(f" lut_size: {self.lut_size}") 260 | print( 261 | f" output_colour_primaries: {self.output_colour_primaries.__str__()}" 262 | ) 263 | print( 264 | " output_transfer_function:" 265 | f" {self.output_colour_transfer_characteristics.__str__()}" 266 | ) 267 | print("}") 268 | 269 | def write_to_cube_file(self, dst): 270 | """Writes lut_value to a file. 271 | 272 | Args: 273 | dst: a text file path (.cube). 274 | 275 | Returns: 276 | True if succeeds. Otherwise False will be returned. 277 | """ 278 | 279 | outfile = os.path.abspath(dst) 280 | try: 281 | out_fc = open(outfile, "w") 282 | except ValueError: 283 | print( 284 | f"Error: failed to open {outfile}\n Error message:" 285 | f" {ValueError.message}" 286 | ) 287 | return False 288 | 289 | out_fc.write(f"LUT_3D_SIZE {self.lut_size}\n") 290 | for b_major_index in range(0, len(self.lut_value)): 291 | i = self.shuffle_indices(b_major_index) 292 | out_fc.write( 293 | "{0:.7f} {1:.7f} {2:.7f}\n".format( 294 | self.lut_value[i][0], 295 | self.lut_value[i][1], 296 | self.lut_value[i][2], 297 | ) 298 | ) 299 | out_fc.close() 300 | print(f"lut3d saved in file: {outfile}") 301 | return True 302 | 303 | 304 | def prmr_box(connection_uuid): 305 | """Creates a Production Metadata Reference (prmr) box. 306 | 307 | Args: 308 | connection_uuid: it is used to look up for production metadata found 309 | elsewhere in the TrackBox. 310 | 311 | Returns: 312 | A mpeg Box for Production Metadata Reference 313 | """ 314 | 315 | assert len(connection_uuid) == 16 316 | prmr_leaf = mpeg.Box() 317 | prmr_leaf.name = mpeg.constants.TAG_PRMR 318 | prmr_leaf.header_size = 8 319 | prmr_leaf.contents = struct.pack(">I", 0) + connection_uuid 320 | prmr_leaf.content_size = len(prmr_leaf.contents) 321 | return prmr_leaf 322 | 323 | 324 | def prmd_box(lut3d): 325 | """Creates a Production Metadata (prmd) box. 326 | 327 | Args: 328 | lut3d: a Lut3d object. 329 | 330 | Returns: 331 | an mpeg box containing Production Metadata (containing lut3d). 332 | """ 333 | 334 | prmd_leaf = mpeg.Box() 335 | prmd_leaf.name = mpeg.constants.TAG_PRMD 336 | prmd_leaf.header_size = 8 337 | prmd_leaf.contents = lut3d.create_prmd_contents().getvalue() 338 | prmd_leaf.content_size = len(prmd_leaf.contents) 339 | return prmd_leaf 340 | 341 | 342 | def udta_box(lut3d): 343 | """Constructs a user data box container which contains a production metadata box (prmd). 344 | 345 | Args: 346 | lut3d: a Lut3d object to be added to the user data. 347 | 348 | Returns: 349 | an mpeg box containing user data (lut3d). 350 | """ 351 | udta_container = mpeg.Container() 352 | udta_container.name = mpeg.constants.TAG_UDTA 353 | udta_container.header_size = 8 354 | udta_container.add(prmd_box(lut3d)) 355 | udta_container.content_size = len(udta_container.contents) 356 | 357 | return udta_container 358 | 359 | 360 | def mpeg4_add_lut3d(mpeg4_file, in_fh, lut3d): 361 | """Adds a lut3d metadata to an mpeg4 file for all video tracks. 362 | 363 | For every video track, creates and adds a Production Metadata box (pmrd) 364 | containing the lut3d metadata the user data box (udta) of the track. It also 365 | crates and adds a corresponding Production Metadata Reference box (prmr) to 366 | the visual sample entry box. 367 | 368 | Args: 369 | mpeg4_file: mpeg4 file structure to add lut3d. 370 | in_fh: file handle to read uncached file contents. 371 | lut3d: a Lut3d object to be added to the mpeg4 file. 372 | 373 | Returns: 374 | True if succeeds. Otherwise False will be returned. 375 | """ 376 | injected = False 377 | for element in mpeg4_file.moov_box.contents: 378 | if element.name == mpeg.constants.TAG_TRAK: 379 | added = False 380 | for sub_element in element.contents: 381 | if sub_element.name != mpeg.constants.TAG_MDIA: 382 | continue 383 | for mdia_sub_element in sub_element.contents: 384 | if mdia_sub_element.name != mpeg.constants.TAG_HDLR: 385 | continue 386 | position = mdia_sub_element.content_start() + 8 387 | in_fh.seek(position) 388 | if in_fh.read(4) == mpeg.constants.TRAK_TYPE_VIDE: 389 | added = True 390 | break 391 | if added: 392 | for mdia_sub_element in sub_element.contents: 393 | if mdia_sub_element.name != mpeg.constants.TAG_MINF: 394 | continue 395 | for minf_sub_element in mdia_sub_element.contents: 396 | if minf_sub_element.name != mpeg.constants.TAG_STBL: 397 | continue 398 | for stbl_sub_element in minf_sub_element.contents: 399 | if stbl_sub_element.name != mpeg.constants.TAG_STSD: 400 | continue 401 | for stsd_sub_element in stbl_sub_element.contents: 402 | if ( 403 | stsd_sub_element.name 404 | in mpeg.constants.VISUAL_SAMPLE_ENTRY_TYPES 405 | ): 406 | stsd_sub_element.remove(mpeg.constants.TAG_PRMR) 407 | stsd_sub_element.add(prmr_box(lut3d.connection_uuid)) 408 | print("Successfully added prmr box to Visual Sample Entry.") 409 | 410 | if added: 411 | add_udta = True 412 | for sub_element in element.contents: 413 | if sub_element.name == mpeg.constants.TAG_UDTA: 414 | add_udta = False 415 | sub_element.remove(mpeg.constants.TAG_PRMD) 416 | sub_element.add(prmd_box(lut3d)) 417 | print("Successfully added lut3d to prmd box.") 418 | if add_udta: 419 | element.add(udta_box(lut3d)) 420 | print("Successfully added udta box.") 421 | injected = True 422 | 423 | mpeg4_file.resize() 424 | return injected 425 | 426 | 427 | def inject_lut3d_mpeg4(input_file, output_file, lut3d): 428 | """Injects a lut3d metadata to an mpeg4 file. 429 | 430 | Args: 431 | input_file: the path of the mpeg4 file to add lut3d 432 | output_file: the file path to save the input mpeg4 file with lut3d added. 433 | lut3d: a Lut3d object. 434 | 435 | Returns: 436 | True if succeeds. Otherwise False will be returned. 437 | """ 438 | 439 | infile = os.path.abspath(input_file) 440 | outfile = os.path.abspath(output_file) 441 | 442 | if infile == outfile: 443 | print("Error: Input and output cannot be the same") 444 | return False 445 | 446 | try: 447 | in_fh = open(infile, "rb") 448 | except IOError: 449 | print(f"Error: {infile} does not exist or we do not have permission.") 450 | return False 451 | 452 | print(f"Processing: {infile}") 453 | 454 | extension = os.path.splitext(infile)[1].lower() 455 | 456 | if extension not in MPEG_FILE_EXTENSIONS: 457 | print("Error: Unknown file type") 458 | return False 459 | with open(infile, "rb") as in_fh: 460 | mpeg4_file = mpeg.load(in_fh) 461 | if mpeg4_file is None: 462 | print("Error: file could not be opened.") 463 | return False 464 | 465 | if not mpeg4_add_lut3d(mpeg4_file, in_fh, lut3d): 466 | print("Error failed to insert lut3d data") 467 | return False 468 | 469 | with open(outfile, "wb") as out_fh: 470 | mpeg4_file.save(in_fh, out_fh) 471 | print(f"Injected the lut3d to file: {outfile}") 472 | return True 473 | 474 | return inject_lut3d_mpeg4(infile, outfile, lut3d) 475 | 476 | 477 | def parse_lut3d_mpeg4(input_file): 478 | """Parses a lut3d metadata from an mpeg4 file. 479 | 480 | It looks for a Production Metadata box (pmrd) in video tracks, parses the 481 | lut3d in the one found first and return the lut3d. 482 | 483 | Args: 484 | input_file: the path of the mpeg4 file from which to parse lut3d. 485 | 486 | Returns: 487 | the parsed lut3d in a Lut3d object or None if not found. 488 | """ 489 | 490 | infile = os.path.abspath(input_file) 491 | try: 492 | in_fh = open(infile, "rb") 493 | in_fh.close() 494 | except IOError: 495 | print( 496 | f"Error: {infile} does not exist or we do not have permission. Error:" 497 | f" {Error.mro()}" 498 | ) 499 | return None 500 | 501 | print(f"Parsing: {infile}") 502 | 503 | extension = os.path.splitext(infile)[1].lower() 504 | 505 | if extension not in MPEG_FILE_EXTENSIONS: 506 | print("Error: Unknown file type") 507 | return None 508 | with open(input_file, "rb") as in_fh: 509 | mpeg4_file = mpeg.load(in_fh) 510 | if mpeg4_file is None: 511 | print("Error: file could not be opened.") 512 | return None 513 | ref_uuid = [] 514 | for element in mpeg4_file.moov_box.contents: 515 | if element.name == mpeg.constants.TAG_TRAK: 516 | parse = False 517 | for sub_element in element.contents: 518 | if sub_element.name != mpeg.constants.TAG_MDIA: 519 | continue 520 | for mdia_sub_element in sub_element.contents: 521 | if mdia_sub_element.name != mpeg.constants.TAG_HDLR: 522 | continue 523 | position = mdia_sub_element.content_start() + 8 524 | in_fh.seek(position) 525 | if in_fh.read(4) == mpeg.constants.TRAK_TYPE_VIDE: 526 | parse = True 527 | break 528 | if parse: 529 | for mdia_sub_element in sub_element.contents: 530 | if mdia_sub_element.name != mpeg.constants.TAG_MINF: 531 | continue 532 | for minf_sub_element in mdia_sub_element.contents: 533 | if minf_sub_element.name != mpeg.constants.TAG_STBL: 534 | continue 535 | for stbl_sub_element in minf_sub_element.contents: 536 | if stbl_sub_element.name != mpeg.constants.TAG_STSD: 537 | continue 538 | for stsd_sub_element in stbl_sub_element.contents: 539 | if ( 540 | stsd_sub_element.name 541 | in mpeg.constants.VISUAL_SAMPLE_ENTRY_TYPES 542 | ): 543 | for vse_sub_element in stsd_sub_element.contents: 544 | if vse_sub_element.name == mpeg.constants.TAG_PRMR: 545 | position = vse_sub_element.content_start() 546 | if vse_sub_element.content_size != 20: 547 | print(f"prmr box is incorrect size {vse_sub_element.content_size} != 20") 548 | else: 549 | in_fh.seek(position + 4) # Seek past version and flags 550 | ref_uuid.append( 551 | in_fh.read(vse_sub_element.content_size - 4) 552 | ) 553 | if parse: 554 | for sub_element in element.contents: 555 | if sub_element.name == mpeg.constants.TAG_UDTA: 556 | for udta_sub_element in sub_element.contents: 557 | if udta_sub_element.name == mpeg.constants.TAG_PRMD: 558 | lut3d = Lut3d() 559 | position = udta_sub_element.content_start() 560 | in_fh.seek(position) 561 | if not lut3d.read_from_prmd_contents( 562 | in_fh.read(udta_sub_element.content_size) 563 | ): 564 | return None 565 | if lut3d.connection_uuid not in ref_uuid: 566 | print( 567 | "Warning: No ref UUID was matched for the parsed lut3d!" 568 | ) 569 | return lut3d 570 | return None 571 | -------------------------------------------------------------------------------- /lut3d_utils/lut3d_util_test.py: -------------------------------------------------------------------------------- 1 | import math 2 | import tempfile 3 | import unittest 4 | 5 | from lut3d_utils import lut3d_util 6 | from lut3d_utils import mpeg 7 | from lut3d_utils.lut3d_util import Lut3d 8 | 9 | 10 | def compute_lut_diff(lut1, lut2): 11 | rmse = 0 12 | max_abs_diff = 0 13 | 14 | for i in range(0, len(lut1.lut_value)): 15 | for j in range(0, 3): 16 | diff = lut1.lut_value[i][j] - lut2.lut_value[i][j] 17 | max_abs_diff = max(max_abs_diff, max(abs(diff), max_abs_diff)) 18 | rmse += diff * diff 19 | 20 | return math.sqrt(diff / len(lut1.lut_value)), max_abs_diff 21 | 22 | 23 | class TestLut3dUtil(unittest.TestCase): 24 | 25 | def testFailsOnMissingInput(self): 26 | self.assertFalse( 27 | lut3d_util.inject_lut3d_mpeg4( 28 | '/path/to/invalid/file', '/dev/null', None 29 | ) 30 | ) 31 | 32 | def testReadWriteCube(self): 33 | lut = Lut3d() 34 | with tempfile.NamedTemporaryFile(suffix='.cube') as f: 35 | self.assertTrue( 36 | lut.read_from_cube_file( 37 | 'lut3d_utils/data/hlg_bt2020_to_bt709_33x33x33.cube' 38 | ) 39 | ) 40 | self.assertTrue(lut.write_to_cube_file(f.name)) 41 | 42 | actual_lut = Lut3d() 43 | self.assertTrue(actual_lut.read_from_cube_file(f.name)) 44 | self.assertEqual(actual_lut.lut_size, lut.lut_size) 45 | 46 | rmse, max_abs_diff = compute_lut_diff(lut, actual_lut) 47 | self.assertLess(rmse, 1e-6) 48 | self.assertLess(max_abs_diff, 1e-6) 49 | 50 | def testInsertMetadata(self): 51 | lut = Lut3d( 52 | output_colour_primaries=mpeg.constants.ColourPrimaries.COLOUR_PRIMARIES_BT709, 53 | output_colour_transfer_characteristics=mpeg.constants.ColourTransferCharacteristics.COLOUR_TRANSFER_CHARACTERISTICS_BT709, 54 | ) 55 | self.assertTrue( 56 | lut.read_from_cube_file( 57 | 'lut3d_utils/data/hlg_bt2020_to_bt709_33x33x33.cube' 58 | ) 59 | ) 60 | self.assertFalse( 61 | lut3d_util.parse_lut3d_mpeg4('lut3d_utils/data/testsrc_1920x1080.mp4') 62 | ) 63 | 64 | with tempfile.NamedTemporaryFile(suffix='.mp4') as output_file: 65 | self.assertTrue( 66 | lut3d_util.inject_lut3d_mpeg4( 67 | 'lut3d_utils/data/testsrc_1920x1080.mp4', output_file.name, lut 68 | ) 69 | ) 70 | 71 | actual_lut = lut3d_util.parse_lut3d_mpeg4(output_file.name) 72 | self.assertTrue(actual_lut != None) 73 | self.assertEqual(actual_lut.lut_size, lut.lut_size) 74 | 75 | rmse, max_abs_diff = compute_lut_diff(lut, actual_lut) 76 | self.assertLess(rmse, 1e-6) 77 | self.assertLess(max_abs_diff, 2e-5) 78 | 79 | 80 | if __name__ == '__main__': 81 | unittest.main() 82 | -------------------------------------------------------------------------------- /lut3d_utils/mpeg/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2023 Google LLC All rights reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | import lut3d_utils.mpeg.box 19 | import lut3d_utils.mpeg.constants 20 | import lut3d_utils.mpeg.container 21 | import lut3d_utils.mpeg.mpeg4_container 22 | 23 | load = mpeg4_container.load 24 | 25 | Box = box.Box 26 | Container = container.Container 27 | Mpeg4Container = mpeg4_container.Mpeg4Container 28 | 29 | __all__ = ["box", "mpeg4", "container", "constants"] 30 | -------------------------------------------------------------------------------- /lut3d_utils/mpeg/box.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2023 Google LLC All rights reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """MPEG processing classes. 18 | 19 | Tool for loading mpeg4 files and manipulating atoms. 20 | """ 21 | 22 | import io 23 | import struct 24 | 25 | from lut3d_utils.mpeg import constants 26 | 27 | 28 | def load(fh, position, end): 29 | """Loads the box located at a position in a mp4 file. 30 | 31 | Args: 32 | fh: file handle, input file handle. 33 | position: int or None, current file position. 34 | end: int or None, the end of file. 35 | 36 | Returns: 37 | box: box, box from loaded file location or None. 38 | """ 39 | if position is None: 40 | position = fh.tell() 41 | 42 | fh.seek(position) 43 | header_size = 8 44 | size = struct.unpack(">I", fh.read(4))[0] 45 | name = fh.read(4) 46 | 47 | if size == 1: 48 | size = struct.unpack(">Q", fh.read(8))[0] 49 | header_size = 16 50 | 51 | if size < 8: 52 | print("Error, invalid size {} in {} at {}".format(size, name, position)) 53 | return None 54 | 55 | if (position + size) > end: 56 | print("Error: Leaf box size exceeds bounds.") 57 | return None 58 | 59 | new_box = Box() 60 | new_box.name = name 61 | new_box.position = position 62 | new_box.header_size = header_size 63 | new_box.content_size = size - header_size 64 | new_box.contents = None 65 | 66 | return new_box 67 | 68 | 69 | class Box(object): 70 | """MPEG4 box contents and behaviour true for all boxes.""" 71 | 72 | def __init__(self): 73 | self.name = "" 74 | self.position = 0 75 | self.header_size = 0 76 | self.content_size = 0 77 | self.contents = None 78 | 79 | def content_start(self): 80 | return self.position + self.header_size 81 | 82 | def save(self, in_fh, out_fh, delta): 83 | """Save box contents prioritizing set contents. 84 | 85 | Args: 86 | in_fh: file handle, source to read box contents from. 87 | out_fh: file handle, destination for written box contents. 88 | delta: int, index update amount. 89 | """ 90 | if self.header_size == 16: 91 | out_fh.write(struct.pack(">I", 1)) 92 | out_fh.write(self.name) 93 | out_fh.write(struct.pack(">Q", self.size())) 94 | elif self.header_size == 8: 95 | out_fh.write(struct.pack(">I", self.size())) 96 | out_fh.write(self.name) 97 | 98 | if self.content_start(): 99 | in_fh.seek(self.content_start()) 100 | 101 | if self.name == constants.TAG_STCO: 102 | stco_copy(in_fh, out_fh, self, delta) 103 | elif self.name == constants.TAG_CO64: 104 | co64_copy(in_fh, out_fh, self, delta) 105 | elif self.contents: 106 | out_fh.write(self.contents) 107 | else: 108 | tag_copy(in_fh, out_fh, self.content_size) 109 | 110 | def set(self, new_contents): 111 | """Sets / overwrites the box contents.""" 112 | self.contents = new_contents 113 | self.content_size = len(self.contents) 114 | 115 | def size(self): 116 | """Total size of a box. 117 | 118 | Returns: 119 | Int, total size in bytes of the box. 120 | """ 121 | return self.header_size + self.content_size 122 | 123 | def print_structure(self, indent=""): 124 | """Prints the box structure.""" 125 | size1 = self.header_size 126 | size2 = self.content_size 127 | print("{0} {1} [{2}, {3}]".format(indent, self.name, size1, size2)) 128 | 129 | 130 | def tag_copy(in_fh, out_fh, size): 131 | """Copies a block of data from in_fh to out_fh. 132 | 133 | Args: 134 | in_fh: file handle, source of uncached file contents. 135 | out_fh: file handle, destination for saved file. 136 | size: int, amount of data to copy. 137 | """ 138 | 139 | # On 32-bit systems reading / writing is limited to 2GB chunks. 140 | # To prevent overflow, read/write 64 MB chunks. 141 | block_size = 64 * 1024 * 1024 142 | while size > block_size: 143 | contents = in_fh.read(block_size) 144 | out_fh.write(contents) 145 | size = size - block_size 146 | 147 | contents = in_fh.read(size) 148 | out_fh.write(contents) 149 | 150 | 151 | def index_copy(in_fh, out_fh, box, mode, mode_length, delta=0): 152 | """Update and copy index table for stco/co64 files. 153 | 154 | Args: 155 | in_fh: file handle, source to read index table from. 156 | out_fh: file handle, destination for index file. 157 | box: box, stco/co64 box to copy. 158 | mode: string, bit packing mode for index entries. 159 | mode_length: int, number of bytes for index entires. 160 | delta: int, offset change for index entries. 161 | """ 162 | fh = in_fh 163 | if not box.contents: 164 | fh.seek(box.content_start()) 165 | else: 166 | fh = io.BytesIO(box.contents) 167 | 168 | header = struct.unpack(">I", fh.read(4))[0] 169 | values = struct.unpack(">I", fh.read(4))[0] 170 | 171 | new_contents = [] 172 | new_contents.append(struct.pack(">I", header)) 173 | new_contents.append(struct.pack(">I", values)) 174 | for _ in range(values): 175 | content = fh.read(mode_length) 176 | content = struct.unpack(mode, content)[0] + delta 177 | new_contents.append(struct.pack(mode, content)) 178 | out_fh.write(b"".join(new_contents)) 179 | 180 | 181 | def stco_copy(in_fh, out_fh, box, delta=0): 182 | """Copy for stco box. 183 | 184 | Args: 185 | in_fh: file handle, source to read index table from. 186 | out_fh: file handle, destination for index file. 187 | box: box, stco box to copy. 188 | delta: int, offset change for index entries. 189 | """ 190 | index_copy(in_fh, out_fh, box, ">I", 4, delta) 191 | 192 | 193 | def co64_copy(in_fh, out_fh, box, delta=0): 194 | """Copy for co64 box. 195 | 196 | Args: 197 | in_fh: file handle, source to read index table from. 198 | out_fh: file handle, destination for index file. 199 | box: box, co64 box to copy. 200 | delta: int, offset change for index entries. 201 | """ 202 | index_copy(in_fh, out_fh, box, ">Q", 8, delta) 203 | -------------------------------------------------------------------------------- /lut3d_utils/mpeg/constants.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2023 Google LLC All rights reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http:#www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """MPEG-4 constants.""" 18 | 19 | from enum import Enum 20 | 21 | 22 | class ColourPrimaries(Enum): 23 | """ 24 | 25 | """ 26 | # Also ITU-R BT1361 / IEC 61966-2-4 / SMPTE RP177 Annex B. 27 | COLOUR_PRIMARIES_BT709 = 1 28 | COLOUR_PRIMARIES_UNSPECIFIED = 2 29 | COLOUR_PRIMARIES_BT470M = 4 30 | # Also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM. 31 | COLOUR_PRIMARIES_BT470BG = 5 32 | # Also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC. 33 | COLOUR_PRIMARIES_SMPTE170M = 6 34 | # Functionally identical to above. 35 | COLOUR_PRIMARIES_SMPTE240M = 7 36 | # Colour filters using Illuminant C. 37 | COLOUR_PRIMARIES_FILM = 8 38 | # ITU-R BT2020. 39 | COLOUR_PRIMARIES_BT2020 = 9 40 | # SMPTE ST428-1. 41 | COLOUR_PRIMARIES_SMPTEST428_1 = 10 42 | # SMPTE ST431-2 43 | COLOUR_PRIMARIES_SMPTE431 = 11 44 | # SMPTE ST432-1 45 | COLOUR_PRIMARIES_SMPTE432 = 12 46 | # JEDEC P22 phosphors 47 | COLOUR_PRIMARIES_JEDEC_P22 = 22 48 | 49 | 50 | class ColourTransferCharacteristics(Enum): 51 | """ 52 | 53 | """ 54 | COLOUR_TRANSFER_CHARACTERISTICS_BT709 = 1 55 | COLOUR_TRANSFER_CHARACTERISTICS_UNSPECIFIED = 2 56 | # Also ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM. 57 | COLOUR_TRANSFER_CHARACTERISTICS_GAMMA22 = 4 58 | # Also ITU-R BT470BG. 59 | COLOUR_TRANSFER_CHARACTERISTICS_GAMMA28 = 5 60 | # Also ITU-R BT601-6 525 or 625 / ITU-R BT1358 525 or 625 / ITU-R 61 | # BT1700 NTSC. 62 | COLOUR_TRANSFER_CHARACTERISTICS_SMPTE170M = 6 63 | COLOUR_TRANSFER_CHARACTERISTICS_SMPTE240M = 7 64 | # Linear transfer characteristics. 65 | COLOUR_TRANSFER_CHARACTERISTICS_LINEAR = 8 66 | # Logarithmic transfer characteristic (100:1 range). 67 | COLOUR_TRANSFER_CHARACTERISTICS_LOG = 9 68 | # Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range). 69 | COLOUR_TRANSFER_CHARACTERISTICS_LOG_SQRT = 10 70 | # IEC 61966-2-4. 71 | COLOUR_TRANSFER_CHARACTERISTICS_IEC61966_2_4 = 11 72 | # ITU-R BT1361 Extended Colour Gamut. 73 | COLOUR_TRANSFER_CHARACTERISTICS_BT1361_ECG = 12 74 | # IEC 61966-2-1 (sRGB or sYCC). 75 | COLOUR_TRANSFER_CHARACTERISTICS_IEC61966_2_1 = 13 76 | # ITU-R BT2020 for 10 bit system. 77 | COLOUR_TRANSFER_CHARACTERISTICS_BT2020_10 = 14 78 | # ITU-R BT2020 for 12 bit system. 79 | COLOUR_TRANSFER_CHARACTERISTICS_BT2020_12 = 15 80 | # SMPTE ST 2084 for 10, 12, 14 and 16 bit systems. 81 | COLOUR_TRANSFER_CHARACTERISTICS_SMPTEST2084 = 16 82 | # SMPTE ST 428-1. 83 | COLOUR_TRANSFER_CHARACTERISTICS_SMPTEST428_1 = 17 84 | # ARIB STD-B67, known as "Hybrid log-gamma". 85 | COLOUR_TRANSFER_CHARACTERISTICS_ARIB_STD_B67 = 18 86 | 87 | TRAK_TYPE_VIDE = b"vide" 88 | 89 | # Leaf types. 90 | TAG_STCO = b"stco" 91 | TAG_CO64 = b"co64" 92 | TAG_FREE = b"free" 93 | TAG_MDAT = b"mdat" 94 | TAG_XML = b"xml " 95 | TAG_HDLR = b"hdlr" 96 | TAG_FTYP = b"ftyp" 97 | TAG_ESDS = b"esds" 98 | TAG_SOUN = b"soun" 99 | TAG_SA3D = b"SA3D" 100 | TAG_PRMD = b"prmd" 101 | TAG_PRMR = b"prmr" 102 | 103 | # Container types. 104 | TAG_MOOV = b"moov" 105 | TAG_UDTA = b"udta" 106 | TAG_META = b"meta" 107 | TAG_TRAK = b"trak" 108 | TAG_MDIA = b"mdia" 109 | TAG_MINF = b"minf" 110 | TAG_STBL = b"stbl" 111 | TAG_STSD = b"stsd" 112 | TAG_UUID = b"uuid" 113 | TAG_WAVE = b"wave" 114 | 115 | # Visual sample entry types. 116 | TAG_AVC1 = b"avc1" 117 | TAG_MP4V = b"mp4v" 118 | TAG_ENCV = b"encv" 119 | TAG_S263 = b"s263" 120 | TAG_VP09 = b"vp09" 121 | TAG_AV01 = b"av01" 122 | TAG_HEV1 = b"hev1" 123 | TAG_DVH1 = b"dvh1" 124 | 125 | # Sound sample descriptions. 126 | TAG_NONE = b"NONE" 127 | TAG_RAW_ = b"raw " 128 | TAG_TWOS = b"twos" 129 | TAG_SOWT = b"sowt" 130 | TAG_FL32 = b"fl32" 131 | TAG_FL64 = b"fl64" 132 | TAG_IN24 = b"in24" 133 | TAG_IN32 = b"in32" 134 | TAG_ULAW = b"ulaw" 135 | TAG_ALAW = b"alaw" 136 | TAG_LPCM = b"lpcm" 137 | TAG_MP4A = b"mp4a" 138 | TAG_OPUS = b"Opus" 139 | 140 | SOUND_SAMPLE_DESCRIPTIONS = frozenset([ 141 | TAG_NONE, 142 | TAG_RAW_, 143 | TAG_TWOS, 144 | TAG_SOWT, 145 | TAG_FL32, 146 | TAG_FL64, 147 | TAG_IN24, 148 | TAG_IN32, 149 | TAG_ULAW, 150 | TAG_ALAW, 151 | TAG_LPCM, 152 | TAG_MP4A, 153 | TAG_OPUS, 154 | ]) 155 | 156 | VISUAL_SAMPLE_ENTRY_TYPES = frozenset([ 157 | TAG_AVC1, 158 | TAG_MP4V, 159 | TAG_ENCV, 160 | TAG_S263, 161 | TAG_VP09, 162 | TAG_AV01, 163 | TAG_HEV1, 164 | TAG_DVH1 165 | ]) 166 | 167 | CONTAINERS_LIST = frozenset([ 168 | TAG_MDIA, 169 | TAG_MINF, 170 | TAG_MOOV, 171 | TAG_STBL, 172 | TAG_STSD, 173 | TAG_TRAK, 174 | TAG_UDTA, 175 | TAG_WAVE, 176 | ]).union(SOUND_SAMPLE_DESCRIPTIONS).union(VISUAL_SAMPLE_ENTRY_TYPES) 177 | 178 | -------------------------------------------------------------------------------- /lut3d_utils/mpeg/container.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2023 Google LLC All rights reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """MPEG processing classes. 18 | 19 | Functions for loading MPEG files and manipulating boxes. 20 | """ 21 | 22 | import struct 23 | 24 | from lut3d_utils.mpeg import box 25 | from lut3d_utils.mpeg import constants 26 | 27 | 28 | def load(fh, position, end): 29 | """ 30 | 31 | Args: 32 | fh: file handle, input file handle. 33 | position: int or None, current file position. 34 | end: int or None, the end of file. 35 | 36 | Returns: 37 | box: box, box from loaded file location or None. 38 | 39 | """ 40 | if position is None: 41 | position = fh.tell() 42 | 43 | fh.seek(position) 44 | header_size = 8 45 | size = struct.unpack(">I", fh.read(4))[0] 46 | name = fh.read(4) 47 | 48 | if name not in constants.CONTAINERS_LIST: 49 | return box.load(fh, position, end) 50 | 51 | if size == 1: 52 | size = struct.unpack(">Q", fh.read(8))[0] 53 | header_size = 16 54 | 55 | if size < 8: 56 | print("Error, invalid size", size, "in", name, "at", position) 57 | return None 58 | 59 | if (position + size) > end: 60 | print("Error: Container box size exceeds bounds.") 61 | return None 62 | 63 | padding = 0 64 | if name == constants.TAG_STSD: 65 | padding = 8 66 | if name in constants.VISUAL_SAMPLE_ENTRY_TYPES: 67 | padding = 78 68 | if name in constants.SOUND_SAMPLE_DESCRIPTIONS: 69 | current_pos = fh.tell() 70 | fh.seek(current_pos + 8) 71 | sample_description_version = struct.unpack(">h", fh.read(2))[0] 72 | fh.seek(current_pos) 73 | 74 | if sample_description_version == 0: 75 | padding = 28 76 | elif sample_description_version == 1: 77 | padding = 28 + 16 78 | elif sample_description_version == 2: 79 | padding = 64 80 | else: 81 | print("Unsupported sample description version:", 82 | sample_description_version) 83 | 84 | new_box = Container() 85 | new_box.name = name 86 | new_box.position = position 87 | new_box.header_size = header_size 88 | new_box.content_size = size - header_size 89 | new_box.padding = padding 90 | new_box.contents = load_multiple(fh, position + header_size + padding, 91 | position + size) 92 | 93 | if new_box.contents is None: 94 | return None 95 | 96 | return new_box 97 | 98 | 99 | def load_multiple(fh, position=None, end=None): 100 | loaded = list() 101 | while (position < end): 102 | new_box = load(fh, position, end) 103 | if new_box is None: 104 | print("Error, failed to load box.") 105 | return None 106 | loaded.append(new_box) 107 | position = new_box.position + new_box.size() 108 | 109 | return loaded 110 | 111 | 112 | class Container(box.Box): 113 | """MPEG4 container box contents / behaviour.""" 114 | 115 | def __init__(self, padding=0): 116 | self.name = "" 117 | self.position = 0 118 | self.header_size = 0 119 | self.content_size = 0 120 | self.contents = list() 121 | self.padding = padding 122 | 123 | def resize(self): 124 | """Recomputes the box size and recurses on contents.""" 125 | self.content_size = self.padding 126 | for element in self.contents: 127 | if isinstance(element, Container): 128 | element.resize() 129 | self.content_size += element.size() 130 | 131 | def print_structure(self, indent=""): 132 | """Prints the box structure and recurses on contents.""" 133 | size1 = self.header_size 134 | size2 = self.content_size 135 | print("{0} {1} [{2}, {3}]".format(indent, self.name, size1, size2)) 136 | 137 | size = len(self.contents) 138 | for i in range(size): 139 | next_indent = indent 140 | 141 | next_indent = next_indent.replace("├", "│") 142 | next_indent = next_indent.replace("└", " ") 143 | next_indent = next_indent.replace("─", " ") 144 | 145 | if i == (size - 1): 146 | next_indent = next_indent + " └──" 147 | else: 148 | next_indent = next_indent + " ├──" 149 | 150 | element = self.contents[i] 151 | element.print_structure(next_indent) 152 | 153 | def remove(self, tag): 154 | """Removes a tag recursively from all containers.""" 155 | new_contents = [] 156 | self.content_size = 0 157 | for element in self.contents: 158 | if element.name != tag: 159 | new_contents.append(element) 160 | if isinstance(element, Container): 161 | element.remove(tag) 162 | self.content_size += element.size() 163 | self.contents = new_contents 164 | 165 | def add(self, element): 166 | """Adds an element, merging with containers of the same type. 167 | 168 | Returns: 169 | Int, increased size of container. 170 | """ 171 | for content in self.contents: 172 | if content.name == element.name: 173 | if isinstance(content, container_leaf): 174 | return content.merge(element) 175 | print("Error, cannot merge leafs.") 176 | return False 177 | 178 | self.contents.append(element) 179 | return True 180 | 181 | def merge(self, element): 182 | """Merges structure with container. 183 | 184 | Returns: 185 | Int, increased size of container. 186 | """ 187 | assert self.name == element.name 188 | assert isinstance(element, container_box) 189 | for sub_element in element.contents: 190 | if not self.add(sub_element): 191 | return False 192 | 193 | return True 194 | 195 | def save(self, in_fh, out_fh, delta): 196 | """Saves box to out_fh reading uncached content from in_fh. 197 | 198 | Args: 199 | in_fh: file handle, source of uncached file contents. 200 | out_fh: file_hande, destination for saved file. 201 | delta: int, file change size for updating stco and co64 files. 202 | """ 203 | if self.header_size == 16: 204 | out_fh.write(struct.pack(">I", 1)) 205 | out_fh.write(self.name) 206 | out_fh.write(struct.pack(">Q", self.size())) 207 | elif self.header_size == 8: 208 | out_fh.write(struct.pack(">I", self.size())) 209 | out_fh.write(self.name) 210 | 211 | if self.padding > 0: 212 | in_fh.seek(self.content_start()) 213 | box.tag_copy(in_fh, out_fh, self.padding) 214 | 215 | for element in self.contents: 216 | element.save(in_fh, out_fh, delta) 217 | -------------------------------------------------------------------------------- /lut3d_utils/mpeg/mpeg4_container.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2023 Google LLC All rights reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """MPEG4 processing classes. 18 | 19 | Functions for loading MP4/MOV files and manipulating boxes. 20 | """ 21 | 22 | from lut3d_utils.mpeg import constants 23 | from lut3d_utils.mpeg import container 24 | 25 | 26 | def load(fh): 27 | """Load the mpeg4 file structure of a file. 28 | 29 | Args: 30 | fh: file handle, input file handle. 31 | position: int, current file position. 32 | size: int, maximum size. This is used to ensure correct box sizes. 33 | return: mpeg4, the loaded mpeg4 structure. 34 | """ 35 | 36 | fh.seek(0, 2) 37 | size = fh.tell() 38 | contents = container.load_multiple(fh, 0, size) 39 | 40 | if not contents: 41 | print("Error, failed to load .mp4 file.") 42 | return None 43 | elif not contents: 44 | print("Error, no boxes found.") 45 | return None 46 | 47 | loaded_mpeg4 = Mpeg4Container() 48 | loaded_mpeg4.contents = contents 49 | 50 | for element in loaded_mpeg4.contents: 51 | if element.name == constants.TAG_MOOV: 52 | loaded_mpeg4.moov_box = element 53 | if element.name == constants.TAG_FREE: 54 | loaded_mpeg4.free_box = element 55 | if element.name == constants.TAG_MDAT and not loaded_mpeg4.first_mdat_box: 56 | loaded_mpeg4.first_mdat_box = element 57 | if element.name == constants.TAG_FTYP: 58 | loaded_mpeg4.ftyp_box = element 59 | 60 | if not loaded_mpeg4.moov_box: 61 | print("Error, file does not contain moov box.") 62 | return None 63 | 64 | if not loaded_mpeg4.first_mdat_box: 65 | print("Error, file does not contain mdat box.") 66 | return None 67 | 68 | loaded_mpeg4.first_mdat_position = loaded_mpeg4.first_mdat_box.position 69 | loaded_mpeg4.first_mdat_position += loaded_mpeg4.first_mdat_box.header_size 70 | 71 | loaded_mpeg4.content_size = 0 72 | for element in loaded_mpeg4.contents: 73 | loaded_mpeg4.content_size += element.size() 74 | 75 | return loaded_mpeg4 76 | 77 | 78 | class Mpeg4Container(container.Container): 79 | """Specialized behaviour for the root mpeg4 container.""" 80 | 81 | def __init__(self): 82 | self.contents = list() 83 | self.content_size = 0 84 | self.header_size = 0 85 | self.moov_box = None 86 | self.free_box = None 87 | self.first_mdat_box = None 88 | self.ftyp_box = None 89 | self.first_mdat_position = None 90 | self.padding = 0 91 | 92 | def merge(self, element): 93 | """Mpeg4 containers do not support merging.""" 94 | print("Cannot merge mpeg4 files") 95 | exit(0) 96 | 97 | def print_structure(self): 98 | """Print mpeg4 file structure recursively.""" 99 | print("mpeg4 [{}]".format(self.content_size)) 100 | 101 | size = len(self.contents) 102 | for i in range(size): 103 | next_indent = " ├──" 104 | if i == (size - 1): 105 | next_indent = " └──" 106 | 107 | self.contents[i].print_structure(next_indent) 108 | 109 | def save(self, in_fh, out_fh): 110 | """Save mpeg4 filecontent to file. 111 | 112 | Args: 113 | in_fh: file handle, source file handle for uncached contents. 114 | out_fh: file handle, destination file hand for saved file. 115 | """ 116 | self.resize() 117 | new_position = 0 118 | for element in self.contents: 119 | if element.name == constants.TAG_MDAT: 120 | new_position += element.header_size 121 | break 122 | new_position += element.size() 123 | delta = new_position - self.first_mdat_position 124 | 125 | for element in self.contents: 126 | element.save(in_fh, out_fh, delta) 127 | --------------------------------------------------------------------------------