├── .github ├── actions │ └── setup-release-env │ │ └── action.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── rust-toolchain.toml └── src ├── commands ├── convert.rs ├── edl.rs └── mod.rs ├── functions ├── convert.rs ├── edl.rs └── mod.rs ├── main.rs └── metadata ├── cmv29 ├── display.rs ├── frame.rs ├── mod.rs ├── shot.rs └── track.rs ├── cmv40 ├── display.rs ├── frame.rs ├── mod.rs ├── shot.rs └── track.rs ├── display ├── characteristics.rs ├── chromaticity.rs ├── mod.rs └── primary.rs ├── levels ├── level1.rs ├── level11.rs ├── level2.rs ├── level254.rs ├── level3.rs ├── level5.rs ├── level6.rs ├── level8.rs ├── level9.rs └── mod.rs └── mod.rs /.github/actions/setup-release-env/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup release env 2 | runs: 3 | using: "composite" 4 | steps: 5 | - name: Install Rust 6 | uses: dtolnay/rust-toolchain@stable 7 | 8 | - name: Get the package versions 9 | shell: bash 10 | run: | 11 | RELEASE_PKG_VERSION=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[].version') 12 | 13 | echo "RELEASE_PKG_VERSION=${RELEASE_PKG_VERSION}" >> $GITHUB_ENV 14 | echo "ARCHIVE_PREFIX=${{ env.RELEASE_BIN }}-${RELEASE_PKG_VERSION}" >> $GITHUB_ENV 15 | 16 | - name: Create artifacts directory 17 | shell: bash 18 | run: | 19 | mkdir ${{ env.RELEASE_DIR }} -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | ci: 11 | name: Check, test, rustfmt and clippy 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Install Rust, clippy and rustfmt 17 | uses: dtolnay/rust-toolchain@stable 18 | with: 19 | components: clippy, rustfmt 20 | 21 | - name: Check 22 | run: | 23 | cargo check --workspace --all-features 24 | 25 | # TODO: Test 26 | # - name: Test 27 | # run: | 28 | # cargo test --workspace --all-features 29 | 30 | - name: Rustfmt 31 | run: | 32 | cargo fmt --all --check 33 | 34 | - name: Clippy 35 | run: | 36 | cargo clippy --workspace --all-features --all-targets --tests -- --deny warnings 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | name: Artifacts 5 | 6 | env: 7 | RELEASE_BIN: dovi_meta 8 | RELEASE_DIR: artifacts 9 | WINDOWS_TARGET: x86_64-pc-windows-msvc 10 | MACOS_X86_TARGET: x86_64-apple-darwin 11 | MACOS_ARM_TARGET: aarch64-apple-darwin 12 | LINUX_TARGET: x86_64-unknown-linux-musl 13 | 14 | jobs: 15 | linux-binary: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: ./.github/actions/setup-release-env 21 | 22 | - name: Install musl-tools 23 | run: | 24 | sudo apt-get update -y 25 | sudo apt-get install musl-tools -y 26 | 27 | - name: Build 28 | run: | 29 | rustup target add ${{ env.LINUX_TARGET }} 30 | cargo build --release --target ${{ env.LINUX_TARGET }} 31 | 32 | - name: Create tarball and checksum 33 | run: | 34 | ARCHIVE_FILE=${{ env.RELEASE_DIR }}/${{ env.ARCHIVE_PREFIX }}-${{ env.LINUX_TARGET }}.tar.gz 35 | strip ./target/${{ env.LINUX_TARGET }}/release/${{ env.RELEASE_BIN }} 36 | 37 | mv ./target/${{ env.LINUX_TARGET }}/release/${{ env.RELEASE_BIN }} ./${{ env.RELEASE_BIN }} 38 | tar -cvzf ./${ARCHIVE_FILE} ./${{ env.RELEASE_BIN }} 39 | 40 | python -c "import hashlib; import pathlib; print(hashlib.sha256(pathlib.Path('${ARCHIVE_FILE}').read_bytes()).hexdigest())" > ${ARCHIVE_FILE}.sha256 41 | 42 | - name: Upload artifacts 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: Linux artifacts 46 | path: ./${{ env.RELEASE_DIR }}/* 47 | 48 | windows-binary: 49 | runs-on: windows-latest 50 | 51 | steps: 52 | - uses: actions/checkout@v4 53 | - uses: ./.github/actions/setup-release-env 54 | 55 | - name: Install cargo-c 56 | run: | 57 | $LINK = "https://github.com/lu-zero/cargo-c/releases/latest/download" 58 | $CARGO_C_FILE = "cargo-c-windows-msvc" 59 | curl -LO "$LINK/$CARGO_C_FILE.zip" 60 | 7z e -y "$CARGO_C_FILE.zip" -o"${env:USERPROFILE}\.cargo\bin" 61 | 62 | - name: Build 63 | run: cargo build --release 64 | 65 | - name: Create zipfile 66 | shell: bash 67 | run: | 68 | ARCHIVE_FILE=${{ env.RELEASE_DIR }}/${{ env.ARCHIVE_PREFIX }}-${{ env.WINDOWS_TARGET }}.zip 69 | mv ./target/release/${{ env.RELEASE_BIN }}.exe ./${{ env.RELEASE_BIN }}.exe 70 | 7z a ./${ARCHIVE_FILE} ./${{ env.RELEASE_BIN }}.exe 71 | 72 | python -c "import hashlib; import pathlib; print(hashlib.sha256(pathlib.Path('${ARCHIVE_FILE}').read_bytes()).hexdigest())" > ${ARCHIVE_FILE}.sha256 73 | 74 | - name: Upload artifacts 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: Windows artifacts 78 | path: ./${{ env.RELEASE_DIR }}/* 79 | 80 | macos-binary: 81 | runs-on: macos-latest 82 | 83 | steps: 84 | - uses: actions/checkout@v4 85 | - uses: ./.github/actions/setup-release-env 86 | 87 | - name: Build 88 | run: | 89 | rustup target add ${{ env.MACOS_X86_TARGET }} 90 | 91 | cargo build --release 92 | cargo build --release --target ${{ env.MACOS_X86_TARGET }} 93 | 94 | - name: Create universal macOS binary 95 | run: | 96 | strip ./target/release/${{ env.RELEASE_BIN }} 97 | strip ./target/${{ env.MACOS_X86_TARGET }}/release/${{ env.RELEASE_BIN }} 98 | 99 | lipo -create \ 100 | ./target/${{ env.MACOS_X86_TARGET }}/release/${{ env.RELEASE_BIN }} \ 101 | ./target/release/${{ env.RELEASE_BIN }} \ 102 | -output ./${{ env.RELEASE_BIN }} 103 | 104 | - name: Create zipfile 105 | run: | 106 | ARCHIVE_FILE=${{ env.RELEASE_DIR }}/${{ env.ARCHIVE_PREFIX }}-universal-macOS.zip 107 | zip -9 ./${ARCHIVE_FILE} ./${{ env.RELEASE_BIN }} 108 | 109 | python -c "import hashlib; import pathlib; print(hashlib.sha256(pathlib.Path('${ARCHIVE_FILE}').read_bytes()).hexdigest())" > ${ARCHIVE_FILE}.sha256 110 | 111 | - name: Upload artifacts 112 | uses: actions/upload-artifact@v4 113 | with: 114 | name: macOS artifacts 115 | path: ./${{ env.RELEASE_DIR }}/* 116 | 117 | create-release: 118 | needs: [linux-binary, windows-binary, macos-binary] 119 | runs-on: ubuntu-latest 120 | permissions: 121 | contents: write 122 | 123 | steps: 124 | - name: Download artifacts 125 | uses: actions/download-artifact@v4 126 | 127 | - name: Display structure of downloaded files 128 | run: ls -R 129 | 130 | - name: Create a draft release 131 | uses: softprops/action-gh-release@v2 132 | with: 133 | tag_name: ${{ env.RELEASE_PKG_VERSION }} 134 | draft: true 135 | files: | 136 | Linux artifacts/* 137 | Windows artifacts/* 138 | macOS artifacts/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android-tzdata" 16 | version = "0.1.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 19 | 20 | [[package]] 21 | name = "android_system_properties" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 | dependencies = [ 26 | "libc", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.14" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.4" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" 64 | dependencies = [ 65 | "windows-sys 0.52.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.3" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 73 | dependencies = [ 74 | "anstyle", 75 | "windows-sys 0.52.0", 76 | ] 77 | 78 | [[package]] 79 | name = "anyhow" 80 | version = "1.0.93" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" 83 | 84 | [[package]] 85 | name = "autocfg" 86 | version = "1.3.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 89 | 90 | [[package]] 91 | name = "bitflags" 92 | version = "2.5.0" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 95 | 96 | [[package]] 97 | name = "bitstream-io" 98 | version = "2.4.2" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "415f8399438eb5e4b2f73ed3152a3448b98149dda642a957ee704e1daa5cf1d8" 101 | 102 | [[package]] 103 | name = "bitvec" 104 | version = "1.0.1" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" 107 | dependencies = [ 108 | "funty", 109 | "radium", 110 | "tap", 111 | "wyz", 112 | ] 113 | 114 | [[package]] 115 | name = "bitvec_helpers" 116 | version = "3.1.5" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "2e6539ed4bcd1be8442a26b154a1e363cbcb1410b9c275646d6f6ca532fd142f" 119 | dependencies = [ 120 | "bitstream-io", 121 | ] 122 | 123 | [[package]] 124 | name = "bumpalo" 125 | version = "3.16.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 128 | 129 | [[package]] 130 | name = "cc" 131 | version = "1.0.99" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" 134 | 135 | [[package]] 136 | name = "cfg-if" 137 | version = "1.0.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 140 | 141 | [[package]] 142 | name = "chrono" 143 | version = "0.4.38" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 146 | dependencies = [ 147 | "android-tzdata", 148 | "iana-time-zone", 149 | "js-sys", 150 | "num-traits", 151 | "wasm-bindgen", 152 | "windows-targets", 153 | ] 154 | 155 | [[package]] 156 | name = "clap" 157 | version = "4.5.21" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" 160 | dependencies = [ 161 | "clap_builder", 162 | "clap_derive", 163 | ] 164 | 165 | [[package]] 166 | name = "clap_builder" 167 | version = "4.5.21" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" 170 | dependencies = [ 171 | "anstream", 172 | "anstyle", 173 | "clap_lex", 174 | "strsim", 175 | "terminal_size", 176 | ] 177 | 178 | [[package]] 179 | name = "clap_derive" 180 | version = "4.5.18" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 183 | dependencies = [ 184 | "heck", 185 | "proc-macro2", 186 | "quote", 187 | "syn", 188 | ] 189 | 190 | [[package]] 191 | name = "clap_lex" 192 | version = "0.7.1" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" 195 | 196 | [[package]] 197 | name = "colorchoice" 198 | version = "1.0.1" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 201 | 202 | [[package]] 203 | name = "core-foundation-sys" 204 | version = "0.8.6" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 207 | 208 | [[package]] 209 | name = "crc" 210 | version = "3.2.1" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" 213 | dependencies = [ 214 | "crc-catalog", 215 | ] 216 | 217 | [[package]] 218 | name = "crc-catalog" 219 | version = "2.4.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 222 | 223 | [[package]] 224 | name = "darling" 225 | version = "0.20.10" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 228 | dependencies = [ 229 | "darling_core", 230 | "darling_macro", 231 | ] 232 | 233 | [[package]] 234 | name = "darling_core" 235 | version = "0.20.10" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 238 | dependencies = [ 239 | "fnv", 240 | "ident_case", 241 | "proc-macro2", 242 | "quote", 243 | "strsim", 244 | "syn", 245 | ] 246 | 247 | [[package]] 248 | name = "darling_macro" 249 | version = "0.20.10" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 252 | dependencies = [ 253 | "darling_core", 254 | "quote", 255 | "syn", 256 | ] 257 | 258 | [[package]] 259 | name = "deranged" 260 | version = "0.3.11" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 263 | dependencies = [ 264 | "powerfmt", 265 | ] 266 | 267 | [[package]] 268 | name = "derive_builder" 269 | version = "0.20.2" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" 272 | dependencies = [ 273 | "derive_builder_macro", 274 | ] 275 | 276 | [[package]] 277 | name = "derive_builder_core" 278 | version = "0.20.2" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" 281 | dependencies = [ 282 | "darling", 283 | "proc-macro2", 284 | "quote", 285 | "syn", 286 | ] 287 | 288 | [[package]] 289 | name = "derive_builder_macro" 290 | version = "0.20.2" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" 293 | dependencies = [ 294 | "derive_builder_core", 295 | "syn", 296 | ] 297 | 298 | [[package]] 299 | name = "dolby_vision" 300 | version = "3.3.1" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "3074f03f3790777f1fcb29cd0ba1edc23fecbcb99121af5b3bacadd7f90147e0" 303 | dependencies = [ 304 | "anyhow", 305 | "bitvec", 306 | "bitvec_helpers", 307 | "crc", 308 | "tinyvec", 309 | ] 310 | 311 | [[package]] 312 | name = "dovi_meta" 313 | version = "0.3.1" 314 | dependencies = [ 315 | "anyhow", 316 | "chrono", 317 | "clap", 318 | "dolby_vision", 319 | "itertools", 320 | "num-derive", 321 | "num-traits", 322 | "quick-xml", 323 | "serde", 324 | "serde-aux", 325 | "uuid", 326 | "vergen-gitcl", 327 | "vtc", 328 | ] 329 | 330 | [[package]] 331 | name = "either" 332 | version = "1.12.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" 335 | 336 | [[package]] 337 | name = "errno" 338 | version = "0.3.9" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 341 | dependencies = [ 342 | "libc", 343 | "windows-sys 0.52.0", 344 | ] 345 | 346 | [[package]] 347 | name = "fnv" 348 | version = "1.0.7" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 351 | 352 | [[package]] 353 | name = "funty" 354 | version = "2.0.0" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" 357 | 358 | [[package]] 359 | name = "getrandom" 360 | version = "0.2.15" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 363 | dependencies = [ 364 | "cfg-if", 365 | "libc", 366 | "wasi", 367 | ] 368 | 369 | [[package]] 370 | name = "heck" 371 | version = "0.5.0" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 374 | 375 | [[package]] 376 | name = "iana-time-zone" 377 | version = "0.1.60" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 380 | dependencies = [ 381 | "android_system_properties", 382 | "core-foundation-sys", 383 | "iana-time-zone-haiku", 384 | "js-sys", 385 | "wasm-bindgen", 386 | "windows-core", 387 | ] 388 | 389 | [[package]] 390 | name = "iana-time-zone-haiku" 391 | version = "0.1.2" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 394 | dependencies = [ 395 | "cc", 396 | ] 397 | 398 | [[package]] 399 | name = "ident_case" 400 | version = "1.0.1" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 403 | 404 | [[package]] 405 | name = "is_terminal_polyfill" 406 | version = "1.70.0" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 409 | 410 | [[package]] 411 | name = "itertools" 412 | version = "0.13.0" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 415 | dependencies = [ 416 | "either", 417 | ] 418 | 419 | [[package]] 420 | name = "itoa" 421 | version = "1.0.11" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 424 | 425 | [[package]] 426 | name = "js-sys" 427 | version = "0.3.69" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 430 | dependencies = [ 431 | "wasm-bindgen", 432 | ] 433 | 434 | [[package]] 435 | name = "lazy_static" 436 | version = "1.4.0" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 439 | 440 | [[package]] 441 | name = "libc" 442 | version = "0.2.155" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 445 | 446 | [[package]] 447 | name = "linux-raw-sys" 448 | version = "0.4.14" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 451 | 452 | [[package]] 453 | name = "log" 454 | version = "0.4.21" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 457 | 458 | [[package]] 459 | name = "memchr" 460 | version = "2.7.4" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 463 | 464 | [[package]] 465 | name = "num" 466 | version = "0.4.3" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" 469 | dependencies = [ 470 | "num-bigint", 471 | "num-complex", 472 | "num-integer", 473 | "num-iter", 474 | "num-rational", 475 | "num-traits", 476 | ] 477 | 478 | [[package]] 479 | name = "num-bigint" 480 | version = "0.4.5" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" 483 | dependencies = [ 484 | "num-integer", 485 | "num-traits", 486 | ] 487 | 488 | [[package]] 489 | name = "num-complex" 490 | version = "0.4.6" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" 493 | dependencies = [ 494 | "num-traits", 495 | ] 496 | 497 | [[package]] 498 | name = "num-conv" 499 | version = "0.1.0" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 502 | 503 | [[package]] 504 | name = "num-derive" 505 | version = "0.4.2" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 508 | dependencies = [ 509 | "proc-macro2", 510 | "quote", 511 | "syn", 512 | ] 513 | 514 | [[package]] 515 | name = "num-integer" 516 | version = "0.1.46" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 519 | dependencies = [ 520 | "num-traits", 521 | ] 522 | 523 | [[package]] 524 | name = "num-iter" 525 | version = "0.1.45" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 528 | dependencies = [ 529 | "autocfg", 530 | "num-integer", 531 | "num-traits", 532 | ] 533 | 534 | [[package]] 535 | name = "num-rational" 536 | version = "0.4.2" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 539 | dependencies = [ 540 | "num-bigint", 541 | "num-integer", 542 | "num-traits", 543 | ] 544 | 545 | [[package]] 546 | name = "num-traits" 547 | version = "0.2.19" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 550 | dependencies = [ 551 | "autocfg", 552 | ] 553 | 554 | [[package]] 555 | name = "num_threads" 556 | version = "0.1.7" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 559 | dependencies = [ 560 | "libc", 561 | ] 562 | 563 | [[package]] 564 | name = "once_cell" 565 | version = "1.19.0" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 568 | 569 | [[package]] 570 | name = "powerfmt" 571 | version = "0.2.0" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 574 | 575 | [[package]] 576 | name = "proc-macro2" 577 | version = "1.0.85" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" 580 | dependencies = [ 581 | "unicode-ident", 582 | ] 583 | 584 | [[package]] 585 | name = "quick-xml" 586 | version = "0.37.1" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03" 589 | dependencies = [ 590 | "memchr", 591 | "serde", 592 | ] 593 | 594 | [[package]] 595 | name = "quote" 596 | version = "1.0.36" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 599 | dependencies = [ 600 | "proc-macro2", 601 | ] 602 | 603 | [[package]] 604 | name = "radium" 605 | version = "0.7.0" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" 608 | 609 | [[package]] 610 | name = "regex" 611 | version = "1.10.5" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" 614 | dependencies = [ 615 | "aho-corasick", 616 | "memchr", 617 | "regex-automata", 618 | "regex-syntax", 619 | ] 620 | 621 | [[package]] 622 | name = "regex-automata" 623 | version = "0.4.7" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 626 | dependencies = [ 627 | "aho-corasick", 628 | "memchr", 629 | "regex-syntax", 630 | ] 631 | 632 | [[package]] 633 | name = "regex-syntax" 634 | version = "0.8.4" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 637 | 638 | [[package]] 639 | name = "rustix" 640 | version = "0.38.34" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 643 | dependencies = [ 644 | "bitflags", 645 | "errno", 646 | "libc", 647 | "linux-raw-sys", 648 | "windows-sys 0.52.0", 649 | ] 650 | 651 | [[package]] 652 | name = "rustversion" 653 | version = "1.0.17" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 656 | 657 | [[package]] 658 | name = "ryu" 659 | version = "1.0.18" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 662 | 663 | [[package]] 664 | name = "serde" 665 | version = "1.0.215" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" 668 | dependencies = [ 669 | "serde_derive", 670 | ] 671 | 672 | [[package]] 673 | name = "serde-aux" 674 | version = "4.5.0" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "0d2e8bfba469d06512e11e3311d4d051a4a387a5b42d010404fecf3200321c95" 677 | dependencies = [ 678 | "chrono", 679 | "serde", 680 | "serde_json", 681 | ] 682 | 683 | [[package]] 684 | name = "serde_derive" 685 | version = "1.0.215" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" 688 | dependencies = [ 689 | "proc-macro2", 690 | "quote", 691 | "syn", 692 | ] 693 | 694 | [[package]] 695 | name = "serde_json" 696 | version = "1.0.117" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" 699 | dependencies = [ 700 | "itoa", 701 | "ryu", 702 | "serde", 703 | ] 704 | 705 | [[package]] 706 | name = "strsim" 707 | version = "0.11.1" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 710 | 711 | [[package]] 712 | name = "syn" 713 | version = "2.0.87" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" 716 | dependencies = [ 717 | "proc-macro2", 718 | "quote", 719 | "unicode-ident", 720 | ] 721 | 722 | [[package]] 723 | name = "tap" 724 | version = "1.0.1" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 727 | 728 | [[package]] 729 | name = "terminal_size" 730 | version = "0.4.0" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" 733 | dependencies = [ 734 | "rustix", 735 | "windows-sys 0.59.0", 736 | ] 737 | 738 | [[package]] 739 | name = "time" 740 | version = "0.3.36" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 743 | dependencies = [ 744 | "deranged", 745 | "itoa", 746 | "libc", 747 | "num-conv", 748 | "num_threads", 749 | "powerfmt", 750 | "serde", 751 | "time-core", 752 | "time-macros", 753 | ] 754 | 755 | [[package]] 756 | name = "time-core" 757 | version = "0.1.2" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 760 | 761 | [[package]] 762 | name = "time-macros" 763 | version = "0.2.18" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 766 | dependencies = [ 767 | "num-conv", 768 | "time-core", 769 | ] 770 | 771 | [[package]] 772 | name = "tinyvec" 773 | version = "1.8.0" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 776 | 777 | [[package]] 778 | name = "unicode-ident" 779 | version = "1.0.12" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 782 | 783 | [[package]] 784 | name = "utf8parse" 785 | version = "0.2.2" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 788 | 789 | [[package]] 790 | name = "uuid" 791 | version = "1.11.0" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" 794 | dependencies = [ 795 | "getrandom", 796 | ] 797 | 798 | [[package]] 799 | name = "vergen" 800 | version = "9.0.1" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "349ed9e45296a581f455bc18039878f409992999bc1d5da12a6800eb18c8752f" 803 | dependencies = [ 804 | "anyhow", 805 | "derive_builder", 806 | "rustversion", 807 | "time", 808 | "vergen-lib", 809 | ] 810 | 811 | [[package]] 812 | name = "vergen-gitcl" 813 | version = "1.0.1" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "2a3a7f91caabecefc3c249fd864b11d4abe315c166fbdb568964421bccfd2b7a" 816 | dependencies = [ 817 | "anyhow", 818 | "derive_builder", 819 | "rustversion", 820 | "time", 821 | "vergen", 822 | "vergen-lib", 823 | ] 824 | 825 | [[package]] 826 | name = "vergen-lib" 827 | version = "0.1.4" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "229eaddb0050920816cf051e619affaf18caa3dd512de8de5839ccbc8e53abb0" 830 | dependencies = [ 831 | "anyhow", 832 | "derive_builder", 833 | "rustversion", 834 | ] 835 | 836 | [[package]] 837 | name = "vtc" 838 | version = "0.1.13" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "364d6272ee71123cd5dadd1a00df292e3fe8287d6488be47b5c66d419f2c5614" 841 | dependencies = [ 842 | "lazy_static", 843 | "num", 844 | "regex", 845 | ] 846 | 847 | [[package]] 848 | name = "wasi" 849 | version = "0.11.0+wasi-snapshot-preview1" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 852 | 853 | [[package]] 854 | name = "wasm-bindgen" 855 | version = "0.2.92" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 858 | dependencies = [ 859 | "cfg-if", 860 | "wasm-bindgen-macro", 861 | ] 862 | 863 | [[package]] 864 | name = "wasm-bindgen-backend" 865 | version = "0.2.92" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 868 | dependencies = [ 869 | "bumpalo", 870 | "log", 871 | "once_cell", 872 | "proc-macro2", 873 | "quote", 874 | "syn", 875 | "wasm-bindgen-shared", 876 | ] 877 | 878 | [[package]] 879 | name = "wasm-bindgen-macro" 880 | version = "0.2.92" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 883 | dependencies = [ 884 | "quote", 885 | "wasm-bindgen-macro-support", 886 | ] 887 | 888 | [[package]] 889 | name = "wasm-bindgen-macro-support" 890 | version = "0.2.92" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 893 | dependencies = [ 894 | "proc-macro2", 895 | "quote", 896 | "syn", 897 | "wasm-bindgen-backend", 898 | "wasm-bindgen-shared", 899 | ] 900 | 901 | [[package]] 902 | name = "wasm-bindgen-shared" 903 | version = "0.2.92" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 906 | 907 | [[package]] 908 | name = "windows-core" 909 | version = "0.52.0" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 912 | dependencies = [ 913 | "windows-targets", 914 | ] 915 | 916 | [[package]] 917 | name = "windows-sys" 918 | version = "0.52.0" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 921 | dependencies = [ 922 | "windows-targets", 923 | ] 924 | 925 | [[package]] 926 | name = "windows-sys" 927 | version = "0.59.0" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 930 | dependencies = [ 931 | "windows-targets", 932 | ] 933 | 934 | [[package]] 935 | name = "windows-targets" 936 | version = "0.52.6" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 939 | dependencies = [ 940 | "windows_aarch64_gnullvm", 941 | "windows_aarch64_msvc", 942 | "windows_i686_gnu", 943 | "windows_i686_gnullvm", 944 | "windows_i686_msvc", 945 | "windows_x86_64_gnu", 946 | "windows_x86_64_gnullvm", 947 | "windows_x86_64_msvc", 948 | ] 949 | 950 | [[package]] 951 | name = "windows_aarch64_gnullvm" 952 | version = "0.52.6" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 955 | 956 | [[package]] 957 | name = "windows_aarch64_msvc" 958 | version = "0.52.6" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 961 | 962 | [[package]] 963 | name = "windows_i686_gnu" 964 | version = "0.52.6" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 967 | 968 | [[package]] 969 | name = "windows_i686_gnullvm" 970 | version = "0.52.6" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 973 | 974 | [[package]] 975 | name = "windows_i686_msvc" 976 | version = "0.52.6" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 979 | 980 | [[package]] 981 | name = "windows_x86_64_gnu" 982 | version = "0.52.6" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 985 | 986 | [[package]] 987 | name = "windows_x86_64_gnullvm" 988 | version = "0.52.6" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 991 | 992 | [[package]] 993 | name = "windows_x86_64_msvc" 994 | version = "0.52.6" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 997 | 998 | [[package]] 999 | name = "wyz" 1000 | version = "0.5.1" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" 1003 | dependencies = [ 1004 | "tap", 1005 | ] 1006 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dovi_meta" 3 | version = "0.3.1" 4 | edition = "2021" 5 | authors = ["Rainbaby"] 6 | rust-version = "1.79.0" 7 | license = "MIT" 8 | build = "build.rs" 9 | 10 | [dependencies] 11 | num-traits = "0.2.19" 12 | num-derive = "0.4.2" 13 | 14 | uuid = { version = "1.11.0", features = ["v4"] } 15 | 16 | dolby_vision = "3.3.1" 17 | # TODO: Use timecode as unique id, as an option. 18 | vtc = "0.1.13" 19 | chrono = "0.4.38" 20 | serde = { version = "1.0.215", features = ["derive"] } 21 | serde-aux = "4.5.0" 22 | quick-xml = { version = "0.37.1", features = ["serialize"] } 23 | 24 | clap = { version = "4.5.21", features = ["derive", "wrap_help"] } 25 | anyhow = "1.0.93" 26 | itertools = "0.13.0" 27 | 28 | [build-dependencies] 29 | anyhow = "1.0.93" 30 | vergen-gitcl = { version = "1.0.0", default-features = false, features = ["build"] } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Rainbaby 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **dovi_meta** 2 | [![CI](https://github.com/saindriches/dovi_meta/workflows/CI/badge.svg)](https://github.com/saindriches/dovi_meta/actions/workflows/ci.yml) 3 | [![Artifacts](https://github.com/saindriches/dovi_meta/workflows/Artifacts/badge.svg)](https://github.com/saindriches/dovi_meta/actions/workflows/release.yml) 4 | [![Github all releases](https://img.shields.io/github/downloads/saindriches/dovi_meta/total.svg)](https://GitHub.com/saindriches/dovi_meta/releases/) 5 | 6 | 7 | **`dovi_meta`** is a CLI tool for creating Dolby Vision XML metadata from an encoded deliverable with binary metadata. 8 | 9 | ## **Building** 10 | ### **Toolchain** 11 | 12 | The minimum Rust version to build **`dovi_meta`** is 1.79.0. 13 | 14 | ### **Release binary** 15 | To build release binary in `target/release/dovi_meta` run: 16 | ```console 17 | cargo build --release 18 | ``` 19 | 20 | ## Usage 21 | ```properties 22 | dovi_meta [OPTIONS] 23 | ``` 24 | **To get more detailed options for a subcommand** 25 | ```properties 26 | dovi_meta --help 27 | ``` 28 | 29 | ## All options 30 | - `--help`, `--version` 31 | ## All subcommands 32 | Currently, the available subcommand is **`convert`** and **`edl`**. 33 | 34 | **More information and detailed examples for the subcommands below.** 35 | 36 | 37 | * ### **convert** 38 | Convert a binary RPU to XML Metadata (DolbyLabsMDF). 39 | * Currently, it should support RPU with any Dolby Vision profile using **PQ** as EOTF. 40 | * Supported XML Version: **CM v2.9** (v2.0.5), **CM v4.0** (v4.0.2 and v5.1.0) 41 | - The output version is determined by input automatically. 42 | 43 | **Arguments** 44 | * `INPUT` Set the input RPU file to use 45 | - No limitation for RPU file extension. 46 | * `OUTPUT` Set the output XML file location 47 | - When `OUTPUT` is not set, the output file is `metadata.xml` at current path. 48 | 49 | **Options** 50 | * `-s`, `--size` Set the canvas size. Use `x` as delimiter 51 | - Default value is `3840x2160` 52 | * `-r`, `--rate` Set the frame rate. Format: integer `NUM` or `NUM/DENOM` 53 | - Default value is `24000/1001` 54 | * `-t`, `--skip` Set the number of frames to be skipped from start 55 | - Default value is `0` 56 | * `-n`, `--count` Set the number of frames to be parsed explicitly 57 | * `-o`, `--offset` Set the number of frames to be added to the index 58 | - Default value is `0` 59 | 60 | **Flags** 61 | * `-6`, `--use-level6` Use MaxCLL and MaxFALL from RPU, if possible 62 | - It's not a default behavior, as ST.2086 metadata is not required for a Dolby Vision deliverable. 63 | * `-d`, `--drop-per-frame` Drop per-frame metadata in shots 64 | * `-k`, `--keep-offset` Keep the offset of frames when `--skip` is set 65 | 66 | **Example to get metadata for RPU from a 29.97 fps HD video, dropping first 24 frames**: 67 | 68 | ```console 69 | dovi_meta convert RPU.bin metadata.xml --skip 24 --rate 30000/1001 --size 1920x1080 70 | ``` 71 | The default color encoding is **BT.2020 PQ 16-bit RGB Full Range**. 72 | 73 | The default color space of mastering display and target displays (except the anchor target) is **P3 D65** for CM v2.9 XML, also for CM v4.0 XML when it can't be determined by input. 74 | 75 | * ### **edl** 76 | Convert a binary RPU to EDL (Edit Decision List). 77 | * Currently, the per-frame metadata in RPU is not parsed to transition. 78 | 79 | **Arguments** 80 | * `INPUT` Set the input RPU file to use 81 | - No limitation for RPU file extension. 82 | * `OUTPUT` Set the output XML file location 83 | - When `OUTPUT` is not set, the output file is `metadata.edl` at current path. 84 | * `CLIP_NAME` Set the clip name in EDL 85 | - If there are too many cuts to be saved in a single file, 86 | multiple files will be saved with a suffix added to the file name. 87 | 88 | **Options** 89 | * `-r`, `--rate` Set the frame rate. Format: integer `NUM` or `NUM/DENOM` 90 | - Default value is `24000/1001` 91 | * `-s`, `--start-timecode` Set the starting timecode in timeline. Format: `HH:MM:SS:FF` or integer `FRAMES` offset 92 | - Default value is `01:00:00:00` 93 | * `-t`, `--skip` Set the number of frames to be skipped from start 94 | - Default value is `0` 95 | * `-n`, `--count` Set the number of frames to be parsed explicitly 96 | 97 | **Flags** 98 | * `-f`, `--force` Force output even if per-frame RPU is detected 99 | 100 | ## **Notes** 101 | The current build only support RPU as input. To extract RPU from an HEVC file, see [dovi_tool](https://github.com/quietvoid/dovi_tool) for more info. 102 | 103 | 104 | Build artifacts can be found in the GitHub Actions. 105 | More features may or may not be added in the future. 106 | Please report an issue if you have any question. 107 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use vergen_gitcl::{Emitter, GitclBuilder}; 3 | 4 | fn main() -> Result<()> { 5 | let gitcl = GitclBuilder::default() 6 | .describe(true, true, Some("[0-9]*")) 7 | .build()?; 8 | 9 | let gitcl_res = Emitter::default() 10 | .idempotent() 11 | .fail_on_error() 12 | .add_instructions(&gitcl) 13 | .and_then(|emitter| emitter.emit()); 14 | 15 | if let Err(e) = gitcl_res { 16 | eprintln!("error occured while generating instructions: {e:?}"); 17 | Emitter::default().idempotent().fail_on_error().emit()?; 18 | } 19 | 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" -------------------------------------------------------------------------------- /src/commands/convert.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, ValueHint}; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Args, Debug)] 5 | pub struct ConvertArgs { 6 | #[clap( 7 | help = "Set the input RPU file to use", 8 | value_hint = ValueHint::FilePath 9 | )] 10 | pub input: Option, 11 | 12 | #[clap( 13 | help = "Set the output XML file location", 14 | value_hint = ValueHint::FilePath 15 | )] 16 | pub output: Option, 17 | 18 | #[clap( 19 | short = 's', 20 | long, 21 | default_value = "3840x2160", 22 | value_delimiter = 'x', 23 | help = "Set the canvas size" 24 | )] 25 | pub size: Vec, 26 | 27 | #[clap( 28 | short = 'r', 29 | long, 30 | default_value = "24000/1001", 31 | value_delimiter = '/', 32 | help = "Set the frame rate. Format: integer NUM or NUM/DENOM" 33 | )] 34 | pub rate: Vec, 35 | 36 | #[clap( 37 | short = '6', 38 | long, 39 | help = "Use MaxCLL and MaxFALL from RPU, if possible" 40 | )] 41 | pub use_level6: bool, 42 | 43 | #[clap(short = 'd', long, help = "Drop per-frame metadata in shots")] 44 | pub drop_per_frame: bool, 45 | 46 | #[clap( 47 | short = 't', 48 | long, 49 | default_value = "0", 50 | help = "Set the number of frames to be skipped from start" 51 | )] 52 | pub skip: usize, 53 | 54 | #[clap( 55 | short = 'n', 56 | long, 57 | help = "Set the number of frames to be parsed explicitly" 58 | )] 59 | pub count: Option, 60 | 61 | #[clap( 62 | short = 'k', 63 | long, 64 | requires = "skip", 65 | help = "Keep the offset of frames when --skip is set" 66 | )] 67 | pub keep_offset: bool, 68 | 69 | #[clap( 70 | short = 'o', 71 | long, 72 | default_value = "0", 73 | help = "Set the number of frames to be added to the index" 74 | )] 75 | pub offset: usize, 76 | } 77 | -------------------------------------------------------------------------------- /src/commands/edl.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, ValueHint}; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Args, Debug)] 5 | pub struct EdlArgs { 6 | #[clap( 7 | help = "Set the input RPU file to use", 8 | value_hint = ValueHint::FilePath 9 | )] 10 | pub input: Option, 11 | 12 | #[clap( 13 | help = "Set the output EDL file location. See --help for more info", 14 | long_help = "Set the output EDL file location.\n \ 15 | If there are too many cuts to be saved in a single file,\n \ 16 | multiple files will be saved with a suffix added to the file name.", 17 | value_hint = ValueHint::FilePath 18 | )] 19 | pub output: Option, 20 | 21 | #[clap( 22 | help = "Set the clip name in EDL", 23 | required = false, 24 | value_hint = ValueHint::FilePath 25 | )] 26 | pub clip_name: String, 27 | 28 | #[clap( 29 | short = 'f', 30 | long, 31 | help = "Force output even if per-frame RPU is detected" 32 | )] 33 | pub force: bool, 34 | 35 | #[clap( 36 | short = 'r', 37 | long, 38 | default_value = "24000/1001", 39 | value_delimiter = '/', 40 | help = "Set the frame rate. Format: integer NUM or NUM/DENOM" 41 | )] 42 | pub rate: Vec, 43 | 44 | #[clap( 45 | short = 's', 46 | long, 47 | default_value = "01:00:00:00", 48 | help = "Set the starting timecode in timeline. Format: HH:MM:SS:FF or integer FRAMES offset" 49 | )] 50 | pub start_timecode: String, 51 | 52 | #[clap( 53 | short = 't', 54 | long, 55 | default_value = "0", 56 | help = "Set the number of frames to be skipped from start" 57 | )] 58 | pub skip: usize, 59 | 60 | #[clap( 61 | short = 'n', 62 | long, 63 | help = "Set the number of frames to be parsed explicitly" 64 | )] 65 | pub count: Option, 66 | } 67 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod convert; 2 | pub mod edl; 3 | 4 | // use crate::commands::analyze::AnalyzeArgs; 5 | use crate::commands::convert::ConvertArgs; 6 | use crate::commands::edl::EdlArgs; 7 | use clap::Parser; 8 | 9 | #[derive(Parser, Debug)] 10 | pub enum Command { 11 | #[clap( 12 | about = "Convert a binary RPU to XML Metadata (DolbyLabsMDF)", 13 | arg_required_else_help(true) 14 | )] 15 | Convert(ConvertArgs), 16 | 17 | #[clap( 18 | about = "Convert a binary RPU to EDL (Edit Decision List)", 19 | arg_required_else_help(true) 20 | )] 21 | Edl(EdlArgs), 22 | } 23 | -------------------------------------------------------------------------------- /src/functions/convert.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs::File; 3 | use std::io::{BufWriter, Write}; 4 | 5 | use anyhow::{bail, ensure, Result}; 6 | use dolby_vision::rpu::utils::parse_rpu_file; 7 | use quick_xml::events::Event; 8 | use quick_xml::se::Serializer; 9 | use quick_xml::{Reader, Writer}; 10 | use serde::Serialize; 11 | 12 | use crate::cmv40::{Characteristics, EditRate, Output, Shot, Track}; 13 | use crate::commands::convert::ConvertArgs; 14 | use crate::metadata::levels::Level11; 15 | use crate::metadata::levels::Level5; 16 | use crate::MDFType::{CMV29, CMV40}; 17 | use crate::{cmv40, display, IntoCMV29, Level254, Level6, XML_PREFIX}; 18 | 19 | #[derive(Debug, Default)] 20 | pub struct Converter { 21 | frame_index: usize, 22 | // scene_count: usize, 23 | invalid_frame_count: usize, 24 | first_valid_frame_index: Option, 25 | shots: Option>, 26 | last_shot: Shot, 27 | track: Track, 28 | level5: Option, 29 | level11: Option, 30 | level254: Option, 31 | } 32 | 33 | impl Converter { 34 | pub fn convert(args: ConvertArgs) -> Result<()> { 35 | let input = match args.input { 36 | Some(input) => input, 37 | None => bail!("No input file provided."), 38 | }; 39 | 40 | ensure!(args.count != Some(0), "Invalid specified frame count."); 41 | ensure!( 42 | args.skip < args.count.unwrap_or(usize::MAX), 43 | "Invalid skip count." 44 | ); 45 | 46 | ensure!( 47 | args.size.len() == 2, 48 | "Invalid canvas size. Use 'x' as delimiter, like 3840x2160" 49 | ); 50 | ensure!( 51 | args.size[0] != 0 && args.size[1] != 0, 52 | "Invalid canvas size." 53 | ); 54 | 55 | ensure!( 56 | args.rate.len() <= 2, 57 | "Invalid frame rate. Use '/' as delimiter if needed, like 24 or 24000/1001" 58 | ); 59 | 60 | let canvas = Converter::parse_canvas_ar(args.size)?; 61 | 62 | println!("Parsing RPU file..."); 63 | 64 | let rpus = parse_rpu_file(input)?; 65 | 66 | let mut count = if let Some(count) = args.count { 67 | if count + args.skip > rpus.len() { 68 | println!("Specified frame count exceeds the end."); 69 | rpus.len() 70 | } else { 71 | count 72 | } 73 | } else { 74 | rpus.len() 75 | }; 76 | 77 | let mut converter = Converter::default(); 78 | 79 | let edit_rate = EditRate::from(args.rate); 80 | edit_rate.validate()?; 81 | 82 | println!("Converting RPU file..."); 83 | 84 | let mut targets_map = HashMap::new(); 85 | 86 | // Parse shot-based and frame-based metadata 87 | for rpu in rpus { 88 | if count > 0 { 89 | if let Some(ref vdr) = rpu.vdr_dm_data { 90 | if converter.frame_index >= args.skip { 91 | let frame_index = converter.frame_index - args.skip + args.offset; 92 | // TODO: Use real offset if first valid frame index is not 0? 93 | 94 | if converter.first_valid_frame_index.is_none() 95 | || vdr.scene_refresh_flag == 1 96 | { 97 | match &mut converter.shots { 98 | // Initialize 99 | None => { 100 | converter.shots = Some(Vec::new()); 101 | } 102 | Some(shots) => { 103 | shots.push(converter.last_shot.clone()); 104 | } 105 | } 106 | 107 | converter.last_shot = Shot::with_canvas(vdr, canvas); 108 | converter.last_shot.update_record(Some(frame_index), None); 109 | 110 | // FIXME: Assume input rpu file is valid, 111 | // so only use the first valid frame to get global information we need 112 | if converter.first_valid_frame_index.is_none() { 113 | if converter.invalid_frame_count > 0 { 114 | println!( 115 | "Skipped {} invalid frame(s) from start.", 116 | converter.invalid_frame_count 117 | ); 118 | converter.invalid_frame_count = 0; 119 | } 120 | if args.keep_offset { 121 | converter.last_shot.update_record(None, Some(args.skip)); 122 | converter.frame_index += args.skip; 123 | } 124 | converter.first_valid_frame_index = Some(frame_index); 125 | converter.track = Track::with_single_vdr(vdr); 126 | 127 | if !args.use_level6 { 128 | converter.track.level6 = Some(Level6::default()); 129 | } 130 | 131 | converter 132 | .level254 133 | .clone_from(&converter.track.plugin_node.level254); 134 | 135 | converter.track.edit_rate = if converter.level254.is_none() { 136 | CMV29(edit_rate) 137 | } else { 138 | CMV40(edit_rate) 139 | }; 140 | }; 141 | } else { 142 | converter.last_shot.update_record(None, None); 143 | if !args.drop_per_frame { 144 | converter 145 | .last_shot 146 | .append_metadata(&Shot::with_canvas(vdr, canvas)); 147 | } 148 | } 149 | 150 | if let Some(d) = display::Characteristics::get_targets(vdr) { 151 | d.iter().for_each(|c| { 152 | let target = Characteristics::from(c.clone()); 153 | targets_map.entry(target.id).or_insert(target); 154 | }) 155 | } 156 | 157 | count -= 1; 158 | } 159 | 160 | converter.frame_index += 1; 161 | } else { 162 | // Should not happen 163 | if converter.first_valid_frame_index.is_some() { 164 | // Invalid RPU in the middle of sequence, use last valid frame 165 | converter.frame_index += 1; 166 | converter.last_shot.update_record(None, None); 167 | if let Some(ref mut frames) = converter.last_shot.frames { 168 | if let Some(frame) = frames.pop() { 169 | frames.push(frame.clone()); 170 | frames.push(frame); 171 | } 172 | } 173 | 174 | count -= 1; 175 | } 176 | 177 | converter.invalid_frame_count += 1; 178 | } 179 | } 180 | } 181 | 182 | if converter.invalid_frame_count > 0 { 183 | println!( 184 | "Skipped {} invalid frame(s) in the middle, replaced with previous metadata.", 185 | converter.invalid_frame_count 186 | ); 187 | } 188 | 189 | // Push remained shot 190 | if converter.shots.is_none() { 191 | converter.shots = Some(Vec::new()); 192 | } 193 | 194 | if let Some(ref mut shots) = converter.shots { 195 | shots.push(converter.last_shot.clone()); 196 | 197 | let mut targets = targets_map.values().cloned().collect::>(); 198 | if !targets.is_empty() { 199 | targets.sort_by_key(|c| c.id); 200 | converter.track.plugin_node.dv_global_data.target_displays = Some(targets); 201 | } 202 | 203 | let mut level5_map = HashMap::new(); 204 | let mut level11_map = HashMap::new(); 205 | 206 | shots.iter().for_each(|shot| { 207 | let mut shot_level_duration = shot.record.duration; 208 | 209 | if let Some(ref frames) = shot.frames { 210 | shot_level_duration -= frames.len(); 211 | 212 | frames.iter().for_each(|frame| { 213 | *level5_map 214 | .entry(&frame.plugin_node.dv_dynamic_data.level5) 215 | .or_insert(0) += 1_usize; 216 | 217 | *level11_map.entry(&frame.plugin_node.level11).or_insert(0) += 1_usize; 218 | }); 219 | } 220 | 221 | *level5_map 222 | .entry(&shot.plugin_node.dv_dynamic_data.level5) 223 | .or_insert(0) += shot_level_duration; 224 | 225 | *level11_map.entry(&shot.plugin_node.level11).or_insert(0) += shot_level_duration; 226 | }); 227 | 228 | // converter.level5 = Some(Self::get_global_ar(level5_map, canvas)); 229 | converter.level5 = 230 | Self::get_common(level5_map).or_else(|| Some(Level5::with_canvas(None, canvas))); 231 | 232 | // Choose the most common level11 as track-level metadata, 233 | converter.level11 = Self::get_common(level11_map); 234 | 235 | // and remove them in shot-level. 236 | shots.iter_mut().for_each(|shot| { 237 | let shot_level5 = shot.plugin_node.dv_dynamic_data.level5.clone(); 238 | if shot_level5 == converter.level5 { 239 | shot.plugin_node.dv_dynamic_data.level5 = None; 240 | }; 241 | 242 | shot.plugin_node.level11 = None; 243 | 244 | // Level 5 can not exist in per-frame metadata anyway, 245 | // but it's not our responsibility to validate it here. 246 | // TODO: test case 247 | if let Some(ref mut frames) = shot.frames { 248 | frames.iter_mut().for_each(|frame| { 249 | let plugin_node = &mut frame.plugin_node; 250 | let dv_dynamic_data = &mut plugin_node.dv_dynamic_data; 251 | 252 | let frame_level5 = dv_dynamic_data.level5.clone(); 253 | if frame_level5 == converter.level5 || frame_level5 == shot_level5 { 254 | dv_dynamic_data.level5 = None; 255 | } 256 | 257 | plugin_node.level11 = None; 258 | }) 259 | } 260 | }); 261 | } 262 | 263 | converter.track.shots = converter.shots; 264 | converter.track.plugin_node.level11 = converter.level11; 265 | 266 | let output = Output::with_level5(converter.track, converter.level5); 267 | 268 | let md = cmv40::DolbyLabsMDF::with_single_output(output)?; 269 | 270 | let mut serializer_buffer = String::new(); 271 | let ser = Serializer::new(&mut serializer_buffer); 272 | 273 | if converter.level254.is_none() { 274 | println!("CM v2.9 RPU found, saving as v2.0.5 XML..."); 275 | md.into_cmv29().serialize(ser)?; 276 | } else { 277 | println!("CM v4.0 RPU found, saving as v{} XML...", md.version); 278 | md.serialize(ser)?; 279 | } 280 | 281 | let output = if let Some(output) = args.output { 282 | output 283 | } else { 284 | println!("No output file provided, writing to metadata.xml at current path..."); 285 | "./metadata.xml".into() 286 | }; 287 | 288 | let mut output_buffer = BufWriter::new(File::create(output)?); 289 | write!( 290 | output_buffer, 291 | "{}{}", 292 | XML_PREFIX, 293 | Self::prettify_xml(serializer_buffer) 294 | )?; 295 | 296 | Ok(()) 297 | } 298 | 299 | /// None: Standard UHD 300 | fn parse_canvas_ar(vec: Vec) -> Result<(usize, usize)> { 301 | ensure!( 302 | vec.len() == 2, 303 | "Invalid canvas size. Use 'x' as delimiter, like 3840x2160" 304 | ); 305 | ensure!(vec[0] != 0 && vec[1] != 0, "Invalid canvas size."); 306 | Ok((vec[0], vec[1])) 307 | } 308 | 309 | fn get_common(map: HashMap<&Option, V>) -> Option 310 | where 311 | K: Clone, 312 | V: Copy + Ord, 313 | { 314 | map.into_iter() 315 | .filter(|(value, _)| value.is_some()) 316 | .max_by_key(|&(_, count)| count) 317 | .and_then(|(value, _)| value.clone()) 318 | } 319 | 320 | // https://gist.github.com/lwilli/14fb3178bd9adac3a64edfbc11f42e0d/forks 321 | fn prettify_xml(xml: String) -> String { 322 | let mut buf = Vec::new(); 323 | 324 | let mut reader = Reader::from_str(&xml); 325 | reader.config_mut().trim_text(true); 326 | 327 | let mut writer = Writer::new_with_indent(Vec::new(), b' ', 2); 328 | 329 | loop { 330 | let ev = reader.read_event_into(&mut buf); 331 | 332 | match ev { 333 | Ok(Event::Eof) => break, 334 | Ok(event) => writer.write_event(event), 335 | Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e), 336 | } 337 | .expect("Failed to parse XML"); 338 | 339 | buf.clear(); 340 | } 341 | 342 | let result = std::str::from_utf8(&writer.into_inner()) 343 | .expect("Failed to convert a slice of bytes to a string slice") 344 | .to_string(); 345 | 346 | result 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/functions/edl.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::fs::File; 3 | use std::io::{stdin, BufWriter, Cursor, Write}; 4 | use std::path::PathBuf; 5 | 6 | use crate::commands::edl::EdlArgs; 7 | use anyhow::{bail, ensure, Result}; 8 | use dolby_vision::rpu::utils::parse_rpu_file; 9 | 10 | use crate::cmv40::EditRate; 11 | use vtc::{Framerate, Ntsc, Timecode}; 12 | 13 | #[derive(Debug, Default)] 14 | pub struct EdlConverter { 15 | frame_index: usize, 16 | shots: Vec, 17 | } 18 | 19 | impl EdlConverter { 20 | pub fn convert(args: EdlArgs) -> Result<()> { 21 | let input = match args.input { 22 | Some(input) => input, 23 | None => bail!("No input file provided."), 24 | }; 25 | 26 | ensure!(args.count != Some(0), "Invalid specified frame count."); 27 | 28 | ensure!( 29 | args.rate.len() <= 2, 30 | "Invalid frame rate. Use '/' as delimiter if needed, like 24 or 24000/1001" 31 | ); 32 | 33 | println!("Parsing RPU file..."); 34 | 35 | let rpus = parse_rpu_file(input.clone())?; 36 | 37 | let mut count = if let Some(count) = args.count { 38 | if count + args.skip > rpus.len() { 39 | println!("Specified frame count exceeds the end."); 40 | rpus.len() 41 | } else { 42 | count 43 | } 44 | } else { 45 | rpus.len() 46 | }; 47 | 48 | let mut edl = EdlConverter::default(); 49 | 50 | let mut skip_count = args.skip; 51 | 52 | for rpu in rpus { 53 | if skip_count > 0 { 54 | skip_count -= 1; 55 | continue; 56 | } 57 | 58 | if count > 0 { 59 | if let Some(ref vdr) = rpu.vdr_dm_data { 60 | if vdr.scene_refresh_flag == 1 { 61 | edl.shots.push(edl.frame_index); 62 | } 63 | } 64 | 65 | edl.frame_index += 1; 66 | count -= 1; 67 | } 68 | } 69 | 70 | if edl.shots.len() == edl.frame_index && edl.frame_index > 1 && !args.force { 71 | println!( 72 | "Per-frame rpu detected, no need to generate EDL. Do you want to proceed? (Y/n)" 73 | ); 74 | 75 | let mut input = String::new(); 76 | stdin().read_line(&mut input)?; 77 | 78 | if input.trim().to_lowercase() != "y" { 79 | println!("Aborted."); 80 | return Ok(()); 81 | } 82 | } 83 | 84 | // Last frame 85 | edl.shots.push(edl.frame_index); 86 | 87 | let edit_rate = EditRate::from(args.rate); 88 | 89 | // We do not need to consider playback time here, so always uses NDF. 90 | let ntsc_flag = match edit_rate.0[1] { 91 | 1 => Ntsc::None, 92 | 1001 => Ntsc::NonDropFrame, 93 | _ => unimplemented!("Only /1 or /1001 denom is supported."), 94 | }; 95 | 96 | let frame_rate = Framerate::with_playback(format!("{edit_rate}"), ntsc_flag).unwrap(); 97 | 98 | let start_tc_record = Timecode::with_frames(args.start_timecode, frame_rate).unwrap(); 99 | // let start_tc_source = Timecode::with_frames(0, frame_rate).unwrap(); 100 | 101 | let mut frame_in = 0; 102 | 103 | let mut buffer_vec = Vec::new(); 104 | 105 | for (i, chunk) in edl.shots.chunks(9999).enumerate() { 106 | let mut edl_buffer = Vec::::new(); 107 | let mut writer = Cursor::new(&mut edl_buffer); 108 | 109 | // TODO: rename 110 | write!( 111 | writer, 112 | "TITLE: Timeline {} {i}\r\nFCM: NON-DROP FRAME\r\n\r\n", 113 | input.file_stem().unwrap().to_str().unwrap() 114 | )?; 115 | 116 | for (j, &shot) in chunk.iter().enumerate() { 117 | if shot == 0 { 118 | continue; 119 | } 120 | 121 | let frame_out = shot; 122 | 123 | let tc_source_in = Timecode::with_frames(frame_in, frame_rate).unwrap(); 124 | let tc_source_out = Timecode::with_frames(frame_out, frame_rate).unwrap(); 125 | 126 | let tc_duration = tc_source_out - tc_source_in; 127 | 128 | let tc_record_in = start_tc_record + tc_source_in; 129 | let tc_record_out = start_tc_record + tc_source_out; 130 | 131 | let k = j - if i == 0 { 0 } else { 1 }; 132 | 133 | // TODO: Transition 134 | write!( 135 | writer, 136 | "{:>04} AX V C {} {} {} {} \r\n* FROM CLIP NAME: {}\r\n\r\n", 137 | k, 138 | tc_source_in.timecode(), 139 | tc_duration.timecode(), 140 | tc_record_in.timecode(), 141 | tc_record_out.timecode(), 142 | args.clip_name 143 | )?; 144 | 145 | frame_in = frame_out; 146 | } 147 | 148 | // println!("{}", String::from_utf8(edl_buffer.clone())?); 149 | buffer_vec.push(edl_buffer); 150 | } 151 | 152 | let output = if let Some(output) = args.output { 153 | output 154 | } else { 155 | println!("No output file provided, writing to metadata.edl at current path..."); 156 | "./metadata.edl".into() 157 | }; 158 | 159 | if buffer_vec.len() == 1 { 160 | let mut output_buffer = BufWriter::new(File::create(output)?); 161 | write!( 162 | output_buffer, 163 | "{}", 164 | String::from_utf8(buffer_vec[0].clone())? 165 | )?; 166 | } else { 167 | let prefix = output.file_stem().unwrap().to_os_string(); 168 | let extension = if let Some(extension) = output.extension() { 169 | extension 170 | } else { 171 | OsStr::new("edl") 172 | }; 173 | 174 | for (i, buffer) in buffer_vec.iter().enumerate() { 175 | let suffix_string = format!("_{i}"); 176 | let suffix = OsStr::new(suffix_string.as_str()); 177 | let mut output_name = prefix.clone(); 178 | output_name.extend([suffix, extension]); 179 | let mut output_buffer = BufWriter::new(File::create(PathBuf::from(output_name))?); 180 | write!(output_buffer, "{}", String::from_utf8(buffer.clone())?)?; 181 | } 182 | } 183 | 184 | Ok(()) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/functions/mod.rs: -------------------------------------------------------------------------------- 1 | pub use convert::Converter; 2 | pub use edl::EdlConverter; 3 | mod convert; 4 | mod edl; 5 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | 4 | use crate::commands::Command; 5 | use crate::functions::{Converter, EdlConverter}; 6 | use crate::levels::*; 7 | use crate::metadata::*; 8 | use crate::Command::{Convert, Edl}; 9 | 10 | mod commands; 11 | mod functions; 12 | mod metadata; 13 | 14 | #[derive(Parser, Debug)] 15 | #[command( 16 | name = env!("CARGO_PKG_NAME"), 17 | about = "CLI tool for creating Dolby Vision XML metadata from an encoded deliverable with binary metadata", 18 | author = "Rainbaby", 19 | version = option_env!("VERGEN_GIT_DESCRIBE").unwrap_or(env!("CARGO_PKG_VERSION")) 20 | )] 21 | struct Opt { 22 | #[clap(subcommand)] 23 | cmd: Command, 24 | } 25 | 26 | fn main() -> Result<()> { 27 | let opt = Opt::parse(); 28 | 29 | match opt.cmd { 30 | Convert(args) => Converter::convert(args), 31 | Edl(args) => EdlConverter::convert(args), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/metadata/cmv29/display.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use crate::display::Chromaticity; 4 | use crate::{ColorSpace, Encoding, MDFType, Primaries, SignalRange}; 5 | 6 | #[derive(Debug, Serialize)] 7 | pub struct Characteristics { 8 | // 0 9 | #[serde(rename = "@level")] 10 | pub level: usize, 11 | #[serde(rename = "MasteringDisplay")] 12 | pub mastering_display: CharacteristicsLegacy, 13 | #[serde(rename = "TargetDisplay")] 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | pub target_displays: Option>, 16 | } 17 | 18 | #[derive(Debug, Serialize)] 19 | pub struct CharacteristicsLegacy { 20 | // 0 21 | #[serde(rename = "@level")] 22 | pub level: usize, 23 | #[serde(rename = "ID")] 24 | pub id: usize, 25 | #[serde(rename = "Name")] 26 | pub name: String, 27 | #[serde(rename = "Primaries")] 28 | pub primaries: Primaries, 29 | #[serde(rename = "WhitePoint")] 30 | pub white_point: MDFType, 31 | #[serde(rename = "PeakBrightness")] 32 | pub peak_brightness: usize, 33 | #[serde(rename = "MinimumBrightness")] 34 | pub minimum_brightness: f32, 35 | #[serde(rename = "DiagonalSize")] 36 | pub diagonal_size: usize, 37 | #[serde(rename = "Encoding")] 38 | pub encoding: Encoding, 39 | #[serde(rename = "BitDepth")] 40 | pub bit_depth: usize, 41 | #[serde(rename = "ColorSpace")] 42 | pub color_space: ColorSpace, 43 | #[serde(rename = "SignalRange")] 44 | pub signal_range: SignalRange, 45 | } 46 | -------------------------------------------------------------------------------- /src/metadata/cmv29/frame.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use crate::cmv29::ShotPluginNode; 4 | use crate::UUIDv4; 5 | 6 | #[derive(Debug, Serialize)] 7 | pub struct Frame { 8 | #[serde(rename = "UniqueID")] 9 | pub unique_id: UUIDv4, 10 | #[serde(rename = "EditOffset")] 11 | pub edit_offset: usize, 12 | #[serde(rename = "PluginNode")] 13 | pub plugin_node: ShotPluginNode, 14 | } 15 | -------------------------------------------------------------------------------- /src/metadata/cmv29/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use std::array; 3 | 4 | pub use display::*; 5 | pub use frame::*; 6 | pub use shot::*; 7 | pub use track::*; 8 | 9 | use crate::{RevisionHistory, UUIDv4, Version}; 10 | 11 | mod display; 12 | mod frame; 13 | mod shot; 14 | mod track; 15 | 16 | #[derive(Debug, Serialize)] 17 | pub struct DolbyLabsMDF { 18 | #[serde(rename = "@version")] 19 | pub version: Version, 20 | #[serde(rename = "@xmlns:xsd")] 21 | pub xmlns_xsd: String, 22 | #[serde(rename = "@xmlns:xsi")] 23 | pub xmlns_xsi: String, 24 | #[serde(rename = "SourceList")] 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | pub source_list: Option, 27 | #[serde(rename = "RevisionHistory")] 28 | #[serde(skip_serializing_if = "Option::is_none")] 29 | pub revision_history: Option, 30 | #[serde(rename = "Outputs")] 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | pub outputs: Option, 33 | } 34 | 35 | #[derive(Debug, Serialize)] 36 | pub struct SourceList { 37 | #[serde(rename = "Source")] 38 | #[serde(skip_serializing_if = "Option::is_none")] 39 | pub sources: Option>, 40 | } 41 | 42 | // TODO: Some other fields are available here 43 | #[derive(Debug, Serialize)] 44 | pub struct Source { 45 | #[serde(rename = "@type")] 46 | pub type_: String, 47 | #[serde(rename = "UniqueID")] 48 | pub unique_id: UUIDv4, 49 | #[serde(rename = "In")] 50 | pub in_: usize, 51 | #[serde(rename = "Duration")] 52 | pub duration: usize, 53 | } 54 | 55 | #[derive(Debug, Serialize)] 56 | pub struct Outputs { 57 | #[serde(rename = "Output")] 58 | #[serde(skip_serializing_if = "Option::is_none")] 59 | pub outputs: Option>, 60 | } 61 | 62 | impl Outputs { 63 | pub fn get_source_list(&self) -> Option { 64 | if let Some(sources) = self.outputs.as_ref().map(|outputs| { 65 | outputs 66 | .iter() 67 | .filter_map(|output| { 68 | output 69 | .video 70 | .tracks 71 | .last() 72 | .and_then(|track| track.shots.as_ref()) 73 | .and_then(|shots| shots.last()) 74 | .map(|shot| { 75 | let record = shot.record.clone(); 76 | let source = shot.source.clone(); 77 | 78 | let duration = record.in_ + record.duration; 79 | let unique_id = source.parent_id; 80 | 81 | Source { 82 | type_: "Video".to_string(), 83 | unique_id, 84 | in_: 0, 85 | duration, 86 | } 87 | }) 88 | }) 89 | .collect::>() 90 | }) { 91 | let source_list = SourceList { 92 | sources: Some(sources), 93 | }; 94 | 95 | Some(source_list) 96 | } else { 97 | None 98 | } 99 | } 100 | } 101 | 102 | #[derive(Debug, Serialize)] 103 | pub struct Output { 104 | #[serde(rename = "@name")] 105 | pub name: String, 106 | #[serde(rename = "UniqueID")] 107 | pub unique_id: UUIDv4, 108 | #[serde(rename = "NumberVideoTracks")] 109 | pub number_video_tracks: usize, 110 | #[serde(rename = "NumberAudioTracks")] 111 | pub number_audio_tracks: usize, 112 | #[serde(rename = "CanvasAspectRatio")] 113 | pub canvas_aspect_ratio: f32, 114 | #[serde(rename = "ImageAspectRatio")] 115 | pub image_aspect_ratio: f32, 116 | #[serde(rename = "Video")] 117 | pub video: Video, 118 | } 119 | 120 | #[derive(Debug, Serialize)] 121 | pub struct Video { 122 | #[serde(rename = "Track")] 123 | pub tracks: Vec, 124 | } 125 | 126 | #[derive(Debug, Clone, Copy)] 127 | pub struct AlgorithmVersions([usize; 2]); 128 | 129 | impl Default for AlgorithmVersions { 130 | fn default() -> Self { 131 | Self([2, 1]) 132 | } 133 | } 134 | 135 | impl IntoIterator for AlgorithmVersions { 136 | type Item = usize; 137 | type IntoIter = array::IntoIter; 138 | 139 | fn into_iter(self) -> Self::IntoIter { 140 | self.0.into_iter() 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/metadata/cmv29/shot.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use crate::cmv29::{Frame, Source}; 4 | use crate::cmv40::DVDynamicData; 5 | use crate::{cmv40, IntoCMV29, Level1, Level2, Level5, UUIDv4}; 6 | 7 | #[derive(Debug, Serialize)] 8 | pub struct Shot { 9 | #[serde(rename = "UniqueID")] 10 | pub unique_id: UUIDv4, 11 | #[serde(rename = "Source")] 12 | pub source: ShotSource, 13 | #[serde(rename = "Record")] 14 | pub record: Record, 15 | #[serde(rename = "PluginNode")] 16 | pub plugin_node: ShotPluginNode, 17 | #[serde(rename = "Frame")] 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | pub frames: Option>, 20 | } 21 | 22 | // CMv2.9 only 23 | #[derive(Debug, Clone, Default, Serialize)] 24 | pub struct ShotSource { 25 | #[serde(rename = "ParentID")] 26 | pub parent_id: UUIDv4, 27 | #[serde(rename = "In")] 28 | pub in_: usize, 29 | } 30 | 31 | impl From for ShotSource { 32 | fn from(s: Source) -> Self { 33 | Self { 34 | parent_id: s.unique_id, 35 | in_: s.in_, 36 | } 37 | } 38 | } 39 | 40 | #[derive(Debug, Clone, Serialize)] 41 | pub struct Record { 42 | #[serde(rename = "In")] 43 | pub in_: usize, 44 | #[serde(rename = "Duration")] 45 | pub duration: usize, 46 | } 47 | 48 | impl From for Record { 49 | fn from(record: cmv40::Record) -> Self { 50 | Self { 51 | in_: record.in_, 52 | duration: record.duration, 53 | } 54 | } 55 | } 56 | 57 | #[derive(Debug, Serialize)] 58 | pub struct ShotPluginNode { 59 | #[serde(rename = "DolbyEDR")] 60 | pub level1: Level1, 61 | #[serde(rename = "DolbyEDR")] 62 | #[serde(skip_serializing_if = "Option::is_none")] 63 | pub level2: Option>, 64 | #[serde(rename = "DolbyEDR")] 65 | #[serde(skip_serializing_if = "Option::is_none")] 66 | pub level5: Option, 67 | } 68 | 69 | impl From for ShotPluginNode { 70 | fn from(data: DVDynamicData) -> Self { 71 | Self { 72 | level1: data.level1.into_cmv29(), 73 | level2: data.level2.into_cmv29(), 74 | level5: data.level5.into_cmv29(), 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/metadata/cmv29/track.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use crate::cmv29::{AlgorithmVersions, Characteristics, Shot}; 4 | use crate::display::Chromaticity; 5 | use crate::MDFType::CMV29; 6 | use crate::{ 7 | cmv40, ColorSpace, Encoding, IntoCMV29, Level6, MDFType, Primaries, SignalRange, 8 | SignalRangeEnum, UUIDv4, 9 | }; 10 | 11 | #[derive(Debug, Serialize)] 12 | pub struct Track { 13 | #[serde(rename = "@name")] 14 | pub name: String, 15 | #[serde(rename = "UniqueID")] 16 | pub unique_id: UUIDv4, 17 | #[serde(rename = "Rate")] 18 | pub rate: Rate, 19 | #[serde(rename = "ColorEncoding")] 20 | pub color_encoding: ColorEncoding, 21 | #[serde(rename = "Level6")] 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | pub level6: Option, 24 | #[serde(rename = "PluginNode")] 25 | pub plugin_node: TrackPluginNode, 26 | #[serde(rename = "Shot")] 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | pub shots: Option>, 29 | } 30 | 31 | #[derive(Debug, Serialize)] 32 | pub struct Rate { 33 | #[serde(rename = "n")] 34 | pub n: usize, 35 | #[serde(rename = "d")] 36 | pub d: usize, 37 | } 38 | 39 | #[derive(Debug, Serialize)] 40 | pub struct ColorEncoding { 41 | #[serde(rename = "Primaries")] 42 | pub primaries: Primaries, 43 | // Format: f32,f32 44 | #[serde(rename = "WhitePoint")] 45 | pub white_point: MDFType, 46 | #[serde(rename = "PeakBrightness")] 47 | pub peak_brightness: usize, 48 | #[serde(rename = "MinimumBrightness")] 49 | pub minimum_brightness: usize, 50 | #[serde(rename = "Encoding")] 51 | pub encoding: Encoding, 52 | #[serde(rename = "BitDepth")] 53 | pub bit_depth: usize, 54 | #[serde(rename = "ColorSpace")] 55 | pub color_space: ColorSpace, 56 | // FIXME: use usize? 57 | #[serde(rename = "ChromaFormat")] 58 | pub chroma_format: String, 59 | #[serde(rename = "SignalRange")] 60 | pub signal_range: SignalRange, 61 | } 62 | 63 | impl From for ColorEncoding { 64 | fn from(c: cmv40::ColorEncoding) -> Self { 65 | Self { 66 | primaries: c.primaries.into_cmv29(), 67 | white_point: c.white_point.into_cmv29(), 68 | peak_brightness: c.peak_brightness, 69 | minimum_brightness: c.minimum_brightness, 70 | encoding: c.encoding, 71 | // TODO: as an option? 72 | bit_depth: 16, 73 | color_space: c.color_space, 74 | chroma_format: "444".to_string(), 75 | signal_range: SignalRange { 76 | signal_range: SignalRangeEnum::Computer, 77 | }, 78 | } 79 | } 80 | } 81 | 82 | #[derive(Debug, Serialize)] 83 | pub struct TrackPluginNode { 84 | #[serde(rename = "DolbyEDR")] 85 | pub dolby_edr: TrackDolbyEDR, 86 | } 87 | 88 | impl From for TrackPluginNode { 89 | fn from(t: cmv40::TrackPluginNode) -> Self { 90 | Self { 91 | dolby_edr: TrackDolbyEDR { 92 | algorithm_versions: CMV29(AlgorithmVersions::default()), 93 | characteristics: Characteristics { 94 | level: 0, 95 | mastering_display: t.dv_global_data.mastering_display.into_cmv29(), 96 | target_displays: t.dv_global_data.target_displays.into_cmv29(), 97 | }, 98 | }, 99 | } 100 | } 101 | } 102 | 103 | #[derive(Debug, Serialize)] 104 | pub struct TrackDolbyEDR { 105 | // Format: usize,usize 106 | #[serde(rename = "AlgorithmVersions")] 107 | pub algorithm_versions: MDFType, 108 | #[serde(rename = "Characteristics")] 109 | pub characteristics: Characteristics, 110 | } 111 | -------------------------------------------------------------------------------- /src/metadata/cmv40/display.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use crate::cmv29::CharacteristicsLegacy; 4 | use crate::display::Chromaticity; 5 | use crate::MDFType::CMV40; 6 | use crate::{ 7 | display, ApplicationType, ColorSpace, ColorSpaceEnum, Encoding, IntoCMV29, MDFType, Primaries, 8 | SignalRange, SignalRangeEnum, 9 | }; 10 | 11 | #[derive(Debug, Clone, Default, Serialize)] 12 | pub struct Characteristics { 13 | #[serde(rename = "ID")] 14 | pub id: usize, 15 | #[serde(rename = "Name")] 16 | pub name: String, 17 | #[serde(rename = "Primaries")] 18 | pub primaries: Primaries, 19 | #[serde(rename = "WhitePoint")] 20 | pub white_point: MDFType, 21 | #[serde(rename = "PeakBrightness")] 22 | pub peak_brightness: usize, 23 | #[serde(rename = "MinimumBrightness")] 24 | pub minimum_brightness: f32, 25 | #[serde(rename = "EOTF")] 26 | pub eotf: Encoding, 27 | #[serde(rename = "DiagonalSize")] 28 | pub diagonal_size: usize, 29 | // Version 5.0.0+ 30 | #[serde(rename = "ApplicationType")] 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | pub application_type: Option, 33 | } 34 | 35 | impl From for Characteristics { 36 | fn from(d: display::Characteristics) -> Self { 37 | Self { 38 | id: d.id, 39 | name: d.name, 40 | primaries: d.primaries.into(), 41 | white_point: CMV40(d.primaries.white_point), 42 | peak_brightness: d.peak_brightness, 43 | minimum_brightness: d.minimum_brightness, 44 | eotf: d.encoding, 45 | diagonal_size: d.diagonal_size, 46 | application_type: None, 47 | } 48 | } 49 | } 50 | 51 | impl IntoCMV29 for Characteristics { 52 | fn into_cmv29(self) -> CharacteristicsLegacy { 53 | CharacteristicsLegacy { 54 | level: 0, 55 | id: self.id, 56 | name: self.name, 57 | primaries: self.primaries.into_cmv29(), 58 | white_point: self.white_point.into_cmv29(), 59 | peak_brightness: self.peak_brightness, 60 | minimum_brightness: self.minimum_brightness, 61 | diagonal_size: self.diagonal_size, 62 | encoding: self.eotf, 63 | bit_depth: 16, 64 | color_space: ColorSpace { 65 | color_space: ColorSpaceEnum::Rgb, 66 | }, 67 | signal_range: SignalRange { 68 | signal_range: SignalRangeEnum::Computer, 69 | }, 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/metadata/cmv40/frame.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use crate::cmv40::Shot; 4 | use crate::metadata::cmv40::ShotPluginNode; 5 | use crate::{cmv29, IntoCMV29, UUIDv4}; 6 | 7 | #[derive(Debug, Clone, Default, Serialize)] 8 | pub struct Frame { 9 | #[serde(rename = "EditOffset")] 10 | pub edit_offset: usize, 11 | #[serde(rename = "PluginNode")] 12 | pub plugin_node: ShotPluginNode, 13 | } 14 | 15 | impl Frame { 16 | pub fn with_offset(shot: &Shot, offset: usize) -> Self { 17 | let mut dv_dynamic_data = shot.plugin_node.dv_dynamic_data.clone(); 18 | // Remove Level 9 in per-frame metadata 19 | dv_dynamic_data.level9 = None; 20 | Self { 21 | edit_offset: offset, 22 | plugin_node: ShotPluginNode { 23 | dv_dynamic_data, 24 | level11: None, 25 | }, 26 | } 27 | } 28 | } 29 | 30 | impl From<&Shot> for Frame { 31 | fn from(shot: &Shot) -> Self { 32 | Self::with_offset(shot, 0) 33 | } 34 | } 35 | 36 | impl IntoCMV29 for Frame { 37 | fn into_cmv29(self) -> cmv29::Frame { 38 | cmv29::Frame { 39 | unique_id: UUIDv4::new(), 40 | edit_offset: self.edit_offset, 41 | plugin_node: self.plugin_node.dv_dynamic_data.into(), 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/metadata/cmv40/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use serde::Serialize; 3 | 4 | pub use display::*; 5 | pub use frame::*; 6 | pub use shot::*; 7 | pub use track::*; 8 | 9 | use crate::XMLVersion::{V402, V510}; 10 | use crate::{ 11 | cmv29, ApplicationType, ApplicationTypeEnum, IntoCMV29, Level5, RevisionHistory, UUIDv4, 12 | Version, XMLVersion, CMV40_MIN_VERSION, UHD_AR, 13 | }; 14 | 15 | mod display; 16 | mod frame; 17 | mod shot; 18 | mod track; 19 | 20 | #[derive(Debug, Serialize)] 21 | pub struct DolbyLabsMDF { 22 | pub xmlns: String, 23 | #[serde(rename = "Version")] 24 | pub version: Version, 25 | #[serde(rename = "RevisionHistory")] 26 | #[serde(skip_serializing_if = "Option::is_none")] 27 | pub revision_history: Option, 28 | #[serde(rename = "Outputs")] 29 | pub outputs: Outputs, 30 | } 31 | 32 | impl DolbyLabsMDF { 33 | pub fn with_single_output(output: Output) -> Result { 34 | let mut output = output; 35 | 36 | let has_level11 = output 37 | .video 38 | .tracks 39 | .first() 40 | .map(|track| track.plugin_node.level11.is_some()) 41 | .context("No track in output.")?; 42 | 43 | let version: Version = match has_level11 { 44 | true => V510, 45 | false => V402, 46 | } 47 | .into(); 48 | 49 | if version > CMV40_MIN_VERSION { 50 | output.video.tracks.iter_mut().for_each(|track| { 51 | track 52 | .plugin_node 53 | .dv_global_data 54 | .mastering_display 55 | .application_type = Some(ApplicationType { 56 | application_type: ApplicationTypeEnum::All, 57 | }); 58 | 59 | if let Some(ds) = track.plugin_node.dv_global_data.target_displays.as_mut() { 60 | ds.iter_mut().for_each(|d| { 61 | d.application_type = Some(ApplicationType { 62 | application_type: ApplicationTypeEnum::Home, 63 | }) 64 | }) 65 | } 66 | }); 67 | } 68 | 69 | Ok(Self { 70 | xmlns: version.get_dolby_xmlns(), 71 | version, 72 | revision_history: Some(RevisionHistory::new()), 73 | outputs: Outputs { 74 | outputs: vec![output], 75 | }, 76 | }) 77 | } 78 | } 79 | 80 | impl IntoCMV29 for DolbyLabsMDF { 81 | fn into_cmv29(self) -> cmv29::DolbyLabsMDF { 82 | let outputs = self.outputs.into_cmv29(); 83 | 84 | cmv29::DolbyLabsMDF { 85 | version: XMLVersion::V205.into(), 86 | xmlns_xsd: "http://www.w3.org/2001/XMLSchema".to_string(), 87 | xmlns_xsi: "http://www.w3.org/2001/XMLSchema-instance".to_string(), 88 | source_list: outputs.get_source_list(), 89 | revision_history: self.revision_history, 90 | outputs: Some(outputs), 91 | } 92 | } 93 | } 94 | 95 | #[derive(Debug, Serialize)] 96 | pub struct Outputs { 97 | #[serde(rename = "Output")] 98 | pub outputs: Vec, 99 | } 100 | 101 | impl IntoCMV29 for Outputs { 102 | fn into_cmv29(self) -> cmv29::Outputs { 103 | cmv29::Outputs { 104 | outputs: Some(self.outputs.into_cmv29()), 105 | } 106 | } 107 | } 108 | 109 | #[derive(Debug, Clone, Serialize)] 110 | pub struct Output { 111 | #[serde(rename = "CompositionName")] 112 | pub composition_name: String, 113 | #[serde(rename = "UniqueID")] 114 | pub unique_id: UUIDv4, 115 | #[serde(rename = "NumberVideoTracks")] 116 | pub number_video_tracks: usize, 117 | #[serde(rename = "CanvasAspectRatio")] 118 | pub canvas_aspect_ratio: f32, 119 | #[serde(rename = "ImageAspectRatio")] 120 | pub image_aspect_ratio: f32, 121 | #[serde(rename = "Video")] 122 | pub video: Video, 123 | } 124 | 125 | impl IntoCMV29 for Output { 126 | fn into_cmv29(self) -> cmv29::Output { 127 | let mut video = self.video.into_cmv29(); 128 | let parent_id = UUIDv4::new(); 129 | video.tracks.iter_mut().for_each(|track| { 130 | track.shots.iter_mut().for_each(|shots| { 131 | shots.iter_mut().for_each(|shot| { 132 | shot.source.parent_id = parent_id.clone(); 133 | }) 134 | }) 135 | }); 136 | 137 | cmv29::Output { 138 | name: self.composition_name, 139 | unique_id: self.unique_id, 140 | number_video_tracks: self.number_video_tracks, 141 | number_audio_tracks: 0, 142 | canvas_aspect_ratio: self.canvas_aspect_ratio, 143 | image_aspect_ratio: self.image_aspect_ratio, 144 | video, 145 | } 146 | } 147 | } 148 | 149 | impl Output { 150 | pub fn with_level5(track: Track, level5: Option) -> Self { 151 | let (canvas_aspect_ratio, image_aspect_ratio) = if let Some(level5) = level5 { 152 | level5.get_ar() 153 | } else { 154 | // Should not happen 155 | (UHD_AR, UHD_AR) 156 | }; 157 | 158 | Self { 159 | composition_name: "Timeline".to_string(), 160 | unique_id: UUIDv4::new(), 161 | number_video_tracks: 1, 162 | canvas_aspect_ratio, 163 | image_aspect_ratio, 164 | video: Video { 165 | tracks: vec![track], 166 | }, 167 | } 168 | } 169 | } 170 | 171 | #[derive(Debug, Clone, Serialize)] 172 | pub struct Video { 173 | #[serde(rename = "Track")] 174 | pub tracks: Vec, 175 | } 176 | 177 | impl IntoCMV29 for Video { 178 | fn into_cmv29(self) -> cmv29::Video { 179 | cmv29::Video { 180 | tracks: self.tracks.into_cmv29(), 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/metadata/cmv40/shot.rs: -------------------------------------------------------------------------------- 1 | use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlock; 2 | use dolby_vision::rpu::vdr_dm_data::VdrDmData; 3 | use serde::Serialize; 4 | 5 | use crate::cmv40::Frame; 6 | use crate::levels::*; 7 | use crate::metadata::update_levels; 8 | use crate::{cmv29, IntoCMV29, UUIDv4}; 9 | 10 | #[derive(Debug, Clone, Default, Serialize)] 11 | pub struct Shot { 12 | #[serde(rename = "UniqueID")] 13 | pub unique_id: UUIDv4, 14 | #[serde(rename = "Record")] 15 | pub record: Record, 16 | #[serde(rename = "PluginNode")] 17 | pub plugin_node: ShotPluginNode, 18 | #[serde(rename = "Frame")] 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | pub frames: Option>, 21 | } 22 | 23 | impl Shot { 24 | pub fn update_record(&mut self, index: Option, duration_override: Option) { 25 | match index { 26 | Some(index) => { 27 | self.record.in_ = index; 28 | self.record.duration = 1; 29 | } 30 | None => { 31 | // FIXME: dirty 32 | self.record.duration += 1; 33 | } 34 | } 35 | 36 | if let Some(duration) = duration_override { 37 | self.record.duration = duration + 1; 38 | } 39 | } 40 | 41 | pub fn with_canvas(vdr: &VdrDmData, canvas: (usize, usize)) -> Self { 42 | Self { 43 | unique_id: UUIDv4::new(), 44 | record: Default::default(), 45 | plugin_node: ShotPluginNode::with_canvas(vdr, canvas), 46 | frames: None, 47 | } 48 | } 49 | 50 | pub fn append_metadata(&mut self, other: &Self) { 51 | if self.frames.is_none() && self.plugin_node != other.plugin_node { 52 | self.frames = Some(Vec::new()); 53 | } 54 | 55 | // Always parse per-frame metadata until next shot 56 | if let Some(ref mut frames) = self.frames { 57 | let offset = self.record.duration - 1; 58 | let mut new_frame = Frame::with_offset(other, offset); 59 | new_frame 60 | .plugin_node 61 | .update_per_frame_default_metadata(&self.plugin_node); 62 | frames.push(new_frame); 63 | } 64 | } 65 | } 66 | 67 | impl From<&VdrDmData> for Shot { 68 | fn from(vdr: &VdrDmData) -> Self { 69 | Self::with_canvas(vdr, UHD_CANVAS) 70 | } 71 | } 72 | 73 | impl IntoCMV29 for Shot { 74 | fn into_cmv29(self) -> cmv29::Shot { 75 | cmv29::Shot { 76 | unique_id: self.unique_id, 77 | source: cmv29::ShotSource::default(), 78 | record: self.record.into(), 79 | plugin_node: self.plugin_node.dv_dynamic_data.into(), 80 | frames: self.frames.into_cmv29(), 81 | } 82 | } 83 | } 84 | 85 | #[derive(Debug, Clone, Default, PartialEq, Serialize)] 86 | pub struct ShotPluginNode { 87 | #[serde(rename = "DVDynamicData")] 88 | pub dv_dynamic_data: DVDynamicData, 89 | // Version 5.1.0+ 90 | #[serde(rename = "Level11")] 91 | #[serde(skip_serializing_if = "Option::is_none")] 92 | pub level11: Option, 93 | } 94 | 95 | impl ShotPluginNode { 96 | fn with_canvas(vdr: &VdrDmData, canvas: (usize, usize)) -> Self { 97 | let level11 = vdr.get_block(11).and_then(|b| match b { 98 | ExtMetadataBlock::Level11(b) => Some(Level11::from(b)), 99 | _ => None, 100 | }); 101 | 102 | Self { 103 | dv_dynamic_data: DVDynamicData::with_canvas(vdr, canvas), 104 | level11, 105 | } 106 | } 107 | 108 | fn update_per_frame_default_metadata(&mut self, reference: &Self) { 109 | update_levels( 110 | &mut self.dv_dynamic_data.level2, 111 | &reference.dv_dynamic_data.level2, 112 | ); 113 | update_levels( 114 | &mut self.dv_dynamic_data.level8, 115 | &reference.dv_dynamic_data.level8, 116 | ); 117 | 118 | if self.dv_dynamic_data.level3.is_none() { 119 | self.dv_dynamic_data.level3 = Some(Level3::default()); 120 | } 121 | } 122 | } 123 | 124 | impl From<&VdrDmData> for ShotPluginNode { 125 | fn from(vdr: &VdrDmData) -> Self { 126 | Self::with_canvas(vdr, UHD_CANVAS) 127 | } 128 | } 129 | 130 | #[derive(Debug, Clone, Default, PartialEq, Serialize)] 131 | pub struct DVDynamicData { 132 | #[serde(rename = "Level1")] 133 | pub level1: Level1, 134 | #[serde(rename = "Level2")] 135 | #[serde(skip_serializing_if = "Option::is_none")] 136 | pub level2: Option>, 137 | #[serde(rename = "Level3")] 138 | #[serde(skip_serializing_if = "Option::is_none")] 139 | pub level3: Option, 140 | #[serde(rename = "Level5")] 141 | #[serde(skip_serializing_if = "Option::is_none")] 142 | pub level5: Option, 143 | #[serde(rename = "Level8")] 144 | #[serde(skip_serializing_if = "Option::is_none")] 145 | pub level8: Option>, 146 | #[serde(rename = "Level9")] 147 | #[serde(skip_serializing_if = "Option::is_none")] 148 | pub level9: Option, 149 | } 150 | 151 | impl DVDynamicData { 152 | pub fn with_canvas(vdr: &VdrDmData, canvas: (usize, usize)) -> Self { 153 | let level1 = if let Some(ExtMetadataBlock::Level1(block)) = vdr.get_block(1) { 154 | Level1::from(block) 155 | } else { 156 | Level1::default() 157 | }; 158 | 159 | let mut primary = None; 160 | 161 | let level9 = vdr.get_block(9).and_then(|b| match b { 162 | ExtMetadataBlock::Level9(b) => { 163 | primary = Some(b.source_primary_index as usize); 164 | Some(Level9::from(b)) 165 | } 166 | _ => None, 167 | }); 168 | 169 | let level2 = vdr 170 | .level_blocks_iter(2) 171 | .map(|b| match b { 172 | ExtMetadataBlock::Level2(b) => Some(Level2::with_primary_index(b, primary)), 173 | _ => None, 174 | }) 175 | .collect::>>(); 176 | 177 | let level3 = vdr.get_block(3).and_then(|b| match b { 178 | ExtMetadataBlock::Level3(b) => Some(Level3::from(b)), 179 | _ => None, 180 | }); 181 | 182 | let level5 = { 183 | let b = vdr.get_block(5).and_then(|b| match b { 184 | ExtMetadataBlock::Level5(b) => Some(b), 185 | _ => None, 186 | }); 187 | 188 | Some(Level5::with_canvas(b, canvas)) 189 | }; 190 | 191 | let level8 = vdr 192 | .level_blocks_iter(8) 193 | .map(|b| match b { 194 | ExtMetadataBlock::Level8(b) => Some(Level8::from(b)), 195 | _ => None, 196 | }) 197 | .collect::>>(); 198 | 199 | Self { 200 | level1, 201 | level2, 202 | level3, 203 | level5, 204 | level8, 205 | level9, 206 | } 207 | } 208 | } 209 | 210 | impl From<&VdrDmData> for DVDynamicData { 211 | fn from(vdr: &VdrDmData) -> Self { 212 | Self::with_canvas(vdr, UHD_CANVAS) 213 | } 214 | } 215 | 216 | // TODO: Start duration is 1 217 | #[derive(Debug, Clone, Default, Serialize)] 218 | pub struct Record { 219 | #[serde(rename = "In")] 220 | pub in_: usize, 221 | #[serde(rename = "Duration")] 222 | pub duration: usize, 223 | } 224 | -------------------------------------------------------------------------------- /src/metadata/cmv40/track.rs: -------------------------------------------------------------------------------- 1 | use std::array; 2 | use std::fmt::{Display, Formatter}; 3 | 4 | use anyhow::{ensure, Result}; 5 | use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlock; 6 | use dolby_vision::rpu::vdr_dm_data::VdrDmData; 7 | use itertools::Itertools; 8 | use serde::Serialize; 9 | 10 | use crate::cmv29::Rate; 11 | use crate::cmv40::display::Characteristics; 12 | use crate::cmv40::Shot; 13 | use crate::display::Chromaticity; 14 | use crate::levels::*; 15 | use crate::MDFType::CMV40; 16 | use crate::{ 17 | cmv29, display, ColorSpace, ColorSpaceEnum, Encoding, EncodingEnum, IntoCMV29, MDFType, 18 | Primaries, SignalRange, SignalRangeEnum, UUIDv4, 19 | }; 20 | 21 | #[derive(Debug, Clone, Default, Serialize)] 22 | pub struct Track { 23 | #[serde(rename = "TrackName")] 24 | pub track_name: String, 25 | #[serde(rename = "UniqueID")] 26 | pub unique_id: UUIDv4, 27 | #[serde(rename = "EditRate")] 28 | pub edit_rate: MDFType, 29 | #[serde(rename = "ColorEncoding")] 30 | pub color_encoding: ColorEncoding, 31 | #[serde(rename = "Level6")] 32 | #[serde(skip_serializing_if = "Option::is_none")] 33 | pub level6: Option, 34 | #[serde(rename = "PluginNode")] 35 | pub plugin_node: TrackPluginNode, 36 | #[serde(rename = "Shot")] 37 | #[serde(skip_serializing_if = "Option::is_none")] 38 | pub shots: Option>, 39 | } 40 | 41 | impl Track { 42 | pub fn with_single_vdr(vdr: &VdrDmData) -> Self { 43 | let level6 = match vdr.get_block(6) { 44 | Some(ExtMetadataBlock::Level6(b)) => Some(Level6::from(b)), 45 | _ => None, 46 | }; 47 | 48 | Self { 49 | // TODO: as option 50 | track_name: "V1".to_string(), 51 | unique_id: UUIDv4::new(), 52 | edit_rate: CMV40(EditRate::default()), 53 | color_encoding: Default::default(), 54 | level6, 55 | plugin_node: vdr.into(), 56 | shots: None, 57 | } 58 | } 59 | } 60 | 61 | impl IntoCMV29 for Track { 62 | fn into_cmv29(self) -> cmv29::Track { 63 | cmv29::Track { 64 | name: self.track_name, 65 | unique_id: self.unique_id, 66 | rate: self.edit_rate.into_inner().into_cmv29(), 67 | color_encoding: self.color_encoding.into(), 68 | level6: self.level6, 69 | plugin_node: self.plugin_node.into(), 70 | // Source UUID in each shot is not updated yet 71 | shots: self.shots.into_cmv29(), 72 | } 73 | } 74 | } 75 | 76 | #[derive(Clone, Copy, Debug, Serialize)] 77 | pub struct EditRate(pub [usize; 2]); 78 | 79 | impl EditRate { 80 | pub fn validate(&self) -> Result<()> { 81 | ensure!(self.0[0] != 0 && self.0[1] != 0, "Invalid frame rate."); 82 | 83 | Ok(()) 84 | } 85 | } 86 | 87 | impl Default for EditRate { 88 | fn default() -> Self { 89 | // TODO 90 | Self([24000, 1001]) 91 | } 92 | } 93 | 94 | impl From> for EditRate { 95 | fn from(vec: Vec) -> Self { 96 | let mut array = [1; 2]; 97 | 98 | vec.iter().enumerate().for_each(|(i, n)| array[i] = *n); 99 | 100 | Self(array) 101 | } 102 | } 103 | 104 | impl IntoIterator for EditRate { 105 | type Item = usize; 106 | type IntoIter = array::IntoIter; 107 | 108 | fn into_iter(self) -> Self::IntoIter { 109 | self.0.into_iter() 110 | } 111 | } 112 | 113 | impl IntoCMV29 for EditRate { 114 | fn into_cmv29(self) -> Rate { 115 | Rate { 116 | n: self.0[0], 117 | d: self.0[1], 118 | } 119 | } 120 | } 121 | 122 | impl Display for EditRate { 123 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 124 | write!(f, "{}", self.into_iter().join("/")) 125 | } 126 | } 127 | 128 | #[derive(Debug, Clone, Default, Serialize)] 129 | pub struct TrackPluginNode { 130 | #[serde(rename = "DVGlobalData")] 131 | pub dv_global_data: DVGlobalData, 132 | // Version 5.1.0+ 133 | #[serde(rename = "Level11")] 134 | #[serde(skip_serializing_if = "Option::is_none")] 135 | pub level11: Option, 136 | // For Version 4.0.2+, level254 should not be None. 137 | #[serde(rename = "Level254")] 138 | #[serde(skip_serializing_if = "Option::is_none")] 139 | pub level254: Option, 140 | } 141 | 142 | impl From<&VdrDmData> for TrackPluginNode { 143 | fn from(vdr: &VdrDmData) -> Self { 144 | let level11 = vdr.get_block(11).and_then(|b| match b { 145 | ExtMetadataBlock::Level11(b) => Some(Level11::from(b)), 146 | _ => None, 147 | }); 148 | 149 | let level254 = vdr.get_block(254).and_then(|b| match b { 150 | ExtMetadataBlock::Level254(b) => Some(Level254::from(b)), 151 | _ => None, 152 | }); 153 | 154 | let mastering_display = display::Characteristics::get_source_or_default(vdr).into(); 155 | 156 | Self { 157 | dv_global_data: DVGlobalData { 158 | level: 0, 159 | mastering_display, 160 | target_displays: None, 161 | }, 162 | level11, 163 | level254, 164 | } 165 | } 166 | } 167 | 168 | #[derive(Debug, Clone, Serialize)] 169 | pub struct ColorEncoding { 170 | #[serde(rename = "Primaries")] 171 | pub primaries: Primaries, 172 | #[serde(rename = "WhitePoint")] 173 | pub white_point: MDFType, 174 | #[serde(rename = "PeakBrightness")] 175 | pub peak_brightness: usize, 176 | #[serde(rename = "MinimumBrightness")] 177 | pub minimum_brightness: usize, 178 | #[serde(rename = "Encoding")] 179 | pub encoding: Encoding, 180 | #[serde(rename = "ColorSpace")] 181 | pub color_space: ColorSpace, 182 | #[serde(rename = "SignalRange")] 183 | pub signal_range: SignalRange, 184 | } 185 | 186 | // TODO: Default is BT.2020 PQ, should provide other options 187 | impl Default for ColorEncoding { 188 | fn default() -> Self { 189 | let p = display::Primaries::get_index_primary(2, false).unwrap_or_default(); 190 | 191 | Self { 192 | primaries: p.into(), 193 | white_point: CMV40(p.white_point), 194 | peak_brightness: 10000, 195 | minimum_brightness: 0, 196 | encoding: Encoding { 197 | encoding: EncodingEnum::Pq, 198 | }, 199 | color_space: ColorSpace { 200 | color_space: ColorSpaceEnum::Rgb, 201 | }, 202 | signal_range: SignalRange { 203 | signal_range: SignalRangeEnum::Computer, 204 | }, 205 | } 206 | } 207 | } 208 | 209 | #[derive(Debug, Clone, Default, Serialize)] 210 | pub struct DVGlobalData { 211 | // 0 212 | #[serde(rename = "@level")] 213 | pub level: usize, 214 | #[serde(rename = "MasteringDisplay")] 215 | pub mastering_display: Characteristics, 216 | #[serde(rename = "TargetDisplay")] 217 | #[serde(skip_serializing_if = "Option::is_none")] 218 | pub target_displays: Option>, 219 | } 220 | -------------------------------------------------------------------------------- /src/metadata/display/characteristics.rs: -------------------------------------------------------------------------------- 1 | use std::hash::{Hash, Hasher}; 2 | use std::intrinsics::transmute; 3 | 4 | use dolby_vision::rpu::extension_metadata::blocks::{ 5 | ExtMetadataBlock, ExtMetadataBlockInfo, ExtMetadataBlockLevel10, ExtMetadataBlockLevel2, 6 | ExtMetadataBlockLevel8, 7 | }; 8 | use dolby_vision::rpu::vdr_dm_data::VdrDmData; 9 | use itertools::Itertools; 10 | 11 | use crate::display::{PREDEFINED_MASTERING_DISPLAYS, PREDEFINED_TARGET_DISPLAYS, RPU_PQ_MAX}; 12 | use crate::metadata::display::primary::Primaries; 13 | use crate::{display, Encoding, EncodingEnum}; 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct Characteristics { 17 | pub name: String, 18 | pub id: usize, 19 | pub primary_index: usize, 20 | pub primaries: Primaries, 21 | pub peak_brightness: usize, 22 | pub minimum_brightness: f32, 23 | pub encoding: Encoding, 24 | pub diagonal_size: usize, 25 | } 26 | 27 | impl PartialEq for Characteristics { 28 | fn eq(&self, other: &Self) -> bool { 29 | self.name == other.name 30 | && self.id == other.id 31 | && self.primary_index == other.primary_index 32 | && self.primaries == other.primaries 33 | && self.peak_brightness == other.peak_brightness 34 | && self.minimum_brightness.to_bits() == other.minimum_brightness.to_bits() 35 | && self.encoding == other.encoding 36 | && self.diagonal_size == other.diagonal_size 37 | } 38 | } 39 | 40 | impl Eq for Characteristics {} 41 | 42 | impl Hash for Characteristics { 43 | fn hash(&self, state: &mut H) { 44 | self.name.hash(state); 45 | self.id.hash(state); 46 | self.primary_index.hash(state); 47 | self.primaries.hash(state); 48 | self.peak_brightness.hash(state); 49 | self.minimum_brightness.to_bits().hash(state); 50 | self.encoding.hash(state); 51 | self.diagonal_size.hash(state); 52 | } 53 | } 54 | 55 | impl Characteristics { 56 | pub fn update_name(&mut self) { 57 | let color_model = match self.primary_index { 58 | 0 => "P3, D65", 59 | 1 => "BT.709", 60 | 2 => "BT.2020", 61 | 5 => "P3, DCI", 62 | 9 => "WCG, D65", 63 | _ => "Custom", 64 | }; 65 | 66 | let eotf = match self.encoding.encoding { 67 | EncodingEnum::Pq => "ST.2084", 68 | EncodingEnum::Linear => "Linear", 69 | EncodingEnum::GammaBT1886 => "BT.1886", 70 | EncodingEnum::GammaDCI => "Gamma2.6", 71 | EncodingEnum::Gamma22 => "Gamma2.2", 72 | EncodingEnum::Gamma24 => "Gamma2.4", 73 | EncodingEnum::Hlg => "HLG", 74 | }; 75 | 76 | self.name = format!( 77 | "{}-nits, {}, {}, Full", 78 | self.peak_brightness, color_model, eotf 79 | ) 80 | } 81 | 82 | pub fn max_u16_from_rpu_pq_u12(u: u16) -> usize { 83 | match u { 84 | // Common cases 85 | 2081 => 100, 86 | 2851 => 600, 87 | 3079 => 1000, 88 | 3696 => 4000, 89 | _ => { 90 | let n = display::pq2l(u as f32 / RPU_PQ_MAX).round(); 91 | // smooth large values 92 | if n > 500.0 { 93 | (n / 50.0 + 1.0) as usize * 50 94 | } else { 95 | n as usize 96 | } 97 | } 98 | } 99 | } 100 | 101 | fn min_f32_from_rpu_pq_u12(u: u16) -> f32 { 102 | match u { 103 | // Common cases 104 | 0 => 0.0, 105 | 7 => 0.0001, 106 | 26 => 0.001, 107 | 62 => 0.005, 108 | _ => display::pq2l(u as f32 / RPU_PQ_MAX), 109 | } 110 | } 111 | 112 | fn get_primary_target(block: &ExtMetadataBlockLevel2, primary: Primaries) -> Self { 113 | let max_luminance = Self::max_u16_from_rpu_pq_u12(block.target_max_pq); 114 | 115 | let primary_index = if let Some(primary) = primary.get_index() { 116 | if max_luminance == 100 { 117 | 1 118 | } else { 119 | primary 120 | } 121 | } else { 122 | 0 123 | }; 124 | 125 | if let Some(target) = 126 | Self::get_display(PREDEFINED_TARGET_DISPLAYS, max_luminance, primary_index) 127 | { 128 | target 129 | } else { 130 | let mut target = Self { 131 | id: block.target_max_pq as usize, 132 | primary_index, 133 | primaries: primary, 134 | peak_brightness: max_luminance, 135 | minimum_brightness: 0.0, 136 | ..Default::default() 137 | }; 138 | 139 | target.update_name(); 140 | target 141 | } 142 | } 143 | 144 | fn get_target(block: &ExtMetadataBlockLevel8) -> Option { 145 | let index = block.target_display_index as usize; 146 | 147 | PREDEFINED_TARGET_DISPLAYS 148 | .iter() 149 | .find(|d| (**d)[0] == index) 150 | .map(|d| Self::from(*d)) 151 | } 152 | 153 | pub fn get_targets(vdr: &VdrDmData) -> Option> { 154 | let mut targets = Vec::new(); 155 | 156 | let primary = Primaries::from(vdr); 157 | 158 | vdr.level_blocks_iter(10).for_each(|b| { 159 | if let ExtMetadataBlock::Level10(b) = b { 160 | let d = Self::from(b); 161 | targets.push(d); 162 | } 163 | }); 164 | 165 | vdr.level_blocks_iter(8).for_each(|b| { 166 | if let ExtMetadataBlock::Level8(b) = b { 167 | if let Some(d) = Self::get_target(b) { 168 | targets.push(d) 169 | } 170 | } 171 | }); 172 | 173 | vdr.level_blocks_iter(2).for_each(|b| { 174 | if let ExtMetadataBlock::Level2(b) = b { 175 | targets.push(Self::get_primary_target(b, primary)) 176 | } 177 | }); 178 | 179 | let targets = targets 180 | .into_iter() 181 | .unique() 182 | .sorted_by_key(|c| c.id) 183 | .collect::>(); 184 | 185 | if targets.is_empty() { 186 | None 187 | } else { 188 | Some(targets) 189 | } 190 | } 191 | 192 | pub fn default_source() -> Self { 193 | Self::from(PREDEFINED_MASTERING_DISPLAYS[0]) 194 | } 195 | 196 | pub fn get_source_or_default(vdr: &VdrDmData) -> Self { 197 | let primary = Primaries::from(vdr); 198 | let primary_index = primary.get_index().unwrap_or(0); 199 | 200 | // Prefer level 6 metadata 201 | let max_luminance = match vdr.get_block(6) { 202 | Some(ExtMetadataBlock::Level6(b)) => b.max_display_mastering_luminance as usize, 203 | _ => Characteristics::max_u16_from_rpu_pq_u12(vdr.source_max_pq), 204 | }; 205 | 206 | if let Some(source) = 207 | Self::get_display(PREDEFINED_MASTERING_DISPLAYS, max_luminance, primary_index) 208 | { 209 | source 210 | } else { 211 | let mut source = Self::default_source(); 212 | 213 | if vdr.get_block(254).is_some() { 214 | // Custom mastering display for CM v4.0 215 | // For convenience, use source_max_pq as custom mastering display id 216 | source.id = vdr.source_max_pq as usize; 217 | source.primaries = primary; 218 | 219 | source.primary_index = if primary.get_index().is_none() { 220 | // Random invalid value 221 | 255 222 | } else { 223 | primary_index 224 | }; 225 | 226 | // BT.709 BT.1886 227 | if primary_index == 1 { 228 | source.encoding = Encoding { 229 | encoding: EncodingEnum::GammaBT1886, 230 | }; 231 | source.peak_brightness = 100; 232 | // Default source (4000-nit) min_brightness is 0.005-nit 233 | } 234 | 235 | source.update_name(); 236 | } 237 | 238 | source 239 | } 240 | } 241 | 242 | /*pub fn update_luminance_range_with_l6_block(&mut self, block: &ExtMetadataBlockLevel6) { 243 | self.peak_brightness = block.max_display_mastering_luminance as usize; 244 | self.minimum_brightness = block.min_display_mastering_luminance as f32 / RPU_L6_MIN_FACTOR; 245 | }*/ 246 | 247 | fn get_display(list: &[[usize; 6]], max_luminance: usize, primary: usize) -> Option { 248 | list.iter() 249 | .find(|d| (**d)[2] == max_luminance && (**d)[1] == primary) 250 | .map(|d| Self::from(*d)) 251 | } 252 | } 253 | 254 | impl Default for Characteristics { 255 | fn default() -> Self { 256 | Self::from(PREDEFINED_TARGET_DISPLAYS[0]) 257 | } 258 | } 259 | 260 | impl From<[usize; 6]> for Characteristics { 261 | fn from(input: [usize; 6]) -> Self { 262 | let mut result = Self { 263 | name: String::new(), 264 | id: input[0], 265 | primary_index: input[1], 266 | primaries: Primaries::get_index_primary(input[1], true).unwrap_or_default(), 267 | peak_brightness: input[2], 268 | minimum_brightness: Self::min_f32_from_rpu_pq_u12(input[3] as u16), 269 | // :( 270 | encoding: Encoding { 271 | encoding: unsafe { transmute::(input[4]) }, 272 | }, 273 | // TODO 274 | diagonal_size: 42, 275 | }; 276 | 277 | result.update_name(); 278 | result 279 | } 280 | } 281 | 282 | impl From<&ExtMetadataBlockLevel10> for Characteristics { 283 | fn from(block: &ExtMetadataBlockLevel10) -> Self { 284 | let mut result = Self { 285 | id: block.target_display_index as usize, 286 | primary_index: block.target_primary_index as usize, 287 | primaries: match block.bytes_size() { 288 | 21 => Primaries::from([ 289 | block.target_primary_red_x, 290 | block.target_primary_red_y, 291 | block.target_primary_green_x, 292 | block.target_primary_green_y, 293 | block.target_primary_blue_x, 294 | block.target_primary_blue_y, 295 | block.target_primary_white_x, 296 | block.target_primary_white_y, 297 | ]), 298 | 5 => Primaries::get_index_primary(block.target_primary_index as usize, true) 299 | .unwrap_or_default(), 300 | _ => unreachable!(), 301 | }, 302 | peak_brightness: Self::max_u16_from_rpu_pq_u12(block.target_max_pq), 303 | minimum_brightness: Self::min_f32_from_rpu_pq_u12(block.target_min_pq), 304 | encoding: Encoding { 305 | encoding: EncodingEnum::Pq, 306 | }, 307 | diagonal_size: 42, 308 | ..Default::default() 309 | }; 310 | 311 | result.update_name(); 312 | result 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/metadata/display/chromaticity.rs: -------------------------------------------------------------------------------- 1 | use std::array; 2 | use std::hash::{Hash, Hasher}; 3 | 4 | use crate::display::CHROMATICITY_EPSILON; 5 | 6 | #[derive(Clone, Copy, Default, Debug)] 7 | pub struct Chromaticity(pub(crate) [f32; 2]); 8 | 9 | impl PartialEq for Chromaticity { 10 | fn eq(&self, other: &Self) -> bool { 11 | let dx = (self.0[0] - other.0[0]).abs(); 12 | let dy = (self.0[1] - other.0[1]).abs(); 13 | 14 | dx <= CHROMATICITY_EPSILON && dy <= CHROMATICITY_EPSILON 15 | } 16 | } 17 | 18 | impl Eq for Chromaticity {} 19 | 20 | impl Hash for Chromaticity { 21 | fn hash(&self, state: &mut H) { 22 | self.0[0].to_bits().hash(state); 23 | self.0[1].to_bits().hash(state); 24 | } 25 | } 26 | 27 | impl IntoIterator for Chromaticity { 28 | type Item = f32; 29 | type IntoIter = array::IntoIter; 30 | 31 | fn into_iter(self) -> Self::IntoIter { 32 | self.0.into_iter() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/metadata/display/mod.rs: -------------------------------------------------------------------------------- 1 | pub use characteristics::Characteristics; 2 | pub use chromaticity::Chromaticity; 3 | pub use primary::Primaries; 4 | 5 | mod characteristics; 6 | mod chromaticity; 7 | mod primary; 8 | 9 | pub const RPU_PQ_MAX: f32 = 4095.0; 10 | pub const CHROMATICITY_EPSILON: f32 = 1.0 / 32767.0; 11 | // pub const RPU_L6_MIN_FACTOR: f32 = 10000.0; 12 | 13 | #[rustfmt::skip] 14 | pub const PREDEFINED_COLORSPACE_PRIMARIES: &[[f32; 8]] = &[ 15 | [0.68 , 0.32 , 0.265 , 0.69 , 0.15 , 0.06 , 0.3127 , 0.329 ], // 0, DCI-P3 D65 16 | [0.64 , 0.33 , 0.30 , 0.60 , 0.15 , 0.06 , 0.3127 , 0.329 ], // 1, BT.709 17 | [0.708 , 0.292 , 0.170 , 0.797 , 0.131 , 0.046 , 0.3127 , 0.329 ], // 2, BT.2020 18 | [0.63 , 0.34 , 0.31 , 0.595 , 0.155 , 0.07 , 0.3127 , 0.329 ], // 3, BT.601 NTSC / SMPTE-C 19 | [0.64 , 0.33 , 0.29 , 0.60 , 0.15 , 0.06 , 0.3127 , 0.329 ], // 4, BT.601 PAL / BT.470 BG 20 | [0.68 , 0.32 , 0.265 , 0.69 , 0.15 , 0.06 , 0.314 , 0.351 ], // 5, DCI-P3 21 | [0.7347, 0.2653, 0.0 , 1.0 , 0.0001,-0.077 , 0.32168, 0.33767], // 6, ACES 22 | [0.73 , 0.28 , 0.14 , 0.855 , 0.10 ,-0.05 , 0.3127 , 0.329 ], // 7, S-Gamut 23 | [0.766 , 0.275 , 0.225 , 0.80 , 0.089 ,-0.087 , 0.3127 , 0.329 ], // 8, S-Gamut-3.Cine 24 | [0.693 , 0.304 , 0.208 , 0.761 , 0.1467, 0.0527, 0.3127 , 0.329 ], // 9, DolbyCinemaWCG 25 | [0.6867, 0.3085, 0.231 , 0.69 , 0.1489, 0.0638, 0.3127 , 0.329 ], // 10, Canon DP-V2420 26 | [0.6781, 0.3189, 0.2365, 0.7048, 0.141 , 0.0489, 0.3127 , 0.329 ], // 11, Sony BVM-X300 27 | [0.68 , 0.32 , 0.265 , 0.69 , 0.15 , 0.06 , 0.3127 , 0.329 ], // 12, DCI-P3 D65 28 | [0.7042, 0.294 , 0.2271, 0.725 , 0.1416, 0.0516, 0.3127 , 0.329 ], // 13, Pulsar 29 | [0.6745, 0.310 , 0.2212, 0.7109, 0.152 , 0.0619, 0.3127 , 0.329 ], // 14, Eizo CG3145 30 | [0.6805, 0.3191, 0.2522, 0.6702, 0.1397, 0.0554, 0.3127 , 0.329 ], // 15, LG2017 B7 OLED 31 | [0.6838, 0.3085, 0.2709, 0.6378, 0.1478, 0.0589, 0.3127 , 0.329 ], // 16, PRM32FHD-QT 32 | [0.6753, 0.3193, 0.2636, 0.6835, 0.1521, 0.0627, 0.3127 , 0.329 ], // 17, Apollo PRD DCI-P3 Model 33 | [0.6981, 0.2898, 0.1814, 0.7189, 0.1517, 0.0567, 0.3127 , 0.329 ], // 18, Apollo PRD WCG Model 34 | ]; 35 | 36 | /// Format: `[id, primary_index, peak_brightness, min_pq, eotf(enum usize), range]` 37 | #[rustfmt::skip] 38 | pub const PREDEFINED_MASTERING_DISPLAYS: &[[usize; 6]] = &[ 39 | [ 7, 0, 4000, 62, 0, 0], // Default: 4000-nit, P3, D65, ST.2084 40 | [ 8, 2, 4000, 62, 0, 0], 41 | [20, 0, 1000, 7, 0, 0], 42 | [21, 2, 1000, 7, 0, 0], 43 | [30, 0, 2000, 7, 0, 0], 44 | [31, 2, 2000, 7, 0, 0], 45 | ]; 46 | 47 | // pub const CMV29_MASTERING_DISPLAYS_LIST: &[u8] = &[7, 8, 20, 21, 30, 31]; 48 | 49 | /// Only HOME targets are included. 50 | /// 51 | /// Format: `[id, primary_index, peak_brightness, min_pq, eotf(enum usize), range]` 52 | #[rustfmt::skip] 53 | pub const PREDEFINED_TARGET_DISPLAYS: &[[usize; 6]] = &[ 54 | [ 1, 1, 100, 62, 2, 0], 55 | [ 24, 0, 300, 0, 0, 0], 56 | [ 25, 2, 300, 0, 0, 0], 57 | [ 27, 0, 600, 0, 0, 0], 58 | [ 28, 2, 600, 0, 0, 0], 59 | [ 37, 0, 2000, 0, 0, 0], 60 | [ 38, 2, 2000, 0, 0, 0], 61 | [ 48, 0, 1000, 0, 0, 0], 62 | [ 49, 2, 1000, 0, 0, 0], 63 | ]; 64 | 65 | // pub const CMV29_TARGET_DISPLAYS_LIST: &[u8] = &[1, 27, 28, 37, 38, 48, 49]; 66 | 67 | const ST2084_Y_MAX: f32 = 10000.0; 68 | const ST2084_M1: f32 = 2610.0 / 16384.0; 69 | const ST2084_M2: f32 = (2523.0 / 4096.0) * 128.0; 70 | const ST2084_C1: f32 = 3424.0 / 4096.0; 71 | const ST2084_C2: f32 = (2413.0 / 4096.0) * 32.0; 72 | const ST2084_C3: f32 = (2392.0 / 4096.0) * 32.0; 73 | 74 | pub fn pq2l(pq: f32) -> f32 { 75 | let y = ((pq.powf(1.0 / ST2084_M2) - ST2084_C1) 76 | / (ST2084_C2 - ST2084_C3 * pq.powf(1.0 / ST2084_M2))) 77 | .powf(1.0 / ST2084_M1); 78 | 79 | y * ST2084_Y_MAX 80 | } 81 | 82 | pub fn find_target_id(max: usize, primary: usize) -> Option { 83 | get_display_id(PREDEFINED_TARGET_DISPLAYS, max, primary) 84 | } 85 | 86 | fn get_display_id(list: &[[usize; 6]], max_luminance: usize, primary: usize) -> Option { 87 | list.iter() 88 | .find(|t| (**t)[2] == max_luminance && (**t)[1] == primary) 89 | .map(|d| d[0]) 90 | } 91 | -------------------------------------------------------------------------------- /src/metadata/display/primary.rs: -------------------------------------------------------------------------------- 1 | use std::array; 2 | 3 | use dolby_vision::rpu::extension_metadata::blocks::{ 4 | ExtMetadataBlock, ExtMetadataBlockInfo, ExtMetadataBlockLevel9, 5 | }; 6 | use dolby_vision::rpu::vdr_dm_data::VdrDmData; 7 | 8 | use crate::display::chromaticity::Chromaticity; 9 | use crate::display::PREDEFINED_COLORSPACE_PRIMARIES; 10 | 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 12 | pub struct Primaries { 13 | pub red: Chromaticity, 14 | pub green: Chromaticity, 15 | pub blue: Chromaticity, 16 | pub white_point: Chromaticity, 17 | } 18 | 19 | impl Primaries { 20 | pub fn f32_from_rpu_u16(u: u16) -> f32 { 21 | (match u { 22 | 0..=32767 => u as f32, 23 | // input value 32768 is undefined, should not happen 24 | _ => u as f32 - 65536.0, 25 | }) / 32767.0 26 | } 27 | 28 | pub fn get_index(&self) -> Option { 29 | PREDEFINED_COLORSPACE_PRIMARIES 30 | .iter() 31 | .enumerate() 32 | .find(|(_, p)| Self::from(**p) == *self) 33 | .map(|(i, _)| i) 34 | } 35 | 36 | pub fn get_index_primary(index: usize, is_target: bool) -> Option { 37 | let index_max = PREDEFINED_COLORSPACE_PRIMARIES.len(); 38 | let index = if index >= index_max || is_target && index > 8 { 39 | None 40 | } else { 41 | Some(index) 42 | }; 43 | 44 | index.map(|index| Primaries::from(PREDEFINED_COLORSPACE_PRIMARIES[index])) 45 | } 46 | } 47 | 48 | impl IntoIterator for Primaries { 49 | type Item = f32; 50 | type IntoIter = array::IntoIter; 51 | 52 | fn into_iter(self) -> Self::IntoIter { 53 | let mut result = [0.0; 8]; 54 | 55 | // We know size is 8 56 | let vec = [self.red, self.green, self.blue, self.white_point] 57 | .into_iter() 58 | .flatten() 59 | .collect::>(); 60 | 61 | for (i, v) in result.iter_mut().zip(vec) { 62 | *i = v 63 | } 64 | 65 | result.into_iter() 66 | } 67 | } 68 | 69 | impl Default for Primaries { 70 | fn default() -> Self { 71 | Self::from(PREDEFINED_COLORSPACE_PRIMARIES[0]) 72 | } 73 | } 74 | 75 | impl From<[f32; 8]> for Primaries { 76 | fn from(p: [f32; 8]) -> Self { 77 | Self { 78 | red: Chromaticity([p[0], p[1]]), 79 | green: Chromaticity([p[2], p[3]]), 80 | blue: Chromaticity([p[4], p[5]]), 81 | white_point: Chromaticity([p[6], p[7]]), 82 | } 83 | } 84 | } 85 | 86 | impl From<[u16; 8]> for Primaries { 87 | fn from(p: [u16; 8]) -> Self { 88 | let mut result = [0.0f32; 8]; 89 | 90 | for (i, j) in result.iter_mut().zip(p) { 91 | *i = Self::f32_from_rpu_u16(j) 92 | } 93 | 94 | Primaries::from(result) 95 | } 96 | } 97 | 98 | impl From<&ExtMetadataBlockLevel9> for Primaries { 99 | fn from(block: &ExtMetadataBlockLevel9) -> Self { 100 | match block.bytes_size() { 101 | 1 => Primaries::get_index_primary(block.source_primary_index as usize, false) 102 | .unwrap_or_default(), 103 | 17 => Primaries::from([ 104 | block.source_primary_red_x, 105 | block.source_primary_red_y, 106 | block.source_primary_green_x, 107 | block.source_primary_green_y, 108 | block.source_primary_blue_x, 109 | block.source_primary_blue_y, 110 | block.source_primary_white_x, 111 | block.source_primary_white_y, 112 | ]), 113 | _ => unreachable!(), 114 | } 115 | } 116 | } 117 | 118 | // For source display 119 | impl From<&VdrDmData> for Primaries { 120 | fn from(vdr: &VdrDmData) -> Self { 121 | vdr.get_block(9) 122 | .and_then(|b| match b { 123 | ExtMetadataBlock::Level9(b) => Some(Self::from(b)), 124 | _ => None, 125 | }) 126 | .unwrap_or_default() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/metadata/levels/level1.rs: -------------------------------------------------------------------------------- 1 | use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel1; 2 | use serde::Serialize; 3 | 4 | use crate::metadata::MDFType::*; 5 | use crate::metadata::{IntoCMV29, MDFType}; 6 | 7 | use super::ImageCharacter; 8 | 9 | #[derive(Debug, Clone, PartialEq, Serialize)] 10 | pub struct Level1 { 11 | #[serde(rename = "@level")] 12 | pub level: u8, 13 | #[serde(rename = "ImageCharacter")] 14 | pub image_character: MDFType, 15 | } 16 | 17 | impl From<&ExtMetadataBlockLevel1> for Level1 { 18 | fn from(block: &ExtMetadataBlockLevel1) -> Self { 19 | Self { 20 | level: 1, 21 | image_character: CMV40(block.into()), 22 | } 23 | } 24 | } 25 | 26 | impl IntoCMV29 for Level1 { 27 | fn into_cmv29(self) -> Self { 28 | Self { 29 | level: 1, 30 | image_character: self.image_character.into_cmv29(), 31 | } 32 | } 33 | } 34 | 35 | impl Default for Level1 { 36 | fn default() -> Self { 37 | Self { 38 | level: 0, 39 | image_character: CMV40(ImageCharacter([0.0; 3])), 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/metadata/levels/level11.rs: -------------------------------------------------------------------------------- 1 | use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel11; 2 | use serde::Serialize; 3 | 4 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] 5 | pub struct Level11 { 6 | #[serde(rename = "@level")] 7 | pub level: u8, 8 | #[serde(rename = "ContentType")] 9 | pub content_type: u8, 10 | #[serde(rename = "IntendedWhitePoint")] 11 | pub intended_white_point: u8, 12 | // FIXME: Rename 13 | #[serde(rename = "ExtensionProperties")] 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | pub extension_properties: Option, 16 | } 17 | 18 | impl Default for Level11 { 19 | fn default() -> Self { 20 | Self { 21 | level: 11, 22 | content_type: 1, // Movies 23 | intended_white_point: 0, 24 | extension_properties: None, 25 | } 26 | } 27 | } 28 | 29 | impl From<&ExtMetadataBlockLevel11> for Level11 { 30 | fn from(block: &ExtMetadataBlockLevel11) -> Self { 31 | Self { 32 | level: 11, 33 | content_type: block.content_type, 34 | intended_white_point: block.whitepoint, 35 | // TODO: byte3? 36 | extension_properties: None, 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/metadata/levels/level2.rs: -------------------------------------------------------------------------------- 1 | use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel2; 2 | use serde::ser::SerializeStruct; 3 | use serde::{Serialize, Serializer}; 4 | 5 | use super::TrimSixField; 6 | use crate::display::find_target_id; 7 | use crate::f32_from_rpu_u12_with_bias; 8 | use crate::metadata::display::Characteristics; 9 | use crate::metadata::MDFType::*; 10 | use crate::metadata::{IntoCMV29, MDFType, WithTid}; 11 | 12 | #[derive(Debug, Clone, PartialEq)] 13 | pub struct Level2 { 14 | pub level: u8, 15 | pub tid: usize, 16 | // Format: 0 0 0 f32 f32 f32 f32 f32 f32 17 | pub trim: MDFType, 18 | } 19 | 20 | impl WithTid for Level2 { 21 | fn tid(&self) -> usize { 22 | self.tid 23 | } 24 | 25 | fn with_tid(tid: usize) -> Self { 26 | Self { 27 | level: 2, 28 | tid, 29 | trim: Default::default(), 30 | } 31 | } 32 | } 33 | 34 | impl Serialize for Level2 { 35 | fn serialize(&self, serializer: S) -> Result 36 | where 37 | S: Serializer, 38 | { 39 | let mut trim = self.trim; 40 | let mut new_trim = [0.0; 9]; 41 | new_trim 42 | .iter_mut() 43 | .skip(3) 44 | .zip(self.trim.into_inner().0) 45 | .for_each(|(t, s)| *t = s); 46 | 47 | let mut state = serializer.serialize_struct("Level2", 3)?; 48 | 49 | state.serialize_field("@level", &self.level)?; 50 | state.serialize_field("TID", &self.tid)?; 51 | state.serialize_field("Trim", &trim.with_new_inner(new_trim))?; 52 | 53 | state.end() 54 | } 55 | } 56 | 57 | impl IntoCMV29 for Level2 { 58 | fn into_cmv29(self) -> Self { 59 | Self { 60 | level: 2, 61 | tid: self.tid, 62 | trim: self.trim.into_cmv29(), 63 | } 64 | } 65 | } 66 | 67 | impl Level2 { 68 | pub fn with_primary_index(block: &ExtMetadataBlockLevel2, primary: Option) -> Self { 69 | // identical definition for all negative values, use -1 for v2.0.5+ 70 | let ms_weight = if block.ms_weight < 0 { 71 | -1.0 72 | } else { 73 | f32_from_rpu_u12_with_bias(block.ms_weight as u16) 74 | }; 75 | 76 | let luminance = Characteristics::max_u16_from_rpu_pq_u12(block.target_max_pq); 77 | let primary = if luminance == 100 { 78 | 1 79 | } else { 80 | // P3 D65 81 | primary.unwrap_or(0) 82 | }; 83 | 84 | // For convenience, use target_max_pq as Level2 custom target display id 85 | let tid = find_target_id(luminance, primary).unwrap_or(block.target_max_pq as usize); 86 | 87 | let mut trim = TrimSixField([ 88 | f32_from_rpu_u12_with_bias(block.trim_slope), 89 | f32_from_rpu_u12_with_bias(block.trim_offset), 90 | f32_from_rpu_u12_with_bias(block.trim_power), 91 | f32_from_rpu_u12_with_bias(block.trim_chroma_weight), 92 | f32_from_rpu_u12_with_bias(block.trim_saturation_gain), 93 | ms_weight, 94 | ]); 95 | 96 | trim.sop_to_lgg(); 97 | 98 | Self { 99 | level: 2, 100 | tid, 101 | trim: CMV40(trim), 102 | } 103 | } 104 | } 105 | 106 | impl From<&ExtMetadataBlockLevel2> for Level2 { 107 | fn from(block: &ExtMetadataBlockLevel2) -> Self { 108 | // P3 D65 109 | Self::with_primary_index(block, None) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/metadata/levels/level254.rs: -------------------------------------------------------------------------------- 1 | use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel254; 2 | use serde::Serialize; 3 | 4 | #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 5 | pub struct Level254 { 6 | #[serde(rename = "@level")] 7 | pub level: u8, 8 | #[serde(rename = "DMMode")] 9 | pub dm_mode: u8, 10 | #[serde(rename = "DMVersion")] 11 | pub dm_version: u8, 12 | // Format: u8 u8 13 | #[serde(rename = "CMVersion")] 14 | pub cm_version: String, 15 | } 16 | 17 | impl Default for Level254 { 18 | fn default() -> Self { 19 | (&ExtMetadataBlockLevel254::cmv402_default()).into() 20 | } 21 | } 22 | 23 | impl From<&ExtMetadataBlockLevel254> for Level254 { 24 | fn from(block: &ExtMetadataBlockLevel254) -> Self { 25 | Self { 26 | level: 254, 27 | dm_mode: block.dm_mode, 28 | dm_version: block.dm_version_index, 29 | // FIXME: Hardcode 30 | cm_version: "4 1".to_string(), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/metadata/levels/level3.rs: -------------------------------------------------------------------------------- 1 | use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel3; 2 | use serde::Serialize; 3 | 4 | use crate::MDFType::CMV40; 5 | use crate::{ImageCharacter, MDFType}; 6 | 7 | #[derive(Debug, Clone, PartialEq, Serialize)] 8 | pub struct Level3 { 9 | #[serde(rename = "@level")] 10 | pub level: u8, 11 | // Format: f32 f32 f32 12 | #[serde(rename = "L1Offset")] 13 | pub l1_offset: MDFType, 14 | } 15 | 16 | impl Default for Level3 { 17 | fn default() -> Self { 18 | Self { 19 | level: 3, 20 | l1_offset: Default::default(), 21 | } 22 | } 23 | } 24 | 25 | impl From<&ExtMetadataBlockLevel3> for Level3 { 26 | fn from(block: &ExtMetadataBlockLevel3) -> Self { 27 | Self { 28 | level: 3, 29 | l1_offset: CMV40(ImageCharacter::from(block)), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/metadata/levels/level5.rs: -------------------------------------------------------------------------------- 1 | use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel5; 2 | use serde::Serialize; 3 | use std::cmp::Ordering; 4 | 5 | use crate::metadata::levels::UHD_CANVAS; 6 | use crate::MDFType::CMV40; 7 | use crate::{IntoCMV29, MDFType}; 8 | 9 | use super::AspectRatio; 10 | 11 | #[derive(Debug, Clone, Serialize, Hash, PartialEq, Eq)] 12 | pub struct Level5 { 13 | #[serde(rename = "@level")] 14 | pub level: u8, 15 | // Format: f32 f32 16 | #[serde(rename = "AspectRatios")] 17 | pub aspect_ratio: MDFType, 18 | } 19 | 20 | impl Level5 { 21 | pub fn get_ar(&self) -> (f32, f32) { 22 | let ar = self.aspect_ratio.into_inner().0; 23 | (ar[0], ar[1]) 24 | } 25 | } 26 | 27 | // For convenience, it assumes the canvas is standard UHD 28 | impl From<&ExtMetadataBlockLevel5> for Level5 { 29 | fn from(block: &ExtMetadataBlockLevel5) -> Self { 30 | Self::with_canvas(Some(block), UHD_CANVAS) 31 | } 32 | } 33 | 34 | impl From for Level5 { 35 | fn from(ar: f32) -> Self { 36 | Self { 37 | level: 5, 38 | aspect_ratio: CMV40(AspectRatio([ar, ar])), 39 | } 40 | } 41 | } 42 | 43 | impl IntoCMV29 for Level5 { 44 | fn into_cmv29(self) -> Self { 45 | Self { 46 | level: 5, 47 | aspect_ratio: self.aspect_ratio.into_cmv29(), 48 | } 49 | } 50 | } 51 | 52 | impl Level5 { 53 | pub fn with_canvas(block: Option<&ExtMetadataBlockLevel5>, canvas: (usize, usize)) -> Self { 54 | let (width, height) = canvas; 55 | let canvas_ar = width as f32 / height as f32; 56 | 57 | let image_ar = if let Some(block) = block { 58 | let horizontal_crop = block.active_area_left_offset + block.active_area_right_offset; 59 | let vertical_crop = block.active_area_top_offset + block.active_area_bottom_offset; 60 | 61 | if horizontal_crop > 0 { 62 | (width as f32 - horizontal_crop as f32) / height as f32 63 | } else { 64 | // Ok because only one of the crop types will be 0 65 | width as f32 / (height as f32 - vertical_crop as f32) 66 | } 67 | } else { 68 | canvas_ar 69 | }; 70 | 71 | Self { 72 | level: 5, 73 | aspect_ratio: CMV40(AspectRatio([canvas_ar, image_ar])), 74 | } 75 | } 76 | } 77 | 78 | impl PartialOrd for Level5 { 79 | fn partial_cmp(&self, other: &Self) -> Option { 80 | Some(self.cmp(other)) 81 | } 82 | } 83 | 84 | impl Ord for Level5 { 85 | fn cmp(&self, other: &Self) -> Ordering { 86 | self.aspect_ratio 87 | .into_inner() 88 | .cmp(&other.aspect_ratio.into_inner()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/metadata/levels/level6.rs: -------------------------------------------------------------------------------- 1 | use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel6; 2 | use serde::Serialize; 3 | 4 | #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 5 | pub struct Level6 { 6 | #[serde(rename = "@level")] 7 | pub level: usize, 8 | #[serde(rename = "MaxCLL")] 9 | pub max_cll: usize, 10 | #[serde(rename = "MaxFALL")] 11 | pub max_fall: usize, 12 | } 13 | 14 | impl Default for Level6 { 15 | fn default() -> Self { 16 | Self { 17 | level: 6, 18 | max_cll: 0, 19 | max_fall: 0, 20 | } 21 | } 22 | } 23 | 24 | impl From<&ExtMetadataBlockLevel6> for Level6 { 25 | fn from(block: &ExtMetadataBlockLevel6) -> Self { 26 | Self { 27 | level: 6, 28 | max_cll: block.max_content_light_level as usize, 29 | max_fall: block.max_frame_average_light_level as usize, 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/metadata/levels/level8.rs: -------------------------------------------------------------------------------- 1 | use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel8; 2 | use serde::Serialize; 3 | 4 | use super::TrimSixField; 5 | use crate::metadata::WithTid; 6 | use crate::MDFType; 7 | use crate::MDFType::CMV40; 8 | 9 | #[derive(Debug, Clone, PartialEq, Serialize)] 10 | pub struct Level8 { 11 | #[serde(rename = "@level")] 12 | pub level: u8, 13 | #[serde(rename = "TID")] 14 | pub tid: u8, 15 | // Format: f32 f32 f32 f32 f32 f32 16 | #[serde(rename = "L8Trim")] 17 | pub l8_trim: MDFType, 18 | #[serde(rename = "MidContrastBias")] 19 | pub mid_contrast_bias: f32, 20 | #[serde(rename = "HighlightClipping")] 21 | pub highlight_clipping: f32, 22 | // Format: f32 f32 f32 f32 f32 f32 23 | #[serde(rename = "SaturationVectorField")] 24 | pub sat_vector_field: MDFType, 25 | // Format: f32 f32 f32 f32 f32 f32 26 | #[serde(rename = "HueVectorField")] 27 | pub hue_vector_field: MDFType, 28 | } 29 | 30 | impl WithTid for Level8 { 31 | fn tid(&self) -> usize { 32 | self.tid as usize 33 | } 34 | 35 | fn with_tid(tid: usize) -> Self { 36 | Self { 37 | level: 8, 38 | tid: tid as u8, 39 | l8_trim: Default::default(), 40 | mid_contrast_bias: 0.0, 41 | highlight_clipping: 0.0, 42 | sat_vector_field: Default::default(), 43 | hue_vector_field: Default::default(), 44 | } 45 | } 46 | } 47 | 48 | impl From<&ExtMetadataBlockLevel8> for Level8 { 49 | fn from(block: &ExtMetadataBlockLevel8) -> Self { 50 | let mut trim = TrimSixField([ 51 | crate::f32_from_rpu_u12_with_bias(block.trim_slope), 52 | crate::f32_from_rpu_u12_with_bias(block.trim_offset), 53 | crate::f32_from_rpu_u12_with_bias(block.trim_power), 54 | crate::f32_from_rpu_u12_with_bias(block.trim_chroma_weight), 55 | crate::f32_from_rpu_u12_with_bias(block.trim_saturation_gain), 56 | crate::f32_from_rpu_u12_with_bias(block.ms_weight), 57 | ]); 58 | 59 | trim.sop_to_lgg(); 60 | 61 | Self { 62 | level: 8, 63 | tid: block.target_display_index, 64 | l8_trim: CMV40(trim), 65 | mid_contrast_bias: crate::f32_from_rpu_u12_with_bias(block.target_mid_contrast), 66 | highlight_clipping: crate::f32_from_rpu_u12_with_bias(block.clip_trim), 67 | sat_vector_field: CMV40(TrimSixField([ 68 | crate::f32_from_rpu_u8_with_bias(block.saturation_vector_field0), 69 | crate::f32_from_rpu_u8_with_bias(block.saturation_vector_field1), 70 | crate::f32_from_rpu_u8_with_bias(block.saturation_vector_field2), 71 | crate::f32_from_rpu_u8_with_bias(block.saturation_vector_field3), 72 | crate::f32_from_rpu_u8_with_bias(block.saturation_vector_field4), 73 | crate::f32_from_rpu_u8_with_bias(block.saturation_vector_field5), 74 | ])), 75 | hue_vector_field: CMV40(TrimSixField([ 76 | crate::f32_from_rpu_u8_with_bias(block.hue_vector_field0), 77 | crate::f32_from_rpu_u8_with_bias(block.hue_vector_field1), 78 | crate::f32_from_rpu_u8_with_bias(block.hue_vector_field2), 79 | crate::f32_from_rpu_u8_with_bias(block.hue_vector_field3), 80 | crate::f32_from_rpu_u8_with_bias(block.hue_vector_field4), 81 | crate::f32_from_rpu_u8_with_bias(block.hue_vector_field5), 82 | ])), 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/metadata/levels/level9.rs: -------------------------------------------------------------------------------- 1 | use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel9; 2 | use serde::Serialize; 3 | 4 | use crate::MDFType::CMV40; 5 | use crate::{display, MDFType}; 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 8 | pub struct Level9 { 9 | #[serde(rename = "@level")] 10 | pub level: u8, 11 | // 255 12 | #[serde(rename = "SourceColorModel")] 13 | pub source_color_model: u8, 14 | // Format: f32 f32 f32 f32 f32 f32 f32 f32 15 | #[serde(rename = "SourceColorPrimary")] 16 | pub source_color_primary: MDFType, 17 | } 18 | 19 | impl From for Level9 { 20 | fn from(p: display::Primaries) -> Self { 21 | Self { 22 | level: 9, 23 | source_color_model: 255, 24 | source_color_primary: CMV40(p), 25 | } 26 | } 27 | } 28 | 29 | impl From<&ExtMetadataBlockLevel9> for Level9 { 30 | fn from(block: &ExtMetadataBlockLevel9) -> Self { 31 | display::Primaries::from(block).into() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/metadata/levels/mod.rs: -------------------------------------------------------------------------------- 1 | use std::array; 2 | use std::cmp::Ordering; 3 | use std::hash::{Hash, Hasher}; 4 | use std::ops::Add; 5 | 6 | use dolby_vision::rpu::extension_metadata::blocks::{ 7 | ExtMetadataBlockLevel1, ExtMetadataBlockLevel3, 8 | }; 9 | 10 | pub use level1::Level1; 11 | pub use level11::Level11; 12 | pub use level2::Level2; 13 | pub use level254::Level254; 14 | pub use level3::Level3; 15 | pub use level5::Level5; 16 | pub use level6::Level6; 17 | pub use level8::Level8; 18 | pub use level9::Level9; 19 | 20 | mod level1; 21 | mod level11; 22 | mod level2; 23 | mod level254; 24 | mod level3; 25 | mod level5; 26 | mod level6; 27 | mod level8; 28 | mod level9; 29 | 30 | pub const RPU_PQ_MAX: f32 = 4095.0; 31 | // pub const RPU_PQ_OFFSET: f32 = 2048.0; 32 | pub const RPU_U8_BIAS: f32 = 128.0; 33 | pub const RPU_U12_BIAS: f32 = 2048.0; 34 | pub const UHD_WIDTH: usize = 3840; 35 | pub const UHD_HEIGHT: usize = 2160; 36 | pub const UHD_CANVAS: (usize, usize) = (UHD_WIDTH, UHD_HEIGHT); 37 | pub const UHD_AR: f32 = 16.0 / 9.0; 38 | 39 | pub fn f32_from_rpu_u12_with_bias(u: u16) -> f32 { 40 | let u = if u == 4095 { 4096 } else { u }; 41 | 42 | (u as f32 - RPU_U12_BIAS) / RPU_U12_BIAS 43 | } 44 | 45 | pub fn f32_from_rpu_u8_with_bias(u: u8) -> f32 { 46 | let u = if u == 255 { 256 } else { u as u16 }; 47 | 48 | (u as f32 - RPU_U8_BIAS) / RPU_U8_BIAS 49 | } 50 | 51 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 52 | pub struct TrimSixField([f32; 6]); 53 | 54 | impl TrimSixField { 55 | pub fn sop_to_lgg(&mut self) { 56 | let slope = self.0[0]; 57 | let offset = self.0[1]; 58 | let power = self.0[2]; 59 | 60 | let gain = slope + offset; 61 | let lift = 2.0 * offset / (gain + 2.0); 62 | let gamma = (4.0 / (power + 2.0) - 2.0).min(1.0); 63 | 64 | self.0[0] = lift.clamp(-1.0, 1.0); 65 | self.0[1] = gain.clamp(-1.0, 1.0); 66 | self.0[2] = gamma; 67 | } 68 | } 69 | 70 | impl IntoIterator for TrimSixField { 71 | type Item = f32; 72 | type IntoIter = array::IntoIter; 73 | 74 | fn into_iter(self) -> Self::IntoIter { 75 | self.0.into_iter() 76 | } 77 | } 78 | 79 | #[derive(Clone, Copy, Debug, PartialEq, Default)] 80 | pub struct ImageCharacter([f32; 3]); 81 | 82 | impl ImageCharacter { 83 | pub fn new() -> Self { 84 | Self([0.0; 3]) 85 | } 86 | } 87 | 88 | impl Add for ImageCharacter { 89 | type Output = ImageCharacter; 90 | 91 | fn add(self, rhs: Self) -> Self::Output { 92 | let mut result = Self::new(); 93 | let Self(lhs) = self; 94 | let Self(rhs) = rhs; 95 | 96 | for ((i, a), b) in result.0.iter_mut().zip(&lhs).zip(&rhs) { 97 | *i = a + b; 98 | } 99 | 100 | result 101 | } 102 | } 103 | 104 | impl From<&ExtMetadataBlockLevel1> for ImageCharacter { 105 | fn from(block: &ExtMetadataBlockLevel1) -> Self { 106 | Self([ 107 | block.min_pq as f32 / RPU_PQ_MAX, 108 | block.avg_pq as f32 / RPU_PQ_MAX, 109 | block.max_pq as f32 / RPU_PQ_MAX, 110 | ]) 111 | } 112 | } 113 | 114 | impl From<&ExtMetadataBlockLevel3> for ImageCharacter { 115 | fn from(block: &ExtMetadataBlockLevel3) -> Self { 116 | Self([ 117 | f32_from_rpu_u12_with_bias(block.min_pq_offset), 118 | f32_from_rpu_u12_with_bias(block.avg_pq_offset), 119 | f32_from_rpu_u12_with_bias(block.max_pq_offset), 120 | ]) 121 | } 122 | } 123 | 124 | impl IntoIterator for ImageCharacter { 125 | type Item = f32; 126 | type IntoIter = array::IntoIter; 127 | 128 | fn into_iter(self) -> Self::IntoIter { 129 | self.0.into_iter() 130 | } 131 | } 132 | 133 | #[derive(Clone, Copy, Debug)] 134 | pub struct AspectRatio([f32; 2]); 135 | 136 | impl IntoIterator for AspectRatio { 137 | type Item = f32; 138 | type IntoIter = array::IntoIter; 139 | 140 | fn into_iter(self) -> Self::IntoIter { 141 | self.0.into_iter() 142 | } 143 | } 144 | 145 | impl PartialEq for AspectRatio { 146 | fn eq(&self, other: &Self) -> bool { 147 | let self_ar = self.0[1] / self.0[0]; 148 | let other_ar = other.0[1] / other.0[0]; 149 | 150 | self_ar.to_bits() == other_ar.to_bits() 151 | } 152 | } 153 | 154 | #[allow(clippy::non_canonical_partial_ord_impl)] 155 | impl PartialOrd for AspectRatio { 156 | fn partial_cmp(&self, other: &Self) -> Option { 157 | let self_ar = self.0[1] / self.0[0]; 158 | let other_ar = other.0[1] / other.0[0]; 159 | 160 | self_ar.partial_cmp(&other_ar) 161 | } 162 | } 163 | 164 | // NaN should not happen for AspectRatio 165 | impl Ord for AspectRatio { 166 | fn cmp(&self, other: &Self) -> Ordering { 167 | self.partial_cmp(other).unwrap_or(Ordering::Equal) 168 | } 169 | } 170 | 171 | impl Eq for AspectRatio {} 172 | 173 | impl Hash for AspectRatio { 174 | fn hash(&self, state: &mut H) { 175 | self.0.iter().for_each(|f| f.to_bits().hash(state)) 176 | } 177 | } 178 | 179 | #[cfg(test)] 180 | mod tests { 181 | use super::*; 182 | #[test] 183 | fn test_sop_to_lgg() { 184 | let mut trim = TrimSixField([ 185 | f32_from_rpu_u12_with_bias(4095), 186 | f32_from_rpu_u12_with_bias(0), 187 | 0.0, 188 | 0.0, 189 | 0.0, 190 | 0.0, 191 | ]); 192 | trim.sop_to_lgg(); 193 | 194 | assert_eq!(trim.0, [-1.0, 0.0, 0.0, 0.0, 0.0, 0.0]); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/metadata/mod.rs: -------------------------------------------------------------------------------- 1 | use std::array; 2 | use std::fmt::{Debug, Display, Formatter}; 3 | 4 | use chrono::{SecondsFormat, Utc}; 5 | use itertools::Itertools; 6 | use serde::{Serialize, Serializer}; 7 | // use serde_aux::prelude::serde_introspect; 8 | use uuid::Uuid; 9 | 10 | use display::Chromaticity; 11 | 12 | use crate::MDFType::{CMV29, CMV40}; 13 | 14 | pub mod cmv29; 15 | pub mod cmv40; 16 | pub mod display; 17 | pub mod levels; 18 | 19 | pub const XML_PREFIX: &str = "\n"; 20 | pub const DOLBY_XMLNS_PREFIX: &str = "http://www.dolby.com/schemas/dvmd/"; 21 | 22 | /// UUID v4. 23 | #[derive(Debug, Clone, Serialize)] 24 | pub struct UUIDv4(String); 25 | 26 | impl UUIDv4 { 27 | pub fn new() -> Self { 28 | Self(Uuid::new_v4().to_string()) 29 | } 30 | } 31 | 32 | impl Default for UUIDv4 { 33 | fn default() -> Self { 34 | Self(Uuid::default().to_string()) 35 | } 36 | } 37 | 38 | pub const CMV40_MIN_VERSION: Version = Version { 39 | major: 4, 40 | minor: 0, 41 | revision: 2, 42 | }; 43 | 44 | // #[derive(Debug)] 45 | // pub enum CMVersion { 46 | // CMV29, 47 | // CMV40, 48 | // } 49 | // 50 | // impl Default for CMVersion { 51 | // fn default() -> Self { 52 | // Self::CMV40 53 | // } 54 | // } 55 | 56 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 57 | pub enum MDFType { 58 | CMV29(T), 59 | CMV40(T), 60 | } 61 | 62 | impl MDFType { 63 | pub fn into_inner(self) -> T { 64 | match self { 65 | CMV29(t) | CMV40(t) => t, 66 | } 67 | } 68 | 69 | pub fn with_new_inner(&mut self, value: U) -> MDFType { 70 | match self { 71 | CMV29(_) => CMV29(value), 72 | CMV40(_) => CMV40(value), 73 | } 74 | } 75 | } 76 | 77 | impl Default for MDFType 78 | where 79 | T: Default, 80 | { 81 | fn default() -> Self { 82 | CMV40(T::default()) 83 | } 84 | } 85 | 86 | impl Serialize for MDFType 87 | where 88 | T: IntoIterator + Copy, 89 | I: Display, 90 | { 91 | fn serialize(&self, serializer: S) -> Result 92 | where 93 | S: Serializer, 94 | { 95 | serializer.serialize_str(&format!("{}", &self)) 96 | } 97 | } 98 | 99 | impl Display for MDFType 100 | where 101 | T: IntoIterator + Copy, 102 | I: Display, 103 | { 104 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 105 | let join_str = match &self { 106 | CMV29(_) => ",", 107 | CMV40(_) => " ", 108 | }; 109 | 110 | write!(f, "{}", self.into_inner().into_iter().join(join_str)) 111 | } 112 | } 113 | 114 | pub trait IntoCMV29 { 115 | /// Convert inner `MDFType` to `CMV29(T)`. 116 | fn into_cmv29(self) -> T; 117 | } 118 | 119 | impl IntoCMV29> for Option 120 | where 121 | T: IntoCMV29, 122 | { 123 | fn into_cmv29(self) -> Option { 124 | self.map(|i| i.into_cmv29()) 125 | } 126 | } 127 | 128 | impl IntoCMV29> for Vec 129 | where 130 | T: IntoCMV29, 131 | { 132 | fn into_cmv29(self) -> Vec { 133 | self.into_iter().map(|b| b.into_cmv29()).collect::>() 134 | } 135 | } 136 | 137 | impl IntoCMV29 for MDFType { 138 | fn into_cmv29(self) -> Self { 139 | match self { 140 | CMV29(t) | CMV40(t) => CMV29(t), 141 | } 142 | } 143 | } 144 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize)] 145 | pub struct Encoding { 146 | #[serde(rename = "$text")] 147 | pub encoding: EncodingEnum, 148 | } 149 | 150 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize)] 151 | #[repr(usize)] 152 | // FIXME 153 | #[allow(dead_code)] 154 | pub enum EncodingEnum { 155 | #[serde(rename = "pq")] 156 | #[default] 157 | Pq, 158 | #[serde(rename = "linear")] 159 | Linear, 160 | #[serde(rename = "gamma_bt1886")] 161 | GammaBT1886, 162 | #[serde(rename = "gamma_dci")] 163 | GammaDCI, 164 | #[serde(rename = "gamma_22")] 165 | Gamma22, 166 | #[serde(rename = "gamma_24")] 167 | Gamma24, 168 | #[serde(rename = "hlg")] 169 | Hlg, 170 | } 171 | 172 | #[derive(Debug, Clone, Serialize)] 173 | pub struct ColorSpace { 174 | #[serde(rename = "$text")] 175 | pub color_space: ColorSpaceEnum, 176 | } 177 | 178 | #[derive(Debug, Clone, Serialize)] 179 | pub enum ColorSpaceEnum { 180 | #[serde(rename = "rgb")] 181 | Rgb, 182 | // #[serde(rename = "xyz")] 183 | // Xyz, 184 | // #[serde(rename = "ycbcr_bt709")] 185 | // YCbCrBT709, 186 | // #[serde(rename = "ycbcr_bt2020")] 187 | // YCbCrBT2020, 188 | // #[serde(rename = "ycbcr_native")] 189 | // YCbCrNative, 190 | } 191 | 192 | #[derive(Debug, Clone, Copy, Serialize)] 193 | pub struct ApplicationType { 194 | #[serde(rename = "$text")] 195 | pub application_type: ApplicationTypeEnum, 196 | } 197 | 198 | #[derive(Debug, Clone, Copy, Serialize)] 199 | pub enum ApplicationTypeEnum { 200 | #[serde(rename = "ALL")] 201 | All, 202 | #[serde(rename = "HOME")] 203 | Home, 204 | // #[serde(rename = "CINEMA")] 205 | // Cinema, 206 | } 207 | 208 | #[derive(Debug, Clone, Serialize)] 209 | pub struct SignalRange { 210 | #[serde(rename = "$text")] 211 | pub signal_range: SignalRangeEnum, 212 | } 213 | 214 | #[derive(Debug, Clone, Serialize)] 215 | pub enum SignalRangeEnum { 216 | #[serde(rename = "computer")] 217 | Computer, 218 | // #[serde(rename = "video")] 219 | // Video, 220 | } 221 | 222 | pub const XML_VERSION_LIST: &[[usize; 3]] = &[[2, 0, 5], [4, 0, 2], [5, 1, 0]]; 223 | 224 | pub enum XMLVersion { 225 | V205, 226 | V402, 227 | V510, 228 | } 229 | 230 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)] 231 | pub struct Version { 232 | major: usize, 233 | minor: usize, 234 | revision: usize, 235 | } 236 | 237 | impl Version { 238 | pub fn get_dolby_xmlns(&self) -> String { 239 | DOLBY_XMLNS_PREFIX.to_string() + &self.into_iter().join("_") 240 | } 241 | } 242 | 243 | impl From<[usize; 3]> for Version { 244 | fn from(u: [usize; 3]) -> Self { 245 | Self { 246 | major: u[0], 247 | minor: u[1], 248 | revision: u[2], 249 | } 250 | } 251 | } 252 | 253 | impl From for Version { 254 | fn from(u: XMLVersion) -> Self { 255 | Self::from(XML_VERSION_LIST[u as usize]) 256 | } 257 | } 258 | 259 | impl Default for Version { 260 | fn default() -> Self { 261 | Self::from(XML_VERSION_LIST[0]) 262 | } 263 | } 264 | 265 | impl Display for Version { 266 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 267 | // write!(f, "{}.{}.{}", &self.major, &self.minor, &self.revision) 268 | write!(f, "{}", self.into_iter().join(".")) 269 | } 270 | } 271 | 272 | impl IntoIterator for Version { 273 | type Item = usize; 274 | type IntoIter = array::IntoIter; 275 | 276 | fn into_iter(self) -> Self::IntoIter { 277 | [self.major, self.minor, self.revision].into_iter() 278 | } 279 | } 280 | 281 | impl Serialize for Version { 282 | fn serialize(&self, serializer: S) -> Result 283 | where 284 | S: Serializer, 285 | { 286 | serializer.serialize_str(&self.to_string()) 287 | } 288 | } 289 | 290 | impl Version { 291 | // pub fn from_summary(summary:) 292 | } 293 | 294 | #[derive(Debug, Serialize)] 295 | pub struct RevisionHistory { 296 | #[serde(rename = "Revision")] 297 | #[serde(skip_serializing_if = "Option::is_none")] 298 | pub revisions: Option>, 299 | } 300 | 301 | impl RevisionHistory { 302 | pub fn new() -> Self { 303 | Self { 304 | revisions: Some(vec![Revision::new()]), 305 | } 306 | } 307 | } 308 | 309 | #[derive(Debug, Serialize)] 310 | pub struct Revision { 311 | #[serde(rename = "DateTime")] 312 | pub date_time: DateTime, 313 | #[serde(rename = "Author")] 314 | pub author: String, 315 | #[serde(rename = "Software")] 316 | pub software: String, 317 | #[serde(rename = "SoftwareVersion")] 318 | pub software_version: String, 319 | #[serde(rename = "Comment")] 320 | #[serde(skip_serializing_if = "Option::is_none")] 321 | pub comment: Option, 322 | } 323 | 324 | impl Revision { 325 | pub fn new() -> Self { 326 | Self { 327 | date_time: DateTime::new(), 328 | author: env!("CARGO_PKG_AUTHORS").to_string(), 329 | software: env!("CARGO_PKG_NAME").to_string(), 330 | software_version: option_env!("VERGEN_GIT_DESCRIBE") 331 | .unwrap_or(env!("CARGO_PKG_VERSION")) 332 | .to_string(), 333 | comment: None, 334 | } 335 | } 336 | } 337 | 338 | #[derive(Debug, Serialize)] 339 | pub struct DateTime(String); 340 | 341 | impl DateTime { 342 | pub fn new() -> Self { 343 | Self(Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)) 344 | } 345 | } 346 | 347 | // Format: f32,f32 in CMv2.9, f32 f32 in CMv4.0 348 | #[derive(Debug, Clone, Default, Serialize)] 349 | pub struct Primaries { 350 | #[serde(rename = "Red")] 351 | pub red: MDFType, 352 | #[serde(rename = "Green")] 353 | pub green: MDFType, 354 | #[serde(rename = "Blue")] 355 | pub blue: MDFType, 356 | } 357 | 358 | impl From for Primaries { 359 | fn from(p: display::Primaries) -> Self { 360 | Self { 361 | red: CMV40(p.red), 362 | green: CMV40(p.green), 363 | blue: CMV40(p.blue), 364 | } 365 | } 366 | } 367 | 368 | impl IntoCMV29 for Primaries { 369 | fn into_cmv29(self) -> Self { 370 | Self { 371 | red: self.red.into_cmv29(), 372 | green: self.green.into_cmv29(), 373 | blue: self.blue.into_cmv29(), 374 | } 375 | } 376 | } 377 | 378 | fn update_levels(a: &mut Option>, b: &Option>) { 379 | if let Some(b_vec) = b { 380 | if a.is_none() { 381 | *a = Some(b_vec.iter().map(|level| T::with_tid(level.tid())).collect()); 382 | } else if let Some(a_vec) = a { 383 | let a_tids: Vec<_> = a_vec.iter().map(|level| level.tid()).collect(); 384 | for level in b_vec { 385 | if !a_tids.contains(&level.tid()) { 386 | a_vec.push(T::with_tid(level.tid())); 387 | } 388 | } 389 | } 390 | } 391 | } 392 | 393 | pub(crate) trait WithTid { 394 | fn tid(&self) -> usize; 395 | fn with_tid(tid: usize) -> Self; 396 | } 397 | --------------------------------------------------------------------------------