From 31018291032910680a4c3044a69eb19ae75798f4 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Sun, 5 Oct 2025 03:12:00 -0700 Subject: [PATCH] Implement splash screen feature. --- Cargo.lock | 119 ++ Cargo.toml | 16 + LICENSE | 202 ++- boot/Dockerfile | 2 + deps/README.md | 13 + deps/moxcms/.gitignore | 9 + deps/moxcms/Cargo.toml | 49 + deps/moxcms/LICENSE-APACHE.md | 201 +++ deps/moxcms/LICENSE.md | 26 + deps/moxcms/README.md | 74 + deps/moxcms/src/chad.rs | 172 ++ deps/moxcms/src/chromaticity.rs | 143 ++ deps/moxcms/src/cicp.rs | 642 +++++++ deps/moxcms/src/conversions/bpc.rs | 121 ++ deps/moxcms/src/conversions/gray2rgb.rs | 388 ++++ .../src/conversions/gray2rgb_extended.rs | 383 ++++ deps/moxcms/src/conversions/interpolator.rs | 599 +++++++ .../src/conversions/katana/finalizers.rs | 118 ++ deps/moxcms/src/conversions/katana/md3x3.rs | 483 +++++ deps/moxcms/src/conversions/katana/md4x3.rs | 321 ++++ deps/moxcms/src/conversions/katana/md_3xn.rs | 284 +++ deps/moxcms/src/conversions/katana/md_nx3.rs | 294 +++ .../src/conversions/katana/md_pipeline.rs | 393 ++++ deps/moxcms/src/conversions/katana/mod.rs | 56 + .../src/conversions/katana/pcs_stages.rs | 100 ++ deps/moxcms/src/conversions/katana/rgb_xyz.rs | 162 ++ deps/moxcms/src/conversions/katana/stages.rs | 85 + deps/moxcms/src/conversions/katana/xyz_lab.rs | 62 + deps/moxcms/src/conversions/katana/xyz_rgb.rs | 223 +++ deps/moxcms/src/conversions/lut3x3.rs | 428 +++++ deps/moxcms/src/conversions/lut3x4.rs | 249 +++ deps/moxcms/src/conversions/lut4.rs | 360 ++++ deps/moxcms/src/conversions/lut_transforms.rs | 802 +++++++++ deps/moxcms/src/conversions/mab.rs | 559 ++++++ deps/moxcms/src/conversions/mab4x3.rs | 307 ++++ deps/moxcms/src/conversions/mba3x4.rs | 302 ++++ deps/moxcms/src/conversions/md_lut.rs | 728 ++++++++ .../moxcms/src/conversions/md_luts_factory.rs | 188 ++ deps/moxcms/src/conversions/mod.rs | 66 + .../src/conversions/prelude_lut_xyz_rgb.rs | 328 ++++ deps/moxcms/src/conversions/rgb2gray.rs | 189 ++ .../src/conversions/rgb2gray_extended.rs | 181 ++ .../moxcms/src/conversions/rgb_xyz_factory.rs | 395 ++++ deps/moxcms/src/conversions/rgbxyz.rs | 899 ++++++++++ deps/moxcms/src/conversions/rgbxyz_fixed.rs | 560 ++++++ deps/moxcms/src/conversions/rgbxyz_float.rs | 330 ++++ .../src/conversions/transform_lut3_to_3.rs | 266 +++ .../src/conversions/transform_lut3_to_4.rs | 274 +++ .../src/conversions/transform_lut4_to_3.rs | 351 ++++ deps/moxcms/src/conversions/xyz_lab.rs | 61 + deps/moxcms/src/dat.rs | 154 ++ deps/moxcms/src/defaults.rs | 541 ++++++ deps/moxcms/src/dt_ucs.rs | 359 ++++ deps/moxcms/src/err.rs | 141 ++ deps/moxcms/src/gamma.rs | 1078 +++++++++++ deps/moxcms/src/gamut.rs | 66 + deps/moxcms/src/helpers.rs | 223 +++ deps/moxcms/src/ictcp.rs | 192 ++ deps/moxcms/src/jzazbz.rs | 434 +++++ deps/moxcms/src/jzczhz.rs | 375 ++++ deps/moxcms/src/lab.rs | 242 +++ deps/moxcms/src/lib.rs | 133 ++ deps/moxcms/src/lut_hint.rs | 106 ++ deps/moxcms/src/luv.rs | 698 ++++++++ deps/moxcms/src/matan/curve_shape.rs | 72 + deps/moxcms/src/matan/degeneration.rs | 60 + deps/moxcms/src/matan/discontinuity.rs | 74 + deps/moxcms/src/matan/mod.rs | 40 + deps/moxcms/src/matan/monotonic.rs | 52 + deps/moxcms/src/matan/slope_limit.rs | 84 + deps/moxcms/src/math/mod.rs | 68 + deps/moxcms/src/matrix.rs | 1272 +++++++++++++ deps/moxcms/src/mlaf.rs | 82 + deps/moxcms/src/nd_array.rs | 1239 +++++++++++++ deps/moxcms/src/oklab.rs | 354 ++++ deps/moxcms/src/oklch.rs | 294 +++ deps/moxcms/src/profile.rs | 1365 ++++++++++++++ deps/moxcms/src/reader.rs | 955 ++++++++++ deps/moxcms/src/rgb.rs | 723 ++++++++ deps/moxcms/src/safe_math.rs | 103 ++ deps/moxcms/src/srlab2.rs | 102 ++ deps/moxcms/src/tag.rs | 271 +++ deps/moxcms/src/transform.rs | 1334 ++++++++++++++ deps/moxcms/src/trc.rs | 1583 +++++++++++++++++ deps/moxcms/src/writer.rs | 889 +++++++++ deps/moxcms/src/xyy.rs | 98 + deps/moxcms/src/yrg.rs | 177 ++ deps/simd-adler32/CHANGELOG.md | 12 + deps/simd-adler32/Cargo.toml | 39 + deps/simd-adler32/LICENSE.md | 21 + deps/simd-adler32/README.md | 131 ++ deps/simd-adler32/src/hash.rs | 156 ++ deps/simd-adler32/src/imp/mod.rs | 13 + deps/simd-adler32/src/imp/scalar.rs | 69 + deps/simd-adler32/src/lib.rs | 309 ++++ hack/assets/edera-splash.png | Bin 0 -> 3751312 bytes hack/build.sh | 2 + hack/configs/kernel.sprout.toml | 6 +- src/actions.rs | 19 +- src/actions/splash.rs | 121 ++ src/config.rs | 16 + 101 files changed, 30504 insertions(+), 6 deletions(-) create mode 100644 deps/README.md create mode 100644 deps/moxcms/.gitignore create mode 100644 deps/moxcms/Cargo.toml create mode 100644 deps/moxcms/LICENSE-APACHE.md create mode 100644 deps/moxcms/LICENSE.md create mode 100644 deps/moxcms/README.md create mode 100644 deps/moxcms/src/chad.rs create mode 100644 deps/moxcms/src/chromaticity.rs create mode 100644 deps/moxcms/src/cicp.rs create mode 100644 deps/moxcms/src/conversions/bpc.rs create mode 100644 deps/moxcms/src/conversions/gray2rgb.rs create mode 100644 deps/moxcms/src/conversions/gray2rgb_extended.rs create mode 100644 deps/moxcms/src/conversions/interpolator.rs create mode 100644 deps/moxcms/src/conversions/katana/finalizers.rs create mode 100644 deps/moxcms/src/conversions/katana/md3x3.rs create mode 100644 deps/moxcms/src/conversions/katana/md4x3.rs create mode 100644 deps/moxcms/src/conversions/katana/md_3xn.rs create mode 100644 deps/moxcms/src/conversions/katana/md_nx3.rs create mode 100644 deps/moxcms/src/conversions/katana/md_pipeline.rs create mode 100644 deps/moxcms/src/conversions/katana/mod.rs create mode 100644 deps/moxcms/src/conversions/katana/pcs_stages.rs create mode 100644 deps/moxcms/src/conversions/katana/rgb_xyz.rs create mode 100644 deps/moxcms/src/conversions/katana/stages.rs create mode 100644 deps/moxcms/src/conversions/katana/xyz_lab.rs create mode 100644 deps/moxcms/src/conversions/katana/xyz_rgb.rs create mode 100644 deps/moxcms/src/conversions/lut3x3.rs create mode 100644 deps/moxcms/src/conversions/lut3x4.rs create mode 100644 deps/moxcms/src/conversions/lut4.rs create mode 100644 deps/moxcms/src/conversions/lut_transforms.rs create mode 100644 deps/moxcms/src/conversions/mab.rs create mode 100644 deps/moxcms/src/conversions/mab4x3.rs create mode 100644 deps/moxcms/src/conversions/mba3x4.rs create mode 100644 deps/moxcms/src/conversions/md_lut.rs create mode 100644 deps/moxcms/src/conversions/md_luts_factory.rs create mode 100644 deps/moxcms/src/conversions/mod.rs create mode 100644 deps/moxcms/src/conversions/prelude_lut_xyz_rgb.rs create mode 100644 deps/moxcms/src/conversions/rgb2gray.rs create mode 100644 deps/moxcms/src/conversions/rgb2gray_extended.rs create mode 100644 deps/moxcms/src/conversions/rgb_xyz_factory.rs create mode 100644 deps/moxcms/src/conversions/rgbxyz.rs create mode 100644 deps/moxcms/src/conversions/rgbxyz_fixed.rs create mode 100644 deps/moxcms/src/conversions/rgbxyz_float.rs create mode 100644 deps/moxcms/src/conversions/transform_lut3_to_3.rs create mode 100644 deps/moxcms/src/conversions/transform_lut3_to_4.rs create mode 100644 deps/moxcms/src/conversions/transform_lut4_to_3.rs create mode 100644 deps/moxcms/src/conversions/xyz_lab.rs create mode 100644 deps/moxcms/src/dat.rs create mode 100644 deps/moxcms/src/defaults.rs create mode 100644 deps/moxcms/src/dt_ucs.rs create mode 100644 deps/moxcms/src/err.rs create mode 100644 deps/moxcms/src/gamma.rs create mode 100644 deps/moxcms/src/gamut.rs create mode 100644 deps/moxcms/src/helpers.rs create mode 100644 deps/moxcms/src/ictcp.rs create mode 100644 deps/moxcms/src/jzazbz.rs create mode 100644 deps/moxcms/src/jzczhz.rs create mode 100644 deps/moxcms/src/lab.rs create mode 100644 deps/moxcms/src/lib.rs create mode 100644 deps/moxcms/src/lut_hint.rs create mode 100644 deps/moxcms/src/luv.rs create mode 100644 deps/moxcms/src/matan/curve_shape.rs create mode 100644 deps/moxcms/src/matan/degeneration.rs create mode 100644 deps/moxcms/src/matan/discontinuity.rs create mode 100644 deps/moxcms/src/matan/mod.rs create mode 100644 deps/moxcms/src/matan/monotonic.rs create mode 100644 deps/moxcms/src/matan/slope_limit.rs create mode 100644 deps/moxcms/src/math/mod.rs create mode 100644 deps/moxcms/src/matrix.rs create mode 100644 deps/moxcms/src/mlaf.rs create mode 100644 deps/moxcms/src/nd_array.rs create mode 100644 deps/moxcms/src/oklab.rs create mode 100644 deps/moxcms/src/oklch.rs create mode 100644 deps/moxcms/src/profile.rs create mode 100644 deps/moxcms/src/reader.rs create mode 100644 deps/moxcms/src/rgb.rs create mode 100644 deps/moxcms/src/safe_math.rs create mode 100644 deps/moxcms/src/srlab2.rs create mode 100644 deps/moxcms/src/tag.rs create mode 100644 deps/moxcms/src/transform.rs create mode 100644 deps/moxcms/src/trc.rs create mode 100644 deps/moxcms/src/writer.rs create mode 100644 deps/moxcms/src/xyy.rs create mode 100644 deps/moxcms/src/yrg.rs create mode 100644 deps/simd-adler32/CHANGELOG.md create mode 100644 deps/simd-adler32/Cargo.toml create mode 100644 deps/simd-adler32/LICENSE.md create mode 100644 deps/simd-adler32/README.md create mode 100644 deps/simd-adler32/src/hash.rs create mode 100644 deps/simd-adler32/src/imp/mod.rs create mode 100644 deps/simd-adler32/src/imp/scalar.rs create mode 100644 deps/simd-adler32/src/lib.rs create mode 100644 hack/assets/edera-splash.png create mode 100644 src/actions/splash.rs diff --git a/Cargo.lock b/Cargo.lock index 2fd3968..0454c52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "bit_field" version = "0.10.3" @@ -14,24 +26,77 @@ version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "cfg-if" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04bcaeafafdd3cd1cb5d986ff32096ad1136630207c49b9091e3ae541090d938" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +[[package]] +name = "image" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", +] + [[package]] name = "indexmap" version = "2.11.4" @@ -48,6 +113,46 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.7.6" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "proc-macro2" version = "1.0.101" @@ -77,6 +182,15 @@ dependencies = [ "syn", ] +[[package]] +name = "pxfm" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde" +dependencies = [ + "num-traits", +] + [[package]] name = "quote" version = "1.0.41" @@ -125,10 +239,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" + [[package]] name = "sprout" version = "0.1.0" dependencies = [ + "image", "log", "serde", "toml", diff --git a/Cargo.toml b/Cargo.toml index 6192c56..fdd39e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,12 @@ edition = "2024" toml = "0.9.7" log = "0.4.28" +[dependencies.image] +version = "0.25.6" +default-features = false +features = ["png"] +optional = true + [dependencies.serde] version = "1.0.228" features = ["derive"] @@ -19,6 +25,10 @@ features = ["alloc", "logger"] lto = "thin" strip = "symbols" +[features] +default = ["splash"] +splash = ["dep:image"] + [profile.release-debuginfo] inherits = "release" strip = "none" @@ -28,3 +38,9 @@ debug = 1 inherits = "dev" strip = "debuginfo" debug = 0 + +[patch.crates-io.simd-adler32] +path = "deps/simd-adler32" + +[patch.crates-io.moxcms] +path = "deps/moxcms" diff --git a/LICENSE b/LICENSE index 3dc7e5b..105cf36 100644 --- a/LICENSE +++ b/LICENSE @@ -1 +1,201 @@ -This repository is proprietary and owned by Edera, Inc. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Edera Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/boot/Dockerfile b/boot/Dockerfile index 2782e49..459036a 100644 --- a/boot/Dockerfile +++ b/boot/Dockerfile @@ -9,6 +9,7 @@ COPY sprout.efi /work/${EFI_NAME}.EFI COPY sprout.toml /work/SPROUT.TOML COPY kernel.efi /work/KERNEL.EFI COPY shell.efi /work/SHELL.EFI +COPY edera-splash.png /work/EDERA-SPLASH.PNG RUN truncate -s256MiB sprout.img && \ parted --script sprout.img mklabel gpt > /dev/null 2>&1 && \ parted --script sprout.img mkpart primary fat32 1MiB 100% > /dev/null 2>&1 && \ @@ -20,6 +21,7 @@ RUN truncate -s256MiB sprout.img && \ mcopy -i sprout.img KERNEL.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img SHELL.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img SPROUT.TOML ::/ && \ + mcopy -i sprout.img EDERA-SPLASH.PNG ::/ && \ mv sprout.img /sprout.img FROM scratch AS final diff --git a/deps/README.md b/deps/README.md new file mode 100644 index 0000000..e0d4c8b --- /dev/null +++ b/deps/README.md @@ -0,0 +1,13 @@ +# Vendored Dependencies + +Currently, sprout requires some vendored dependencies to work around usage of simd. + +Both `moxcms` and `simd-adler32` are used for the image library for the splash screen feature. + +## moxcms + +- Removed NEON, SSE, and AVX support. + +## simd-adler2 + +- Made compilation function on UEFI targets. diff --git a/deps/moxcms/.gitignore b/deps/moxcms/.gitignore new file mode 100644 index 0000000..9bfd895 --- /dev/null +++ b/deps/moxcms/.gitignore @@ -0,0 +1,9 @@ +/target +Cargo.lock +.idea +app/target +flamegraph.svg +perf.data +profile.json.gz +.cargo +rust-toolchain.toml \ No newline at end of file diff --git a/deps/moxcms/Cargo.toml b/deps/moxcms/Cargo.toml new file mode 100644 index 0000000..3db6a95 --- /dev/null +++ b/deps/moxcms/Cargo.toml @@ -0,0 +1,49 @@ +workspace = { members = ["app", "fuzz"] } + +[package] +name = "moxcms" +version = "0.7.6" +edition = "2024" +description = "Simple Color Management in Rust" +readme = "./README.md" +keywords = ["icc", "cms", "color", "cmyk"] +license = "BSD-3-Clause OR Apache-2.0" +authors = ["Radzivon Bartoshyk"] +documentation = "https://github.com/awxkee/moxcms" +categories = ["multimedia::images"] +homepage = "https://github.com/awxkee/moxcms" +repository = "https://github.com/awxkee/moxcms.git" +exclude = ["*.jpg", "../../assets/*", "*.png", "*.icc", "./assets/*"] +rust-version = "1.85.0" + +[dependencies] +num-traits = "0.2" +pxfm = "^0.1.1" + +[dev-dependencies] +rand = "0.9" + +[features] +# If no unsafe intrinsics active then `forbid(unsafe)` will be used. +default = [] +# Enables AVX2 acceleration where possible +avx = [] +# Enables SSE4.1 acceleration where possible +sse = [] +# Enables NEON intrinsics where possible +neon = [] +# Enables AVX-512 acceleration where possible. This will work only from 1.89 on stable. +avx512 = [] +# Allows configuring interpolation methods and LUT weights precision. +# Disabled by default to prevent binary bloat. +options = [] + +[package.metadata.docs.rs] +# To build locally: +# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps --open --manifest-path ./Cargo.toml +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[profile.profiling] +inherits = "release" +debug = true diff --git a/deps/moxcms/LICENSE-APACHE.md b/deps/moxcms/LICENSE-APACHE.md new file mode 100644 index 0000000..86a13a8 --- /dev/null +++ b/deps/moxcms/LICENSE-APACHE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Radzivon Bartoshyk + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/deps/moxcms/LICENSE.md b/deps/moxcms/LICENSE.md new file mode 100644 index 0000000..bf616fd --- /dev/null +++ b/deps/moxcms/LICENSE.md @@ -0,0 +1,26 @@ +Copyright (c) Radzivon Bartoshyk. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/deps/moxcms/README.md b/deps/moxcms/README.md new file mode 100644 index 0000000..f6976f3 --- /dev/null +++ b/deps/moxcms/README.md @@ -0,0 +1,74 @@ +# Rust ICC Management + +Fast and safe conversion between ICC profiles; in pure Rust. + +Supports CMYK⬌RGBX, RGBX⬌RGBX, RGBX⬌GRAY, LAB⬌RGBX and CMYK⬌LAB, GRAY⬌RGB, any 3/4 color profiles to RGB and vice versa. Also supports almost any to any Display Class ICC profiles up to 16 inks. + +## Example + +```rust +let f_str = "./assets/dci_p3_profile.jpeg"; +let file = File::open(f_str).expect("Failed to open file"); + +let img = image::ImageReader::open(f_str).unwrap().decode().unwrap(); +let rgb = img.to_rgb8(); + +let mut decoder = JpegDecoder::new(BufReader::new(file)).unwrap(); +let icc = decoder.icc_profile().unwrap().unwrap(); +let color_profile = ColorProfile::new_from_slice(&icc).unwrap(); +let dest_profile = ColorProfile::new_srgb(); +let transform = color_profile + .create_transform_8bit(&dest_profile, Layout::Rgb8, TransformOptions::default()) + .unwrap(); +let mut dst = vec![0u8; rgb.len()]; + +for (src, dst) in rgb + .chunks_exact(img.width() as usize * 3) + .zip(dst.chunks_exact_mut(img.dimensions().0 as usize * 3)) +{ + transform + .transform( + &src[..img.dimensions().0 as usize * 3], + &mut dst[..img.dimensions().0 as usize * 3], + ) + .unwrap(); +} +image::save_buffer( + "v1.jpg", + &dst, + img.dimensions().0, + img.dimensions().1, + image::ExtendedColorType::Rgb8, +) + .unwrap(); +``` + +## Benchmarks + +### ICC Transform 8-Bit + +Tests were ran with a 1997×1331 resolution image. + +| Conversion | time(NEON) | Time(AVX2) | +|--------------------|:----------:|:----------:| +| moxcms RGB⮕RGB | 2.68ms | 4.52ms | +| moxcms LUT RGB⮕RGB | 7.18ms | 17.50ms | +| moxcms RGBA⮕RGBA | 2.96ms | 4.83ms | +| moxcms CMYK⮕RGBA | 11.86ms | 27.98ms | +| lcms2 RGB⮕RGB | 13.1ms | 27.73ms | +| lcms2 LUT RGB⮕RGB | 27.60ms | 58.26ms | +| lcms2 RGBA⮕RGBA | 21.97ms | 35.70ms | +| lcms2 CMYK⮕RGBA | 39.71ms | 79.40ms | +| qcms RGB⮕RGB | 6.47ms | 4.59ms | +| qcms LUT RGB⮕RGB | 26.72ms | 60.80ms | +| qcms RGBA⮕RGBA | 6.83ms | 4.99ms | +| qcms CMYK⮕RGBA | 25.97ms | 61.54ms | + +## License + +This project is licensed under either of + +- BSD-3-Clause License (see [LICENSE](LICENSE.md)) +- Apache License, Version 2.0 (see [LICENSE](LICENSE-APACHE.md)) + +at your option. diff --git a/deps/moxcms/src/chad.rs b/deps/moxcms/src/chad.rs new file mode 100644 index 0000000..eaace43 --- /dev/null +++ b/deps/moxcms/src/chad.rs @@ -0,0 +1,172 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::matrix::{Matrix3f, Vector3f, Xyz}; +use crate::{Chromaticity, Matrix3d, Vector3d, XyY}; + +pub(crate) const BRADFORD_D: Matrix3d = Matrix3d { + v: [ + [0.8951, 0.2664, -0.1614], + [-0.7502, 1.7135, 0.0367], + [0.0389, -0.0685, 1.0296], + ], +}; + +pub(crate) const BRADFORD_F: Matrix3f = BRADFORD_D.to_f32(); + +#[inline] +pub(crate) const fn compute_chromatic_adaption( + source_white_point: Xyz, + dest_white_point: Xyz, + chad: Matrix3f, +) -> Matrix3f { + let cone_source_xyz = Vector3f { + v: [ + source_white_point.x, + source_white_point.y, + source_white_point.z, + ], + }; + let cone_source_rgb = chad.mul_vector(cone_source_xyz); + + let cone_dest_xyz = Vector3f { + v: [dest_white_point.x, dest_white_point.y, dest_white_point.z], + }; + let cone_dest_rgb = chad.mul_vector(cone_dest_xyz); + + let cone = Matrix3f { + v: [ + [cone_dest_rgb.v[0] / cone_source_rgb.v[0], 0., 0.], + [0., cone_dest_rgb.v[1] / cone_source_rgb.v[1], 0.], + [0., 0., cone_dest_rgb.v[2] / cone_source_rgb.v[2]], + ], + }; + + let chad_inv = chad.inverse(); + + let p0 = cone.mat_mul_const(chad); + chad_inv.mat_mul_const(p0) +} + +#[inline] +pub(crate) const fn compute_chromatic_adaption_d( + source_white_point: Xyz, + dest_white_point: Xyz, + chad: Matrix3d, +) -> Matrix3d { + let cone_source_xyz = Vector3d { + v: [ + source_white_point.x as f64, + source_white_point.y as f64, + source_white_point.z as f64, + ], + }; + let cone_source_rgb = chad.mul_vector(cone_source_xyz); + + let cone_dest_xyz = Vector3d { + v: [ + dest_white_point.x as f64, + dest_white_point.y as f64, + dest_white_point.z as f64, + ], + }; + let cone_dest_rgb = chad.mul_vector(cone_dest_xyz); + + let cone = Matrix3d { + v: [ + [cone_dest_rgb.v[0] / cone_source_rgb.v[0], 0., 0.], + [0., cone_dest_rgb.v[1] / cone_source_rgb.v[1], 0.], + [0., 0., cone_dest_rgb.v[2] / cone_source_rgb.v[2]], + ], + }; + + let chad_inv = chad.inverse(); + + let p0 = cone.mat_mul_const(chad); + chad_inv.mat_mul_const(p0) +} + +pub const fn adaption_matrix(source_illumination: Xyz, target_illumination: Xyz) -> Matrix3f { + compute_chromatic_adaption(source_illumination, target_illumination, BRADFORD_F) +} + +pub const fn adaption_matrix_d(source_illumination: Xyz, target_illumination: Xyz) -> Matrix3d { + compute_chromatic_adaption_d(source_illumination, target_illumination, BRADFORD_D) +} + +pub const fn adapt_to_d50(r: Matrix3f, source_white_pt: XyY) -> Matrix3f { + adapt_to_illuminant(r, source_white_pt, Chromaticity::D50.to_xyz()) +} + +pub const fn adapt_to_d50_d(r: Matrix3d, source_white_pt: XyY) -> Matrix3d { + adapt_to_illuminant_d(r, source_white_pt, Chromaticity::D50.to_xyz()) +} + +pub const fn adapt_to_illuminant( + r: Matrix3f, + source_white_pt: XyY, + illuminant_xyz: Xyz, +) -> Matrix3f { + let bradford = adaption_matrix(source_white_pt.to_xyz(), illuminant_xyz); + bradford.mat_mul_const(r) +} + +pub const fn adapt_to_illuminant_d( + r: Matrix3d, + source_white_pt: XyY, + illuminant_xyz: Xyz, +) -> Matrix3d { + let bradford = adaption_matrix_d(source_white_pt.to_xyz(), illuminant_xyz); + bradford.mat_mul_const(r) +} + +pub const fn adapt_to_illuminant_xyz( + r: Matrix3f, + source_white_pt: Xyz, + illuminant_xyz: Xyz, +) -> Matrix3f { + if source_white_pt.y == 0.0 { + return r; + } + + let bradford = adaption_matrix(source_white_pt, illuminant_xyz); + bradford.mat_mul_const(r) +} + +pub const fn adapt_to_illuminant_xyz_d( + r: Matrix3d, + source_white_pt: Xyz, + illuminant_xyz: Xyz, +) -> Matrix3d { + if source_white_pt.y == 0.0 { + return r; + } + + let bradford = adaption_matrix_d(source_white_pt, illuminant_xyz); + bradford.mat_mul_const(r) +} diff --git a/deps/moxcms/src/chromaticity.rs b/deps/moxcms/src/chromaticity.rs new file mode 100644 index 0000000..2c0f6b2 --- /dev/null +++ b/deps/moxcms/src/chromaticity.rs @@ -0,0 +1,143 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 8/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::{CmsError, XyY, XyYRepresentable, Xyz, Xyzd}; + +#[derive(Clone, Debug, Copy)] +#[repr(C)] +pub struct Chromaticity { + pub x: f32, + pub y: f32, +} + +impl Chromaticity { + #[inline] + pub const fn new(x: f32, y: f32) -> Self { + Self { x, y } + } + + /// Converts this chromaticity (`x`, `y`) to a tristimulus [`Xyz`] value, + /// normalized such that `y = 1.0`. + #[inline] + pub const fn to_xyz(&self) -> Xyz { + let reciprocal = if self.y != 0. { 1. / self.y } else { 0. }; + Xyz { + x: self.x * reciprocal, + y: 1f32, + z: (1f32 - self.x - self.y) * reciprocal, + } + } + + /// Get the color representation with component sum `1`. + /// + /// In contrast to the XYZ representation defined through setting `Y` to a known + /// value (such as `1` in [`Self::to_xyz`]) this representation can be uniquely + /// derived from the `xy` coordinates with no ambiguities. It is scaled from the + /// original XYZ color by diving by `X + Y + Z`. Note that, in particular, this + /// method is well-defined even if the original color had pure chromamatic + /// information with no luminance (Y = `0`) and will preserve that information, + /// whereas [`Self::to_xyz`] is ill-defined and returns an incorrect value. + #[inline] + pub const fn to_scaled_xyzd(&self) -> Xyzd { + let z = 1.0 - self.x as f64 - self.y as f64; + Xyzd::new(self.x as f64, self.y as f64, z) + } + + /// Get the color representation with component sum `1`. + /// + /// In contrast to the XYZ representation defined through setting `Y` to a known + /// value (such as `1` in [`Self::to_xyz`]) this representation can be uniquely + /// derived from the `xy` coordinates with no ambiguities. It is scaled from the + /// original XYZ color by diving by `X + Y + Z`. Note that, in particular, this + /// method is well-defined even if the original color had pure chromamatic + /// information with no luminance (Y = `0`) and will preserve that information, + /// whereas [`Self::to_xyz`] is ill-defined and returns an incorrect value. + #[inline] + pub const fn to_scaled_xyz(&self) -> Xyz { + let z = 1.0 - self.x - self.y; + Xyz::new(self.x, self.y, z) + } + + #[inline] + pub const fn to_xyzd(&self) -> Xyzd { + let reciprocal = if self.y != 0. { 1. / self.y } else { 0. }; + Xyzd { + x: self.x as f64 * reciprocal as f64, + y: 1f64, + z: (1f64 - self.x as f64 - self.y as f64) * reciprocal as f64, + } + } + + #[inline] + pub const fn to_xyyb(&self) -> XyY { + XyY { + x: self.x as f64, + y: self.y as f64, + yb: 1., + } + } + + pub const D65: Chromaticity = Chromaticity { + x: 0.31272, + y: 0.32903, + }; + + pub const D50: Chromaticity = Chromaticity { + x: 0.34567, + y: 0.35850, + }; +} + +impl XyYRepresentable for Chromaticity { + fn to_xyy(self) -> XyY { + self.to_xyyb() + } +} + +impl TryFrom for Chromaticity { + type Error = CmsError; + + #[inline] + fn try_from(xyz: Xyz) -> Result { + let sum = xyz.x + xyz.y + xyz.z; + + // Avoid division by zero or invalid XYZ values + if sum == 0.0 { + return Err(CmsError::DivisionByZero); + } + let rec = 1f32 / sum; + + let chromaticity_x = xyz.x * rec; + let chromaticity_y = xyz.y * rec; + + Ok(Chromaticity { + x: chromaticity_x, + y: chromaticity_y, + }) + } +} diff --git a/deps/moxcms/src/cicp.rs b/deps/moxcms/src/cicp.rs new file mode 100644 index 0000000..6ed9ee7 --- /dev/null +++ b/deps/moxcms/src/cicp.rs @@ -0,0 +1,642 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::gamma::{ + bt1361_to_linear, hlg_to_linear, iec61966_to_linear, log100_sqrt10_to_linear, log100_to_linear, + pq_to_linear, smpte240_to_linear, smpte428_to_linear, +}; +use crate::{ + Chromaticity, ColorProfile, Matrix3d, Matrix3f, XyYRepresentable, + err::CmsError, + trc::{ToneReprCurve, build_trc_table, curve_from_gamma}, +}; +use std::convert::TryFrom; + +/// See [Rec. ITU-T H.273 (12/2016)](https://www.itu.int/rec/T-REC-H.273-201612-I/en) Table 2 +/// Values 0, 3, 13–21, 23–255 are all reserved so all map to the same variant +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum CicpColorPrimaries { + /// For future use by ITU-T | ISO/IEC + Reserved, + /// Rec. ITU-R BT.709-6
+ /// Rec. ITU-R BT.1361-0 conventional colour gamut system and extended colour gamut system (historical)
+ /// IEC 61966-2-1 sRGB or sYCC IEC 61966-2-4
+ /// Society of Motion Picture and Television Engineers (MPTE) RP 177 (1993) Annex B
+ Bt709 = 1, + /// Unspecified
+ /// Image characteristics are unknown or are determined by the application. + Unspecified = 2, + /// Rec. ITU-R BT.470-6 System M (historical)
+ /// United States National Television System Committee 1953 Recommendation for transmission standards for color television
+ /// United States Federal Communications Commission (2003) Title 47 Code of Federal Regulations 73.682 (a) (20)
+ Bt470M = 4, + /// Rec. ITU-R BT.470-6 System B, G (historical) Rec. ITU-R BT.601-7 625
+ /// Rec. ITU-R BT.1358-0 625 (historical)
+ /// Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM
+ Bt470Bg = 5, + /// Rec. ITU-R BT.601-7 525
+ /// Rec. ITU-R BT.1358-1 525 or 625 (historical) Rec. ITU-R BT.1700-0 NTSC
+ /// SMPTE 170M (2004)
+ /// (functionally the same as the value 7)
+ Bt601 = 6, + /// SMPTE 240M (1999) (historical) (functionally the same as the value 6)
+ Smpte240 = 7, + /// Generic film (colour filters using Illuminant C)
+ GenericFilm = 8, + /// Rec. ITU-R BT.2020-2
+ /// Rec. ITU-R BT.2100-0
+ Bt2020 = 9, + /// SMPTE ST 428-1
+ /// (CIE 1931 XYZ as in ISO 11664-1)
+ Xyz = 10, + /// SMPTE RP 431-2 (2011)
+ Smpte431 = 11, + /// SMPTE EG 432-1 (2010)
+ Smpte432 = 12, + /// EBU Tech. 3213-E (1975)
+ Ebu3213 = 22, +} + +impl TryFrom for CicpColorPrimaries { + type Error = CmsError; + + #[allow(unreachable_patterns)] + fn try_from(value: u8) -> Result { + match value { + // Values 0, 3, 13–21, 23–255 are all reserved so all map to the + // same variant. + 0 | 3 | 13..=21 | 23..=255 => Ok(Self::Reserved), + 1 => Ok(Self::Bt709), + 2 => Ok(Self::Unspecified), + 4 => Ok(Self::Bt470M), + 5 => Ok(Self::Bt470Bg), + 6 => Ok(Self::Bt601), + 7 => Ok(Self::Smpte240), + 8 => Ok(Self::GenericFilm), + 9 => Ok(Self::Bt2020), + 10 => Ok(Self::Xyz), + 11 => Ok(Self::Smpte431), + 12 => Ok(Self::Smpte432), + 22 => Ok(Self::Ebu3213), + _ => Err(CmsError::InvalidCicp), + } + } +} + +#[derive(Clone, Copy, Debug)] +#[repr(C)] +pub struct ColorPrimaries { + pub red: Chromaticity, + pub green: Chromaticity, + pub blue: Chromaticity, +} + +/// See [Rec. ITU-T H.273 (12/2016)](https://www.itu.int/rec/T-REC-H.273-201612-I/en) Table 2. +impl ColorPrimaries { + /// [ACEScg](https://en.wikipedia.org/wiki/Academy_Color_Encoding_System#ACEScg). + pub const ACES_CG: ColorPrimaries = ColorPrimaries { + red: Chromaticity { x: 0.713, y: 0.293 }, + green: Chromaticity { x: 0.165, y: 0.830 }, + blue: Chromaticity { x: 0.128, y: 0.044 }, + }; + + /// [ACES2065-1](https://en.wikipedia.org/wiki/Academy_Color_Encoding_System#ACES2065-1). + pub const ACES_2065_1: ColorPrimaries = ColorPrimaries { + red: Chromaticity { + x: 0.7347, + y: 0.2653, + }, + green: Chromaticity { + x: 0.0000, + y: 1.0000, + }, + blue: Chromaticity { + x: 0.0001, + y: -0.0770, + }, + }; + + /// [Adobe RGB](https://en.wikipedia.org/wiki/Adobe_RGB_color_space) (1998). + pub const ADOBE_RGB: ColorPrimaries = ColorPrimaries { + red: Chromaticity { x: 0.64, y: 0.33 }, + green: Chromaticity { x: 0.21, y: 0.71 }, + blue: Chromaticity { x: 0.15, y: 0.06 }, + }; + + /// [DCI P3](https://en.wikipedia.org/wiki/DCI-P3#DCI_P3). + /// + /// This is the same as [`DISPLAY_P3`](Self::DISPLAY_P3), + /// [`SMPTE_431`](Self::SMPTE_431) and [`SMPTE_432`](Self::SMPTE_432). + pub const DCI_P3: ColorPrimaries = ColorPrimaries { + red: Chromaticity { x: 0.680, y: 0.320 }, + green: Chromaticity { x: 0.265, y: 0.690 }, + blue: Chromaticity { x: 0.150, y: 0.060 }, + }; + + /// [Diplay P3](https://en.wikipedia.org/wiki/DCI-P3#Display_P3). + /// + /// This is the same as [`DCI_P3`](Self::DCI_P3), + /// [`SMPTE_431`](Self::SMPTE_431) and [`SMPTE_432`](Self::SMPTE_432). + pub const DISPLAY_P3: ColorPrimaries = Self::DCI_P3; + + /// SMPTE RP 431-2 (2011). + /// + /// This is the same as [`DCI_P3`](Self::DCI_P3), + /// [`DISPLAY_P3`](Self::DISPLAY_P3) and [`SMPTE_432`](Self::SMPTE_432). + pub const SMPTE_431: ColorPrimaries = Self::DCI_P3; + + /// SMPTE EG 432-1 (2010). + /// + /// This is the same as [`DCI_P3`](Self::DCI_P3), + /// [`DISPLAY_P3`](Self::DISPLAY_P3) and [`SMPTE_431`](Self::SMPTE_431). + pub const SMPTE_432: ColorPrimaries = Self::DCI_P3; + + /// [ProPhoto RGB](https://en.wikipedia.org/wiki/ProPhoto_RGB_color_space). + pub const PRO_PHOTO_RGB: ColorPrimaries = ColorPrimaries { + red: Chromaticity { + x: 0.734699, + y: 0.265301, + }, + green: Chromaticity { + x: 0.159597, + y: 0.840403, + }, + blue: Chromaticity { + x: 0.036598, + y: 0.000105, + }, + }; + + /// Rec. ITU-R BT.709-6 + /// + /// Rec. ITU-R BT.1361-0 conventional colour gamut system and extended + /// colour gamut system (historical). + /// + /// IEC 61966-2-1 sRGB or sYCC IEC 61966-2-4). + /// + /// Society of Motion Picture and Television Engineers (MPTE) RP 177 (1993) Annex B. + pub const BT_709: ColorPrimaries = ColorPrimaries { + red: Chromaticity { x: 0.64, y: 0.33 }, + green: Chromaticity { x: 0.30, y: 0.60 }, + blue: Chromaticity { x: 0.15, y: 0.06 }, + }; + + /// Rec. ITU-R BT.470-6 System M (historical). + /// + /// United States National Television System Committee 1953 Recommendation + /// for transmission standards for color television. + /// + /// United States Federal Communications Commission (2003) Title 47 Code of + /// Federal Regulations 73.682 (a) (20). + pub const BT_470M: ColorPrimaries = ColorPrimaries { + red: Chromaticity { x: 0.67, y: 0.33 }, + green: Chromaticity { x: 0.21, y: 0.71 }, + blue: Chromaticity { x: 0.14, y: 0.08 }, + }; + + /// Rec. ITU-R BT.470-6 System B, G (historical) Rec. ITU-R BT.601-7 625. + /// + /// Rec. ITU-R BT.1358-0 625 (historical). + /// Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM. + pub const BT_470BG: ColorPrimaries = ColorPrimaries { + red: Chromaticity { x: 0.64, y: 0.33 }, + green: Chromaticity { x: 0.29, y: 0.60 }, + blue: Chromaticity { x: 0.15, y: 0.06 }, + }; + + /// Rec. ITU-R BT.601-7 525. + /// + /// Rec. ITU-R BT.1358-1 525 or 625 (historical) Rec. ITU-R BT.1700-0 NTSC. + /// + /// SMPTE 170M (2004) (functionally the same as the [`SMPTE_240`](Self::SMPTE_240)). + pub const BT_601: ColorPrimaries = ColorPrimaries { + red: Chromaticity { x: 0.630, y: 0.340 }, + green: Chromaticity { x: 0.310, y: 0.595 }, + blue: Chromaticity { x: 0.155, y: 0.070 }, + }; + + /// SMPTE 240M (1999) (historical) (functionally the same as [`BT_601`](Self::BT_601)). + pub const SMPTE_240: ColorPrimaries = Self::BT_601; + + /// Generic film (colour filters using Illuminant C). + pub const GENERIC_FILM: ColorPrimaries = ColorPrimaries { + red: Chromaticity { x: 0.681, y: 0.319 }, + green: Chromaticity { x: 0.243, y: 0.692 }, + blue: Chromaticity { x: 0.145, y: 0.049 }, + }; + + /// Rec. ITU-R BT.2020-2. + /// + /// Rec. ITU-R BT.2100-0. + pub const BT_2020: ColorPrimaries = ColorPrimaries { + red: Chromaticity { x: 0.708, y: 0.292 }, + green: Chromaticity { x: 0.170, y: 0.797 }, + blue: Chromaticity { x: 0.131, y: 0.046 }, + }; + + /// SMPTE ST 428-1 (CIE 1931 XYZ as in ISO 11664-1). + pub const XYZ: ColorPrimaries = ColorPrimaries { + red: Chromaticity { x: 1.0, y: 0.0 }, + green: Chromaticity { x: 0.0, y: 1.0 }, + blue: Chromaticity { x: 0.0, y: 0.0 }, + }; + + /// EBU Tech. 3213-E (1975). + pub const EBU_3213: ColorPrimaries = ColorPrimaries { + red: Chromaticity { x: 0.630, y: 0.340 }, + green: Chromaticity { x: 0.295, y: 0.605 }, + blue: Chromaticity { x: 0.155, y: 0.077 }, + }; +} + +impl ColorPrimaries { + /// Returns RGB -> XYZ conversion matrix + /// + /// # Arguments + /// + /// * `white_point`: [Chromaticity] or [crate::XyY] or any item conforming [XyYRepresentable] + /// + /// returns: [Matrix3d] + pub fn transform_to_xyz_d(self, white_point: impl XyYRepresentable) -> Matrix3d { + let red_xyz = self.red.to_scaled_xyzd(); + let green_xyz = self.green.to_scaled_xyzd(); + let blue_xyz = self.blue.to_scaled_xyzd(); + + let xyz_matrix = Matrix3d { + v: [ + [red_xyz.x, green_xyz.x, blue_xyz.x], + [red_xyz.y, green_xyz.y, blue_xyz.y], + [red_xyz.z, green_xyz.z, blue_xyz.z], + ], + }; + ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyy().to_xyzd()) + } + + /// Returns RGB -> XYZ conversion matrix + /// + /// # Arguments + /// + /// * `white_point`: [Chromaticity] or [crate::XyY] or any item conforming [XyYRepresentable] + /// + /// returns: [Matrix3f] + pub fn transform_to_xyz(self, white_point: impl XyYRepresentable) -> Matrix3f { + let red_xyz = self.red.to_scaled_xyz(); + let green_xyz = self.green.to_scaled_xyz(); + let blue_xyz = self.blue.to_scaled_xyz(); + + let xyz_matrix = Matrix3f { + v: [ + [red_xyz.x, green_xyz.x, blue_xyz.x], + [red_xyz.y, green_xyz.y, blue_xyz.y], + [red_xyz.z, green_xyz.z, blue_xyz.z], + ], + }; + ColorProfile::rgb_to_xyz_static(xyz_matrix, white_point.to_xyy().to_xyz()) + } +} + +/// See [Rec. ITU-T H.273 (12/2016)](https://www.itu.int/rec/T-REC-H.273-201612-I/en) Table 3 +/// Values 0, 3, 19–255 are all reserved so all map to the same variant +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum TransferCharacteristics { + /// For future use by ITU-T | ISO/IEC + Reserved, + /// Rec. ITU-R BT.709-6
+ /// Rec. ITU-R BT.1361-0 conventional colour gamut system (historical)
+ /// (functionally the same as the values 6, 14 and 15)
+ Bt709 = 1, + /// Image characteristics are unknown or are determined by the application.
+ Unspecified = 2, + /// Rec. ITU-R BT.470-6 System M (historical)
+ /// United States National Television System Committee 1953 Recommendation for transmission standards for color television
+ /// United States Federal Communications Commission (2003) Title 47 Code of Federal Regulations 73.682 (a) (20)
+ /// Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM
+ Bt470M = 4, + /// Rec. ITU-R BT.470-6 System B, G (historical)
+ Bt470Bg = 5, + /// Rec. ITU-R BT.601-7 525 or 625
+ /// Rec. ITU-R BT.1358-1 525 or 625 (historical)
+ /// Rec. ITU-R BT.1700-0 NTSC SMPTE 170M (2004)
+ /// (functionally the same as the values 1, 14 and 15)
+ Bt601 = 6, + /// SMPTE 240M (1999) (historical)
+ Smpte240 = 7, + /// Linear transfer characteristics
+ Linear = 8, + /// Logarithmic transfer characteristic (100:1 range)
+ Log100 = 9, + /// Logarithmic transfer characteristic (100 * Sqrt( 10 ) : 1 range)
+ Log100sqrt10 = 10, + /// IEC 61966-2-4
+ Iec61966 = 11, + /// Rec. ITU-R BT.1361-0 extended colour gamut system (historical)
+ Bt1361 = 12, + /// IEC 61966-2-1 sRGB or sYCC
+ Srgb = 13, + /// Rec. ITU-R BT.2020-2 (10-bit system)
+ /// (functionally the same as the values 1, 6 and 15)
+ Bt202010bit = 14, + /// Rec. ITU-R BT.2020-2 (12-bit system)
+ /// (functionally the same as the values 1, 6 and 14)
+ Bt202012bit = 15, + /// SMPTE ST 2084 for 10-, 12-, 14- and 16-bitsystems
+ /// Rec. ITU-R BT.2100-0 perceptual quantization (PQ) system
+ Smpte2084 = 16, + /// SMPTE ST 428-1
+ Smpte428 = 17, + /// ARIB STD-B67
+ /// Rec. ITU-R BT.2100-0 hybrid log- gamma (HLG) system
+ Hlg = 18, +} + +impl TryFrom for TransferCharacteristics { + type Error = CmsError; + + #[allow(unreachable_patterns)] + fn try_from(value: u8) -> Result { + match value { + 0 | 3 | 19..=255 => Ok(Self::Reserved), + 1 => Ok(Self::Bt709), + 2 => Ok(Self::Unspecified), + 4 => Ok(Self::Bt470M), + 5 => Ok(Self::Bt470Bg), + 6 => Ok(Self::Bt601), + 7 => Ok(Self::Smpte240), // unimplemented + 8 => Ok(Self::Linear), + 9 => Ok(Self::Log100), + 10 => Ok(Self::Log100sqrt10), + 11 => Ok(Self::Iec61966), // unimplemented + 12 => Ok(Self::Bt1361), // unimplemented + 13 => Ok(Self::Srgb), + 14 => Ok(Self::Bt202010bit), + 15 => Ok(Self::Bt202012bit), + 16 => Ok(Self::Smpte2084), + 17 => Ok(Self::Smpte428), // unimplemented + 18 => Ok(Self::Hlg), + _ => Err(CmsError::InvalidCicp), + } + } +} + +impl CicpColorPrimaries { + pub(crate) const fn has_chromaticity(self) -> bool { + self as u8 != Self::Reserved as u8 && self as u8 != Self::Unspecified as u8 + } + + pub(crate) const fn white_point(self) -> Result { + Ok(match self { + Self::Reserved => return Err(CmsError::UnsupportedColorPrimaries(self as u8)), + Self::Bt709 + | Self::Bt470Bg + | Self::Bt601 + | Self::Smpte240 + | Self::Bt2020 + | Self::Smpte432 + | Self::Ebu3213 => Chromaticity::D65, + Self::Unspecified => return Err(CmsError::UnsupportedColorPrimaries(self as u8)), + Self::Bt470M => Chromaticity { x: 0.310, y: 0.316 }, + Self::GenericFilm => Chromaticity { x: 0.310, y: 0.316 }, + Self::Xyz => Chromaticity { + x: 1. / 3., + y: 1. / 3., + }, + Self::Smpte431 => Chromaticity { x: 0.314, y: 0.351 }, + }) + } +} + +impl TryFrom for ColorPrimaries { + type Error = CmsError; + + fn try_from(value: CicpColorPrimaries) -> Result { + match value { + CicpColorPrimaries::Reserved => Err(CmsError::UnsupportedColorPrimaries(value as u8)), + CicpColorPrimaries::Bt709 => Ok(ColorPrimaries::BT_709), + CicpColorPrimaries::Unspecified => { + Err(CmsError::UnsupportedColorPrimaries(value as u8)) + } + CicpColorPrimaries::Bt470M => Ok(ColorPrimaries::BT_470M), + CicpColorPrimaries::Bt470Bg => Ok(ColorPrimaries::BT_470BG), + CicpColorPrimaries::Bt601 | CicpColorPrimaries::Smpte240 => Ok(ColorPrimaries::BT_601), + CicpColorPrimaries::GenericFilm => Ok(ColorPrimaries::GENERIC_FILM), + CicpColorPrimaries::Bt2020 => Ok(ColorPrimaries::BT_2020), + CicpColorPrimaries::Xyz => Ok(ColorPrimaries::XYZ), + // These two share primaries, but have distinct white points + CicpColorPrimaries::Smpte431 | CicpColorPrimaries::Smpte432 => { + Ok(ColorPrimaries::SMPTE_431) + } + CicpColorPrimaries::Ebu3213 => Ok(ColorPrimaries::EBU_3213), + } + } +} + +impl TransferCharacteristics { + pub(crate) fn has_transfer_curve(self) -> bool { + self != Self::Reserved && self != Self::Unspecified + } +} + +pub(crate) fn create_rec709_parametric() -> [f32; 5] { + const POW_EXP: f32 = 0.45; + + const G: f32 = 1. / POW_EXP; + const B: f32 = (0.09929682680944f64 / 1.09929682680944f64) as f32; + const C: f32 = 1f32 / 4.5f32; + const D: f32 = (4.5f64 * 0.018053968510807f64) as f32; + const A: f32 = (1. / 1.09929682680944f64) as f32; + + [G, A, B, C, D] +} + +impl TryFrom for ToneReprCurve { + type Error = CmsError; + /// See [ICC.1:2010](https://www.color.org/specification/ICC1v43_2010-12.pdf) + /// See [Rec. ITU-R BT.2100-2](https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2100-2-201807-I!!PDF-E.pdf) + fn try_from(value: TransferCharacteristics) -> Result { + const NUM_TRC_TABLE_ENTRIES: i32 = 1024; + + Ok(match value { + TransferCharacteristics::Reserved => { + return Err(CmsError::UnsupportedTrc(value as u8)); + } + TransferCharacteristics::Bt709 + | TransferCharacteristics::Bt601 + | TransferCharacteristics::Bt202010bit + | TransferCharacteristics::Bt202012bit => { + // The opto-electronic transfer characteristic function (OETF) + // as defined in ITU-T H.273 table 3, row 1: + // + // V = (α * Lc^0.45) − (α − 1) for 1 >= Lc >= β + // V = 4.500 * Lc for β > Lc >= 0 + // + // Inverting gives the electro-optical transfer characteristic + // function (EOTF) which can be represented as ICC + // parametricCurveType with 4 parameters (ICC.1:2010 Table 5). + // Converting between the two (Lc ↔︎ Y, V ↔︎ X): + // + // Y = (a * X + b)^g for (X >= d) + // Y = c * X for (X < d) + // + // g, a, b, c, d can then be defined in terms of α and β: + // + // g = 1 / 0.45 + // a = 1 / α + // b = 1 - α + // c = 1 / 4.500 + // d = 4.500 * β + // + // α and β are determined by solving the piecewise equations to + // ensure continuity of both value and slope at the value β. + // We use the values specified for 10-bit systems in + // https://www.itu.int/rec/R-REC-BT.2020-2-201510-I Table 4 + // since this results in the similar values as available ICC + // profiles after converting to s15Fixed16Number, providing us + // good test coverage. + + ToneReprCurve::Parametric(create_rec709_parametric().to_vec()) + } + TransferCharacteristics::Unspecified => { + return Err(CmsError::UnsupportedTrc(value as u8)); + } + TransferCharacteristics::Bt470M => curve_from_gamma(2.2), + TransferCharacteristics::Bt470Bg => curve_from_gamma(2.8), + TransferCharacteristics::Smpte240 => { + let table = build_trc_table(NUM_TRC_TABLE_ENTRIES, smpte240_to_linear); + ToneReprCurve::Lut(table) + } + TransferCharacteristics::Linear => curve_from_gamma(1.), + TransferCharacteristics::Log100 => { + let table = build_trc_table(NUM_TRC_TABLE_ENTRIES, log100_to_linear); + ToneReprCurve::Lut(table) + } + TransferCharacteristics::Log100sqrt10 => { + let table = build_trc_table(NUM_TRC_TABLE_ENTRIES, log100_sqrt10_to_linear); + ToneReprCurve::Lut(table) + } + TransferCharacteristics::Iec61966 => { + let table = build_trc_table(NUM_TRC_TABLE_ENTRIES, iec61966_to_linear); + ToneReprCurve::Lut(table) + } + TransferCharacteristics::Bt1361 => { + let table = build_trc_table(NUM_TRC_TABLE_ENTRIES, bt1361_to_linear); + ToneReprCurve::Lut(table) + } + TransferCharacteristics::Srgb => { + ToneReprCurve::Parametric(vec![2.4, 1. / 1.055, 0.055 / 1.055, 1. / 12.92, 0.04045]) + } + TransferCharacteristics::Smpte2084 => { + let table = build_trc_table(NUM_TRC_TABLE_ENTRIES, pq_to_linear); + ToneReprCurve::Lut(table) + } + TransferCharacteristics::Smpte428 => { + let table = build_trc_table(NUM_TRC_TABLE_ENTRIES, smpte428_to_linear); + ToneReprCurve::Lut(table) + } + TransferCharacteristics::Hlg => { + let table = build_trc_table(NUM_TRC_TABLE_ENTRIES, hlg_to_linear); + ToneReprCurve::Lut(table) + } + }) + } +} + +/// Matrix Coefficients Enum (from ISO/IEC 23091-4 / MPEG CICP) +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[repr(C)] +pub enum MatrixCoefficients { + Identity = 0, // RGB (Identity matrix) + Bt709 = 1, // Rec. 709 + Unspecified = 2, // Unspecified + Reserved = 3, // Reserved + Fcc = 4, // FCC + Bt470Bg = 5, // BT.470BG / BT.601-625 + Smpte170m = 6, // SMPTE 170M / BT.601-525 + Smpte240m = 7, // SMPTE 240M + YCgCo = 8, // YCgCo + Bt2020Ncl = 9, // BT.2020 (non-constant luminance) + Bt2020Cl = 10, // BT.2020 (constant luminance) + Smpte2085 = 11, // SMPTE ST 2085 + ChromaticityDerivedNCL = 12, // Chromaticity-derived non-constant luminance + ChromaticityDerivedCL = 13, // Chromaticity-derived constant luminance + ICtCp = 14, // ICtCp +} + +impl TryFrom for MatrixCoefficients { + type Error = CmsError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(MatrixCoefficients::Identity), + 1 => Ok(MatrixCoefficients::Bt709), + 2 => Ok(MatrixCoefficients::Unspecified), + 3 => Ok(MatrixCoefficients::Reserved), + 4 => Ok(MatrixCoefficients::Fcc), + 5 => Ok(MatrixCoefficients::Bt470Bg), + 6 => Ok(MatrixCoefficients::Smpte170m), + 7 => Ok(MatrixCoefficients::Smpte240m), + 8 => Ok(MatrixCoefficients::YCgCo), + 9 => Ok(MatrixCoefficients::Bt2020Ncl), + 10 => Ok(MatrixCoefficients::Bt2020Cl), + 11 => Ok(MatrixCoefficients::Smpte2085), + 12 => Ok(MatrixCoefficients::ChromaticityDerivedNCL), + 13 => Ok(MatrixCoefficients::ChromaticityDerivedCL), + 14 => Ok(MatrixCoefficients::ICtCp), + _ => Err(CmsError::InvalidCicp), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::WHITE_POINT_D65; + + #[test] + fn test_to_xyz_using_absolute_coordinates() { + let conversion_matrix = ColorPrimaries::BT_709.transform_to_xyz_d(WHITE_POINT_D65); + assert!((conversion_matrix.v[0][0] - 0.4121524015214193).abs() < 1e-14); + assert!((conversion_matrix.v[1][1] - 0.7153537403945436).abs() < 1e-14); + assert!((conversion_matrix.v[2][2] - 0.9497138466283235).abs() < 1e-14); + } + + #[test] + fn test_to_xyz_using_absolute_coordinates_xyz() { + let conversion_matrix = ColorPrimaries::XYZ.transform_to_xyz_d(WHITE_POINT_D65); + assert!((conversion_matrix.v[0][0] - 0.95015469385536477).abs() < 1e-14); + assert!((conversion_matrix.v[1][1] - 1.0).abs() < 1e-14); + assert!((conversion_matrix.v[2][2] - 1.0882590676722474).abs() < 1e-14); + } + + #[test] + fn test_to_xyz_using_absolute_coordinates_f() { + let conversion_matrix = ColorPrimaries::BT_709.transform_to_xyz(WHITE_POINT_D65); + assert!((conversion_matrix.v[0][0] - 0.4121524015214193).abs() < 1e-5); + assert!((conversion_matrix.v[1][1] - 0.7153537403945436).abs() < 1e-5); + assert!((conversion_matrix.v[2][2] - 0.9497138466283235).abs() < 1e-5); + } +} diff --git a/deps/moxcms/src/conversions/bpc.rs b/deps/moxcms/src/conversions/bpc.rs new file mode 100644 index 0000000..799d023 --- /dev/null +++ b/deps/moxcms/src/conversions/bpc.rs @@ -0,0 +1,121 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +// +// use crate::conversions::interpolator::{MultidimensionalInterpolation, Tetrahedral}; +// use crate::conversions::transform_lut4_to_4::{NonFiniteVector3fLerp, Vector3fCmykLerp}; +// use crate::mlaf::mlaf; +// use crate::{Chromaticity, ColorProfile, DataColorSpace, Lab, Xyz}; +// +// impl ColorProfile { +// #[inline] +// pub(crate) fn detect_black_point(&self, lut: &[f32]) -> Option { +// if self.color_space == DataColorSpace::Cmyk { +// // if let Some(mut bp) = self.black_point { +// // if let Some(wp) = self.media_white_point.map(|x| x.normalize()) { +// // if wp != Chromaticity::D50.to_xyz() { +// // let ad = adaption_matrix(wp, Chromaticity::D50.to_xyz()); +// // let v = ad.mul_vector(bp.to_vector()); +// // bp = Xyz { +// // x: v.v[0], +// // y: v.v[1], +// // z: v.v[2], +// // }; +// // } +// // } +// // let mut lab = Lab::from_xyz(bp); +// // lab.a = 0.; +// // lab.b = 0.; +// // if lab.l > 50. { +// // lab.l = 50.; +// // } +// // bp = lab.to_xyz(); +// // return Some(bp); +// // } +// let c = 65535; +// let m = 65535; +// let y = 65535; +// let k = 65535; +// +// let linear_k: f32 = k as f32 * (1. / 65535.); +// let w: i32 = k * (GRID_SIZE as i32 - 1) / 65535; +// let w_n: i32 = (w + 1).min(GRID_SIZE as i32 - 1); +// let t: f32 = linear_k * (GRID_SIZE as i32 - 1) as f32 - w as f32; +// +// let grid_size = GRID_SIZE as i32; +// let grid_size3 = grid_size * grid_size * grid_size; +// +// let table1 = &lut[(w * grid_size3 * 3) as usize..]; +// let table2 = &lut[(w_n * grid_size3 * 3) as usize..]; +// +// let tetrahedral1 = Tetrahedral::::new(table1); +// let tetrahedral2 = Tetrahedral::::new(table2); +// let r1 = tetrahedral1.inter3(c, m, y); +// let r2 = tetrahedral2.inter3(c, m, y); +// let r = NonFiniteVector3fLerp::interpolate(r1, r2, t, 1.0); +// +// let mut lab = Lab::from_xyz(Xyz { +// x: r.v[0], +// y: r.v[1], +// z: r.v[2], +// }); +// lab.a = 0.; +// lab.b = 0.; +// if lab.l > 50. { +// lab.l = 50.; +// } +// let bp = lab.to_xyz(); +// +// return Some(bp); +// } +// if self.color_space == DataColorSpace::Rgb { +// return Some(Xyz::new(0.0, 0.0, 0.0)); +// } +// None +// } +// } +// +// pub(crate) fn compensate_bpc_in_lut(lut_xyz: &mut [f32], src_bp: Xyz, dst_bp: Xyz) { +// const WP_50: Xyz = Chromaticity::D50.to_xyz(); +// let tx = src_bp.x - WP_50.x; +// let ty = src_bp.y - WP_50.y; +// let tz = src_bp.z - WP_50.z; +// let ax = (dst_bp.x - WP_50.x) / tx; +// let ay = (dst_bp.y - WP_50.y) / ty; +// let az = (dst_bp.z - WP_50.z) / tz; +// +// let bx = -WP_50.x * (dst_bp.x - src_bp.x) / tx; +// let by = -WP_50.y * (dst_bp.y - src_bp.y) / ty; +// let bz = -WP_50.z * (dst_bp.z - src_bp.z) / tz; +// +// for dst in lut_xyz.chunks_exact_mut(3) { +// dst[0] = mlaf(bx, dst[0], ax); +// dst[1] = mlaf(by, dst[1], ay); +// dst[2] = mlaf(bz, dst[2], az); +// } +// } diff --git a/deps/moxcms/src/conversions/gray2rgb.rs b/deps/moxcms/src/conversions/gray2rgb.rs new file mode 100644 index 0000000..7c4eb95 --- /dev/null +++ b/deps/moxcms/src/conversions/gray2rgb.rs @@ -0,0 +1,388 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::transform::PointeeSizeExpressible; +use crate::{CmsError, Layout, TransformExecutor}; +use num_traits::AsPrimitive; + +#[derive(Clone)] +struct TransformGray2RgbFusedExecutor { + fused_gamma: Box<[T; 65536]>, + bit_depth: usize, +} + +pub(crate) fn make_gray_to_x< + T: Copy + Default + PointeeSizeExpressible + 'static + Send + Sync, + const BUCKET: usize, +>( + src_layout: Layout, + dst_layout: Layout, + gray_linear: &[f32; BUCKET], + gray_gamma: &[T; 65536], + bit_depth: usize, + gamma_lut: usize, +) -> Result + Sync + Send>, CmsError> +where + u32: AsPrimitive, +{ + if src_layout != Layout::Gray && src_layout != Layout::GrayAlpha { + return Err(CmsError::UnsupportedProfileConnection); + } + + let mut fused_gamma = Box::new([T::default(); 65536]); + let max_lut_size = (gamma_lut - 1) as f32; + for (&src, dst) in gray_linear.iter().zip(fused_gamma.iter_mut()) { + let possible_value = ((src * max_lut_size).round() as u32).min(max_lut_size as u32) as u16; + *dst = gray_gamma[possible_value as usize]; + } + + match src_layout { + Layout::Gray => match dst_layout { + Layout::Rgb => Ok(Box::new(TransformGray2RgbFusedExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Rgb as u8 }, + > { + fused_gamma, + bit_depth, + })), + Layout::Rgba => Ok(Box::new(TransformGray2RgbFusedExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Rgba as u8 }, + > { + fused_gamma, + bit_depth, + })), + Layout::Gray => Ok(Box::new(TransformGray2RgbFusedExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Gray as u8 }, + > { + fused_gamma, + bit_depth, + })), + Layout::GrayAlpha => Ok(Box::new(TransformGray2RgbFusedExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::GrayAlpha as u8 }, + > { + fused_gamma, + bit_depth, + })), + _ => unreachable!(), + }, + Layout::GrayAlpha => match dst_layout { + Layout::Rgb => Ok(Box::new(TransformGray2RgbFusedExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::GrayAlpha as u8 }, + > { + fused_gamma, + bit_depth, + })), + Layout::Rgba => Ok(Box::new(TransformGray2RgbFusedExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Rgba as u8 }, + > { + fused_gamma, + bit_depth, + })), + Layout::Gray => Ok(Box::new(TransformGray2RgbFusedExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Gray as u8 }, + > { + fused_gamma, + bit_depth, + })), + Layout::GrayAlpha => Ok(Box::new(TransformGray2RgbFusedExecutor::< + T, + { Layout::GrayAlpha as u8 }, + { Layout::GrayAlpha as u8 }, + > { + fused_gamma, + bit_depth, + })), + _ => unreachable!(), + }, + _ => Err(CmsError::UnsupportedProfileConnection), + } +} + +impl< + T: Copy + Default + PointeeSizeExpressible + 'static, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, +> TransformExecutor for TransformGray2RgbFusedExecutor +where + u32: AsPrimitive, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + let src_cn = Layout::from(SRC_LAYOUT); + let dst_cn = Layout::from(DST_LAYOUT); + let src_channels = src_cn.channels(); + let dst_channels = dst_cn.channels(); + + if src.len() / src_channels != dst.len() / dst_channels { + return Err(CmsError::LaneSizeMismatch); + } + if src.len() % src_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % dst_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let is_gray_alpha = src_cn == Layout::GrayAlpha; + + let max_value: T = ((1u32 << self.bit_depth as u32) - 1u32).as_(); + + for (src, dst) in src + .chunks_exact(src_channels) + .zip(dst.chunks_exact_mut(dst_channels)) + { + let g = self.fused_gamma[src[0]._as_usize()]; + let a = if is_gray_alpha { src[1] } else { max_value }; + + dst[0] = g; + if dst_cn == Layout::GrayAlpha { + dst[1] = a; + } else if dst_cn == Layout::Rgb { + dst[1] = g; + dst[2] = g; + } else if dst_cn == Layout::Rgba { + dst[1] = g; + dst[2] = g; + dst[3] = a; + } + } + + Ok(()) + } +} + +#[derive(Clone)] +struct TransformGrayToRgbExecutor { + gray_linear: Box<[f32; 65536]>, + red_gamma: Box<[T; 65536]>, + green_gamma: Box<[T; 65536]>, + blue_gamma: Box<[T; 65536]>, + bit_depth: usize, + gamma_lut: usize, +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn make_gray_to_unfused< + T: Copy + Default + PointeeSizeExpressible + 'static + Send + Sync, + const BUCKET: usize, +>( + src_layout: Layout, + dst_layout: Layout, + gray_linear: Box<[f32; 65536]>, + red_gamma: Box<[T; 65536]>, + green_gamma: Box<[T; 65536]>, + blue_gamma: Box<[T; 65536]>, + bit_depth: usize, + gamma_lut: usize, +) -> Result + Sync + Send>, CmsError> +where + u32: AsPrimitive, +{ + if src_layout != Layout::Gray && src_layout != Layout::GrayAlpha { + return Err(CmsError::UnsupportedProfileConnection); + } + if dst_layout != Layout::Rgb && dst_layout != Layout::Rgba { + return Err(CmsError::UnsupportedProfileConnection); + } + match src_layout { + Layout::Gray => match dst_layout { + Layout::Rgb => Ok(Box::new(TransformGrayToRgbExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Rgb as u8 }, + > { + gray_linear, + red_gamma, + green_gamma, + blue_gamma, + bit_depth, + gamma_lut, + })), + Layout::Rgba => Ok(Box::new(TransformGrayToRgbExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Rgba as u8 }, + > { + gray_linear, + red_gamma, + green_gamma, + blue_gamma, + bit_depth, + gamma_lut, + })), + Layout::Gray => Ok(Box::new(TransformGrayToRgbExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Gray as u8 }, + > { + gray_linear, + red_gamma, + green_gamma, + blue_gamma, + bit_depth, + gamma_lut, + })), + Layout::GrayAlpha => Ok(Box::new(TransformGrayToRgbExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::GrayAlpha as u8 }, + > { + gray_linear, + red_gamma, + green_gamma, + blue_gamma, + bit_depth, + gamma_lut, + })), + _ => Err(CmsError::UnsupportedProfileConnection), + }, + Layout::GrayAlpha => match dst_layout { + Layout::Rgb => Ok(Box::new(TransformGrayToRgbExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::GrayAlpha as u8 }, + > { + gray_linear, + red_gamma, + green_gamma, + blue_gamma, + bit_depth, + gamma_lut, + })), + Layout::Rgba => Ok(Box::new(TransformGrayToRgbExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Rgba as u8 }, + > { + gray_linear, + red_gamma, + green_gamma, + blue_gamma, + bit_depth, + gamma_lut, + })), + Layout::Gray => Ok(Box::new(TransformGrayToRgbExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Gray as u8 }, + > { + gray_linear, + red_gamma, + green_gamma, + blue_gamma, + bit_depth, + gamma_lut, + })), + Layout::GrayAlpha => Ok(Box::new(TransformGrayToRgbExecutor::< + T, + { Layout::GrayAlpha as u8 }, + { Layout::GrayAlpha as u8 }, + > { + gray_linear, + red_gamma, + green_gamma, + blue_gamma, + bit_depth, + gamma_lut, + })), + _ => Err(CmsError::UnsupportedProfileConnection), + }, + _ => Err(CmsError::UnsupportedProfileConnection), + } +} + +impl< + T: Copy + Default + PointeeSizeExpressible + 'static, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, +> TransformExecutor for TransformGrayToRgbExecutor +where + u32: AsPrimitive, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + let src_cn = Layout::from(SRC_LAYOUT); + let dst_cn = Layout::from(DST_LAYOUT); + let src_channels = src_cn.channels(); + let dst_channels = dst_cn.channels(); + + if src.len() / src_channels != dst.len() / dst_channels { + return Err(CmsError::LaneSizeMismatch); + } + if src.len() % src_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % dst_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let is_gray_alpha = src_cn == Layout::GrayAlpha; + + let max_value: T = ((1u32 << self.bit_depth as u32) - 1u32).as_(); + let max_lut_size = (self.gamma_lut - 1) as f32; + + for (src, dst) in src + .chunks_exact(src_channels) + .zip(dst.chunks_exact_mut(dst_channels)) + { + let g = self.gray_linear[src[0]._as_usize()]; + let a = if is_gray_alpha { src[1] } else { max_value }; + + let possible_value = ((g * max_lut_size).round() as u16) as usize; + let red_value = self.red_gamma[possible_value]; + let green_value = self.green_gamma[possible_value]; + let blue_value = self.blue_gamma[possible_value]; + + if dst_cn == Layout::Rgb { + dst[0] = red_value; + dst[1] = green_value; + dst[2] = blue_value; + } else if dst_cn == Layout::Rgba { + dst[0] = red_value; + dst[1] = green_value; + dst[2] = blue_value; + dst[3] = a; + } else { + return Err(CmsError::UnsupportedProfileConnection); + } + } + + Ok(()) + } +} diff --git a/deps/moxcms/src/conversions/gray2rgb_extended.rs b/deps/moxcms/src/conversions/gray2rgb_extended.rs new file mode 100644 index 0000000..9d4f95d --- /dev/null +++ b/deps/moxcms/src/conversions/gray2rgb_extended.rs @@ -0,0 +1,383 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 7/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::transform::PointeeSizeExpressible; +use crate::trc::ToneCurveEvaluator; +use crate::{CmsError, Layout, Rgb, TransformExecutor}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +struct TransformGrayOneToOneExecutor { + linear_eval: Box, + gamma_eval: Box, + _phantom: PhantomData, + bit_depth: usize, +} + +pub(crate) fn make_gray_to_one_trc_extended< + T: Copy + Default + PointeeSizeExpressible + 'static + Send + Sync + AsPrimitive, +>( + src_layout: Layout, + dst_layout: Layout, + linear_eval: Box, + gamma_eval: Box, + bit_depth: usize, +) -> Result + Sync + Send>, CmsError> +where + u32: AsPrimitive, + f32: AsPrimitive, +{ + if src_layout != Layout::Gray && src_layout != Layout::GrayAlpha { + return Err(CmsError::UnsupportedProfileConnection); + } + + match src_layout { + Layout::Gray => match dst_layout { + Layout::Rgb => Ok(Box::new(TransformGrayOneToOneExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Rgb as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + Layout::Rgba => Ok(Box::new(TransformGrayOneToOneExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Rgba as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + Layout::Gray => Ok(Box::new(TransformGrayOneToOneExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Gray as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + Layout::GrayAlpha => Ok(Box::new(TransformGrayOneToOneExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::GrayAlpha as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + _ => unreachable!(), + }, + Layout::GrayAlpha => match dst_layout { + Layout::Rgb => Ok(Box::new(TransformGrayOneToOneExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::GrayAlpha as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + Layout::Rgba => Ok(Box::new(TransformGrayOneToOneExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Rgba as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + Layout::Gray => Ok(Box::new(TransformGrayOneToOneExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Gray as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + Layout::GrayAlpha => Ok(Box::new(TransformGrayOneToOneExecutor::< + T, + { Layout::GrayAlpha as u8 }, + { Layout::GrayAlpha as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + _ => unreachable!(), + }, + _ => Err(CmsError::UnsupportedProfileConnection), + } +} + +impl< + T: Copy + Default + PointeeSizeExpressible + 'static + AsPrimitive, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, +> TransformExecutor for TransformGrayOneToOneExecutor +where + u32: AsPrimitive, + f32: AsPrimitive, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + let src_cn = Layout::from(SRC_LAYOUT); + let dst_cn = Layout::from(DST_LAYOUT); + let src_channels = src_cn.channels(); + let dst_channels = dst_cn.channels(); + + if src.len() / src_channels != dst.len() / dst_channels { + return Err(CmsError::LaneSizeMismatch); + } + if src.len() % src_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % dst_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let is_gray_alpha = src_cn == Layout::GrayAlpha; + + let max_value: T = ((1u32 << self.bit_depth as u32) - 1u32).as_(); + + for (src, dst) in src + .chunks_exact(src_channels) + .zip(dst.chunks_exact_mut(dst_channels)) + { + let linear_value = self.linear_eval.evaluate_value(src[0].as_()); + let g = self.gamma_eval.evaluate_value(linear_value).as_(); + let a = if is_gray_alpha { src[1] } else { max_value }; + + dst[0] = g; + if dst_cn == Layout::GrayAlpha { + dst[1] = a; + } else if dst_cn == Layout::Rgb { + dst[1] = g; + dst[2] = g; + } else if dst_cn == Layout::Rgba { + dst[1] = g; + dst[2] = g; + dst[3] = a; + } + } + + Ok(()) + } +} + +struct TransformGrayToRgbExtendedExecutor { + linear_eval: Box, + gamma_eval: Box, + _phantom: PhantomData, + bit_depth: usize, +} + +pub(crate) fn make_gray_to_rgb_extended< + T: Copy + Default + PointeeSizeExpressible + 'static + Send + Sync + AsPrimitive, +>( + src_layout: Layout, + dst_layout: Layout, + linear_eval: Box, + gamma_eval: Box, + bit_depth: usize, +) -> Result + Sync + Send>, CmsError> +where + u32: AsPrimitive, + f32: AsPrimitive, +{ + if src_layout != Layout::Gray && src_layout != Layout::GrayAlpha { + return Err(CmsError::UnsupportedProfileConnection); + } + if dst_layout != Layout::Rgb && dst_layout != Layout::Rgba { + return Err(CmsError::UnsupportedProfileConnection); + } + match src_layout { + Layout::Gray => match dst_layout { + Layout::Rgb => Ok(Box::new(TransformGrayToRgbExtendedExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Rgb as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + Layout::Rgba => Ok(Box::new(TransformGrayToRgbExtendedExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Rgba as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + Layout::Gray => Ok(Box::new(TransformGrayToRgbExtendedExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Gray as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + Layout::GrayAlpha => Ok(Box::new(TransformGrayToRgbExtendedExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::GrayAlpha as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + _ => Err(CmsError::UnsupportedProfileConnection), + }, + Layout::GrayAlpha => match dst_layout { + Layout::Rgb => Ok(Box::new(TransformGrayToRgbExtendedExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::GrayAlpha as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + Layout::Rgba => Ok(Box::new(TransformGrayToRgbExtendedExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Rgba as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + Layout::Gray => Ok(Box::new(TransformGrayToRgbExtendedExecutor::< + T, + { Layout::Gray as u8 }, + { Layout::Gray as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + Layout::GrayAlpha => Ok(Box::new(TransformGrayToRgbExtendedExecutor::< + T, + { Layout::GrayAlpha as u8 }, + { Layout::GrayAlpha as u8 }, + > { + linear_eval, + gamma_eval, + _phantom: PhantomData, + bit_depth, + })), + _ => Err(CmsError::UnsupportedProfileConnection), + }, + _ => Err(CmsError::UnsupportedProfileConnection), + } +} + +impl< + T: Copy + Default + PointeeSizeExpressible + 'static + AsPrimitive, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, +> TransformExecutor for TransformGrayToRgbExtendedExecutor +where + u32: AsPrimitive, + f32: AsPrimitive, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + let src_cn = Layout::from(SRC_LAYOUT); + let dst_cn = Layout::from(DST_LAYOUT); + let src_channels = src_cn.channels(); + let dst_channels = dst_cn.channels(); + + if src.len() / src_channels != dst.len() / dst_channels { + return Err(CmsError::LaneSizeMismatch); + } + if src.len() % src_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % dst_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let is_gray_alpha = src_cn == Layout::GrayAlpha; + + let max_value: T = ((1u32 << self.bit_depth as u32) - 1u32).as_(); + + for (src, dst) in src + .chunks_exact(src_channels) + .zip(dst.chunks_exact_mut(dst_channels)) + { + let linear_value = self.linear_eval.evaluate_value(src[0].as_()); + let a = if is_gray_alpha { src[1] } else { max_value }; + + let tristimulus = self.gamma_eval.evaluate_tristimulus(Rgb::new( + linear_value, + linear_value, + linear_value, + )); + + let red_value = tristimulus.r.as_(); + let green_value = tristimulus.g.as_(); + let blue_value = tristimulus.b.as_(); + + if dst_cn == Layout::Rgb { + dst[0] = red_value; + dst[1] = green_value; + dst[2] = blue_value; + } else if dst_cn == Layout::Rgba { + dst[0] = red_value; + dst[1] = green_value; + dst[2] = blue_value; + dst[3] = a; + } else { + return Err(CmsError::UnsupportedProfileConnection); + } + } + + Ok(()) + } +} diff --git a/deps/moxcms/src/conversions/interpolator.rs b/deps/moxcms/src/conversions/interpolator.rs new file mode 100644 index 0000000..b9c7dda --- /dev/null +++ b/deps/moxcms/src/conversions/interpolator.rs @@ -0,0 +1,599 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#![allow(dead_code)] +use crate::conversions::lut_transforms::LUT_SAMPLING; +use crate::math::{FusedMultiplyAdd, FusedMultiplyNegAdd}; +use crate::{Vector3f, Vector4f}; +use std::ops::{Add, Mul, Sub}; + +#[cfg(feature = "options")] +pub(crate) struct Tetrahedral {} + +#[cfg(feature = "options")] +pub(crate) struct Pyramidal {} + +#[cfg(feature = "options")] +pub(crate) struct Prismatic {} + +pub(crate) struct Trilinear {} + +#[derive(Debug, Copy, Clone, Default)] +pub(crate) struct BarycentricWeight { + pub x: i32, + pub x_n: i32, + pub w: V, +} + +impl BarycentricWeight { + pub(crate) fn create_ranged_256() -> Box<[BarycentricWeight; 256]> + { + let mut weights = Box::new([BarycentricWeight::default(); 256]); + for (index, weight) in weights.iter_mut().enumerate() { + const SCALE: f32 = 1.0 / LUT_SAMPLING as f32; + let x: i32 = index as i32 * (GRID_SIZE as i32 - 1) / LUT_SAMPLING as i32; + + let x_n: i32 = (x + 1).min(GRID_SIZE as i32 - 1); + + let scale = (GRID_SIZE as i32 - 1) as f32 * SCALE; + + let dr = index as f32 * scale - x as f32; + *weight = BarycentricWeight { x, x_n, w: dr }; + } + weights + } + + #[cfg(feature = "options")] + pub(crate) fn create_binned() + -> Box<[BarycentricWeight; 65536]> { + let mut weights = Box::new([BarycentricWeight::::default(); 65536]); + let b_scale: f32 = 1.0 / (BINS - 1) as f32; + for (index, weight) in weights.iter_mut().enumerate().take(BINS) { + let x: i32 = (index as f32 * (GRID_SIZE as i32 - 1) as f32 * b_scale).floor() as i32; + + let x_n: i32 = (x + 1).min(GRID_SIZE as i32 - 1); + + let scale = (GRID_SIZE as i32 - 1) as f32 * b_scale; + + let dr = index as f32 * scale - x as f32; + *weight = BarycentricWeight { x, x_n, w: dr }; + } + weights + } +} + +#[allow(dead_code)] +impl BarycentricWeight { + pub(crate) fn create_ranged_256() -> Box<[BarycentricWeight; 256]> + { + let mut weights = Box::new([BarycentricWeight::default(); 256]); + for (index, weight) in weights.iter_mut().enumerate() { + const SCALE: f32 = 1.0 / LUT_SAMPLING as f32; + let x: i32 = index as i32 * (GRID_SIZE as i32 - 1) / LUT_SAMPLING as i32; + + let x_n: i32 = (x + 1).min(GRID_SIZE as i32 - 1); + + let scale = (GRID_SIZE as i32 - 1) as f32 * SCALE; + + const Q: f32 = ((1i32 << 15) - 1) as f32; + + let dr = ((index as f32 * scale - x as f32) * Q) + .round() + .min(i16::MAX as f32) + .max(-i16::MAX as f32) as i16; + *weight = BarycentricWeight { x, x_n, w: dr }; + } + weights + } + + #[cfg(feature = "options")] + pub(crate) fn create_binned() + -> Box<[BarycentricWeight; 65536]> { + let mut weights = Box::new([BarycentricWeight::::default(); 65536]); + let b_scale: f32 = 1.0 / (BINS - 1) as f32; + for (index, weight) in weights.iter_mut().enumerate().take(BINS) { + let x: i32 = (index as f32 * (GRID_SIZE as i32 - 1) as f32 * b_scale).floor() as i32; + + let x_n: i32 = (x + 1).min(GRID_SIZE as i32 - 1); + + let scale = (GRID_SIZE as i32 - 1) as f32 * b_scale; + + const Q: f32 = ((1i32 << 15) - 1) as f32; + + let dr = ((index as f32 * scale - x as f32) * Q) + .round() + .min(i16::MAX as f32) + .max(-i16::MAX as f32) as i16; + *weight = BarycentricWeight { x, x_n, w: dr }; + } + weights + } +} + +trait Fetcher { + fn fetch(&self, x: i32, y: i32, z: i32) -> T; +} + +struct TetrahedralFetchVector3f<'a, const GRID_SIZE: usize> { + cube: &'a [f32], +} + +pub(crate) trait MultidimensionalInterpolation { + fn inter3( + &self, + cube: &[f32], + lut_r: &BarycentricWeight, + lut_g: &BarycentricWeight, + lut_b: &BarycentricWeight, + ) -> Vector3f; + fn inter4( + &self, + cube: &[f32], + lut_r: &BarycentricWeight, + lut_g: &BarycentricWeight, + lut_b: &BarycentricWeight, + ) -> Vector4f; +} + +impl Fetcher for TetrahedralFetchVector3f<'_, GRID_SIZE> { + #[inline(always)] + fn fetch(&self, x: i32, y: i32, z: i32) -> Vector3f { + let offset = (x as u32 * (GRID_SIZE as u32 * GRID_SIZE as u32) + + y as u32 * GRID_SIZE as u32 + + z as u32) as usize + * 3; + let jx = &self.cube[offset..offset + 3]; + Vector3f { + v: [jx[0], jx[1], jx[2]], + } + } +} + +struct TetrahedralFetchVector4f<'a, const GRID_SIZE: usize> { + cube: &'a [f32], +} + +impl Fetcher for TetrahedralFetchVector4f<'_, GRID_SIZE> { + #[inline(always)] + fn fetch(&self, x: i32, y: i32, z: i32) -> Vector4f { + let offset = (x as u32 * (GRID_SIZE as u32 * GRID_SIZE as u32) + + y as u32 * GRID_SIZE as u32 + + z as u32) as usize + * 4; + let jx = &self.cube[offset..offset + 4]; + Vector4f { + v: [jx[0], jx[1], jx[2], jx[3]], + } + } +} + +#[cfg(feature = "options")] +impl Tetrahedral { + #[inline] + fn interpolate< + T: Copy + + Sub + + Mul + + Mul + + Add + + From + + FusedMultiplyAdd, + >( + &self, + lut_r: &BarycentricWeight, + lut_g: &BarycentricWeight, + lut_b: &BarycentricWeight, + r: impl Fetcher, + ) -> T { + let x: i32 = lut_r.x; + let y: i32 = lut_g.x; + let z: i32 = lut_b.x; + + let x_n: i32 = lut_r.x_n; + let y_n: i32 = lut_g.x_n; + let z_n: i32 = lut_b.x_n; + + let rx = lut_r.w; + let ry = lut_g.w; + let rz = lut_b.w; + + let c0 = r.fetch(x, y, z); + let c2; + let c1; + let c3; + if rx >= ry { + if ry >= rz { + //rx >= ry && ry >= rz + c1 = r.fetch(x_n, y, z) - c0; + c2 = r.fetch(x_n, y_n, z) - r.fetch(x_n, y, z); + c3 = r.fetch(x_n, y_n, z_n) - r.fetch(x_n, y_n, z); + } else if rx >= rz { + //rx >= rz && rz >= ry + c1 = r.fetch(x_n, y, z) - c0; + c2 = r.fetch(x_n, y_n, z_n) - r.fetch(x_n, y, z_n); + c3 = r.fetch(x_n, y, z_n) - r.fetch(x_n, y, z); + } else { + //rz > rx && rx >= ry + c1 = r.fetch(x_n, y, z_n) - r.fetch(x, y, z_n); + c2 = r.fetch(x_n, y_n, z_n) - r.fetch(x_n, y, z_n); + c3 = r.fetch(x, y, z_n) - c0; + } + } else if rx >= rz { + //ry > rx && rx >= rz + c1 = r.fetch(x_n, y_n, z) - r.fetch(x, y_n, z); + c2 = r.fetch(x, y_n, z) - c0; + c3 = r.fetch(x_n, y_n, z_n) - r.fetch(x_n, y_n, z); + } else if ry >= rz { + //ry >= rz && rz > rx + c1 = r.fetch(x_n, y_n, z_n) - r.fetch(x, y_n, z_n); + c2 = r.fetch(x, y_n, z) - c0; + c3 = r.fetch(x, y_n, z_n) - r.fetch(x, y_n, z); + } else { + //rz > ry && ry > rx + c1 = r.fetch(x_n, y_n, z_n) - r.fetch(x, y_n, z_n); + c2 = r.fetch(x, y_n, z_n) - r.fetch(x, y, z_n); + c3 = r.fetch(x, y, z_n) - c0; + } + let s0 = c0.mla(c1, T::from(rx)); + let s1 = s0.mla(c2, T::from(ry)); + s1.mla(c3, T::from(rz)) + } +} + +macro_rules! define_md_inter { + ($interpolator: ident) => { + impl MultidimensionalInterpolation for $interpolator { + fn inter3( + &self, + cube: &[f32], + lut_r: &BarycentricWeight, + lut_g: &BarycentricWeight, + lut_b: &BarycentricWeight, + ) -> Vector3f { + self.interpolate::( + lut_r, + lut_g, + lut_b, + TetrahedralFetchVector3f:: { cube }, + ) + } + + fn inter4( + &self, + cube: &[f32], + lut_r: &BarycentricWeight, + lut_g: &BarycentricWeight, + lut_b: &BarycentricWeight, + ) -> Vector4f { + self.interpolate::( + lut_r, + lut_g, + lut_b, + TetrahedralFetchVector4f:: { cube }, + ) + } + } + }; +} + +#[cfg(feature = "options")] +define_md_inter!(Tetrahedral); +#[cfg(feature = "options")] +define_md_inter!(Pyramidal); +#[cfg(feature = "options")] +define_md_inter!(Prismatic); +define_md_inter!(Trilinear); + +#[cfg(feature = "options")] +impl Pyramidal { + #[inline] + fn interpolate< + T: Copy + + Sub + + Mul + + Mul + + Add + + From + + FusedMultiplyAdd, + >( + &self, + lut_r: &BarycentricWeight, + lut_g: &BarycentricWeight, + lut_b: &BarycentricWeight, + r: impl Fetcher, + ) -> T { + let x: i32 = lut_r.x; + let y: i32 = lut_g.x; + let z: i32 = lut_b.x; + + let x_n: i32 = lut_r.x_n; + let y_n: i32 = lut_g.x_n; + let z_n: i32 = lut_b.x_n; + + let dr = lut_r.w; + let dg = lut_g.w; + let db = lut_b.w; + + let c0 = r.fetch(x, y, z); + + if dr > db && dg > db { + let x0 = r.fetch(x_n, y_n, z_n); + let x1 = r.fetch(x_n, y_n, z); + let x2 = r.fetch(x_n, y, z); + let x3 = r.fetch(x, y_n, z); + + let c1 = x0 - x1; + let c2 = x2 - c0; + let c3 = x3 - c0; + let c4 = c0 - x3 - x2 + x1; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + s2.mla(c4, T::from(dr * dg)) + } else if db > dr && dg > dr { + let x0 = r.fetch(x, y, z_n); + let x1 = r.fetch(x_n, y_n, z_n); + let x2 = r.fetch(x, y_n, z_n); + let x3 = r.fetch(x, y_n, z); + + let c1 = x0 - c0; + let c2 = x1 - x2; + let c3 = x3 - c0; + let c4 = c0 - x3 - x0 + x2; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + s2.mla(c4, T::from(dg * db)) + } else { + let x0 = r.fetch(x, y, z_n); + let x1 = r.fetch(x_n, y, z); + let x2 = r.fetch(x_n, y, z_n); + let x3 = r.fetch(x_n, y_n, z_n); + + let c1 = x0 - c0; + let c2 = x1 - c0; + let c3 = x3 - x2; + let c4 = c0 - x1 - x0 + x2; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + s2.mla(c4, T::from(db * dr)) + } + } +} + +#[cfg(feature = "options")] +impl Prismatic { + #[inline(always)] + fn interpolate< + T: Copy + + Sub + + Mul + + Mul + + Add + + From + + FusedMultiplyAdd, + >( + &self, + lut_r: &BarycentricWeight, + lut_g: &BarycentricWeight, + lut_b: &BarycentricWeight, + r: impl Fetcher, + ) -> T { + let x: i32 = lut_r.x; + let y: i32 = lut_g.x; + let z: i32 = lut_b.x; + + let x_n: i32 = lut_r.x_n; + let y_n: i32 = lut_g.x_n; + let z_n: i32 = lut_b.x_n; + + let dr = lut_r.w; + let dg = lut_g.w; + let db = lut_b.w; + + let c0 = r.fetch(x, y, z); + + if db >= dr { + let x0 = r.fetch(x, y, z_n); + let x1 = r.fetch(x_n, y, z_n); + let x2 = r.fetch(x, y_n, z); + let x3 = r.fetch(x, y_n, z_n); + let x4 = r.fetch(x_n, y_n, z_n); + + let c1 = x0 - c0; + let c2 = x1 - x0; + let c3 = x2 - c0; + let c4 = c0 - x2 - x0 + x3; + let c5 = x0 - x3 - x1 + x4; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + let s3 = s2.mla(c4, T::from(dg * db)); + s3.mla(c5, T::from(dr * dg)) + } else { + let x0 = r.fetch(x_n, y, z); + let x1 = r.fetch(x_n, y, z_n); + let x2 = r.fetch(x, y_n, z); + let x3 = r.fetch(x_n, y_n, z); + let x4 = r.fetch(x_n, y_n, z_n); + + let c1 = x1 - x0; + let c2 = x0 - c0; + let c3 = x2 - c0; + let c4 = x0 - x3 - x1 + x4; + let c5 = c0 - x2 - x0 + x3; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + let s3 = s2.mla(c4, T::from(dg * db)); + s3.mla(c5, T::from(dr * dg)) + } + } +} + +impl Trilinear { + #[inline(always)] + fn interpolate< + T: Copy + + Sub + + Mul + + Mul + + Add + + From + + FusedMultiplyAdd + + FusedMultiplyNegAdd, + >( + &self, + lut_r: &BarycentricWeight, + lut_g: &BarycentricWeight, + lut_b: &BarycentricWeight, + r: impl Fetcher, + ) -> T { + let x: i32 = lut_r.x; + let y: i32 = lut_g.x; + let z: i32 = lut_b.x; + + let x_n: i32 = lut_r.x_n; + let y_n: i32 = lut_g.x_n; + let z_n: i32 = lut_b.x_n; + + let dr = lut_r.w; + let dg = lut_g.w; + let db = lut_b.w; + + let w0 = T::from(dr); + let w1 = T::from(dg); + let w2 = T::from(db); + + let c000 = r.fetch(x, y, z); + let c100 = r.fetch(x_n, y, z); + let c010 = r.fetch(x, y_n, z); + let c110 = r.fetch(x_n, y_n, z); + let c001 = r.fetch(x, y, z_n); + let c101 = r.fetch(x_n, y, z_n); + let c011 = r.fetch(x, y_n, z_n); + let c111 = r.fetch(x_n, y_n, z_n); + + let dx = T::from(dr); + + let c00 = c000.neg_mla(c000, dx).mla(c100, w0); + let c10 = c010.neg_mla(c010, dx).mla(c110, w0); + let c01 = c001.neg_mla(c001, dx).mla(c101, w0); + let c11 = c011.neg_mla(c011, dx).mla(c111, w0); + + let dy = T::from(dg); + + let c0 = c00.neg_mla(c00, dy).mla(c10, w1); + let c1 = c01.neg_mla(c01, dy).mla(c11, w1); + + let dz = T::from(db); + + c0.neg_mla(c0, dz).mla(c1, w2) + } +} + +pub(crate) trait LutBarycentricReduction { + fn reduce(v: T) -> U; +} + +impl LutBarycentricReduction for () { + #[inline(always)] + fn reduce(v: u8) -> u8 { + v + } +} + +impl LutBarycentricReduction for () { + #[inline(always)] + fn reduce(v: u8) -> u16 { + if BINS == 65536 { + return u16::from_ne_bytes([v, v]); + } + if BINS == 16384 { + return u16::from_ne_bytes([v, v]) >> 2; + } + unimplemented!() + } +} + +impl LutBarycentricReduction for () { + #[inline(always)] + fn reduce(v: f32) -> u8 { + (v * 255.).round().min(255.).max(0.) as u8 + } +} + +impl LutBarycentricReduction for () { + #[inline(always)] + fn reduce(v: f32) -> u16 { + let scale = (BINS - 1) as f32; + (v * scale).round().min(scale).max(0.) as u16 + } +} + +impl LutBarycentricReduction for () { + #[inline(always)] + fn reduce(v: f64) -> u8 { + (v * 255.).round().min(255.).max(0.) as u8 + } +} + +impl LutBarycentricReduction for () { + #[inline(always)] + fn reduce(v: f64) -> u16 { + let scale = (BINS - 1) as f64; + (v * scale).round().min(scale).max(0.) as u16 + } +} + +impl LutBarycentricReduction for () { + #[inline(always)] + fn reduce(v: u16) -> u16 { + let src_scale = 1. / ((1 << SRC_BP) - 1) as f32; + let scale = src_scale * (BINS - 1) as f32; + (v as f32 * scale).round().min(scale).max(0.) as u16 + } +} + +impl LutBarycentricReduction for () { + #[inline(always)] + fn reduce(v: u16) -> u8 { + let shift = SRC_BP as u16 - 8; + if SRC_BP == 16 { + (v >> 8) as u8 + } else { + (v >> shift).min(255) as u8 + } + } +} diff --git a/deps/moxcms/src/conversions/katana/finalizers.rs b/deps/moxcms/src/conversions/katana/finalizers.rs new file mode 100644 index 0000000..3527821 --- /dev/null +++ b/deps/moxcms/src/conversions/katana/finalizers.rs @@ -0,0 +1,118 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 8/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::katana::KatanaPostFinalizationStage; +use crate::{CmsError, DataColorSpace, Layout, PointeeSizeExpressible}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +pub(crate) struct InjectAlphaStage { + pub(crate) dst_layout: Layout, + pub(crate) target_color_space: DataColorSpace, + pub(crate) _phantom: PhantomData, + pub(crate) bit_depth: usize, +} + +pub(crate) struct CopyAlphaStage { + pub(crate) src_layout: Layout, + pub(crate) dst_layout: Layout, + pub(crate) target_color_space: DataColorSpace, + pub(crate) _phantom: PhantomData, +} + +impl + PointeeSizeExpressible + Send + Sync> + KatanaPostFinalizationStage for InjectAlphaStage +where + f32: AsPrimitive, +{ + fn finalize(&self, _: &[T], dst: &mut [T]) -> Result<(), CmsError> { + let norm_value: T = (if T::FINITE { + ((1u32 << self.bit_depth) - 1) as f32 + } else { + 1.0 + }) + .as_(); + if self.dst_layout == Layout::Rgba && self.target_color_space == DataColorSpace::Rgb { + for dst in dst.chunks_exact_mut(self.dst_layout.channels()) { + dst[3] = norm_value; + } + } else if self.dst_layout == Layout::GrayAlpha + && self.target_color_space == DataColorSpace::Gray + { + for dst in dst.chunks_exact_mut(self.dst_layout.channels()) { + dst[1] = norm_value; + } + } + Ok(()) + } +} + +impl + PointeeSizeExpressible + Send + Sync> + KatanaPostFinalizationStage for CopyAlphaStage +where + f32: AsPrimitive, +{ + fn finalize(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + if self.dst_layout == Layout::Rgba && self.target_color_space == DataColorSpace::Rgb { + if self.src_layout == Layout::Rgba { + for (src, dst) in src + .chunks_exact(self.src_layout.channels()) + .zip(dst.chunks_exact_mut(self.dst_layout.channels())) + { + dst[3] = src[3]; + } + } else if self.src_layout == Layout::GrayAlpha { + for (src, dst) in src + .chunks_exact(self.src_layout.channels()) + .zip(dst.chunks_exact_mut(self.dst_layout.channels())) + { + dst[3] = src[1]; + } + } + } else if self.dst_layout == Layout::GrayAlpha + && self.target_color_space == DataColorSpace::Gray + { + if self.src_layout == Layout::Rgba { + for (src, dst) in src + .chunks_exact(self.src_layout.channels()) + .zip(dst.chunks_exact_mut(self.dst_layout.channels())) + { + dst[1] = src[3]; + } + } else if self.src_layout == Layout::GrayAlpha { + for (src, dst) in src + .chunks_exact(self.src_layout.channels()) + .zip(dst.chunks_exact_mut(self.dst_layout.channels())) + { + dst[1] = src[1]; + } + } + } + Ok(()) + } +} diff --git a/deps/moxcms/src/conversions/katana/md3x3.rs b/deps/moxcms/src/conversions/katana/md3x3.rs new file mode 100644 index 0000000..bc0f875 --- /dev/null +++ b/deps/moxcms/src/conversions/katana/md3x3.rs @@ -0,0 +1,483 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::katana::{KatanaFinalStage, KatanaInitialStage}; +use crate::mlaf::mlaf; +use crate::safe_math::SafeMul; +use crate::trc::lut_interp_linear_float; +use crate::{ + CmsError, Cube, DataColorSpace, InterpolationMethod, LutMultidimensionalType, MalformedSize, + Matrix3d, Matrix3f, PointeeSizeExpressible, TransformOptions, Vector3d, Vector3f, +}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] +pub(crate) enum MultidimensionalDirection { + DeviceToPcs, + PcsToDevice, +} + +struct Multidimensional3x3< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +> { + a_curves: Option; 3]>>, + m_curves: Option; 3]>>, + b_curves: Option; 3]>>, + clut: Option>, + matrix: Matrix3f, + bias: Vector3f, + direction: MultidimensionalDirection, + options: TransformOptions, + pcs: DataColorSpace, + grid_size: [u8; 3], + _phantom: PhantomData, + bit_depth: usize, +} + +impl + PointeeSizeExpressible + Send + Sync> + Multidimensional3x3 +{ + fn execute_matrix_stage(&self, dst: &mut [f32]) { + let m = self.matrix; + let b = self.bias; + + if !m.test_equality(Matrix3f::IDENTITY) || !b.eq(&Vector3f::default()) { + for dst in dst.chunks_exact_mut(3) { + let x = dst[0]; + let y = dst[1]; + let z = dst[2]; + dst[0] = mlaf(mlaf(mlaf(b.v[0], x, m.v[0][0]), y, m.v[0][1]), z, m.v[0][2]); + dst[1] = mlaf(mlaf(mlaf(b.v[1], x, m.v[1][0]), y, m.v[1][1]), z, m.v[1][2]); + dst[2] = mlaf(mlaf(mlaf(b.v[2], x, m.v[2][0]), y, m.v[2][1]), z, m.v[2][2]); + } + } + } + + fn execute_simple_curves(&self, dst: &mut [f32], curves: &[Vec; 3]) { + let curve0 = &curves[0]; + let curve1 = &curves[1]; + let curve2 = &curves[2]; + + for dst in dst.chunks_exact_mut(3) { + let a0 = dst[0]; + let a1 = dst[1]; + let a2 = dst[2]; + let b0 = lut_interp_linear_float(a0, curve0); + let b1 = lut_interp_linear_float(a1, curve1); + let b2 = lut_interp_linear_float(a2, curve2); + dst[0] = b0; + dst[1] = b1; + dst[2] = b2; + } + } + + fn to_pcs_impl Vector3f>( + &self, + input: &[T], + dst: &mut [f32], + fetch: Fetch, + ) -> Result<(), CmsError> { + let norm_value = if T::FINITE { + 1.0 / ((1u32 << self.bit_depth) - 1) as f32 + } else { + 1.0 + }; + assert_eq!( + self.direction, + MultidimensionalDirection::DeviceToPcs, + "PCS to device cannot be used on `to pcs` stage" + ); + + // A -> B + // OR B - A A - curves stage + + if let (Some(a_curves), Some(clut)) = (self.a_curves.as_ref(), self.clut.as_ref()) { + if !clut.is_empty() { + let curve0 = &a_curves[0]; + let curve1 = &a_curves[1]; + let curve2 = &a_curves[2]; + for (src, dst) in input.chunks_exact(3).zip(dst.chunks_exact_mut(3)) { + let b0 = lut_interp_linear_float(src[0].as_() * norm_value, curve0); + let b1 = lut_interp_linear_float(src[1].as_() * norm_value, curve1); + let b2 = lut_interp_linear_float(src[2].as_() * norm_value, curve2); + let interpolated = fetch(b0, b1, b2); + dst[0] = interpolated.v[0]; + dst[1] = interpolated.v[1]; + dst[2] = interpolated.v[2]; + } + } else { + for (src, dst) in input.chunks_exact(3).zip(dst.chunks_exact_mut(3)) { + dst[0] = src[0].as_() * norm_value; + dst[1] = src[1].as_() * norm_value; + dst[2] = src[2].as_() * norm_value; + } + } + } else { + for (src, dst) in input.chunks_exact(3).zip(dst.chunks_exact_mut(3)) { + dst[0] = src[0].as_() * norm_value; + dst[1] = src[1].as_() * norm_value; + dst[2] = src[2].as_() * norm_value; + } + } + + // Matrix stage + + if let Some(m_curves) = self.m_curves.as_ref() { + self.execute_simple_curves(dst, m_curves); + self.execute_matrix_stage(dst); + } + + // B-curves is mandatory + if let Some(b_curves) = &self.b_curves.as_ref() { + self.execute_simple_curves(dst, b_curves); + } + + Ok(()) + } +} + +impl + PointeeSizeExpressible + Send + Sync> + KatanaInitialStage for Multidimensional3x3 +{ + fn to_pcs(&self, input: &[T]) -> Result, CmsError> { + if input.len() % 3 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + let fixed_new_clut = Vec::new(); + let new_clut = self.clut.as_ref().unwrap_or(&fixed_new_clut); + let lut = Cube::new_cube(new_clut, self.grid_size); + + let mut new_dst = vec![0f32; input.len()]; + + // If PCS is LAB then linear interpolation should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + self.to_pcs_impl(input, &mut new_dst, |x, y, z| lut.trilinear_vec3(x, y, z))?; + return Ok(new_dst); + } + + match self.options.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.to_pcs_impl(input, &mut new_dst, |x, y, z| lut.tetra_vec3(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.to_pcs_impl(input, &mut new_dst, |x, y, z| lut.pyramid_vec3(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.to_pcs_impl(input, &mut new_dst, |x, y, z| lut.prism_vec3(x, y, z))?; + } + InterpolationMethod::Linear => { + self.to_pcs_impl(input, &mut new_dst, |x, y, z| lut.trilinear_vec3(x, y, z))?; + } + } + Ok(new_dst) + } +} + +impl + PointeeSizeExpressible + Send + Sync> + Multidimensional3x3 +where + f32: AsPrimitive, +{ + fn to_output_impl Vector3f>( + &self, + src: &mut [f32], + dst: &mut [T], + fetch: Fetch, + ) -> Result<(), CmsError> { + let norm_value = if T::FINITE { + ((1u32 << self.bit_depth) - 1) as f32 + } else { + 1.0 + }; + assert_eq!( + self.direction, + MultidimensionalDirection::PcsToDevice, + "Device to PCS cannot be used on `to output` stage" + ); + + if let Some(b_curves) = &self.b_curves.as_ref() { + self.execute_simple_curves(src, b_curves); + } + + // Matrix stage + + if let Some(m_curves) = self.m_curves.as_ref() { + self.execute_matrix_stage(src); + self.execute_simple_curves(src, m_curves); + } + + if let (Some(a_curves), Some(clut)) = (self.a_curves.as_ref(), self.clut.as_ref()) { + if !clut.is_empty() { + let curve0 = &a_curves[0]; + let curve1 = &a_curves[1]; + let curve2 = &a_curves[2]; + for (src, dst) in src.chunks_exact(3).zip(dst.chunks_exact_mut(3)) { + let b0 = lut_interp_linear_float(src[0], curve0); + let b1 = lut_interp_linear_float(src[1], curve1); + let b2 = lut_interp_linear_float(src[2], curve2); + let interpolated = fetch(b0, b1, b2); + if T::FINITE { + dst[0] = (interpolated.v[0] * norm_value) + .round() + .max(0.0) + .min(norm_value) + .as_(); + dst[1] = (interpolated.v[1] * norm_value) + .round() + .max(0.0) + .min(norm_value) + .as_(); + dst[2] = (interpolated.v[2] * norm_value) + .round() + .max(0.0) + .min(norm_value) + .as_(); + } else { + dst[0] = interpolated.v[0].as_(); + dst[1] = interpolated.v[1].as_(); + dst[2] = interpolated.v[2].as_(); + } + } + } else { + for (src, dst) in src.chunks_exact(3).zip(dst.chunks_exact_mut(3)) { + if T::FINITE { + dst[0] = (src[0] * norm_value).round().max(0.0).min(norm_value).as_(); + dst[1] = (src[1] * norm_value).round().max(0.0).min(norm_value).as_(); + dst[2] = (src[2] * norm_value).round().max(0.0).min(norm_value).as_(); + } else { + dst[0] = src[0].as_(); + dst[1] = src[1].as_(); + dst[2] = src[2].as_(); + } + } + } + } else { + for (src, dst) in src.chunks_exact(3).zip(dst.chunks_exact_mut(3)) { + if T::FINITE { + dst[0] = (src[0] * norm_value).round().max(0.0).min(norm_value).as_(); + dst[1] = (src[1] * norm_value).round().max(0.0).min(norm_value).as_(); + dst[2] = (src[2] * norm_value).round().max(0.0).min(norm_value).as_(); + } else { + dst[0] = src[0].as_(); + dst[1] = src[1].as_(); + dst[2] = src[2].as_(); + } + } + } + + Ok(()) + } +} + +impl + PointeeSizeExpressible + Send + Sync> + KatanaFinalStage for Multidimensional3x3 +where + f32: AsPrimitive, +{ + fn to_output(&self, src: &mut [f32], dst: &mut [T]) -> Result<(), CmsError> { + if src.len() % 3 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % 3 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if src.len() != dst.len() { + return Err(CmsError::LaneSizeMismatch); + } + let fixed_new_clut = Vec::new(); + let new_clut = self.clut.as_ref().unwrap_or(&fixed_new_clut); + let lut = Cube::new_cube(new_clut, self.grid_size); + + // If PCS is LAB then linear interpolation should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + return self.to_output_impl(src, dst, |x, y, z| lut.trilinear_vec3(x, y, z)); + } + + match self.options.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.to_output_impl(src, dst, |x, y, z| lut.tetra_vec3(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.to_output_impl(src, dst, |x, y, z| lut.pyramid_vec3(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.to_output_impl(src, dst, |x, y, z| lut.prism_vec3(x, y, z))?; + } + InterpolationMethod::Linear => { + self.to_output_impl(src, dst, |x, y, z| lut.trilinear_vec3(x, y, z))?; + } + } + Ok(()) + } +} + +fn make_multidimensional_3x3< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +>( + mab: &LutMultidimensionalType, + options: TransformOptions, + pcs: DataColorSpace, + direction: MultidimensionalDirection, + bit_depth: usize, +) -> Result, CmsError> { + if mab.num_input_channels != 3 && mab.num_output_channels != 3 { + return Err(CmsError::UnsupportedProfileConnection); + } + if mab.b_curves.is_empty() || mab.b_curves.len() != 3 { + return Err(CmsError::InvalidAtoBLut); + } + + let grid_size = [mab.grid_points[0], mab.grid_points[1], mab.grid_points[2]]; + + let clut: Option> = if mab.a_curves.len() == 3 && mab.clut.is_some() { + let clut = mab.clut.as_ref().map(|x| x.to_clut_f32()).unwrap(); + let lut_grid = (mab.grid_points[0] as usize) + .safe_mul(mab.grid_points[1] as usize)? + .safe_mul(mab.grid_points[2] as usize)? + .safe_mul(mab.num_output_channels as usize)?; + if clut.len() != lut_grid { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: clut.len(), + expected: lut_grid, + })); + } + Some(clut) + } else { + None + }; + + let a_curves: Option; 3]>> = if mab.a_curves.len() == 3 && mab.clut.is_some() { + let mut arr = Box::<[Vec; 3]>::default(); + for (a_curve, dst) in mab.a_curves.iter().zip(arr.iter_mut()) { + *dst = a_curve.to_clut()?; + } + Some(arr) + } else { + None + }; + + let b_curves: Option; 3]>> = if mab.b_curves.len() == 3 { + let mut arr = Box::<[Vec; 3]>::default(); + let all_curves_linear = mab.b_curves.iter().all(|curve| curve.is_linear()); + if all_curves_linear { + None + } else { + for (c_curve, dst) in mab.b_curves.iter().zip(arr.iter_mut()) { + *dst = c_curve.to_clut()?; + } + Some(arr) + } + } else { + return Err(CmsError::InvalidAtoBLut); + }; + + let matrix = mab.matrix.to_f32(); + + let m_curves: Option; 3]>> = if mab.m_curves.len() == 3 { + let all_curves_linear = mab.m_curves.iter().all(|curve| curve.is_linear()); + if !all_curves_linear + || !mab.matrix.test_equality(Matrix3d::IDENTITY) + || mab.bias.ne(&Vector3d::default()) + { + let mut arr = Box::<[Vec; 3]>::default(); + for (curve, dst) in mab.m_curves.iter().zip(arr.iter_mut()) { + *dst = curve.to_clut()?; + } + Some(arr) + } else { + None + } + } else { + None + }; + + let bias = mab.bias.cast(); + + let transform = Multidimensional3x3:: { + a_curves, + b_curves, + m_curves, + matrix, + direction, + options, + clut, + pcs, + grid_size, + bias, + _phantom: PhantomData, + bit_depth, + }; + + Ok(transform) +} + +pub(crate) fn multi_dimensional_3x3_to_pcs< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +>( + mab: &LutMultidimensionalType, + options: TransformOptions, + pcs: DataColorSpace, + bit_depth: usize, +) -> Result + Send + Sync>, CmsError> { + let transform = make_multidimensional_3x3::( + mab, + options, + pcs, + MultidimensionalDirection::DeviceToPcs, + bit_depth, + )?; + Ok(Box::new(transform)) +} + +pub(crate) fn multi_dimensional_3x3_to_device< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +>( + mab: &LutMultidimensionalType, + options: TransformOptions, + pcs: DataColorSpace, + bit_depth: usize, +) -> Result + Send + Sync>, CmsError> +where + f32: AsPrimitive, +{ + let transform = make_multidimensional_3x3::( + mab, + options, + pcs, + MultidimensionalDirection::PcsToDevice, + bit_depth, + )?; + Ok(Box::new(transform)) +} diff --git a/deps/moxcms/src/conversions/katana/md4x3.rs b/deps/moxcms/src/conversions/katana/md4x3.rs new file mode 100644 index 0000000..96a4976 --- /dev/null +++ b/deps/moxcms/src/conversions/katana/md4x3.rs @@ -0,0 +1,321 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::katana::KatanaInitialStage; +use crate::conversions::katana::md3x3::MultidimensionalDirection; +use crate::mlaf::mlaf; +use crate::safe_math::SafeMul; +use crate::trc::lut_interp_linear_float; +use crate::{ + CmsError, DataColorSpace, Hypercube, InterpolationMethod, LutMultidimensionalType, + MalformedSize, Matrix3d, Matrix3f, PointeeSizeExpressible, TransformOptions, Vector3d, + Vector3f, +}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +pub(crate) fn execute_simple_curves3(dst: &mut [f32], curves: &[Vec; 3]) { + let curve0 = &curves[0]; + let curve1 = &curves[1]; + let curve2 = &curves[2]; + + for dst in dst.chunks_exact_mut(3) { + let a0 = dst[0]; + let a1 = dst[1]; + let a2 = dst[2]; + let b0 = lut_interp_linear_float(a0, curve0); + let b1 = lut_interp_linear_float(a1, curve1); + let b2 = lut_interp_linear_float(a2, curve2); + dst[0] = b0; + dst[1] = b1; + dst[2] = b2; + } +} + +pub(crate) fn execute_matrix_stage3(matrix: Matrix3f, bias: Vector3f, dst: &mut [f32]) { + let m = matrix; + let b = bias; + + if !m.test_equality(Matrix3f::IDENTITY) || !b.eq(&Vector3f::default()) { + for dst in dst.chunks_exact_mut(3) { + let x = dst[0]; + let y = dst[1]; + let z = dst[2]; + dst[0] = mlaf(mlaf(mlaf(b.v[0], x, m.v[0][0]), y, m.v[0][1]), z, m.v[0][2]); + dst[1] = mlaf(mlaf(mlaf(b.v[1], x, m.v[1][0]), y, m.v[1][1]), z, m.v[1][2]); + dst[2] = mlaf(mlaf(mlaf(b.v[2], x, m.v[2][0]), y, m.v[2][1]), z, m.v[2][2]); + } + } +} + +struct Multidimensional4x3< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +> { + a_curves: Option; 4]>>, + m_curves: Option; 3]>>, + b_curves: Option; 3]>>, + clut: Option>, + matrix: Matrix3f, + bias: Vector3f, + direction: MultidimensionalDirection, + options: TransformOptions, + pcs: DataColorSpace, + grid_size: [u8; 4], + _phantom: PhantomData, + bit_depth: usize, +} + +impl + PointeeSizeExpressible + Send + Sync> + Multidimensional4x3 +{ + fn to_pcs_impl Vector3f>( + &self, + input: &[T], + dst: &mut [f32], + fetch: Fetch, + ) -> Result<(), CmsError> { + let norm_value = if T::FINITE { + 1.0 / ((1u32 << self.bit_depth) - 1) as f32 + } else { + 1.0 + }; + assert_eq!( + self.direction, + MultidimensionalDirection::DeviceToPcs, + "PCS to device cannot be used on `to pcs` stage" + ); + + // A -> B + // OR B - A A - curves stage + + if let (Some(a_curves), Some(clut)) = (self.a_curves.as_ref(), self.clut.as_ref()) { + if !clut.is_empty() { + let curve0 = &a_curves[0]; + let curve1 = &a_curves[1]; + let curve2 = &a_curves[2]; + let curve3 = &a_curves[3]; + for (src, dst) in input.chunks_exact(4).zip(dst.chunks_exact_mut(3)) { + let b0 = lut_interp_linear_float(src[0].as_() * norm_value, curve0); + let b1 = lut_interp_linear_float(src[1].as_() * norm_value, curve1); + let b2 = lut_interp_linear_float(src[2].as_() * norm_value, curve2); + let b3 = lut_interp_linear_float(src[3].as_() * norm_value, curve3); + let interpolated = fetch(b0, b1, b2, b3); + dst[0] = interpolated.v[0]; + dst[1] = interpolated.v[1]; + dst[2] = interpolated.v[2]; + } + } + } else { + return Err(CmsError::InvalidAtoBLut); + } + + // Matrix stage + + if let Some(m_curves) = self.m_curves.as_ref() { + execute_simple_curves3(dst, m_curves); + execute_matrix_stage3(self.matrix, self.bias, dst); + } + + // B-curves is mandatory + if let Some(b_curves) = &self.b_curves.as_ref() { + execute_simple_curves3(dst, b_curves); + } + + Ok(()) + } +} + +impl + PointeeSizeExpressible + Send + Sync> + KatanaInitialStage for Multidimensional4x3 +{ + fn to_pcs(&self, input: &[T]) -> Result, CmsError> { + if input.len() % 4 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + let fixed_new_clut = Vec::new(); + let new_clut = self.clut.as_ref().unwrap_or(&fixed_new_clut); + let lut = Hypercube::new_hypercube(new_clut, self.grid_size); + + let mut new_dst = vec![0f32; (input.len() / 4) * 3]; + + // If PCS is LAB then linear interpolation should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + self.to_pcs_impl(input, &mut new_dst, |x, y, z, w| { + lut.quadlinear_vec3(x, y, z, w) + })?; + return Ok(new_dst); + } + + match self.options.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.to_pcs_impl(input, &mut new_dst, |x, y, z, w| lut.tetra_vec3(x, y, z, w))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.to_pcs_impl(input, &mut new_dst, |x, y, z, w| { + lut.pyramid_vec3(x, y, z, w) + })?; + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.to_pcs_impl(input, &mut new_dst, |x, y, z, w| lut.prism_vec3(x, y, z, w))?; + } + InterpolationMethod::Linear => { + self.to_pcs_impl(input, &mut new_dst, |x, y, z, w| { + lut.quadlinear_vec3(x, y, z, w) + })?; + } + } + Ok(new_dst) + } +} + +fn make_multidimensional_4x3< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +>( + mab: &LutMultidimensionalType, + options: TransformOptions, + pcs: DataColorSpace, + direction: MultidimensionalDirection, + bit_depth: usize, +) -> Result, CmsError> { + if mab.num_input_channels != 4 && mab.num_output_channels != 3 { + return Err(CmsError::UnsupportedProfileConnection); + } + if mab.b_curves.is_empty() || mab.b_curves.len() != 3 { + return Err(CmsError::InvalidAtoBLut); + } + + let grid_size = [ + mab.grid_points[0], + mab.grid_points[1], + mab.grid_points[2], + mab.grid_points[3], + ]; + + let clut: Option> = if mab.a_curves.len() == 4 && mab.clut.is_some() { + let clut = mab.clut.as_ref().map(|x| x.to_clut_f32()).unwrap(); + let lut_grid = (mab.grid_points[0] as usize) + .safe_mul(mab.grid_points[1] as usize)? + .safe_mul(mab.grid_points[2] as usize)? + .safe_mul(mab.grid_points[3] as usize)? + .safe_mul(mab.num_output_channels as usize)?; + if clut.len() != lut_grid { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: clut.len(), + expected: lut_grid, + })); + } + Some(clut) + } else { + return Err(CmsError::InvalidAtoBLut); + }; + + let a_curves: Option; 4]>> = if mab.a_curves.len() == 4 && mab.clut.is_some() { + let mut arr = Box::<[Vec; 4]>::default(); + for (a_curve, dst) in mab.a_curves.iter().zip(arr.iter_mut()) { + *dst = a_curve.to_clut()?; + } + Some(arr) + } else { + None + }; + + let b_curves: Option; 3]>> = if mab.b_curves.len() == 3 { + let mut arr = Box::<[Vec; 3]>::default(); + let all_curves_linear = mab.b_curves.iter().all(|curve| curve.is_linear()); + if all_curves_linear { + None + } else { + for (c_curve, dst) in mab.b_curves.iter().zip(arr.iter_mut()) { + *dst = c_curve.to_clut()?; + } + Some(arr) + } + } else { + return Err(CmsError::InvalidAtoBLut); + }; + + let matrix = mab.matrix.to_f32(); + + let m_curves: Option; 3]>> = if mab.m_curves.len() == 3 { + let all_curves_linear = mab.m_curves.iter().all(|curve| curve.is_linear()); + if !all_curves_linear + || !mab.matrix.test_equality(Matrix3d::IDENTITY) + || mab.bias.ne(&Vector3d::default()) + { + let mut arr = Box::<[Vec; 3]>::default(); + for (curve, dst) in mab.m_curves.iter().zip(arr.iter_mut()) { + *dst = curve.to_clut()?; + } + Some(arr) + } else { + None + } + } else { + None + }; + + let bias = mab.bias.cast(); + + let transform = Multidimensional4x3:: { + a_curves, + b_curves, + m_curves, + matrix, + direction, + options, + clut, + pcs, + grid_size, + bias, + _phantom: PhantomData, + bit_depth, + }; + + Ok(transform) +} + +pub(crate) fn multi_dimensional_4x3_to_pcs< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +>( + mab: &LutMultidimensionalType, + options: TransformOptions, + pcs: DataColorSpace, + bit_depth: usize, +) -> Result + Send + Sync>, CmsError> { + let transform = make_multidimensional_4x3::( + mab, + options, + pcs, + MultidimensionalDirection::DeviceToPcs, + bit_depth, + )?; + Ok(Box::new(transform)) +} diff --git a/deps/moxcms/src/conversions/katana/md_3xn.rs b/deps/moxcms/src/conversions/katana/md_3xn.rs new file mode 100644 index 0000000..e7862b8 --- /dev/null +++ b/deps/moxcms/src/conversions/katana/md_3xn.rs @@ -0,0 +1,284 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::katana::KatanaFinalStage; +use crate::conversions::katana::md3x3::MultidimensionalDirection; +use crate::conversions::katana::md4x3::{execute_matrix_stage3, execute_simple_curves3}; +use crate::conversions::md_lut::{MultidimensionalLut, tetra_3i_to_any_vec}; +use crate::safe_math::SafeMul; +use crate::trc::lut_interp_linear_float; +use crate::{ + CmsError, DataColorSpace, Layout, LutMultidimensionalType, MalformedSize, Matrix3d, Matrix3f, + PointeeSizeExpressible, TransformOptions, Vector3d, Vector3f, +}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +struct Multidimensional3xN< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +> { + a_curves: Option>>, + m_curves: Option; 3]>>, + b_curves: Option; 3]>>, + clut: Option>, + matrix: Matrix3f, + bias: Vector3f, + direction: MultidimensionalDirection, + grid_size: [u8; 16], + output_inks: usize, + _phantom: PhantomData, + dst_layout: Layout, + bit_depth: usize, +} + +impl + PointeeSizeExpressible + Send + Sync> + Multidimensional3xN +where + f32: AsPrimitive, +{ + fn to_output_impl(&self, src: &mut [f32], dst: &mut [T]) -> Result<(), CmsError> { + let norm_value = if T::FINITE { + ((1u32 << self.bit_depth) - 1) as f32 + } else { + 1.0 + }; + assert_eq!( + self.direction, + MultidimensionalDirection::PcsToDevice, + "PCS to device cannot be used on `to pcs` stage" + ); + + // B-curves is mandatory + if let Some(b_curves) = &self.b_curves.as_ref() { + execute_simple_curves3(src, b_curves); + } + + // Matrix stage + + if let Some(m_curves) = self.m_curves.as_ref() { + execute_matrix_stage3(self.matrix, self.bias, src); + execute_simple_curves3(src, m_curves); + } + + if let (Some(a_curves), Some(clut)) = (self.a_curves.as_ref(), self.clut.as_ref()) { + let mut inks = vec![0.; self.output_inks]; + + if clut.is_empty() { + return Err(CmsError::InvalidAtoBLut); + } + + let md_lut = MultidimensionalLut::new(self.grid_size, 3, self.output_inks); + + for (src, dst) in src + .chunks_exact(3) + .zip(dst.chunks_exact_mut(self.dst_layout.channels())) + { + tetra_3i_to_any_vec( + &md_lut, + clut, + src[0], + src[1], + src[2], + &mut inks, + self.output_inks, + ); + + for (ink, curve) in inks.iter_mut().zip(a_curves.iter()) { + *ink = lut_interp_linear_float(*ink, curve); + } + + if T::FINITE { + for (dst, ink) in dst.iter_mut().zip(inks.iter()) { + *dst = (*ink * norm_value).round().max(0.).min(norm_value).as_(); + } + } else { + for (dst, ink) in dst.iter_mut().zip(inks.iter()) { + *dst = (*ink * norm_value).as_(); + } + } + } + } else { + return Err(CmsError::InvalidAtoBLut); + } + + Ok(()) + } +} + +impl + PointeeSizeExpressible + Send + Sync> + KatanaFinalStage for Multidimensional3xN +where + f32: AsPrimitive, +{ + fn to_output(&self, src: &mut [f32], dst: &mut [T]) -> Result<(), CmsError> { + if src.len() % 3 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % self.output_inks != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + self.to_output_impl(src, dst)?; + Ok(()) + } +} + +fn make_multidimensional_nx3< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +>( + dst_layout: Layout, + mab: &LutMultidimensionalType, + _: TransformOptions, + pcs: DataColorSpace, + direction: MultidimensionalDirection, + bit_depth: usize, +) -> Result, CmsError> { + let real_inks = if pcs == DataColorSpace::Rgb { + 3 + } else { + dst_layout.channels() + }; + + if mab.num_output_channels != real_inks as u8 { + return Err(CmsError::UnsupportedProfileConnection); + } + + if mab.b_curves.is_empty() || mab.b_curves.len() != 3 { + return Err(CmsError::InvalidAtoBLut); + } + + let clut: Option> = + if mab.a_curves.len() == mab.num_output_channels as usize && mab.clut.is_some() { + let clut = mab.clut.as_ref().map(|x| x.to_clut_f32()).unwrap(); + let mut lut_grid = 1usize; + for grid in mab.grid_points.iter().take(mab.num_input_channels as usize) { + lut_grid = lut_grid.safe_mul(*grid as usize)?; + } + let lut_grid = lut_grid.safe_mul(mab.num_output_channels as usize)?; + if clut.len() != lut_grid { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: clut.len(), + expected: lut_grid, + })); + } + Some(clut) + } else { + return Err(CmsError::InvalidAtoBLut); + }; + + let a_curves: Option>> = + if mab.a_curves.len() == mab.num_output_channels as usize && mab.clut.is_some() { + let mut arr = Vec::new(); + for a_curve in mab.a_curves.iter() { + arr.push(a_curve.to_clut()?); + } + Some(arr) + } else { + None + }; + + let b_curves: Option; 3]>> = if mab.b_curves.len() == 3 { + let mut arr = Box::<[Vec; 3]>::default(); + let all_curves_linear = mab.b_curves.iter().all(|curve| curve.is_linear()); + if all_curves_linear { + None + } else { + for (c_curve, dst) in mab.b_curves.iter().zip(arr.iter_mut()) { + *dst = c_curve.to_clut()?; + } + Some(arr) + } + } else { + return Err(CmsError::InvalidAtoBLut); + }; + + let matrix = mab.matrix.to_f32(); + + let m_curves: Option; 3]>> = if mab.m_curves.len() == 3 { + let all_curves_linear = mab.m_curves.iter().all(|curve| curve.is_linear()); + if !all_curves_linear + || !mab.matrix.test_equality(Matrix3d::IDENTITY) + || mab.bias.ne(&Vector3d::default()) + { + let mut arr = Box::<[Vec; 3]>::default(); + for (curve, dst) in mab.m_curves.iter().zip(arr.iter_mut()) { + *dst = curve.to_clut()?; + } + Some(arr) + } else { + None + } + } else { + None + }; + + let bias = mab.bias.cast(); + + let transform = Multidimensional3xN:: { + a_curves, + b_curves, + m_curves, + matrix, + direction, + clut, + grid_size: mab.grid_points, + bias, + dst_layout, + output_inks: real_inks, + _phantom: PhantomData, + bit_depth, + }; + + Ok(transform) +} + +pub(crate) fn katana_multi_dimensional_3xn_to_device< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +>( + dst_layout: Layout, + mab: &LutMultidimensionalType, + options: TransformOptions, + pcs: DataColorSpace, + bit_depth: usize, +) -> Result + Send + Sync>, CmsError> +where + f32: AsPrimitive, +{ + if mab.num_input_channels == 0 { + return Err(CmsError::UnsupportedProfileConnection); + } + let transform = make_multidimensional_nx3::( + dst_layout, + mab, + options, + pcs, + MultidimensionalDirection::PcsToDevice, + bit_depth, + )?; + Ok(Box::new(transform)) +} diff --git a/deps/moxcms/src/conversions/katana/md_nx3.rs b/deps/moxcms/src/conversions/katana/md_nx3.rs new file mode 100644 index 0000000..4c12bf3 --- /dev/null +++ b/deps/moxcms/src/conversions/katana/md_nx3.rs @@ -0,0 +1,294 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::katana::KatanaInitialStage; +use crate::conversions::katana::md3x3::MultidimensionalDirection; +use crate::conversions::katana::md4x3::{execute_matrix_stage3, execute_simple_curves3}; +use crate::conversions::md_lut::{ + MultidimensionalLut, NVector, linear_1i_vec3f, linear_2i_vec3f_direct, linear_3i_vec3f_direct, + linear_4i_vec3f, linear_5i_vec3f, linear_6i_vec3f, linear_7i_vec3f, linear_8i_vec3f, + linear_9i_vec3f, linear_10i_vec3f, linear_11i_vec3f, linear_12i_vec3f, linear_13i_vec3f, + linear_14i_vec3f, linear_15i_vec3f, +}; +use crate::safe_math::SafeMul; +use crate::trc::lut_interp_linear_float; +use crate::{ + CmsError, DataColorSpace, Layout, LutMultidimensionalType, MalformedSize, Matrix3d, Matrix3f, + PointeeSizeExpressible, TransformOptions, Vector3d, Vector3f, +}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +struct MultidimensionalNx3< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +> { + a_curves: Option>>, + m_curves: Option; 3]>>, + b_curves: Option; 3]>>, + clut: Option>, + matrix: Matrix3f, + bias: Vector3f, + direction: MultidimensionalDirection, + grid_size: [u8; 16], + input_inks: usize, + _phantom: PhantomData, + bit_depth: usize, +} + +#[inline(never)] +pub(crate) fn interpolate_out_function( + layout: Layout, +) -> fn(lut: &MultidimensionalLut, arr: &[f32], inputs: &[f32]) -> NVector { + const OUT: usize = 3; + match layout { + Layout::Rgb => linear_3i_vec3f_direct::, + Layout::Rgba => linear_4i_vec3f::, + Layout::Gray => linear_1i_vec3f::, + Layout::GrayAlpha => linear_2i_vec3f_direct::, + Layout::Inks5 => linear_5i_vec3f::, + Layout::Inks6 => linear_6i_vec3f::, + Layout::Inks7 => linear_7i_vec3f::, + Layout::Inks8 => linear_8i_vec3f::, + Layout::Inks9 => linear_9i_vec3f::, + Layout::Inks10 => linear_10i_vec3f::, + Layout::Inks11 => linear_11i_vec3f::, + Layout::Inks12 => linear_12i_vec3f::, + Layout::Inks13 => linear_13i_vec3f::, + Layout::Inks14 => linear_14i_vec3f::, + Layout::Inks15 => linear_15i_vec3f::, + } +} + +impl + PointeeSizeExpressible + Send + Sync> + MultidimensionalNx3 +{ + fn to_pcs_impl(&self, input: &[T], dst: &mut [f32]) -> Result<(), CmsError> { + let norm_value = if T::FINITE { + 1.0 / ((1u32 << self.bit_depth) - 1) as f32 + } else { + 1.0 + }; + assert_eq!( + self.direction, + MultidimensionalDirection::DeviceToPcs, + "PCS to device cannot be used on `to pcs` stage" + ); + + // A -> B + // OR B - A A - curves stage + + if let (Some(a_curves), Some(clut)) = (self.a_curves.as_ref(), self.clut.as_ref()) { + let layout = Layout::from_inks(self.input_inks); + + let mut inks = vec![0.; self.input_inks]; + + if clut.is_empty() { + return Err(CmsError::InvalidAtoBLut); + } + + let fetcher = interpolate_out_function(layout); + + let md_lut = MultidimensionalLut::new(self.grid_size, self.input_inks, 3); + + for (src, dst) in input + .chunks_exact(layout.channels()) + .zip(dst.chunks_exact_mut(3)) + { + for ((ink, src_ink), curve) in inks.iter_mut().zip(src).zip(a_curves.iter()) { + *ink = lut_interp_linear_float(src_ink.as_() * norm_value, curve); + } + + let interpolated = fetcher(&md_lut, clut, &inks); + + dst[0] = interpolated.v[0]; + dst[1] = interpolated.v[1]; + dst[2] = interpolated.v[2]; + } + } else { + return Err(CmsError::InvalidAtoBLut); + } + + // Matrix stage + + if let Some(m_curves) = self.m_curves.as_ref() { + execute_simple_curves3(dst, m_curves); + execute_matrix_stage3(self.matrix, self.bias, dst); + } + + // B-curves is mandatory + if let Some(b_curves) = &self.b_curves.as_ref() { + execute_simple_curves3(dst, b_curves); + } + + Ok(()) + } +} + +impl + PointeeSizeExpressible + Send + Sync> + KatanaInitialStage for MultidimensionalNx3 +{ + fn to_pcs(&self, input: &[T]) -> Result, CmsError> { + if input.len() % self.input_inks != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let mut new_dst = vec![0f32; (input.len() / self.input_inks) * 3]; + + self.to_pcs_impl(input, &mut new_dst)?; + Ok(new_dst) + } +} + +fn make_multidimensional_nx3< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +>( + mab: &LutMultidimensionalType, + _: TransformOptions, + _: DataColorSpace, + direction: MultidimensionalDirection, + bit_depth: usize, +) -> Result, CmsError> { + if mab.num_output_channels != 3 { + return Err(CmsError::UnsupportedProfileConnection); + } + if mab.b_curves.is_empty() || mab.b_curves.len() != 3 { + return Err(CmsError::InvalidAtoBLut); + } + + let clut: Option> = + if mab.a_curves.len() == mab.num_input_channels as usize && mab.clut.is_some() { + let clut = mab.clut.as_ref().map(|x| x.to_clut_f32()).unwrap(); + let mut lut_grid = 1usize; + for grid in mab.grid_points.iter().take(mab.num_input_channels as usize) { + lut_grid = lut_grid.safe_mul(*grid as usize)?; + } + let lut_grid = lut_grid.safe_mul(mab.num_output_channels as usize)?; + if clut.len() != lut_grid { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: clut.len(), + expected: lut_grid, + })); + } + Some(clut) + } else { + return Err(CmsError::InvalidAtoBLut); + }; + + let a_curves: Option>> = + if mab.a_curves.len() == mab.num_input_channels as usize && mab.clut.is_some() { + let mut arr = Vec::new(); + for a_curve in mab.a_curves.iter() { + arr.push(a_curve.to_clut()?); + } + Some(arr) + } else { + None + }; + + let b_curves: Option; 3]>> = if mab.b_curves.len() == 3 { + let mut arr = Box::<[Vec; 3]>::default(); + let all_curves_linear = mab.b_curves.iter().all(|curve| curve.is_linear()); + if all_curves_linear { + None + } else { + for (c_curve, dst) in mab.b_curves.iter().zip(arr.iter_mut()) { + *dst = c_curve.to_clut()?; + } + Some(arr) + } + } else { + return Err(CmsError::InvalidAtoBLut); + }; + + let matrix = mab.matrix.to_f32(); + + let m_curves: Option; 3]>> = if mab.m_curves.len() == 3 { + let all_curves_linear = mab.m_curves.iter().all(|curve| curve.is_linear()); + if !all_curves_linear + || !mab.matrix.test_equality(Matrix3d::IDENTITY) + || mab.bias.ne(&Vector3d::default()) + { + let mut arr = Box::<[Vec; 3]>::default(); + for (curve, dst) in mab.m_curves.iter().zip(arr.iter_mut()) { + *dst = curve.to_clut()?; + } + Some(arr) + } else { + None + } + } else { + None + }; + + let bias = mab.bias.cast(); + + let transform = MultidimensionalNx3:: { + a_curves, + b_curves, + m_curves, + matrix, + direction, + clut, + grid_size: mab.grid_points, + bias, + input_inks: mab.num_input_channels as usize, + _phantom: PhantomData, + bit_depth, + }; + + Ok(transform) +} + +pub(crate) fn katana_multi_dimensional_nx3_to_pcs< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +>( + src_layout: Layout, + mab: &LutMultidimensionalType, + options: TransformOptions, + pcs: DataColorSpace, + bit_depth: usize, +) -> Result + Send + Sync>, CmsError> { + if pcs == DataColorSpace::Rgb { + if mab.num_input_channels != 3 { + return Err(CmsError::InvalidAtoBLut); + } + if src_layout != Layout::Rgba && src_layout != Layout::Rgb { + return Err(CmsError::InvalidInksCountForProfile); + } + } else if mab.num_input_channels != src_layout.channels() as u8 { + return Err(CmsError::InvalidInksCountForProfile); + } + let transform = make_multidimensional_nx3::( + mab, + options, + pcs, + MultidimensionalDirection::DeviceToPcs, + bit_depth, + )?; + Ok(Box::new(transform)) +} diff --git a/deps/moxcms/src/conversions/katana/md_pipeline.rs b/deps/moxcms/src/conversions/katana/md_pipeline.rs new file mode 100644 index 0000000..e581ccd --- /dev/null +++ b/deps/moxcms/src/conversions/katana/md_pipeline.rs @@ -0,0 +1,393 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::katana::md_nx3::interpolate_out_function; +use crate::conversions::katana::{KatanaFinalStage, KatanaInitialStage}; +use crate::conversions::md_lut::{MultidimensionalLut, tetra_3i_to_any_vec}; +use crate::profile::LutDataType; +use crate::safe_math::{SafeMul, SafePowi}; +use crate::trc::lut_interp_linear_float; +use crate::{ + CmsError, DataColorSpace, Layout, MalformedSize, PointeeSizeExpressible, TransformOptions, +}; +use num_traits::AsPrimitive; +use std::array::from_fn; +use std::marker::PhantomData; + +#[derive(Default)] +struct KatanaLutNx3 { + linearization: Vec>, + clut: Vec, + grid_size: u8, + input_inks: usize, + output: [Vec; 3], + _phantom: PhantomData, + bit_depth: usize, +} + +struct KatanaLut3xN { + linearization: [Vec; 3], + clut: Vec, + grid_size: u8, + output_inks: usize, + output: Vec>, + dst_layout: Layout, + target_color_space: DataColorSpace, + _phantom: PhantomData, + bit_depth: usize, +} + +impl> KatanaLutNx3 { + fn to_pcs_impl(&self, input: &[T]) -> Result, CmsError> { + if input.len() % self.input_inks != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + let norm_value = if T::FINITE { + 1.0 / ((1u32 << self.bit_depth) - 1) as f32 + } else { + 1.0 + }; + + let grid_sizes: [u8; 16] = from_fn(|i| { + if i < self.input_inks { + self.grid_size + } else { + 0 + } + }); + + let md_lut = MultidimensionalLut::new(grid_sizes, self.input_inks, 3); + + let layout = Layout::from_inks(self.input_inks); + + let mut inks = vec![0.; self.input_inks]; + + let mut dst = vec![0.; (input.len() / layout.channels()) * 3]; + + let fetcher = interpolate_out_function(layout); + + for (dest, src) in dst + .chunks_exact_mut(3) + .zip(input.chunks_exact(layout.channels())) + { + for ((ink, src_ink), curve) in inks.iter_mut().zip(src).zip(self.linearization.iter()) { + *ink = lut_interp_linear_float(src_ink.as_() * norm_value, curve); + } + + let clut = fetcher(&md_lut, &self.clut, &inks); + + let pcs_x = lut_interp_linear_float(clut.v[0], &self.output[0]); + let pcs_y = lut_interp_linear_float(clut.v[1], &self.output[1]); + let pcs_z = lut_interp_linear_float(clut.v[2], &self.output[2]); + + dest[0] = pcs_x; + dest[1] = pcs_y; + dest[2] = pcs_z; + } + Ok(dst) + } +} + +impl> KatanaInitialStage + for KatanaLutNx3 +{ + fn to_pcs(&self, input: &[T]) -> Result, CmsError> { + if input.len() % self.input_inks != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + self.to_pcs_impl(input) + } +} + +impl> KatanaFinalStage + for KatanaLut3xN +where + f32: AsPrimitive, +{ + fn to_output(&self, src: &mut [f32], dst: &mut [T]) -> Result<(), CmsError> { + if src.len() % 3 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let grid_sizes: [u8; 16] = from_fn(|i| { + if i < self.output_inks { + self.grid_size + } else { + 0 + } + }); + + let md_lut = MultidimensionalLut::new(grid_sizes, 3, self.output_inks); + + let scale_value = if T::FINITE { + ((1u32 << self.bit_depth) - 1) as f32 + } else { + 1.0 + }; + + let mut working = vec![0.; self.output_inks]; + + for (dest, src) in dst + .chunks_exact_mut(self.dst_layout.channels()) + .zip(src.chunks_exact(3)) + { + let x = lut_interp_linear_float(src[0], &self.linearization[0]); + let y = lut_interp_linear_float(src[1], &self.linearization[1]); + let z = lut_interp_linear_float(src[2], &self.linearization[2]); + + tetra_3i_to_any_vec(&md_lut, &self.clut, x, y, z, &mut working, self.output_inks); + + for (ink, curve) in working.iter_mut().zip(self.output.iter()) { + *ink = lut_interp_linear_float(*ink, curve); + } + + if T::FINITE { + for (dst, ink) in dest.iter_mut().zip(working.iter()) { + *dst = (*ink * scale_value).round().max(0.).min(scale_value).as_(); + } + } else { + for (dst, ink) in dest.iter_mut().zip(working.iter()) { + *dst = (*ink * scale_value).as_(); + } + } + } + + if self.dst_layout == Layout::Rgba && self.target_color_space == DataColorSpace::Rgb { + for dst in dst.chunks_exact_mut(self.dst_layout.channels()) { + dst[3] = scale_value.as_(); + } + } + + Ok(()) + } +} + +fn katana_make_lut_nx3>( + inks: usize, + lut: &LutDataType, + _: TransformOptions, + _: DataColorSpace, + bit_depth: usize, +) -> Result, CmsError> { + if inks != lut.num_input_channels as usize { + return Err(CmsError::UnsupportedProfileConnection); + } + if lut.num_output_channels != 3 { + return Err(CmsError::UnsupportedProfileConnection); + } + let clut_length: usize = (lut.num_clut_grid_points as usize) + .safe_powi(lut.num_input_channels as u32)? + .safe_mul(lut.num_output_channels as usize)?; + + let clut_table = lut.clut_table.to_clut_f32(); + if clut_table.len() != clut_length { + return Err(CmsError::MalformedClut(MalformedSize { + size: clut_table.len(), + expected: clut_length, + })); + } + + let linearization_table = lut.input_table.to_clut_f32(); + + if linearization_table.len() < lut.num_input_table_entries as usize * inks { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: linearization_table.len(), + expected: lut.num_input_table_entries as usize * inks, + })); + } + + let linearization = (0..inks) + .map(|x| { + linearization_table[x * lut.num_input_table_entries as usize + ..(x + 1) * lut.num_input_table_entries as usize] + .to_vec() + }) + .collect::<_>(); + + let gamma_table = lut.output_table.to_clut_f32(); + + if gamma_table.len() < lut.num_output_table_entries as usize * 3 { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: gamma_table.len(), + expected: lut.num_output_table_entries as usize * 3, + })); + } + + let gamma_curve0 = gamma_table[..lut.num_output_table_entries as usize].to_vec(); + let gamma_curve1 = gamma_table + [lut.num_output_table_entries as usize..lut.num_output_table_entries as usize * 2] + .to_vec(); + let gamma_curve2 = gamma_table + [lut.num_output_table_entries as usize * 2..lut.num_output_table_entries as usize * 3] + .to_vec(); + + let transform = KatanaLutNx3:: { + linearization, + clut: clut_table, + grid_size: lut.num_clut_grid_points, + output: [gamma_curve0, gamma_curve1, gamma_curve2], + input_inks: inks, + _phantom: PhantomData, + bit_depth, + }; + Ok(transform) +} + +fn katana_make_lut_3xn>( + inks: usize, + dst_layout: Layout, + lut: &LutDataType, + _: TransformOptions, + target_color_space: DataColorSpace, + bit_depth: usize, +) -> Result, CmsError> { + if lut.num_input_channels as usize != 3 { + return Err(CmsError::UnsupportedProfileConnection); + } + if target_color_space == DataColorSpace::Rgb { + if lut.num_output_channels != 3 || lut.num_output_channels != 4 { + return Err(CmsError::InvalidInksCountForProfile); + } + if dst_layout != Layout::Rgb || dst_layout != Layout::Rgba { + return Err(CmsError::InvalidInksCountForProfile); + } + } else if lut.num_output_channels as usize != dst_layout.channels() { + return Err(CmsError::InvalidInksCountForProfile); + } + let clut_length: usize = (lut.num_clut_grid_points as usize) + .safe_powi(lut.num_input_channels as u32)? + .safe_mul(lut.num_output_channels as usize)?; + + let clut_table = lut.clut_table.to_clut_f32(); + if clut_table.len() != clut_length { + return Err(CmsError::MalformedClut(MalformedSize { + size: clut_table.len(), + expected: clut_length, + })); + } + + let linearization_table = lut.input_table.to_clut_f32(); + + if linearization_table.len() < lut.num_input_table_entries as usize * 3 { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: linearization_table.len(), + expected: lut.num_input_table_entries as usize * 3, + })); + } + + let linear_curve0 = linearization_table[..lut.num_input_table_entries as usize].to_vec(); + let linear_curve1 = linearization_table + [lut.num_input_table_entries as usize..lut.num_input_table_entries as usize * 2] + .to_vec(); + let linear_curve2 = linearization_table + [lut.num_input_table_entries as usize * 2..lut.num_input_table_entries as usize * 3] + .to_vec(); + + let gamma_table = lut.output_table.to_clut_f32(); + + if gamma_table.len() < lut.num_output_table_entries as usize * inks { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: gamma_table.len(), + expected: lut.num_output_table_entries as usize * inks, + })); + } + + let gamma = (0..inks) + .map(|x| { + gamma_table[x * lut.num_output_table_entries as usize + ..(x + 1) * lut.num_output_table_entries as usize] + .to_vec() + }) + .collect::<_>(); + + let transform = KatanaLut3xN:: { + linearization: [linear_curve0, linear_curve1, linear_curve2], + clut: clut_table, + grid_size: lut.num_clut_grid_points, + output: gamma, + output_inks: inks, + _phantom: PhantomData, + target_color_space, + dst_layout, + bit_depth, + }; + Ok(transform) +} + +pub(crate) fn katana_input_make_lut_nx3< + T: Copy + PointeeSizeExpressible + AsPrimitive + Send + Sync, +>( + src_layout: Layout, + inks: usize, + lut: &LutDataType, + options: TransformOptions, + pcs: DataColorSpace, + bit_depth: usize, +) -> Result + Send + Sync>, CmsError> { + if pcs == DataColorSpace::Rgb { + if lut.num_input_channels != 3 { + return Err(CmsError::InvalidAtoBLut); + } + if src_layout != Layout::Rgba && src_layout != Layout::Rgb { + return Err(CmsError::InvalidInksCountForProfile); + } + } else if lut.num_input_channels != src_layout.channels() as u8 { + return Err(CmsError::InvalidInksCountForProfile); + } + let z0 = katana_make_lut_nx3::(inks, lut, options, pcs, bit_depth)?; + Ok(Box::new(z0)) +} + +pub(crate) fn katana_output_make_lut_3xn< + T: Copy + PointeeSizeExpressible + AsPrimitive + Send + Sync, +>( + dst_layout: Layout, + lut: &LutDataType, + options: TransformOptions, + target_color_space: DataColorSpace, + bit_depth: usize, +) -> Result + Send + Sync>, CmsError> +where + f32: AsPrimitive, +{ + let real_inks = if target_color_space == DataColorSpace::Rgb { + 3 + } else { + dst_layout.channels() + }; + let z0 = katana_make_lut_3xn::( + real_inks, + dst_layout, + lut, + options, + target_color_space, + bit_depth, + )?; + Ok(Box::new(z0)) +} diff --git a/deps/moxcms/src/conversions/katana/mod.rs b/deps/moxcms/src/conversions/katana/mod.rs new file mode 100644 index 0000000..11e8f92 --- /dev/null +++ b/deps/moxcms/src/conversions/katana/mod.rs @@ -0,0 +1,56 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +mod finalizers; +mod md3x3; +mod md4x3; +mod md_3xn; +mod md_nx3; +mod md_pipeline; +mod pcs_stages; +mod rgb_xyz; +mod stages; +mod xyz_lab; +mod xyz_rgb; + +pub(crate) use finalizers::{CopyAlphaStage, InjectAlphaStage}; +pub(crate) use md_3xn::katana_multi_dimensional_3xn_to_device; +pub(crate) use md_nx3::katana_multi_dimensional_nx3_to_pcs; +pub(crate) use md_pipeline::{katana_input_make_lut_nx3, katana_output_make_lut_3xn}; +pub(crate) use md3x3::{multi_dimensional_3x3_to_device, multi_dimensional_3x3_to_pcs}; +pub(crate) use md4x3::multi_dimensional_4x3_to_pcs; +pub(crate) use pcs_stages::{ + KatanaDefaultIntermediate, katana_pcs_lab_v2_to_v4, katana_pcs_lab_v4_to_v2, +}; +pub(crate) use rgb_xyz::katana_create_rgb_lin_lut; +pub(crate) use stages::{ + Katana, KatanaFinalStage, KatanaInitialStage, KatanaIntermediateStage, + KatanaPostFinalizationStage, +}; +pub(crate) use xyz_lab::{KatanaStageLabToXyz, KatanaStageXyzToLab}; +pub(crate) use xyz_rgb::katana_prepare_inverse_lut_rgb_xyz; diff --git a/deps/moxcms/src/conversions/katana/pcs_stages.rs b/deps/moxcms/src/conversions/katana/pcs_stages.rs new file mode 100644 index 0000000..14fbacb --- /dev/null +++ b/deps/moxcms/src/conversions/katana/pcs_stages.rs @@ -0,0 +1,100 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::katana::KatanaIntermediateStage; +use crate::conversions::katana::stages::BlackholeIntermediateStage; +use crate::mlaf::mlaf; +use crate::{CmsError, ColorProfile, DataColorSpace, Matrix3f, ProfileVersion}; +use std::marker::PhantomData; + +pub(crate) struct KatanaMatrixStage { + pub(crate) matrices: Vec, +} + +impl KatanaMatrixStage { + pub(crate) fn new(matrix: Matrix3f) -> Self { + Self { + matrices: vec![matrix], + } + } +} + +pub(crate) type KatanaDefaultIntermediate = dyn KatanaIntermediateStage + Send + Sync; + +impl KatanaIntermediateStage for KatanaMatrixStage { + fn stage(&self, input: &mut Vec) -> Result, CmsError> { + if input.len() % 3 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + for m in self.matrices.iter() { + for dst in input.chunks_exact_mut(3) { + let x = dst[0]; + let y = dst[1]; + let z = dst[2]; + dst[0] = mlaf(mlaf(x * m.v[0][0], y, m.v[0][1]), z, m.v[0][2]); + dst[1] = mlaf(mlaf(x * m.v[1][0], y, m.v[1][1]), z, m.v[1][2]); + dst[2] = mlaf(mlaf(x * m.v[2][0], y, m.v[2][1]), z, m.v[2][2]); + } + } + + Ok(std::mem::take(input)) + } +} + +pub(crate) fn katana_pcs_lab_v4_to_v2(profile: &ColorProfile) -> Box { + if profile.pcs == DataColorSpace::Lab && profile.version_internal <= ProfileVersion::V4_0 { + let v_mat = vec![Matrix3f { + v: [ + [65280.0 / 65535.0, 0., 0.], + [0., 65280.0 / 65535.0, 0.], + [0., 0., 65280.0 / 65535.0], + ], + }]; + return Box::new(KatanaMatrixStage { matrices: v_mat }); + } + Box::new(BlackholeIntermediateStage { + _phantom: PhantomData, + }) +} + +pub(crate) fn katana_pcs_lab_v2_to_v4(profile: &ColorProfile) -> Box { + if profile.pcs == DataColorSpace::Lab && profile.version_internal <= ProfileVersion::V4_0 { + let v_mat = vec![Matrix3f { + v: [ + [65535.0 / 65280.0, 0., 0.], + [0., 65535.0 / 65280.0, 0.], + [0., 0., 65535.0 / 65280.0], + ], + }]; + return Box::new(KatanaMatrixStage { matrices: v_mat }); + } + Box::new(BlackholeIntermediateStage { + _phantom: PhantomData, + }) +} diff --git a/deps/moxcms/src/conversions/katana/rgb_xyz.rs b/deps/moxcms/src/conversions/katana/rgb_xyz.rs new file mode 100644 index 0000000..29cce25 --- /dev/null +++ b/deps/moxcms/src/conversions/katana/rgb_xyz.rs @@ -0,0 +1,162 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::katana::pcs_stages::KatanaMatrixStage; +use crate::conversions::katana::{KatanaInitialStage, KatanaIntermediateStage}; +use crate::err::try_vec; +use crate::{CmsError, ColorProfile, Layout, Matrix3f, PointeeSizeExpressible, TransformOptions}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +struct KatanaRgbLinearizationStage { + r_lin: Box<[f32; LINEAR_CAP]>, + g_lin: Box<[f32; LINEAR_CAP]>, + b_lin: Box<[f32; LINEAR_CAP]>, + linear_cap: usize, + bit_depth: usize, + _phantom: PhantomData, +} + +impl< + T: Clone + AsPrimitive + PointeeSizeExpressible, + const LAYOUT: u8, + const LINEAR_CAP: usize, +> KatanaInitialStage for KatanaRgbLinearizationStage +{ + fn to_pcs(&self, input: &[T]) -> Result, CmsError> { + let src_layout = Layout::from(LAYOUT); + if input.len() % src_layout.channels() != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + let mut dst = try_vec![0.; input.len() / src_layout.channels() * 3]; + + let scale = if T::FINITE { + (self.linear_cap as f32 - 1.) / ((1 << self.bit_depth) - 1) as f32 + } else { + (T::NOT_FINITE_LINEAR_TABLE_SIZE - 1) as f32 + }; + + let cap_value = if T::FINITE { + ((1 << self.bit_depth) - 1) as f32 + } else { + (T::NOT_FINITE_LINEAR_TABLE_SIZE - 1) as f32 + }; + + for (src, dst) in input + .chunks_exact(src_layout.channels()) + .zip(dst.chunks_exact_mut(3)) + { + let j_r = src[0].as_() * scale; + let j_g = src[1].as_() * scale; + let j_b = src[2].as_() * scale; + dst[0] = self.r_lin[(j_r.round().min(cap_value).max(0.) as u16) as usize]; + dst[1] = self.g_lin[(j_g.round().min(cap_value).max(0.) as u16) as usize]; + dst[2] = self.b_lin[(j_b.round().min(cap_value).max(0.) as u16) as usize]; + } + Ok(dst) + } +} + +pub(crate) struct KatanaRgbLinearizationState { + pub(crate) stages: Vec + Send + Sync>>, + pub(crate) initial_stage: Box + Send + Sync>, +} + +pub(crate) fn katana_create_rgb_lin_lut< + T: Copy + Default + AsPrimitive + Send + Sync + AsPrimitive + PointeeSizeExpressible, + const BIT_DEPTH: usize, + const LINEAR_CAP: usize, +>( + layout: Layout, + source: &ColorProfile, + opts: TransformOptions, +) -> Result, CmsError> +where + u32: AsPrimitive, + f32: AsPrimitive, +{ + let lin_r = + source.build_r_linearize_table::(opts.allow_use_cicp_transfer)?; + let lin_g = + source.build_g_linearize_table::(opts.allow_use_cicp_transfer)?; + let lin_b = + source.build_b_linearize_table::(opts.allow_use_cicp_transfer)?; + + let lin_stage: Box + Send + Sync> = match layout { + Layout::Rgb => { + Box::new( + KatanaRgbLinearizationStage:: { + r_lin: lin_r, + g_lin: lin_g, + b_lin: lin_b, + bit_depth: BIT_DEPTH, + linear_cap: LINEAR_CAP, + _phantom: PhantomData, + }, + ) + } + Layout::Rgba => { + Box::new( + KatanaRgbLinearizationStage:: { + r_lin: lin_r, + g_lin: lin_g, + b_lin: lin_b, + bit_depth: BIT_DEPTH, + linear_cap: LINEAR_CAP, + _phantom: PhantomData, + }, + ) + } + Layout::Gray => unimplemented!("Gray should not be called on Rgb/Rgba execution path"), + Layout::GrayAlpha => { + unimplemented!("GrayAlpha should not be called on Rgb/Rgba execution path") + } + _ => unreachable!(), + }; + + let xyz_to_rgb = source.rgb_to_xyz_matrix(); + + let matrices: Vec + Send + Sync>> = + vec![Box::new(KatanaMatrixStage { + matrices: vec![ + xyz_to_rgb.to_f32(), + Matrix3f { + v: [ + [32768.0 / 65535.0, 0.0, 0.0], + [0.0, 32768.0 / 65535.0, 0.0], + [0.0, 0.0, 32768.0 / 65535.0], + ], + }, + ], + })]; + + Ok(KatanaRgbLinearizationState { + stages: matrices, + initial_stage: lin_stage, + }) +} diff --git a/deps/moxcms/src/conversions/katana/stages.rs b/deps/moxcms/src/conversions/katana/stages.rs new file mode 100644 index 0000000..de2ee42 --- /dev/null +++ b/deps/moxcms/src/conversions/katana/stages.rs @@ -0,0 +1,85 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::{CmsError, TransformExecutor}; +use std::marker::PhantomData; + +/// W storage working data type +/// I input/output data type +pub(crate) trait KatanaInitialStage { + fn to_pcs(&self, input: &[I]) -> Result, CmsError>; +} + +/// W storage working data type +/// I input/output data type +pub(crate) trait KatanaFinalStage { + fn to_output(&self, src: &mut [W], dst: &mut [I]) -> Result<(), CmsError>; +} + +/// W storage working data type +pub(crate) trait KatanaIntermediateStage { + fn stage(&self, input: &mut Vec) -> Result, CmsError>; +} + +pub(crate) struct BlackholeIntermediateStage { + pub(crate) _phantom: PhantomData, +} + +impl KatanaIntermediateStage for BlackholeIntermediateStage { + fn stage(&self, input: &mut Vec) -> Result, CmsError> { + Ok(std::mem::take(input)) + } +} + +/// I input/output data type +pub(crate) trait KatanaPostFinalizationStage { + fn finalize(&self, src: &[I], dst: &mut [I]) -> Result<(), CmsError>; +} + +/// W storage working data type +/// I input/output data type +pub(crate) struct Katana { + pub(crate) initial_stage: Box + Send + Sync>, + pub(crate) final_stage: Box + Sync + Send>, + pub(crate) stages: Vec + Send + Sync>>, + pub(crate) post_finalization: Vec + Send + Sync>>, +} + +impl TransformExecutor for Katana { + fn transform(&self, src: &[I], dst: &mut [I]) -> Result<(), CmsError> { + let mut working_vec = self.initial_stage.to_pcs(src)?; + for stage in self.stages.iter() { + working_vec = stage.stage(&mut working_vec)?; + } + self.final_stage.to_output(&mut working_vec, dst)?; + for finalization in self.post_finalization.iter() { + finalization.finalize(src, dst)?; + } + Ok(()) + } +} diff --git a/deps/moxcms/src/conversions/katana/xyz_lab.rs b/deps/moxcms/src/conversions/katana/xyz_lab.rs new file mode 100644 index 0000000..9295623 --- /dev/null +++ b/deps/moxcms/src/conversions/katana/xyz_lab.rs @@ -0,0 +1,62 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::katana::KatanaIntermediateStage; +use crate::{CmsError, Lab, Xyz}; + +#[derive(Default)] +pub(crate) struct KatanaStageLabToXyz {} + +impl KatanaIntermediateStage for KatanaStageLabToXyz { + fn stage(&self, input: &mut Vec) -> Result, CmsError> { + for dst in input.chunks_exact_mut(3) { + let lab = Lab::new(dst[0], dst[1], dst[2]); + let xyz = lab.to_pcs_xyz(); + dst[0] = xyz.x; + dst[1] = xyz.y; + dst[2] = xyz.z; + } + Ok(std::mem::take(input)) + } +} + +#[derive(Default)] +pub(crate) struct KatanaStageXyzToLab {} + +impl KatanaIntermediateStage for KatanaStageXyzToLab { + fn stage(&self, input: &mut Vec) -> Result, CmsError> { + for dst in input.chunks_exact_mut(3) { + let xyz = Xyz::new(dst[0], dst[1], dst[2]); + let lab = Lab::from_pcs_xyz(xyz); + dst[0] = lab.l; + dst[1] = lab.a; + dst[2] = lab.b; + } + Ok(std::mem::take(input)) + } +} diff --git a/deps/moxcms/src/conversions/katana/xyz_rgb.rs b/deps/moxcms/src/conversions/katana/xyz_rgb.rs new file mode 100644 index 0000000..bec05be --- /dev/null +++ b/deps/moxcms/src/conversions/katana/xyz_rgb.rs @@ -0,0 +1,223 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::katana::pcs_stages::KatanaMatrixStage; +use crate::conversions::katana::{ + KatanaDefaultIntermediate, KatanaFinalStage, KatanaIntermediateStage, +}; +use crate::mlaf::mlaf; +use crate::{ + CmsError, ColorProfile, GammaLutInterpolate, Layout, Matrix3f, PointeeSizeExpressible, + RenderingIntent, Rgb, TransformOptions, filmlike_clip, +}; +use num_traits::AsPrimitive; + +pub(crate) struct KatanaXyzToRgbStage { + pub(crate) r_gamma: Box<[T; 65536]>, + pub(crate) g_gamma: Box<[T; 65536]>, + pub(crate) b_gamma: Box<[T; 65536]>, + pub(crate) intent: RenderingIntent, + pub(crate) bit_depth: usize, + pub(crate) gamma_lut: usize, +} + +impl + PointeeSizeExpressible, const LAYOUT: u8> + KatanaFinalStage for KatanaXyzToRgbStage +where + u32: AsPrimitive, + f32: AsPrimitive, +{ + fn to_output(&self, src: &mut [f32], dst: &mut [T]) -> Result<(), CmsError> { + let dst_cn = Layout::from(LAYOUT); + let dst_channels = dst_cn.channels(); + if src.len() % 3 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % dst_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + let src_chunks = src.len() / 3; + let dst_chunks = dst.len() / dst_channels; + if src_chunks != dst_chunks { + return Err(CmsError::LaneSizeMismatch); + } + + let max_colors: T = (if T::FINITE { + ((1u32 << self.bit_depth) - 1) as f32 + } else { + 1.0 + }) + .as_(); + let lut_cap = (self.gamma_lut - 1) as f32; + + if self.intent != RenderingIntent::AbsoluteColorimetric { + for (src, dst) in src.chunks_exact(3).zip(dst.chunks_exact_mut(dst_channels)) { + let mut rgb = Rgb::new(src[0], src[1], src[2]); + if rgb.is_out_of_gamut() { + rgb = filmlike_clip(rgb); + } + let r = mlaf(0.5, rgb.r, lut_cap).min(lut_cap).max(0.) as u16; + let g = mlaf(0.5, rgb.g, lut_cap).min(lut_cap).max(0.) as u16; + let b = mlaf(0.5, rgb.b, lut_cap).min(lut_cap).max(0.) as u16; + + dst[0] = self.r_gamma[r as usize]; + dst[1] = self.g_gamma[g as usize]; + dst[2] = self.b_gamma[b as usize]; + if dst_cn == Layout::Rgba { + dst[3] = max_colors; + } + } + } else { + for (src, dst) in src.chunks_exact(3).zip(dst.chunks_exact_mut(dst_channels)) { + let rgb = Rgb::new(src[0], src[1], src[2]); + let r = mlaf(0.5, rgb.r, lut_cap).min(lut_cap).max(0.) as u16; + let g = mlaf(0.5, rgb.g, lut_cap).min(lut_cap).max(0.) as u16; + let b = mlaf(0.5, rgb.b, lut_cap).min(lut_cap).max(0.) as u16; + + dst[0] = self.r_gamma[r as usize]; + dst[1] = self.g_gamma[g as usize]; + dst[2] = self.b_gamma[b as usize]; + if dst_cn == Layout::Rgba { + dst[3] = max_colors; + } + } + } + + Ok(()) + } +} + +pub(crate) struct KatanaXyzRgbState { + pub(crate) stages: Vec + Send + Sync>>, + pub(crate) final_stage: Box + Send + Sync>, +} + +pub(crate) fn katana_prepare_inverse_lut_rgb_xyz< + T: Copy + + Default + + AsPrimitive + + Send + + Sync + + AsPrimitive + + PointeeSizeExpressible + + GammaLutInterpolate, + const BIT_DEPTH: usize, + const GAMMA_LUT: usize, +>( + dest: &ColorProfile, + dest_layout: Layout, + options: TransformOptions, +) -> Result, CmsError> +where + f32: AsPrimitive, + u32: AsPrimitive, +{ + // if !T::FINITE { + // if let Some(extended_gamma) = dest.try_extended_gamma_evaluator() { + // let xyz_to_rgb = dest.rgb_to_xyz_matrix().inverse(); + // + // let mut matrices = vec![Matrix3f { + // v: [ + // [65535.0 / 32768.0, 0.0, 0.0], + // [0.0, 65535.0 / 32768.0, 0.0], + // [0.0, 0.0, 65535.0 / 32768.0], + // ], + // }]; + // + // matrices.push(xyz_to_rgb.to_f32()); + // let xyz_to_rgb_stage = XyzToRgbStageExtended:: { + // gamma_evaluator: extended_gamma, + // matrices, + // phantom_data: PhantomData, + // }; + // xyz_to_rgb_stage.transform(lut)?; + // return Ok(()); + // } + // } + let gamma_map_r = dest.build_gamma_table::( + &dest.red_trc, + options.allow_use_cicp_transfer, + )?; + let gamma_map_g = dest.build_gamma_table::( + &dest.green_trc, + options.allow_use_cicp_transfer, + )?; + let gamma_map_b = dest.build_gamma_table::( + &dest.blue_trc, + options.allow_use_cicp_transfer, + )?; + + let xyz_to_rgb = dest.rgb_to_xyz_matrix().inverse(); + + let mut matrices: Vec> = + vec![Box::new(KatanaMatrixStage::new(Matrix3f { + v: [ + [65535.0 / 32768.0, 0.0, 0.0], + [0.0, 65535.0 / 32768.0, 0.0], + [0.0, 0.0, 65535.0 / 32768.0], + ], + }))]; + + matrices.push(Box::new(KatanaMatrixStage::new(xyz_to_rgb.to_f32()))); + match dest_layout { + Layout::Rgb => { + let xyz_to_rgb_stage = KatanaXyzToRgbStage:: { + r_gamma: gamma_map_r, + g_gamma: gamma_map_g, + b_gamma: gamma_map_b, + intent: options.rendering_intent, + bit_depth: BIT_DEPTH, + gamma_lut: GAMMA_LUT, + }; + Ok(KatanaXyzRgbState { + stages: matrices, + final_stage: Box::new(xyz_to_rgb_stage), + }) + } + Layout::Rgba => { + let xyz_to_rgb_stage = KatanaXyzToRgbStage:: { + r_gamma: gamma_map_r, + g_gamma: gamma_map_g, + b_gamma: gamma_map_b, + intent: options.rendering_intent, + bit_depth: BIT_DEPTH, + gamma_lut: GAMMA_LUT, + }; + Ok(KatanaXyzRgbState { + stages: matrices, + final_stage: Box::new(xyz_to_rgb_stage), + }) + } + Layout::Gray => unreachable!("Gray layout must not be called on Rgb/Rgba path"), + Layout::GrayAlpha => unreachable!("Gray layout must not be called on Rgb/Rgba path"), + _ => unreachable!( + "layout {:?} should not be called on xyz->rgb path", + dest_layout + ), + } +} diff --git a/deps/moxcms/src/conversions/lut3x3.rs b/deps/moxcms/src/conversions/lut3x3.rs new file mode 100644 index 0000000..1cb9aca --- /dev/null +++ b/deps/moxcms/src/conversions/lut3x3.rs @@ -0,0 +1,428 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::katana::{KatanaFinalStage, KatanaInitialStage}; +use crate::err::{MalformedSize, try_vec}; +use crate::profile::LutDataType; +use crate::safe_math::{SafeMul, SafePowi}; +use crate::trc::lut_interp_linear_float; +use crate::{ + CmsError, Cube, DataColorSpace, InterpolationMethod, PointeeSizeExpressible, Stage, + TransformOptions, Vector3f, +}; +use num_traits::AsPrimitive; + +#[derive(Default)] +struct Lut3x3 { + input: [Vec; 3], + clut: Vec, + grid_size: u8, + gamma: [Vec; 3], + interpolation_method: InterpolationMethod, + pcs: DataColorSpace, +} + +#[derive(Default)] +struct KatanaLut3x3 { + input: [Vec; 3], + clut: Vec, + grid_size: u8, + gamma: [Vec; 3], + interpolation_method: InterpolationMethod, + pcs: DataColorSpace, + _phantom: std::marker::PhantomData, + bit_depth: usize, +} + +fn make_lut_3x3( + lut: &LutDataType, + options: TransformOptions, + pcs: DataColorSpace, +) -> Result { + let clut_length: usize = (lut.num_clut_grid_points as usize) + .safe_powi(lut.num_input_channels as u32)? + .safe_mul(lut.num_output_channels as usize)?; + + let lin_table = lut.input_table.to_clut_f32(); + + if lin_table.len() < lut.num_input_table_entries as usize * 3 { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: lin_table.len(), + expected: lut.num_input_table_entries as usize * 3, + })); + } + + let lin_curve0 = lin_table[..lut.num_input_table_entries as usize].to_vec(); + let lin_curve1 = lin_table + [lut.num_input_table_entries as usize..lut.num_input_table_entries as usize * 2] + .to_vec(); + let lin_curve2 = lin_table + [lut.num_input_table_entries as usize * 2..lut.num_input_table_entries as usize * 3] + .to_vec(); + + let clut_table = lut.clut_table.to_clut_f32(); + if clut_table.len() != clut_length { + return Err(CmsError::MalformedClut(MalformedSize { + size: clut_table.len(), + expected: clut_length, + })); + } + + let gamma_curves = lut.output_table.to_clut_f32(); + + if gamma_curves.len() < lut.num_output_table_entries as usize * 3 { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: gamma_curves.len(), + expected: lut.num_output_table_entries as usize * 3, + })); + } + + let gamma_curve0 = gamma_curves[..lut.num_output_table_entries as usize].to_vec(); + let gamma_curve1 = gamma_curves + [lut.num_output_table_entries as usize..lut.num_output_table_entries as usize * 2] + .to_vec(); + let gamma_curve2 = gamma_curves + [lut.num_output_table_entries as usize * 2..lut.num_output_table_entries as usize * 3] + .to_vec(); + + let transform = Lut3x3 { + input: [lin_curve0, lin_curve1, lin_curve2], + gamma: [gamma_curve0, gamma_curve1, gamma_curve2], + interpolation_method: options.interpolation_method, + clut: clut_table, + grid_size: lut.num_clut_grid_points, + pcs, + }; + + Ok(transform) +} + +fn stage_lut_3x3( + lut: &LutDataType, + options: TransformOptions, + pcs: DataColorSpace, +) -> Result, CmsError> { + let lut = make_lut_3x3(lut, options, pcs)?; + + let transform = Lut3x3 { + input: lut.input, + gamma: lut.gamma, + interpolation_method: lut.interpolation_method, + clut: lut.clut, + grid_size: lut.grid_size, + pcs: lut.pcs, + }; + + Ok(Box::new(transform)) +} + +pub(crate) fn katana_input_stage_lut_3x3< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +>( + lut: &LutDataType, + options: TransformOptions, + pcs: DataColorSpace, + bit_depth: usize, +) -> Result + Send + Sync>, CmsError> +where + f32: AsPrimitive, +{ + let lut = make_lut_3x3(lut, options, pcs)?; + + let transform = KatanaLut3x3:: { + input: lut.input, + gamma: lut.gamma, + interpolation_method: lut.interpolation_method, + clut: lut.clut, + grid_size: lut.grid_size, + pcs: lut.pcs, + _phantom: std::marker::PhantomData, + bit_depth, + }; + + Ok(Box::new(transform)) +} + +pub(crate) fn katana_output_stage_lut_3x3< + T: Copy + Default + AsPrimitive + PointeeSizeExpressible + Send + Sync, +>( + lut: &LutDataType, + options: TransformOptions, + pcs: DataColorSpace, + bit_depth: usize, +) -> Result + Send + Sync>, CmsError> +where + f32: AsPrimitive, +{ + let lut = make_lut_3x3(lut, options, pcs)?; + + let transform = KatanaLut3x3:: { + input: lut.input, + gamma: lut.gamma, + interpolation_method: lut.interpolation_method, + clut: lut.clut, + grid_size: lut.grid_size, + pcs: lut.pcs, + _phantom: std::marker::PhantomData, + bit_depth, + }; + + Ok(Box::new(transform)) +} + +impl Lut3x3 { + fn transform_impl Vector3f>( + &self, + src: &[f32], + dst: &mut [f32], + fetch: Fetch, + ) -> Result<(), CmsError> { + let linearization_0 = &self.input[0]; + let linearization_1 = &self.input[1]; + let linearization_2 = &self.input[2]; + for (dest, src) in dst.chunks_exact_mut(3).zip(src.chunks_exact(3)) { + debug_assert!(self.grid_size as i32 >= 1); + let linear_x = lut_interp_linear_float(src[0], linearization_0); + let linear_y = lut_interp_linear_float(src[1], linearization_1); + let linear_z = lut_interp_linear_float(src[2], linearization_2); + + let clut = fetch(linear_x, linear_y, linear_z); + + let pcs_x = lut_interp_linear_float(clut.v[0], &self.gamma[0]); + let pcs_y = lut_interp_linear_float(clut.v[1], &self.gamma[1]); + let pcs_z = lut_interp_linear_float(clut.v[2], &self.gamma[2]); + dest[0] = pcs_x; + dest[1] = pcs_y; + dest[2] = pcs_z; + } + Ok(()) + } +} + +impl Stage for Lut3x3 { + fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError> { + let l_tbl = Cube::new(&self.clut, self.grid_size as usize); + + // If PCS is LAB then linear interpolation should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + return self.transform_impl(src, dst, |x, y, z| l_tbl.trilinear_vec3(x, y, z)); + } + + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.transform_impl(src, dst, |x, y, z| l_tbl.tetra_vec3(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.transform_impl(src, dst, |x, y, z| l_tbl.pyramid_vec3(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.transform_impl(src, dst, |x, y, z| l_tbl.prism_vec3(x, y, z))?; + } + InterpolationMethod::Linear => { + self.transform_impl(src, dst, |x, y, z| l_tbl.trilinear_vec3(x, y, z))?; + } + } + Ok(()) + } +} + +impl> KatanaLut3x3 +where + f32: AsPrimitive, +{ + fn to_pcs_impl Vector3f>( + &self, + input: &[T], + fetch: Fetch, + ) -> Result, CmsError> { + if input.len() % 3 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + let normalizing_value = if T::FINITE { + 1.0 / ((1u32 << self.bit_depth) - 1) as f32 + } else { + 1.0 + }; + let mut dst = try_vec![0.; input.len()]; + let linearization_0 = &self.input[0]; + let linearization_1 = &self.input[1]; + let linearization_2 = &self.input[2]; + for (dest, src) in dst.chunks_exact_mut(3).zip(input.chunks_exact(3)) { + let linear_x = + lut_interp_linear_float(src[0].as_() * normalizing_value, linearization_0); + let linear_y = + lut_interp_linear_float(src[1].as_() * normalizing_value, linearization_1); + let linear_z = + lut_interp_linear_float(src[2].as_() * normalizing_value, linearization_2); + + let clut = fetch(linear_x, linear_y, linear_z); + + let pcs_x = lut_interp_linear_float(clut.v[0], &self.gamma[0]); + let pcs_y = lut_interp_linear_float(clut.v[1], &self.gamma[1]); + let pcs_z = lut_interp_linear_float(clut.v[2], &self.gamma[2]); + dest[0] = pcs_x; + dest[1] = pcs_y; + dest[2] = pcs_z; + } + Ok(dst) + } + + fn to_output Vector3f>( + &self, + src: &[f32], + dst: &mut [T], + fetch: Fetch, + ) -> Result<(), CmsError> { + if src.len() % 3 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % 3 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() != src.len() { + return Err(CmsError::LaneSizeMismatch); + } + let norm_value = if T::FINITE { + ((1u32 << self.bit_depth) - 1) as f32 + } else { + 1.0 + }; + + let linearization_0 = &self.input[0]; + let linearization_1 = &self.input[1]; + let linearization_2 = &self.input[2]; + for (dest, src) in dst.chunks_exact_mut(3).zip(src.chunks_exact(3)) { + let linear_x = lut_interp_linear_float(src[0], linearization_0); + let linear_y = lut_interp_linear_float(src[1], linearization_1); + let linear_z = lut_interp_linear_float(src[2], linearization_2); + + let clut = fetch(linear_x, linear_y, linear_z); + + let pcs_x = lut_interp_linear_float(clut.v[0], &self.gamma[0]); + let pcs_y = lut_interp_linear_float(clut.v[1], &self.gamma[1]); + let pcs_z = lut_interp_linear_float(clut.v[2], &self.gamma[2]); + + if T::FINITE { + dest[0] = (pcs_x * norm_value).round().max(0.0).min(norm_value).as_(); + dest[1] = (pcs_y * norm_value).round().max(0.0).min(norm_value).as_(); + dest[2] = (pcs_z * norm_value).round().max(0.0).min(norm_value).as_(); + } else { + dest[0] = pcs_x.as_(); + dest[1] = pcs_y.as_(); + dest[2] = pcs_z.as_(); + } + } + Ok(()) + } +} + +impl> KatanaInitialStage + for KatanaLut3x3 +where + f32: AsPrimitive, +{ + fn to_pcs(&self, input: &[T]) -> Result, CmsError> { + let l_tbl = Cube::new(&self.clut, self.grid_size as usize); + + // If PCS is LAB then linear interpolation should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + return self.to_pcs_impl(input, |x, y, z| l_tbl.trilinear_vec3(x, y, z)); + } + + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.to_pcs_impl(input, |x, y, z| l_tbl.tetra_vec3(x, y, z)) + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.to_pcs_impl(input, |x, y, z| l_tbl.pyramid_vec3(x, y, z)) + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.to_pcs_impl(input, |x, y, z| l_tbl.prism_vec3(x, y, z)) + } + InterpolationMethod::Linear => { + self.to_pcs_impl(input, |x, y, z| l_tbl.trilinear_vec3(x, y, z)) + } + } + } +} + +impl> KatanaFinalStage + for KatanaLut3x3 +where + f32: AsPrimitive, +{ + fn to_output(&self, src: &mut [f32], dst: &mut [T]) -> Result<(), CmsError> { + let l_tbl = Cube::new(&self.clut, self.grid_size as usize); + + // If PCS is LAB then linear interpolation should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + return self.to_output(src, dst, |x, y, z| l_tbl.trilinear_vec3(x, y, z)); + } + + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.to_output(src, dst, |x, y, z| l_tbl.tetra_vec3(x, y, z)) + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.to_output(src, dst, |x, y, z| l_tbl.pyramid_vec3(x, y, z)) + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.to_output(src, dst, |x, y, z| l_tbl.prism_vec3(x, y, z)) + } + InterpolationMethod::Linear => { + self.to_output(src, dst, |x, y, z| l_tbl.trilinear_vec3(x, y, z)) + } + } + } +} + +pub(crate) fn create_lut3x3( + lut: &LutDataType, + src: &[f32], + options: TransformOptions, + pcs: DataColorSpace, +) -> Result, CmsError> { + if lut.num_input_channels != 3 || lut.num_output_channels != 3 { + return Err(CmsError::UnsupportedProfileConnection); + } + + let mut dest = try_vec![0.; src.len()]; + + let lut_stage = stage_lut_3x3(lut, options, pcs)?; + lut_stage.transform(src, &mut dest)?; + Ok(dest) +} diff --git a/deps/moxcms/src/conversions/lut3x4.rs b/deps/moxcms/src/conversions/lut3x4.rs new file mode 100644 index 0000000..6432be2 --- /dev/null +++ b/deps/moxcms/src/conversions/lut3x4.rs @@ -0,0 +1,249 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::err::try_vec; +use crate::profile::LutDataType; +use crate::safe_math::{SafeMul, SafePowi}; +use crate::trc::lut_interp_linear_float; +use crate::{ + CmsError, Cube, DataColorSpace, InterpolationMethod, MalformedSize, Stage, TransformOptions, + Vector4f, +}; +use num_traits::AsPrimitive; + +#[derive(Default)] +struct Lut3x4 { + input: [Vec; 3], + clut: Vec, + grid_size: u8, + gamma: [Vec; 4], + interpolation_method: InterpolationMethod, + pcs: DataColorSpace, +} + +fn make_lut_3x4( + lut: &LutDataType, + options: TransformOptions, + pcs: DataColorSpace, +) -> Result { + let clut_length: usize = (lut.num_clut_grid_points as usize) + .safe_powi(lut.num_input_channels as u32)? + .safe_mul(lut.num_output_channels as usize)?; + + let clut_table = lut.clut_table.to_clut_f32(); + if clut_table.len() != clut_length { + return Err(CmsError::MalformedClut(MalformedSize { + size: clut_table.len(), + expected: clut_length, + })); + } + + let linearization_table = lut.input_table.to_clut_f32(); + + if linearization_table.len() < lut.num_input_table_entries as usize * 3 { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: linearization_table.len(), + expected: lut.num_input_table_entries as usize * 3, + })); + } + + let linear_curve0 = linearization_table[..lut.num_input_table_entries as usize].to_vec(); + let linear_curve1 = linearization_table + [lut.num_input_table_entries as usize..lut.num_input_table_entries as usize * 2] + .to_vec(); + let linear_curve2 = linearization_table + [lut.num_input_table_entries as usize * 2..lut.num_input_table_entries as usize * 3] + .to_vec(); + + let gamma_table = lut.output_table.to_clut_f32(); + + if gamma_table.len() < lut.num_output_table_entries as usize * 4 { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: gamma_table.len(), + expected: lut.num_output_table_entries as usize * 4, + })); + } + + let gamma_curve0 = gamma_table[..lut.num_output_table_entries as usize].to_vec(); + let gamma_curve1 = gamma_table + [lut.num_output_table_entries as usize..lut.num_output_table_entries as usize * 2] + .to_vec(); + let gamma_curve2 = gamma_table + [lut.num_output_table_entries as usize * 2..lut.num_output_table_entries as usize * 3] + .to_vec(); + let gamma_curve3 = gamma_table + [lut.num_output_table_entries as usize * 3..lut.num_output_table_entries as usize * 4] + .to_vec(); + + let transform = Lut3x4 { + input: [linear_curve0, linear_curve1, linear_curve2], + interpolation_method: options.interpolation_method, + clut: clut_table, + grid_size: lut.num_clut_grid_points, + pcs, + gamma: [gamma_curve0, gamma_curve1, gamma_curve2, gamma_curve3], + }; + Ok(transform) +} + +fn stage_lut_3x4( + lut: &LutDataType, + options: TransformOptions, + pcs: DataColorSpace, +) -> Result, CmsError> { + let lut = make_lut_3x4(lut, options, pcs)?; + + let transform = Lut3x4 { + input: lut.input, + interpolation_method: lut.interpolation_method, + clut: lut.clut, + grid_size: lut.grid_size, + pcs: lut.pcs, + gamma: lut.gamma, + }; + Ok(Box::new(transform)) +} + +impl Lut3x4 { + fn transform_impl Vector4f>( + &self, + src: &[f32], + dst: &mut [f32], + fetch: Fetch, + ) -> Result<(), CmsError> { + let linearization_0 = &self.input[0]; + let linearization_1 = &self.input[1]; + let linearization_2 = &self.input[2]; + for (dest, src) in dst.chunks_exact_mut(4).zip(src.chunks_exact(3)) { + debug_assert!(self.grid_size as i32 >= 1); + let linear_x = lut_interp_linear_float(src[0], linearization_0); + let linear_y = lut_interp_linear_float(src[1], linearization_1); + let linear_z = lut_interp_linear_float(src[2], linearization_2); + + let clut = fetch(linear_x, linear_y, linear_z); + + let pcs_x = lut_interp_linear_float(clut.v[0], &self.gamma[0]); + let pcs_y = lut_interp_linear_float(clut.v[1], &self.gamma[1]); + let pcs_z = lut_interp_linear_float(clut.v[2], &self.gamma[2]); + let pcs_w = lut_interp_linear_float(clut.v[3], &self.gamma[3]); + dest[0] = pcs_x; + dest[1] = pcs_y; + dest[2] = pcs_z; + dest[3] = pcs_w; + } + Ok(()) + } +} + +impl Stage for Lut3x4 { + fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError> { + let l_tbl = Cube::new(&self.clut, self.grid_size as usize); + + // If PCS is LAB then linear interpolation should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + return self.transform_impl(src, dst, |x, y, z| l_tbl.trilinear_vec4(x, y, z)); + } + + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.transform_impl(src, dst, |x, y, z| l_tbl.tetra_vec4(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.transform_impl(src, dst, |x, y, z| l_tbl.pyramid_vec4(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.transform_impl(src, dst, |x, y, z| l_tbl.prism_vec4(x, y, z))?; + } + InterpolationMethod::Linear => { + self.transform_impl(src, dst, |x, y, z| l_tbl.trilinear_vec4(x, y, z))?; + } + } + Ok(()) + } +} + +pub(crate) fn create_lut3_samples() -> Vec +where + u32: AsPrimitive, +{ + let lut_size: u32 = (3 * SAMPLES * SAMPLES * SAMPLES) as u32; + + assert!(SAMPLES >= 1); + + let mut src = Vec::with_capacity(lut_size as usize); + for x in 0..SAMPLES as u32 { + for y in 0..SAMPLES as u32 { + for z in 0..SAMPLES as u32 { + src.push(x.as_()); + src.push(y.as_()); + src.push(z.as_()); + } + } + } + src +} + +pub(crate) fn create_lut3_samples_norm() -> Vec { + let lut_size: u32 = (3 * SAMPLES * SAMPLES * SAMPLES) as u32; + + assert!(SAMPLES >= 1); + + let scale = 1. / (SAMPLES as f32 - 1.0); + + let mut src = Vec::with_capacity(lut_size as usize); + for x in 0..SAMPLES as u32 { + for y in 0..SAMPLES as u32 { + for z in 0..SAMPLES as u32 { + src.push(x as f32 * scale); + src.push(y as f32 * scale); + src.push(z as f32 * scale); + } + } + } + src +} + +pub(crate) fn create_lut3x4( + lut: &LutDataType, + src: &[f32], + options: TransformOptions, + pcs: DataColorSpace, +) -> Result, CmsError> { + if lut.num_input_channels != 3 || lut.num_output_channels != 4 { + return Err(CmsError::UnsupportedProfileConnection); + } + + let mut dest = try_vec![0.; (src.len() / 3) * 4]; + + let lut_stage = stage_lut_3x4(lut, options, pcs)?; + lut_stage.transform(src, &mut dest)?; + Ok(dest) +} diff --git a/deps/moxcms/src/conversions/lut4.rs b/deps/moxcms/src/conversions/lut4.rs new file mode 100644 index 0000000..68b1fe1 --- /dev/null +++ b/deps/moxcms/src/conversions/lut4.rs @@ -0,0 +1,360 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::katana::KatanaInitialStage; +use crate::err::try_vec; +use crate::profile::LutDataType; +use crate::safe_math::{SafeMul, SafePowi}; +use crate::trc::lut_interp_linear_float; +use crate::{ + CmsError, DataColorSpace, Hypercube, InterpolationMethod, MalformedSize, + PointeeSizeExpressible, Stage, TransformOptions, Vector3f, +}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +#[allow(unused)] +#[derive(Default)] +struct Lut4x3 { + linearization: [Vec; 4], + clut: Vec, + grid_size: u8, + output: [Vec; 3], + interpolation_method: InterpolationMethod, + pcs: DataColorSpace, +} + +#[allow(unused)] +#[derive(Default)] +struct KatanaLut4x3> { + linearization: [Vec; 4], + clut: Vec, + grid_size: u8, + output: [Vec; 3], + interpolation_method: InterpolationMethod, + pcs: DataColorSpace, + _phantom: PhantomData, + bit_depth: usize, +} + +#[allow(unused)] +impl Lut4x3 { + fn transform_impl Vector3f>( + &self, + src: &[f32], + dst: &mut [f32], + fetch: Fetch, + ) -> Result<(), CmsError> { + let linearization_0 = &self.linearization[0]; + let linearization_1 = &self.linearization[1]; + let linearization_2 = &self.linearization[2]; + let linearization_3 = &self.linearization[3]; + for (dest, src) in dst.chunks_exact_mut(3).zip(src.chunks_exact(4)) { + debug_assert!(self.grid_size as i32 >= 1); + let linear_x = lut_interp_linear_float(src[0], linearization_0); + let linear_y = lut_interp_linear_float(src[1], linearization_1); + let linear_z = lut_interp_linear_float(src[2], linearization_2); + let linear_w = lut_interp_linear_float(src[3], linearization_3); + + let clut = fetch(linear_x, linear_y, linear_z, linear_w); + + let pcs_x = lut_interp_linear_float(clut.v[0], &self.output[0]); + let pcs_y = lut_interp_linear_float(clut.v[1], &self.output[1]); + let pcs_z = lut_interp_linear_float(clut.v[2], &self.output[2]); + dest[0] = pcs_x; + dest[1] = pcs_y; + dest[2] = pcs_z; + } + Ok(()) + } +} + +macro_rules! define_lut4_dispatch { + ($dispatcher: ident) => { + impl Stage for $dispatcher { + fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError> { + let l_tbl = Hypercube::new(&self.clut, self.grid_size as usize); + + // If Source PCS is LAB trilinear should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + return self + .transform_impl(src, dst, |x, y, z, w| l_tbl.quadlinear_vec3(x, y, z, w)); + } + + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.transform_impl(src, dst, |x, y, z, w| l_tbl.tetra_vec3(x, y, z, w))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.transform_impl(src, dst, |x, y, z, w| l_tbl.pyramid_vec3(x, y, z, w))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.transform_impl(src, dst, |x, y, z, w| l_tbl.prism_vec3(x, y, z, w))? + } + InterpolationMethod::Linear => { + self.transform_impl(src, dst, |x, y, z, w| { + l_tbl.quadlinear_vec3(x, y, z, w) + })? + } + } + Ok(()) + } + } + }; +} + +impl> KatanaLut4x3 { + fn to_pcs_impl Vector3f>( + &self, + input: &[T], + fetch: Fetch, + ) -> Result, CmsError> { + if input.len() % 4 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + let norm_value = if T::FINITE { + 1.0 / ((1u32 << self.bit_depth) - 1) as f32 + } else { + 1.0 + }; + let mut dst = try_vec![0.; (input.len() / 4) * 3]; + let linearization_0 = &self.linearization[0]; + let linearization_1 = &self.linearization[1]; + let linearization_2 = &self.linearization[2]; + let linearization_3 = &self.linearization[3]; + for (dest, src) in dst.chunks_exact_mut(3).zip(input.chunks_exact(4)) { + let linear_x = lut_interp_linear_float(src[0].as_() * norm_value, linearization_0); + let linear_y = lut_interp_linear_float(src[1].as_() * norm_value, linearization_1); + let linear_z = lut_interp_linear_float(src[2].as_() * norm_value, linearization_2); + let linear_w = lut_interp_linear_float(src[3].as_() * norm_value, linearization_3); + + let clut = fetch(linear_x, linear_y, linear_z, linear_w); + + let pcs_x = lut_interp_linear_float(clut.v[0], &self.output[0]); + let pcs_y = lut_interp_linear_float(clut.v[1], &self.output[1]); + let pcs_z = lut_interp_linear_float(clut.v[2], &self.output[2]); + dest[0] = pcs_x; + dest[1] = pcs_y; + dest[2] = pcs_z; + } + Ok(dst) + } +} + +impl> KatanaInitialStage + for KatanaLut4x3 +{ + fn to_pcs(&self, input: &[T]) -> Result, CmsError> { + if input.len() % 4 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + let l_tbl = Hypercube::new(&self.clut, self.grid_size as usize); + + // If Source PCS is LAB trilinear should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + return self.to_pcs_impl(input, |x, y, z, w| l_tbl.quadlinear_vec3(x, y, z, w)); + } + + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.to_pcs_impl(input, |x, y, z, w| l_tbl.tetra_vec3(x, y, z, w)) + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.to_pcs_impl(input, |x, y, z, w| l_tbl.pyramid_vec3(x, y, z, w)) + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.to_pcs_impl(input, |x, y, z, w| l_tbl.prism_vec3(x, y, z, w)) + } + InterpolationMethod::Linear => { + self.to_pcs_impl(input, |x, y, z, w| l_tbl.quadlinear_vec3(x, y, z, w)) + } + } + } +} + +define_lut4_dispatch!(Lut4x3); + +fn make_lut_4x3( + lut: &LutDataType, + options: TransformOptions, + pcs: DataColorSpace, +) -> Result { + // There is 4 possible cases: + // - All curves are non-linear + // - Linearization curves are non-linear, but gamma is linear + // - Gamma curves are non-linear, but linearization is linear + // - All curves linear + let clut_length: usize = (lut.num_clut_grid_points as usize) + .safe_powi(lut.num_input_channels as u32)? + .safe_mul(lut.num_output_channels as usize)?; + + let clut_table = lut.clut_table.to_clut_f32(); + if clut_table.len() != clut_length { + return Err(CmsError::MalformedClut(MalformedSize { + size: clut_table.len(), + expected: clut_length, + })); + } + + let linearization_table = lut.input_table.to_clut_f32(); + + if linearization_table.len() < lut.num_input_table_entries as usize * 4 { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: linearization_table.len(), + expected: lut.num_input_table_entries as usize * 4, + })); + } + + let lin_curve0 = linearization_table[0..lut.num_input_table_entries as usize].to_vec(); + let lin_curve1 = linearization_table + [lut.num_input_table_entries as usize..lut.num_input_table_entries as usize * 2] + .to_vec(); + let lin_curve2 = linearization_table + [lut.num_input_table_entries as usize * 2..lut.num_input_table_entries as usize * 3] + .to_vec(); + let lin_curve3 = linearization_table + [lut.num_input_table_entries as usize * 3..lut.num_input_table_entries as usize * 4] + .to_vec(); + + let gamma_table = lut.output_table.to_clut_f32(); + + if gamma_table.len() < lut.num_output_table_entries as usize * 3 { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: gamma_table.len(), + expected: lut.num_output_table_entries as usize * 3, + })); + } + + let gamma_curve0 = gamma_table[..lut.num_output_table_entries as usize].to_vec(); + let gamma_curve1 = gamma_table + [lut.num_output_table_entries as usize..lut.num_output_table_entries as usize * 2] + .to_vec(); + let gamma_curve2 = gamma_table + [lut.num_output_table_entries as usize * 2..lut.num_output_table_entries as usize * 3] + .to_vec(); + + let transform = Lut4x3 { + linearization: [lin_curve0, lin_curve1, lin_curve2, lin_curve3], + interpolation_method: options.interpolation_method, + pcs, + clut: clut_table, + grid_size: lut.num_clut_grid_points, + output: [gamma_curve0, gamma_curve1, gamma_curve2], + }; + Ok(transform) +} + +fn stage_lut_4x3( + lut: &LutDataType, + options: TransformOptions, + pcs: DataColorSpace, +) -> Result, CmsError> { + let lut = make_lut_4x3(lut, options, pcs)?; + let transform = Lut4x3 { + linearization: lut.linearization, + interpolation_method: lut.interpolation_method, + pcs: lut.pcs, + clut: lut.clut, + grid_size: lut.grid_size, + output: lut.output, + }; + Ok(Box::new(transform)) +} + +pub(crate) fn katana_input_stage_lut_4x3< + T: Copy + PointeeSizeExpressible + AsPrimitive + Send + Sync, +>( + lut: &LutDataType, + options: TransformOptions, + pcs: DataColorSpace, + bit_depth: usize, +) -> Result + Send + Sync>, CmsError> { + // There is 4 possible cases: + // - All curves are non-linear + // - Linearization curves are non-linear, but gamma is linear + // - Gamma curves are non-linear, but linearization is linear + // - All curves linear + let lut = make_lut_4x3(lut, options, pcs)?; + + let transform = KatanaLut4x3:: { + linearization: lut.linearization, + interpolation_method: lut.interpolation_method, + pcs: lut.pcs, + clut: lut.clut, + grid_size: lut.grid_size, + output: lut.output, + _phantom: PhantomData, + bit_depth, + }; + Ok(Box::new(transform)) +} + +pub(crate) fn create_lut4_norm_samples() -> Vec { + let lut_size: u32 = (4 * SAMPLES * SAMPLES * SAMPLES * SAMPLES) as u32; + + let mut src = Vec::with_capacity(lut_size as usize); + + let recpeq = 1f32 / (SAMPLES - 1) as f32; + for k in 0..SAMPLES { + for c in 0..SAMPLES { + for m in 0..SAMPLES { + for y in 0..SAMPLES { + src.push(c as f32 * recpeq); + src.push(m as f32 * recpeq); + src.push(y as f32 * recpeq); + src.push(k as f32 * recpeq); + } + } + } + } + src +} + +pub(crate) fn create_lut4( + lut: &LutDataType, + options: TransformOptions, + pcs: DataColorSpace, +) -> Result, CmsError> { + if lut.num_input_channels != 4 { + return Err(CmsError::UnsupportedProfileConnection); + } + let lut_size: u32 = (4 * SAMPLES * SAMPLES * SAMPLES * SAMPLES) as u32; + + let src = create_lut4_norm_samples::(); + let mut dest = try_vec![0.; (lut_size as usize) / 4 * 3]; + + let lut_stage = stage_lut_4x3(lut, options, pcs)?; + lut_stage.transform(&src, &mut dest)?; + Ok(dest) +} diff --git a/deps/moxcms/src/conversions/lut_transforms.rs b/deps/moxcms/src/conversions/lut_transforms.rs new file mode 100644 index 0000000..432903e --- /dev/null +++ b/deps/moxcms/src/conversions/lut_transforms.rs @@ -0,0 +1,802 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::lut3x3::{ + create_lut3x3, katana_input_stage_lut_3x3, katana_output_stage_lut_3x3, +}; +use crate::conversions::lut3x4::{create_lut3_samples_norm, create_lut3x4}; +use crate::conversions::lut4::{create_lut4, create_lut4_norm_samples, katana_input_stage_lut_4x3}; +use crate::conversions::mab::{prepare_mab_3x3, prepare_mba_3x3}; +use crate::conversions::transform_lut3_to_4::make_transform_3x4; +use crate::mlaf::mlaf; +use crate::{ + CmsError, ColorProfile, DataColorSpace, InPlaceStage, Layout, LutWarehouse, Matrix3f, + ProfileVersion, TransformExecutor, TransformOptions, +}; +use num_traits::AsPrimitive; + +pub(crate) struct MatrixStage { + pub(crate) matrices: Vec, +} + +impl InPlaceStage for MatrixStage { + fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> { + if !self.matrices.is_empty() { + let m = self.matrices[0]; + for dst in dst.chunks_exact_mut(3) { + let x = dst[0]; + let y = dst[1]; + let z = dst[2]; + dst[0] = mlaf(mlaf(x * m.v[0][0], y, m.v[0][1]), z, m.v[0][2]); + dst[1] = mlaf(mlaf(x * m.v[1][0], y, m.v[1][1]), z, m.v[1][2]); + dst[2] = mlaf(mlaf(x * m.v[2][0], y, m.v[2][1]), z, m.v[2][2]); + } + } + + for m in self.matrices.iter().skip(1) { + for dst in dst.chunks_exact_mut(3) { + let x = dst[0]; + let y = dst[1]; + let z = dst[2]; + dst[0] = mlaf(mlaf(x * m.v[0][0], y, m.v[0][1]), z, m.v[0][2]); + dst[1] = mlaf(mlaf(x * m.v[1][0], y, m.v[1][1]), z, m.v[1][2]); + dst[2] = mlaf(mlaf(x * m.v[2][0], y, m.v[2][1]), z, m.v[2][2]); + } + } + + Ok(()) + } +} + +pub(crate) const LUT_SAMPLING: u16 = 255; + +pub(crate) trait Lut3x3Factory { + fn make_transform_3x3< + T: Copy + AsPrimitive + Default + PointeeSizeExpressible + 'static + Send + Sync, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + >( + lut: Vec, + options: TransformOptions, + color_space: DataColorSpace, + is_linear: bool, + ) -> Box + Send + Sync> + where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, + (): LutBarycentricReduction; +} + +pub(crate) trait Lut4x3Factory { + fn make_transform_4x3< + T: Copy + AsPrimitive + Default + PointeeSizeExpressible + 'static + Send + Sync, + const LAYOUT: u8, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + >( + lut: Vec, + options: TransformOptions, + color_space: DataColorSpace, + is_linear: bool, + ) -> Box + Sync + Send> + where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, + (): LutBarycentricReduction; +} + +fn pcs_lab_v4_to_v2(profile: &ColorProfile, lut: &mut [f32]) { + if profile.pcs == DataColorSpace::Lab + && profile.version_internal <= ProfileVersion::V4_0 + && lut.len() % 3 == 0 + { + assert_eq!( + lut.len() % 3, + 0, + "Lut {:?} is not a multiple of 3, this should not happen for lab", + lut.len() + ); + let v_mat = vec![Matrix3f { + v: [ + [65280.0 / 65535.0, 0f32, 0f32], + [0f32, 65280.0 / 65535.0, 0f32], + [0f32, 0f32, 65280.0 / 65535.0f32], + ], + }]; + let stage = MatrixStage { matrices: v_mat }; + stage.transform(lut).unwrap(); + } +} + +fn pcs_lab_v2_to_v4(profile: &ColorProfile, lut: &mut [f32]) { + if profile.pcs == DataColorSpace::Lab + && profile.version_internal <= ProfileVersion::V4_0 + && lut.len() % 3 == 0 + { + assert_eq!( + lut.len() % 3, + 0, + "Lut {:?} is not a multiple of 3, this should not happen for lab", + lut.len() + ); + let v_mat = vec![Matrix3f { + v: [ + [65535.0 / 65280.0f32, 0f32, 0f32], + [0f32, 65535.0f32 / 65280.0f32, 0f32], + [0f32, 0f32, 65535.0f32 / 65280.0f32], + ], + }]; + let stage = MatrixStage { matrices: v_mat }; + stage.transform(lut).unwrap(); + } +} + +macro_rules! make_transform_3x3_fn { + ($method_name: ident, $exec_impl: ident) => { + fn $method_name< + T: Copy + + Default + + AsPrimitive + + Send + + Sync + + AsPrimitive + + PointeeSizeExpressible, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + >( + src_layout: Layout, + dst_layout: Layout, + lut: Vec, + options: TransformOptions, + color_space: DataColorSpace, + is_linear: bool, + ) -> Box + Send + Sync> + where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, + (): LutBarycentricReduction, + { + match src_layout { + Layout::Rgb => match dst_layout { + Layout::Rgb => $exec_impl::make_transform_3x3::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgb as u8 }, + GRID_SIZE, + BIT_DEPTH, + >(lut, options, color_space, is_linear), + Layout::Rgba => $exec_impl::make_transform_3x3::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgba as u8 }, + GRID_SIZE, + BIT_DEPTH, + >(lut, options, color_space, is_linear), + _ => unimplemented!(), + }, + Layout::Rgba => match dst_layout { + Layout::Rgb => $exec_impl::make_transform_3x3::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgb as u8 }, + GRID_SIZE, + BIT_DEPTH, + >(lut, options, color_space, is_linear), + Layout::Rgba => $exec_impl::make_transform_3x3::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgba as u8 }, + GRID_SIZE, + BIT_DEPTH, + >(lut, options, color_space, is_linear), + _ => unimplemented!(), + }, + _ => unimplemented!(), + } + } + }; +} + +macro_rules! make_transform_4x3_fn { + ($method_name: ident, $exec_name: ident) => { + fn $method_name< + T: Copy + + Default + + AsPrimitive + + Send + + Sync + + AsPrimitive + + PointeeSizeExpressible, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + >( + dst_layout: Layout, + lut: Vec, + options: TransformOptions, + data_color_space: DataColorSpace, + is_linear: bool, + ) -> Box + Send + Sync> + where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, + (): LutBarycentricReduction, + { + match dst_layout { + Layout::Rgb => $exec_name::make_transform_4x3::< + T, + { Layout::Rgb as u8 }, + GRID_SIZE, + BIT_DEPTH, + >(lut, options, data_color_space, is_linear), + Layout::Rgba => $exec_name::make_transform_4x3::< + T, + { Layout::Rgba as u8 }, + GRID_SIZE, + BIT_DEPTH, + >(lut, options, data_color_space, is_linear), + _ => unimplemented!(), + } + } + }; +} + +#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] +use crate::conversions::neon::NeonLut3x3Factory; +#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] +make_transform_3x3_fn!(make_transformer_3x3, NeonLut3x3Factory); + +#[cfg(not(all(target_arch = "aarch64", target_feature = "neon", feature = "neon")))] +use crate::conversions::transform_lut3_to_3::DefaultLut3x3Factory; +#[cfg(not(all(target_arch = "aarch64", target_feature = "neon", feature = "neon")))] +make_transform_3x3_fn!(make_transformer_3x3, DefaultLut3x3Factory); + +#[cfg(all(target_arch = "x86_64", feature = "avx"))] +use crate::conversions::avx::AvxLut3x3Factory; +#[cfg(all(target_arch = "x86_64", feature = "avx"))] +make_transform_3x3_fn!(make_transformer_3x3_avx_fma, AvxLut3x3Factory); + +#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] +use crate::conversions::sse::SseLut3x3Factory; +#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] +make_transform_3x3_fn!(make_transformer_3x3_sse41, SseLut3x3Factory); + +#[cfg(all(target_arch = "x86_64", feature = "avx"))] +use crate::conversions::avx::AvxLut4x3Factory; +use crate::conversions::interpolator::LutBarycentricReduction; +use crate::conversions::katana::{ + Katana, KatanaDefaultIntermediate, KatanaInitialStage, KatanaPostFinalizationStage, + KatanaStageLabToXyz, KatanaStageXyzToLab, katana_create_rgb_lin_lut, katana_pcs_lab_v2_to_v4, + katana_pcs_lab_v4_to_v2, katana_prepare_inverse_lut_rgb_xyz, multi_dimensional_3x3_to_device, + multi_dimensional_3x3_to_pcs, multi_dimensional_4x3_to_pcs, +}; +use crate::conversions::mab4x3::prepare_mab_4x3; +use crate::conversions::mba3x4::prepare_mba_3x4; +use crate::conversions::md_luts_factory::{do_any_to_any, prepare_alpha_finalizer}; +// use crate::conversions::bpc::compensate_bpc_in_lut; + +#[cfg(all(target_arch = "x86_64", feature = "avx"))] +make_transform_4x3_fn!(make_transformer_4x3_avx_fma, AvxLut4x3Factory); + +#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] +use crate::conversions::sse::SseLut4x3Factory; +#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] +make_transform_4x3_fn!(make_transformer_4x3_sse41, SseLut4x3Factory); + +#[cfg(not(all(target_arch = "aarch64", target_feature = "neon", feature = "neon")))] +use crate::conversions::transform_lut4_to_3::DefaultLut4x3Factory; + +#[cfg(not(all(target_arch = "aarch64", target_feature = "neon", feature = "neon")))] +make_transform_4x3_fn!(make_transformer_4x3, DefaultLut4x3Factory); + +#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] +use crate::conversions::neon::NeonLut4x3Factory; +use crate::conversions::prelude_lut_xyz_rgb::{create_rgb_lin_lut, prepare_inverse_lut_rgb_xyz}; +use crate::conversions::xyz_lab::{StageLabToXyz, StageXyzToLab}; +use crate::transform::PointeeSizeExpressible; +use crate::trc::GammaLutInterpolate; + +#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] +make_transform_4x3_fn!(make_transformer_4x3, NeonLut4x3Factory); + +#[inline(never)] +#[cold] +pub(crate) fn make_lut_transform< + T: Copy + + Default + + AsPrimitive + + Send + + Sync + + AsPrimitive + + PointeeSizeExpressible + + GammaLutInterpolate, + const BIT_DEPTH: usize, + const LINEAR_CAP: usize, + const GAMMA_LUT: usize, +>( + src_layout: Layout, + source: &ColorProfile, + dst_layout: Layout, + dest: &ColorProfile, + options: TransformOptions, +) -> Result + Send + Sync>, CmsError> +where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, + (): LutBarycentricReduction, +{ + if (source.color_space == DataColorSpace::Cmyk || source.color_space == DataColorSpace::Color4) + && (dest.color_space == DataColorSpace::Rgb || dest.color_space == DataColorSpace::Lab) + { + source.color_space.check_layout(src_layout)?; + dest.color_space.check_layout(dst_layout)?; + if source.pcs != DataColorSpace::Xyz && source.pcs != DataColorSpace::Lab { + return Err(CmsError::UnsupportedProfileConnection); + } + if dest.pcs != DataColorSpace::Lab && dest.pcs != DataColorSpace::Xyz { + return Err(CmsError::UnsupportedProfileConnection); + } + + const GRID_SIZE: usize = 17; + + let is_katana_required_for_source = source + .get_device_to_pcs(options.rendering_intent) + .ok_or(CmsError::UnsupportedLutRenderingIntent( + source.rendering_intent, + )) + .map(|x| x.is_katana_required())?; + + let is_katana_required_for_destination = + if dest.is_matrix_shaper() || dest.pcs == DataColorSpace::Xyz { + false + } else if dest.pcs == DataColorSpace::Lab { + dest.get_pcs_to_device(options.rendering_intent) + .ok_or(CmsError::UnsupportedProfileConnection) + .map(|x| x.is_katana_required())? + } else { + return Err(CmsError::UnsupportedProfileConnection); + }; + + if is_katana_required_for_source || is_katana_required_for_destination { + let initial_stage: Box + Send + Sync> = + match source.get_device_to_pcs(options.rendering_intent).ok_or( + CmsError::UnsupportedLutRenderingIntent(source.rendering_intent), + )? { + LutWarehouse::Lut(lut) => { + katana_input_stage_lut_4x3::(lut, options, source.pcs, BIT_DEPTH)? + } + LutWarehouse::Multidimensional(mab) => { + multi_dimensional_4x3_to_pcs::(mab, options, source.pcs, BIT_DEPTH)? + } + }; + + let mut stages = Vec::new(); + + stages.push(katana_pcs_lab_v2_to_v4(source)); + if source.pcs == DataColorSpace::Lab { + stages.push(Box::new(KatanaStageLabToXyz::default())); + } + if dest.pcs == DataColorSpace::Lab { + stages.push(Box::new(KatanaStageXyzToLab::default())); + } + stages.push(katana_pcs_lab_v4_to_v2(dest)); + + let final_stage = if dest.has_pcs_to_device_lut() { + let pcs_to_device = dest + .get_pcs_to_device(options.rendering_intent) + .ok_or(CmsError::UnsupportedProfileConnection)?; + match pcs_to_device { + LutWarehouse::Lut(lut) => { + katana_output_stage_lut_3x3::(lut, options, dest.pcs, BIT_DEPTH)? + } + LutWarehouse::Multidimensional(mab) => { + multi_dimensional_3x3_to_device::(mab, options, dest.pcs, BIT_DEPTH)? + } + } + } else if dest.is_matrix_shaper() { + let state = katana_prepare_inverse_lut_rgb_xyz::( + dest, dst_layout, options, + )?; + stages.extend(state.stages); + state.final_stage + } else { + return Err(CmsError::UnsupportedProfileConnection); + }; + + let mut post_finalization: Vec + Send + Sync>> = + Vec::new(); + if let Some(stage) = + prepare_alpha_finalizer::(src_layout, source, dst_layout, dest, BIT_DEPTH) + { + post_finalization.push(stage); + } + + return Ok(Box::new(Katana:: { + initial_stage, + final_stage, + stages, + post_finalization, + })); + } + + let mut lut = match source.get_device_to_pcs(options.rendering_intent).ok_or( + CmsError::UnsupportedLutRenderingIntent(source.rendering_intent), + )? { + LutWarehouse::Lut(lut) => create_lut4::(lut, options, source.pcs)?, + LutWarehouse::Multidimensional(m_curves) => { + let mut samples = create_lut4_norm_samples::(); + prepare_mab_4x3(m_curves, &mut samples, options, source.pcs)? + } + }; + + pcs_lab_v2_to_v4(source, &mut lut); + + if source.pcs == DataColorSpace::Lab { + let lab_to_xyz_stage = StageLabToXyz::default(); + lab_to_xyz_stage.transform(&mut lut)?; + } + + // if source.color_space == DataColorSpace::Cmyk + // && (options.rendering_intent == RenderingIntent::Perceptual + // || options.rendering_intent == RenderingIntent::RelativeColorimetric) + // && options.black_point_compensation + // { + // if let (Some(src_bp), Some(dst_bp)) = ( + // source.detect_black_point::(&lut), + // dest.detect_black_point::(&lut), + // ) { + // compensate_bpc_in_lut(&mut lut, src_bp, dst_bp); + // } + // } + + if dest.pcs == DataColorSpace::Lab { + let lab_to_xyz_stage = StageXyzToLab::default(); + lab_to_xyz_stage.transform(&mut lut)?; + } + + pcs_lab_v4_to_v2(dest, &mut lut); + + if dest.pcs == DataColorSpace::Xyz { + if dest.is_matrix_shaper() { + prepare_inverse_lut_rgb_xyz::(dest, &mut lut, options)?; + } else { + return Err(CmsError::UnsupportedProfileConnection); + } + } else if dest.pcs == DataColorSpace::Lab { + let pcs_to_device = dest + .get_pcs_to_device(options.rendering_intent) + .ok_or(CmsError::UnsupportedProfileConnection)?; + match pcs_to_device { + LutWarehouse::Lut(lut_data_type) => { + lut = create_lut3x3(lut_data_type, &lut, options, dest.pcs)? + } + LutWarehouse::Multidimensional(mab) => { + prepare_mba_3x3(mab, &mut lut, options, dest.pcs)? + } + } + } + + let is_dest_linear_profile = dest.color_space == DataColorSpace::Rgb + && dest.is_matrix_shaper() + && dest.is_linear_matrix_shaper(); + + #[cfg(all(target_arch = "x86_64", feature = "avx"))] + if std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma") + { + return Ok(make_transformer_4x3_avx_fma::( + dst_layout, + lut, + options, + dest.color_space, + is_dest_linear_profile, + )); + } + #[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] + if std::arch::is_x86_feature_detected!("sse4.1") { + return Ok(make_transformer_4x3_sse41::( + dst_layout, + lut, + options, + dest.color_space, + is_dest_linear_profile, + )); + } + + Ok(make_transformer_4x3::( + dst_layout, + lut, + options, + dest.color_space, + is_dest_linear_profile, + )) + } else if (source.color_space == DataColorSpace::Rgb + || source.color_space == DataColorSpace::Lab) + && (dest.color_space == DataColorSpace::Cmyk || dest.color_space == DataColorSpace::Color4) + { + source.color_space.check_layout(src_layout)?; + dest.color_space.check_layout(dst_layout)?; + + if source.pcs != DataColorSpace::Xyz && source.pcs != DataColorSpace::Lab { + return Err(CmsError::UnsupportedProfileConnection); + } + + const GRID_SIZE: usize = 33; + + let mut lut: Vec; + + if source.has_device_to_pcs_lut() { + let device_to_pcs = source + .get_device_to_pcs(options.rendering_intent) + .ok_or(CmsError::UnsupportedProfileConnection)?; + lut = create_lut3_samples_norm::(); + + match device_to_pcs { + LutWarehouse::Lut(lut_data_type) => { + lut = create_lut3x3(lut_data_type, &lut, options, source.pcs)?; + } + LutWarehouse::Multidimensional(mab) => { + prepare_mab_3x3(mab, &mut lut, options, source.pcs)? + } + } + } else if source.is_matrix_shaper() { + lut = create_rgb_lin_lut::(source, options)?; + } else { + return Err(CmsError::UnsupportedProfileConnection); + } + + pcs_lab_v2_to_v4(source, &mut lut); + + if source.pcs == DataColorSpace::Xyz && dest.pcs == DataColorSpace::Lab { + let xyz_to_lab = StageXyzToLab::default(); + xyz_to_lab.transform(&mut lut)?; + } else if source.pcs == DataColorSpace::Lab && dest.pcs == DataColorSpace::Xyz { + let lab_to_xyz_stage = StageLabToXyz::default(); + lab_to_xyz_stage.transform(&mut lut)?; + } + + pcs_lab_v4_to_v2(dest, &mut lut); + + let lut = match dest + .get_pcs_to_device(options.rendering_intent) + .ok_or(CmsError::UnsupportedProfileConnection)? + { + LutWarehouse::Lut(lut_type) => create_lut3x4(lut_type, &lut, options, dest.pcs)?, + LutWarehouse::Multidimensional(m_curves) => { + prepare_mba_3x4(m_curves, &mut lut, options, dest.pcs)? + } + }; + + let is_dest_linear_profile = dest.color_space == DataColorSpace::Rgb + && dest.is_matrix_shaper() + && dest.is_linear_matrix_shaper(); + + Ok(make_transform_3x4::( + src_layout, + lut, + options, + dest.color_space, + is_dest_linear_profile, + )) + } else if (source.color_space.is_three_channels()) && (dest.color_space.is_three_channels()) { + source.color_space.check_layout(src_layout)?; + dest.color_space.check_layout(dst_layout)?; + + const GRID_SIZE: usize = 33; + + let is_katana_required_for_source = if source.is_matrix_shaper() { + false + } else { + source + .get_device_to_pcs(options.rendering_intent) + .ok_or(CmsError::UnsupportedLutRenderingIntent( + source.rendering_intent, + )) + .map(|x| x.is_katana_required())? + }; + + let is_katana_required_for_destination = + if source.is_matrix_shaper() || dest.pcs == DataColorSpace::Xyz { + false + } else if dest.pcs == DataColorSpace::Lab { + dest.get_pcs_to_device(options.rendering_intent) + .ok_or(CmsError::UnsupportedProfileConnection) + .map(|x| x.is_katana_required())? + } else { + return Err(CmsError::UnsupportedProfileConnection); + }; + + let mut stages: Vec> = Vec::new(); + + // Slow and accurate fallback if anything not acceptable is detected by curve analysis + if is_katana_required_for_source || is_katana_required_for_destination { + let source_stage: Box + Send + Sync> = + if source.is_matrix_shaper() { + let state = katana_create_rgb_lin_lut::( + src_layout, source, options, + )?; + stages.extend(state.stages); + state.initial_stage + } else { + match source.get_device_to_pcs(options.rendering_intent).ok_or( + CmsError::UnsupportedLutRenderingIntent(source.rendering_intent), + )? { + LutWarehouse::Lut(lut) => { + katana_input_stage_lut_3x3::(lut, options, source.pcs, BIT_DEPTH)? + } + LutWarehouse::Multidimensional(mab) => { + multi_dimensional_3x3_to_pcs::(mab, options, source.pcs, BIT_DEPTH)? + } + } + }; + + stages.push(katana_pcs_lab_v2_to_v4(source)); + if source.pcs == DataColorSpace::Lab { + stages.push(Box::new(KatanaStageLabToXyz::default())); + } + if dest.pcs == DataColorSpace::Lab { + stages.push(Box::new(KatanaStageXyzToLab::default())); + } + stages.push(katana_pcs_lab_v4_to_v2(dest)); + + let final_stage = if dest.has_pcs_to_device_lut() { + let pcs_to_device = dest + .get_pcs_to_device(options.rendering_intent) + .ok_or(CmsError::UnsupportedProfileConnection)?; + match pcs_to_device { + LutWarehouse::Lut(lut) => { + katana_output_stage_lut_3x3::(lut, options, dest.pcs, BIT_DEPTH)? + } + LutWarehouse::Multidimensional(mab) => { + multi_dimensional_3x3_to_device::(mab, options, dest.pcs, BIT_DEPTH)? + } + } + } else if dest.is_matrix_shaper() { + let state = katana_prepare_inverse_lut_rgb_xyz::( + dest, dst_layout, options, + )?; + stages.extend(state.stages); + state.final_stage + } else { + return Err(CmsError::UnsupportedProfileConnection); + }; + + let mut post_finalization: Vec + Send + Sync>> = + Vec::new(); + if let Some(stage) = + prepare_alpha_finalizer::(src_layout, source, dst_layout, dest, BIT_DEPTH) + { + post_finalization.push(stage); + } + + return Ok(Box::new(Katana:: { + initial_stage: source_stage, + final_stage, + stages, + post_finalization, + })); + } + + let mut lut: Vec; + + if source.has_device_to_pcs_lut() { + let device_to_pcs = source + .get_device_to_pcs(options.rendering_intent) + .ok_or(CmsError::UnsupportedProfileConnection)?; + lut = create_lut3_samples_norm::(); + + match device_to_pcs { + LutWarehouse::Lut(lut_data_type) => { + lut = create_lut3x3(lut_data_type, &lut, options, source.pcs)?; + } + LutWarehouse::Multidimensional(mab) => { + prepare_mab_3x3(mab, &mut lut, options, source.pcs)? + } + } + } else if source.is_matrix_shaper() { + lut = create_rgb_lin_lut::(source, options)?; + } else { + return Err(CmsError::UnsupportedProfileConnection); + } + + pcs_lab_v2_to_v4(source, &mut lut); + + if source.pcs == DataColorSpace::Xyz && dest.pcs == DataColorSpace::Lab { + let xyz_to_lab = StageXyzToLab::default(); + xyz_to_lab.transform(&mut lut)?; + } else if source.pcs == DataColorSpace::Lab && dest.pcs == DataColorSpace::Xyz { + let lab_to_xyz_stage = StageLabToXyz::default(); + lab_to_xyz_stage.transform(&mut lut)?; + } + + pcs_lab_v4_to_v2(dest, &mut lut); + + if dest.has_pcs_to_device_lut() { + let pcs_to_device = dest + .get_pcs_to_device(options.rendering_intent) + .ok_or(CmsError::UnsupportedProfileConnection)?; + match pcs_to_device { + LutWarehouse::Lut(lut_data_type) => { + lut = create_lut3x3(lut_data_type, &lut, options, dest.pcs)?; + } + LutWarehouse::Multidimensional(mab) => { + prepare_mba_3x3(mab, &mut lut, options, dest.pcs)? + } + } + } else if dest.is_matrix_shaper() { + prepare_inverse_lut_rgb_xyz::(dest, &mut lut, options)?; + } else { + return Err(CmsError::UnsupportedProfileConnection); + } + + let is_dest_linear_profile = dest.color_space == DataColorSpace::Rgb + && dest.is_matrix_shaper() + && dest.is_linear_matrix_shaper(); + + #[cfg(all(feature = "avx", target_arch = "x86_64"))] + if std::arch::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") { + return Ok(make_transformer_3x3_avx_fma::( + src_layout, + dst_layout, + lut, + options, + dest.color_space, + is_dest_linear_profile, + )); + } + #[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] + if std::arch::is_x86_feature_detected!("sse4.1") { + return Ok(make_transformer_3x3_sse41::( + src_layout, + dst_layout, + lut, + options, + dest.color_space, + is_dest_linear_profile, + )); + } + + Ok(make_transformer_3x3::( + src_layout, + dst_layout, + lut, + options, + dest.color_space, + is_dest_linear_profile, + )) + } else { + do_any_to_any::( + src_layout, source, dst_layout, dest, options, + ) + } +} diff --git a/deps/moxcms/src/conversions/mab.rs b/deps/moxcms/src/conversions/mab.rs new file mode 100644 index 0000000..ec579d0 --- /dev/null +++ b/deps/moxcms/src/conversions/mab.rs @@ -0,0 +1,559 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::mlaf::mlaf; +use crate::safe_math::SafeMul; +use crate::{ + CmsError, Cube, DataColorSpace, InPlaceStage, InterpolationMethod, LutMultidimensionalType, + MalformedSize, Matrix3d, Matrix3f, TransformOptions, Vector3d, Vector3f, +}; + +#[allow(unused)] +struct ACurves3<'a> { + curve0: Box<[f32; 65536]>, + curve1: Box<[f32; 65536]>, + curve2: Box<[f32; 65536]>, + clut: &'a [f32], + grid_size: [u8; 3], + interpolation_method: InterpolationMethod, + pcs: DataColorSpace, + depth: usize, +} + +#[allow(unused)] +struct ACurves3Optimized<'a> { + clut: &'a [f32], + grid_size: [u8; 3], + interpolation_method: InterpolationMethod, + pcs: DataColorSpace, +} + +#[allow(unused)] +impl ACurves3<'_> { + fn transform_impl Vector3f>( + &self, + dst: &mut [f32], + fetch: Fetch, + ) -> Result<(), CmsError> { + let scale_value = (self.depth - 1) as f32; + + for dst in dst.chunks_exact_mut(3) { + let a0 = (dst[0] * scale_value).round().min(scale_value) as u16; + let a1 = (dst[1] * scale_value).round().min(scale_value) as u16; + let a2 = (dst[2] * scale_value).round().min(scale_value) as u16; + let b0 = self.curve0[a0 as usize]; + let b1 = self.curve1[a1 as usize]; + let b2 = self.curve2[a2 as usize]; + let interpolated = fetch(b0, b1, b2); + dst[0] = interpolated.v[0]; + dst[1] = interpolated.v[1]; + dst[2] = interpolated.v[2]; + } + Ok(()) + } +} + +#[allow(unused)] +impl ACurves3Optimized<'_> { + fn transform_impl Vector3f>( + &self, + dst: &mut [f32], + fetch: Fetch, + ) -> Result<(), CmsError> { + for dst in dst.chunks_exact_mut(3) { + let a0 = dst[0]; + let a1 = dst[1]; + let a2 = dst[2]; + let interpolated = fetch(a0, a1, a2); + dst[0] = interpolated.v[0]; + dst[1] = interpolated.v[1]; + dst[2] = interpolated.v[2]; + } + Ok(()) + } +} + +impl InPlaceStage for ACurves3<'_> { + fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> { + let lut = Cube::new_cube(self.clut, self.grid_size); + + // If PCS is LAB then linear interpolation should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + return self.transform_impl(dst, |x, y, z| lut.trilinear_vec3(x, y, z)); + } + + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.transform_impl(dst, |x, y, z| lut.tetra_vec3(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.transform_impl(dst, |x, y, z| lut.pyramid_vec3(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.transform_impl(dst, |x, y, z| lut.prism_vec3(x, y, z))?; + } + InterpolationMethod::Linear => { + self.transform_impl(dst, |x, y, z| lut.trilinear_vec3(x, y, z))?; + } + } + Ok(()) + } +} + +impl InPlaceStage for ACurves3Optimized<'_> { + fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> { + let lut = Cube::new_cube(self.clut, self.grid_size); + + // If PCS is LAB then linear interpolation should be used + if self.pcs == DataColorSpace::Lab { + return self.transform_impl(dst, |x, y, z| lut.trilinear_vec3(x, y, z)); + } + + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.transform_impl(dst, |x, y, z| lut.tetra_vec3(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.transform_impl(dst, |x, y, z| lut.pyramid_vec3(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.transform_impl(dst, |x, y, z| lut.prism_vec3(x, y, z))?; + } + InterpolationMethod::Linear => { + self.transform_impl(dst, |x, y, z| lut.trilinear_vec3(x, y, z))?; + } + } + Ok(()) + } +} + +#[allow(unused)] +struct ACurves3Inverse<'a> { + curve0: Box<[f32; 65536]>, + curve1: Box<[f32; 65536]>, + curve2: Box<[f32; 65536]>, + clut: &'a [f32], + grid_size: [u8; 3], + interpolation_method: InterpolationMethod, + pcs: DataColorSpace, + depth: usize, +} + +#[allow(unused)] +impl ACurves3Inverse<'_> { + fn transform_impl Vector3f>( + &self, + dst: &mut [f32], + fetch: Fetch, + ) -> Result<(), CmsError> { + let scale_value = (self.depth as u32 - 1u32) as f32; + + for dst in dst.chunks_exact_mut(3) { + let interpolated = fetch(dst[0], dst[1], dst[2]); + let a0 = (interpolated.v[0] * scale_value).round().min(scale_value) as u16; + let a1 = (interpolated.v[1] * scale_value).round().min(scale_value) as u16; + let a2 = (interpolated.v[2] * scale_value).round().min(scale_value) as u16; + let b0 = self.curve0[a0 as usize]; + let b1 = self.curve1[a1 as usize]; + let b2 = self.curve2[a2 as usize]; + dst[0] = b0; + dst[1] = b1; + dst[2] = b2; + } + Ok(()) + } +} + +impl InPlaceStage for ACurves3Inverse<'_> { + fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> { + let lut = Cube::new_cube(self.clut, self.grid_size); + + // If PCS is LAB then linear interpolation should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + return self.transform_impl(dst, |x, y, z| lut.trilinear_vec3(x, y, z)); + } + + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.transform_impl(dst, |x, y, z| lut.tetra_vec3(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.transform_impl(dst, |x, y, z| lut.pyramid_vec3(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.transform_impl(dst, |x, y, z| lut.prism_vec3(x, y, z))?; + } + InterpolationMethod::Linear => { + self.transform_impl(dst, |x, y, z| lut.trilinear_vec3(x, y, z))?; + } + } + Ok(()) + } +} + +pub(crate) struct MCurves3 { + pub(crate) curve0: Box<[f32; 65536]>, + pub(crate) curve1: Box<[f32; 65536]>, + pub(crate) curve2: Box<[f32; 65536]>, + pub(crate) matrix: Matrix3f, + pub(crate) bias: Vector3f, + pub(crate) inverse: bool, + pub(crate) depth: usize, +} + +impl MCurves3 { + fn execute_matrix_stage(&self, dst: &mut [f32]) { + let m = self.matrix; + let b = self.bias; + + if !m.test_equality(Matrix3f::IDENTITY) || !b.eq(&Vector3f::default()) { + for dst in dst.chunks_exact_mut(3) { + let x = dst[0]; + let y = dst[1]; + let z = dst[2]; + dst[0] = mlaf(mlaf(mlaf(b.v[0], x, m.v[0][0]), y, m.v[0][1]), z, m.v[0][2]); + dst[1] = mlaf(mlaf(mlaf(b.v[1], x, m.v[1][0]), y, m.v[1][1]), z, m.v[1][2]); + dst[2] = mlaf(mlaf(mlaf(b.v[2], x, m.v[2][0]), y, m.v[2][1]), z, m.v[2][2]); + } + } + } +} + +impl InPlaceStage for MCurves3 { + fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> { + let scale_value = (self.depth - 1) as f32; + + if self.inverse { + self.execute_matrix_stage(dst); + } + + for dst in dst.chunks_exact_mut(3) { + let a0 = (dst[0] * scale_value).round().min(scale_value) as u16; + let a1 = (dst[1] * scale_value).round().min(scale_value) as u16; + let a2 = (dst[2] * scale_value).round().min(scale_value) as u16; + let b0 = self.curve0[a0 as usize]; + let b1 = self.curve1[a1 as usize]; + let b2 = self.curve2[a2 as usize]; + dst[0] = b0; + dst[1] = b1; + dst[2] = b2; + } + + if !self.inverse { + self.execute_matrix_stage(dst); + } + + Ok(()) + } +} + +pub(crate) struct BCurves3 { + pub(crate) curve0: Box<[f32; 65536]>, + pub(crate) curve1: Box<[f32; 65536]>, + pub(crate) curve2: Box<[f32; 65536]>, +} + +impl InPlaceStage for BCurves3 { + fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> { + let scale_value = (DEPTH - 1) as f32; + + for dst in dst.chunks_exact_mut(3) { + let a0 = (dst[0] * scale_value).round().min(scale_value) as u16; + let a1 = (dst[1] * scale_value).round().min(scale_value) as u16; + let a2 = (dst[2] * scale_value).round().min(scale_value) as u16; + let b0 = self.curve0[a0 as usize]; + let b1 = self.curve1[a1 as usize]; + let b2 = self.curve2[a2 as usize]; + dst[0] = b0; + dst[1] = b1; + dst[2] = b2; + } + + Ok(()) + } +} + +pub(crate) fn prepare_mab_3x3( + mab: &LutMultidimensionalType, + lut: &mut [f32], + options: TransformOptions, + pcs: DataColorSpace, +) -> Result<(), CmsError> { + const LERP_DEPTH: usize = 65536; + const BP: usize = 13; + const DEPTH: usize = 8192; + + if mab.num_input_channels != 3 && mab.num_output_channels != 3 { + return Err(CmsError::UnsupportedProfileConnection); + } + if mab.a_curves.len() == 3 && mab.clut.is_some() { + let clut = &mab.clut.as_ref().map(|x| x.to_clut_f32()).unwrap(); + let lut_grid = (mab.grid_points[0] as usize) + .safe_mul(mab.grid_points[1] as usize)? + .safe_mul(mab.grid_points[2] as usize)? + .safe_mul(mab.num_output_channels as usize)?; + if clut.len() != lut_grid { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: clut.len(), + expected: lut_grid, + })); + } + + let all_curves_linear = mab.a_curves.iter().all(|curve| curve.is_linear()); + let grid_size = [mab.grid_points[0], mab.grid_points[1], mab.grid_points[2]]; + + if all_curves_linear { + let l = ACurves3Optimized { + clut, + grid_size, + interpolation_method: options.interpolation_method, + pcs, + }; + l.transform(lut)?; + } else { + let curves: Result, _> = mab + .a_curves + .iter() + .map(|c| { + c.build_linearize_table::() + .ok_or(CmsError::InvalidTrcCurve) + }) + .collect(); + + let [curve0, curve1, curve2] = + curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?; + let l = ACurves3 { + curve0, + curve1, + curve2, + clut, + grid_size, + interpolation_method: options.interpolation_method, + pcs, + depth: DEPTH, + }; + l.transform(lut)?; + } + } + + if mab.m_curves.len() == 3 { + let all_curves_linear = mab.m_curves.iter().all(|curve| curve.is_linear()); + if !all_curves_linear + || !mab.matrix.test_equality(Matrix3d::IDENTITY) + || mab.bias.ne(&Vector3d::default()) + { + let curves: Result, _> = mab + .m_curves + .iter() + .map(|c| { + c.build_linearize_table::() + .ok_or(CmsError::InvalidTrcCurve) + }) + .collect(); + + let [curve0, curve1, curve2] = + curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?; + let matrix = mab.matrix.to_f32(); + let bias: Vector3f = mab.bias.cast(); + let m_curves = MCurves3 { + curve0, + curve1, + curve2, + matrix, + bias, + inverse: false, + depth: DEPTH, + }; + m_curves.transform(lut)?; + } + } + + if mab.b_curves.len() == 3 { + const LERP_DEPTH: usize = 65536; + const BP: usize = 13; + const DEPTH: usize = 8192; + let all_curves_linear = mab.b_curves.iter().all(|curve| curve.is_linear()); + if !all_curves_linear { + let curves: Result, _> = mab + .b_curves + .iter() + .map(|c| { + c.build_linearize_table::() + .ok_or(CmsError::InvalidTrcCurve) + }) + .collect(); + + let [curve0, curve1, curve2] = + curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?; + + let b_curves = BCurves3:: { + curve0, + curve1, + curve2, + }; + b_curves.transform(lut)?; + } + } else { + return Err(CmsError::InvalidAtoBLut); + } + + Ok(()) +} + +pub(crate) fn prepare_mba_3x3( + mab: &LutMultidimensionalType, + lut: &mut [f32], + options: TransformOptions, + pcs: DataColorSpace, +) -> Result<(), CmsError> { + if mab.num_input_channels != 3 && mab.num_output_channels != 3 { + return Err(CmsError::UnsupportedProfileConnection); + } + const LERP_DEPTH: usize = 65536; + const BP: usize = 13; + const DEPTH: usize = 8192; + + if mab.b_curves.len() == 3 { + let all_curves_linear = mab.b_curves.iter().all(|curve| curve.is_linear()); + if !all_curves_linear { + let curves: Result, _> = mab + .b_curves + .iter() + .map(|c| { + c.build_linearize_table::() + .ok_or(CmsError::InvalidTrcCurve) + }) + .collect(); + + let [curve0, curve1, curve2] = + curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?; + let b_curves = BCurves3:: { + curve0, + curve1, + curve2, + }; + b_curves.transform(lut)?; + } + } else { + return Err(CmsError::InvalidAtoBLut); + } + + if mab.m_curves.len() == 3 { + let all_curves_linear = mab.m_curves.iter().all(|curve| curve.is_linear()); + if !all_curves_linear + || !mab.matrix.test_equality(Matrix3d::IDENTITY) + || mab.bias.ne(&Vector3d::default()) + { + let curves: Result, _> = mab + .m_curves + .iter() + .map(|c| { + c.build_linearize_table::() + .ok_or(CmsError::InvalidTrcCurve) + }) + .collect(); + + let [curve0, curve1, curve2] = + curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?; + + let matrix = mab.matrix.to_f32(); + let bias: Vector3f = mab.bias.cast(); + let m_curves = MCurves3 { + curve0, + curve1, + curve2, + matrix, + bias, + inverse: true, + depth: DEPTH, + }; + m_curves.transform(lut)?; + } + } + + if mab.a_curves.len() == 3 && mab.clut.is_some() { + let clut = &mab.clut.as_ref().map(|x| x.to_clut_f32()).unwrap(); + let lut_grid = (mab.grid_points[0] as usize) + .safe_mul(mab.grid_points[1] as usize)? + .safe_mul(mab.grid_points[2] as usize)? + .safe_mul(mab.num_output_channels as usize)?; + if clut.len() != lut_grid { + return Err(CmsError::MalformedCurveLutTable(MalformedSize { + size: clut.len(), + expected: lut_grid, + })); + } + + let all_curves_linear = mab.a_curves.iter().all(|curve| curve.is_linear()); + let grid_size = [mab.grid_points[0], mab.grid_points[1], mab.grid_points[2]]; + + if all_curves_linear { + let l = ACurves3Optimized { + clut, + grid_size, + interpolation_method: options.interpolation_method, + pcs, + }; + l.transform(lut)?; + } else { + let curves: Result, _> = mab + .a_curves + .iter() + .map(|c| { + c.build_linearize_table::() + .ok_or(CmsError::InvalidTrcCurve) + }) + .collect(); + + let [curve0, curve1, curve2] = + curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?; + let l = ACurves3Inverse { + curve0, + curve1, + curve2, + clut, + grid_size, + interpolation_method: options.interpolation_method, + pcs, + depth: DEPTH, + }; + l.transform(lut)?; + } + } + + Ok(()) +} diff --git a/deps/moxcms/src/conversions/mab4x3.rs b/deps/moxcms/src/conversions/mab4x3.rs new file mode 100644 index 0000000..d5f8d1f --- /dev/null +++ b/deps/moxcms/src/conversions/mab4x3.rs @@ -0,0 +1,307 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::mab::{BCurves3, MCurves3}; +use crate::err::try_vec; +use crate::safe_math::SafeMul; +use crate::{ + CmsError, DataColorSpace, Hypercube, InPlaceStage, InterpolationMethod, + LutMultidimensionalType, MalformedSize, Matrix3d, Stage, TransformOptions, Vector3d, Vector3f, +}; + +#[allow(dead_code)] +struct ACurves4x3<'a> { + curve0: Box<[f32; 65536]>, + curve1: Box<[f32; 65536]>, + curve2: Box<[f32; 65536]>, + curve3: Box<[f32; 65536]>, + clut: &'a [f32], + grid_size: [u8; 4], + interpolation_method: InterpolationMethod, + pcs: DataColorSpace, + depth: usize, +} + +#[allow(dead_code)] +struct ACurves4x3Optimized<'a> { + clut: &'a [f32], + grid_size: [u8; 4], + interpolation_method: InterpolationMethod, + pcs: DataColorSpace, +} + +#[allow(dead_code)] +impl ACurves4x3<'_> { + fn transform_impl Vector3f>( + &self, + src: &[f32], + dst: &mut [f32], + fetch: Fetch, + ) -> Result<(), CmsError> { + let scale_value = (self.depth - 1) as f32; + + assert_eq!(src.len() / 4, dst.len() / 3); + + for (src, dst) in src.chunks_exact(4).zip(dst.chunks_exact_mut(3)) { + let a0 = (src[0] * scale_value).round().min(scale_value) as u16; + let a1 = (src[1] * scale_value).round().min(scale_value) as u16; + let a2 = (src[2] * scale_value).round().min(scale_value) as u16; + let a3 = (src[3] * scale_value).round().min(scale_value) as u16; + let c = self.curve0[a0 as usize]; + let m = self.curve1[a1 as usize]; + let y = self.curve2[a2 as usize]; + let k = self.curve3[a3 as usize]; + + let r = fetch(c, m, y, k); + dst[0] = r.v[0]; + dst[1] = r.v[1]; + dst[2] = r.v[2]; + } + Ok(()) + } +} + +#[allow(dead_code)] +impl ACurves4x3Optimized<'_> { + fn transform_impl Vector3f>( + &self, + src: &[f32], + dst: &mut [f32], + fetch: Fetch, + ) -> Result<(), CmsError> { + assert_eq!(src.len() / 4, dst.len() / 3); + + for (src, dst) in src.chunks_exact(4).zip(dst.chunks_exact_mut(3)) { + let c = src[0]; + let m = src[1]; + let y = src[2]; + let k = src[3]; + + let r = fetch(c, m, y, k); + dst[0] = r.v[0]; + dst[1] = r.v[1]; + dst[2] = r.v[2]; + } + Ok(()) + } +} + +impl Stage for ACurves4x3<'_> { + fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError> { + let lut = Hypercube::new_hypercube(self.clut, self.grid_size); + + // If PCS is LAB then linear interpolation should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + return self.transform_impl(src, dst, |x, y, z, w| lut.quadlinear_vec3(x, y, z, w)); + } + + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.transform_impl(src, dst, |x, y, z, w| lut.tetra_vec3(x, y, z, w))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.transform_impl(src, dst, |x, y, z, w| lut.pyramid_vec3(x, y, z, w))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.transform_impl(src, dst, |x, y, z, w| lut.prism_vec3(x, y, z, w))?; + } + InterpolationMethod::Linear => { + self.transform_impl(src, dst, |x, y, z, w| lut.quadlinear_vec3(x, y, z, w))?; + } + } + Ok(()) + } +} + +impl Stage for ACurves4x3Optimized<'_> { + fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError> { + let lut = Hypercube::new_hypercube(self.clut, self.grid_size); + + // If PCS is LAB then linear interpolation should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + return self.transform_impl(src, dst, |x, y, z, w| lut.quadlinear_vec3(x, y, z, w)); + } + + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.transform_impl(src, dst, |x, y, z, w| lut.tetra_vec3(x, y, z, w))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.transform_impl(src, dst, |x, y, z, w| lut.pyramid_vec3(x, y, z, w))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.transform_impl(src, dst, |x, y, z, w| lut.prism_vec3(x, y, z, w))?; + } + InterpolationMethod::Linear => { + self.transform_impl(src, dst, |x, y, z, w| lut.quadlinear_vec3(x, y, z, w))?; + } + } + Ok(()) + } +} + +pub(crate) fn prepare_mab_4x3( + mab: &LutMultidimensionalType, + lut: &mut [f32], + options: TransformOptions, + pcs: DataColorSpace, +) -> Result, CmsError> { + const LERP_DEPTH: usize = 65536; + const BP: usize = 13; + const DEPTH: usize = 8192; + if mab.num_input_channels != 4 && mab.num_output_channels != 3 { + return Err(CmsError::UnsupportedProfileConnection); + } + let mut new_lut = try_vec![0f32; (lut.len() / 4) * 3]; + if mab.a_curves.len() == 4 && mab.clut.is_some() { + let clut = &mab.clut.as_ref().map(|x| x.to_clut_f32()).unwrap(); + + let lut_grid = (mab.grid_points[0] as usize) + .safe_mul(mab.grid_points[1] as usize)? + .safe_mul(mab.grid_points[2] as usize)? + .safe_mul(mab.grid_points[3] as usize)? + .safe_mul(mab.num_output_channels as usize)?; + if clut.len() != lut_grid { + return Err(CmsError::MalformedClut(MalformedSize { + size: clut.len(), + expected: lut_grid, + })); + } + + let all_curves_linear = mab.a_curves.iter().all(|curve| curve.is_linear()); + let grid_size = [ + mab.grid_points[0], + mab.grid_points[1], + mab.grid_points[2], + mab.grid_points[3], + ]; + + if all_curves_linear { + let l = ACurves4x3Optimized { + clut, + grid_size, + interpolation_method: options.interpolation_method, + pcs, + }; + l.transform(lut, &mut new_lut)?; + } else { + let curves: Result, _> = mab + .a_curves + .iter() + .map(|c| { + c.build_linearize_table::() + .ok_or(CmsError::InvalidTrcCurve) + }) + .collect(); + + let [curve0, curve1, curve2, curve3] = + curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?; + let l = ACurves4x3 { + curve0, + curve1, + curve2, + curve3, + clut, + grid_size, + interpolation_method: options.interpolation_method, + pcs, + depth: DEPTH, + }; + l.transform(lut, &mut new_lut)?; + } + } else { + // Not supported + return Err(CmsError::UnsupportedProfileConnection); + } + + if mab.m_curves.len() == 3 { + let all_curves_linear = mab.m_curves.iter().all(|curve| curve.is_linear()); + if !all_curves_linear + || !mab.matrix.test_equality(Matrix3d::IDENTITY) + || mab.bias.ne(&Vector3d::default()) + { + let curves: Result, _> = mab + .m_curves + .iter() + .map(|c| { + c.build_linearize_table::() + .ok_or(CmsError::InvalidTrcCurve) + }) + .collect(); + + let [curve0, curve1, curve2] = + curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?; + + let matrix = mab.matrix.to_f32(); + let bias: Vector3f = mab.bias.cast(); + let m_curves = MCurves3 { + curve0, + curve1, + curve2, + matrix, + bias, + inverse: false, + depth: DEPTH, + }; + m_curves.transform(&mut new_lut)?; + } + } + + if mab.b_curves.len() == 3 { + let all_curves_linear = mab.b_curves.iter().all(|curve| curve.is_linear()); + if !all_curves_linear { + let curves: Result, _> = mab + .b_curves + .iter() + .map(|c| { + c.build_linearize_table::() + .ok_or(CmsError::InvalidTrcCurve) + }) + .collect(); + + let [curve0, curve1, curve2] = + curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?; + let b_curves = BCurves3:: { + curve0, + curve1, + curve2, + }; + b_curves.transform(&mut new_lut)?; + } + } else { + return Err(CmsError::InvalidAtoBLut); + } + + Ok(new_lut) +} diff --git a/deps/moxcms/src/conversions/mba3x4.rs b/deps/moxcms/src/conversions/mba3x4.rs new file mode 100644 index 0000000..3b1bf2c --- /dev/null +++ b/deps/moxcms/src/conversions/mba3x4.rs @@ -0,0 +1,302 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::mab::{BCurves3, MCurves3}; +use crate::err::try_vec; +use crate::safe_math::SafeMul; +use crate::{ + CmsError, Cube, DataColorSpace, InPlaceStage, InterpolationMethod, LutMultidimensionalType, + MalformedSize, Matrix3d, Stage, TransformOptions, Vector3d, Vector4f, +}; + +struct ACurves3x4Inverse<'a> { + curve0: Box<[f32; 65536]>, + curve1: Box<[f32; 65536]>, + curve2: Box<[f32; 65536]>, + curve3: Box<[f32; 65536]>, + clut: &'a [f32], + grid_size: [u8; 3], + interpolation_method: InterpolationMethod, + pcs: DataColorSpace, + depth: usize, +} + +struct ACurves3x4InverseOptimized<'a> { + clut: &'a [f32], + grid_size: [u8; 3], + interpolation_method: InterpolationMethod, + pcs: DataColorSpace, +} + +impl ACurves3x4Inverse<'_> { + fn transform_impl Vector4f>( + &self, + src: &[f32], + dst: &mut [f32], + fetch: Fetch, + ) -> Result<(), CmsError> { + let scale_value = (self.depth as u32 - 1u32) as f32; + + assert_eq!(src.len() / 3, dst.len() / 4); + + for (src, dst) in src.chunks_exact(3).zip(dst.chunks_exact_mut(4)) { + let interpolated = fetch(src[0], src[1], src[2]); + let a0 = (interpolated.v[0] * scale_value).round().min(scale_value) as u16; + let a1 = (interpolated.v[1] * scale_value).round().min(scale_value) as u16; + let a2 = (interpolated.v[2] * scale_value).round().min(scale_value) as u16; + let a3 = (interpolated.v[3] * scale_value).round().min(scale_value) as u16; + let b0 = self.curve0[a0 as usize]; + let b1 = self.curve1[a1 as usize]; + let b2 = self.curve2[a2 as usize]; + let b3 = self.curve3[a3 as usize]; + dst[0] = b0; + dst[1] = b1; + dst[2] = b2; + dst[3] = b3; + } + Ok(()) + } +} + +impl ACurves3x4InverseOptimized<'_> { + fn transform_impl Vector4f>( + &self, + src: &[f32], + dst: &mut [f32], + fetch: Fetch, + ) -> Result<(), CmsError> { + assert_eq!(src.len() / 3, dst.len() / 4); + + for (src, dst) in src.chunks_exact(3).zip(dst.chunks_exact_mut(4)) { + let interpolated = fetch(src[0], src[1], src[2]); + let b0 = interpolated.v[0]; + let b1 = interpolated.v[1]; + let b2 = interpolated.v[2]; + let b3 = interpolated.v[3]; + dst[0] = b0; + dst[1] = b1; + dst[2] = b2; + dst[3] = b3; + } + Ok(()) + } +} + +impl Stage for ACurves3x4Inverse<'_> { + fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError> { + let lut = Cube::new_cube(self.clut, self.grid_size); + + // If PCS is LAB then linear interpolation should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + return self.transform_impl(src, dst, |x, y, z| lut.trilinear_vec4(x, y, z)); + } + + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.transform_impl(src, dst, |x, y, z| lut.tetra_vec4(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.transform_impl(src, dst, |x, y, z| lut.pyramid_vec4(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.transform_impl(src, dst, |x, y, z| lut.prism_vec4(x, y, z))?; + } + InterpolationMethod::Linear => { + self.transform_impl(src, dst, |x, y, z| lut.trilinear_vec4(x, y, z))?; + } + } + Ok(()) + } +} + +impl Stage for ACurves3x4InverseOptimized<'_> { + fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError> { + let lut = Cube::new_cube(self.clut, self.grid_size); + + // If PCS is LAB then linear interpolation should be used + if self.pcs == DataColorSpace::Lab || self.pcs == DataColorSpace::Xyz { + return self.transform_impl(src, dst, |x, y, z| lut.trilinear_vec4(x, y, z)); + } + + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + self.transform_impl(src, dst, |x, y, z| lut.tetra_vec4(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + self.transform_impl(src, dst, |x, y, z| lut.pyramid_vec4(x, y, z))?; + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + self.transform_impl(src, dst, |x, y, z| lut.prism_vec4(x, y, z))?; + } + InterpolationMethod::Linear => { + self.transform_impl(src, dst, |x, y, z| lut.trilinear_vec4(x, y, z))?; + } + } + Ok(()) + } +} + +pub(crate) fn prepare_mba_3x4( + mab: &LutMultidimensionalType, + lut: &mut [f32], + options: TransformOptions, + pcs: DataColorSpace, +) -> Result, CmsError> { + if mab.num_input_channels != 3 && mab.num_output_channels != 4 { + return Err(CmsError::UnsupportedProfileConnection); + } + + const LERP_DEPTH: usize = 65536; + const BP: usize = 13; + const DEPTH: usize = 8192; + + if mab.b_curves.len() == 3 { + let all_curves_linear = mab.b_curves.iter().all(|curve| curve.is_linear()); + + if !all_curves_linear { + let curves: Result, _> = mab + .b_curves + .iter() + .map(|c| { + c.build_linearize_table::() + .ok_or(CmsError::InvalidTrcCurve) + }) + .collect(); + + let [curve0, curve1, curve2] = + curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?; + let b_curves = BCurves3:: { + curve0, + curve1, + curve2, + }; + b_curves.transform(lut)?; + } + } else { + return Err(CmsError::InvalidAtoBLut); + } + + if mab.m_curves.len() == 3 { + let all_curves_linear = mab.m_curves.iter().all(|curve| curve.is_linear()); + if !all_curves_linear + || !mab.matrix.test_equality(Matrix3d::IDENTITY) + || mab.bias.ne(&Vector3d::default()) + { + let curves: Result, _> = mab + .m_curves + .iter() + .map(|c| { + c.build_linearize_table::() + .ok_or(CmsError::InvalidTrcCurve) + }) + .collect(); + + let [curve0, curve1, curve2] = + curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?; + + let matrix = mab.matrix.to_f32(); + let bias = mab.bias.cast(); + let m_curves = MCurves3 { + curve0, + curve1, + curve2, + matrix, + bias, + inverse: true, + depth: DEPTH, + }; + m_curves.transform(lut)?; + } + } + + let mut new_lut = try_vec![0f32; (lut.len() / 3) * 4]; + + if mab.a_curves.len() == 4 && mab.clut.is_some() { + let clut = &mab.clut.as_ref().map(|x| x.to_clut_f32()).unwrap(); + + let lut_grid = (mab.grid_points[0] as usize) + .safe_mul(mab.grid_points[1] as usize)? + .safe_mul(mab.grid_points[2] as usize)? + .safe_mul(mab.num_output_channels as usize)?; + if clut.len() != lut_grid { + return Err(CmsError::MalformedClut(MalformedSize { + size: clut.len(), + expected: lut_grid, + })); + } + + let grid_size = [mab.grid_points[0], mab.grid_points[1], mab.grid_points[2]]; + + let all_curves_linear = mab.a_curves.iter().all(|curve| curve.is_linear()); + + if all_curves_linear { + let a_curves = ACurves3x4InverseOptimized { + clut, + grid_size: [mab.grid_points[0], mab.grid_points[1], mab.grid_points[2]], + interpolation_method: options.interpolation_method, + pcs, + }; + a_curves.transform(lut, &mut new_lut)?; + } else { + let curves: Result, _> = mab + .a_curves + .iter() + .map(|c| { + c.build_linearize_table::() + .ok_or(CmsError::InvalidTrcCurve) + }) + .collect(); + + let [curve0, curve1, curve2, curve3] = + curves?.try_into().map_err(|_| CmsError::InvalidTrcCurve)?; + + let a_curves = ACurves3x4Inverse { + curve0, + curve1, + curve2, + curve3, + clut, + grid_size, + interpolation_method: options.interpolation_method, + depth: DEPTH, + pcs, + }; + a_curves.transform(lut, &mut new_lut)?; + } + } else { + return Err(CmsError::UnsupportedProfileConnection); + } + + Ok(new_lut) +} diff --git a/deps/moxcms/src/conversions/md_lut.rs b/deps/moxcms/src/conversions/md_lut.rs new file mode 100644 index 0000000..4feae6e --- /dev/null +++ b/deps/moxcms/src/conversions/md_lut.rs @@ -0,0 +1,728 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::math::{FusedMultiplyAdd, FusedMultiplyNegAdd}; +use crate::mlaf::{mlaf, neg_mlaf}; +use crate::nd_array::{ArrayFetch, lerp}; +use crate::{Vector3f, Vector3i}; +use num_traits::MulAdd; +use std::array::from_fn; +use std::marker::PhantomData; +use std::ops::{Add, Mul, Neg, Sub}; + +pub(crate) struct MultidimensionalLut { + pub(crate) grid_strides: [u32; 16], + pub(crate) grid_filling_size: [u32; 16], + pub(crate) grid_scale: [f32; 16], + pub(crate) output_inks: usize, +} + +struct FastCube> { + fetch: F, + _phantom: PhantomData, +} + +struct ArrayFetchVectorN<'a> { + array: &'a [f32], + x_stride: u32, + y_stride: u32, + z_stride: u32, + output_inks: usize, +} + +#[repr(transparent)] +#[derive(Copy, Clone, Debug)] +pub(crate) struct NVector { + pub(crate) v: [T; N], +} + +impl NVector { + pub(crate) fn from_slice(v: &[T; N]) -> Self { + Self { v: *v } + } +} + +impl From for NVector { + #[inline] + fn from(value: T) -> Self { + Self { v: [value; N] } + } +} + +impl + Mul + MulAdd, const N: usize> + FusedMultiplyAdd> for NVector +{ + #[inline] + fn mla(&self, b: NVector, c: NVector) -> NVector { + Self { + v: from_fn(|i| mlaf(self.v[i], b.v[i], c.v[i])), + } + } +} + +impl< + T: Copy + Add + Mul + MulAdd + Neg, + const N: usize, +> FusedMultiplyNegAdd> for NVector +{ + #[inline] + fn neg_mla(&self, b: NVector, c: NVector) -> NVector { + Self { + v: from_fn(|i| neg_mlaf(self.v[i], b.v[i], c.v[i])), + } + } +} + +impl + Default + Copy, const N: usize> Sub> for NVector { + type Output = Self; + + #[inline] + fn sub(self, rhs: NVector) -> Self::Output { + Self { + v: from_fn(|i| self.v[i] - rhs.v[i]), + } + } +} + +impl + Default + Copy, const N: usize> Add> for NVector { + type Output = Self; + + #[inline] + fn add(self, rhs: NVector) -> Self::Output { + Self { + v: from_fn(|i| self.v[i] + rhs.v[i]), + } + } +} + +impl + Default + Copy, const N: usize> Mul> for NVector { + type Output = Self; + + #[inline] + fn mul(self, rhs: NVector) -> Self::Output { + Self { + v: from_fn(|i| self.v[i] * rhs.v[i]), + } + } +} + +impl ArrayFetch> for ArrayFetchVectorN<'_> { + #[inline(always)] + fn fetch(&self, x: i32, y: i32, z: i32) -> NVector { + let start = (x as u32 * self.x_stride + y as u32 * self.y_stride + z as u32 * self.z_stride) + as usize + * self.output_inks; + let k = &self.array[start..start + N]; + NVector::::from_slice(k.try_into().unwrap()) + } +} + +impl> FastCube +where + T: Copy + + From + + Sub + + Mul + + Add + + FusedMultiplyNegAdd + + FusedMultiplyAdd, +{ + #[inline(never)] + fn tetra(&self, src: Vector3i, src_next: Vector3i, w: Vector3f) -> T { + let x = src.v[0]; + let y = src.v[1]; + let z = src.v[2]; + + let x_n = src_next.v[0]; + let y_n = src_next.v[1]; + let z_n = src_next.v[2]; + + let rx = w.v[0]; + let ry = w.v[1]; + let rz = w.v[2]; + + let c0 = self.fetch.fetch(x, y, z); + let c2; + let c1; + let c3; + if rx >= ry { + if ry >= rz { + //rx >= ry && ry >= rz + c1 = self.fetch.fetch(x_n, y, z) - c0; + c2 = self.fetch.fetch(x_n, y_n, z) - self.fetch.fetch(x_n, y, z); + c3 = self.fetch.fetch(x_n, y_n, z_n) - self.fetch.fetch(x_n, y_n, z); + } else if rx >= rz { + //rx >= rz && rz >= ry + c1 = self.fetch.fetch(x_n, y, z) - c0; + c2 = self.fetch.fetch(x_n, y_n, z_n) - self.fetch.fetch(x_n, y, z_n); + c3 = self.fetch.fetch(x_n, y, z_n) - self.fetch.fetch(x_n, y, z); + } else { + //rz > rx && rx >= ry + c1 = self.fetch.fetch(x_n, y, z_n) - self.fetch.fetch(x, y, z_n); + c2 = self.fetch.fetch(x_n, y_n, z_n) - self.fetch.fetch(x_n, y, z_n); + c3 = self.fetch.fetch(x, y, z_n) - c0; + } + } else if rx >= rz { + //ry > rx && rx >= rz + c1 = self.fetch.fetch(x_n, y_n, z) - self.fetch.fetch(x, y_n, z); + c2 = self.fetch.fetch(x, y_n, z) - c0; + c3 = self.fetch.fetch(x_n, y_n, z_n) - self.fetch.fetch(x_n, y_n, z); + } else if ry >= rz { + //ry >= rz && rz > rx + c1 = self.fetch.fetch(x_n, y_n, z_n) - self.fetch.fetch(x, y_n, z_n); + c2 = self.fetch.fetch(x, y_n, z) - c0; + c3 = self.fetch.fetch(x, y_n, z_n) - self.fetch.fetch(x, y_n, z); + } else { + //rz > ry && ry > rx + c1 = self.fetch.fetch(x_n, y_n, z_n) - self.fetch.fetch(x, y_n, z_n); + c2 = self.fetch.fetch(x, y_n, z_n) - self.fetch.fetch(x, y, z_n); + c3 = self.fetch.fetch(x, y, z_n) - c0; + } + let s0 = c0.mla(c1, T::from(rx)); + let s1 = s0.mla(c2, T::from(ry)); + s1.mla(c3, T::from(rz)) + } +} + +impl MultidimensionalLut { + pub(crate) fn new(grid_size: [u8; 16], input_inks: usize, output_inks: usize) -> Self { + assert!(input_inks <= 16); + let mut grid_strides = [1u32; 16]; + let mut grid_filling_size = [1u32; 16]; + + for (ink, dst_stride) in grid_strides.iter_mut().take(input_inks - 1).enumerate() { + let mut stride = 1u32; + let how_many = input_inks.saturating_sub(ink).saturating_sub(1); + for &grid_stride in grid_size.iter().take(how_many) { + stride *= grid_stride as u32; + } + *dst_stride = stride; + } + + for (ink, dst_stride) in grid_filling_size.iter_mut().take(input_inks).enumerate() { + let mut stride = output_inks as u32; + let how_many = input_inks.saturating_sub(ink).saturating_sub(1); + for &grid_stride in grid_size.iter().take(how_many) { + stride *= grid_stride as u32; + } + *dst_stride = stride; + } + + let mut grid_strides_f = [0f32; 16]; + + for (dst, src) in grid_strides_f + .iter_mut() + .zip(grid_size.iter()) + .take(input_inks) + { + *dst = (*src - 1) as f32; + } + + Self { + grid_strides, + grid_scale: grid_strides_f, + grid_filling_size, + output_inks, + } + } +} + +pub(crate) fn linear_4i_vec3f_direct( + lut: &MultidimensionalLut, + arr: &[f32], + lx: f32, + ly: f32, + lz: f32, + lw: f32, +) -> NVector { + let lin_x = lx.max(0.0).min(1.0); + let lin_y = ly.max(0.0).min(1.0); + let lin_z = lz.max(0.0).min(1.0); + let lin_w = lw.max(0.0).min(1.0); + + let scale_x = lut.grid_scale[0]; + let scale_y = lut.grid_scale[1]; + let scale_z = lut.grid_scale[2]; + let scale_w = lut.grid_scale[3]; + + let lx = lin_x * scale_x; + let ly = lin_y * scale_y; + let lz = lin_z * scale_z; + let lw = lin_w * scale_w; + + let x = lx.floor() as i32; + let y = ly.floor() as i32; + let z = lz.floor() as i32; + let w = lw.floor() as i32; + + let src_x = Vector3i { v: [x, y, z] }; + + let x_n = lx.ceil() as i32; + let y_n = ly.ceil() as i32; + let z_n = lz.ceil() as i32; + let w_n = lw.ceil() as i32; + + let src_next = Vector3i { v: [x_n, y_n, z_n] }; + + let x_w = lx - x as f32; + let y_w = ly - y as f32; + let z_w = lz - z as f32; + let w_w = lw - w as f32; + + let weights = Vector3f { v: [x_w, y_w, z_w] }; + + let cube0 = &arr[(w as usize * lut.grid_filling_size[3] as usize)..]; + let cube1 = &arr[(w_n as usize * lut.grid_filling_size[3] as usize)..]; + + let fast_cube0 = FastCube { + fetch: ArrayFetchVectorN { + array: cube0, + x_stride: lut.grid_strides[0], + y_stride: lut.grid_strides[1], + z_stride: lut.grid_strides[2], + output_inks: lut.output_inks, + }, + _phantom: PhantomData, + }; + let fast_cube1 = FastCube { + fetch: ArrayFetchVectorN { + array: cube1, + x_stride: lut.grid_strides[0], + y_stride: lut.grid_strides[1], + z_stride: lut.grid_strides[2], + output_inks: lut.output_inks, + }, + _phantom: PhantomData, + }; + let w0 = fast_cube0.tetra(src_x, src_next, weights); + let w1 = fast_cube1.tetra(src_x, src_next, weights); + lerp(w0, w1, NVector::::from(w_w)) +} + +pub(crate) fn linear_3i_vec3f_direct( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + linear_3i_vec3f(lut, arr, inputs[0], inputs[1], inputs[2]) +} + +fn linear_3i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + x: f32, + y: f32, + z: f32, +) -> NVector { + let lin_x = x.max(0.0).min(1.0); + let lin_y = y.max(0.0).min(1.0); + let lin_z = z.max(0.0).min(1.0); + + let scale_x = lut.grid_scale[0]; + let scale_y = lut.grid_scale[1]; + let scale_z = lut.grid_scale[2]; + + let lx = lin_x * scale_x; + let ly = lin_y * scale_y; + let lz = lin_z * scale_z; + + let x = lx.floor() as i32; + let y = ly.floor() as i32; + let z = lz.floor() as i32; + + let src_x = Vector3i { v: [x, y, z] }; + + let x_n = lx.ceil() as i32; + let y_n = ly.ceil() as i32; + let z_n = lz.ceil() as i32; + + let src_next = Vector3i { v: [x_n, y_n, z_n] }; + + let x_w = lx - x as f32; + let y_w = ly - y as f32; + let z_w = lz - z as f32; + + let weights = Vector3f { v: [x_w, y_w, z_w] }; + + let fast_cube = FastCube { + fetch: ArrayFetchVectorN { + array: arr, + x_stride: lut.grid_strides[0], + y_stride: lut.grid_strides[1], + z_stride: lut.grid_strides[2], + output_inks: lut.output_inks, + }, + _phantom: PhantomData, + }; + + fast_cube.tetra(src_x, src_next, weights) +} + +pub(crate) fn linear_1i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + let lin_x = inputs[0].max(0.0).min(1.0); + + let scale_x = lut.grid_scale[0]; + + let lx = lin_x * scale_x; + + let x = lx.floor() as i32; + + let x_n = lx.ceil() as i32; + + let x_w = lx - x as f32; + + let x_stride = lut.grid_strides[0]; + + let offset = |xi: i32| -> usize { (xi as u32 * x_stride) as usize * lut.output_inks }; + + // Sample 2 corners + let a = NVector::::from_slice(&arr[offset(x)..][..N].try_into().unwrap()); + let b = NVector::::from_slice(&arr[offset(x_n)..][..N].try_into().unwrap()); + + a * NVector::::from(1.0 - x_w) + b * NVector::::from(x_w) +} + +pub(crate) fn linear_2i_vec3f_direct( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + linear_2i_vec3f(lut, arr, inputs[0], inputs[1]) +} + +fn linear_2i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + x: f32, + y: f32, +) -> NVector { + let lin_x = x.max(0.0).min(1.0); + let lin_y = y.max(0.0).min(1.0); + + let scale_x = lut.grid_scale[0]; + let scale_y = lut.grid_scale[1]; + + let lx = lin_x * scale_x; + let ly = lin_y * scale_y; + + let x = lx.floor() as i32; + let y = ly.floor() as i32; + + let x_n = lx.ceil() as i32; + let y_n = ly.ceil() as i32; + + let x_w = lx - x as f32; + let y_w = ly - y as f32; + + let x_stride = lut.grid_strides[0]; + let y_stride = lut.grid_strides[1]; + + let offset = |xi: i32, yi: i32| -> usize { + (xi as u32 * x_stride + yi as u32 * y_stride) as usize * lut.output_inks + }; + + // Sample 4 corners + let a = NVector::::from_slice(&arr[offset(x, y)..][..N].try_into().unwrap()); + let b = NVector::::from_slice(&arr[offset(x_n, y)..][..N].try_into().unwrap()); + let c = NVector::::from_slice(&arr[offset(x, y_n)..][..N].try_into().unwrap()); + let d = NVector::::from_slice(&arr[offset(x_n, y_n)..][..N].try_into().unwrap()); + + let ab = a * NVector::::from(1.0 - x_w) + b * NVector::::from(x_w); + let cd = c * NVector::::from(1.0 - x_w) + d * NVector::::from(x_w); + + ab * NVector::::from(1.0 - y_w) + cd * NVector::::from(y_w) +} + +pub(crate) fn linear_4i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + linear_4i_vec3f_direct(lut, arr, inputs[0], inputs[1], inputs[2], inputs[3]) +} + +type FHandle = fn(&MultidimensionalLut, &[f32], &[f32]) -> NVector; + +#[inline(never)] +pub(crate) fn linear_n_i_vec3f< + const N: usize, + const I: usize, + Handle: Fn(&MultidimensionalLut, &[f32], &[f32]) -> NVector, +>( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], + handle: Handle, +) -> NVector { + let lin_w = inputs[I]; + + let w_c = lin_w.max(0.).min(1.); + let scale_p = lut.grid_scale[I]; + let wf = w_c * scale_p; + let w0 = wf.min(scale_p) as usize; + let w1 = (wf + 1.).min(scale_p) as usize; + let w = wf - w0 as f32; + + let cube0 = &arr[(w0 * lut.grid_filling_size[I] as usize)..]; + let cube1 = &arr[(w1 * lut.grid_filling_size[I] as usize)..]; + + let inputs_sliced = &inputs[0..I]; + let w0 = handle(lut, cube0, inputs_sliced); + let w1 = handle(lut, cube1, inputs_sliced); + lerp(w0, w1, NVector::::from(w)) +} + +#[inline(never)] +pub(crate) fn linear_5i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + let lin_w = inputs[4]; + + let w_c = lin_w.max(0.).min(1.); + let scale_p = lut.grid_scale[4]; + let wf = w_c * scale_p; + let w0 = wf.min(scale_p) as usize; + let w1 = (wf + 1.).min(scale_p) as usize; + let w = wf - w0 as f32; + + let cube0 = &arr[(w0 * lut.grid_filling_size[4] as usize)..]; + let cube1 = &arr[(w1 * lut.grid_filling_size[4] as usize)..]; + + let w0 = linear_4i_vec3f_direct(lut, cube0, inputs[0], inputs[1], inputs[2], inputs[3]); + let w1 = linear_4i_vec3f_direct(lut, cube1, inputs[0], inputs[1], inputs[2], inputs[3]); + lerp(w0, w1, NVector::::from(w)) +} + +#[inline(never)] +pub(crate) fn linear_6i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + let f = linear_5i_vec3f::; + linear_n_i_vec3f::>(lut, arr, inputs, f) +} + +#[inline(never)] +pub(crate) fn linear_7i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + let f = linear_6i_vec3f::; + linear_n_i_vec3f::>(lut, arr, inputs, f) +} + +#[inline(never)] +pub(crate) fn linear_8i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + let f = linear_7i_vec3f::; + linear_n_i_vec3f::>(lut, arr, inputs, f) +} + +#[inline(never)] +pub(crate) fn linear_9i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + let f = linear_8i_vec3f::; + linear_n_i_vec3f::>(lut, arr, inputs, f) +} + +#[inline(never)] +pub(crate) fn linear_10i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + let f = linear_9i_vec3f::; + linear_n_i_vec3f::>(lut, arr, inputs, f) +} + +#[inline(never)] +pub(crate) fn linear_11i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + let f = linear_10i_vec3f::; + linear_n_i_vec3f::>(lut, arr, inputs, f) +} + +#[inline(never)] +pub(crate) fn linear_12i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + let f = linear_11i_vec3f::; + linear_n_i_vec3f::>(lut, arr, inputs, f) +} + +#[inline(never)] +pub(crate) fn linear_13i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + let f = linear_12i_vec3f::; + linear_n_i_vec3f::>(lut, arr, inputs, f) +} + +#[inline(never)] +pub(crate) fn linear_14i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + let f = linear_13i_vec3f::; + linear_n_i_vec3f::>(lut, arr, inputs, f) +} + +#[inline(never)] +pub(crate) fn linear_15i_vec3f( + lut: &MultidimensionalLut, + arr: &[f32], + inputs: &[f32], +) -> NVector { + let f = linear_14i_vec3f::; + linear_n_i_vec3f::>(lut, arr, inputs, f) +} + +#[inline(never)] +pub(crate) fn tetra_3i_to_any_vec( + lut: &MultidimensionalLut, + arr: &[f32], + x: f32, + y: f32, + z: f32, + dst: &mut [f32], + inks: usize, +) { + match inks { + 1 => { + let vec3 = linear_3i_vec3f::<1>(lut, arr, x, y, z); + dst[0] = vec3.v[0]; + } + 2 => { + let vec3 = linear_3i_vec3f::<2>(lut, arr, x, y, z); + for (dst, src) in dst.iter_mut().zip(vec3.v.iter()) { + *dst = *src; + } + } + 3 => { + let vec3 = linear_3i_vec3f::<3>(lut, arr, x, y, z); + for (dst, src) in dst.iter_mut().zip(vec3.v.iter()) { + *dst = *src; + } + } + 4 => { + let vec3 = linear_3i_vec3f::<4>(lut, arr, x, y, z); + for (dst, src) in dst.iter_mut().zip(vec3.v.iter()) { + *dst = *src; + } + } + 5 => { + let vec3 = linear_3i_vec3f::<5>(lut, arr, x, y, z); + for (dst, src) in dst.iter_mut().zip(vec3.v.iter()) { + *dst = *src; + } + } + 6 => { + let vec3 = linear_3i_vec3f::<6>(lut, arr, x, y, z); + for (dst, src) in dst.iter_mut().zip(vec3.v.iter()) { + *dst = *src; + } + } + 7 => { + let vec3 = linear_3i_vec3f::<7>(lut, arr, x, y, z); + for (dst, src) in dst.iter_mut().zip(vec3.v.iter()) { + *dst = *src; + } + } + 8 => { + let vec3 = linear_3i_vec3f::<8>(lut, arr, x, y, z); + for (dst, src) in dst.iter_mut().zip(vec3.v.iter()) { + *dst = *src; + } + } + 9 => { + let vec3 = linear_3i_vec3f::<9>(lut, arr, x, y, z); + for (dst, src) in dst.iter_mut().zip(vec3.v.iter()) { + *dst = *src; + } + } + 10 => { + let vec3 = linear_3i_vec3f::<10>(lut, arr, x, y, z); + for (dst, src) in dst.iter_mut().zip(vec3.v.iter()) { + *dst = *src; + } + } + 11 => { + let vec3 = linear_3i_vec3f::<11>(lut, arr, x, y, z); + for (dst, src) in dst.iter_mut().zip(vec3.v.iter()) { + *dst = *src; + } + } + 12 => { + let vec3 = linear_3i_vec3f::<12>(lut, arr, x, y, z); + for (dst, src) in dst.iter_mut().zip(vec3.v.iter()) { + *dst = *src; + } + } + 13 => { + let vec3 = linear_3i_vec3f::<13>(lut, arr, x, y, z); + for (dst, src) in dst.iter_mut().zip(vec3.v.iter()) { + *dst = *src; + } + } + 14 => { + let vec3 = linear_3i_vec3f::<14>(lut, arr, x, y, z); + for (dst, src) in dst.iter_mut().zip(vec3.v.iter()) { + *dst = *src; + } + } + 15 => { + let vec3 = linear_3i_vec3f::<15>(lut, arr, x, y, z); + for (dst, src) in dst.iter_mut().zip(vec3.v.iter()) { + *dst = *src; + } + } + _ => unreachable!(), + } +} diff --git a/deps/moxcms/src/conversions/md_luts_factory.rs b/deps/moxcms/src/conversions/md_luts_factory.rs new file mode 100644 index 0000000..72e43ca --- /dev/null +++ b/deps/moxcms/src/conversions/md_luts_factory.rs @@ -0,0 +1,188 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::LutBarycentricReduction; +use crate::conversions::katana::{ + CopyAlphaStage, InjectAlphaStage, Katana, KatanaInitialStage, KatanaIntermediateStage, + KatanaPostFinalizationStage, KatanaStageLabToXyz, KatanaStageXyzToLab, + katana_create_rgb_lin_lut, katana_input_make_lut_nx3, katana_multi_dimensional_3xn_to_device, + katana_multi_dimensional_nx3_to_pcs, katana_output_make_lut_3xn, katana_pcs_lab_v2_to_v4, + katana_pcs_lab_v4_to_v2, katana_prepare_inverse_lut_rgb_xyz, +}; +use crate::{ + CmsError, ColorProfile, DataColorSpace, GammaLutInterpolate, Layout, LutWarehouse, + PointeeSizeExpressible, TransformExecutor, TransformOptions, +}; +use num_traits::AsPrimitive; + +pub(crate) fn do_any_to_any< + T: Copy + + Default + + AsPrimitive + + Send + + Sync + + AsPrimitive + + PointeeSizeExpressible + + GammaLutInterpolate, + const BIT_DEPTH: usize, + const LINEAR_CAP: usize, + const GAMMA_LUT: usize, +>( + src_layout: Layout, + source: &ColorProfile, + dst_layout: Layout, + dest: &ColorProfile, + options: TransformOptions, +) -> Result + Send + Sync>, CmsError> +where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, + (): LutBarycentricReduction, +{ + let mut stages: Vec + Send + Sync>> = Vec::new(); + + let initial_stage: Box + Send + Sync> = match source + .is_matrix_shaper() + { + true => { + let state = + katana_create_rgb_lin_lut::(src_layout, source, options)?; + stages.extend(state.stages); + state.initial_stage + } + false => match source.get_device_to_pcs(options.rendering_intent).ok_or( + CmsError::UnsupportedLutRenderingIntent(source.rendering_intent), + )? { + LutWarehouse::Lut(lut) => katana_input_make_lut_nx3::( + src_layout, + src_layout.channels(), + lut, + options, + source.pcs, + BIT_DEPTH, + )?, + LutWarehouse::Multidimensional(mab) => katana_multi_dimensional_nx3_to_pcs::( + src_layout, mab, options, source.pcs, BIT_DEPTH, + )?, + }, + }; + + stages.push(katana_pcs_lab_v2_to_v4(source)); + if source.pcs == DataColorSpace::Lab { + stages.push(Box::new(KatanaStageLabToXyz::default())); + } + if dest.pcs == DataColorSpace::Lab { + stages.push(Box::new(KatanaStageXyzToLab::default())); + } + stages.push(katana_pcs_lab_v4_to_v2(dest)); + + let final_stage = if dest.has_pcs_to_device_lut() { + let pcs_to_device = dest + .get_pcs_to_device(options.rendering_intent) + .ok_or(CmsError::UnsupportedProfileConnection)?; + match pcs_to_device { + LutWarehouse::Lut(lut) => katana_output_make_lut_3xn::( + dst_layout, + lut, + options, + dest.color_space, + BIT_DEPTH, + )?, + LutWarehouse::Multidimensional(mab) => katana_multi_dimensional_3xn_to_device::( + dst_layout, mab, options, dest.pcs, BIT_DEPTH, + )?, + } + } else if dest.is_matrix_shaper() { + let state = katana_prepare_inverse_lut_rgb_xyz::( + dest, dst_layout, options, + )?; + stages.extend(state.stages); + state.final_stage + } else { + return Err(CmsError::UnsupportedProfileConnection); + }; + + let mut post_finalization: Vec + Send + Sync>> = + Vec::new(); + if let Some(stage) = + prepare_alpha_finalizer::(src_layout, source, dst_layout, dest, BIT_DEPTH) + { + post_finalization.push(stage); + } + + Ok(Box::new(Katana:: { + initial_stage, + final_stage, + stages, + post_finalization, + })) +} + +pub(crate) fn prepare_alpha_finalizer< + T: Copy + + Default + + AsPrimitive + + Send + + Sync + + AsPrimitive + + PointeeSizeExpressible + + GammaLutInterpolate, +>( + src_layout: Layout, + source: &ColorProfile, + dst_layout: Layout, + dest: &ColorProfile, + bit_depth: usize, +) -> Option + Send + Sync>> +where + f32: AsPrimitive, +{ + if (dst_layout == Layout::GrayAlpha && dest.color_space == DataColorSpace::Gray) + || (dst_layout == Layout::Rgba || dest.color_space == DataColorSpace::Rgb) + { + return if (src_layout == Layout::GrayAlpha && source.color_space == DataColorSpace::Gray) + || (src_layout == Layout::Rgba || source.color_space == DataColorSpace::Rgb) + { + Some(Box::new(CopyAlphaStage { + src_layout, + dst_layout, + target_color_space: dest.color_space, + _phantom: Default::default(), + })) + } else { + Some(Box::new(InjectAlphaStage { + dst_layout, + target_color_space: dest.color_space, + _phantom: Default::default(), + bit_depth, + })) + }; + } + None +} diff --git a/deps/moxcms/src/conversions/mod.rs b/deps/moxcms/src/conversions/mod.rs new file mode 100644 index 0000000..8333b5d --- /dev/null +++ b/deps/moxcms/src/conversions/mod.rs @@ -0,0 +1,66 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +mod bpc; +mod gray2rgb; +mod gray2rgb_extended; +mod interpolator; +mod katana; +mod lut3x3; +mod lut3x4; +mod lut4; +mod lut_transforms; +mod mab; +mod mab4x3; +mod mba3x4; +mod md_lut; +mod md_luts_factory; +mod prelude_lut_xyz_rgb; +mod rgb2gray; +mod rgb2gray_extended; +mod rgb_xyz_factory; +mod rgbxyz; +mod rgbxyz_fixed; +mod rgbxyz_float; +mod transform_lut3_to_3; +mod transform_lut3_to_4; +mod transform_lut4_to_3; +mod xyz_lab; + +pub(crate) use gray2rgb::{make_gray_to_unfused, make_gray_to_x}; +pub(crate) use gray2rgb_extended::{make_gray_to_one_trc_extended, make_gray_to_rgb_extended}; +pub(crate) use interpolator::LutBarycentricReduction; +pub(crate) use lut_transforms::make_lut_transform; +pub(crate) use rgb_xyz_factory::{RgbXyzFactory, RgbXyzFactoryOpt}; +pub(crate) use rgb2gray::{ToneReproductionRgbToGray, make_rgb_to_gray}; +pub(crate) use rgb2gray_extended::make_rgb_to_gray_extended; +pub(crate) use rgbxyz::{TransformMatrixShaper, TransformMatrixShaperOptimized}; +pub(crate) use rgbxyz_float::{ + TransformShaperFloatInOut, TransformShaperRgbFloat, make_rgb_xyz_rgb_transform_float, + make_rgb_xyz_rgb_transform_float_in_out, +}; diff --git a/deps/moxcms/src/conversions/prelude_lut_xyz_rgb.rs b/deps/moxcms/src/conversions/prelude_lut_xyz_rgb.rs new file mode 100644 index 0000000..cbacd4e --- /dev/null +++ b/deps/moxcms/src/conversions/prelude_lut_xyz_rgb.rs @@ -0,0 +1,328 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 4/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::lut3x4::create_lut3_samples; +use crate::err::try_vec; +use crate::mlaf::mlaf; +use crate::trc::ToneCurveEvaluator; +use crate::{ + CmsError, ColorProfile, GammaLutInterpolate, InPlaceStage, Matrix3f, PointeeSizeExpressible, + RenderingIntent, Rgb, TransformOptions, filmlike_clip, +}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +pub(crate) struct XyzToRgbStage { + pub(crate) r_gamma: Box<[T; 65536]>, + pub(crate) g_gamma: Box<[T; 65536]>, + pub(crate) b_gamma: Box<[T; 65536]>, + pub(crate) matrices: Vec, + pub(crate) intent: RenderingIntent, + pub(crate) bit_depth: usize, + pub(crate) gamma_lut: usize, +} + +impl> InPlaceStage for XyzToRgbStage { + fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> { + assert!(self.bit_depth > 0); + if !self.matrices.is_empty() { + let m = self.matrices[0]; + for dst in dst.chunks_exact_mut(3) { + let x = dst[0]; + let y = dst[1]; + let z = dst[2]; + dst[0] = mlaf(mlaf(x * m.v[0][0], y, m.v[0][1]), z, m.v[0][2]); + dst[1] = mlaf(mlaf(x * m.v[1][0], y, m.v[1][1]), z, m.v[1][2]); + dst[2] = mlaf(mlaf(x * m.v[2][0], y, m.v[2][1]), z, m.v[2][2]); + } + } + + for m in self.matrices.iter().skip(1) { + for dst in dst.chunks_exact_mut(3) { + let x = dst[0]; + let y = dst[1]; + let z = dst[2]; + dst[0] = mlaf(mlaf(x * m.v[0][0], y, m.v[0][1]), z, m.v[0][2]); + dst[1] = mlaf(mlaf(x * m.v[1][0], y, m.v[1][1]), z, m.v[1][2]); + dst[2] = mlaf(mlaf(x * m.v[2][0], y, m.v[2][1]), z, m.v[2][2]); + } + } + + let max_colors = (1 << self.bit_depth) - 1; + let color_scale = 1f32 / max_colors as f32; + let lut_cap = (self.gamma_lut - 1) as f32; + + if self.intent != RenderingIntent::AbsoluteColorimetric { + for dst in dst.chunks_exact_mut(3) { + let mut rgb = Rgb::new(dst[0], dst[1], dst[2]); + if rgb.is_out_of_gamut() { + rgb = filmlike_clip(rgb); + } + let r = mlaf(0.5f32, rgb.r, lut_cap).min(lut_cap).max(0f32) as u16; + let g = mlaf(0.5f32, rgb.g, lut_cap).min(lut_cap).max(0f32) as u16; + let b = mlaf(0.5f32, rgb.b, lut_cap).min(lut_cap).max(0f32) as u16; + + dst[0] = self.r_gamma[r as usize].as_() * color_scale; + dst[1] = self.g_gamma[g as usize].as_() * color_scale; + dst[2] = self.b_gamma[b as usize].as_() * color_scale; + } + } else { + for dst in dst.chunks_exact_mut(3) { + let rgb = Rgb::new(dst[0], dst[1], dst[2]); + let r = mlaf(0.5f32, rgb.r, lut_cap).min(lut_cap).max(0f32) as u16; + let g = mlaf(0.5f32, rgb.g, lut_cap).min(lut_cap).max(0f32) as u16; + let b = mlaf(0.5f32, rgb.b, lut_cap).min(lut_cap).max(0f32) as u16; + + dst[0] = self.r_gamma[r as usize].as_() * color_scale; + dst[1] = self.g_gamma[g as usize].as_() * color_scale; + dst[2] = self.b_gamma[b as usize].as_() * color_scale; + } + } + + Ok(()) + } +} + +pub(crate) struct XyzToRgbStageExtended { + pub(crate) gamma_evaluator: Box, + pub(crate) matrices: Vec, + pub(crate) phantom_data: PhantomData, +} + +impl> InPlaceStage for XyzToRgbStageExtended { + fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> { + if !self.matrices.is_empty() { + let m = self.matrices[0]; + for dst in dst.chunks_exact_mut(3) { + let x = dst[0]; + let y = dst[1]; + let z = dst[2]; + dst[0] = mlaf(mlaf(x * m.v[0][0], y, m.v[0][1]), z, m.v[0][2]); + dst[1] = mlaf(mlaf(x * m.v[1][0], y, m.v[1][1]), z, m.v[1][2]); + dst[2] = mlaf(mlaf(x * m.v[2][0], y, m.v[2][1]), z, m.v[2][2]); + } + } + + for m in self.matrices.iter().skip(1) { + for dst in dst.chunks_exact_mut(3) { + let x = dst[0]; + let y = dst[1]; + let z = dst[2]; + dst[0] = mlaf(mlaf(x * m.v[0][0], y, m.v[0][1]), z, m.v[0][2]); + dst[1] = mlaf(mlaf(x * m.v[1][0], y, m.v[1][1]), z, m.v[1][2]); + dst[2] = mlaf(mlaf(x * m.v[2][0], y, m.v[2][1]), z, m.v[2][2]); + } + } + + for dst in dst.chunks_exact_mut(3) { + let mut rgb = Rgb::new(dst[0], dst[1], dst[2]); + rgb = self.gamma_evaluator.evaluate_tristimulus(rgb); + dst[0] = rgb.r.as_(); + dst[1] = rgb.g.as_(); + dst[2] = rgb.b.as_(); + } + + Ok(()) + } +} + +struct RgbLinearizationStage { + r_lin: Box<[f32; LINEAR_CAP]>, + g_lin: Box<[f32; LINEAR_CAP]>, + b_lin: Box<[f32; LINEAR_CAP]>, + _phantom: PhantomData, + bit_depth: usize, +} + +impl< + T: Clone + AsPrimitive + PointeeSizeExpressible, + const LINEAR_CAP: usize, + const SAMPLES: usize, +> RgbLinearizationStage +{ + fn transform(&self, src: &[T], dst: &mut [f32]) -> Result<(), CmsError> { + if src.len() % 3 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % 3 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let scale = if T::FINITE { + ((1 << self.bit_depth) - 1) as f32 / (SAMPLES as f32 - 1f32) + } else { + (T::NOT_FINITE_LINEAR_TABLE_SIZE - 1) as f32 / (SAMPLES as f32 - 1f32) + }; + + let capped_value = if T::FINITE { + (1 << self.bit_depth) - 1 + } else { + T::NOT_FINITE_LINEAR_TABLE_SIZE - 1 + }; + + for (src, dst) in src.chunks_exact(3).zip(dst.chunks_exact_mut(3)) { + let j_r = src[0].as_() as f32 * scale; + let j_g = src[1].as_() as f32 * scale; + let j_b = src[2].as_() as f32 * scale; + dst[0] = self.r_lin[(j_r.round().max(0.0).min(capped_value as f32) as u16) as usize]; + dst[1] = self.g_lin[(j_g.round().max(0.0).min(capped_value as f32) as u16) as usize]; + dst[2] = self.b_lin[(j_b.round().max(0.0).min(capped_value as f32) as u16) as usize]; + } + Ok(()) + } +} + +pub(crate) fn create_rgb_lin_lut< + T: Copy + Default + AsPrimitive + Send + Sync + AsPrimitive + PointeeSizeExpressible, + const BIT_DEPTH: usize, + const LINEAR_CAP: usize, + const GRID_SIZE: usize, +>( + source: &ColorProfile, + opts: TransformOptions, +) -> Result, CmsError> +where + u32: AsPrimitive, + f32: AsPrimitive, +{ + let lut_origins = create_lut3_samples::(); + + let lin_r = + source.build_r_linearize_table::(opts.allow_use_cicp_transfer)?; + let lin_g = + source.build_g_linearize_table::(opts.allow_use_cicp_transfer)?; + let lin_b = + source.build_b_linearize_table::(opts.allow_use_cicp_transfer)?; + + let lin_stage = RgbLinearizationStage:: { + r_lin: lin_r, + g_lin: lin_g, + b_lin: lin_b, + _phantom: PhantomData, + bit_depth: BIT_DEPTH, + }; + + let mut lut = try_vec![0f32; lut_origins.len()]; + lin_stage.transform(&lut_origins, &mut lut)?; + + let xyz_to_rgb = source.rgb_to_xyz_matrix(); + + let matrices = vec![ + xyz_to_rgb.to_f32(), + Matrix3f { + v: [ + [32768.0 / 65535.0, 0.0, 0.0], + [0.0, 32768.0 / 65535.0, 0.0], + [0.0, 0.0, 32768.0 / 65535.0], + ], + }, + ]; + + let matrix_stage = crate::conversions::lut_transforms::MatrixStage { matrices }; + matrix_stage.transform(&mut lut)?; + Ok(lut) +} + +pub(crate) fn prepare_inverse_lut_rgb_xyz< + T: Copy + + Default + + AsPrimitive + + Send + + Sync + + AsPrimitive + + PointeeSizeExpressible + + GammaLutInterpolate, + const BIT_DEPTH: usize, + const GAMMA_LUT: usize, +>( + dest: &ColorProfile, + lut: &mut [f32], + options: TransformOptions, +) -> Result<(), CmsError> +where + f32: AsPrimitive, + u32: AsPrimitive, +{ + if !T::FINITE { + if let Some(extended_gamma) = dest.try_extended_gamma_evaluator() { + let xyz_to_rgb = dest.rgb_to_xyz_matrix().inverse(); + + let mut matrices = vec![Matrix3f { + v: [ + [65535.0 / 32768.0, 0.0, 0.0], + [0.0, 65535.0 / 32768.0, 0.0], + [0.0, 0.0, 65535.0 / 32768.0], + ], + }]; + + matrices.push(xyz_to_rgb.to_f32()); + let xyz_to_rgb_stage = XyzToRgbStageExtended:: { + gamma_evaluator: extended_gamma, + matrices, + phantom_data: PhantomData, + }; + xyz_to_rgb_stage.transform(lut)?; + return Ok(()); + } + } + let gamma_map_r = dest.build_gamma_table::( + &dest.red_trc, + options.allow_use_cicp_transfer, + )?; + let gamma_map_g = dest.build_gamma_table::( + &dest.green_trc, + options.allow_use_cicp_transfer, + )?; + let gamma_map_b = dest.build_gamma_table::( + &dest.blue_trc, + options.allow_use_cicp_transfer, + )?; + + let xyz_to_rgb = dest.rgb_to_xyz_matrix().inverse(); + + let mut matrices = vec![Matrix3f { + v: [ + [65535.0 / 32768.0, 0.0, 0.0], + [0.0, 65535.0 / 32768.0, 0.0], + [0.0, 0.0, 65535.0 / 32768.0], + ], + }]; + + matrices.push(xyz_to_rgb.to_f32()); + let xyz_to_rgb_stage = XyzToRgbStage:: { + r_gamma: gamma_map_r, + g_gamma: gamma_map_g, + b_gamma: gamma_map_b, + matrices, + intent: options.rendering_intent, + gamma_lut: GAMMA_LUT, + bit_depth: BIT_DEPTH, + }; + xyz_to_rgb_stage.transform(lut)?; + Ok(()) +} diff --git a/deps/moxcms/src/conversions/rgb2gray.rs b/deps/moxcms/src/conversions/rgb2gray.rs new file mode 100644 index 0000000..fc4fe8a --- /dev/null +++ b/deps/moxcms/src/conversions/rgb2gray.rs @@ -0,0 +1,189 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::mlaf::mlaf; +use crate::transform::PointeeSizeExpressible; +use crate::{CmsError, Layout, TransformExecutor, Vector3f}; +use num_traits::AsPrimitive; + +#[derive(Clone)] +pub(crate) struct ToneReproductionRgbToGray { + pub(crate) r_linear: Box<[f32; BUCKET]>, + pub(crate) g_linear: Box<[f32; BUCKET]>, + pub(crate) b_linear: Box<[f32; BUCKET]>, + pub(crate) gray_gamma: Box<[T; 65536]>, +} + +#[derive(Clone)] +struct TransformRgbToGrayExecutor< + T, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const BUCKET: usize, +> { + trc_box: ToneReproductionRgbToGray, + weights: Vector3f, + bit_depth: usize, + gamma_lut: usize, +} + +pub(crate) fn make_rgb_to_gray< + T: Copy + Default + PointeeSizeExpressible + Send + Sync + 'static, + const BUCKET: usize, +>( + src_layout: Layout, + dst_layout: Layout, + trc: ToneReproductionRgbToGray, + weights: Vector3f, + gamma_lut: usize, + bit_depth: usize, +) -> Box + Send + Sync> +where + u32: AsPrimitive, +{ + match src_layout { + Layout::Rgb => match dst_layout { + Layout::Rgb => unreachable!(), + Layout::Rgba => unreachable!(), + Layout::Gray => Box::new(TransformRgbToGrayExecutor::< + T, + { Layout::Rgb as u8 }, + { Layout::Gray as u8 }, + BUCKET, + > { + trc_box: trc, + weights, + bit_depth, + gamma_lut, + }), + Layout::GrayAlpha => Box::new(TransformRgbToGrayExecutor::< + T, + { Layout::Rgb as u8 }, + { Layout::GrayAlpha as u8 }, + BUCKET, + > { + trc_box: trc, + weights, + bit_depth, + gamma_lut, + }), + _ => unreachable!(), + }, + Layout::Rgba => match dst_layout { + Layout::Rgb => unreachable!(), + Layout::Rgba => unreachable!(), + Layout::Gray => Box::new(TransformRgbToGrayExecutor::< + T, + { Layout::Rgba as u8 }, + { Layout::Gray as u8 }, + BUCKET, + > { + trc_box: trc, + weights, + bit_depth, + gamma_lut, + }), + Layout::GrayAlpha => Box::new(TransformRgbToGrayExecutor::< + T, + { Layout::Rgba as u8 }, + { Layout::GrayAlpha as u8 }, + BUCKET, + > { + trc_box: trc, + weights, + bit_depth, + gamma_lut, + }), + _ => unreachable!(), + }, + Layout::Gray => unreachable!(), + Layout::GrayAlpha => unreachable!(), + _ => unreachable!(), + } +} + +impl< + T: Copy + Default + PointeeSizeExpressible + 'static, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const BUCKET: usize, +> TransformExecutor for TransformRgbToGrayExecutor +where + u32: AsPrimitive, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + let src_cn = Layout::from(SRC_LAYOUT); + let dst_cn = Layout::from(DST_LAYOUT); + let src_channels = src_cn.channels(); + let dst_channels = dst_cn.channels(); + + if src.len() / src_channels != dst.len() / dst_channels { + return Err(CmsError::LaneSizeMismatch); + } + if src.len() % src_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % dst_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let scale_value = (self.gamma_lut - 1) as f32; + let max_value = ((1u32 << self.bit_depth) - 1).as_(); + + for (src, dst) in src + .chunks_exact(src_channels) + .zip(dst.chunks_exact_mut(dst_channels)) + { + let r = self.trc_box.r_linear[src[src_cn.r_i()]._as_usize()]; + let g = self.trc_box.g_linear[src[src_cn.g_i()]._as_usize()]; + let b = self.trc_box.b_linear[src[src_cn.b_i()]._as_usize()]; + let a = if src_channels == 4 { + src[src_cn.a_i()] + } else { + max_value + }; + let grey = mlaf( + 0.5, + mlaf( + mlaf(self.weights.v[0] * r, self.weights.v[1], g), + self.weights.v[2], + b, + ) + .min(1.) + .max(0.), + scale_value, + ); + dst[0] = self.trc_box.gray_gamma[(grey as u16) as usize]; + if dst_channels == 2 { + dst[1] = a; + } + } + + Ok(()) + } +} diff --git a/deps/moxcms/src/conversions/rgb2gray_extended.rs b/deps/moxcms/src/conversions/rgb2gray_extended.rs new file mode 100644 index 0000000..f550ea3 --- /dev/null +++ b/deps/moxcms/src/conversions/rgb2gray_extended.rs @@ -0,0 +1,181 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::mlaf::mlaf; +use crate::transform::PointeeSizeExpressible; +use crate::trc::ToneCurveEvaluator; +use crate::{CmsError, Layout, Rgb, TransformExecutor, Vector3f}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +struct TransformRgbToGrayExtendedExecutor { + linear_eval: Box, + gamma_eval: Box, + weights: Vector3f, + _phantom: PhantomData, + bit_depth: usize, +} + +pub(crate) fn make_rgb_to_gray_extended< + T: Copy + Default + PointeeSizeExpressible + Send + Sync + 'static + AsPrimitive, +>( + src_layout: Layout, + dst_layout: Layout, + linear_eval: Box, + gamma_eval: Box, + weights: Vector3f, + bit_depth: usize, +) -> Box + Send + Sync> +where + u32: AsPrimitive, + f32: AsPrimitive, +{ + match src_layout { + Layout::Rgb => match dst_layout { + Layout::Rgb => unreachable!(), + Layout::Rgba => unreachable!(), + Layout::Gray => Box::new(TransformRgbToGrayExtendedExecutor::< + T, + { Layout::Rgb as u8 }, + { Layout::Gray as u8 }, + > { + linear_eval, + gamma_eval, + weights, + _phantom: PhantomData, + bit_depth, + }), + Layout::GrayAlpha => Box::new(TransformRgbToGrayExtendedExecutor::< + T, + { Layout::Rgb as u8 }, + { Layout::GrayAlpha as u8 }, + > { + linear_eval, + gamma_eval, + weights, + _phantom: PhantomData, + bit_depth, + }), + _ => unreachable!(), + }, + Layout::Rgba => match dst_layout { + Layout::Rgb => unreachable!(), + Layout::Rgba => unreachable!(), + Layout::Gray => Box::new(TransformRgbToGrayExtendedExecutor::< + T, + { Layout::Rgba as u8 }, + { Layout::Gray as u8 }, + > { + linear_eval, + gamma_eval, + weights, + _phantom: PhantomData, + bit_depth, + }), + Layout::GrayAlpha => Box::new(TransformRgbToGrayExtendedExecutor::< + T, + { Layout::Rgba as u8 }, + { Layout::GrayAlpha as u8 }, + > { + linear_eval, + gamma_eval, + weights, + _phantom: PhantomData, + bit_depth, + }), + _ => unreachable!(), + }, + Layout::Gray => unreachable!(), + Layout::GrayAlpha => unreachable!(), + _ => unreachable!(), + } +} + +impl< + T: Copy + Default + PointeeSizeExpressible + 'static + AsPrimitive, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, +> TransformExecutor for TransformRgbToGrayExtendedExecutor +where + u32: AsPrimitive, + f32: AsPrimitive, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + let src_cn = Layout::from(SRC_LAYOUT); + let dst_cn = Layout::from(DST_LAYOUT); + let src_channels = src_cn.channels(); + let dst_channels = dst_cn.channels(); + + if src.len() / src_channels != dst.len() / dst_channels { + return Err(CmsError::LaneSizeMismatch); + } + if src.len() % src_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % dst_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let max_value = ((1u32 << self.bit_depth) - 1).as_(); + + for (src, dst) in src + .chunks_exact(src_channels) + .zip(dst.chunks_exact_mut(dst_channels)) + { + let in_tristimulus = Rgb::::new( + src[src_cn.r_i()].as_(), + src[src_cn.g_i()].as_(), + src[src_cn.b_i()].as_(), + ); + let lin_tristimulus = self.linear_eval.evaluate_tristimulus(in_tristimulus); + let a = if src_channels == 4 { + src[src_cn.a_i()] + } else { + max_value + }; + let grey = mlaf( + mlaf( + self.weights.v[0] * lin_tristimulus.r, + self.weights.v[1], + lin_tristimulus.g, + ), + self.weights.v[2], + lin_tristimulus.b, + ) + .min(1.) + .max(0.); + let gamma_value = self.gamma_eval.evaluate_value(grey); + dst[0] = gamma_value.as_(); + if dst_channels == 2 { + dst[1] = a; + } + } + + Ok(()) + } +} diff --git a/deps/moxcms/src/conversions/rgb_xyz_factory.rs b/deps/moxcms/src/conversions/rgb_xyz_factory.rs new file mode 100644 index 0000000..f0f118f --- /dev/null +++ b/deps/moxcms/src/conversions/rgb_xyz_factory.rs @@ -0,0 +1,395 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 4/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::TransformMatrixShaper; +use crate::conversions::rgbxyz::{ + TransformMatrixShaperOptimized, make_rgb_xyz_rgb_transform, make_rgb_xyz_rgb_transform_opt, +}; +use crate::conversions::rgbxyz_fixed::{make_rgb_xyz_q2_13, make_rgb_xyz_q2_13_opt}; +use crate::{CmsError, Layout, TransformExecutor, TransformOptions}; +use num_traits::AsPrimitive; + +const FIXED_POINT_SCALE: i32 = 13; // Q2.13; + +pub(crate) trait RgbXyzFactory + Default> { + fn make_transform( + src_layout: Layout, + dst_layout: Layout, + profile: TransformMatrixShaper, + transform_options: TransformOptions, + ) -> Result + Send + Sync>, CmsError>; +} + +pub(crate) trait RgbXyzFactoryOpt + Default> { + fn make_optimized_transform< + const LINEAR_CAP: usize, + const GAMMA_LUT: usize, + const BIT_DEPTH: usize, + >( + src_layout: Layout, + dst_layout: Layout, + profile: TransformMatrixShaperOptimized, + transform_options: TransformOptions, + ) -> Result + Send + Sync>, CmsError>; +} + +impl RgbXyzFactory for u16 { + fn make_transform( + src_layout: Layout, + dst_layout: Layout, + profile: TransformMatrixShaper, + transform_options: TransformOptions, + ) -> Result + Send + Sync>, CmsError> { + if BIT_DEPTH < 16 && transform_options.prefer_fixed_point { + #[cfg(all(target_arch = "x86_64", feature = "avx"))] + { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q2_13_transform_avx2; + if std::arch::is_x86_feature_detected!("avx2") { + return make_rgb_xyz_q2_13_transform_avx2::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ); + } + } + #[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] + { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q2_13_transform_sse_41; + if std::arch::is_x86_feature_detected!("sse4.1") { + return make_rgb_xyz_q2_13_transform_sse_41::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ); + } + } + #[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] + { + return make_rgb_xyz_q2_13::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ); + } + } + make_rgb_xyz_rgb_transform::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ) + } +} + +impl RgbXyzFactory for f32 { + fn make_transform( + src_layout: Layout, + dst_layout: Layout, + profile: TransformMatrixShaper, + transform_options: TransformOptions, + ) -> Result + Send + Sync>, CmsError> { + if transform_options.prefer_fixed_point { + #[cfg(all(target_arch = "x86_64", feature = "avx"))] + { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q2_13_transform_avx2; + if std::arch::is_x86_feature_detected!("avx2") { + return make_rgb_xyz_q2_13_transform_avx2::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ); + } + } + #[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] + { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q2_13_transform_sse_41; + if std::arch::is_x86_feature_detected!("sse4.1") { + return make_rgb_xyz_q2_13_transform_sse_41::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ); + } + } + #[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] + { + return make_rgb_xyz_q2_13::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ); + } + } + make_rgb_xyz_rgb_transform::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ) + } +} + +impl RgbXyzFactory for f64 { + fn make_transform( + src_layout: Layout, + dst_layout: Layout, + profile: TransformMatrixShaper, + _: TransformOptions, + ) -> Result + Send + Sync>, CmsError> { + make_rgb_xyz_rgb_transform::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ) + } +} + +impl RgbXyzFactory for u8 { + fn make_transform( + src_layout: Layout, + dst_layout: Layout, + profile: TransformMatrixShaper, + transform_options: TransformOptions, + ) -> Result + Send + Sync>, CmsError> { + if transform_options.prefer_fixed_point { + #[cfg(all(target_arch = "x86_64", feature = "avx"))] + { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q2_13_transform_avx2; + if std::arch::is_x86_feature_detected!("avx2") { + return make_rgb_xyz_q2_13_transform_avx2::( + src_layout, dst_layout, profile, GAMMA_LUT, 8, + ); + } + } + #[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] + { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q2_13_transform_sse_41; + if std::arch::is_x86_feature_detected!("sse4.1") { + return make_rgb_xyz_q2_13_transform_sse_41::( + src_layout, dst_layout, profile, GAMMA_LUT, 8, + ); + } + } + make_rgb_xyz_q2_13::( + src_layout, dst_layout, profile, GAMMA_LUT, 8, + ) + } else { + make_rgb_xyz_rgb_transform::( + src_layout, dst_layout, profile, GAMMA_LUT, 8, + ) + } + } +} + +// Optimized factories + +impl RgbXyzFactoryOpt for u16 { + fn make_optimized_transform< + const LINEAR_CAP: usize, + const GAMMA_LUT: usize, + const BIT_DEPTH: usize, + >( + src_layout: Layout, + dst_layout: Layout, + profile: TransformMatrixShaperOptimized, + transform_options: TransformOptions, + ) -> Result + Send + Sync>, CmsError> { + if BIT_DEPTH >= 12 && transform_options.prefer_fixed_point { + #[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] + { + if std::arch::is_aarch64_feature_detected!("rdm") { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q1_30_opt; + return make_rgb_xyz_q1_30_opt::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ); + } + } + } + if BIT_DEPTH < 16 && transform_options.prefer_fixed_point { + #[cfg(all(target_arch = "x86_64", feature = "avx"))] + { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q2_13_transform_avx2_opt; + if std::arch::is_x86_feature_detected!("avx2") { + return make_rgb_xyz_q2_13_transform_avx2_opt::< + u16, + LINEAR_CAP, + FIXED_POINT_SCALE, + >( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH + ); + } + } + #[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] + { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q2_13_transform_sse_41_opt; + if std::arch::is_x86_feature_detected!("sse4.1") { + return make_rgb_xyz_q2_13_transform_sse_41_opt::< + u16, + LINEAR_CAP, + FIXED_POINT_SCALE, + >( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH + ); + } + } + #[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] + { + return make_rgb_xyz_q2_13_opt::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ); + } + } + make_rgb_xyz_rgb_transform_opt::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ) + } +} + +impl RgbXyzFactoryOpt for f32 { + fn make_optimized_transform< + const LINEAR_CAP: usize, + const GAMMA_LUT: usize, + const BIT_DEPTH: usize, + >( + src_layout: Layout, + dst_layout: Layout, + profile: TransformMatrixShaperOptimized, + transform_options: TransformOptions, + ) -> Result + Send + Sync>, CmsError> { + if transform_options.prefer_fixed_point { + #[cfg(all(target_arch = "x86_64", feature = "avx"))] + { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q2_13_transform_avx2_opt; + if std::arch::is_x86_feature_detected!("avx2") { + return make_rgb_xyz_q2_13_transform_avx2_opt::< + f32, + LINEAR_CAP, + FIXED_POINT_SCALE, + >( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH + ); + } + } + #[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] + { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q2_13_transform_sse_41_opt; + if std::arch::is_x86_feature_detected!("sse4.1") { + return make_rgb_xyz_q2_13_transform_sse_41_opt::< + f32, + LINEAR_CAP, + FIXED_POINT_SCALE, + >( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH + ); + } + } + #[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] + { + return if std::arch::is_aarch64_feature_detected!("rdm") { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q1_30_opt; + make_rgb_xyz_q1_30_opt::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ) + } else { + make_rgb_xyz_q2_13_opt::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ) + }; + } + } + make_rgb_xyz_rgb_transform_opt::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ) + } +} + +impl RgbXyzFactoryOpt for f64 { + fn make_optimized_transform< + const LINEAR_CAP: usize, + const GAMMA_LUT: usize, + const BIT_DEPTH: usize, + >( + src_layout: Layout, + dst_layout: Layout, + profile: TransformMatrixShaperOptimized, + transform_options: TransformOptions, + ) -> Result + Send + Sync>, CmsError> { + if transform_options.prefer_fixed_point { + #[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] + { + if std::arch::is_aarch64_feature_detected!("rdm") { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q1_30_opt; + return make_rgb_xyz_q1_30_opt::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ); + } + } + } + make_rgb_xyz_rgb_transform_opt::( + src_layout, dst_layout, profile, GAMMA_LUT, BIT_DEPTH, + ) + } +} + +impl RgbXyzFactoryOpt for u8 { + fn make_optimized_transform< + const LINEAR_CAP: usize, + const GAMMA_LUT: usize, + const BIT_DEPTH: usize, + >( + src_layout: Layout, + dst_layout: Layout, + profile: TransformMatrixShaperOptimized, + transform_options: TransformOptions, + ) -> Result + Send + Sync>, CmsError> { + if transform_options.prefer_fixed_point { + #[cfg(all(target_arch = "x86_64", feature = "avx512"))] + { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q2_13_transform_avx512_opt; + if std::arch::is_x86_feature_detected!("avx512bw") + && std::arch::is_x86_feature_detected!("avx512vl") + { + return make_rgb_xyz_q2_13_transform_avx512_opt::< + u8, + LINEAR_CAP, + FIXED_POINT_SCALE, + >(src_layout, dst_layout, profile, GAMMA_LUT, 8); + } + } + #[cfg(all(target_arch = "x86_64", feature = "avx"))] + { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q2_13_transform_avx2_opt; + if std::arch::is_x86_feature_detected!("avx2") { + return make_rgb_xyz_q2_13_transform_avx2_opt::< + u8, + LINEAR_CAP, + FIXED_POINT_SCALE, + >(src_layout, dst_layout, profile, GAMMA_LUT, 8); + } + } + #[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] + { + use crate::conversions::rgbxyz_fixed::make_rgb_xyz_q2_13_transform_sse_41_opt; + if std::arch::is_x86_feature_detected!("sse4.1") { + return make_rgb_xyz_q2_13_transform_sse_41_opt::< + u8, + LINEAR_CAP, + FIXED_POINT_SCALE, + >(src_layout, dst_layout, profile, GAMMA_LUT, 8); + } + } + make_rgb_xyz_q2_13_opt::( + src_layout, dst_layout, profile, GAMMA_LUT, 8, + ) + } else { + make_rgb_xyz_rgb_transform_opt::( + src_layout, dst_layout, profile, GAMMA_LUT, 8, + ) + } + } +} diff --git a/deps/moxcms/src/conversions/rgbxyz.rs b/deps/moxcms/src/conversions/rgbxyz.rs new file mode 100644 index 0000000..d308397 --- /dev/null +++ b/deps/moxcms/src/conversions/rgbxyz.rs @@ -0,0 +1,899 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::{CmsError, Layout, Matrix3, Matrix3f, TransformExecutor}; +use num_traits::AsPrimitive; + +pub(crate) struct TransformMatrixShaper { + pub(crate) r_linear: Box<[f32; BUCKET]>, + pub(crate) g_linear: Box<[f32; BUCKET]>, + pub(crate) b_linear: Box<[f32; BUCKET]>, + pub(crate) r_gamma: Box<[T; 65536]>, + pub(crate) g_gamma: Box<[T; 65536]>, + pub(crate) b_gamma: Box<[T; 65536]>, + pub(crate) adaptation_matrix: Matrix3f, +} + +impl TransformMatrixShaper { + #[inline(never)] + #[allow(dead_code)] + fn convert_to_v(self) -> TransformMatrixShaperV { + TransformMatrixShaperV { + r_linear: self.r_linear.iter().copied().collect(), + g_linear: self.g_linear.iter().copied().collect(), + b_linear: self.b_linear.iter().copied().collect(), + r_gamma: self.r_gamma, + g_gamma: self.g_gamma, + b_gamma: self.b_gamma, + adaptation_matrix: self.adaptation_matrix, + } + } +} + +#[allow(dead_code)] +pub(crate) struct TransformMatrixShaperV { + pub(crate) r_linear: Vec, + pub(crate) g_linear: Vec, + pub(crate) b_linear: Vec, + pub(crate) r_gamma: Box<[T; 65536]>, + pub(crate) g_gamma: Box<[T; 65536]>, + pub(crate) b_gamma: Box<[T; 65536]>, + pub(crate) adaptation_matrix: Matrix3f, +} + +/// Low memory footprint optimized routine for matrix shaper profiles with the same +/// Gamma and linear curves. +pub(crate) struct TransformMatrixShaperOptimized { + pub(crate) linear: Box<[f32; BUCKET]>, + pub(crate) gamma: Box<[T; 65536]>, + pub(crate) adaptation_matrix: Matrix3f, +} + +#[allow(dead_code)] +impl TransformMatrixShaperOptimized { + fn convert_to_v(self) -> TransformMatrixShaperOptimizedV { + TransformMatrixShaperOptimizedV { + linear: self.linear.iter().copied().collect::>(), + gamma: self.gamma, + adaptation_matrix: self.adaptation_matrix, + } + } +} + +/// Low memory footprint optimized routine for matrix shaper profiles with the same +/// Gamma and linear curves. +#[allow(dead_code)] +pub(crate) struct TransformMatrixShaperOptimizedV { + pub(crate) linear: Vec, + pub(crate) gamma: Box<[T; 65536]>, + pub(crate) adaptation_matrix: Matrix3f, +} + +impl TransformMatrixShaper { + #[inline(never)] + #[allow(dead_code)] + pub(crate) fn to_q2_13_n< + R: Copy + 'static + Default, + const PRECISION: i32, + const LINEAR_CAP: usize, + >( + &self, + gamma_lut: usize, + bit_depth: usize, + ) -> TransformMatrixShaperFixedPoint + where + f32: AsPrimitive, + { + let linear_scale = if T::FINITE { + let lut_scale = (gamma_lut - 1) as f32 / ((1 << bit_depth) - 1) as f32; + ((1 << bit_depth) - 1) as f32 * lut_scale + } else { + let lut_scale = (gamma_lut - 1) as f32 / (T::NOT_FINITE_LINEAR_TABLE_SIZE - 1) as f32; + (T::NOT_FINITE_LINEAR_TABLE_SIZE - 1) as f32 * lut_scale + }; + let mut new_box_r = Box::new([R::default(); BUCKET]); + let mut new_box_g = Box::new([R::default(); BUCKET]); + let mut new_box_b = Box::new([R::default(); BUCKET]); + for (dst, &src) in new_box_r.iter_mut().zip(self.r_linear.iter()) { + *dst = (src * linear_scale).round().as_(); + } + for (dst, &src) in new_box_g.iter_mut().zip(self.g_linear.iter()) { + *dst = (src * linear_scale).round().as_(); + } + for (dst, &src) in new_box_b.iter_mut().zip(self.b_linear.iter()) { + *dst = (src * linear_scale).round().as_(); + } + let scale: f32 = (1i32 << PRECISION) as f32; + let source_matrix = self.adaptation_matrix; + let mut dst_matrix = Matrix3:: { v: [[0i16; 3]; 3] }; + for i in 0..3 { + for j in 0..3 { + dst_matrix.v[i][j] = (source_matrix.v[i][j] * scale) as i16; + } + } + TransformMatrixShaperFixedPoint { + r_linear: new_box_r, + g_linear: new_box_g, + b_linear: new_box_b, + r_gamma: self.r_gamma.clone(), + g_gamma: self.g_gamma.clone(), + b_gamma: self.b_gamma.clone(), + adaptation_matrix: dst_matrix, + } + } + + #[inline(never)] + #[allow(dead_code)] + pub(crate) fn to_q2_13_i( + &self, + gamma_lut: usize, + bit_depth: usize, + ) -> TransformMatrixShaperFp + where + f32: AsPrimitive, + { + let linear_scale = if T::FINITE { + let lut_scale = (gamma_lut - 1) as f32 / ((1 << bit_depth) - 1) as f32; + ((1 << bit_depth) - 1) as f32 * lut_scale + } else { + let lut_scale = (gamma_lut - 1) as f32 / (T::NOT_FINITE_LINEAR_TABLE_SIZE - 1) as f32; + (T::NOT_FINITE_LINEAR_TABLE_SIZE - 1) as f32 * lut_scale + }; + let new_box_r = self + .r_linear + .iter() + .map(|&x| (x * linear_scale).round().as_()) + .collect::>(); + let new_box_g = self + .g_linear + .iter() + .map(|&x| (x * linear_scale).round().as_()) + .collect::>(); + let new_box_b = self + .b_linear + .iter() + .map(|&x| (x * linear_scale).round().as_()) + .collect::>(); + let scale: f32 = (1i32 << PRECISION) as f32; + let source_matrix = self.adaptation_matrix; + let mut dst_matrix = Matrix3:: { v: [[0i16; 3]; 3] }; + for i in 0..3 { + for j in 0..3 { + dst_matrix.v[i][j] = (source_matrix.v[i][j] * scale) as i16; + } + } + TransformMatrixShaperFp { + r_linear: new_box_r, + g_linear: new_box_g, + b_linear: new_box_b, + r_gamma: self.r_gamma.clone(), + g_gamma: self.g_gamma.clone(), + b_gamma: self.b_gamma.clone(), + adaptation_matrix: dst_matrix, + } + } +} + +impl + TransformMatrixShaperOptimized +{ + #[allow(dead_code)] + pub(crate) fn to_q2_13_n< + R: Copy + 'static + Default, + const PRECISION: i32, + const LINEAR_CAP: usize, + >( + &self, + gamma_lut: usize, + bit_depth: usize, + ) -> TransformMatrixShaperFixedPointOpt + where + f32: AsPrimitive, + { + let linear_scale = if T::FINITE { + let lut_scale = (gamma_lut - 1) as f32 / ((1 << bit_depth) - 1) as f32; + ((1 << bit_depth) - 1) as f32 * lut_scale + } else { + let lut_scale = (gamma_lut - 1) as f32 / (T::NOT_FINITE_LINEAR_TABLE_SIZE - 1) as f32; + (T::NOT_FINITE_LINEAR_TABLE_SIZE - 1) as f32 * lut_scale + }; + let mut new_box_linear = Box::new([R::default(); BUCKET]); + for (dst, src) in new_box_linear.iter_mut().zip(self.linear.iter()) { + *dst = (*src * linear_scale).round().as_(); + } + let scale: f32 = (1i32 << PRECISION) as f32; + let source_matrix = self.adaptation_matrix; + let mut dst_matrix = Matrix3:: { + v: [[i16::default(); 3]; 3], + }; + for i in 0..3 { + for j in 0..3 { + dst_matrix.v[i][j] = (source_matrix.v[i][j] * scale) as i16; + } + } + TransformMatrixShaperFixedPointOpt { + linear: new_box_linear, + gamma: self.gamma.clone(), + adaptation_matrix: dst_matrix, + } + } + + #[allow(dead_code)] + pub(crate) fn to_q2_13_i( + &self, + gamma_lut: usize, + bit_depth: usize, + ) -> TransformMatrixShaperFpOptVec + where + f32: AsPrimitive, + { + let linear_scale = if T::FINITE { + let lut_scale = (gamma_lut - 1) as f32 / ((1 << bit_depth) - 1) as f32; + ((1 << bit_depth) - 1) as f32 * lut_scale + } else { + let lut_scale = (gamma_lut - 1) as f32 / (T::NOT_FINITE_LINEAR_TABLE_SIZE - 1) as f32; + (T::NOT_FINITE_LINEAR_TABLE_SIZE - 1) as f32 * lut_scale + }; + let new_box_linear = self + .linear + .iter() + .map(|&x| (x * linear_scale).round().as_()) + .collect::>(); + let scale: f32 = (1i32 << PRECISION) as f32; + let source_matrix = self.adaptation_matrix; + let mut dst_matrix = Matrix3:: { + v: [[i16::default(); 3]; 3], + }; + for i in 0..3 { + for j in 0..3 { + dst_matrix.v[i][j] = (source_matrix.v[i][j] * scale) as i16; + } + } + TransformMatrixShaperFpOptVec { + linear: new_box_linear, + gamma: self.gamma.clone(), + adaptation_matrix: dst_matrix, + } + } + + #[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] + pub(crate) fn to_q1_30_n( + &self, + gamma_lut: usize, + bit_depth: usize, + ) -> TransformMatrixShaperFpOptVec + where + f32: AsPrimitive, + f64: AsPrimitive, + { + // It is important to scale 1 bit more to compensate vqrdmlah Q0.31, because we're going to use Q1.30 + let table_size = if T::FINITE { + (1 << bit_depth) - 1 + } else { + T::NOT_FINITE_LINEAR_TABLE_SIZE - 1 + }; + let ext_bp = if T::FINITE { + bit_depth as u32 + 1 + } else { + let bp = (T::NOT_FINITE_LINEAR_TABLE_SIZE - 1).count_ones(); + bp + 1 + }; + let linear_scale = { + let lut_scale = (gamma_lut - 1) as f64 / table_size as f64; + ((1u32 << ext_bp) - 1) as f64 * lut_scale + }; + let new_box_linear = self + .linear + .iter() + .map(|&v| (v as f64 * linear_scale).round().as_()) + .collect::>(); + let scale: f64 = (1i64 << PRECISION) as f64; + let source_matrix = self.adaptation_matrix; + let mut dst_matrix = Matrix3:: { + v: [[i32::default(); 3]; 3], + }; + for i in 0..3 { + for j in 0..3 { + dst_matrix.v[i][j] = (source_matrix.v[i][j] as f64 * scale) as i32; + } + } + TransformMatrixShaperFpOptVec { + linear: new_box_linear, + gamma: self.gamma.clone(), + adaptation_matrix: dst_matrix, + } + } +} + +#[allow(unused)] +struct TransformMatrixShaperScalar< + T: Clone, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const LINEAR_CAP: usize, +> { + pub(crate) profile: TransformMatrixShaper, + pub(crate) gamma_lut: usize, + pub(crate) bit_depth: usize, +} + +#[allow(unused)] +struct TransformMatrixShaperOptScalar< + T: Clone, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const LINEAR_CAP: usize, +> { + pub(crate) profile: TransformMatrixShaperOptimized, + pub(crate) gamma_lut: usize, + pub(crate) bit_depth: usize, +} + +#[cfg(any( + any(target_arch = "x86", target_arch = "x86_64"), + all(target_arch = "aarch64", target_feature = "neon") +))] +#[allow(unused)] +macro_rules! create_rgb_xyz_dependant_executor { + ($dep_name: ident, $dependant: ident, $shaper: ident) => { + pub(crate) fn $dep_name< + T: Clone + Send + Sync + Default + PointeeSizeExpressible + Copy + 'static, + const LINEAR_CAP: usize, + >( + src_layout: Layout, + dst_layout: Layout, + profile: $shaper, + gamma_lut: usize, + bit_depth: usize, + ) -> Result + Send + Sync>, CmsError> + where + u32: AsPrimitive, + { + if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgba) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgba as u8 }, + LINEAR_CAP, + > { + profile, + bit_depth, + gamma_lut, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgba) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgba as u8 }, + LINEAR_CAP, + > { + profile, + bit_depth, + gamma_lut, + })); + } else if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgb) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgb as u8 }, + LINEAR_CAP, + > { + profile, + bit_depth, + gamma_lut, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgb) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgb as u8 }, + LINEAR_CAP, + > { + profile, + bit_depth, + gamma_lut, + })); + } + Err(CmsError::UnsupportedProfileConnection) + } + }; +} + +#[cfg(any( + any(target_arch = "x86", target_arch = "x86_64"), + all(target_arch = "aarch64", target_feature = "neon") +))] +#[allow(unused)] +macro_rules! create_rgb_xyz_dependant_executor_to_v { + ($dep_name: ident, $dependant: ident, $shaper: ident) => { + pub(crate) fn $dep_name< + T: Clone + Send + Sync + Default + PointeeSizeExpressible + Copy + 'static, + const LINEAR_CAP: usize, + >( + src_layout: Layout, + dst_layout: Layout, + profile: $shaper, + gamma_lut: usize, + bit_depth: usize, + ) -> Result + Send + Sync>, CmsError> + where + u32: AsPrimitive, + { + let profile = profile.convert_to_v(); + if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgba) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgba as u8 }, + > { + profile, + bit_depth, + gamma_lut, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgba) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgba as u8 }, + > { + profile, + bit_depth, + gamma_lut, + })); + } else if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgb) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgb as u8 }, + > { + profile, + bit_depth, + gamma_lut, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgb) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgb as u8 }, + > { + profile, + bit_depth, + gamma_lut, + })); + } + Err(CmsError::UnsupportedProfileConnection) + } + }; +} + +#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] +use crate::conversions::sse::{TransformShaperRgbOptSse, TransformShaperRgbSse}; + +#[cfg(all(target_arch = "x86_64", feature = "avx"))] +use crate::conversions::avx::{TransformShaperRgbAvx, TransformShaperRgbOptAvx}; + +#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] +create_rgb_xyz_dependant_executor!( + make_rgb_xyz_rgb_transform_sse_41, + TransformShaperRgbSse, + TransformMatrixShaper +); + +#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] +create_rgb_xyz_dependant_executor_to_v!( + make_rgb_xyz_rgb_transform_sse_41_opt, + TransformShaperRgbOptSse, + TransformMatrixShaperOptimized +); + +#[cfg(all(target_arch = "x86_64", feature = "avx"))] +create_rgb_xyz_dependant_executor!( + make_rgb_xyz_rgb_transform_avx2, + TransformShaperRgbAvx, + TransformMatrixShaper +); + +#[cfg(all(target_arch = "x86_64", feature = "avx"))] +create_rgb_xyz_dependant_executor_to_v!( + make_rgb_xyz_rgb_transform_avx2_opt, + TransformShaperRgbOptAvx, + TransformMatrixShaperOptimized +); + +#[cfg(all(target_arch = "x86_64", feature = "avx512"))] +use crate::conversions::avx512::TransformShaperRgbOptAvx512; + +#[cfg(all(target_arch = "x86_64", feature = "avx512"))] +create_rgb_xyz_dependant_executor!( + make_rgb_xyz_rgb_transform_avx512_opt, + TransformShaperRgbOptAvx512, + TransformMatrixShaperOptimized +); + +#[cfg(not(all(target_arch = "aarch64", target_feature = "neon", feature = "neon")))] +pub(crate) fn make_rgb_xyz_rgb_transform< + T: Clone + Send + Sync + PointeeSizeExpressible + 'static + Copy + Default, + const LINEAR_CAP: usize, +>( + src_layout: Layout, + dst_layout: Layout, + profile: TransformMatrixShaper, + gamma_lut: usize, + bit_depth: usize, +) -> Result + Send + Sync>, CmsError> +where + u32: AsPrimitive, +{ + #[cfg(all(feature = "avx", target_arch = "x86_64"))] + if std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma") { + return make_rgb_xyz_rgb_transform_avx2::( + src_layout, dst_layout, profile, gamma_lut, bit_depth, + ); + } + #[cfg(all(feature = "sse", any(target_arch = "x86", target_arch = "x86_64")))] + if std::arch::is_x86_feature_detected!("sse4.1") { + return make_rgb_xyz_rgb_transform_sse_41::( + src_layout, dst_layout, profile, gamma_lut, bit_depth, + ); + } + if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgba) { + return Ok(Box::new(TransformMatrixShaperScalar::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgba as u8 }, + LINEAR_CAP, + > { + profile, + gamma_lut, + bit_depth, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgba) { + return Ok(Box::new(TransformMatrixShaperScalar::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgba as u8 }, + LINEAR_CAP, + > { + profile, + gamma_lut, + bit_depth, + })); + } else if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgb) { + return Ok(Box::new(TransformMatrixShaperScalar::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgb as u8 }, + LINEAR_CAP, + > { + profile, + gamma_lut, + bit_depth, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgb) { + return Ok(Box::new(TransformMatrixShaperScalar::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgb as u8 }, + LINEAR_CAP, + > { + profile, + gamma_lut, + bit_depth, + })); + } + Err(CmsError::UnsupportedProfileConnection) +} + +#[cfg(not(all(target_arch = "aarch64", target_feature = "neon", feature = "neon")))] +pub(crate) fn make_rgb_xyz_rgb_transform_opt< + T: Clone + Send + Sync + PointeeSizeExpressible + 'static + Copy + Default, + const LINEAR_CAP: usize, +>( + src_layout: Layout, + dst_layout: Layout, + profile: TransformMatrixShaperOptimized, + gamma_lut: usize, + bit_depth: usize, +) -> Result + Send + Sync>, CmsError> +where + u32: AsPrimitive, +{ + #[cfg(all(feature = "avx512", target_arch = "x86_64"))] + if std::arch::is_x86_feature_detected!("avx512bw") + && std::arch::is_x86_feature_detected!("avx512vl") + && std::arch::is_x86_feature_detected!("fma") + { + return make_rgb_xyz_rgb_transform_avx512_opt::( + src_layout, dst_layout, profile, gamma_lut, bit_depth, + ); + } + #[cfg(all(feature = "avx", target_arch = "x86_64"))] + if std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma") { + return make_rgb_xyz_rgb_transform_avx2_opt::( + src_layout, dst_layout, profile, gamma_lut, bit_depth, + ); + } + #[cfg(all(feature = "sse", any(target_arch = "x86", target_arch = "x86_64")))] + if std::arch::is_x86_feature_detected!("sse4.1") { + return make_rgb_xyz_rgb_transform_sse_41_opt::( + src_layout, dst_layout, profile, gamma_lut, bit_depth, + ); + } + if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgba) { + return Ok(Box::new(TransformMatrixShaperOptScalar::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgba as u8 }, + LINEAR_CAP, + > { + profile, + gamma_lut, + bit_depth, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgba) { + return Ok(Box::new(TransformMatrixShaperOptScalar::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgba as u8 }, + LINEAR_CAP, + > { + profile, + gamma_lut, + bit_depth, + })); + } else if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgb) { + return Ok(Box::new(TransformMatrixShaperOptScalar::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgb as u8 }, + LINEAR_CAP, + > { + profile, + gamma_lut, + bit_depth, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgb) { + return Ok(Box::new(TransformMatrixShaperOptScalar::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgb as u8 }, + LINEAR_CAP, + > { + profile, + gamma_lut, + bit_depth, + })); + } + Err(CmsError::UnsupportedProfileConnection) +} + +#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] +use crate::conversions::neon::{TransformShaperRgbNeon, TransformShaperRgbOptNeon}; +use crate::conversions::rgbxyz_fixed::TransformMatrixShaperFpOptVec; +use crate::conversions::rgbxyz_fixed::{ + TransformMatrixShaperFixedPoint, TransformMatrixShaperFixedPointOpt, TransformMatrixShaperFp, +}; +use crate::transform::PointeeSizeExpressible; + +#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] +create_rgb_xyz_dependant_executor_to_v!( + make_rgb_xyz_rgb_transform, + TransformShaperRgbNeon, + TransformMatrixShaper +); + +#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] +create_rgb_xyz_dependant_executor_to_v!( + make_rgb_xyz_rgb_transform_opt, + TransformShaperRgbOptNeon, + TransformMatrixShaperOptimized +); + +#[allow(unused)] +impl< + T: Clone + PointeeSizeExpressible + Copy + Default + 'static, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const LINEAR_CAP: usize, +> TransformExecutor for TransformMatrixShaperScalar +where + u32: AsPrimitive, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + use crate::mlaf::mlaf; + let src_cn = Layout::from(SRC_LAYOUT); + let dst_cn = Layout::from(DST_LAYOUT); + let src_channels = src_cn.channels(); + let dst_channels = dst_cn.channels(); + + if src.len() / src_channels != dst.len() / dst_channels { + return Err(CmsError::LaneSizeMismatch); + } + if src.len() % src_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % dst_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let transform = self.profile.adaptation_matrix; + let scale = (self.gamma_lut - 1) as f32; + let max_colors: T = ((1 << self.bit_depth) - 1).as_(); + + for (src, dst) in src + .chunks_exact(src_channels) + .zip(dst.chunks_exact_mut(dst_channels)) + { + let r = self.profile.r_linear[src[src_cn.r_i()]._as_usize()]; + let g = self.profile.g_linear[src[src_cn.g_i()]._as_usize()]; + let b = self.profile.b_linear[src[src_cn.b_i()]._as_usize()]; + let a = if src_channels == 4 { + src[src_cn.a_i()] + } else { + max_colors + }; + + let new_r = mlaf( + 0.5f32, + mlaf( + mlaf(r * transform.v[0][0], g, transform.v[0][1]), + b, + transform.v[0][2], + ) + .max(0f32) + .min(1f32), + scale, + ); + + let new_g = mlaf( + 0.5f32, + mlaf( + mlaf(r * transform.v[1][0], g, transform.v[1][1]), + b, + transform.v[1][2], + ) + .max(0f32) + .min(1f32), + scale, + ); + + let new_b = mlaf( + 0.5f32, + mlaf( + mlaf(r * transform.v[2][0], g, transform.v[2][1]), + b, + transform.v[2][2], + ) + .max(0f32) + .min(1f32), + scale, + ); + + dst[dst_cn.r_i()] = self.profile.r_gamma[(new_r as u16) as usize]; + dst[dst_cn.g_i()] = self.profile.g_gamma[(new_g as u16) as usize]; + dst[dst_cn.b_i()] = self.profile.b_gamma[(new_b as u16) as usize]; + if dst_channels == 4 { + dst[dst_cn.a_i()] = a; + } + } + + Ok(()) + } +} + +#[allow(unused)] +impl< + T: Clone + PointeeSizeExpressible + Copy + Default + 'static, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const LINEAR_CAP: usize, +> TransformExecutor for TransformMatrixShaperOptScalar +where + u32: AsPrimitive, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + use crate::mlaf::mlaf; + let src_cn = Layout::from(SRC_LAYOUT); + let dst_cn = Layout::from(DST_LAYOUT); + let src_channels = src_cn.channels(); + let dst_channels = dst_cn.channels(); + + if src.len() / src_channels != dst.len() / dst_channels { + return Err(CmsError::LaneSizeMismatch); + } + if src.len() % src_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % dst_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let transform = self.profile.adaptation_matrix; + let scale = (self.gamma_lut - 1) as f32; + let max_colors: T = ((1 << self.bit_depth) - 1).as_(); + + for (src, dst) in src + .chunks_exact(src_channels) + .zip(dst.chunks_exact_mut(dst_channels)) + { + let r = self.profile.linear[src[src_cn.r_i()]._as_usize()]; + let g = self.profile.linear[src[src_cn.g_i()]._as_usize()]; + let b = self.profile.linear[src[src_cn.b_i()]._as_usize()]; + let a = if src_channels == 4 { + src[src_cn.a_i()] + } else { + max_colors + }; + + let new_r = mlaf( + 0.5f32, + mlaf( + mlaf(r * transform.v[0][0], g, transform.v[0][1]), + b, + transform.v[0][2], + ) + .max(0f32) + .min(1f32), + scale, + ); + + let new_g = mlaf( + 0.5f32, + mlaf( + mlaf(r * transform.v[1][0], g, transform.v[1][1]), + b, + transform.v[1][2], + ) + .max(0f32) + .min(1f32), + scale, + ); + + let new_b = mlaf( + 0.5f32, + mlaf( + mlaf(r * transform.v[2][0], g, transform.v[2][1]), + b, + transform.v[2][2], + ) + .max(0f32) + .min(1f32), + scale, + ); + + dst[dst_cn.r_i()] = self.profile.gamma[(new_r as u16) as usize]; + dst[dst_cn.g_i()] = self.profile.gamma[(new_g as u16) as usize]; + dst[dst_cn.b_i()] = self.profile.gamma[(new_b as u16) as usize]; + if dst_channels == 4 { + dst[dst_cn.a_i()] = a; + } + } + + Ok(()) + } +} diff --git a/deps/moxcms/src/conversions/rgbxyz_fixed.rs b/deps/moxcms/src/conversions/rgbxyz_fixed.rs new file mode 100644 index 0000000..6fb180f --- /dev/null +++ b/deps/moxcms/src/conversions/rgbxyz_fixed.rs @@ -0,0 +1,560 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::Layout; +use crate::conversions::TransformMatrixShaper; +use crate::matrix::Matrix3; +use crate::{CmsError, TransformExecutor}; +use num_traits::AsPrimitive; + +/// Fixed point conversion Q2.13 +pub(crate) struct TransformMatrixShaperFixedPoint { + pub(crate) r_linear: Box<[R; LINEAR_CAP]>, + pub(crate) g_linear: Box<[R; LINEAR_CAP]>, + pub(crate) b_linear: Box<[R; LINEAR_CAP]>, + pub(crate) r_gamma: Box<[T; 65536]>, + pub(crate) g_gamma: Box<[T; 65536]>, + pub(crate) b_gamma: Box<[T; 65536]>, + pub(crate) adaptation_matrix: Matrix3, +} + +/// Fixed point conversion Q2.13 +#[allow(dead_code)] +pub(crate) struct TransformMatrixShaperFp { + pub(crate) r_linear: Vec, + pub(crate) g_linear: Vec, + pub(crate) b_linear: Vec, + pub(crate) r_gamma: Box<[T; 65536]>, + pub(crate) g_gamma: Box<[T; 65536]>, + pub(crate) b_gamma: Box<[T; 65536]>, + pub(crate) adaptation_matrix: Matrix3, +} + +/// Fixed point conversion Q2.13 +/// +/// Optimized routine for *all same curves* matrix shaper. +pub(crate) struct TransformMatrixShaperFixedPointOpt { + pub(crate) linear: Box<[R; LINEAR_CAP]>, + pub(crate) gamma: Box<[T; 65536]>, + pub(crate) adaptation_matrix: Matrix3, +} + +/// Fixed point conversion Q2.13 +/// +/// Optimized routine for *all same curves* matrix shaper. +#[allow(dead_code)] +pub(crate) struct TransformMatrixShaperFpOptVec { + pub(crate) linear: Vec, + pub(crate) gamma: Box<[T; 65536]>, + pub(crate) adaptation_matrix: Matrix3, +} + +#[allow(unused)] +struct TransformMatrixShaperQ2_13< + T: Copy, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const LINEAR_CAP: usize, + const PRECISION: i32, +> { + pub(crate) profile: TransformMatrixShaperFixedPoint, + pub(crate) bit_depth: usize, + pub(crate) gamma_lut: usize, +} + +#[allow(unused)] +struct TransformMatrixShaperQ2_13Optimized< + T: Copy, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const LINEAR_CAP: usize, + const PRECISION: i32, +> { + pub(crate) profile: TransformMatrixShaperFixedPointOpt, + pub(crate) bit_depth: usize, + pub(crate) gamma_lut: usize, +} + +#[allow(unused)] +impl< + T: Clone + PointeeSizeExpressible + Copy + Default + 'static, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const LINEAR_CAP: usize, + const PRECISION: i32, +> TransformExecutor + for TransformMatrixShaperQ2_13 +where + u32: AsPrimitive, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + let src_cn = Layout::from(SRC_LAYOUT); + let dst_cn = Layout::from(DST_LAYOUT); + let src_channels = src_cn.channels(); + let dst_channels = dst_cn.channels(); + + if src.len() / src_channels != dst.len() / dst_channels { + return Err(CmsError::LaneSizeMismatch); + } + if src.len() % src_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % dst_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let transform = self.profile.adaptation_matrix; + let max_colors: T = ((1 << self.bit_depth as u32) - 1u32).as_(); + let rnd: i32 = (1i32 << (PRECISION - 1)); + + let v_gamma_max = self.gamma_lut as i32 - 1; + + for (src, dst) in src + .chunks_exact(src_channels) + .zip(dst.chunks_exact_mut(dst_channels)) + { + let r = self.profile.r_linear[src[src_cn.r_i()]._as_usize()]; + let g = self.profile.g_linear[src[src_cn.g_i()]._as_usize()]; + let b = self.profile.b_linear[src[src_cn.b_i()]._as_usize()]; + let a = if src_channels == 4 { + src[src_cn.a_i()] + } else { + max_colors + }; + + let new_r = r as i32 * transform.v[0][0] as i32 + + g as i32 * transform.v[0][1] as i32 + + b as i32 * transform.v[0][2] as i32 + + rnd; + + let r_q2_13 = (new_r >> PRECISION).min(v_gamma_max).max(0) as u16; + + let new_g = r as i32 * transform.v[1][0] as i32 + + g as i32 * transform.v[1][1] as i32 + + b as i32 * transform.v[1][2] as i32 + + rnd; + + let g_q2_13 = (new_g >> PRECISION).min(v_gamma_max).max(0) as u16; + + let new_b = r as i32 * transform.v[2][0] as i32 + + g as i32 * transform.v[2][1] as i32 + + b as i32 * transform.v[2][2] as i32 + + rnd; + + let b_q2_13 = (new_b >> PRECISION).min(v_gamma_max).max(0) as u16; + + dst[dst_cn.r_i()] = self.profile.r_gamma[r_q2_13 as usize]; + dst[dst_cn.g_i()] = self.profile.g_gamma[g_q2_13 as usize]; + dst[dst_cn.b_i()] = self.profile.b_gamma[b_q2_13 as usize]; + if dst_channels == 4 { + dst[dst_cn.a_i()] = a; + } + } + Ok(()) + } +} + +#[allow(unused)] +impl< + T: Clone + PointeeSizeExpressible + Copy + Default + 'static, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const LINEAR_CAP: usize, + const PRECISION: i32, +> TransformExecutor + for TransformMatrixShaperQ2_13Optimized +where + u32: AsPrimitive, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + let src_cn = Layout::from(SRC_LAYOUT); + let dst_cn = Layout::from(DST_LAYOUT); + let src_channels = src_cn.channels(); + let dst_channels = dst_cn.channels(); + + if src.len() / src_channels != dst.len() / dst_channels { + return Err(CmsError::LaneSizeMismatch); + } + if src.len() % src_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % dst_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let transform = self.profile.adaptation_matrix; + let max_colors: T = ((1 << self.bit_depth as u32) - 1u32).as_(); + let rnd: i32 = (1i32 << (PRECISION - 1)); + + let v_gamma_max = self.gamma_lut as i32 - 1; + + for (src, dst) in src + .chunks_exact(src_channels) + .zip(dst.chunks_exact_mut(dst_channels)) + { + let r = self.profile.linear[src[src_cn.r_i()]._as_usize()]; + let g = self.profile.linear[src[src_cn.g_i()]._as_usize()]; + let b = self.profile.linear[src[src_cn.b_i()]._as_usize()]; + let a = if src_channels == 4 { + src[src_cn.a_i()] + } else { + max_colors + }; + + let new_r = r as i32 * transform.v[0][0] as i32 + + g as i32 * transform.v[0][1] as i32 + + b as i32 * transform.v[0][2] as i32 + + rnd; + + let r_q2_13 = (new_r >> PRECISION).min(v_gamma_max).max(0) as u16; + + let new_g = r as i32 * transform.v[1][0] as i32 + + g as i32 * transform.v[1][1] as i32 + + b as i32 * transform.v[1][2] as i32 + + rnd; + + let g_q2_13 = (new_g >> PRECISION).min(v_gamma_max).max(0) as u16; + + let new_b = r as i32 * transform.v[2][0] as i32 + + g as i32 * transform.v[2][1] as i32 + + b as i32 * transform.v[2][2] as i32 + + rnd; + + let b_q2_13 = (new_b >> PRECISION).min(v_gamma_max).max(0) as u16; + + dst[dst_cn.r_i()] = self.profile.gamma[r_q2_13 as usize]; + dst[dst_cn.g_i()] = self.profile.gamma[g_q2_13 as usize]; + dst[dst_cn.b_i()] = self.profile.gamma[b_q2_13 as usize]; + if dst_channels == 4 { + dst[dst_cn.a_i()] = a; + } + } + Ok(()) + } +} + +#[allow(unused_macros)] +macro_rules! create_rgb_xyz_dependant_q2_13_executor { + ($dep_name: ident, $dependant: ident, $resolution: ident, $shaper: ident) => { + pub(crate) fn $dep_name< + T: Clone + Send + Sync + AsPrimitive + Default + PointeeSizeExpressible, + const LINEAR_CAP: usize, + const PRECISION: i32, + >( + src_layout: Layout, + dst_layout: Layout, + profile: $shaper, + gamma_lut: usize, + bit_depth: usize, + ) -> Result + Send + Sync>, CmsError> + where + u32: AsPrimitive, + { + let q2_13_profile = + profile.to_q2_13_n::<$resolution, PRECISION, LINEAR_CAP>(gamma_lut, bit_depth); + if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgba) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgba as u8 }, + LINEAR_CAP, + PRECISION, + > { + profile: q2_13_profile, + bit_depth, + gamma_lut, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgba) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgba as u8 }, + LINEAR_CAP, + PRECISION, + > { + profile: q2_13_profile, + bit_depth, + gamma_lut, + })); + } else if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgb) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgb as u8 }, + LINEAR_CAP, + PRECISION, + > { + profile: q2_13_profile, + bit_depth, + gamma_lut, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgb) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgb as u8 }, + LINEAR_CAP, + PRECISION, + > { + profile: q2_13_profile, + bit_depth, + gamma_lut, + })); + } + Err(CmsError::UnsupportedProfileConnection) + } + }; +} + +#[allow(unused_macros)] +macro_rules! create_rgb_xyz_dependant_q2_13_executor_fp { + ($dep_name: ident, $dependant: ident, $resolution: ident, $shaper: ident) => { + pub(crate) fn $dep_name< + T: Clone + Send + Sync + AsPrimitive + Default + PointeeSizeExpressible, + const LINEAR_CAP: usize, + const PRECISION: i32, + >( + src_layout: Layout, + dst_layout: Layout, + profile: $shaper, + gamma_lut: usize, + bit_depth: usize, + ) -> Result + Send + Sync>, CmsError> + where + u32: AsPrimitive, + { + let q2_13_profile = profile.to_q2_13_i::<$resolution, PRECISION>(gamma_lut, bit_depth); + if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgba) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgba as u8 }, + PRECISION, + > { + profile: q2_13_profile, + bit_depth, + gamma_lut, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgba) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgba as u8 }, + PRECISION, + > { + profile: q2_13_profile, + bit_depth, + gamma_lut, + })); + } else if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgb) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgb as u8 }, + PRECISION, + > { + profile: q2_13_profile, + bit_depth, + gamma_lut, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgb) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgb as u8 }, + PRECISION, + > { + profile: q2_13_profile, + bit_depth, + gamma_lut, + })); + } + Err(CmsError::UnsupportedProfileConnection) + } + }; +} + +#[cfg(all(target_arch = "aarch64", feature = "neon"))] +macro_rules! create_rgb_xyz_dependant_q1_30_executor { + ($dep_name: ident, $dependant: ident, $resolution: ident, $shaper: ident) => { + pub(crate) fn $dep_name< + T: Clone + Send + Sync + AsPrimitive + Default + PointeeSizeExpressible, + const LINEAR_CAP: usize, + const PRECISION: i32, + >( + src_layout: Layout, + dst_layout: Layout, + profile: $shaper, + gamma_lut: usize, + bit_depth: usize, + ) -> Result + Send + Sync>, CmsError> + where + u32: AsPrimitive, + { + let q1_30_profile = profile.to_q1_30_n::<$resolution, PRECISION>(gamma_lut, bit_depth); + if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgba) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgba as u8 }, + > { + profile: q1_30_profile, + gamma_lut, + bit_depth, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgba) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgba as u8 }, + > { + profile: q1_30_profile, + gamma_lut, + bit_depth, + })); + } else if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgb) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgb as u8 }, + > { + profile: q1_30_profile, + gamma_lut, + bit_depth, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgb) { + return Ok(Box::new($dependant::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgb as u8 }, + > { + profile: q1_30_profile, + gamma_lut, + bit_depth, + })); + } + Err(CmsError::UnsupportedProfileConnection) + } + }; +} + +#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] +use crate::conversions::neon::{ + TransformShaperQ1_30NeonOpt, TransformShaperQ2_13Neon, TransformShaperQ2_13NeonOpt, +}; + +#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] +create_rgb_xyz_dependant_q2_13_executor_fp!( + make_rgb_xyz_q2_13, + TransformShaperQ2_13Neon, + i16, + TransformMatrixShaper +); + +#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] +create_rgb_xyz_dependant_q2_13_executor_fp!( + make_rgb_xyz_q2_13_opt, + TransformShaperQ2_13NeonOpt, + i16, + TransformMatrixShaperOptimized +); + +#[cfg(all(target_arch = "aarch64", target_feature = "neon", feature = "neon"))] +create_rgb_xyz_dependant_q1_30_executor!( + make_rgb_xyz_q1_30_opt, + TransformShaperQ1_30NeonOpt, + i32, + TransformMatrixShaperOptimized +); + +#[cfg(not(all(target_arch = "aarch64", target_feature = "neon", feature = "neon")))] +create_rgb_xyz_dependant_q2_13_executor!( + make_rgb_xyz_q2_13, + TransformMatrixShaperQ2_13, + i16, + TransformMatrixShaper +); + +#[cfg(not(all(target_arch = "aarch64", target_feature = "neon", feature = "neon")))] +create_rgb_xyz_dependant_q2_13_executor!( + make_rgb_xyz_q2_13_opt, + TransformMatrixShaperQ2_13Optimized, + i16, + TransformMatrixShaperOptimized +); + +#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] +use crate::conversions::sse::{TransformShaperQ2_13OptSse, TransformShaperQ2_13Sse}; + +#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] +create_rgb_xyz_dependant_q2_13_executor_fp!( + make_rgb_xyz_q2_13_transform_sse_41, + TransformShaperQ2_13Sse, + i32, + TransformMatrixShaper +); + +#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "sse"))] +create_rgb_xyz_dependant_q2_13_executor_fp!( + make_rgb_xyz_q2_13_transform_sse_41_opt, + TransformShaperQ2_13OptSse, + i32, + TransformMatrixShaperOptimized +); + +#[cfg(all(target_arch = "x86_64", feature = "avx"))] +use crate::conversions::avx::{TransformShaperRgbQ2_13Avx, TransformShaperRgbQ2_13OptAvx}; +use crate::conversions::rgbxyz::TransformMatrixShaperOptimized; +use crate::transform::PointeeSizeExpressible; + +#[cfg(all(target_arch = "x86_64", feature = "avx"))] +create_rgb_xyz_dependant_q2_13_executor_fp!( + make_rgb_xyz_q2_13_transform_avx2, + TransformShaperRgbQ2_13Avx, + i32, + TransformMatrixShaper +); + +#[cfg(all(target_arch = "x86_64", feature = "avx"))] +create_rgb_xyz_dependant_q2_13_executor_fp!( + make_rgb_xyz_q2_13_transform_avx2_opt, + TransformShaperRgbQ2_13OptAvx, + i32, + TransformMatrixShaperOptimized +); + +#[cfg(all(target_arch = "x86_64", feature = "avx512"))] +use crate::conversions::avx512::TransformShaperRgbQ2_13OptAvx512; + +#[cfg(all(target_arch = "x86_64", feature = "avx512"))] +create_rgb_xyz_dependant_q2_13_executor!( + make_rgb_xyz_q2_13_transform_avx512_opt, + TransformShaperRgbQ2_13OptAvx512, + i32, + TransformMatrixShaperOptimized +); diff --git a/deps/moxcms/src/conversions/rgbxyz_float.rs b/deps/moxcms/src/conversions/rgbxyz_float.rs new file mode 100644 index 0000000..2fbc883 --- /dev/null +++ b/deps/moxcms/src/conversions/rgbxyz_float.rs @@ -0,0 +1,330 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::trc::ToneCurveEvaluator; +use crate::{CmsError, Layout, Matrix3f, PointeeSizeExpressible, Rgb, TransformExecutor}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +pub(crate) struct TransformShaperRgbFloat { + pub(crate) r_linear: Box<[f32; BUCKET]>, + pub(crate) g_linear: Box<[f32; BUCKET]>, + pub(crate) b_linear: Box<[f32; BUCKET]>, + pub(crate) gamma_evaluator: Box, + pub(crate) adaptation_matrix: Matrix3f, + pub(crate) phantom_data: PhantomData, +} + +pub(crate) struct TransformShaperFloatInOut { + pub(crate) linear_evaluator: Box, + pub(crate) gamma_evaluator: Box, + pub(crate) adaptation_matrix: Matrix3f, + pub(crate) phantom_data: PhantomData, +} + +struct TransformShaperFloatScalar< + T: Clone, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const LINEAR_CAP: usize, +> { + pub(crate) profile: TransformShaperRgbFloat, + pub(crate) bit_depth: usize, +} + +struct TransformShaperRgbFloatInOut { + pub(crate) profile: TransformShaperFloatInOut, + pub(crate) bit_depth: usize, +} + +pub(crate) fn make_rgb_xyz_rgb_transform_float< + T: Clone + Send + Sync + PointeeSizeExpressible + 'static + Copy + Default, + const LINEAR_CAP: usize, +>( + src_layout: Layout, + dst_layout: Layout, + profile: TransformShaperRgbFloat, + bit_depth: usize, +) -> Result + Send + Sync>, CmsError> +where + u32: AsPrimitive, + f32: AsPrimitive, +{ + if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgba) { + return Ok(Box::new(TransformShaperFloatScalar::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgba as u8 }, + LINEAR_CAP, + > { + profile, + bit_depth, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgba) { + return Ok(Box::new(TransformShaperFloatScalar::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgba as u8 }, + LINEAR_CAP, + > { + profile, + bit_depth, + })); + } else if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgb) { + return Ok(Box::new(TransformShaperFloatScalar::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgb as u8 }, + LINEAR_CAP, + > { + profile, + bit_depth, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgb) { + return Ok(Box::new(TransformShaperFloatScalar::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgb as u8 }, + LINEAR_CAP, + > { + profile, + bit_depth, + })); + } + Err(CmsError::UnsupportedProfileConnection) +} + +pub(crate) fn make_rgb_xyz_rgb_transform_float_in_out< + T: Clone + Send + Sync + PointeeSizeExpressible + 'static + Copy + Default + AsPrimitive, +>( + src_layout: Layout, + dst_layout: Layout, + profile: TransformShaperFloatInOut, + bit_depth: usize, +) -> Result + Send + Sync>, CmsError> +where + u32: AsPrimitive, + f32: AsPrimitive, +{ + if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgba) { + return Ok(Box::new(TransformShaperRgbFloatInOut::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgba as u8 }, + > { + profile, + bit_depth, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgba) { + return Ok(Box::new(TransformShaperRgbFloatInOut::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgba as u8 }, + > { + profile, + bit_depth, + })); + } else if (src_layout == Layout::Rgba) && (dst_layout == Layout::Rgb) { + return Ok(Box::new(TransformShaperRgbFloatInOut::< + T, + { Layout::Rgba as u8 }, + { Layout::Rgb as u8 }, + > { + profile, + bit_depth, + })); + } else if (src_layout == Layout::Rgb) && (dst_layout == Layout::Rgb) { + return Ok(Box::new(TransformShaperRgbFloatInOut::< + T, + { Layout::Rgb as u8 }, + { Layout::Rgb as u8 }, + > { + profile, + bit_depth, + })); + } + Err(CmsError::UnsupportedProfileConnection) +} + +impl< + T: Clone + PointeeSizeExpressible + Copy + Default + 'static, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const LINEAR_CAP: usize, +> TransformExecutor for TransformShaperFloatScalar +where + u32: AsPrimitive, + f32: AsPrimitive, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + use crate::mlaf::mlaf; + let src_cn = Layout::from(SRC_LAYOUT); + let dst_cn = Layout::from(DST_LAYOUT); + let src_channels = src_cn.channels(); + let dst_channels = dst_cn.channels(); + + if src.len() / src_channels != dst.len() / dst_channels { + return Err(CmsError::LaneSizeMismatch); + } + if src.len() % src_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % dst_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let transform = self.profile.adaptation_matrix; + let max_colors: T = ((1 << self.bit_depth) - 1).as_(); + + for (src, dst) in src + .chunks_exact(src_channels) + .zip(dst.chunks_exact_mut(dst_channels)) + { + let r = self.profile.r_linear[src[src_cn.r_i()]._as_usize()]; + let g = self.profile.g_linear[src[src_cn.g_i()]._as_usize()]; + let b = self.profile.b_linear[src[src_cn.b_i()]._as_usize()]; + let a = if src_channels == 4 { + src[src_cn.a_i()] + } else { + max_colors + }; + + let new_r = mlaf( + mlaf(r * transform.v[0][0], g, transform.v[0][1]), + b, + transform.v[0][2], + ); + + let new_g = mlaf( + mlaf(r * transform.v[1][0], g, transform.v[1][1]), + b, + transform.v[1][2], + ); + + let new_b = mlaf( + mlaf(r * transform.v[2][0], g, transform.v[2][1]), + b, + transform.v[2][2], + ); + + let mut rgb = Rgb::new(new_r, new_g, new_b); + rgb = self.profile.gamma_evaluator.evaluate_tristimulus(rgb); + + dst[dst_cn.r_i()] = rgb.r.as_(); + dst[dst_cn.g_i()] = rgb.g.as_(); + dst[dst_cn.b_i()] = rgb.b.as_(); + if dst_channels == 4 { + dst[dst_cn.a_i()] = a; + } + } + + Ok(()) + } +} + +impl< + T: Clone + PointeeSizeExpressible + Copy + Default + 'static + AsPrimitive, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, +> TransformExecutor for TransformShaperRgbFloatInOut +where + u32: AsPrimitive, + f32: AsPrimitive, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + use crate::mlaf::mlaf; + let src_cn = Layout::from(SRC_LAYOUT); + let dst_cn = Layout::from(DST_LAYOUT); + let src_channels = src_cn.channels(); + let dst_channels = dst_cn.channels(); + + if src.len() / src_channels != dst.len() / dst_channels { + return Err(CmsError::LaneSizeMismatch); + } + if src.len() % src_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % dst_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + + let transform = self.profile.adaptation_matrix; + let max_colors: T = ((1 << self.bit_depth) - 1).as_(); + + for (src, dst) in src + .chunks_exact(src_channels) + .zip(dst.chunks_exact_mut(dst_channels)) + { + let mut src_rgb = Rgb::new( + src[src_cn.r_i()].as_(), + src[src_cn.g_i()].as_(), + src[src_cn.b_i()].as_(), + ); + src_rgb = self.profile.linear_evaluator.evaluate_tristimulus(src_rgb); + let r = src_rgb.r; + let g = src_rgb.g; + let b = src_rgb.b; + let a = if src_channels == 4 { + src[src_cn.a_i()] + } else { + max_colors + }; + + let new_r = mlaf( + mlaf(r * transform.v[0][0], g, transform.v[0][1]), + b, + transform.v[0][2], + ); + + let new_g = mlaf( + mlaf(r * transform.v[1][0], g, transform.v[1][1]), + b, + transform.v[1][2], + ); + + let new_b = mlaf( + mlaf(r * transform.v[2][0], g, transform.v[2][1]), + b, + transform.v[2][2], + ); + + let mut rgb = Rgb::new(new_r, new_g, new_b); + rgb = self.profile.gamma_evaluator.evaluate_tristimulus(rgb); + + dst[dst_cn.r_i()] = rgb.r.as_(); + dst[dst_cn.g_i()] = rgb.g.as_(); + dst[dst_cn.b_i()] = rgb.b.as_(); + + if dst_channels == 4 { + dst[dst_cn.a_i()] = a; + } + } + + Ok(()) + } +} diff --git a/deps/moxcms/src/conversions/transform_lut3_to_3.rs b/deps/moxcms/src/conversions/transform_lut3_to_3.rs new file mode 100644 index 0000000..f40c6a6 --- /dev/null +++ b/deps/moxcms/src/conversions/transform_lut3_to_3.rs @@ -0,0 +1,266 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#![allow(dead_code)] +use crate::conversions::LutBarycentricReduction; +use crate::conversions::interpolator::{BarycentricWeight, MultidimensionalInterpolation}; +use crate::conversions::lut_transforms::Lut3x3Factory; +use crate::transform::PointeeSizeExpressible; +use crate::{ + BarycentricWeightScale, CmsError, DataColorSpace, InterpolationMethod, Layout, + TransformExecutor, TransformOptions, +}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +pub(crate) struct TransformLut3x3< + T, + U, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + const BINS: usize, + const BARYCENTRIC_BINS: usize, +> { + pub(crate) lut: Vec, + pub(crate) _phantom: PhantomData, + pub(crate) _phantom1: PhantomData, + pub(crate) interpolation_method: InterpolationMethod, + pub(crate) weights: Box<[BarycentricWeight; BINS]>, + pub(crate) color_space: DataColorSpace, + pub(crate) is_linear: bool, +} + +impl< + T: Copy + AsPrimitive + Default + PointeeSizeExpressible, + U: AsPrimitive, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + const BINS: usize, + const BARYCENTRIC_BINS: usize, +> TransformLut3x3 +where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, +{ + #[inline(never)] + fn transform_chunk( + &self, + src: &[T], + dst: &mut [T], + interpolator: Box, + ) { + let src_cn = Layout::from(SRC_LAYOUT); + let src_channels = src_cn.channels(); + + let dst_cn = Layout::from(DST_LAYOUT); + let dst_channels = dst_cn.channels(); + + let value_scale = ((1 << BIT_DEPTH) - 1) as f32; + let max_value = ((1u32 << BIT_DEPTH) - 1).as_(); + + for (src, dst) in src + .chunks_exact(src_channels) + .zip(dst.chunks_exact_mut(dst_channels)) + { + let x = <() as LutBarycentricReduction>::reduce::( + src[src_cn.r_i()], + ); + let y = <() as LutBarycentricReduction>::reduce::( + src[src_cn.g_i()], + ); + let z = <() as LutBarycentricReduction>::reduce::( + src[src_cn.b_i()], + ); + + let a = if src_channels == 4 { + src[src_cn.a_i()] + } else { + max_value + }; + + let v = interpolator.inter3( + &self.lut, + &self.weights[x.as_()], + &self.weights[y.as_()], + &self.weights[z.as_()], + ); + if T::FINITE { + let r = v * value_scale + 0.5; + dst[dst_cn.r_i()] = r.v[0].min(value_scale).max(0.).as_(); + dst[dst_cn.g_i()] = r.v[1].min(value_scale).max(0.).as_(); + dst[dst_cn.b_i()] = r.v[2].min(value_scale).max(0.).as_(); + if dst_channels == 4 { + dst[dst_cn.a_i()] = a; + } + } else { + dst[dst_cn.r_i()] = v.v[0].as_(); + dst[dst_cn.g_i()] = v.v[1].as_(); + dst[dst_cn.b_i()] = v.v[2].as_(); + if dst_channels == 4 { + dst[dst_cn.a_i()] = a; + } + } + } + } +} + +impl< + T: Copy + AsPrimitive + Default + PointeeSizeExpressible, + U: AsPrimitive, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + const BINS: usize, + const BARYCENTRIC_BINS: usize, +> TransformExecutor + for TransformLut3x3 +where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + let src_cn = Layout::from(SRC_LAYOUT); + let src_channels = src_cn.channels(); + + let dst_cn = Layout::from(DST_LAYOUT); + let dst_channels = dst_cn.channels(); + if src.len() % src_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % dst_channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + let src_chunks = src.len() / src_channels; + let dst_chunks = dst.len() / dst_channels; + if src_chunks != dst_chunks { + return Err(CmsError::LaneSizeMismatch); + } + + if self.color_space == DataColorSpace::Lab + || (self.is_linear && self.color_space == DataColorSpace::Rgb) + || self.color_space == DataColorSpace::Xyz + { + use crate::conversions::interpolator::Trilinear; + self.transform_chunk(src, dst, Box::new(Trilinear:: {})); + } else { + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + use crate::conversions::interpolator::Tetrahedral; + self.transform_chunk(src, dst, Box::new(Tetrahedral:: {})); + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + use crate::conversions::interpolator::Pyramidal; + self.transform_chunk(src, dst, Box::new(Pyramidal:: {})); + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + use crate::conversions::interpolator::Prismatic; + self.transform_chunk(src, dst, Box::new(Prismatic:: {})); + } + InterpolationMethod::Linear => { + use crate::conversions::interpolator::Trilinear; + self.transform_chunk(src, dst, Box::new(Trilinear:: {})); + } + } + } + + Ok(()) + } +} + +pub(crate) struct DefaultLut3x3Factory {} + +impl Lut3x3Factory for DefaultLut3x3Factory { + fn make_transform_3x3< + T: Copy + AsPrimitive + Default + PointeeSizeExpressible + 'static + Send + Sync, + const SRC_LAYOUT: u8, + const DST_LAYOUT: u8, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + >( + lut: Vec, + options: TransformOptions, + color_space: DataColorSpace, + is_linear: bool, + ) -> Box + Send + Sync> + where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, + (): LutBarycentricReduction, + { + match options.barycentric_weight_scale { + BarycentricWeightScale::Low => Box::new(TransformLut3x3::< + T, + u8, + SRC_LAYOUT, + DST_LAYOUT, + GRID_SIZE, + BIT_DEPTH, + 256, + 256, + > { + lut, + _phantom: PhantomData, + _phantom1: PhantomData, + interpolation_method: options.interpolation_method, + weights: BarycentricWeight::::create_ranged_256::(), + color_space, + is_linear, + }), + #[cfg(feature = "options")] + BarycentricWeightScale::High => Box::new(TransformLut3x3::< + T, + u16, + SRC_LAYOUT, + DST_LAYOUT, + GRID_SIZE, + BIT_DEPTH, + 65536, + 65536, + > { + lut, + _phantom: PhantomData, + _phantom1: PhantomData, + interpolation_method: options.interpolation_method, + weights: BarycentricWeight::::create_binned::(), + color_space, + is_linear, + }), + } + } +} diff --git a/deps/moxcms/src/conversions/transform_lut3_to_4.rs b/deps/moxcms/src/conversions/transform_lut3_to_4.rs new file mode 100644 index 0000000..e3da058 --- /dev/null +++ b/deps/moxcms/src/conversions/transform_lut3_to_4.rs @@ -0,0 +1,274 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::LutBarycentricReduction; +use crate::conversions::interpolator::{BarycentricWeight, MultidimensionalInterpolation}; +use crate::transform::PointeeSizeExpressible; +use crate::{ + BarycentricWeightScale, CmsError, DataColorSpace, InterpolationMethod, Layout, + TransformExecutor, TransformOptions, +}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +pub(crate) struct TransformLut3x4< + T, + U: AsPrimitive, + const LAYOUT: u8, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + const BINS: usize, + const BARYCENTRIC_BINS: usize, +> { + pub(crate) lut: Vec, + pub(crate) _phantom: PhantomData, + pub(crate) _phantom1: PhantomData, + pub(crate) interpolation_method: InterpolationMethod, + pub(crate) weights: Box<[BarycentricWeight; BINS]>, + pub(crate) color_space: DataColorSpace, + pub(crate) is_linear: bool, +} + +impl< + T: Copy + AsPrimitive + Default + PointeeSizeExpressible, + U: AsPrimitive, + const LAYOUT: u8, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + const BINS: usize, + const BARYCENTRIC_BINS: usize, +> TransformLut3x4 +where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, +{ + #[inline(never)] + fn transform_chunk( + &self, + src: &[T], + dst: &mut [T], + interpolator: Box, + ) { + let cn = Layout::from(LAYOUT); + let channels = cn.channels(); + + let value_scale = ((1 << BIT_DEPTH) - 1) as f32; + + for (src, dst) in src.chunks_exact(channels).zip(dst.chunks_exact_mut(4)) { + let x = <() as LutBarycentricReduction>::reduce::( + src[cn.r_i()], + ); + let y = <() as LutBarycentricReduction>::reduce::( + src[cn.g_i()], + ); + let z = <() as LutBarycentricReduction>::reduce::( + src[cn.b_i()], + ); + + let v = interpolator.inter4( + &self.lut, + &self.weights[x.as_()], + &self.weights[y.as_()], + &self.weights[z.as_()], + ); + if T::FINITE { + let r = v * value_scale + 0.5; + dst[0] = r.v[0].min(value_scale).max(0.).as_(); + dst[1] = r.v[1].min(value_scale).max(0.).as_(); + dst[2] = r.v[2].min(value_scale).max(0.).as_(); + dst[3] = r.v[3].min(value_scale).max(0.).as_(); + } else { + dst[0] = v.v[0].as_(); + dst[1] = v.v[1].as_(); + dst[2] = v.v[2].as_(); + dst[3] = v.v[3].as_(); + } + } + } +} + +impl< + T: Copy + AsPrimitive + Default + PointeeSizeExpressible, + U: AsPrimitive, + const LAYOUT: u8, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + const BINS: usize, + const BARYCENTRIC_BINS: usize, +> TransformExecutor + for TransformLut3x4 +where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + let cn = Layout::from(LAYOUT); + let channels = cn.channels(); + if src.len() % channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % 4 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + let src_chunks = src.len() / channels; + let dst_chunks = dst.len() / 4; + if src_chunks != dst_chunks { + return Err(CmsError::LaneSizeMismatch); + } + + if self.color_space == DataColorSpace::Lab + || (self.is_linear && self.color_space == DataColorSpace::Rgb) + || self.color_space == DataColorSpace::Xyz + { + use crate::conversions::interpolator::Trilinear; + self.transform_chunk(src, dst, Box::new(Trilinear:: {})); + } else { + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + use crate::conversions::interpolator::Tetrahedral; + self.transform_chunk(src, dst, Box::new(Tetrahedral:: {})); + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + use crate::conversions::interpolator::Pyramidal; + self.transform_chunk(src, dst, Box::new(Pyramidal:: {})); + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + use crate::conversions::interpolator::Prismatic; + self.transform_chunk(src, dst, Box::new(Prismatic:: {})); + } + InterpolationMethod::Linear => { + use crate::conversions::interpolator::Trilinear; + self.transform_chunk(src, dst, Box::new(Trilinear:: {})); + } + } + } + + Ok(()) + } +} + +pub(crate) fn make_transform_3x4< + T: Copy + AsPrimitive + Default + PointeeSizeExpressible + 'static + Send + Sync, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, +>( + layout: Layout, + lut: Vec, + options: TransformOptions, + color_space: DataColorSpace, + is_linear: bool, +) -> Box + Sync + Send> +where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, + (): LutBarycentricReduction, +{ + match layout { + Layout::Rgb => match options.barycentric_weight_scale { + BarycentricWeightScale::Low => Box::new(TransformLut3x4::< + T, + u8, + { Layout::Rgb as u8 }, + GRID_SIZE, + BIT_DEPTH, + 256, + 256, + > { + lut, + _phantom: PhantomData, + _phantom1: PhantomData, + interpolation_method: options.interpolation_method, + weights: BarycentricWeight::::create_ranged_256::(), + color_space, + is_linear, + }), + #[cfg(feature = "options")] + BarycentricWeightScale::High => Box::new(TransformLut3x4::< + T, + u16, + { Layout::Rgb as u8 }, + GRID_SIZE, + BIT_DEPTH, + 65536, + 65536, + > { + lut, + _phantom: PhantomData, + _phantom1: PhantomData, + interpolation_method: options.interpolation_method, + weights: BarycentricWeight::::create_binned::(), + color_space, + is_linear, + }), + }, + Layout::Rgba => match options.barycentric_weight_scale { + BarycentricWeightScale::Low => Box::new(TransformLut3x4::< + T, + u8, + { Layout::Rgba as u8 }, + GRID_SIZE, + BIT_DEPTH, + 256, + 256, + > { + lut, + _phantom: PhantomData, + _phantom1: PhantomData, + interpolation_method: options.interpolation_method, + weights: BarycentricWeight::::create_ranged_256::(), + color_space, + is_linear, + }), + #[cfg(feature = "options")] + BarycentricWeightScale::High => Box::new(TransformLut3x4::< + T, + u16, + { Layout::Rgba as u8 }, + GRID_SIZE, + BIT_DEPTH, + 65536, + 65536, + > { + lut, + _phantom: PhantomData, + _phantom1: PhantomData, + interpolation_method: options.interpolation_method, + weights: BarycentricWeight::::create_binned::(), + color_space, + is_linear, + }), + }, + _ => unimplemented!(), + } +} diff --git a/deps/moxcms/src/conversions/transform_lut4_to_3.rs b/deps/moxcms/src/conversions/transform_lut4_to_3.rs new file mode 100644 index 0000000..247426a --- /dev/null +++ b/deps/moxcms/src/conversions/transform_lut4_to_3.rs @@ -0,0 +1,351 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::interpolator::*; +use crate::conversions::lut_transforms::Lut4x3Factory; +use crate::math::{FusedMultiplyAdd, FusedMultiplyNegAdd, m_clamp}; +use crate::{ + BarycentricWeightScale, CmsError, DataColorSpace, InterpolationMethod, Layout, + PointeeSizeExpressible, TransformExecutor, TransformOptions, Vector3f, +}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +pub(crate) trait Vector3fCmykLerp { + fn interpolate(a: Vector3f, b: Vector3f, t: f32, scale: f32) -> Vector3f; +} + +#[allow(unused)] +#[derive(Copy, Clone, Default)] +struct DefaultVector3fLerp; + +impl Vector3fCmykLerp for DefaultVector3fLerp { + #[inline(always)] + fn interpolate(a: Vector3f, b: Vector3f, t: f32, scale: f32) -> Vector3f { + let t = Vector3f::from(t); + let inter = a.neg_mla(a, t).mla(b, t); + let mut new_vec = Vector3f::from(0.5).mla(inter, Vector3f::from(scale)); + new_vec.v[0] = m_clamp(new_vec.v[0], 0.0, scale); + new_vec.v[1] = m_clamp(new_vec.v[1], 0.0, scale); + new_vec.v[2] = m_clamp(new_vec.v[2], 0.0, scale); + new_vec + } +} + +#[allow(unused)] +#[derive(Copy, Clone, Default)] +pub(crate) struct NonFiniteVector3fLerp; + +impl Vector3fCmykLerp for NonFiniteVector3fLerp { + #[inline(always)] + fn interpolate(a: Vector3f, b: Vector3f, t: f32, _: f32) -> Vector3f { + let t = Vector3f::from(t); + a.neg_mla(a, t).mla(b, t) + } +} + +#[allow(unused)] +#[derive(Copy, Clone, Default)] +pub(crate) struct NonFiniteVector3fLerpUnbound; + +impl Vector3fCmykLerp for NonFiniteVector3fLerpUnbound { + #[inline(always)] + fn interpolate(a: Vector3f, b: Vector3f, t: f32, _: f32) -> Vector3f { + let t = Vector3f::from(t); + a.neg_mla(a, t).mla(b, t) + } +} + +#[allow(unused)] +struct TransformLut4To3< + T, + U, + const LAYOUT: u8, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + const BINS: usize, + const BARYCENTRIC_BINS: usize, +> { + lut: Vec, + _phantom: PhantomData, + _phantom1: PhantomData, + interpolation_method: InterpolationMethod, + weights: Box<[BarycentricWeight; BINS]>, + color_space: DataColorSpace, + is_linear: bool, +} + +#[allow(unused)] +impl< + T: Copy + AsPrimitive + Default, + U: AsPrimitive, + const LAYOUT: u8, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + const BINS: usize, + const BARYCENTRIC_BINS: usize, +> TransformLut4To3 +where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, +{ + #[inline(never)] + fn transform_chunk( + &self, + src: &[T], + dst: &mut [T], + interpolator: Box, + ) { + let cn = Layout::from(LAYOUT); + let channels = cn.channels(); + let grid_size = GRID_SIZE as i32; + let grid_size3 = grid_size * grid_size * grid_size; + + let value_scale = ((1 << BIT_DEPTH) - 1) as f32; + let max_value = ((1 << BIT_DEPTH) - 1u32).as_(); + + for (src, dst) in src.chunks_exact(4).zip(dst.chunks_exact_mut(channels)) { + let c = <() as LutBarycentricReduction>::reduce::( + src[0], + ); + let m = <() as LutBarycentricReduction>::reduce::( + src[1], + ); + let y = <() as LutBarycentricReduction>::reduce::( + src[2], + ); + let k = <() as LutBarycentricReduction>::reduce::( + src[3], + ); + + let k_weights = self.weights[k.as_()]; + + let w: i32 = k_weights.x; + let w_n: i32 = k_weights.x_n; + let t: f32 = k_weights.w; + + let table1 = &self.lut[(w * grid_size3 * 3) as usize..]; + let table2 = &self.lut[(w_n * grid_size3 * 3) as usize..]; + + let r1 = interpolator.inter3( + table1, + &self.weights[c.as_()], + &self.weights[m.as_()], + &self.weights[y.as_()], + ); + let r2 = interpolator.inter3( + table2, + &self.weights[c.as_()], + &self.weights[m.as_()], + &self.weights[y.as_()], + ); + let r = Interpolation::interpolate(r1, r2, t, value_scale); + dst[cn.r_i()] = r.v[0].as_(); + dst[cn.g_i()] = r.v[1].as_(); + dst[cn.b_i()] = r.v[2].as_(); + if channels == 4 { + dst[cn.a_i()] = max_value; + } + } + } +} + +#[allow(unused)] +impl< + T: Copy + AsPrimitive + Default + PointeeSizeExpressible, + U: AsPrimitive, + const LAYOUT: u8, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + const BINS: usize, + const BARYCENTRIC_BINS: usize, +> TransformExecutor + for TransformLut4To3 +where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, +{ + fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> { + let cn = Layout::from(LAYOUT); + let channels = cn.channels(); + if src.len() % 4 != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + if dst.len() % channels != 0 { + return Err(CmsError::LaneMultipleOfChannels); + } + let src_chunks = src.len() / 4; + let dst_chunks = dst.len() / channels; + if src_chunks != dst_chunks { + return Err(CmsError::LaneSizeMismatch); + } + + if self.color_space == DataColorSpace::Lab + || (self.is_linear && self.color_space == DataColorSpace::Rgb) + || self.color_space == DataColorSpace::Xyz + { + if T::FINITE { + self.transform_chunk::( + src, + dst, + Box::new(Trilinear:: {}), + ); + } else { + self.transform_chunk::( + src, + dst, + Box::new(Trilinear:: {}), + ); + } + } else { + match self.interpolation_method { + #[cfg(feature = "options")] + InterpolationMethod::Tetrahedral => { + if T::FINITE { + self.transform_chunk::( + src, + dst, + Box::new(Tetrahedral:: {}), + ); + } else { + self.transform_chunk::( + src, + dst, + Box::new(Tetrahedral:: {}), + ); + } + } + #[cfg(feature = "options")] + InterpolationMethod::Pyramid => { + if T::FINITE { + self.transform_chunk::( + src, + dst, + Box::new(Pyramidal:: {}), + ); + } else { + self.transform_chunk::( + src, + dst, + Box::new(Pyramidal:: {}), + ); + } + } + #[cfg(feature = "options")] + InterpolationMethod::Prism => { + if T::FINITE { + self.transform_chunk::( + src, + dst, + Box::new(Prismatic:: {}), + ); + } else { + self.transform_chunk::( + src, + dst, + Box::new(Prismatic:: {}), + ); + } + } + InterpolationMethod::Linear => { + if T::FINITE { + self.transform_chunk::( + src, + dst, + Box::new(Trilinear:: {}), + ); + } else { + self.transform_chunk::( + src, + dst, + Box::new(Trilinear:: {}), + ); + } + } + } + } + + Ok(()) + } +} + +#[allow(dead_code)] +pub(crate) struct DefaultLut4x3Factory {} + +#[allow(dead_code)] +impl Lut4x3Factory for DefaultLut4x3Factory { + fn make_transform_4x3< + T: Copy + AsPrimitive + Default + PointeeSizeExpressible + 'static + Send + Sync, + const LAYOUT: u8, + const GRID_SIZE: usize, + const BIT_DEPTH: usize, + >( + lut: Vec, + options: TransformOptions, + color_space: DataColorSpace, + is_linear: bool, + ) -> Box + Sync + Send> + where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, + (): LutBarycentricReduction, + { + match options.barycentric_weight_scale { + BarycentricWeightScale::Low => { + Box::new( + TransformLut4To3:: { + lut, + _phantom: PhantomData, + _phantom1: PhantomData, + interpolation_method: options.interpolation_method, + weights: BarycentricWeight::::create_ranged_256::(), + color_space, + is_linear, + }, + ) + } + #[cfg(feature = "options")] + BarycentricWeightScale::High => { + Box::new( + TransformLut4To3:: { + lut, + _phantom: PhantomData, + _phantom1: PhantomData, + interpolation_method: options.interpolation_method, + weights: BarycentricWeight::::create_binned::(), + color_space, + is_linear, + }, + ) + } + } + } +} diff --git a/deps/moxcms/src/conversions/xyz_lab.rs b/deps/moxcms/src/conversions/xyz_lab.rs new file mode 100644 index 0000000..1e54749 --- /dev/null +++ b/deps/moxcms/src/conversions/xyz_lab.rs @@ -0,0 +1,61 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::{CmsError, InPlaceStage, Lab, Xyz}; + +#[derive(Default)] +pub(crate) struct StageLabToXyz {} + +impl InPlaceStage for StageLabToXyz { + fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> { + for dst in dst.chunks_exact_mut(3) { + let lab = Lab::new(dst[0], dst[1], dst[2]); + let xyz = lab.to_pcs_xyz(); + dst[0] = xyz.x; + dst[1] = xyz.y; + dst[2] = xyz.z; + } + Ok(()) + } +} + +#[derive(Default)] +pub(crate) struct StageXyzToLab {} + +impl InPlaceStage for StageXyzToLab { + fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> { + for dst in dst.chunks_exact_mut(3) { + let xyz = Xyz::new(dst[0], dst[1], dst[2]); + let lab = Lab::from_pcs_xyz(xyz); + dst[0] = lab.l; + dst[1] = lab.a; + dst[2] = lab.b; + } + Ok(()) + } +} diff --git a/deps/moxcms/src/dat.rs b/deps/moxcms/src/dat.rs new file mode 100644 index 0000000..4962017 --- /dev/null +++ b/deps/moxcms/src/dat.rs @@ -0,0 +1,154 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::CmsError; +use crate::writer::write_u16_be; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[repr(C)] +#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Default)] +pub struct ColorDateTime { + pub year: u16, + pub month: u16, + pub day_of_the_month: u16, + pub hours: u16, + pub minutes: u16, + pub seconds: u16, +} + +fn is_leap(year: i32) -> bool { + (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) +} + +fn days_in_month(year: i32, month: i32) -> i32 { + match month { + 1 => 31, + 2 => { + if is_leap(year) { + 29 + } else { + 28 + } + } + 3 => 31, + 4 => 30, + 5 => 31, + 6 => 30, + 7 => 31, + 8 => 31, + 9 => 30, + 10 => 31, + 11 => 30, + 12 => 31, + _ => unreachable!("Unknown month"), + } +} + +impl ColorDateTime { + /// Parses slice for date time + pub fn new_from_slice(slice: &[u8]) -> Result { + if slice.len() != 12 { + return Err(CmsError::InvalidProfile); + } + let year = u16::from_be_bytes([slice[0], slice[1]]); + let month = u16::from_be_bytes([slice[2], slice[3]]); + let day_of_the_month = u16::from_be_bytes([slice[4], slice[5]]); + let hours = u16::from_be_bytes([slice[6], slice[7]]); + let minutes = u16::from_be_bytes([slice[8], slice[9]]); + let seconds = u16::from_be_bytes([slice[10], slice[11]]); + Ok(ColorDateTime { + year, + month, + day_of_the_month, + hours, + minutes, + seconds, + }) + } + + /// Creates a new `ColorDateTime` from the current system time (UTC) + pub fn now() -> Self { + let now = match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(v) => v, + Err(_) => return Self::default(), + }; + let mut days = (now.as_secs() / 86_400) as i64; + let secs_of_day = (now.as_secs() % 86_400) as i64; + + let mut year = 1970; + loop { + let year_days = if is_leap(year) { 366 } else { 365 }; + if days >= year_days { + days -= year_days; + year += 1; + } else { + break; + } + } + + let mut month = 1; + loop { + let mdays = days_in_month(year, month); + if days >= mdays as i64 { + days -= mdays as i64; + month += 1; + } else { + break; + } + } + let day = days + 1; // days from zero based to 1 base + + let hour = secs_of_day / 3600; + let min = (secs_of_day % 3600) / 60; + let sec = secs_of_day % 60; + Self { + year: year as u16, + month: month as u16, + day_of_the_month: day as u16, + hours: hour as u16, + minutes: min as u16, + seconds: sec as u16, + } + } + + #[inline] + pub(crate) fn encode(&self, into: &mut Vec) { + let year = self.year; + let month = self.month; + let day_of_the_month = self.day_of_the_month; + let hours = self.hours; + let minutes = self.minutes; + let seconds = self.seconds; + write_u16_be(into, year); + write_u16_be(into, month); + write_u16_be(into, day_of_the_month); + write_u16_be(into, hours); + write_u16_be(into, minutes); + write_u16_be(into, seconds); + } +} diff --git a/deps/moxcms/src/defaults.rs b/deps/moxcms/src/defaults.rs new file mode 100644 index 0000000..ddb0312 --- /dev/null +++ b/deps/moxcms/src/defaults.rs @@ -0,0 +1,541 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::chad::BRADFORD_D; +use crate::cicp::create_rec709_parametric; +use crate::trc::{ToneReprCurve, curve_from_gamma}; +use crate::{ + CicpColorPrimaries, CicpProfile, ColorPrimaries, ColorProfile, DataColorSpace, + LocalizableString, Matrix3d, MatrixCoefficients, ProfileClass, ProfileText, RenderingIntent, + TransferCharacteristics, XyY, +}; +use pxfm::{copysignk, exp, floor, pow}; + +/// From lcms: `cmsWhitePointFromTemp` +/// tempK must be >= 4000. and <= 25000. +/// Invalid values of tempK will return +/// (x,y,Y) = (-1.0, -1.0, -1.0) +/// similar to argyll: `icx_DTEMP2XYZ()` +const fn white_point_from_temperature(temp_k: i32) -> XyY { + let mut white_point = XyY { + x: 0., + y: 0., + yb: 0., + }; + // No optimization provided. + let temp_k = temp_k as f64; // Square + let temp_k2 = temp_k * temp_k; // Cube + let temp_k3 = temp_k2 * temp_k; + // For correlated color temperature (T) between 4000K and 7000K: + let x = if temp_k > 4000.0 && temp_k <= 7000.0 { + -4.6070 * (1E9 / temp_k3) + 2.9678 * (1E6 / temp_k2) + 0.09911 * (1E3 / temp_k) + 0.244063 + } else if temp_k > 7000.0 && temp_k <= 25000.0 { + -2.0064 * (1E9 / temp_k3) + 1.9018 * (1E6 / temp_k2) + 0.24748 * (1E3 / temp_k) + 0.237040 + } else { + // or for correlated color temperature (T) between 7000K and 25000K: + // Invalid tempK + white_point.x = -1.0; + white_point.y = -1.0; + white_point.yb = -1.0; + debug_assert!(false, "invalid temp"); + return white_point; + }; + // Obtain y(x) + let y = -3.000 * (x * x) + 2.870 * x - 0.275; + // wave factors (not used, but here for futures extensions) + // let M1 = (-1.3515 - 1.7703*x + 5.9114 *y)/(0.0241 + 0.2562*x - 0.7341*y); + // let M2 = (0.0300 - 31.4424*x + 30.0717*y)/(0.0241 + 0.2562*x - 0.7341*y); + // Fill white_point struct + white_point.x = x; + white_point.y = y; + white_point.yb = 1.0; + white_point +} + +pub const WHITE_POINT_D50: XyY = white_point_from_temperature(5003); +pub const WHITE_POINT_D60: XyY = white_point_from_temperature(6000); +pub const WHITE_POINT_D65: XyY = white_point_from_temperature(6504); +pub const WHITE_POINT_DCI_P3: XyY = white_point_from_temperature(6300); + +// https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2100-2-201807-I!!PDF-F.pdf +// Perceptual Quantization / SMPTE standard ST.2084 +#[inline] +const fn pq_curve(x: f64) -> f64 { + const M1: f64 = 2610.0 / 16384.0; + const M2: f64 = (2523.0 / 4096.0) * 128.0; + const C1: f64 = 3424.0 / 4096.0; + const C2: f64 = (2413.0 / 4096.0) * 32.0; + const C3: f64 = (2392.0 / 4096.0) * 32.0; + + if x == 0.0 { + return 0.0; + } + let sign = x; + let x = x.abs(); + + let xpo = pow(x, 1.0 / M2); + let num = (xpo - C1).max(0.0); + let den = C2 - C3 * xpo; + let res = pow(num / den, 1.0 / M1); + + copysignk(res, sign) +} + +pub(crate) const fn build_trc_table_pq() -> [u16; 4096] { + let mut table = [0u16; 4096]; + + const NUM_ENTRIES: usize = 4096; + let mut i = 0usize; + while i < NUM_ENTRIES { + let x: f64 = i as f64 / (NUM_ENTRIES - 1) as f64; + let y: f64 = pq_curve(x); + let mut output: f64; + output = y * 65535.0 + 0.5; + if output > 65535.0 { + output = 65535.0 + } + if output < 0.0 { + output = 0.0 + } + table[i] = floor(output) as u16; + i += 1; + } + table +} + +pub(crate) const fn build_trc_table_hlg() -> [u16; 4096] { + let mut table = [0u16; 4096]; + + const NUM_ENTRIES: usize = 4096; + let mut i = 0usize; + while i < NUM_ENTRIES { + let x: f64 = i as f64 / (NUM_ENTRIES - 1) as f64; + let y: f64 = hlg_curve(x); + let mut output: f64; + output = y * 65535.0 + 0.5; + if output > 65535.0 { + output = 65535.0 + } + if output < 0.0 { + output = 0.0 + } + table[i] = floor(output) as u16; + i += 1; + } + table +} + +// https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2100-2-201807-I!!PDF-F.pdf +// Hybrid Log-Gamma +const fn hlg_curve(x: f64) -> f64 { + const BETA: f64 = 0.04; + const RA: f64 = 5.591816309728916; // 1.0 / A where A = 0.17883277 + const B: f64 = 0.28466892; // 1.0 - 4.0 * A + const C: f64 = 0.5599107295; // 0,5 –aln(4a) + + let e = (x * (1.0 - BETA) + BETA).max(0.0); + + if e == 0.0 { + return 0.0; + } + + let sign = e.abs(); + + let res = if e <= 0.5 { + e * e / 3.0 + } else { + (exp((e - C) * RA) + B) / 12.0 + }; + + copysignk(res, sign) +} + +/// Perceptual Quantizer Lookup table +pub const PQ_LUT_TABLE: [u16; 4096] = build_trc_table_pq(); +/// Hybrid Log Gamma Lookup table +pub const HLG_LUT_TABLE: [u16; 4096] = build_trc_table_hlg(); + +impl ColorProfile { + const SRGB_COLORANTS: Matrix3d = + ColorProfile::colorants_matrix(WHITE_POINT_D65, ColorPrimaries::BT_709); + + const DISPLAY_P3_COLORANTS: Matrix3d = + ColorProfile::colorants_matrix(WHITE_POINT_D65, ColorPrimaries::SMPTE_432); + + const ADOBE_RGB_COLORANTS: Matrix3d = + ColorProfile::colorants_matrix(WHITE_POINT_D65, ColorPrimaries::ADOBE_RGB); + + const DCI_P3_COLORANTS: Matrix3d = + ColorProfile::colorants_matrix(WHITE_POINT_DCI_P3, ColorPrimaries::DCI_P3); + + const PRO_PHOTO_RGB_COLORANTS: Matrix3d = + ColorProfile::colorants_matrix(WHITE_POINT_D50, ColorPrimaries::PRO_PHOTO_RGB); + + const BT2020_COLORANTS: Matrix3d = + ColorProfile::colorants_matrix(WHITE_POINT_D65, ColorPrimaries::BT_2020); + + const ACES_2065_1_COLORANTS: Matrix3d = + ColorProfile::colorants_matrix(WHITE_POINT_D60, ColorPrimaries::ACES_2065_1); + + const ACES_CG_COLORANTS: Matrix3d = + ColorProfile::colorants_matrix(WHITE_POINT_D60, ColorPrimaries::ACES_CG); + + #[inline] + fn basic_rgb_profile() -> ColorProfile { + ColorProfile { + profile_class: ProfileClass::DisplayDevice, + rendering_intent: RenderingIntent::Perceptual, + color_space: DataColorSpace::Rgb, + pcs: DataColorSpace::Xyz, + chromatic_adaptation: Some(BRADFORD_D), + white_point: WHITE_POINT_D50.to_xyzd(), + ..Default::default() + } + } + + /// Creates new profile from CICP + pub fn new_from_cicp(cicp_color_primaries: CicpProfile) -> ColorProfile { + let mut basic = ColorProfile::basic_rgb_profile(); + basic.update_rgb_colorimetry_from_cicp(cicp_color_primaries); + basic + } + + /// Creates new sRGB profile + pub fn new_srgb() -> ColorProfile { + let mut profile = ColorProfile::basic_rgb_profile(); + profile.update_colorants(ColorProfile::SRGB_COLORANTS); + + let curve = + ToneReprCurve::Parametric(vec![2.4, 1. / 1.055, 0.055 / 1.055, 1. / 12.92, 0.04045]); + profile.red_trc = Some(curve.clone()); + profile.blue_trc = Some(curve.clone()); + profile.green_trc = Some(curve); + profile.media_white_point = Some(WHITE_POINT_D65.to_xyzd()); + profile.cicp = Some(CicpProfile { + color_primaries: CicpColorPrimaries::Bt709, + transfer_characteristics: TransferCharacteristics::Srgb, + matrix_coefficients: MatrixCoefficients::Bt709, + full_range: false, + }); + profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "sRGB IEC61966-2.1".to_string(), + )])); + profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Public Domain".to_string(), + )])); + profile + } + + /// Creates new Adobe RGB profile + pub fn new_adobe_rgb() -> ColorProfile { + let mut profile = ColorProfile::basic_rgb_profile(); + profile.update_colorants(ColorProfile::ADOBE_RGB_COLORANTS); + + let curve = curve_from_gamma(2.19921875f32); + profile.red_trc = Some(curve.clone()); + profile.blue_trc = Some(curve.clone()); + profile.green_trc = Some(curve); + profile.media_white_point = Some(WHITE_POINT_D65.to_xyzd()); + profile.white_point = WHITE_POINT_D50.to_xyzd(); + profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Adobe RGB 1998".to_string(), + )])); + profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Public Domain".to_string(), + )])); + profile + } + + /// Creates new Display P3 profile + pub fn new_display_p3() -> ColorProfile { + let mut profile = ColorProfile::basic_rgb_profile(); + profile.update_colorants(ColorProfile::DISPLAY_P3_COLORANTS); + + let curve = + ToneReprCurve::Parametric(vec![2.4, 1. / 1.055, 0.055 / 1.055, 1. / 12.92, 0.04045]); + profile.red_trc = Some(curve.clone()); + profile.blue_trc = Some(curve.clone()); + profile.green_trc = Some(curve); + profile.media_white_point = Some(WHITE_POINT_D65.to_xyzd()); + profile.cicp = Some(CicpProfile { + color_primaries: CicpColorPrimaries::Smpte431, + transfer_characteristics: TransferCharacteristics::Srgb, + matrix_coefficients: MatrixCoefficients::Bt709, + full_range: false, + }); + profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Display P3".to_string(), + )])); + profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Public Domain".to_string(), + )])); + profile + } + + /// Creates new Display P3 PQ profile + pub fn new_display_p3_pq() -> ColorProfile { + let mut profile = ColorProfile::basic_rgb_profile(); + profile.update_colorants(ColorProfile::DISPLAY_P3_COLORANTS); + + let curve = ToneReprCurve::Lut(PQ_LUT_TABLE.to_vec()); + + profile.red_trc = Some(curve.clone()); + profile.blue_trc = Some(curve.clone()); + profile.green_trc = Some(curve); + profile.media_white_point = Some(WHITE_POINT_D65.to_xyzd()); + profile.cicp = Some(CicpProfile { + color_primaries: CicpColorPrimaries::Smpte431, + transfer_characteristics: TransferCharacteristics::Smpte2084, + matrix_coefficients: MatrixCoefficients::Bt709, + full_range: false, + }); + profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Display P3 PQ".to_string(), + )])); + profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Public Domain".to_string(), + )])); + profile + } + + /// Creates new DCI P3 profile + pub fn new_dci_p3() -> ColorProfile { + let mut profile = ColorProfile::basic_rgb_profile(); + profile.update_colorants(ColorProfile::DCI_P3_COLORANTS); + + let curve = curve_from_gamma(2.6f32); + profile.red_trc = Some(curve.clone()); + profile.blue_trc = Some(curve.clone()); + profile.green_trc = Some(curve); + profile.media_white_point = Some(WHITE_POINT_DCI_P3.to_xyzd()); + profile.cicp = Some(CicpProfile { + color_primaries: CicpColorPrimaries::Smpte432, + transfer_characteristics: TransferCharacteristics::Srgb, + matrix_coefficients: MatrixCoefficients::Bt709, + full_range: false, + }); + profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "DCI P3".to_string(), + )])); + profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Public Domain".to_string(), + )])); + profile + } + + /// Creates new ProPhoto RGB profile + pub fn new_pro_photo_rgb() -> ColorProfile { + let mut profile = ColorProfile::basic_rgb_profile(); + profile.update_colorants(ColorProfile::PRO_PHOTO_RGB_COLORANTS); + + let curve = curve_from_gamma(1.8f32); + profile.red_trc = Some(curve.clone()); + profile.blue_trc = Some(curve.clone()); + profile.green_trc = Some(curve); + profile.media_white_point = Some(WHITE_POINT_D50.to_xyzd()); + profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "ProPhoto RGB".to_string(), + )])); + profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Public Domain".to_string(), + )])); + profile + } + + /// Creates new Bt.2020 profile + pub fn new_bt2020() -> ColorProfile { + let mut profile = ColorProfile::basic_rgb_profile(); + profile.update_colorants(ColorProfile::BT2020_COLORANTS); + + let curve = ToneReprCurve::Parametric(create_rec709_parametric().to_vec()); + profile.red_trc = Some(curve.clone()); + profile.blue_trc = Some(curve.clone()); + profile.green_trc = Some(curve); + profile.media_white_point = Some(WHITE_POINT_D65.to_xyzd()); + profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Rec.2020".to_string(), + )])); + profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Public Domain".to_string(), + )])); + profile + } + + /// Creates new Bt.2020 PQ profile + pub fn new_bt2020_pq() -> ColorProfile { + let mut profile = ColorProfile::basic_rgb_profile(); + profile.update_colorants(ColorProfile::BT2020_COLORANTS); + + let curve = ToneReprCurve::Lut(PQ_LUT_TABLE.to_vec()); + + profile.red_trc = Some(curve.clone()); + profile.blue_trc = Some(curve.clone()); + profile.green_trc = Some(curve); + profile.media_white_point = Some(WHITE_POINT_D65.to_xyzd()); + profile.cicp = Some(CicpProfile { + color_primaries: CicpColorPrimaries::Bt2020, + transfer_characteristics: TransferCharacteristics::Smpte2084, + matrix_coefficients: MatrixCoefficients::Bt709, + full_range: false, + }); + profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Rec.2020 PQ".to_string(), + )])); + profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Public Domain".to_string(), + )])); + profile + } + + /// Creates new Bt.2020 HLG profile + pub fn new_bt2020_hlg() -> ColorProfile { + let mut profile = ColorProfile::basic_rgb_profile(); + profile.update_colorants(ColorProfile::BT2020_COLORANTS); + + let curve = ToneReprCurve::Lut(HLG_LUT_TABLE.to_vec()); + + profile.red_trc = Some(curve.clone()); + profile.blue_trc = Some(curve.clone()); + profile.green_trc = Some(curve); + profile.media_white_point = Some(WHITE_POINT_D65.to_xyzd()); + profile.cicp = Some(CicpProfile { + color_primaries: CicpColorPrimaries::Bt2020, + transfer_characteristics: TransferCharacteristics::Hlg, + matrix_coefficients: MatrixCoefficients::Bt709, + full_range: false, + }); + profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Rec.2020 HLG".to_string(), + )])); + profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Public Domain".to_string(), + )])); + profile + } + + /// Creates new Monochrome profile + pub fn new_gray_with_gamma(gamma: f32) -> ColorProfile { + ColorProfile { + gray_trc: Some(curve_from_gamma(gamma)), + profile_class: ProfileClass::DisplayDevice, + rendering_intent: RenderingIntent::Perceptual, + color_space: DataColorSpace::Gray, + media_white_point: Some(WHITE_POINT_D65.to_xyzd()), + white_point: WHITE_POINT_D50.to_xyzd(), + chromatic_adaptation: Some(BRADFORD_D), + copyright: Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Public Domain".to_string(), + )])), + ..Default::default() + } + } + + /// Creates new ACES 2065-1/AP0 profile + pub fn new_aces_aces_2065_1_linear() -> ColorProfile { + let mut profile = ColorProfile::basic_rgb_profile(); + profile.update_colorants(ColorProfile::ACES_2065_1_COLORANTS); + + let curve = ToneReprCurve::Lut(vec![]); + profile.red_trc = Some(curve.clone()); + profile.blue_trc = Some(curve.clone()); + profile.green_trc = Some(curve); + profile.media_white_point = Some(WHITE_POINT_D60.to_xyzd()); + profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "ACES 2065-1".to_string(), + )])); + profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Public Domain".to_string(), + )])); + profile + } + + /// Creates new ACEScg profile + pub fn new_aces_cg_linear() -> ColorProfile { + let mut profile = ColorProfile::basic_rgb_profile(); + profile.update_colorants(ColorProfile::ACES_CG_COLORANTS); + + let curve = ToneReprCurve::Lut(vec![]); + profile.red_trc = Some(curve.clone()); + profile.blue_trc = Some(curve.clone()); + profile.green_trc = Some(curve); + profile.media_white_point = Some(WHITE_POINT_D60.to_xyzd()); + profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "ACEScg/AP1".to_string(), + )])); + profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new( + "en".to_string(), + "US".to_string(), + "Public Domain".to_string(), + )])); + profile + } +} diff --git a/deps/moxcms/src/dt_ucs.rs b/deps/moxcms/src/dt_ucs.rs new file mode 100644 index 0000000..5eab617 --- /dev/null +++ b/deps/moxcms/src/dt_ucs.rs @@ -0,0 +1,359 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::Xyz; +use crate::mlaf::mlaf; +use pxfm::{f_atan2f, f_powf, f_sincosf}; + +/// Darktable UCS JCH ( Darktable Uniform Color Space ) +#[derive(Copy, Clone, PartialOrd, PartialEq, Debug)] +pub struct DtUchJch { + pub j: f32, + pub c: f32, + pub h: f32, +} + +/// Darktable UCS HSB ( Darktable Uniform Color Space ) +#[derive(Copy, Clone, PartialOrd, PartialEq, Debug)] +pub struct DtUchHsb { + pub h: f32, + pub s: f32, + pub b: f32, +} + +/// Darktable HCB ( Darktable Uniform Color Space ) +#[derive(Copy, Clone, PartialOrd, PartialEq, Debug)] +pub struct DtUchHcb { + pub h: f32, + pub c: f32, + pub b: f32, +} + +const DT_UCS_L_STAR_RANGE: f32 = 2.098883786377; + +#[inline] +fn y_to_dt_ucs_l_star(y: f32) -> f32 { + let y_hat = f_powf(y, 0.631651345306265); + DT_UCS_L_STAR_RANGE * y_hat / (y_hat + 1.12426773749357) +} + +#[inline] +fn dt_ucs_l_star_to_y(x: f32) -> f32 { + f_powf( + 1.12426773749357 * x / (DT_UCS_L_STAR_RANGE - x), + 1.5831518565279648, + ) +} + +const L_WHITE: f32 = 0.98805060; + +#[inline] +fn dt_ucs_luv_to_ucs_jch( + l_star: f32, + l_white: f32, + u_star_prime: f32, + v_star_prime: f32, +) -> DtUchJch { + let m2: f32 = mlaf(u_star_prime * u_star_prime, v_star_prime, v_star_prime); // square of colorfulness M + + // should be JCH[0] = powf(L_star / L_white), cz) but we treat only the case where cz = 1 + let j = l_star / l_white; + let c = + 15.932993652962535 * f_powf(l_star, 0.6523997524738018) * f_powf(m2, 0.6007557017508491) + / l_white; + let h = f_atan2f(v_star_prime, u_star_prime); + DtUchJch::new(j, c, h) +} + +#[inline] +fn dt_ucs_xy_to_uv(x: f32, y: f32) -> (f32, f32) { + const X_C: [f32; 3] = [-0.783941002840055, 0.745273540913283, 0.318707282433486]; + const Y_C: [f32; 3] = [0.277512987809202, -0.205375866083878, 2.16743692732158]; + const BIAS: [f32; 3] = [0.153836578598858, -0.165478376301988, 0.291320554395942]; + + let mut u_c = mlaf(mlaf(BIAS[0], Y_C[0], y), X_C[0], x); + let mut v_c = mlaf(mlaf(BIAS[1], Y_C[1], y), X_C[1], x); + let d_c = mlaf(mlaf(BIAS[2], Y_C[2], y), X_C[2], x); + + let div = if d_c >= 0.0 { + d_c.max(f32::MIN) + } else { + d_c.min(-f32::MIN) + }; + u_c /= div; + v_c /= div; + + const STAR_C: [f32; 2] = [1.39656225667, 1.4513954287]; + const STAR_HF_C: [f32; 2] = [1.49217352929, 1.52488637914]; + + let u_star = STAR_C[0] * u_c / (u_c.abs() + STAR_HF_C[0]); + let v_star = STAR_C[1] * v_c / (v_c.abs() + STAR_HF_C[1]); + + // The following is equivalent to a 2D matrix product + let u_star_prime = mlaf(-1.124983854323892 * u_star, -0.980483721769325, v_star); + let v_star_prime = mlaf(1.86323315098672 * u_star, 1.971853092390862, v_star); + (u_star_prime, v_star_prime) +} + +impl DtUchJch { + #[inline] + pub fn new(j: f32, c: f32, h: f32) -> DtUchJch { + DtUchJch { j, c, h } + } + + #[inline] + pub fn from_xyz(xyz: Xyz) -> DtUchJch { + DtUchJch::from_xyy(xyz.to_xyy()) + } + + #[inline] + pub fn to_xyz(&self) -> Xyz { + let xyy = self.to_xyy(); + Xyz::from_xyy(xyy) + } + + #[inline] + pub fn from_xyy(xyy: [f32; 3]) -> DtUchJch { + let l_star = y_to_dt_ucs_l_star(xyy[2]); + // let l_white = y_to_dt_ucs_l_star(1.); + + let (u_star_prime, v_star_prime) = dt_ucs_xy_to_uv(xyy[0], xyy[1]); + dt_ucs_luv_to_ucs_jch(l_star, L_WHITE, u_star_prime, v_star_prime) + } + + #[inline] + pub fn to_xyy(&self) -> [f32; 3] { + // let l_white: f32 = y_to_dt_ucs_l_star(1.0); + let l_star = (self.j * L_WHITE).max(0.0).min(2.09885); + let m = if l_star != 0. { + f_powf( + self.c * L_WHITE / (15.932993652962535 * f_powf(l_star, 0.6523997524738018)), + 0.8322850678616855, + ) + } else { + 0. + }; + + let sin_cos_h = f_sincosf(self.h); + let u_star_prime = m * sin_cos_h.1; + let v_star_prime = m * sin_cos_h.0; + + // The following is equivalent to a 2D matrix product + let u_star = mlaf( + -5.037522385190711 * u_star_prime, + -2.504856328185843, + v_star_prime, + ); + let v_star = mlaf( + 4.760029407436461 * u_star_prime, + 2.874012963239247, + v_star_prime, + ); + + const F: [f32; 2] = [1.39656225667, 1.4513954287]; + const HF: [f32; 2] = [1.49217352929, 1.52488637914]; + + let u_c = -HF[0] * u_star / (u_star.abs() - F[0]); + let v_c = -HF[1] * v_star / (v_star.abs() - F[1]); + + const U_C: [f32; 3] = [0.167171472114775, -0.150959086409163, 0.940254742367256]; + const V_C: [f32; 3] = [0.141299802443708, -0.155185060382272, 1.000000000000000]; + const BIAS: [f32; 3] = [ + -0.00801531300850582, + -0.00843312433578007, + -0.0256325967652889, + ]; + + let mut x = mlaf(mlaf(BIAS[0], V_C[0], v_c), U_C[0], u_c); + let mut y = mlaf(mlaf(BIAS[1], V_C[1], v_c), U_C[1], u_c); + let d = mlaf(mlaf(BIAS[2], V_C[2], v_c), U_C[2], u_c); + + let div = if d >= 0.0 { + d.max(f32::MIN) + } else { + d.min(-f32::MIN) + }; + x /= div; + y /= div; + let yb = dt_ucs_l_star_to_y(l_star); + [x, y, yb] + } +} + +impl DtUchHsb { + #[inline] + pub fn new(h: f32, s: f32, b: f32) -> DtUchHsb { + DtUchHsb { h, s, b } + } + + #[inline] + pub fn from_jch(jch: DtUchJch) -> DtUchHsb { + let b = jch.j * (f_powf(jch.c, 1.33654221029386) + 1.); + let s = if b > 0. { jch.c / b } else { 0. }; + let h = jch.h; + DtUchHsb::new(h, s, b) + } + + #[inline] + pub fn to_jch(&self) -> DtUchJch { + let h = self.h; + let c = self.s * self.b; + let j = self.b / (f_powf(c, 1.33654221029386) + 1.); + DtUchJch::new(j, c, h) + } +} + +impl DtUchHcb { + #[inline] + pub fn new(h: f32, c: f32, b: f32) -> DtUchHcb { + DtUchHcb { h, c, b } + } + + #[inline] + pub fn from_jch(jch: DtUchJch) -> DtUchHcb { + let b = jch.j * (f_powf(jch.c, 1.33654221029386) + 1.); + let c = jch.c; + let h = jch.h; + DtUchHcb::new(h, c, b) + } + + #[inline] + pub fn to_jch(&self) -> DtUchJch { + let h = self.h; + let c = self.c; + let j = self.b / (f_powf(self.c, 1.33654221029386) + 1.); + DtUchJch::new(j, c, h) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_darktable_ucs_jch() { + let xyy = [0.4, 0.2, 0.5]; + let ucs = DtUchJch::from_xyy(xyy); + let xyy_rev = ucs.to_xyy(); + assert!( + (xyy[0] - xyy_rev[0]).abs() < 1e-5, + "Expected {}, got {}", + xyy[0], + xyy_rev[0] + ); + assert!( + (xyy[1] - xyy_rev[1]).abs() < 1e-5, + "Expected {}, got {}", + xyy[1], + xyy_rev[1] + ); + assert!( + (xyy[2] - xyy_rev[2]).abs() < 1e-5, + "Expected {}, got {}", + xyy[2], + xyy_rev[2] + ); + } + + #[test] + fn test_darktable_hsb() { + let jch = DtUchJch::new(0.3, 0.6, 0.4); + let hsb = DtUchHsb::from_jch(jch); + let r_jch = hsb.to_jch(); + + assert!( + (r_jch.j - jch.j).abs() < 1e-5, + "Expected {}, got {}", + jch.j, + r_jch.j + ); + assert!( + (r_jch.c - jch.c).abs() < 1e-5, + "Expected {}, got {}", + jch.c, + r_jch.c + ); + assert!( + (r_jch.h - jch.h).abs() < 1e-5, + "Expected {}, got {}", + jch.h, + r_jch.h + ); + } + + #[test] + fn test_darktable_hcb() { + let jch = DtUchJch::new(0.3, 0.6, 0.4); + let hcb = DtUchHcb::from_jch(jch); + let r_jch = hcb.to_jch(); + + assert!( + (r_jch.j - jch.j).abs() < 1e-5, + "Expected {}, got {}", + jch.j, + r_jch.j + ); + assert!( + (r_jch.c - jch.c).abs() < 1e-5, + "Expected {}, got {}", + jch.c, + r_jch.c + ); + assert!( + (r_jch.h - jch.h).abs() < 1e-5, + "Expected {}, got {}", + jch.h, + r_jch.h + ); + } + + #[test] + fn test_darktable_ucs_jch_from_xyz() { + let xyz = Xyz::new(0.4, 0.2, 0.5); + let ucs = DtUchJch::from_xyz(xyz); + let xyy_rev = ucs.to_xyz(); + assert!( + (xyz.x - xyz.x).abs() < 1e-5, + "Expected {}, got {}", + xyz.x, + xyy_rev.x + ); + assert!( + (xyz.y - xyz.y).abs() < 1e-5, + "Expected {}, got {}", + xyz.y, + xyy_rev.y + ); + assert!( + (xyz.z - xyz.z).abs() < 1e-5, + "Expected {}, got {}", + xyz.z, + xyy_rev.z + ); + } +} diff --git a/deps/moxcms/src/err.rs b/deps/moxcms/src/err.rs new file mode 100644 index 0000000..26454ef --- /dev/null +++ b/deps/moxcms/src/err.rs @@ -0,0 +1,141 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::RenderingIntent; +use std::error::Error; +use std::fmt::Display; + +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] +pub struct MalformedSize { + pub size: usize, + pub expected: usize, +} + +#[derive(Debug, Clone, PartialOrd, PartialEq)] +pub enum CmsError { + LaneSizeMismatch, + LaneMultipleOfChannels, + InvalidProfile, + InvalidTrcCurve, + InvalidCicp, + CurveLutIsTooLarge, + ParametricCurveZeroDivision, + InvalidRenderingIntent, + DivisionByZero, + UnsupportedColorPrimaries(u8), + UnsupportedTrc(u8), + InvalidLayout, + UnsupportedProfileConnection, + BuildTransferFunction, + UnsupportedChannelConfiguration, + UnknownTag(u32), + UnknownTagTypeDefinition(u32), + UnsupportedLutRenderingIntent(RenderingIntent), + InvalidAtoBLut, + OverflowingError, + LUTTablesInvalidKind, + MalformedClut(MalformedSize), + MalformedCurveLutTable(MalformedSize), + InvalidInksCountForProfile, + MalformedTrcCurve(String), + OutOfMemory(usize), +} + +impl Display for CmsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CmsError::LaneSizeMismatch => f.write_str("Lanes length must match"), + CmsError::LaneMultipleOfChannels => { + f.write_str("Lane length must not be multiple of channel count") + } + CmsError::InvalidProfile => f.write_str("Invalid ICC profile"), + CmsError::InvalidCicp => { + f.write_str("Invalid Code Independent point (CICP) in ICC profile") + } + CmsError::InvalidTrcCurve => f.write_str("Invalid TRC curve"), + CmsError::CurveLutIsTooLarge => f.write_str("Curve Lut is too large"), + CmsError::ParametricCurveZeroDivision => { + f.write_str("Parametric Curve definition causes division by zero") + } + CmsError::InvalidRenderingIntent => f.write_str("Invalid rendering intent"), + CmsError::DivisionByZero => f.write_str("Division by zero"), + CmsError::UnsupportedColorPrimaries(value) => { + f.write_fmt(format_args!("Unsupported color primaries, {value}")) + } + CmsError::UnsupportedTrc(value) => f.write_fmt(format_args!("Unsupported TRC {value}")), + CmsError::InvalidLayout => f.write_str("Invalid layout"), + CmsError::UnsupportedProfileConnection => f.write_str("Unsupported profile connection"), + CmsError::BuildTransferFunction => f.write_str("Can't reconstruct transfer function"), + CmsError::UnsupportedChannelConfiguration => { + f.write_str("Can't reconstruct channel configuration") + } + CmsError::UnknownTag(t) => f.write_fmt(format_args!("Unknown tag: {t}")), + CmsError::UnknownTagTypeDefinition(t) => { + f.write_fmt(format_args!("Unknown tag type definition: {t}")) + } + CmsError::UnsupportedLutRenderingIntent(intent) => f.write_fmt(format_args!( + "Can't find LUT for rendering intent: {intent:?}" + )), + CmsError::InvalidAtoBLut => f.write_str("Invalid A to B Lut"), + CmsError::OverflowingError => { + f.write_str("Overflowing was happen, that is not allowed") + } + CmsError::LUTTablesInvalidKind => f.write_str("All LUT curves must have same kind"), + CmsError::MalformedClut(size) => { + f.write_fmt(format_args!("Invalid CLUT size: {size:?}")) + } + CmsError::MalformedCurveLutTable(size) => { + f.write_fmt(format_args!("Malformed curve LUT size: {size:?}")) + } + CmsError::InvalidInksCountForProfile => { + f.write_str("Invalid inks count for profile was provided") + } + CmsError::MalformedTrcCurve(str) => f.write_str(str), + CmsError::OutOfMemory(capacity) => f.write_fmt(format_args!( + "There is no enough memory to allocate {capacity} bytes" + )), + } + } +} + +impl Error for CmsError {} + +macro_rules! try_vec { + () => { + Vec::new() + }; + ($elem:expr; $n:expr) => {{ + let mut v = Vec::new(); + v.try_reserve_exact($n) + .map_err(|_| crate::err::CmsError::OutOfMemory($n))?; + v.resize($n, $elem); + v + }}; +} + +pub(crate) use try_vec; diff --git a/deps/moxcms/src/gamma.rs b/deps/moxcms/src/gamma.rs new file mode 100644 index 0000000..e49f7f5 --- /dev/null +++ b/deps/moxcms/src/gamma.rs @@ -0,0 +1,1078 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::mlaf::{fmla, mlaf}; +use crate::transform::PointeeSizeExpressible; +use crate::{Rgb, TransferCharacteristics}; +use num_traits::AsPrimitive; +use pxfm::{ + dirty_powf, f_exp, f_exp10, f_exp10f, f_expf, f_log, f_log10, f_log10f, f_logf, f_pow, f_powf, +}; + +#[inline] +/// Linear transfer function for sRGB +fn srgb_to_linear(gamma: f64) -> f64 { + if gamma < 0f64 { + 0f64 + } else if gamma < 12.92f64 * 0.0030412825601275209f64 { + gamma * (1f64 / 12.92f64) + } else if gamma < 1.0f64 { + f_pow( + (gamma + 0.0550107189475866f64) / 1.0550107189475866f64, + 2.4f64, + ) + } else { + 1.0f64 + } +} + +#[inline] +/// Linear transfer function for sRGB +fn srgb_to_linearf_extended(gamma: f32) -> f32 { + if gamma < 12.92 * 0.0030412825601275209 { + gamma * (1. / 12.92f32) + } else { + dirty_powf((gamma + 0.0550107189475866) / 1.0550107189475866, 2.4) + } +} + +#[inline] +/// Gamma transfer function for sRGB +fn srgb_from_linear(linear: f64) -> f64 { + if linear < 0.0f64 { + 0.0f64 + } else if linear < 0.0030412825601275209f64 { + linear * 12.92f64 + } else if linear < 1.0f64 { + fmla( + 1.0550107189475866f64, + f_pow(linear, 1.0f64 / 2.4f64), + -0.0550107189475866f64, + ) + } else { + 1.0f64 + } +} + +#[inline] +/// Gamma transfer function for sRGB +pub(crate) fn srgb_from_linear_extended(linear: f32) -> f32 { + if linear < 0.0030412825601275209f32 { + linear * 12.92f32 + } else { + fmla( + 1.0550107189475866f32, + dirty_powf(linear, 1.0f32 / 2.4f32), + -0.0550107189475866f32, + ) + } +} + +#[inline] +/// Linear transfer function for Rec.709 +fn rec709_to_linear(gamma: f64) -> f64 { + if gamma < 0.0f64 { + 0.0f64 + } else if gamma < 4.5f64 * 0.018053968510807f64 { + gamma * (1f64 / 4.5f64) + } else if gamma < 1.0f64 { + f_pow( + (gamma + 0.09929682680944f64) / 1.09929682680944f64, + 1.0f64 / 0.45f64, + ) + } else { + 1.0f64 + } +} + +#[inline] +/// Linear transfer function for Rec.709 +fn rec709_to_linearf_extended(gamma: f32) -> f32 { + if gamma < 4.5 * 0.018053968510807 { + gamma * (1. / 4.5) + } else { + f_powf((gamma + 0.09929682680944) / 1.09929682680944, 1.0 / 0.45) + } +} + +#[inline] +/// Gamma transfer function for Rec.709 +fn rec709_from_linear(linear: f64) -> f64 { + if linear < 0.0f64 { + 0.0f64 + } else if linear < 0.018053968510807f64 { + linear * 4.5f64 + } else if linear < 1.0f64 { + fmla( + 1.09929682680944f64, + f_pow(linear, 0.45f64), + -0.09929682680944f64, + ) + } else { + 1.0f64 + } +} + +#[inline] +/// Gamma transfer function for Rec.709 +fn rec709_from_linearf_extended(linear: f32) -> f32 { + if linear < 0.018053968510807 { + linear * 4.5 + } else { + fmla( + 1.09929682680944, + dirty_powf(linear, 0.45), + -0.09929682680944, + ) + } +} + +#[inline] +/// Linear transfer function for Smpte 428 +pub(crate) fn smpte428_to_linear(gamma: f64) -> f64 { + const SCALE: f64 = 1. / 0.91655527974030934f64; + f_pow(gamma.max(0.).min(1f64), 2.6f64) * SCALE +} + +#[inline] +/// Linear transfer function for Smpte 428 +pub(crate) fn smpte428_to_linearf_extended(gamma: f32) -> f32 { + const SCALE: f32 = 1. / 0.91655527974030934; + dirty_powf(gamma.max(0.), 2.6) * SCALE +} + +#[inline] +/// Gamma transfer function for Smpte 428 +fn smpte428_from_linear(linear: f64) -> f64 { + const POWER_VALUE: f64 = 1.0f64 / 2.6f64; + f_pow(0.91655527974030934f64 * linear.max(0.), POWER_VALUE) +} + +#[inline] +/// Gamma transfer function for Smpte 428 +fn smpte428_from_linearf(linear: f32) -> f32 { + const POWER_VALUE: f32 = 1.0 / 2.6; + dirty_powf(0.91655527974030934 * linear.max(0.), POWER_VALUE) +} + +#[inline] +/// Linear transfer function for Smpte 240 +pub(crate) fn smpte240_to_linear(gamma: f64) -> f64 { + if gamma < 0.0 { + 0.0 + } else if gamma < 4.0 * 0.022821585529445 { + gamma / 4.0 + } else if gamma < 1.0 { + f_pow((gamma + 0.111572195921731) / 1.111572195921731, 1.0 / 0.45) + } else { + 1.0 + } +} + +#[inline] +/// Linear transfer function for Smpte 240 +pub(crate) fn smpte240_to_linearf_extended(gamma: f32) -> f32 { + if gamma < 4.0 * 0.022821585529445 { + gamma / 4.0 + } else { + dirty_powf((gamma + 0.111572195921731) / 1.111572195921731, 1.0 / 0.45) + } +} + +#[inline] +/// Gamma transfer function for Smpte 240 +fn smpte240_from_linear(linear: f64) -> f64 { + if linear < 0.0 { + 0.0 + } else if linear < 0.022821585529445 { + linear * 4.0 + } else if linear < 1.0 { + fmla(1.111572195921731, f_pow(linear, 0.45), -0.111572195921731) + } else { + 1.0 + } +} + +#[inline] +/// Gamma transfer function for Smpte 240 +fn smpte240_from_linearf_extended(linear: f32) -> f32 { + if linear < 0.022821585529445 { + linear * 4.0 + } else { + fmla(1.111572195921731, f_powf(linear, 0.45), -0.111572195921731) + } +} + +#[inline] +/// Gamma transfer function for Log100 +fn log100_from_linear(linear: f64) -> f64 { + if linear <= 0.01f64 { + 0. + } else { + 1. + f_log10(linear.min(1.)) / 2.0 + } +} + +#[inline] +/// Gamma transfer function for Log100 +fn log100_from_linearf(linear: f32) -> f32 { + if linear <= 0.01 { + 0. + } else { + 1. + f_log10f(linear.min(1.)) / 2.0 + } +} + +#[inline] +/// Linear transfer function for Log100 +pub(crate) fn log100_to_linear(gamma: f64) -> f64 { + // The function is non-bijective so choose the middle of [0, 0.00316227766f]. + const MID_INTERVAL: f64 = 0.01 / 2.; + if gamma <= 0. { + MID_INTERVAL + } else { + f_exp10(2. * (gamma.min(1.) - 1.)) + } +} + +#[inline] +/// Linear transfer function for Log100 +pub(crate) fn log100_to_linearf(gamma: f32) -> f32 { + // The function is non-bijective so choose the middle of [0, 0.00316227766f]. + const MID_INTERVAL: f32 = 0.01 / 2.; + if gamma <= 0. { + MID_INTERVAL + } else { + f_exp10f(2. * (gamma.min(1.) - 1.)) + } +} + +#[inline] +/// Linear transfer function for Log100Sqrt10 +pub(crate) fn log100_sqrt10_to_linear(gamma: f64) -> f64 { + // The function is non-bijective so choose the middle of [0, 0.00316227766f]. + const MID_INTERVAL: f64 = 0.00316227766 / 2.; + if gamma <= 0. { + MID_INTERVAL + } else { + f_exp10(2.5 * (gamma.min(1.) - 1.)) + } +} + +#[inline] +/// Linear transfer function for Log100Sqrt10 +pub(crate) fn log100_sqrt10_to_linearf(gamma: f32) -> f32 { + // The function is non-bijective so choose the middle of [0, 0.00316227766f]. + const MID_INTERVAL: f32 = 0.00316227766 / 2.; + if gamma <= 0. { + MID_INTERVAL + } else { + f_exp10f(2.5 * (gamma.min(1.) - 1.)) + } +} + +#[inline] +/// Gamma transfer function for Log100Sqrt10 +fn log100_sqrt10_from_linear(linear: f64) -> f64 { + if linear <= 0.00316227766 { + 0.0 + } else { + 1.0 + f_log10(linear.min(1.)) / 2.5 + } +} + +#[inline] +/// Gamma transfer function for Log100Sqrt10 +fn log100_sqrt10_from_linearf(linear: f32) -> f32 { + if linear <= 0.00316227766 { + 0.0 + } else { + 1.0 + f_log10f(linear.min(1.)) / 2.5 + } +} + +#[inline] +/// Gamma transfer function for Bt.1361 +fn bt1361_from_linear(linear: f64) -> f64 { + if linear < -0.25 { + -0.25 + } else if linear < 0.0 { + fmla( + -0.27482420670236, + f_pow(-4.0 * linear, 0.45), + 0.02482420670236, + ) + } else if linear < 0.018053968510807 { + linear * 4.5 + } else if linear < 1.0 { + fmla(1.09929682680944, f_pow(linear, 0.45), -0.09929682680944) + } else { + 1.0 + } +} + +#[inline] +/// Gamma transfer function for Bt.1361 +fn bt1361_from_linearf(linear: f32) -> f32 { + if linear < -0.25 { + -0.25 + } else if linear < 0.0 { + fmla( + -0.27482420670236, + dirty_powf(-4.0 * linear, 0.45), + 0.02482420670236, + ) + } else if linear < 0.018053968510807 { + linear * 4.5 + } else if linear < 1.0 { + fmla( + 1.09929682680944, + dirty_powf(linear, 0.45), + -0.09929682680944, + ) + } else { + 1.0 + } +} + +#[inline] +/// Linear transfer function for Bt.1361 +pub(crate) fn bt1361_to_linear(gamma: f64) -> f64 { + if gamma < -0.25f64 { + -0.25f64 + } else if gamma < 0.0f64 { + f_pow( + (gamma - 0.02482420670236f64) / -0.27482420670236f64, + 1.0f64 / 0.45f64, + ) / -4.0f64 + } else if gamma < 4.5 * 0.018053968510807 { + gamma / 4.5 + } else if gamma < 1.0 { + f_pow((gamma + 0.09929682680944) / 1.09929682680944, 1.0 / 0.45) + } else { + 1.0f64 + } +} + +#[inline] +/// Linear transfer function for Bt.1361 +fn bt1361_to_linearf(gamma: f32) -> f32 { + if gamma < -0.25 { + -0.25 + } else if gamma < 0.0 { + dirty_powf((gamma - 0.02482420670236) / -0.27482420670236, 1.0 / 0.45) / -4.0 + } else if gamma < 4.5 * 0.018053968510807 { + gamma / 4.5 + } else if gamma < 1.0 { + dirty_powf((gamma + 0.09929682680944) / 1.09929682680944, 1.0 / 0.45) + } else { + 1.0 + } +} + +#[inline(always)] +/// Pure gamma transfer function for gamma 2.2 +fn pure_gamma_function(x: f64, gamma: f64) -> f64 { + if x <= 0f64 { + 0f64 + } else if x >= 1f64 { + 1f64 + } else { + f_pow(x, gamma) + } +} + +#[inline(always)] +/// Pure gamma transfer function for gamma 2.2 +fn pure_gamma_function_f(x: f32, gamma: f32) -> f32 { + if x <= 0. { 0. } else { dirty_powf(x, gamma) } +} + +#[inline] +pub(crate) fn iec61966_to_linear(gamma: f64) -> f64 { + if gamma < -4.5f64 * 0.018053968510807f64 { + f_pow( + (-gamma + 0.09929682680944f64) / -1.09929682680944f64, + 1.0 / 0.45, + ) + } else if gamma < 4.5f64 * 0.018053968510807f64 { + gamma / 4.5 + } else { + f_pow( + (gamma + 0.09929682680944f64) / 1.09929682680944f64, + 1.0 / 0.45, + ) + } +} + +#[inline] +fn iec61966_to_linearf(gamma: f32) -> f32 { + if gamma < -4.5 * 0.018053968510807 { + dirty_powf((-gamma + 0.09929682680944) / -1.09929682680944, 1.0 / 0.45) + } else if gamma < 4.5 * 0.018053968510807 { + gamma / 4.5 + } else { + dirty_powf((gamma + 0.09929682680944) / 1.09929682680944, 1.0 / 0.45) + } +} + +#[inline] +fn iec61966_from_linear(v: f64) -> f64 { + if v < -0.018053968510807f64 { + fmla(-1.09929682680944f64, f_pow(-v, 0.45), 0.09929682680944f64) + } else if v < 0.018053968510807f64 { + v * 4.5f64 + } else { + fmla(1.09929682680944f64, f_pow(v, 0.45), -0.09929682680944f64) + } +} + +#[inline] +fn iec61966_from_linearf(v: f32) -> f32 { + if v < -0.018053968510807 { + fmla(-1.09929682680944, dirty_powf(-v, 0.45), 0.09929682680944) + } else if v < 0.018053968510807 { + v * 4.5 + } else { + fmla(1.09929682680944, dirty_powf(v, 0.45), -0.09929682680944) + } +} + +#[inline] +/// Pure gamma transfer function for gamma 2.2 +fn gamma2p2_from_linear(linear: f64) -> f64 { + pure_gamma_function(linear, 1f64 / 2.2f64) +} + +#[inline] +/// Pure gamma transfer function for gamma 2.2 +fn gamma2p2_from_linear_f(linear: f32) -> f32 { + pure_gamma_function_f(linear, 1. / 2.2) +} + +#[inline] +/// Linear transfer function for gamma 2.2 +fn gamma2p2_to_linear(gamma: f64) -> f64 { + pure_gamma_function(gamma, 2.2f64) +} + +#[inline] +/// Linear transfer function for gamma 2.2 +fn gamma2p2_to_linear_f(gamma: f32) -> f32 { + pure_gamma_function_f(gamma, 2.2) +} + +#[inline] +/// Pure gamma transfer function for gamma 2.8 +fn gamma2p8_from_linear(linear: f64) -> f64 { + pure_gamma_function(linear, 1f64 / 2.8f64) +} + +#[inline] +/// Pure gamma transfer function for gamma 2.8 +fn gamma2p8_from_linear_f(linear: f32) -> f32 { + pure_gamma_function_f(linear, 1. / 2.8) +} + +#[inline] +/// Linear transfer function for gamma 2.8 +fn gamma2p8_to_linear(gamma: f64) -> f64 { + pure_gamma_function(gamma, 2.8f64) +} + +#[inline] +/// Linear transfer function for gamma 2.8 +fn gamma2p8_to_linear_f(gamma: f32) -> f32 { + pure_gamma_function_f(gamma, 2.8) +} + +#[inline] +/// Linear transfer function for PQ +pub(crate) fn pq_to_linear(gamma: f64) -> f64 { + if gamma > 0.0 { + let pow_gamma = f_pow(gamma, 1.0 / 78.84375); + let num = (pow_gamma - 0.8359375).max(0.); + let den = mlaf(18.8515625, -18.6875, pow_gamma).max(f64::MIN); + f_pow(num / den, 1.0 / 0.1593017578125) + } else { + 0.0 + } +} + +#[inline] +/// Linear transfer function for PQ +pub(crate) fn pq_to_linearf(gamma: f32) -> f32 { + if gamma > 0.0 { + let pow_gamma = f_powf(gamma, 1.0 / 78.84375); + let num = (pow_gamma - 0.8359375).max(0.); + let den = mlaf(18.8515625, -18.6875, pow_gamma).max(f32::MIN); + f_powf(num / den, 1.0 / 0.1593017578125) + } else { + 0.0 + } +} + +#[inline] +/// Gamma transfer function for PQ +fn pq_from_linear(linear: f64) -> f64 { + if linear > 0.0 { + let linear = linear.clamp(0., 1.); + let pow_linear = f_pow(linear, 0.1593017578125); + let num = fmla(0.1640625, pow_linear, -0.1640625); + let den = mlaf(1.0, 18.6875, pow_linear); + f_pow(1.0 + num / den, 78.84375) + } else { + 0.0 + } +} + +#[inline] +/// Gamma transfer function for PQ +pub(crate) fn pq_from_linearf(linear: f32) -> f32 { + if linear > 0.0 { + let linear = linear.max(0.); + let pow_linear = f_powf(linear, 0.1593017578125); + let num = fmla(0.1640625, pow_linear, -0.1640625); + let den = mlaf(1.0, 18.6875, pow_linear); + f_powf(1.0 + num / den, 78.84375) + } else { + 0.0 + } +} + +#[inline] +/// Linear transfer function for HLG +pub(crate) fn hlg_to_linear(gamma: f64) -> f64 { + if gamma < 0.0 { + return 0.0; + } + if gamma <= 0.5 { + f_pow((gamma * gamma) * (1.0 / 3.0), 1.2) + } else { + f_pow( + (f_exp((gamma - 0.55991073) / 0.17883277) + 0.28466892) / 12.0, + 1.2, + ) + } +} + +#[inline] +/// Linear transfer function for HLG +pub(crate) fn hlg_to_linearf(gamma: f32) -> f32 { + if gamma < 0.0 { + return 0.0; + } + if gamma <= 0.5 { + f_powf((gamma * gamma) * (1.0 / 3.0), 1.2) + } else { + f_powf( + (f_expf((gamma - 0.55991073) / 0.17883277) + 0.28466892) / 12.0, + 1.2, + ) + } +} + +#[inline] +/// Gamma transfer function for HLG +fn hlg_from_linear(linear: f64) -> f64 { + // Scale from extended SDR range to [0.0, 1.0]. + let mut linear = linear.clamp(0., 1.); + // Inverse OOTF followed by OETF see Table 5 and Note 5i in ITU-R BT.2100-2 page 7-8. + linear = f_pow(linear, 1.0 / 1.2); + if linear < 0.0 { + 0.0 + } else if linear <= (1.0 / 12.0) { + (3.0 * linear).sqrt() + } else { + fmla( + 0.17883277, + f_log(fmla(12.0, linear, -0.28466892)), + 0.55991073, + ) + } +} + +#[inline] +/// Gamma transfer function for HLG +fn hlg_from_linearf(linear: f32) -> f32 { + // Scale from extended SDR range to [0.0, 1.0]. + let mut linear = linear.max(0.); + // Inverse OOTF followed by OETF see Table 5 and Note 5i in ITU-R BT.2100-2 page 7-8. + linear = f_powf(linear, 1.0 / 1.2); + if linear < 0.0 { + 0.0 + } else if linear <= (1.0 / 12.0) { + (3.0 * linear).sqrt() + } else { + 0.17883277 * f_logf(12.0 * linear - 0.28466892) + 0.55991073 + } +} + +#[inline] +fn trc_linear(v: f64) -> f64 { + v.min(1.).max(0.) +} + +impl TransferCharacteristics { + #[inline] + pub fn linearize(self, v: f64) -> f64 { + match self { + TransferCharacteristics::Reserved => 0f64, + TransferCharacteristics::Bt709 + | TransferCharacteristics::Bt601 + | TransferCharacteristics::Bt202010bit + | TransferCharacteristics::Bt202012bit => rec709_to_linear(v), + TransferCharacteristics::Unspecified => 0f64, + TransferCharacteristics::Bt470M => gamma2p2_to_linear(v), + TransferCharacteristics::Bt470Bg => gamma2p8_to_linear(v), + TransferCharacteristics::Smpte240 => smpte240_to_linear(v), + TransferCharacteristics::Linear => trc_linear(v), + TransferCharacteristics::Log100 => log100_to_linear(v), + TransferCharacteristics::Log100sqrt10 => log100_sqrt10_to_linear(v), + TransferCharacteristics::Iec61966 => iec61966_to_linear(v), + TransferCharacteristics::Bt1361 => bt1361_to_linear(v), + TransferCharacteristics::Srgb => srgb_to_linear(v), + TransferCharacteristics::Smpte2084 => pq_to_linear(v), + TransferCharacteristics::Smpte428 => smpte428_to_linear(v), + TransferCharacteristics::Hlg => hlg_to_linear(v), + } + } + + #[inline] + pub fn gamma(self, v: f64) -> f64 { + match self { + TransferCharacteristics::Reserved => 0f64, + TransferCharacteristics::Bt709 + | TransferCharacteristics::Bt601 + | TransferCharacteristics::Bt202010bit + | TransferCharacteristics::Bt202012bit => rec709_from_linear(v), + TransferCharacteristics::Unspecified => 0f64, + TransferCharacteristics::Bt470M => gamma2p2_from_linear(v), + TransferCharacteristics::Bt470Bg => gamma2p8_from_linear(v), + TransferCharacteristics::Smpte240 => smpte240_from_linear(v), + TransferCharacteristics::Linear => trc_linear(v), + TransferCharacteristics::Log100 => log100_from_linear(v), + TransferCharacteristics::Log100sqrt10 => log100_sqrt10_from_linear(v), + TransferCharacteristics::Iec61966 => iec61966_from_linear(v), + TransferCharacteristics::Bt1361 => bt1361_from_linear(v), + TransferCharacteristics::Srgb => srgb_from_linear(v), + TransferCharacteristics::Smpte2084 => pq_from_linear(v), + TransferCharacteristics::Smpte428 => smpte428_from_linear(v), + TransferCharacteristics::Hlg => hlg_from_linear(v), + } + } + + pub(crate) fn extended_gamma_tristimulus(self) -> fn(Rgb) -> Rgb { + match self { + TransferCharacteristics::Reserved => |x| Rgb::new(x.r, x.g, x.b), + TransferCharacteristics::Bt709 + | TransferCharacteristics::Bt601 + | TransferCharacteristics::Bt202010bit + | TransferCharacteristics::Bt202012bit => |x| { + Rgb::new( + rec709_from_linearf_extended(x.r), + rec709_from_linearf_extended(x.g), + rec709_from_linearf_extended(x.b), + ) + }, + TransferCharacteristics::Unspecified => |x| Rgb::new(x.r, x.g, x.b), + TransferCharacteristics::Bt470M => |x| { + Rgb::new( + gamma2p2_from_linear_f(x.r), + gamma2p2_from_linear_f(x.g), + gamma2p2_from_linear_f(x.b), + ) + }, + TransferCharacteristics::Bt470Bg => |x| { + Rgb::new( + gamma2p8_from_linear_f(x.r), + gamma2p8_from_linear_f(x.g), + gamma2p8_from_linear_f(x.b), + ) + }, + TransferCharacteristics::Smpte240 => |x| { + Rgb::new( + smpte240_from_linearf_extended(x.r), + smpte240_from_linearf_extended(x.g), + smpte240_from_linearf_extended(x.b), + ) + }, + TransferCharacteristics::Linear => |x| Rgb::new(x.r, x.g, x.b), + TransferCharacteristics::Log100 => |x| { + Rgb::new( + log100_from_linearf(x.r), + log100_from_linearf(x.g), + log100_from_linearf(x.b), + ) + }, + TransferCharacteristics::Log100sqrt10 => |x| { + Rgb::new( + log100_sqrt10_from_linearf(x.r), + log100_sqrt10_from_linearf(x.g), + log100_sqrt10_from_linearf(x.b), + ) + }, + TransferCharacteristics::Iec61966 => |x| { + Rgb::new( + iec61966_from_linearf(x.r), + iec61966_from_linearf(x.g), + iec61966_from_linearf(x.b), + ) + }, + TransferCharacteristics::Bt1361 => |x| { + Rgb::new( + bt1361_from_linearf(x.r), + bt1361_from_linearf(x.g), + bt1361_from_linearf(x.b), + ) + }, + TransferCharacteristics::Srgb => |x| { + Rgb::new( + srgb_from_linear_extended(x.r), + srgb_from_linear_extended(x.g), + srgb_from_linear_extended(x.b), + ) + }, + TransferCharacteristics::Smpte2084 => |x| { + Rgb::new( + pq_from_linearf(x.r), + pq_from_linearf(x.g), + pq_from_linearf(x.b), + ) + }, + TransferCharacteristics::Smpte428 => |x| { + Rgb::new( + smpte428_from_linearf(x.r), + smpte428_from_linearf(x.g), + smpte428_from_linearf(x.b), + ) + }, + TransferCharacteristics::Hlg => |x| { + Rgb::new( + hlg_from_linearf(x.r), + hlg_from_linearf(x.g), + hlg_from_linearf(x.b), + ) + }, + } + } + + pub(crate) fn extended_gamma_single(self) -> fn(f32) -> f32 { + match self { + TransferCharacteristics::Reserved => |x| x, + TransferCharacteristics::Bt709 + | TransferCharacteristics::Bt601 + | TransferCharacteristics::Bt202010bit + | TransferCharacteristics::Bt202012bit => |x| rec709_from_linearf_extended(x), + TransferCharacteristics::Unspecified => |x| x, + TransferCharacteristics::Bt470M => |x| gamma2p2_from_linear_f(x), + TransferCharacteristics::Bt470Bg => |x| gamma2p8_from_linear_f(x), + TransferCharacteristics::Smpte240 => |x| smpte240_from_linearf_extended(x), + TransferCharacteristics::Linear => |x| x, + TransferCharacteristics::Log100 => |x| log100_from_linearf(x), + TransferCharacteristics::Log100sqrt10 => |x| log100_sqrt10_from_linearf(x), + TransferCharacteristics::Iec61966 => |x| iec61966_from_linearf(x), + TransferCharacteristics::Bt1361 => |x| bt1361_from_linearf(x), + TransferCharacteristics::Srgb => |x| srgb_from_linear_extended(x), + TransferCharacteristics::Smpte2084 => |x| pq_from_linearf(x), + TransferCharacteristics::Smpte428 => |x| smpte428_from_linearf(x), + TransferCharacteristics::Hlg => |x| hlg_from_linearf(x), + } + } + + pub(crate) fn extended_linear_tristimulus(self) -> fn(Rgb) -> Rgb { + match self { + TransferCharacteristics::Reserved => |x| Rgb::new(x.r, x.g, x.b), + TransferCharacteristics::Bt709 + | TransferCharacteristics::Bt601 + | TransferCharacteristics::Bt202010bit + | TransferCharacteristics::Bt202012bit => |x| { + Rgb::new( + rec709_to_linearf_extended(x.r), + rec709_to_linearf_extended(x.g), + rec709_to_linearf_extended(x.b), + ) + }, + TransferCharacteristics::Unspecified => |x| Rgb::new(x.r, x.g, x.b), + TransferCharacteristics::Bt470M => |x| { + Rgb::new( + gamma2p2_to_linear_f(x.r), + gamma2p2_to_linear_f(x.g), + gamma2p2_to_linear_f(x.b), + ) + }, + TransferCharacteristics::Bt470Bg => |x| { + Rgb::new( + gamma2p8_to_linear_f(x.r), + gamma2p8_to_linear_f(x.g), + gamma2p8_to_linear_f(x.b), + ) + }, + TransferCharacteristics::Smpte240 => |x| { + Rgb::new( + smpte240_to_linearf_extended(x.r), + smpte240_to_linearf_extended(x.g), + smpte240_to_linearf_extended(x.b), + ) + }, + TransferCharacteristics::Linear => |x| Rgb::new(x.r, x.g, x.b), + TransferCharacteristics::Log100 => |x| { + Rgb::new( + log100_to_linearf(x.r), + log100_to_linearf(x.g), + log100_to_linearf(x.b), + ) + }, + TransferCharacteristics::Log100sqrt10 => |x| { + Rgb::new( + log100_sqrt10_to_linearf(x.r), + log100_sqrt10_to_linearf(x.g), + log100_sqrt10_to_linearf(x.b), + ) + }, + TransferCharacteristics::Iec61966 => |x| { + Rgb::new( + iec61966_to_linearf(x.r), + iec61966_to_linearf(x.g), + iec61966_to_linearf(x.b), + ) + }, + TransferCharacteristics::Bt1361 => |x| { + Rgb::new( + bt1361_to_linearf(x.r), + bt1361_to_linearf(x.g), + bt1361_to_linearf(x.b), + ) + }, + TransferCharacteristics::Srgb => |x| { + Rgb::new( + srgb_to_linearf_extended(x.r), + srgb_to_linearf_extended(x.g), + srgb_to_linearf_extended(x.b), + ) + }, + TransferCharacteristics::Smpte2084 => { + |x| Rgb::new(pq_to_linearf(x.r), pq_to_linearf(x.g), pq_to_linearf(x.b)) + } + TransferCharacteristics::Smpte428 => |x| { + Rgb::new( + smpte428_to_linearf_extended(x.r), + smpte428_to_linearf_extended(x.g), + smpte428_to_linearf_extended(x.b), + ) + }, + TransferCharacteristics::Hlg => |x| { + Rgb::new( + hlg_to_linearf(x.r), + hlg_to_linearf(x.g), + hlg_to_linearf(x.b), + ) + }, + } + } + + pub(crate) fn extended_linear_single(self) -> fn(f32) -> f32 { + match self { + TransferCharacteristics::Reserved => |x| x, + TransferCharacteristics::Bt709 + | TransferCharacteristics::Bt601 + | TransferCharacteristics::Bt202010bit + | TransferCharacteristics::Bt202012bit => |x| rec709_to_linearf_extended(x), + TransferCharacteristics::Unspecified => |x| x, + TransferCharacteristics::Bt470M => |x| gamma2p2_to_linear_f(x), + TransferCharacteristics::Bt470Bg => |x| gamma2p8_to_linear_f(x), + TransferCharacteristics::Smpte240 => |x| smpte240_to_linearf_extended(x), + TransferCharacteristics::Linear => |x| x, + TransferCharacteristics::Log100 => |x| log100_to_linearf(x), + TransferCharacteristics::Log100sqrt10 => |x| log100_sqrt10_to_linearf(x), + TransferCharacteristics::Iec61966 => |x| iec61966_to_linearf(x), + TransferCharacteristics::Bt1361 => |x| bt1361_to_linearf(x), + TransferCharacteristics::Srgb => |x| srgb_to_linearf_extended(x), + TransferCharacteristics::Smpte2084 => |x| pq_to_linearf(x), + TransferCharacteristics::Smpte428 => |x| smpte428_to_linearf_extended(x), + TransferCharacteristics::Hlg => |x| hlg_to_linearf(x), + } + } + + pub(crate) fn make_linear_table< + T: PointeeSizeExpressible, + const N: usize, + const BIT_DEPTH: usize, + >( + &self, + ) -> Box<[f32; N]> { + let mut gamma_table = Box::new([0f32; N]); + let max_value = if T::FINITE { + (1 << BIT_DEPTH) - 1 + } else { + T::NOT_FINITE_LINEAR_TABLE_SIZE - 1 + }; + let cap_values = if T::FINITE { + (1u32 << BIT_DEPTH) as usize + } else { + T::NOT_FINITE_LINEAR_TABLE_SIZE + }; + assert!(cap_values <= N, "Invalid lut table construction"); + let scale_value = 1f64 / max_value as f64; + for (i, g) in gamma_table.iter_mut().enumerate().take(cap_values) { + *g = self.linearize(i as f64 * scale_value) as f32; + } + gamma_table + } + + pub(crate) fn make_gamma_table< + T: Default + Copy + 'static + PointeeSizeExpressible, + const BUCKET: usize, + const N: usize, + >( + &self, + bit_depth: usize, + ) -> Box<[T; BUCKET]> + where + f32: AsPrimitive, + { + let mut table = Box::new([T::default(); BUCKET]); + let max_range = 1f64 / (N - 1) as f64; + let max_value = ((1 << bit_depth) - 1) as f64; + if T::FINITE { + for (v, output) in table.iter_mut().take(N).enumerate() { + *output = ((self.gamma(v as f64 * max_range) * max_value) as f32) + .round() + .as_(); + } + } else { + for (v, output) in table.iter_mut().take(N).enumerate() { + *output = (self.gamma(v as f64 * max_range) as f32).as_(); + } + } + table + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn srgb_test() { + let srgb_0 = srgb_to_linear(0.5); + let srgb_1 = srgb_from_linear(srgb_0); + assert!((0.5 - srgb_1).abs() < 1e-9f64); + } + + #[test] + fn log100_sqrt10_test() { + let srgb_0 = log100_sqrt10_to_linear(0.5); + let srgb_1 = log100_sqrt10_from_linear(srgb_0); + assert_eq!(0.5, srgb_1); + } + + #[test] + fn log100_test() { + let srgb_0 = log100_to_linear(0.5); + let srgb_1 = log100_from_linear(srgb_0); + assert_eq!(0.5, srgb_1); + } + + #[test] + fn iec61966_test() { + let srgb_0 = iec61966_to_linear(0.5); + let srgb_1 = iec61966_from_linear(srgb_0); + assert!((0.5 - srgb_1).abs() < 1e-9f64); + } + + #[test] + fn smpte240_test() { + let srgb_0 = smpte240_to_linear(0.5); + let srgb_1 = smpte240_from_linear(srgb_0); + assert!((0.5 - srgb_1).abs() < 1e-9f64); + } + + #[test] + fn smpte428_test() { + let srgb_0 = smpte428_to_linear(0.5); + let srgb_1 = smpte428_from_linear(srgb_0); + assert!((0.5 - srgb_1).abs() < 1e-9f64); + } + + #[test] + fn rec709_test() { + let srgb_0 = rec709_to_linear(0.5); + let srgb_1 = rec709_from_linear(srgb_0); + assert!((0.5 - srgb_1).abs() < 1e-9f64); + } + + #[test] + fn rec709f_test() { + let srgb_0 = rec709_to_linearf_extended(0.5); + let srgb_1 = rec709_from_linearf_extended(srgb_0); + assert!((0.5 - srgb_1).abs() < 1e-5f32); + } + + #[test] + fn srgbf_test() { + let srgb_0 = srgb_to_linearf_extended(0.5); + let srgb_1 = srgb_from_linear_extended(srgb_0); + assert!((0.5 - srgb_1).abs() < 1e-5f32); + } + + #[test] + fn hlg_test() { + let z0 = hlg_to_linear(0.5); + let z1 = hlg_from_linear(z0); + assert!((0.5 - z1).abs() < 1e-5f64); + } + + #[test] + fn pq_test() { + let z0 = pq_to_linear(0.5); + let z1 = pq_from_linear(z0); + assert!((0.5 - z1).abs() < 1e-5f64); + } + + #[test] + fn pqf_test() { + let z0 = pq_to_linearf(0.5); + let z1 = pq_from_linearf(z0); + assert!((0.5 - z1).abs() < 1e-5f32); + } + + #[test] + fn iec_test() { + let z0 = iec61966_to_linear(0.5); + let z1 = iec61966_from_linear(z0); + assert!((0.5 - z1).abs() < 1e-5f64); + } + + #[test] + fn bt1361_test() { + let z0 = bt1361_to_linear(0.5); + let z1 = bt1361_from_linear(z0); + assert!((0.5 - z1).abs() < 1e-5f64); + } +} diff --git a/deps/moxcms/src/gamut.rs b/deps/moxcms/src/gamut.rs new file mode 100644 index 0000000..7d191b8 --- /dev/null +++ b/deps/moxcms/src/gamut.rs @@ -0,0 +1,66 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::Rgb; + +#[inline] +fn filmlike_clip_rgb_tone(r: &mut f32, g: &mut f32, b: &mut f32, l: f32) { + let new_r = r.min(l); + let new_b = b.min(l); + let new_g = new_b + ((new_r - new_b) * (*g - *b) / (*r - *b)); + *r = new_r; + *g = new_g; + *b = new_b; +} + +/// Soft clipping out-of-bounds values in S-curve +/// +/// Works only on highlights, negative values are skipped +#[inline] +pub fn filmlike_clip(rgb: Rgb) -> Rgb { + const L: f32 = 1.; + let mut rgb = rgb; + if rgb.r >= rgb.g { + if rgb.g > rgb.b { + filmlike_clip_rgb_tone(&mut rgb.r, &mut rgb.g, &mut rgb.b, L); + } else if rgb.b > rgb.r { + filmlike_clip_rgb_tone(&mut rgb.b, &mut rgb.r, &mut rgb.g, L); + } else if rgb.b > rgb.g { + filmlike_clip_rgb_tone(&mut rgb.r, &mut rgb.b, &mut rgb.g, L); + } else { + Rgb::new(rgb.r.min(L), rgb.g.min(L), rgb.g); + } + } else if rgb.r >= rgb.b { + filmlike_clip_rgb_tone(&mut rgb.g, &mut rgb.r, &mut rgb.b, L); + } else if rgb.b > rgb.g { + filmlike_clip_rgb_tone(&mut rgb.b, &mut rgb.g, &mut rgb.r, L); + } else { + filmlike_clip_rgb_tone(&mut rgb.g, &mut rgb.b, &mut rgb.r, L); + } + rgb +} diff --git a/deps/moxcms/src/helpers.rs b/deps/moxcms/src/helpers.rs new file mode 100644 index 0000000..155e2b4 --- /dev/null +++ b/deps/moxcms/src/helpers.rs @@ -0,0 +1,223 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::matan::{ + does_curve_have_discontinuity, is_curve_ascending, is_curve_degenerated, is_curve_descending, + is_curve_linear8, is_curve_linear16, is_curve_monotonic, +}; +use crate::reader::{ + s15_fixed16_number_to_double, uint8_number_to_float_fast, uint16_number_to_float_fast, +}; +use crate::{CmsError, LutStore, Matrix3d, ToneReprCurve, Vector3d}; + +impl LutStore { + pub fn to_clut_f32(&self) -> Vec { + match self { + LutStore::Store8(store) => store + .iter() + .map(|x| uint8_number_to_float_fast(*x)) + .collect(), + LutStore::Store16(store) => store + .iter() + .map(|x| uint16_number_to_float_fast(*x as u32)) + .collect(), + } + } + + pub(crate) fn is_degenerated(&self, entries: usize, channel: usize) -> bool { + let start = entries * channel; + let end = start + entries; + + match &self { + LutStore::Store8(v) => is_curve_degenerated(&v[start..end]), + LutStore::Store16(v) => is_curve_degenerated(&v[start..end]), + } + } + + pub(crate) fn is_monotonic(&self, entries: usize, channel: usize) -> bool { + let start = entries * channel; + let end = start + entries; + + match &self { + LutStore::Store8(v) => is_curve_monotonic(&v[start..end]), + LutStore::Store16(v) => is_curve_monotonic(&v[start..end]), + } + } + + pub(crate) fn have_discontinuities(&self, entries: usize, channel: usize) -> bool { + let start = entries * channel; + let end = start + entries; + + match &self { + LutStore::Store8(v) => does_curve_have_discontinuity(&v[start..end]), + LutStore::Store16(v) => does_curve_have_discontinuity(&v[start..end]), + } + } + + #[allow(dead_code)] + pub(crate) fn is_linear(&self, entries: usize, channel: usize) -> bool { + let start = entries * channel; + let end = start + entries; + + match &self { + LutStore::Store8(v) => is_curve_linear8(&v[start..end]), + LutStore::Store16(v) => is_curve_linear16(&v[start..end]), + } + } + + #[allow(dead_code)] + pub(crate) fn is_descending(&self, entries: usize, channel: usize) -> bool { + let start = entries * channel; + let end = start + entries; + + match &self { + LutStore::Store8(v) => is_curve_descending(&v[start..end]), + LutStore::Store16(v) => is_curve_descending(&v[start..end]), + } + } + + #[allow(dead_code)] + pub(crate) fn is_ascending(&self, entries: usize, channel: usize) -> bool { + let start = entries * channel; + let end = start + entries; + + match &self { + LutStore::Store8(v) => is_curve_ascending(&v[start..end]), + LutStore::Store16(v) => is_curve_ascending(&v[start..end]), + } + } +} + +impl ToneReprCurve { + pub(crate) fn is_linear(&self) -> bool { + match &self { + ToneReprCurve::Lut(lut) => { + if lut.is_empty() { + return true; + } + if lut.len() == 1 { + let gamma = 1. / crate::trc::u8_fixed_8number_to_float(lut[0]); + if (gamma - 1.).abs() < 1e-4 { + return true; + } + } + is_curve_linear16(lut) + } + ToneReprCurve::Parametric(parametric) => { + if parametric.is_empty() { + return true; + } + if parametric.len() == 1 && parametric[0] == 1. { + return true; + } + false + } + } + } + + pub(crate) fn is_monotonic(&self) -> bool { + match &self { + ToneReprCurve::Lut(lut) => is_curve_monotonic(lut), + ToneReprCurve::Parametric(_) => true, + } + } + + pub(crate) fn is_degenerated(&self) -> bool { + match &self { + ToneReprCurve::Lut(lut) => is_curve_degenerated(lut), + ToneReprCurve::Parametric(_) => false, + } + } + + pub(crate) fn have_discontinuities(&self) -> bool { + match &self { + ToneReprCurve::Lut(lut) => does_curve_have_discontinuity(lut), + ToneReprCurve::Parametric(_) => false, + } + } +} + +pub(crate) fn read_matrix_3d(arr: &[u8]) -> Result { + if arr.len() < 36 { + return Err(CmsError::InvalidProfile); + } + + let m_tag = &arr[..36]; + + let e00 = i32::from_be_bytes([m_tag[0], m_tag[1], m_tag[2], m_tag[3]]); + let e01 = i32::from_be_bytes([m_tag[4], m_tag[5], m_tag[6], m_tag[7]]); + let e02 = i32::from_be_bytes([m_tag[8], m_tag[9], m_tag[10], m_tag[11]]); + + let e10 = i32::from_be_bytes([m_tag[12], m_tag[13], m_tag[14], m_tag[15]]); + let e11 = i32::from_be_bytes([m_tag[16], m_tag[17], m_tag[18], m_tag[19]]); + let e12 = i32::from_be_bytes([m_tag[20], m_tag[21], m_tag[22], m_tag[23]]); + + let e20 = i32::from_be_bytes([m_tag[24], m_tag[25], m_tag[26], m_tag[27]]); + let e21 = i32::from_be_bytes([m_tag[28], m_tag[29], m_tag[30], m_tag[31]]); + let e22 = i32::from_be_bytes([m_tag[32], m_tag[33], m_tag[34], m_tag[35]]); + + Ok(Matrix3d { + v: [ + [ + s15_fixed16_number_to_double(e00), + s15_fixed16_number_to_double(e01), + s15_fixed16_number_to_double(e02), + ], + [ + s15_fixed16_number_to_double(e10), + s15_fixed16_number_to_double(e11), + s15_fixed16_number_to_double(e12), + ], + [ + s15_fixed16_number_to_double(e20), + s15_fixed16_number_to_double(e21), + s15_fixed16_number_to_double(e22), + ], + ], + }) +} + +pub(crate) fn read_vector_3d(arr: &[u8]) -> Result { + if arr.len() < 12 { + return Err(CmsError::InvalidProfile); + } + + let m_tag = &arr[..12]; + + let b0 = i32::from_be_bytes([m_tag[0], m_tag[1], m_tag[2], m_tag[3]]); + let b1 = i32::from_be_bytes([m_tag[4], m_tag[5], m_tag[6], m_tag[7]]); + let b2 = i32::from_be_bytes([m_tag[8], m_tag[9], m_tag[10], m_tag[11]]); + + Ok(Vector3d { + v: [ + s15_fixed16_number_to_double(b0), + s15_fixed16_number_to_double(b1), + s15_fixed16_number_to_double(b2), + ], + }) +} diff --git a/deps/moxcms/src/ictcp.rs b/deps/moxcms/src/ictcp.rs new file mode 100644 index 0000000..0b40180 --- /dev/null +++ b/deps/moxcms/src/ictcp.rs @@ -0,0 +1,192 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::gamma::{pq_from_linearf, pq_to_linearf}; +use crate::{Matrix3f, Rgb, Vector3f, Xyz}; + +const CROSSTALK: Matrix3f = Matrix3f { + v: [[0.92, 0.04, 0.04], [0.04, 0.92, 0.04], [0.04, 0.04, 0.92]], +}; + +const HPE_LMS: Matrix3f = Matrix3f { + v: [ + [0.4002, 0.7076, -0.0808], + [-0.2263, 1.1653, 0.0457], + [0f32, 0f32, 0.9182], + ], +}; + +const XYZ_TO_LMS: Matrix3f = CROSSTALK.mat_mul_const(HPE_LMS); + +const LMS_TO_XYZ: Matrix3f = XYZ_TO_LMS.inverse(); + +const L_LMS_TO_ICTCP: Matrix3f = Matrix3f { + v: [ + [2048. / 4096., 2048. / 4096., 0.], + [6610. / 4096., -13613. / 4096., 7003. / 4096.], + [17933. / 4096., -17390. / 4096., -543. / 4096.], + ], +}; + +const ICTCP_TO_L_LMS: Matrix3f = L_LMS_TO_ICTCP.inverse(); + +#[derive(Copy, Clone, Default, PartialOrd, PartialEq)] +pub struct ICtCp { + /// Lightness + pub i: f32, + /// Tritan + pub ct: f32, + /// Protan + pub cp: f32, +} + +impl ICtCp { + #[inline] + pub const fn new(i: f32, ct: f32, cp: f32) -> ICtCp { + ICtCp { i, ct, cp } + } + + /// Converts XYZ D65 to ICtCp + #[inline] + pub fn from_xyz(xyz: Xyz) -> ICtCp { + let lms = XYZ_TO_LMS.mul_vector(xyz.to_vector()); + let lin_l = pq_from_linearf(lms.v[0]); + let lin_m = pq_from_linearf(lms.v[1]); + let lin_s = pq_from_linearf(lms.v[2]); + let ictcp = L_LMS_TO_ICTCP.mul_vector(Vector3f { + v: [lin_l, lin_m, lin_s], + }); + ICtCp { + i: ictcp.v[0], + ct: ictcp.v[1], + cp: ictcp.v[2], + } + } + + /// Converts to [ICtCp] from linear light [Rgb] + /// + /// Precompute forward matrix by [ICtCp::prepare_to_lms]. + /// D65 white point is assumed. + #[inline] + pub fn from_linear_rgb(rgb: Rgb, matrix: Matrix3f) -> ICtCp { + let lms = matrix.mul_vector(rgb.to_vector()); + let lin_l = pq_from_linearf(lms.v[0]); + let lin_m = pq_from_linearf(lms.v[1]); + let lin_s = pq_from_linearf(lms.v[2]); + let ictcp = L_LMS_TO_ICTCP.mul_vector(Vector3f { + v: [lin_l, lin_m, lin_s], + }); + ICtCp { + i: ictcp.v[0], + ct: ictcp.v[1], + cp: ictcp.v[2], + } + } + + /// Converts [ICtCp] to [Rgb] + /// + /// Precompute forward matrix by [ICtCp::prepare_to_lms] and then inverse it + #[inline] + pub fn to_linear_rgb(&self, matrix: Matrix3f) -> Rgb { + let l_lms = ICTCP_TO_L_LMS.mul_vector(Vector3f { + v: [self.i, self.ct, self.cp], + }); + let gamma_l = pq_to_linearf(l_lms.v[0]); + let gamma_m = pq_to_linearf(l_lms.v[1]); + let gamma_s = pq_to_linearf(l_lms.v[2]); + + let lms = matrix.mul_vector(Vector3f { + v: [gamma_l, gamma_m, gamma_s], + }); + Rgb { + r: lms.v[0], + g: lms.v[1], + b: lms.v[2], + } + } + + /// Converts ICtCp to XYZ D65 + #[inline] + pub fn to_xyz(&self) -> Xyz { + let l_lms = ICTCP_TO_L_LMS.mul_vector(Vector3f { + v: [self.i, self.ct, self.cp], + }); + let gamma_l = pq_to_linearf(l_lms.v[0]); + let gamma_m = pq_to_linearf(l_lms.v[1]); + let gamma_s = pq_to_linearf(l_lms.v[2]); + + let lms = LMS_TO_XYZ.mul_vector(Vector3f { + v: [gamma_l, gamma_m, gamma_s], + }); + Xyz { + x: lms.v[0], + y: lms.v[1], + z: lms.v[2], + } + } + + /// Prepares RGB->LMS matrix + #[inline] + pub const fn prepare_to_lms(rgb_to_xyz: Matrix3f) -> Matrix3f { + XYZ_TO_LMS.mat_mul_const(rgb_to_xyz) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_roundtrip() { + let xyz = Xyz::new(0.5, 0.4, 0.3); + let ictcp = ICtCp::from_xyz(xyz); + let r_xyz = ictcp.to_xyz(); + assert!((r_xyz.x - xyz.x).abs() < 1e-4); + assert!((r_xyz.y - xyz.y).abs() < 1e-4); + assert!((r_xyz.z - xyz.z).abs() < 1e-4); + } + + #[test] + fn check_roundtrip_rgb() { + let rgb_to_xyz = Matrix3f { + v: [ + [0.67345345, 0.165661961, 0.125096574], + [0.27903071, 0.675341845, 0.045627553], + [-0.00193137419, 0.0299795717, 0.797140181], + ], + }; + let prepared_matrix = ICtCp::prepare_to_lms(rgb_to_xyz); + let inversed_matrix = prepared_matrix.inverse(); + let rgb = Rgb::new(0.5, 0.4, 0.3); + let ictcp = ICtCp::from_linear_rgb(rgb, prepared_matrix); + let r_xyz = ictcp.to_linear_rgb(inversed_matrix); + assert!((r_xyz.r - rgb.r).abs() < 1e-4); + assert!((r_xyz.g - rgb.g).abs() < 1e-4); + assert!((r_xyz.b - rgb.b).abs() < 1e-4); + } +} diff --git a/deps/moxcms/src/jzazbz.rs b/deps/moxcms/src/jzazbz.rs new file mode 100644 index 0000000..14bb5f3 --- /dev/null +++ b/deps/moxcms/src/jzazbz.rs @@ -0,0 +1,434 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::Xyz; +use crate::jzczhz::Jzczhz; +use crate::mlaf::mlaf; +use num_traits::Pow; +use pxfm::{dirty_powf, f_cbrtf, f_powf}; +use std::ops::{ + Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign, +}; + +#[inline] +fn perceptual_quantizer(x: f32) -> f32 { + if x <= 0. { + return 0.; + } + let xx = dirty_powf(x * 1e-4, 0.1593017578125); + let rs = dirty_powf( + mlaf(0.8359375, 18.8515625, xx) / mlaf(1., 18.6875, xx), + 134.034375, + ); + if rs.is_nan() { + return 0.; + } + rs +} + +#[inline] +fn perceptual_quantizer_inverse(x: f32) -> f32 { + if x <= 0. { + return 0.; + } + let xx = dirty_powf(x, 7.460772656268214e-03); + let rs = 1e4 + * dirty_powf( + (0.8359375 - xx) / mlaf(-18.8515625, 18.6875, xx), + 6.277394636015326, + ); + if rs.is_nan() { + return 0.; + } + rs +} + +#[repr(C)] +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Default)] +/// Represents Jzazbz +pub struct Jzazbz { + /// Jz(lightness) generally expects to be between `0.0..1.0`. + pub jz: f32, + /// Az generally expects to be between `-0.5..0.5`. + pub az: f32, + /// Bz generally expects to be between `-0.5..0.5`. + pub bz: f32, +} + +impl Jzazbz { + /// Constructs new instance + #[inline] + pub fn new(jz: f32, az: f32, bz: f32) -> Jzazbz { + Jzazbz { jz, az, bz } + } + + /// Creates new [Jzazbz] from CIE [Xyz]. + /// + /// JzAzBz is defined in D65 white point, adapt XYZ if needed first. + #[inline] + pub fn from_xyz(xyz: Xyz) -> Jzazbz { + Self::from_xyz_with_display_luminance(xyz, 200.) + } + + /// Creates new [Jzazbz] from CIE [Xyz]. + /// + /// JzAzBz is defined in D65 white point, adapt XYZ if needed first. + #[inline] + pub fn from_xyz_with_display_luminance(xyz: Xyz, display_luminance: f32) -> Jzazbz { + let abs_xyz = xyz * display_luminance; + let lp = perceptual_quantizer(mlaf( + mlaf(0.674207838 * abs_xyz.x, 0.382799340, abs_xyz.y), + -0.047570458, + abs_xyz.z, + )); + let mp = perceptual_quantizer(mlaf( + mlaf(0.149284160 * abs_xyz.x, 0.739628340, abs_xyz.y), + 0.083327300, + abs_xyz.z, + )); + let sp = perceptual_quantizer(mlaf( + mlaf(0.070941080 * abs_xyz.x, 0.174768000, abs_xyz.y), + 0.670970020, + abs_xyz.z, + )); + let iz = 0.5 * (lp + mp); + let az = mlaf(mlaf(3.524000 * lp, -4.066708, mp), 0.542708, sp); + let bz = mlaf(mlaf(0.199076 * lp, 1.096799, mp), -1.295875, sp); + let jz = (0.44 * iz) / mlaf(1., -0.56, iz) - 1.6295499532821566e-11; + Jzazbz::new(jz, az, bz) + } + + /// Converts [Jzazbz] to [Xyz] D65 + #[inline] + pub fn to_xyz(&self, display_luminance: f32) -> Xyz { + let jz = self.jz + 1.6295499532821566e-11; + + let iz = jz / mlaf(0.44f32, 0.56, jz); + let l = perceptual_quantizer_inverse(mlaf( + mlaf(iz, 1.386050432715393e-1, self.az), + 5.804731615611869e-2, + self.bz, + )); + let m = perceptual_quantizer_inverse(mlaf( + mlaf(iz, -1.386050432715393e-1, self.az), + -5.804731615611891e-2, + self.bz, + )); + let s = perceptual_quantizer_inverse(mlaf( + mlaf(iz, -9.601924202631895e-2, self.az), + -8.118918960560390e-1, + self.bz, + )); + let x = mlaf( + mlaf(1.661373055774069e+00 * l, -9.145230923250668e-01, m), + 2.313620767186147e-01, + s, + ); + let y = mlaf( + mlaf(-3.250758740427037e-01 * l, 1.571847038366936e+00, m), + -2.182538318672940e-01, + s, + ); + let z = mlaf( + mlaf(-9.098281098284756e-02 * l, -3.127282905230740e-01, m), + 1.522766561305260e+00, + s, + ); + let rel_luminance = 1f32 / display_luminance; + Xyz::new(x, y, z) * rel_luminance + } + + /// Converts into *Jzczhz* + #[inline] + pub fn to_jzczhz(&self) -> Jzczhz { + Jzczhz::from_jzazbz(*self) + } + + #[inline] + pub fn euclidean_distance(&self, other: Self) -> f32 { + let djz = self.jz - other.jz; + let daz = self.az - other.az; + let dbz = self.bz - other.bz; + (djz * djz + daz * daz + dbz * dbz).sqrt() + } + + #[inline] + pub fn taxicab_distance(&self, other: Self) -> f32 { + let djz = self.jz - other.jz; + let daz = self.az - other.az; + let dbz = self.bz - other.bz; + djz.abs() + daz.abs() + dbz.abs() + } +} + +impl Index for Jzazbz { + type Output = f32; + + #[inline] + fn index(&self, index: usize) -> &f32 { + match index { + 0 => &self.jz, + 1 => &self.az, + 2 => &self.bz, + _ => panic!("Index out of bounds for Jzazbz"), + } + } +} + +impl IndexMut for Jzazbz { + #[inline] + fn index_mut(&mut self, index: usize) -> &mut f32 { + match index { + 0 => &mut self.jz, + 1 => &mut self.az, + 2 => &mut self.bz, + _ => panic!("Index out of bounds for Jzazbz"), + } + } +} + +impl Add for Jzazbz { + type Output = Jzazbz; + + #[inline] + fn add(self, rhs: f32) -> Self::Output { + Jzazbz::new(self.jz + rhs, self.az + rhs, self.bz + rhs) + } +} + +impl Sub for Jzazbz { + type Output = Jzazbz; + + #[inline] + fn sub(self, rhs: f32) -> Self::Output { + Jzazbz::new(self.jz - rhs, self.az - rhs, self.bz - rhs) + } +} + +impl Mul for Jzazbz { + type Output = Jzazbz; + + #[inline] + fn mul(self, rhs: f32) -> Self::Output { + Jzazbz::new(self.jz * rhs, self.az * rhs, self.bz * rhs) + } +} + +impl Div for Jzazbz { + type Output = Jzazbz; + + #[inline] + fn div(self, rhs: f32) -> Self::Output { + Jzazbz::new(self.jz / rhs, self.az / rhs, self.bz / rhs) + } +} + +impl Add for Jzazbz { + type Output = Jzazbz; + + #[inline] + fn add(self, rhs: Jzazbz) -> Self::Output { + Jzazbz::new(self.jz + rhs.jz, self.az + rhs.az, self.bz + rhs.bz) + } +} + +impl Sub for Jzazbz { + type Output = Jzazbz; + + #[inline] + fn sub(self, rhs: Jzazbz) -> Self::Output { + Jzazbz::new(self.jz - rhs.jz, self.az - rhs.az, self.bz - rhs.bz) + } +} + +impl Mul for Jzazbz { + type Output = Jzazbz; + + #[inline] + fn mul(self, rhs: Jzazbz) -> Self::Output { + Jzazbz::new(self.jz * rhs.jz, self.az * rhs.az, self.bz * rhs.bz) + } +} + +impl Div for Jzazbz { + type Output = Jzazbz; + + #[inline] + fn div(self, rhs: Jzazbz) -> Self::Output { + Jzazbz::new(self.jz / rhs.jz, self.az / rhs.az, self.bz / rhs.bz) + } +} + +impl AddAssign for Jzazbz { + #[inline] + fn add_assign(&mut self, rhs: Jzazbz) { + self.jz += rhs.jz; + self.az += rhs.az; + self.bz += rhs.bz; + } +} + +impl SubAssign for Jzazbz { + #[inline] + fn sub_assign(&mut self, rhs: Jzazbz) { + self.jz -= rhs.jz; + self.az -= rhs.az; + self.bz -= rhs.bz; + } +} + +impl MulAssign for Jzazbz { + #[inline] + fn mul_assign(&mut self, rhs: Jzazbz) { + self.jz *= rhs.jz; + self.az *= rhs.az; + self.bz *= rhs.bz; + } +} + +impl DivAssign for Jzazbz { + #[inline] + fn div_assign(&mut self, rhs: Jzazbz) { + self.jz /= rhs.jz; + self.az /= rhs.az; + self.bz /= rhs.bz; + } +} + +impl AddAssign for Jzazbz { + #[inline] + fn add_assign(&mut self, rhs: f32) { + self.jz += rhs; + self.az += rhs; + self.bz += rhs; + } +} + +impl SubAssign for Jzazbz { + #[inline] + fn sub_assign(&mut self, rhs: f32) { + self.jz -= rhs; + self.az -= rhs; + self.bz -= rhs; + } +} + +impl MulAssign for Jzazbz { + #[inline] + fn mul_assign(&mut self, rhs: f32) { + self.jz *= rhs; + self.az *= rhs; + self.bz *= rhs; + } +} + +impl DivAssign for Jzazbz { + #[inline] + fn div_assign(&mut self, rhs: f32) { + self.jz /= rhs; + self.az /= rhs; + self.bz /= rhs; + } +} + +impl Neg for Jzazbz { + type Output = Jzazbz; + + #[inline] + fn neg(self) -> Self::Output { + Jzazbz::new(-self.jz, -self.az, -self.bz) + } +} + +impl Jzazbz { + #[inline] + pub fn sqrt(&self) -> Jzazbz { + Jzazbz::new(self.jz.sqrt(), self.az.sqrt(), self.bz.sqrt()) + } + + #[inline] + pub fn cbrt(&self) -> Jzazbz { + Jzazbz::new(f_cbrtf(self.jz), f_cbrtf(self.az), f_cbrtf(self.bz)) + } +} + +impl Pow for Jzazbz { + type Output = Jzazbz; + + #[inline] + fn pow(self, rhs: f32) -> Self::Output { + Jzazbz::new( + f_powf(self.jz, rhs), + f_powf(self.az, rhs), + f_powf(self.bz, rhs), + ) + } +} + +impl Pow for Jzazbz { + type Output = Jzazbz; + + #[inline] + fn pow(self, rhs: Jzazbz) -> Self::Output { + Jzazbz::new( + f_powf(self.jz, rhs.jz), + f_powf(self.az, self.az), + f_powf(self.bz, self.bz), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn jzazbz_round() { + let xyz = Xyz::new(0.5, 0.4, 0.3); + let jzazbz = Jzazbz::from_xyz_with_display_luminance(xyz, 253f32); + let old_xyz = jzazbz.to_xyz(253f32); + assert!( + (xyz.x - old_xyz.x).abs() <= 1e-3, + "{:?} != {:?}", + xyz, + old_xyz + ); + assert!( + (xyz.y - old_xyz.y).abs() <= 1e-3, + "{:?} != {:?}", + xyz, + old_xyz + ); + assert!( + (xyz.z - old_xyz.z).abs() <= 1e-3, + "{:?} != {:?}", + xyz, + old_xyz + ); + } +} diff --git a/deps/moxcms/src/jzczhz.rs b/deps/moxcms/src/jzczhz.rs new file mode 100644 index 0000000..422b451 --- /dev/null +++ b/deps/moxcms/src/jzczhz.rs @@ -0,0 +1,375 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::Xyz; +use crate::jzazbz::Jzazbz; +use num_traits::Pow; +use pxfm::{f_atan2f, f_cbrtf, f_hypot3f, f_hypotf, f_powf, f_sincosf, f_sinf}; +use std::ops::{ + Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign, +}; + +/// Represents Jzazbz in polar coordinates as Jzczhz +#[repr(C)] +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] +pub struct Jzczhz { + /// Jz(lightness) generally expects to be between `0.0..1.0`. + pub jz: f32, + /// Cz generally expects to be between `-1.0..1.0`. + pub cz: f32, + /// Hz generally expects to be between `-1.0..1.0`. + pub hz: f32, +} + +impl Jzczhz { + /// Creates new instance of Jzczhz + #[inline] + pub fn new(jz: f32, cz: f32, hz: f32) -> Jzczhz { + Jzczhz { jz, cz, hz } + } + + /// Converts Jzazbz to polar coordinates Jzczhz + #[inline] + pub fn from_jzazbz(jzazbz: Jzazbz) -> Jzczhz { + let cz = f_hypotf(jzazbz.az, jzazbz.bz); + let hz = f_atan2f(jzazbz.bz, jzazbz.az); + Jzczhz::new(jzazbz.jz, cz, hz) + } + + /// Converts Jzczhz into Jzazbz + #[inline] + pub fn to_jzazbz(&self) -> Jzazbz { + let sincos = f_sincosf(self.hz); + let az = self.cz * sincos.1; + let bz = self.cz * sincos.0; + Jzazbz::new(self.jz, az, bz) + } + + /// Converts Jzczhz into Jzazbz + #[inline] + pub fn to_jzazbz_with_luminance(&self) -> Jzazbz { + let sincos = f_sincosf(self.hz); + let az = self.cz * sincos.1; + let bz = self.cz * sincos.0; + Jzazbz::new(self.jz, az, bz) + } + + /// Converts Jzczhz to *Xyz* + #[inline] + pub fn to_xyz(&self, display_luminance: f32) -> Xyz { + let jzazbz = self.to_jzazbz(); + jzazbz.to_xyz(display_luminance) + } + + /// Converts [Xyz] to [Jzczhz] + #[inline] + pub fn from_xyz(xyz: Xyz) -> Jzczhz { + let jzazbz = Jzazbz::from_xyz(xyz); + Jzczhz::from_jzazbz(jzazbz) + } + + /// Converts [Xyz] to [Jzczhz] + #[inline] + pub fn from_xyz_with_display_luminance(xyz: Xyz, luminance: f32) -> Jzczhz { + let jzazbz = Jzazbz::from_xyz_with_display_luminance(xyz, luminance); + Jzczhz::from_jzazbz(jzazbz) + } + + /// Computes distance for *Jzczhz* + #[inline] + pub fn distance(&self, other: Jzczhz) -> f32 { + let djz = self.jz - other.jz; + let dcz = self.cz - other.cz; + let dhz = self.hz - other.hz; + let dh = 2. * (self.cz * other.cz).sqrt() * f_sinf(dhz * 0.5); + f_hypot3f(djz, dcz, dh) + } + + #[inline] + pub fn euclidean_distance(&self, other: Self) -> f32 { + let djz = self.jz - other.jz; + let dhz = self.hz - other.hz; + let dcz = self.cz - other.cz; + (djz * djz + dhz * dhz + dcz * dcz).sqrt() + } + + #[inline] + pub fn taxicab_distance(&self, other: Self) -> f32 { + let djz = self.jz - other.jz; + let dhz = self.hz - other.hz; + let dcz = self.cz - other.cz; + djz.abs() + dhz.abs() + dcz.abs() + } +} + +impl Index for Jzczhz { + type Output = f32; + + #[inline] + fn index(&self, index: usize) -> &f32 { + match index { + 0 => &self.jz, + 1 => &self.cz, + 2 => &self.hz, + _ => panic!("Index out of bounds for Jzczhz"), + } + } +} + +impl IndexMut for Jzczhz { + #[inline] + fn index_mut(&mut self, index: usize) -> &mut f32 { + match index { + 0 => &mut self.jz, + 1 => &mut self.cz, + 2 => &mut self.hz, + _ => panic!("Index out of bounds for Jzczhz"), + } + } +} + +impl Add for Jzczhz { + type Output = Jzczhz; + + #[inline] + fn add(self, rhs: f32) -> Self::Output { + Jzczhz::new(self.jz + rhs, self.cz + rhs, self.hz + rhs) + } +} + +impl Sub for Jzczhz { + type Output = Jzczhz; + + #[inline] + fn sub(self, rhs: f32) -> Self::Output { + Jzczhz::new(self.jz - rhs, self.cz - rhs, self.hz - rhs) + } +} + +impl Mul for Jzczhz { + type Output = Jzczhz; + + #[inline] + fn mul(self, rhs: f32) -> Self::Output { + Jzczhz::new(self.jz * rhs, self.cz * rhs, self.hz * rhs) + } +} + +impl Div for Jzczhz { + type Output = Jzczhz; + + #[inline] + fn div(self, rhs: f32) -> Self::Output { + Jzczhz::new(self.jz / rhs, self.cz / rhs, self.hz / rhs) + } +} + +impl Add for Jzczhz { + type Output = Jzczhz; + + #[inline] + fn add(self, rhs: Jzczhz) -> Self::Output { + Jzczhz::new(self.jz + rhs.jz, self.cz + rhs.cz, self.hz + rhs.hz) + } +} + +impl Sub for Jzczhz { + type Output = Jzczhz; + + #[inline] + fn sub(self, rhs: Jzczhz) -> Self::Output { + Jzczhz::new(self.jz - rhs.jz, self.cz - rhs.cz, self.hz - rhs.hz) + } +} + +impl Mul for Jzczhz { + type Output = Jzczhz; + + #[inline] + fn mul(self, rhs: Jzczhz) -> Self::Output { + Jzczhz::new(self.jz * rhs.jz, self.cz * rhs.cz, self.hz * rhs.hz) + } +} + +impl Div for Jzczhz { + type Output = Jzczhz; + + #[inline] + fn div(self, rhs: Jzczhz) -> Self::Output { + Jzczhz::new(self.jz / rhs.jz, self.cz / rhs.cz, self.hz / rhs.hz) + } +} + +impl AddAssign for Jzczhz { + #[inline] + fn add_assign(&mut self, rhs: Jzczhz) { + self.jz += rhs.jz; + self.cz += rhs.cz; + self.hz += rhs.hz; + } +} + +impl SubAssign for Jzczhz { + #[inline] + fn sub_assign(&mut self, rhs: Jzczhz) { + self.jz -= rhs.jz; + self.cz -= rhs.cz; + self.hz -= rhs.hz; + } +} + +impl MulAssign for Jzczhz { + #[inline] + fn mul_assign(&mut self, rhs: Jzczhz) { + self.jz *= rhs.jz; + self.cz *= rhs.cz; + self.hz *= rhs.hz; + } +} + +impl DivAssign for Jzczhz { + #[inline] + fn div_assign(&mut self, rhs: Jzczhz) { + self.jz /= rhs.jz; + self.cz /= rhs.cz; + self.hz /= rhs.hz; + } +} + +impl AddAssign for Jzczhz { + #[inline] + fn add_assign(&mut self, rhs: f32) { + self.jz += rhs; + self.cz += rhs; + self.hz += rhs; + } +} + +impl SubAssign for Jzczhz { + #[inline] + fn sub_assign(&mut self, rhs: f32) { + self.jz -= rhs; + self.cz -= rhs; + self.hz -= rhs; + } +} + +impl MulAssign for Jzczhz { + #[inline] + fn mul_assign(&mut self, rhs: f32) { + self.jz *= rhs; + self.cz *= rhs; + self.hz *= rhs; + } +} + +impl DivAssign for Jzczhz { + #[inline] + fn div_assign(&mut self, rhs: f32) { + self.jz /= rhs; + self.cz /= rhs; + self.hz /= rhs; + } +} + +impl Jzczhz { + #[inline] + pub fn sqrt(&self) -> Jzczhz { + Jzczhz::new(self.jz.sqrt(), self.cz.sqrt(), self.hz.sqrt()) + } + + #[inline] + pub fn cbrt(&self) -> Jzczhz { + Jzczhz::new(f_cbrtf(self.jz), f_cbrtf(self.cz), f_cbrtf(self.hz)) + } +} + +impl Pow for Jzczhz { + type Output = Jzczhz; + + #[inline] + fn pow(self, rhs: f32) -> Self::Output { + Jzczhz::new( + f_powf(self.jz, rhs), + f_powf(self.cz, rhs), + f_powf(self.hz, rhs), + ) + } +} + +impl Pow for Jzczhz { + type Output = Jzczhz; + + #[inline] + fn pow(self, rhs: Jzczhz) -> Self::Output { + Jzczhz::new( + f_powf(self.jz, rhs.jz), + f_powf(self.cz, self.cz), + f_powf(self.hz, self.hz), + ) + } +} + +impl Neg for Jzczhz { + type Output = Jzczhz; + + #[inline] + fn neg(self) -> Self::Output { + Jzczhz::new(-self.jz, -self.cz, -self.hz) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn jzczhz_round() { + let xyz = Xyz::new(0.5, 0.4, 0.3); + let jzczhz = Jzczhz::from_xyz_with_display_luminance(xyz, 253.); + let old_xyz = jzczhz.to_xyz(253f32); + assert!( + (xyz.x - old_xyz.x).abs() <= 1e-3, + "{:?} != {:?}", + xyz, + old_xyz + ); + assert!( + (xyz.y - old_xyz.y).abs() <= 1e-3, + "{:?} != {:?}", + xyz, + old_xyz + ); + assert!( + (xyz.z - old_xyz.z).abs() <= 1e-3, + "{:?} != {:?}", + xyz, + old_xyz + ); + } +} diff --git a/deps/moxcms/src/lab.rs b/deps/moxcms/src/lab.rs new file mode 100644 index 0000000..b6571e0 --- /dev/null +++ b/deps/moxcms/src/lab.rs @@ -0,0 +1,242 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::mlaf::{fmla, mlaf}; +use crate::{Chromaticity, LCh, Xyz}; +use pxfm::f_cbrtf; + +/// Holds CIE LAB values +#[repr(C)] +#[derive(Copy, Clone, Debug, Default, PartialOrd, PartialEq)] +pub struct Lab { + /// `l`: lightness component (0 to 100) + pub l: f32, + /// `a`: green (negative) and red (positive) component. + pub a: f32, + /// `b`: blue (negative) and yellow (positive) component + pub b: f32, +} + +impl Lab { + /// Create a new CIELAB color. + /// + /// # Arguments + /// + /// * `l`: lightness component (0 to 100). + /// * `a`: green (negative) and red (positive) component. + /// * `b`: blue (negative) and yellow (positive) component. + #[inline] + pub const fn new(l: f32, a: f32, b: f32) -> Self { + Self { l, a, b } + } +} + +#[inline(always)] +const fn f_1(t: f32) -> f32 { + if t <= 24.0 / 116.0 { + (108.0 / 841.0) * (t - 16.0 / 116.0) + } else { + t * t * t + } +} + +#[inline(always)] +fn f(t: f32) -> f32 { + if t <= 24. / 116. * (24. / 116.) * (24. / 116.) { + (841. / 108. * t) + 16. / 116. + } else { + f_cbrtf(t) + } +} + +impl Lab { + /// Converts to CIE Lab from CIE XYZ for PCS encoding + #[inline] + pub fn from_pcs_xyz(xyz: Xyz) -> Self { + const WP: Xyz = Chromaticity::D50.to_xyz(); + let device_x = (xyz.x as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.x as f64) as f32; + let device_y = (xyz.y as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.y as f64) as f32; + let device_z = (xyz.z as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.z as f64) as f32; + + let fx = f(device_x); + let fy = f(device_y); + let fz = f(device_z); + + let lb = mlaf(-16.0, 116.0, fy); + let a = 500.0 * (fx - fy); + let b = 200.0 * (fy - fz); + + let l = lb / 100.0; + let a = (a + 128.0) / 255.0; + let b = (b + 128.0) / 255.0; + Self::new(l, a, b) + } + + /// Converts to CIE Lab from CIE XYZ + #[inline] + pub fn from_xyz(xyz: Xyz) -> Self { + const WP: Xyz = Chromaticity::D50.to_xyz(); + let device_x = (xyz.x as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.x as f64) as f32; + let device_y = (xyz.y as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.y as f64) as f32; + let device_z = (xyz.z as f64 * (1.0f64 + 32767.0f64 / 32768.0f64) / WP.z as f64) as f32; + + let fx = f(device_x); + let fy = f(device_y); + let fz = f(device_z); + + let lb = mlaf(-16.0, 116.0, fy); + let a = 500.0 * (fx - fy); + let b = 200.0 * (fy - fz); + + Self::new(lb, a, b) + } + + /// Converts CIE [Lab] into CIE [Xyz] for PCS encoding + #[inline] + pub fn to_pcs_xyz(self) -> Xyz { + let device_l = self.l * 100.0; + let device_a = fmla(self.a, 255.0, -128.0); + let device_b = fmla(self.b, 255.0, -128.0); + + let y = (device_l + 16.0) / 116.0; + + const WP: Xyz = Chromaticity::D50.to_xyz(); + + let x = f_1(mlaf(y, 0.002, device_a)) * WP.x; + let y1 = f_1(y) * WP.y; + let z = f_1(mlaf(y, -0.005, device_b)) * WP.z; + + let x = (x as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32; + let y = (y1 as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32; + let z = (z as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32; + Xyz::new(x, y, z) + } + + /// Converts CIE [Lab] into CIE [Xyz] + #[inline] + pub fn to_xyz(self) -> Xyz { + let device_l = self.l; + let device_a = self.a; + let device_b = self.b; + + let y = (device_l + 16.0) / 116.0; + + const WP: Xyz = Chromaticity::D50.to_xyz(); + + let x = f_1(mlaf(y, 0.002, device_a)) * WP.x; + let y1 = f_1(y) * WP.y; + let z = f_1(mlaf(y, -0.005, device_b)) * WP.z; + + let x = (x as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32; + let y = (y1 as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32; + let z = (z as f64 / (1.0f64 + 32767.0f64 / 32768.0f64)) as f32; + Xyz::new(x, y, z) + } + + /// Desaturates out of gamut PCS encoded LAB + pub fn desaturate_pcs(self) -> Lab { + if self.l < 0. { + return Lab::new(0., 0., 0.); + } + + let mut new_lab = self; + if new_lab.l > 1. { + new_lab.l = 1.; + } + + let amax = 1.0; + let amin = 0.0; + let bmin = 0.0; + let bmax = 1.0; + if self.a < amin || self.a > amax || self.b < bmin || self.b > bmax { + if self.a == 0.0 { + // Is hue exactly 90? + + // atan will not work, so clamp here + new_lab.b = if new_lab.b < bmin { bmin } else { bmax }; + return Lab::new(self.l, self.a, self.b); + } + + let lch = LCh::from_lab(new_lab); + + let slope = new_lab.b / new_lab.a; + let h = lch.h * (180.0 / std::f32::consts::PI); + + // There are 4 zones + if (0. ..45.).contains(&h) || (315. ..=360.).contains(&h) { + // clip by amax + new_lab.a = amax; + new_lab.b = amax * slope; + } else if (45. ..135.).contains(&h) { + // clip by bmax + new_lab.b = bmax; + new_lab.a = bmax / slope; + } else if (135. ..225.).contains(&h) { + // clip by amin + new_lab.a = amin; + new_lab.b = amin * slope; + } else if (225. ..315.).contains(&h) { + // clip by bmin + new_lab.b = bmin; + new_lab.a = bmin / slope; + } + } + new_lab + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trip() { + let xyz = Xyz::new(0.1, 0.2, 0.3); + let lab = Lab::from_xyz(xyz); + let rolled_back = lab.to_xyz(); + let dx = (xyz.x - rolled_back.x).abs(); + let dy = (xyz.y - rolled_back.y).abs(); + let dz = (xyz.z - rolled_back.z).abs(); + assert!(dx < 1e-5); + assert!(dy < 1e-5); + assert!(dz < 1e-5); + } + + #[test] + fn round_pcs_trip() { + let xyz = Xyz::new(0.1, 0.2, 0.3); + let lab = Lab::from_pcs_xyz(xyz); + let rolled_back = lab.to_pcs_xyz(); + let dx = (xyz.x - rolled_back.x).abs(); + let dy = (xyz.y - rolled_back.y).abs(); + let dz = (xyz.z - rolled_back.z).abs(); + assert!(dx < 1e-5); + assert!(dy < 1e-5); + assert!(dz < 1e-5); + } +} diff --git a/deps/moxcms/src/lib.rs b/deps/moxcms/src/lib.rs new file mode 100644 index 0000000..6e85e79 --- /dev/null +++ b/deps/moxcms/src/lib.rs @@ -0,0 +1,133 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#![allow(clippy::manual_clamp, clippy::excessive_precision)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![deny(unreachable_pub)] +#![deny( + clippy::print_stdout, + clippy::print_stderr, + clippy::print_literal, + clippy::print_in_format_impl +)] +#![allow(stable_features)] +#![cfg_attr( + not(any(feature = "avx", feature = "sse", feature = "avx512", feature = "neon")), + forbid(unsafe_code) +)] +#![cfg_attr(all(feature = "avx512", target_arch = "x86_64"), feature(cfg_version))] +#![cfg_attr( + all(feature = "avx512", target_arch = "x86_64"), + feature(avx512_target_feature) +)] +#![cfg_attr( + all(feature = "avx512", target_arch = "x86_64"), + feature(stdarch_x86_avx512) +)] +mod chad; +mod cicp; +mod conversions; +mod dat; +mod defaults; +mod err; +mod gamma; +mod gamut; +mod ictcp; +mod jzazbz; +mod jzczhz; +mod lab; +mod luv; +/// One of main intent is to provide fast math available in const context +/// ULP most of the methods <= 0.5 +mod math; +mod matrix; +mod mlaf; +mod nd_array; +mod oklab; +mod oklch; +mod profile; +mod reader; +mod rgb; +mod safe_math; +mod tag; +mod transform; +mod trc; +mod writer; +mod yrg; +// Simple math analysis module +mod chromaticity; +mod dt_ucs; +mod helpers; +mod lut_hint; +mod matan; +mod srlab2; +mod xyy; + +pub use chad::{ + adapt_to_d50, adapt_to_d50_d, adapt_to_illuminant, adapt_to_illuminant_d, + adapt_to_illuminant_xyz, adapt_to_illuminant_xyz_d, adaption_matrix, adaption_matrix_d, +}; +pub use chromaticity::Chromaticity; +pub use cicp::{CicpColorPrimaries, ColorPrimaries, MatrixCoefficients, TransferCharacteristics}; +pub use dat::ColorDateTime; +pub use defaults::{ + HLG_LUT_TABLE, PQ_LUT_TABLE, WHITE_POINT_D50, WHITE_POINT_D60, WHITE_POINT_D65, + WHITE_POINT_DCI_P3, +}; +pub use dt_ucs::{DtUchHcb, DtUchHsb, DtUchJch}; +pub use err::{CmsError, MalformedSize}; +pub use gamut::filmlike_clip; +pub use ictcp::ICtCp; +pub use jzazbz::Jzazbz; +pub use jzczhz::Jzczhz; +pub use lab::Lab; +pub use luv::{LCh, Luv}; +pub use math::rounding_div_ceil; +pub use matrix::{ + BT2020_MATRIX, DISPLAY_P3_MATRIX, Matrix3, Matrix3d, Matrix3f, Matrix4f, SRGB_MATRIX, Vector3, + Vector3d, Vector3f, Vector3i, Vector3u, Vector4, Vector4d, Vector4f, Vector4i, Xyz, Xyzd, +}; +pub use nd_array::{Cube, Hypercube}; +pub use oklab::Oklab; +pub use oklch::Oklch; +pub use profile::{ + CicpProfile, ColorProfile, DataColorSpace, DescriptionString, LocalizableString, LutDataType, + LutMultidimensionalType, LutStore, LutType, LutWarehouse, Measurement, MeasurementGeometry, + ParsingOptions, ProfileClass, ProfileSignature, ProfileText, ProfileVersion, RenderingIntent, + StandardIlluminant, StandardObserver, TechnologySignatures, ViewingConditions, +}; +pub use rgb::{FusedExp, FusedExp2, FusedExp10, FusedLog, FusedLog2, FusedLog10, FusedPow, Rgb}; +pub use srlab2::Srlab2; +pub use transform::{ + BarycentricWeightScale, InPlaceStage, InterpolationMethod, Layout, PointeeSizeExpressible, + Stage, Transform8BitExecutor, Transform16BitExecutor, TransformExecutor, + TransformF32BitExecutor, TransformF64BitExecutor, TransformOptions, +}; +pub use trc::{GammaLutInterpolate, ToneCurveEvaluator, ToneReprCurve, curve_from_gamma}; +pub use xyy::{XyY, XyYRepresentable}; +pub use yrg::{Ych, Yrg, cie_y_1931_to_cie_y_2006}; diff --git a/deps/moxcms/src/lut_hint.rs b/deps/moxcms/src/lut_hint.rs new file mode 100644 index 0000000..121494e --- /dev/null +++ b/deps/moxcms/src/lut_hint.rs @@ -0,0 +1,106 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::LutWarehouse; + +impl LutWarehouse { + /// Method tests if mathematical fusion on LUT table is allowed. + /// If it's not, full brute-force pass in [Katana] is required. + pub(crate) fn is_katana_required(&self) -> bool { + match self { + LutWarehouse::Lut(lut) => { + let input_entries = lut.num_input_channels as usize; + let output_entries = lut.num_output_channels as usize; + for i in 0..input_entries { + if lut.input_table.is_degenerated(input_entries, i) { + return true; + } + if !lut.input_table.is_monotonic(input_entries, i) { + return true; + } + if lut.input_table.have_discontinuities(input_entries, i) { + return true; + } + } + + for i in 0..output_entries { + if lut.output_table.is_degenerated(output_entries, i) { + return true; + } + if !lut.output_table.is_monotonic(output_entries, i) { + return true; + } + if lut.output_table.have_discontinuities(output_entries, i) { + return true; + } + } + + false + } + LutWarehouse::Multidimensional(mab) => { + for curve in mab.a_curves.iter() { + if curve.is_degenerated() { + return true; + } + if !curve.is_monotonic() { + return true; + } + if curve.have_discontinuities() { + return true; + } + } + + for curve in mab.m_curves.iter() { + if curve.is_degenerated() { + return true; + } + if !curve.is_monotonic() { + return true; + } + if curve.have_discontinuities() { + return true; + } + } + + for curve in mab.b_curves.iter() { + if curve.is_degenerated() { + return true; + } + if !curve.is_monotonic() { + return true; + } + if curve.have_discontinuities() { + return true; + } + } + + false + } + } + } +} diff --git a/deps/moxcms/src/luv.rs b/deps/moxcms/src/luv.rs new file mode 100644 index 0000000..09a59fd --- /dev/null +++ b/deps/moxcms/src/luv.rs @@ -0,0 +1,698 @@ +/* + * // Copyright 2024 (c) the Radzivon Bartoshyk. All rights reserved. + * // + * // Use of this source code is governed by a BSD-style + * // license that can be found in the LICENSE file. + */ + +//! # Luv +/// Struct representing a color in CIE LUV, a.k.a. L\*u\*v\*, color space +#[repr(C)] +#[derive(Debug, Copy, Clone, Default, PartialOrd)] +pub struct Luv { + /// The L\* value (achromatic luminance) of the colour in 0–100 range. + pub l: f32, + /// The u\* value of the colour. + /// + /// Together with v\* value, it defines chromaticity of the colour. The u\* + /// coordinate represents colour’s position on red-green axis with negative + /// values indicating more red and positive more green colour. Typical + /// values are in -134–220 range (but exact range for ‘valid’ colours + /// depends on luminance and v\* value). + pub u: f32, + /// The u\* value of the colour. + /// + /// Together with u\* value, it defines chromaticity of the colour. The v\* + /// coordinate represents colour’s position on blue-yellow axis with + /// negative values indicating more blue and positive more yellow colour. + /// Typical values are in -140–122 range (but exact range for ‘valid’ + /// colours depends on luminance and u\* value). + pub v: f32, +} + +/// Representing a color in cylindrical CIE LCh(uv) color space +#[repr(C)] +#[derive(Debug, Copy, Clone, Default, PartialOrd)] +pub struct LCh { + /// The L\* value (achromatic luminance) of the colour in 0–100 range. + /// + /// This is the same value as in the [`Luv`] object. + pub l: f32, + /// The C\*_uv value (chroma) of the colour. + /// + /// Together with h_uv, it defines chromaticity of the colour. The typical + /// values of the coordinate go from zero up to around 150 (but exact range + /// for ‘valid’ colours depends on luminance and hue). Zero represents + /// shade of grey. + pub c: f32, + /// The h_uv value (hue) of the colour measured in radians. + /// + /// Together with C\*_uv, it defines chromaticity of the colour. The value + /// represents an angle thus it wraps around τ. Typically, the value will + /// be in the -π–π range. The value is undefined if C\*_uv is zero. + pub h: f32, +} + +use crate::mlaf::mlaf; +use crate::{Chromaticity, Lab, Xyz}; +use num_traits::Pow; +use pxfm::{f_atan2f, f_cbrtf, f_hypotf, f_powf, f_sincosf}; +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +pub(crate) const LUV_WHITE_U_PRIME: f32 = 4.0f32 * Chromaticity::D50.to_xyz().y + / (Chromaticity::D50.to_xyz().x + + 15.0 * Chromaticity::D50.to_xyz().y + + 3.0 * Chromaticity::D50.to_xyz().z); +pub(crate) const LUV_WHITE_V_PRIME: f32 = 9.0f32 * Chromaticity::D50.to_xyz().y + / (Chromaticity::D50.to_xyz().x + + 15.0 * Chromaticity::D50.to_xyz().y + + 3.0 * Chromaticity::D50.to_xyz().z); + +pub(crate) const LUV_CUTOFF_FORWARD_Y: f32 = (6f32 / 29f32) * (6f32 / 29f32) * (6f32 / 29f32); +pub(crate) const LUV_MULTIPLIER_FORWARD_Y: f32 = (29f32 / 3f32) * (29f32 / 3f32) * (29f32 / 3f32); +pub(crate) const LUV_MULTIPLIER_INVERSE_Y: f32 = (3f32 / 29f32) * (3f32 / 29f32) * (3f32 / 29f32); +impl Luv { + /// Converts CIE XYZ to CIE Luv using D50 white point + #[inline] + #[allow(clippy::manual_clamp)] + pub fn from_xyz(xyz: Xyz) -> Self { + let [x, y, z] = [xyz.x, xyz.y, xyz.z]; + let den = mlaf(mlaf(x, 15.0, y), 3.0, z); + + let l = (if y < LUV_CUTOFF_FORWARD_Y { + LUV_MULTIPLIER_FORWARD_Y * y + } else { + 116. * f_cbrtf(y) - 16. + }) + .min(100.) + .max(0.); + let (u, v); + if den != 0f32 { + let u_prime = 4. * x / den; + let v_prime = 9. * y / den; + u = 13. * l * (u_prime - LUV_WHITE_U_PRIME); + v = 13. * l * (v_prime - LUV_WHITE_V_PRIME); + } else { + u = 0.; + v = 0.; + } + + Luv { l, u, v } + } + + /// To [Xyz] using D50 colorimetry + #[inline] + pub fn to_xyz(&self) -> Xyz { + if self.l <= 0. { + return Xyz::new(0., 0., 0.); + } + let l13 = 1. / (13. * self.l); + let u = mlaf(LUV_WHITE_U_PRIME, self.u, l13); + let v = mlaf(LUV_WHITE_V_PRIME, self.v, l13); + let y = if self.l > 8. { + let jx = (self.l + 16.) / 116.; + jx * jx * jx + } else { + self.l * LUV_MULTIPLIER_INVERSE_Y + }; + let (x, z); + if v != 0. { + let den = 1. / (4. * v); + x = y * 9. * u * den; + z = y * mlaf(mlaf(12.0, -3.0, u), -20., v) * den; + } else { + x = 0.; + z = 0.; + } + + Xyz::new(x, y, z) + } + + #[inline] + pub const fn new(l: f32, u: f32, v: f32) -> Luv { + Luv { l, u, v } + } +} + +impl LCh { + #[inline] + pub const fn new(l: f32, c: f32, h: f32) -> Self { + LCh { l, c, h } + } + + /// Converts Lab to LCh(uv) + #[inline] + pub fn from_luv(luv: Luv) -> Self { + LCh { + l: luv.l, + c: f_hypotf(luv.u, luv.v), + h: f_atan2f(luv.v, luv.u), + } + } + + /// Converts Lab to LCh(ab) + #[inline] + pub fn from_lab(lab: Lab) -> Self { + LCh { + l: lab.l, + c: f_hypotf(lab.a, lab.b), + h: f_atan2f(lab.b, lab.a), + } + } + + /// Computes LCh(uv) + #[inline] + pub fn from_xyz(xyz: Xyz) -> Self { + Self::from_luv(Luv::from_xyz(xyz)) + } + + /// Computes LCh(ab) + #[inline] + pub fn from_xyz_lab(xyz: Xyz) -> Self { + Self::from_lab(Lab::from_xyz(xyz)) + } + + /// Converts LCh(uv) to Luv + #[inline] + pub fn to_xyz(&self) -> Xyz { + self.to_luv().to_xyz() + } + + /// Converts LCh(ab) to Lab + #[inline] + pub fn to_xyz_lab(&self) -> Xyz { + self.to_lab().to_xyz() + } + + #[inline] + pub fn to_luv(&self) -> Luv { + let sincos = f_sincosf(self.h); + Luv { + l: self.l, + u: self.c * sincos.1, + v: self.c * sincos.0, + } + } + + #[inline] + pub fn to_lab(&self) -> Lab { + let sincos = f_sincosf(self.h); + Lab { + l: self.l, + a: self.c * sincos.1, + b: self.c * sincos.0, + } + } +} + +impl PartialEq for Luv { + /// Compares two colours ignoring chromaticity if L\* is zero. + #[inline] + fn eq(&self, other: &Self) -> bool { + if self.l != other.l { + false + } else if self.l == 0.0 { + true + } else { + self.u == other.u && self.v == other.v + } + } +} + +impl PartialEq for LCh { + /// Compares two colours ignoring chromaticity if L\* is zero and hue if C\* + /// is zero. Hues which are τ apart are compared equal. + #[inline] + fn eq(&self, other: &Self) -> bool { + if self.l != other.l { + false + } else if self.l == 0.0 { + true + } else if self.c != other.c { + false + } else if self.c == 0.0 { + true + } else { + use std::f32::consts::TAU; + self.h.rem_euclid(TAU) == other.h.rem_euclid(TAU) + } + } +} + +impl Luv { + #[inline] + pub fn euclidean_distance(&self, other: Luv) -> f32 { + let dl = self.l - other.l; + let du = self.u - other.u; + let dv = self.v - other.v; + (dl * dl + du * du + dv * dv).sqrt() + } +} + +impl LCh { + #[inline] + pub fn euclidean_distance(&self, other: LCh) -> f32 { + let dl = self.l - other.l; + let dc = self.c - other.c; + let dh = self.h - other.h; + (dl * dl + dc * dc + dh * dh).sqrt() + } +} + +impl Luv { + #[inline] + pub const fn taxicab_distance(&self, other: Self) -> f32 { + let dl = self.l - other.l; + let du = self.u - other.u; + let dv = self.v - other.v; + dl.abs() + du.abs() + dv.abs() + } +} + +impl LCh { + #[inline] + pub const fn taxicab_distance(&self, other: Self) -> f32 { + let dl = self.l - other.l; + let dc = self.c - other.c; + let dh = self.h - other.h; + dl.abs() + dc.abs() + dh.abs() + } +} + +impl Add for Luv { + type Output = Luv; + + #[inline] + fn add(self, rhs: Luv) -> Luv { + Luv::new(self.l + rhs.l, self.u + rhs.u, self.v + rhs.v) + } +} + +impl Add for LCh { + type Output = LCh; + + #[inline] + fn add(self, rhs: LCh) -> LCh { + LCh::new(self.l + rhs.l, self.c + rhs.c, self.h + rhs.h) + } +} + +impl Sub for Luv { + type Output = Luv; + + #[inline] + fn sub(self, rhs: Luv) -> Luv { + Luv::new(self.l - rhs.l, self.u - rhs.u, self.v - rhs.v) + } +} + +impl Sub for LCh { + type Output = LCh; + + #[inline] + fn sub(self, rhs: LCh) -> LCh { + LCh::new(self.l - rhs.l, self.c - rhs.c, self.h - rhs.h) + } +} + +impl Mul for Luv { + type Output = Luv; + + #[inline] + fn mul(self, rhs: Luv) -> Luv { + Luv::new(self.l * rhs.l, self.u * rhs.u, self.v * rhs.v) + } +} + +impl Mul for LCh { + type Output = LCh; + + #[inline] + fn mul(self, rhs: LCh) -> LCh { + LCh::new(self.l * rhs.l, self.c * rhs.c, self.h * rhs.h) + } +} + +impl Div for Luv { + type Output = Luv; + + #[inline] + fn div(self, rhs: Luv) -> Luv { + Luv::new(self.l / rhs.l, self.u / rhs.u, self.v / rhs.v) + } +} + +impl Div for LCh { + type Output = LCh; + + #[inline] + fn div(self, rhs: LCh) -> LCh { + LCh::new(self.l / rhs.l, self.c / rhs.c, self.h / rhs.h) + } +} + +impl Add for Luv { + type Output = Luv; + + #[inline] + fn add(self, rhs: f32) -> Self::Output { + Luv::new(self.l + rhs, self.u + rhs, self.v + rhs) + } +} + +impl Add for LCh { + type Output = LCh; + + #[inline] + fn add(self, rhs: f32) -> Self::Output { + LCh::new(self.l + rhs, self.c + rhs, self.h + rhs) + } +} + +impl Sub for Luv { + type Output = Luv; + + #[inline] + fn sub(self, rhs: f32) -> Self::Output { + Luv::new(self.l - rhs, self.u - rhs, self.v - rhs) + } +} + +impl Sub for LCh { + type Output = LCh; + + #[inline] + fn sub(self, rhs: f32) -> Self::Output { + LCh::new(self.l - rhs, self.c - rhs, self.h - rhs) + } +} + +impl Mul for Luv { + type Output = Luv; + + #[inline] + fn mul(self, rhs: f32) -> Self::Output { + Luv::new(self.l * rhs, self.u * rhs, self.v * rhs) + } +} + +impl Mul for LCh { + type Output = LCh; + + #[inline] + fn mul(self, rhs: f32) -> Self::Output { + LCh::new(self.l * rhs, self.c * rhs, self.h * rhs) + } +} + +impl Div for Luv { + type Output = Luv; + + #[inline] + fn div(self, rhs: f32) -> Self::Output { + Luv::new(self.l / rhs, self.u / rhs, self.v / rhs) + } +} + +impl Div for LCh { + type Output = LCh; + + #[inline] + fn div(self, rhs: f32) -> Self::Output { + LCh::new(self.l / rhs, self.c / rhs, self.h / rhs) + } +} + +impl AddAssign for Luv { + #[inline] + fn add_assign(&mut self, rhs: Luv) { + self.l += rhs.l; + self.u += rhs.u; + self.v += rhs.v; + } +} + +impl AddAssign for LCh { + #[inline] + fn add_assign(&mut self, rhs: LCh) { + self.l += rhs.l; + self.c += rhs.c; + self.h += rhs.h; + } +} + +impl SubAssign for Luv { + #[inline] + fn sub_assign(&mut self, rhs: Luv) { + self.l -= rhs.l; + self.u -= rhs.u; + self.v -= rhs.v; + } +} + +impl SubAssign for LCh { + #[inline] + fn sub_assign(&mut self, rhs: LCh) { + self.l -= rhs.l; + self.c -= rhs.c; + self.h -= rhs.h; + } +} + +impl MulAssign for Luv { + #[inline] + fn mul_assign(&mut self, rhs: Luv) { + self.l *= rhs.l; + self.u *= rhs.u; + self.v *= rhs.v; + } +} + +impl MulAssign for LCh { + #[inline] + fn mul_assign(&mut self, rhs: LCh) { + self.l *= rhs.l; + self.c *= rhs.c; + self.h *= rhs.h; + } +} + +impl DivAssign for Luv { + #[inline] + fn div_assign(&mut self, rhs: Luv) { + self.l /= rhs.l; + self.u /= rhs.u; + self.v /= rhs.v; + } +} + +impl DivAssign for LCh { + #[inline] + fn div_assign(&mut self, rhs: LCh) { + self.l /= rhs.l; + self.c /= rhs.c; + self.h /= rhs.h; + } +} + +impl AddAssign for Luv { + #[inline] + fn add_assign(&mut self, rhs: f32) { + self.l += rhs; + self.u += rhs; + self.v += rhs; + } +} + +impl AddAssign for LCh { + #[inline] + fn add_assign(&mut self, rhs: f32) { + self.l += rhs; + self.c += rhs; + self.h += rhs; + } +} + +impl SubAssign for Luv { + #[inline] + fn sub_assign(&mut self, rhs: f32) { + self.l -= rhs; + self.u -= rhs; + self.v -= rhs; + } +} + +impl SubAssign for LCh { + #[inline] + fn sub_assign(&mut self, rhs: f32) { + self.l -= rhs; + self.c -= rhs; + self.h -= rhs; + } +} + +impl MulAssign for Luv { + #[inline] + fn mul_assign(&mut self, rhs: f32) { + self.l *= rhs; + self.u *= rhs; + self.v *= rhs; + } +} + +impl MulAssign for LCh { + #[inline] + fn mul_assign(&mut self, rhs: f32) { + self.l *= rhs; + self.c *= rhs; + self.h *= rhs; + } +} + +impl DivAssign for Luv { + #[inline] + fn div_assign(&mut self, rhs: f32) { + self.l /= rhs; + self.u /= rhs; + self.v /= rhs; + } +} + +impl DivAssign for LCh { + #[inline] + fn div_assign(&mut self, rhs: f32) { + self.l /= rhs; + self.c /= rhs; + self.h /= rhs; + } +} + +impl Neg for LCh { + type Output = LCh; + + #[inline] + fn neg(self) -> Self::Output { + LCh::new(-self.l, -self.c, -self.h) + } +} + +impl Neg for Luv { + type Output = Luv; + + #[inline] + fn neg(self) -> Self::Output { + Luv::new(-self.l, -self.u, -self.v) + } +} + +impl Pow for Luv { + type Output = Luv; + + #[inline] + fn pow(self, rhs: f32) -> Self::Output { + Luv::new( + f_powf(self.l, rhs), + f_powf(self.u, rhs), + f_powf(self.v, rhs), + ) + } +} + +impl Pow for LCh { + type Output = LCh; + + #[inline] + fn pow(self, rhs: f32) -> Self::Output { + LCh::new( + f_powf(self.l, rhs), + f_powf(self.c, rhs), + f_powf(self.h, rhs), + ) + } +} + +impl Pow for Luv { + type Output = Luv; + + #[inline] + fn pow(self, rhs: Luv) -> Self::Output { + Luv::new( + f_powf(self.l, rhs.l), + f_powf(self.u, rhs.u), + f_powf(self.v, rhs.v), + ) + } +} + +impl Pow for LCh { + type Output = LCh; + + #[inline] + fn pow(self, rhs: LCh) -> Self::Output { + LCh::new( + f_powf(self.l, rhs.l), + f_powf(self.c, rhs.c), + f_powf(self.h, rhs.h), + ) + } +} + +impl Luv { + #[inline] + pub fn sqrt(&self) -> Luv { + Luv::new(self.l.sqrt(), self.u.sqrt(), self.v.sqrt()) + } + + #[inline] + pub fn cbrt(&self) -> Luv { + Luv::new(f_cbrtf(self.l), f_cbrtf(self.u), f_cbrtf(self.v)) + } +} + +impl LCh { + #[inline] + pub fn sqrt(&self) -> LCh { + LCh::new( + if self.l < 0. { 0. } else { self.l.sqrt() }, + if self.c < 0. { 0. } else { self.c.sqrt() }, + if self.h < 0. { 0. } else { self.h.sqrt() }, + ) + } + + #[inline] + pub fn cbrt(&self) -> LCh { + LCh::new(f_cbrtf(self.l), f_cbrtf(self.c), f_cbrtf(self.h)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trip_luv() { + let xyz = Xyz::new(0.1, 0.2, 0.3); + let lab = Luv::from_xyz(xyz); + let rolled_back = lab.to_xyz(); + let dx = (xyz.x - rolled_back.x).abs(); + let dy = (xyz.y - rolled_back.y).abs(); + let dz = (xyz.z - rolled_back.z).abs(); + assert!(dx < 1e-5); + assert!(dy < 1e-5); + assert!(dz < 1e-5); + } + + #[test] + fn round_trip_lch() { + let xyz = Xyz::new(0.1, 0.2, 0.3); + let luv = Luv::from_xyz(xyz); + let lab = LCh::from_luv(luv); + let rolled_back = lab.to_luv(); + let dx = (luv.l - rolled_back.l).abs(); + let dy = (luv.u - rolled_back.u).abs(); + let dz = (luv.v - rolled_back.v).abs(); + assert!(dx < 1e-4); + assert!(dy < 1e-4); + assert!(dz < 1e-4); + } +} diff --git a/deps/moxcms/src/matan/curve_shape.rs b/deps/moxcms/src/matan/curve_shape.rs new file mode 100644 index 0000000..1d60ec4 --- /dev/null +++ b/deps/moxcms/src/matan/curve_shape.rs @@ -0,0 +1,72 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +pub(crate) fn is_curve_linear16(curve: &[u16]) -> bool { + let scale = 1. / (curve.len() - 1) as f32 * 65535.; + for (index, &value) in curve.iter().enumerate() { + let quantized = (index as f32 * scale).round() as u16; + let diff = (quantized as i32 - value as i32).abs(); + if diff > 0x0f { + return false; + } + } + true +} + +pub(crate) fn is_curve_descending(v: &[T]) -> bool { + if v.is_empty() { + return false; + } + if v.len() == 1 { + return false; + } + v[0] > v[v.len() - 1] +} + +pub(crate) fn is_curve_ascending(v: &[T]) -> bool { + if v.is_empty() { + return false; + } + if v.len() == 1 { + return false; + } + v[0] < v[v.len() - 1] +} + +pub(crate) fn is_curve_linear8(curve: &[u8]) -> bool { + let scale = 1. / (curve.len() - 1) as f32 * 255.; + for (index, &value) in curve.iter().enumerate() { + let quantized = (index as f32 * scale).round() as u16; + let diff = (quantized as i32 - value as i32).abs(); + if diff > 0x03 { + return false; + } + } + true +} diff --git a/deps/moxcms/src/matan/degeneration.rs b/deps/moxcms/src/matan/degeneration.rs new file mode 100644 index 0000000..f4251e1 --- /dev/null +++ b/deps/moxcms/src/matan/degeneration.rs @@ -0,0 +1,60 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#[derive(Copy, Clone, Default, Debug)] +struct DegenerationAmount { + leading: usize, + trailing: usize, +} + +/// Counts amount of duplicates on each side of curve +fn count_leading_trailing_duplicated(lut: &[T]) -> DegenerationAmount { + if lut.is_empty() { + return DegenerationAmount::default(); + } + let first = lut.first().unwrap(); + let last = lut.last().unwrap(); + let leading = lut.iter().take_while(|&v| v.eq(first)).count(); + let trailing = lut.iter().rev().take_while(|&v| v.eq(last)).count(); + DegenerationAmount { leading, trailing } +} + +/// Finds out if curve is degenerated on the sides. +pub(crate) fn is_curve_degenerated(v: &[T]) -> bool { + if v.is_empty() || v.len() < 2 { + return false; + } + let degeneration_amount = count_leading_trailing_duplicated(v); + if degeneration_amount.trailing <= 1 && degeneration_amount.leading <= 1 { + return false; + } + let leading_percentage = degeneration_amount.leading; + let trailing_percentage = degeneration_amount.trailing; + ((leading_percentage / 20) > 0) || ((trailing_percentage / 20) > 0) +} diff --git a/deps/moxcms/src/matan/discontinuity.rs b/deps/moxcms/src/matan/discontinuity.rs new file mode 100644 index 0000000..456255d --- /dev/null +++ b/deps/moxcms/src/matan/discontinuity.rs @@ -0,0 +1,74 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use num_traits::AsPrimitive; + +pub(crate) trait DiscontinuitySpike { + const SPIKE: f64; +} + +impl DiscontinuitySpike for u8 { + const SPIKE: f64 = 16.0; +} + +impl DiscontinuitySpike for u16 { + const SPIKE: f64 = 2100.; +} + +impl DiscontinuitySpike for f32 { + const SPIKE: f64 = 0.07; +} + +/// Searches LUT curve for discontinuity +pub(crate) fn does_curve_have_discontinuity< + T: Copy + PartialEq + DiscontinuitySpike + AsPrimitive + 'static, +>( + curve: &[T], +) -> bool { + if curve.len() < 2 { + return false; + } + let threshold: f64 = T::SPIKE; + let mut discontinuities = 0u64; + let mut previous_element: f64 = curve[0].as_(); + let diff: f64 = (curve[1].as_() - previous_element).abs(); + if diff > threshold { + discontinuities += 1; + } + for element in curve.iter().skip(1) { + let new_diff: f64 = (element.as_() - previous_element).abs(); + if new_diff > threshold { + discontinuities += 1; + if discontinuities > 3 { + break; + } + } + previous_element = element.as_(); + } + discontinuities > 3 +} diff --git a/deps/moxcms/src/matan/mod.rs b/deps/moxcms/src/matan/mod.rs new file mode 100644 index 0000000..10938b9 --- /dev/null +++ b/deps/moxcms/src/matan/mod.rs @@ -0,0 +1,40 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +mod curve_shape; +mod degeneration; +mod discontinuity; +mod monotonic; +mod slope_limit; + +pub(crate) use curve_shape::{ + is_curve_ascending, is_curve_descending, is_curve_linear8, is_curve_linear16, +}; +pub(crate) use degeneration::is_curve_degenerated; +pub(crate) use discontinuity::does_curve_have_discontinuity; +pub(crate) use monotonic::is_curve_monotonic; diff --git a/deps/moxcms/src/matan/monotonic.rs b/deps/moxcms/src/matan/monotonic.rs new file mode 100644 index 0000000..25f02ff --- /dev/null +++ b/deps/moxcms/src/matan/monotonic.rs @@ -0,0 +1,52 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::matan::is_curve_ascending; + +/// Finds out if curve is monotonic. +pub(crate) fn is_curve_monotonic(lut: &[T]) -> bool { + if lut.len() < 2 { + return true; + } + let is_ascending = is_curve_ascending(lut); + let mut violations = 0usize; + if is_ascending { + for (current, previous) in lut.iter().skip(1).zip(lut.iter().take(lut.len() - 1)) { + if current.lt(previous) { + violations += 1; + } + } + } else { + for (current, previous) in lut.iter().skip(1).zip(lut.iter().take(lut.len() - 1)) { + if current.gt(previous) { + violations += 1; + } + } + } + (violations as f64 / lut.len() as f64) < 0.05 +} diff --git a/deps/moxcms/src/matan/slope_limit.rs b/deps/moxcms/src/matan/slope_limit.rs new file mode 100644 index 0000000..6cfe17c --- /dev/null +++ b/deps/moxcms/src/matan/slope_limit.rs @@ -0,0 +1,84 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#![allow(dead_code)] +use crate::PointeeSizeExpressible; +use crate::matan::is_curve_descending; +use num_traits::AsPrimitive; + +pub(crate) fn limit_slope + PartialOrd + PointeeSizeExpressible>( + curve: &mut [T], + value_cap: f32, +) where + f32: AsPrimitive, +{ + let at_begin = (curve.len() as f32 * 0.02 + 0.5).floor() as usize; // Cutoff at 2% + if at_begin == 0 { + return; + } + let at_end = curve.len() - at_begin - 1; // And 98% + let (begin_val, end_val) = if is_curve_descending(curve) { + (value_cap, 0.) + } else { + (0., value_cap) + }; + let val = curve[at_begin].as_(); + let slope = (val - begin_val) / at_begin as f32; + let beta = val - slope * at_begin as f32; + if T::FINITE { + for v in curve.iter_mut().take(at_begin) { + *v = (v.as_() * slope + beta) + .round() + .min(value_cap) + .max(0.0) + .as_(); + } + } else { + for v in curve.iter_mut().take(at_begin) { + *v = (v.as_() * slope + beta).min(value_cap).max(0.0).as_(); + } + } + + let val = curve[at_end].as_(); + let slope = (end_val - val) / at_begin as f32; + let beta = val - slope * at_end as f32; + + if T::FINITE { + for v in curve.iter_mut().skip(at_end) { + *v = (v.as_() * slope + beta) + .round() + .min(value_cap) + .max(0.0) + .as_(); + } + } else { + for v in curve.iter_mut().skip(at_end) { + *v = (v.as_() * slope + beta).min(value_cap).max(0.0).as_(); + } + } +} diff --git a/deps/moxcms/src/math/mod.rs b/deps/moxcms/src/math/mod.rs new file mode 100644 index 0000000..e8f46c8 --- /dev/null +++ b/deps/moxcms/src/math/mod.rs @@ -0,0 +1,68 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#![allow(clippy::approx_constant, clippy::manual_range_contains)] + +use num_traits::Num; + +#[inline(always)] +pub const fn rounding_div_ceil(value: i32, div: i32) -> i32 { + (value + div - 1) / div +} + +// Generic function for max +#[inline(always)] +pub(crate) fn m_max(a: T, b: T) -> T { + if a > b { a } else { b } +} + +// Generic function for min +#[inline(always)] +pub(crate) fn m_min(a: T, b: T) -> T { + if a < b { a } else { b } +} + +#[inline] +pub(crate) fn m_clamp(a: T, min: T, max: T) -> T { + if a > max { + max + } else if a >= min { + a + } else { + // a < min or a is NaN + min + } +} + +pub trait FusedMultiplyAdd { + fn mla(&self, b: T, c: T) -> T; +} + +pub(crate) trait FusedMultiplyNegAdd { + fn neg_mla(&self, b: T, c: T) -> T; +} diff --git a/deps/moxcms/src/matrix.rs b/deps/moxcms/src/matrix.rs new file mode 100644 index 0000000..320719c --- /dev/null +++ b/deps/moxcms/src/matrix.rs @@ -0,0 +1,1272 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::math::{FusedMultiplyAdd, FusedMultiplyNegAdd}; +use crate::mlaf::{mlaf, neg_mlaf}; +use crate::reader::s15_fixed16_number_to_double; +use num_traits::{AsPrimitive, MulAdd}; +use std::ops::{Add, Div, Mul, Neg, Shr, Sub}; + +/// Vector math helper +#[repr(transparent)] +#[derive(Copy, Clone, Debug, Default)] +pub struct Vector3 { + pub v: [T; 3], +} + +/// Vector math helper +#[repr(transparent)] +#[derive(Copy, Clone, Debug, Default)] +pub struct Vector4 { + pub v: [T; 4], +} + +pub type Vector4f = Vector4; +pub type Vector4d = Vector4; +pub type Vector4i = Vector4; + +pub type Vector3f = Vector3; +pub type Vector3d = Vector3; +pub type Vector3i = Vector3; +pub type Vector3u = Vector3; + +impl PartialEq for Vector3 +where + T: AsPrimitive, +{ + #[inline(always)] + fn eq(&self, other: &Self) -> bool { + const TOLERANCE: f32 = 0.0001f32; + let dx = (self.v[0].as_() - other.v[0].as_()).abs(); + let dy = (self.v[1].as_() - other.v[1].as_()).abs(); + let dz = (self.v[2].as_() - other.v[2].as_()).abs(); + dx < TOLERANCE && dy < TOLERANCE && dz < TOLERANCE + } +} + +impl Vector3 { + #[inline(always)] + pub fn to_(self) -> Vector3 + where + T: AsPrimitive, + { + Vector3 { + v: [self.v[0].as_(), self.v[1].as_(), self.v[2].as_()], + } + } +} + +impl Mul> for Vector3 +where + T: Mul + Copy, +{ + type Output = Vector3; + + #[inline(always)] + fn mul(self, rhs: Vector3) -> Self::Output { + Self { + v: [ + self.v[0] * rhs.v[0], + self.v[1] * rhs.v[1], + self.v[2] * rhs.v[2], + ], + } + } +} + +impl Shr for Vector3 +where + T: Shr, +{ + type Output = Vector3; + fn shr(self, rhs: i32) -> Self::Output { + Self { + v: [self.v[0] >> rhs, self.v[1] >> rhs, self.v[2] >> rhs], + } + } +} + +impl Shr for Vector4 +where + T: Shr, +{ + type Output = Vector4; + fn shr(self, rhs: i32) -> Self::Output { + Self { + v: [ + self.v[0] >> rhs, + self.v[1] >> rhs, + self.v[2] >> rhs, + self.v[3] >> rhs, + ], + } + } +} + +impl Mul> for Vector4 +where + T: Mul + Copy, +{ + type Output = Vector4; + + #[inline(always)] + fn mul(self, rhs: Vector4) -> Self::Output { + Self { + v: [ + self.v[0] * rhs.v[0], + self.v[1] * rhs.v[1], + self.v[2] * rhs.v[2], + self.v[3] * rhs.v[3], + ], + } + } +} + +impl Mul for Vector3 +where + T: Mul + Copy, +{ + type Output = Vector3; + + #[inline(always)] + fn mul(self, rhs: T) -> Self::Output { + Self { + v: [self.v[0] * rhs, self.v[1] * rhs, self.v[2] * rhs], + } + } +} + +impl Vector3 { + #[inline(always)] + const fn const_mul_vector(self, v: Vector3f) -> Vector3f { + Vector3f { + v: [self.v[0] * v.v[0], self.v[1] * v.v[1], self.v[2] * v.v[2]], + } + } +} + +impl Vector3d { + #[inline(always)] + const fn const_mul_vector(self, v: Vector3d) -> Vector3d { + Vector3d { + v: [self.v[0] * v.v[0], self.v[1] * v.v[1], self.v[2] * v.v[2]], + } + } +} + +impl Vector3 { + pub fn cast(&self) -> Vector3 + where + T: AsPrimitive, + { + Vector3:: { + v: [self.v[0].as_(), self.v[1].as_(), self.v[2].as_()], + } + } +} + +impl Mul for Vector4 +where + T: Mul + Copy, +{ + type Output = Vector4; + + #[inline(always)] + fn mul(self, rhs: T) -> Self::Output { + Self { + v: [ + self.v[0] * rhs, + self.v[1] * rhs, + self.v[2] * rhs, + self.v[3] * rhs, + ], + } + } +} + +impl + Add + MulAdd> + FusedMultiplyAdd> for Vector3 +{ + #[inline(always)] + fn mla(&self, b: Vector3, c: Vector3) -> Vector3 { + let x0 = mlaf(self.v[0], b.v[0], c.v[0]); + let x1 = mlaf(self.v[1], b.v[1], c.v[1]); + let x2 = mlaf(self.v[2], b.v[2], c.v[2]); + Vector3 { v: [x0, x1, x2] } + } +} + +impl + Add + MulAdd + Neg> + FusedMultiplyNegAdd> for Vector3 +{ + #[inline(always)] + fn neg_mla(&self, b: Vector3, c: Vector3) -> Vector3 { + let x0 = neg_mlaf(self.v[0], b.v[0], c.v[0]); + let x1 = neg_mlaf(self.v[1], b.v[1], c.v[1]); + let x2 = neg_mlaf(self.v[2], b.v[2], c.v[2]); + Vector3 { v: [x0, x1, x2] } + } +} + +impl + Add + MulAdd> + FusedMultiplyAdd> for Vector4 +{ + #[inline(always)] + fn mla(&self, b: Vector4, c: Vector4) -> Vector4 { + let x0 = mlaf(self.v[0], b.v[0], c.v[0]); + let x1 = mlaf(self.v[1], b.v[1], c.v[1]); + let x2 = mlaf(self.v[2], b.v[2], c.v[2]); + let x3 = mlaf(self.v[3], b.v[3], c.v[3]); + Vector4 { + v: [x0, x1, x2, x3], + } + } +} + +impl + Add + MulAdd + Neg> + FusedMultiplyNegAdd> for Vector4 +{ + #[inline(always)] + fn neg_mla(&self, b: Vector4, c: Vector4) -> Vector4 { + let x0 = neg_mlaf(self.v[0], b.v[0], c.v[0]); + let x1 = neg_mlaf(self.v[1], b.v[1], c.v[1]); + let x2 = neg_mlaf(self.v[2], b.v[2], c.v[2]); + let x3 = neg_mlaf(self.v[3], b.v[3], c.v[3]); + Vector4 { + v: [x0, x1, x2, x3], + } + } +} + +impl From for Vector3 +where + T: Copy, +{ + fn from(value: T) -> Self { + Self { + v: [value, value, value], + } + } +} + +impl From for Vector4 +where + T: Copy, +{ + fn from(value: T) -> Self { + Self { + v: [value, value, value, value], + } + } +} + +impl Add> for Vector3 +where + T: Add + Copy, +{ + type Output = Vector3; + + #[inline(always)] + fn add(self, rhs: Vector3) -> Self::Output { + Self { + v: [ + self.v[0] + rhs.v[0], + self.v[1] + rhs.v[1], + self.v[2] + rhs.v[2], + ], + } + } +} + +impl Add> for Vector4 +where + T: Add + Copy, +{ + type Output = Vector4; + + #[inline(always)] + fn add(self, rhs: Vector4) -> Self::Output { + Self { + v: [ + self.v[0] + rhs.v[0], + self.v[1] + rhs.v[1], + self.v[2] + rhs.v[2], + self.v[3] + rhs.v[3], + ], + } + } +} + +impl Add for Vector3 +where + T: Add + Copy, +{ + type Output = Vector3; + + #[inline(always)] + fn add(self, rhs: T) -> Self::Output { + Self { + v: [self.v[0] + rhs, self.v[1] + rhs, self.v[2] + rhs], + } + } +} + +impl Add for Vector4 +where + T: Add + Copy, +{ + type Output = Vector4; + + #[inline(always)] + fn add(self, rhs: T) -> Self::Output { + Self { + v: [ + self.v[0] + rhs, + self.v[1] + rhs, + self.v[2] + rhs, + self.v[3] + rhs, + ], + } + } +} + +impl Sub> for Vector3 +where + T: Sub + Copy, +{ + type Output = Vector3; + + #[inline(always)] + fn sub(self, rhs: Vector3) -> Self::Output { + Self { + v: [ + self.v[0] - rhs.v[0], + self.v[1] - rhs.v[1], + self.v[2] - rhs.v[2], + ], + } + } +} + +impl Sub> for Vector4 +where + T: Sub + Copy, +{ + type Output = Vector4; + + #[inline(always)] + fn sub(self, rhs: Vector4) -> Self::Output { + Self { + v: [ + self.v[0] - rhs.v[0], + self.v[1] - rhs.v[1], + self.v[2] - rhs.v[2], + self.v[3] - rhs.v[3], + ], + } + } +} + +/// Matrix math helper +#[repr(C)] +#[derive(Copy, Clone, Debug, Default)] +pub struct Matrix3f { + pub v: [[f32; 3]; 3], +} + +/// Matrix math helper +#[repr(C)] +#[derive(Copy, Clone, Debug, Default)] +pub struct Matrix3d { + pub v: [[f64; 3]; 3], +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, Default)] +pub struct Matrix3 { + pub v: [[T; 3]; 3], +} + +impl Matrix3 { + #[inline] + #[allow(dead_code)] + pub(crate) fn transpose(&self) -> Matrix3 { + Matrix3 { + v: [ + [self.v[0][0], self.v[1][0], self.v[2][0]], + [self.v[0][1], self.v[1][1], self.v[2][1]], + [self.v[0][2], self.v[1][2], self.v[2][2]], + ], + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, Default)] +pub struct Matrix4f { + pub v: [[f32; 4]; 4], +} + +pub const SRGB_MATRIX: Matrix3d = Matrix3d { + v: [ + [ + s15_fixed16_number_to_double(0x6FA2), + s15_fixed16_number_to_double(0x6299), + s15_fixed16_number_to_double(0x24A0), + ], + [ + s15_fixed16_number_to_double(0x38F5), + s15_fixed16_number_to_double(0xB785), + s15_fixed16_number_to_double(0x0F84), + ], + [ + s15_fixed16_number_to_double(0x0390), + s15_fixed16_number_to_double(0x18DA), + s15_fixed16_number_to_double(0xB6CF), + ], + ], +}; + +pub const DISPLAY_P3_MATRIX: Matrix3d = Matrix3d { + v: [ + [0.515102, 0.291965, 0.157153], + [0.241182, 0.692236, 0.0665819], + [-0.00104941, 0.0418818, 0.784378], + ], +}; + +pub const BT2020_MATRIX: Matrix3d = Matrix3d { + v: [ + [0.673459, 0.165661, 0.125100], + [0.279033, 0.675338, 0.0456288], + [-0.00193139, 0.0299794, 0.797162], + ], +}; + +impl Matrix4f { + #[inline] + pub fn determinant(&self) -> Option { + let a = self.v[0][0]; + let b = self.v[0][1]; + let c = self.v[0][2]; + let d = self.v[0][3]; + + // Cofactor expansion + + let m11 = Matrix3f { + v: [ + [self.v[1][1], self.v[1][2], self.v[1][3]], + [self.v[2][1], self.v[2][2], self.v[2][3]], + [self.v[3][1], self.v[3][2], self.v[3][3]], + ], + }; + + let m12 = Matrix3f { + v: [ + [self.v[1][0], self.v[1][2], self.v[1][3]], + [self.v[2][0], self.v[2][2], self.v[2][3]], + [self.v[3][0], self.v[3][2], self.v[3][3]], + ], + }; + + let m13 = Matrix3f { + v: [ + [self.v[1][0], self.v[1][1], self.v[1][3]], + [self.v[2][0], self.v[2][1], self.v[2][3]], + [self.v[3][0], self.v[3][1], self.v[3][3]], + ], + }; + + let m14 = Matrix3f { + v: [ + [self.v[1][0], self.v[1][1], self.v[1][2]], + [self.v[2][0], self.v[2][1], self.v[2][2]], + [self.v[3][0], self.v[3][1], self.v[3][2]], + ], + }; + + let m1_det = m11.determinant()?; + let m2_det = m12.determinant()?; + let m3_det = m13.determinant()?; + let m4_det = m14.determinant()?; + + // Apply cofactor expansion on the first row + Some(a * m1_det - b * m2_det + c * m3_det - d * m4_det) + } +} + +impl Matrix3f { + #[inline] + pub fn transpose(&self) -> Matrix3f { + Matrix3f { + v: [ + [self.v[0][0], self.v[1][0], self.v[2][0]], + [self.v[0][1], self.v[1][1], self.v[2][1]], + [self.v[0][2], self.v[1][2], self.v[2][2]], + ], + } + } + + pub const IDENTITY: Matrix3f = Matrix3f { + v: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], + }; + + #[inline] + pub const fn test_equality(&self, other: Matrix3f) -> bool { + const TOLERANCE: f32 = 0.001f32; + let diff_r_x = (self.v[0][0] - other.v[0][0]).abs(); + let diff_r_y = (self.v[0][1] - other.v[0][1]).abs(); + let diff_r_z = (self.v[0][2] - other.v[0][2]).abs(); + + if diff_r_x > TOLERANCE || diff_r_y > TOLERANCE || diff_r_z > TOLERANCE { + return false; + } + + let diff_g_x = (self.v[1][0] - other.v[1][0]).abs(); + let diff_g_y = (self.v[1][1] - other.v[1][1]).abs(); + let diff_g_z = (self.v[1][2] - other.v[1][2]).abs(); + + if diff_g_x > TOLERANCE || diff_g_y > TOLERANCE || diff_g_z > TOLERANCE { + return false; + } + + let diff_b_x = (self.v[2][0] - other.v[2][0]).abs(); + let diff_b_y = (self.v[2][1] - other.v[2][1]).abs(); + let diff_b_z = (self.v[2][2] - other.v[2][2]).abs(); + + if diff_b_x > TOLERANCE || diff_b_y > TOLERANCE || diff_b_z > TOLERANCE { + return false; + } + + true + } + + #[inline] + pub const fn determinant(&self) -> Option { + let v = self.v; + let a0 = v[0][0] * v[1][1] * v[2][2]; + let a1 = v[0][1] * v[1][2] * v[2][0]; + let a2 = v[0][2] * v[1][0] * v[2][1]; + + let s0 = v[0][2] * v[1][1] * v[2][0]; + let s1 = v[0][1] * v[1][0] * v[2][2]; + let s2 = v[0][0] * v[1][2] * v[2][1]; + + let j = a0 + a1 + a2 - s0 - s1 - s2; + if j == 0. { + return None; + } + Some(j) + } + + #[inline] + pub const fn inverse(&self) -> Self { + let v = self.v; + let det = self.determinant(); + match det { + None => Matrix3f::IDENTITY, + Some(determinant) => { + let det = 1. / determinant; + let a = v[0][0]; + let b = v[0][1]; + let c = v[0][2]; + let d = v[1][0]; + let e = v[1][1]; + let f = v[1][2]; + let g = v[2][0]; + let h = v[2][1]; + let i = v[2][2]; + + Matrix3f { + v: [ + [ + (e * i - f * h) * det, + (c * h - b * i) * det, + (b * f - c * e) * det, + ], + [ + (f * g - d * i) * det, + (a * i - c * g) * det, + (c * d - a * f) * det, + ], + [ + (d * h - e * g) * det, + (b * g - a * h) * det, + (a * e - b * d) * det, + ], + ], + } + } + } + } + + #[inline] + pub fn mul_row(&self, rhs: f32) -> Self { + if R == 0 { + Self { + v: [(Vector3f { v: self.v[0] } * rhs).v, self.v[1], self.v[2]], + } + } else if R == 1 { + Self { + v: [self.v[0], (Vector3f { v: self.v[1] } * rhs).v, self.v[2]], + } + } else if R == 2 { + Self { + v: [self.v[0], self.v[1], (Vector3f { v: self.v[2] } * rhs).v], + } + } else { + unimplemented!() + } + } + + #[inline] + pub const fn mul_row_vector(&self, rhs: Vector3f) -> Self { + if R == 0 { + Self { + v: [ + (Vector3f { v: self.v[0] }.const_mul_vector(rhs)).v, + self.v[1], + self.v[2], + ], + } + } else if R == 1 { + Self { + v: [ + self.v[0], + (Vector3f { v: self.v[1] }.const_mul_vector(rhs)).v, + self.v[2], + ], + } + } else if R == 2 { + Self { + v: [ + self.v[0], + self.v[1], + (Vector3f { v: self.v[2] }.const_mul_vector(rhs)).v, + ], + } + } else { + unimplemented!() + } + } + + #[inline] + pub const fn mul_vector(&self, other: Vector3f) -> Vector3f { + let x = self.v[0][1] * other.v[1] + self.v[0][2] * other.v[2] + self.v[0][0] * other.v[0]; + let y = self.v[1][0] * other.v[0] + self.v[1][1] * other.v[1] + self.v[1][2] * other.v[2]; + let z = self.v[2][0] * other.v[0] + self.v[2][1] * other.v[1] + self.v[2][2] * other.v[2]; + Vector3f { v: [x, y, z] } + } + + /// Multiply using FMA + #[inline] + pub fn f_mul_vector(&self, other: Vector3f) -> Vector3f { + let x = mlaf( + mlaf(self.v[0][1] * other.v[1], self.v[0][2], other.v[2]), + self.v[0][0], + other.v[0], + ); + let y = mlaf( + mlaf(self.v[1][0] * other.v[0], self.v[1][1], other.v[1]), + self.v[1][2], + other.v[2], + ); + let z = mlaf( + mlaf(self.v[2][0] * other.v[0], self.v[2][1], other.v[1]), + self.v[2][2], + other.v[2], + ); + Vector3f { v: [x, y, z] } + } + + #[inline] + pub fn mat_mul(&self, other: Matrix3f) -> Self { + let mut result = Matrix3f::default(); + + for i in 0..3 { + for j in 0..3 { + result.v[i][j] = mlaf( + mlaf(self.v[i][0] * other.v[0][j], self.v[i][1], other.v[1][j]), + self.v[i][2], + other.v[2][j], + ); + } + } + + result + } + + #[inline] + pub const fn mat_mul_const(&self, other: Matrix3f) -> Self { + let mut result = Matrix3f { v: [[0f32; 3]; 3] }; + let mut i = 0usize; + while i < 3 { + let mut j = 0usize; + while j < 3 { + result.v[i][j] = self.v[i][0] * other.v[0][j] + + self.v[i][1] * other.v[1][j] + + self.v[i][2] * other.v[2][j]; + j += 1; + } + i += 1; + } + + result + } + + #[inline] + pub const fn to_f64(&self) -> Matrix3d { + Matrix3d { + v: [ + [ + self.v[0][0] as f64, + self.v[0][1] as f64, + self.v[0][2] as f64, + ], + [ + self.v[1][0] as f64, + self.v[1][1] as f64, + self.v[1][2] as f64, + ], + [ + self.v[2][0] as f64, + self.v[2][1] as f64, + self.v[2][2] as f64, + ], + ], + } + } +} + +impl Matrix3d { + #[inline] + pub fn transpose(&self) -> Matrix3d { + Matrix3d { + v: [ + [self.v[0][0], self.v[1][0], self.v[2][0]], + [self.v[0][1], self.v[1][1], self.v[2][1]], + [self.v[0][2], self.v[1][2], self.v[2][2]], + ], + } + } + + pub const IDENTITY: Matrix3d = Matrix3d { + v: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], + }; + + #[inline] + pub const fn test_equality(&self, other: Matrix3d) -> bool { + const TOLERANCE: f64 = 0.001f64; + let diff_r_x = (self.v[0][0] - other.v[0][0]).abs(); + let diff_r_y = (self.v[0][1] - other.v[0][1]).abs(); + let diff_r_z = (self.v[0][2] - other.v[0][2]).abs(); + + if diff_r_x > TOLERANCE || diff_r_y > TOLERANCE || diff_r_z > TOLERANCE { + return false; + } + + let diff_g_x = (self.v[1][0] - other.v[1][0]).abs(); + let diff_g_y = (self.v[1][1] - other.v[1][1]).abs(); + let diff_g_z = (self.v[1][2] - other.v[1][2]).abs(); + + if diff_g_x > TOLERANCE || diff_g_y > TOLERANCE || diff_g_z > TOLERANCE { + return false; + } + + let diff_b_x = (self.v[2][0] - other.v[2][0]).abs(); + let diff_b_y = (self.v[2][1] - other.v[2][1]).abs(); + let diff_b_z = (self.v[2][2] - other.v[2][2]).abs(); + + if diff_b_x > TOLERANCE || diff_b_y > TOLERANCE || diff_b_z > TOLERANCE { + return false; + } + + true + } + + #[inline] + pub const fn determinant(&self) -> Option { + let v = self.v; + let a0 = v[0][0] * v[1][1] * v[2][2]; + let a1 = v[0][1] * v[1][2] * v[2][0]; + let a2 = v[0][2] * v[1][0] * v[2][1]; + + let s0 = v[0][2] * v[1][1] * v[2][0]; + let s1 = v[0][1] * v[1][0] * v[2][2]; + let s2 = v[0][0] * v[1][2] * v[2][1]; + + let j = a0 + a1 + a2 - s0 - s1 - s2; + if j == 0. { + return None; + } + Some(j) + } + + #[inline] + pub const fn inverse(&self) -> Self { + let v = self.v; + let det = self.determinant(); + match det { + None => Matrix3d::IDENTITY, + Some(determinant) => { + let det = 1. / determinant; + let a = v[0][0]; + let b = v[0][1]; + let c = v[0][2]; + let d = v[1][0]; + let e = v[1][1]; + let f = v[1][2]; + let g = v[2][0]; + let h = v[2][1]; + let i = v[2][2]; + + Matrix3d { + v: [ + [ + (e * i - f * h) * det, + (c * h - b * i) * det, + (b * f - c * e) * det, + ], + [ + (f * g - d * i) * det, + (a * i - c * g) * det, + (c * d - a * f) * det, + ], + [ + (d * h - e * g) * det, + (b * g - a * h) * det, + (a * e - b * d) * det, + ], + ], + } + } + } + } + + #[inline] + pub fn mul_row(&self, rhs: f64) -> Self { + if R == 0 { + Self { + v: [(Vector3d { v: self.v[0] } * rhs).v, self.v[1], self.v[2]], + } + } else if R == 1 { + Self { + v: [self.v[0], (Vector3d { v: self.v[1] } * rhs).v, self.v[2]], + } + } else if R == 2 { + Self { + v: [self.v[0], self.v[1], (Vector3d { v: self.v[2] } * rhs).v], + } + } else { + unimplemented!() + } + } + + #[inline] + pub const fn mul_row_vector(&self, rhs: Vector3d) -> Self { + if R == 0 { + Self { + v: [ + (Vector3d { v: self.v[0] }.const_mul_vector(rhs)).v, + self.v[1], + self.v[2], + ], + } + } else if R == 1 { + Self { + v: [ + self.v[0], + (Vector3d { v: self.v[1] }.const_mul_vector(rhs)).v, + self.v[2], + ], + } + } else if R == 2 { + Self { + v: [ + self.v[0], + self.v[1], + (Vector3d { v: self.v[2] }.const_mul_vector(rhs)).v, + ], + } + } else { + unimplemented!() + } + } + + #[inline] + pub const fn mul_vector(&self, other: Vector3d) -> Vector3d { + let x = self.v[0][1] * other.v[1] + self.v[0][2] * other.v[2] + self.v[0][0] * other.v[0]; + let y = self.v[1][0] * other.v[0] + self.v[1][1] * other.v[1] + self.v[1][2] * other.v[2]; + let z = self.v[2][0] * other.v[0] + self.v[2][1] * other.v[1] + self.v[2][2] * other.v[2]; + Vector3:: { v: [x, y, z] } + } + + #[inline] + pub fn mat_mul(&self, other: Matrix3d) -> Self { + let mut result = Matrix3d::default(); + + for i in 0..3 { + for j in 0..3 { + result.v[i][j] = mlaf( + mlaf(self.v[i][0] * other.v[0][j], self.v[i][1], other.v[1][j]), + self.v[i][2], + other.v[2][j], + ); + } + } + + result + } + + #[inline] + pub const fn mat_mul_const(&self, other: Matrix3d) -> Self { + let mut result = Matrix3d { v: [[0.; 3]; 3] }; + let mut i = 0usize; + while i < 3 { + let mut j = 0usize; + while j < 3 { + result.v[i][j] = self.v[i][0] * other.v[0][j] + + self.v[i][1] * other.v[1][j] + + self.v[i][2] * other.v[2][j]; + j += 1; + } + i += 1; + } + + result + } + + #[inline] + pub const fn to_f32(&self) -> Matrix3f { + Matrix3f { + v: [ + [ + self.v[0][0] as f32, + self.v[0][1] as f32, + self.v[0][2] as f32, + ], + [ + self.v[1][0] as f32, + self.v[1][1] as f32, + self.v[1][2] as f32, + ], + [ + self.v[2][0] as f32, + self.v[2][1] as f32, + self.v[2][2] as f32, + ], + ], + } + } +} + +impl Mul for Matrix3f { + type Output = Matrix3f; + + #[inline] + fn mul(self, rhs: Matrix3f) -> Self::Output { + self.mat_mul(rhs) + } +} + +impl Mul for Matrix3d { + type Output = Matrix3d; + + #[inline] + fn mul(self, rhs: Matrix3d) -> Self::Output { + self.mat_mul(rhs) + } +} + +/// Holds CIE XYZ representation +#[repr(C)] +#[derive(Clone, Debug, Copy, Default)] +pub struct Xyz { + pub x: f32, + pub y: f32, + pub z: f32, +} + +impl Xyz { + #[inline] + pub fn to_xyy(&self) -> [f32; 3] { + let sums = self.x + self.y + self.z; + if sums == 0. { + return [0., 0., self.y]; + } + let x = self.x / sums; + let y = self.y / sums; + let yb = self.y; + [x, y, yb] + } + + #[inline] + pub fn from_xyy(xyy: [f32; 3]) -> Xyz { + let reciprocal = if xyy[1] != 0. { + 1. / xyy[1] * xyy[2] + } else { + 0. + }; + let x = xyy[0] * reciprocal; + let y = xyy[2]; + let z = (1. - xyy[0] - xyy[1]) * reciprocal; + Xyz { x, y, z } + } +} + +/// Holds CIE XYZ representation, in double precision +#[repr(C)] +#[derive(Clone, Debug, Copy, Default)] +pub struct Xyzd { + pub x: f64, + pub y: f64, + pub z: f64, +} + +macro_rules! define_xyz { + ($xyz_name:ident, $im_type: ident, $matrix: ident) => { + impl PartialEq for $xyz_name { + #[inline] + fn eq(&self, other: &Self) -> bool { + const TOLERANCE: $im_type = 0.0001; + let dx = (self.x - other.x).abs(); + let dy = (self.y - other.y).abs(); + let dz = (self.z - other.z).abs(); + dx < TOLERANCE && dy < TOLERANCE && dz < TOLERANCE + } + } + + impl $xyz_name { + #[inline] + pub const fn new(x: $im_type, y: $im_type, z: $im_type) -> Self { + Self { x, y, z } + } + + #[inline] + pub const fn to_vector(self) -> Vector3f { + Vector3f { + v: [self.x as f32, self.y as f32, self.z as f32], + } + } + + #[inline] + pub const fn to_vector_d(self) -> Vector3d { + Vector3d { + v: [self.x as f64, self.y as f64, self.z as f64], + } + } + + #[inline] + pub fn matrix_mul(&self, matrix: $matrix) -> Self { + let x = mlaf( + mlaf(self.x * matrix.v[0][0], self.y, matrix.v[0][1]), + self.z, + matrix.v[0][2], + ); + let y = mlaf( + mlaf(self.x * matrix.v[1][0], self.y, matrix.v[1][1]), + self.z, + matrix.v[1][2], + ); + let z = mlaf( + mlaf(self.x * matrix.v[2][0], self.y, matrix.v[2][1]), + self.z, + matrix.v[2][2], + ); + Self::new(x, y, z) + } + + #[inline] + pub fn from_linear_rgb(rgb: crate::Rgb<$im_type>, rgb_to_xyz: $matrix) -> Self { + let r = rgb.r; + let g = rgb.g; + let b = rgb.b; + + let transform = rgb_to_xyz; + + let new_r = mlaf( + mlaf(r * transform.v[0][0], g, transform.v[0][1]), + b, + transform.v[0][2], + ); + + let new_g = mlaf( + mlaf(r * transform.v[1][0], g, transform.v[1][1]), + b, + transform.v[1][2], + ); + + let new_b = mlaf( + mlaf(r * transform.v[2][0], g, transform.v[2][1]), + b, + transform.v[2][2], + ); + + $xyz_name::new(new_r, new_g, new_b) + } + + #[inline] + pub fn normalize(self) -> Self { + if self.y == 0. { + return Self { + x: 0., + y: 1.0, + z: 0.0, + }; + } + let reciprocal = 1. / self.y; + Self { + x: self.x * reciprocal, + y: 1.0, + z: self.z * reciprocal, + } + } + + #[inline] + pub fn to_linear_rgb(self, rgb_to_xyz: $matrix) -> crate::Rgb<$im_type> { + let x = self.x; + let y = self.y; + let z = self.z; + + let transform = rgb_to_xyz; + + let new_r = mlaf( + mlaf(x * transform.v[0][0], y, transform.v[0][1]), + z, + transform.v[0][2], + ); + + let new_g = mlaf( + mlaf(x * transform.v[1][0], y, transform.v[1][1]), + z, + transform.v[1][2], + ); + + let new_b = mlaf( + mlaf(x * transform.v[2][0], y, transform.v[2][1]), + z, + transform.v[2][2], + ); + + crate::Rgb::<$im_type>::new(new_r, new_g, new_b) + } + } + + impl Mul<$im_type> for $xyz_name { + type Output = $xyz_name; + + #[inline] + fn mul(self, rhs: $im_type) -> Self::Output { + Self { + x: self.x * rhs, + y: self.y * rhs, + z: self.z * rhs, + } + } + } + + impl Mul<$matrix> for $xyz_name { + type Output = $xyz_name; + + #[inline] + fn mul(self, rhs: $matrix) -> Self::Output { + self.matrix_mul(rhs) + } + } + + impl Mul<$xyz_name> for $xyz_name { + type Output = $xyz_name; + + #[inline] + fn mul(self, rhs: $xyz_name) -> Self::Output { + Self { + x: self.x * rhs.x, + y: self.y * rhs.y, + z: self.z * rhs.z, + } + } + } + + impl Div<$xyz_name> for $xyz_name { + type Output = $xyz_name; + + #[inline] + fn div(self, rhs: $xyz_name) -> Self::Output { + Self { + x: self.x / rhs.x, + y: self.y / rhs.y, + z: self.z / rhs.z, + } + } + } + + impl Div<$im_type> for $xyz_name { + type Output = $xyz_name; + + #[inline] + fn div(self, rhs: $im_type) -> Self::Output { + Self { + x: self.x / rhs, + y: self.y / rhs, + z: self.z / rhs, + } + } + } + }; +} + +impl Xyz { + pub fn to_xyzd(self) -> Xyzd { + Xyzd { + x: self.x as f64, + y: self.y as f64, + z: self.z as f64, + } + } +} + +impl Xyzd { + pub fn to_xyz(self) -> Xyz { + Xyz { + x: self.x as f32, + y: self.y as f32, + z: self.z as f32, + } + } + + pub fn to_xyzd(self) -> Xyzd { + Xyzd { + x: self.x, + y: self.y, + z: self.z, + } + } +} + +define_xyz!(Xyz, f32, Matrix3f); +define_xyz!(Xyzd, f64, Matrix3d); diff --git a/deps/moxcms/src/mlaf.rs b/deps/moxcms/src/mlaf.rs new file mode 100644 index 0000000..80d0493 --- /dev/null +++ b/deps/moxcms/src/mlaf.rs @@ -0,0 +1,82 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use num_traits::MulAdd; +use std::ops::{Add, Mul, Neg}; + +#[cfg(any( + all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "fma" + ), + all(target_arch = "aarch64", target_feature = "neon") +))] +#[inline(always)] +pub(crate) fn mlaf + Add + MulAdd>( + acc: T, + a: T, + b: T, +) -> T { + MulAdd::mul_add(a, b, acc) +} + +#[inline(always)] +#[cfg(not(any( + all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "fma" + ), + all(target_arch = "aarch64", target_feature = "neon") +)))] +pub(crate) fn mlaf + Add + MulAdd>( + acc: T, + a: T, + b: T, +) -> T { + acc + a * b +} + +#[inline(always)] +pub(crate) fn neg_mlaf< + T: Copy + Mul + Add + MulAdd + Neg, +>( + acc: T, + a: T, + b: T, +) -> T { + mlaf(acc, a, -b) +} + +#[inline(always)] +pub(crate) fn fmla + Add + MulAdd>( + a: T, + b: T, + acc: T, +) -> T { + mlaf(acc, a, b) +} diff --git a/deps/moxcms/src/nd_array.rs b/deps/moxcms/src/nd_array.rs new file mode 100644 index 0000000..dc87d44 --- /dev/null +++ b/deps/moxcms/src/nd_array.rs @@ -0,0 +1,1239 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::math::{FusedMultiplyAdd, FusedMultiplyNegAdd}; +use crate::mlaf::{mlaf, neg_mlaf}; +use crate::{Vector3f, Vector4f}; +use std::ops::{Add, Mul, Sub}; + +impl FusedMultiplyAdd for f32 { + #[inline(always)] + fn mla(&self, b: f32, c: f32) -> f32 { + mlaf(*self, b, c) + } +} + +impl FusedMultiplyNegAdd for f32 { + #[inline(always)] + fn neg_mla(&self, b: f32, c: f32) -> f32 { + neg_mlaf(*self, b, c) + } +} + +#[inline(always)] +pub(crate) fn lerp< + T: Mul + + Sub + + Add + + From + + Copy + + FusedMultiplyAdd + + FusedMultiplyNegAdd, +>( + a: T, + b: T, + t: T, +) -> T { + a.neg_mla(a, t).mla(b, t) +} + +/// 4D CLUT helper. +/// +/// Represents hypercube. +pub struct Hypercube<'a> { + array: &'a [f32], + x_stride: u32, + y_stride: u32, + z_stride: u32, + grid_size: [u8; 4], +} + +trait Fetcher4 { + fn fetch(&self, x: i32, y: i32, z: i32, w: i32) -> T; +} + +impl Hypercube<'_> { + pub fn new(array: &[f32], grid_size: usize) -> Hypercube<'_> { + let z_stride = grid_size as u32; + let y_stride = z_stride * z_stride; + let x_stride = z_stride * z_stride * z_stride; + Hypercube { + array, + x_stride, + y_stride, + z_stride, + grid_size: [ + grid_size as u8, + grid_size as u8, + grid_size as u8, + grid_size as u8, + ], + } + } + + pub fn new_hypercube(array: &[f32], grid_size: [u8; 4]) -> Hypercube<'_> { + let z_stride = grid_size[2] as u32; + let y_stride = z_stride * grid_size[1] as u32; + let x_stride = y_stride * grid_size[0] as u32; + Hypercube { + array, + x_stride, + y_stride, + z_stride, + grid_size, + } + } +} + +struct Fetch4Vec3<'a> { + array: &'a [f32], + x_stride: u32, + y_stride: u32, + z_stride: u32, +} + +struct Fetch4Vec4<'a> { + array: &'a [f32], + x_stride: u32, + y_stride: u32, + z_stride: u32, +} + +impl Fetcher4 for Fetch4Vec3<'_> { + #[inline(always)] + fn fetch(&self, x: i32, y: i32, z: i32, w: i32) -> Vector3f { + let start = (x as u32 * self.x_stride + + y as u32 * self.y_stride + + z as u32 * self.z_stride + + w as u32) as usize + * 3; + let k = &self.array[start..start + 3]; + Vector3f { + v: [k[0], k[1], k[2]], + } + } +} + +impl Fetcher4 for Fetch4Vec4<'_> { + #[inline(always)] + fn fetch(&self, x: i32, y: i32, z: i32, w: i32) -> Vector4f { + let start = (x as u32 * self.x_stride + + y as u32 * self.y_stride + + z as u32 * self.z_stride + + w as u32) as usize + * 4; + let k = &self.array[start..start + 4]; + Vector4f { + v: [k[0], k[1], k[2], k[3]], + } + } +} + +impl Hypercube<'_> { + #[inline(always)] + fn quadlinear< + T: From + + Add + + Mul + + FusedMultiplyAdd + + Sub + + Copy + + FusedMultiplyNegAdd, + >( + &self, + lin_x: f32, + lin_y: f32, + lin_z: f32, + lin_w: f32, + r: impl Fetcher4, + ) -> T { + let lin_x = lin_x.max(0.0).min(1.0); + let lin_y = lin_y.max(0.0).min(1.0); + let lin_z = lin_z.max(0.0).min(1.0); + let lin_w = lin_w.max(0.0).min(1.0); + + let scale_x = (self.grid_size[0] as i32 - 1) as f32; + let scale_y = (self.grid_size[1] as i32 - 1) as f32; + let scale_z = (self.grid_size[2] as i32 - 1) as f32; + let scale_w = (self.grid_size[3] as i32 - 1) as f32; + + let x = (lin_x * scale_x).floor() as i32; + let y = (lin_y * scale_y).floor() as i32; + let z = (lin_z * scale_z).floor() as i32; + let w = (lin_w * scale_w).floor() as i32; + + let x_n = (lin_x * scale_x).ceil() as i32; + let y_n = (lin_y * scale_y).ceil() as i32; + let z_n = (lin_z * scale_z).ceil() as i32; + let w_n = (lin_w * scale_w).ceil() as i32; + + let x_d = T::from(lin_x * scale_x - x as f32); + let y_d = T::from(lin_y * scale_y - y as f32); + let z_d = T::from(lin_z * scale_z - z as f32); + let w_d = T::from(lin_w * scale_w - w as f32); + + let r_x1 = lerp(r.fetch(x, y, z, w), r.fetch(x_n, y, z, w), x_d); + let r_x2 = lerp(r.fetch(x, y_n, z, w), r.fetch(x_n, y_n, z, w), x_d); + let r_y1 = lerp(r_x1, r_x2, y_d); + let r_x3 = lerp(r.fetch(x, y, z_n, w), r.fetch(x_n, y, z_n, w), x_d); + let r_x4 = lerp(r.fetch(x, y_n, z_n, w), r.fetch(x_n, y_n, z_n, w), x_d); + let r_y2 = lerp(r_x3, r_x4, y_d); + let r_z1 = lerp(r_y1, r_y2, z_d); + + let r_x1 = lerp(r.fetch(x, y, z, w_n), r.fetch(x_n, y, z, w_n), x_d); + let r_x2 = lerp(r.fetch(x, y_n, z, w_n), r.fetch(x_n, y_n, z, w_n), x_d); + let r_y1 = lerp(r_x1, r_x2, y_d); + let r_x3 = lerp(r.fetch(x, y, z_n, w_n), r.fetch(x_n, y, z_n, w_n), x_d); + let r_x4 = lerp(r.fetch(x, y_n, z_n, w_n), r.fetch(x_n, y_n, z_n, w_n), x_d); + let r_y2 = lerp(r_x3, r_x4, y_d); + let r_z2 = lerp(r_y1, r_y2, z_d); + lerp(r_z1, r_z2, w_d) + } + + #[inline] + pub fn quadlinear_vec3(&self, lin_x: f32, lin_y: f32, lin_z: f32, lin_w: f32) -> Vector3f { + self.quadlinear( + lin_x, + lin_y, + lin_z, + lin_w, + Fetch4Vec3 { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + z_stride: self.z_stride, + }, + ) + } + + #[inline] + pub fn quadlinear_vec4(&self, lin_x: f32, lin_y: f32, lin_z: f32, lin_w: f32) -> Vector4f { + self.quadlinear( + lin_x, + lin_y, + lin_z, + lin_w, + Fetch4Vec4 { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + z_stride: self.z_stride, + }, + ) + } + + #[cfg(feature = "options")] + #[cfg_attr(docsrs, doc(cfg(feature = "options")))] + #[inline(always)] + fn pyramid< + T: From + + Add + + Mul + + FusedMultiplyAdd + + Sub + + Copy + + FusedMultiplyNegAdd, + >( + &self, + lin_x: f32, + lin_y: f32, + lin_z: f32, + lin_w: f32, + r: impl Fetcher4, + ) -> T { + let lin_x = lin_x.max(0.0).min(1.0); + let lin_y = lin_y.max(0.0).min(1.0); + let lin_z = lin_z.max(0.0).min(1.0); + let lin_w = lin_w.max(0.0).min(1.0); + + let scale_x = (self.grid_size[0] as i32 - 1) as f32; + let scale_y = (self.grid_size[1] as i32 - 1) as f32; + let scale_z = (self.grid_size[2] as i32 - 1) as f32; + let scale_w = (self.grid_size[3] as i32 - 1) as f32; + + let x = (lin_x * scale_x).floor() as i32; + let y = (lin_y * scale_y).floor() as i32; + let z = (lin_z * scale_z).floor() as i32; + let w = (lin_w * scale_w).floor() as i32; + + let x_n = (lin_x * scale_x).ceil() as i32; + let y_n = (lin_y * scale_y).ceil() as i32; + let z_n = (lin_z * scale_z).ceil() as i32; + let w_n = (lin_w * scale_w).ceil() as i32; + + let dr = lin_x * scale_x - x as f32; + let dg = lin_y * scale_y - y as f32; + let db = lin_z * scale_z - z as f32; + let dw = lin_w * scale_w - w as f32; + + let c0 = r.fetch(x, y, z, w); + + let w0 = if dr > db && dg > db { + let x0 = r.fetch(x_n, y_n, z_n, w); + let x1 = r.fetch(x_n, y_n, z, w); + let x2 = r.fetch(x_n, y, z, w); + let x3 = r.fetch(x, y_n, z, w); + + let c1 = x0 - x1; + let c2 = x2 - c0; + let c3 = x3 - c0; + let c4 = c0 - x3 - x2 + x1; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + s2.mla(c4, T::from(dr * dg)) + } else if db > dr && dg > dr { + let x0 = r.fetch(x, y, z_n, w); + let x1 = r.fetch(x_n, y_n, z_n, w); + let x2 = r.fetch(x, y_n, z_n, w); + let x3 = r.fetch(x, y_n, z, w); + + let c1 = x0 - c0; + let c2 = x1 - x2; + let c3 = x3 - c0; + let c4 = c0 - x3 - x0 + x2; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + s2.mla(c4, T::from(dg * db)) + } else { + let x0 = r.fetch(x, y, z_n, w); + let x1 = r.fetch(x_n, y, z, w); + let x2 = r.fetch(x_n, y, z_n, w); + let x3 = r.fetch(x_n, y_n, z_n, w); + + let c1 = x0 - c0; + let c2 = x1 - c0; + let c3 = x3 - x2; + let c4 = c0 - x1 - x0 + x2; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + s2.mla(c4, T::from(db * dr)) + }; + + let c0 = r.fetch(x, y, z, w_n); + + let w1 = if dr > db && dg > db { + let x0 = r.fetch(x_n, y_n, z_n, w_n); + let x1 = r.fetch(x_n, y_n, z, w_n); + let x2 = r.fetch(x_n, y, z, w_n); + let x3 = r.fetch(x, y_n, z, w_n); + + let c1 = x0 - x1; + let c2 = x2 - c0; + let c3 = x3 - c0; + let c4 = c0 - x3 - x2 + x1; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + s2.mla(c4, T::from(dr * dg)) + } else if db > dr && dg > dr { + let x0 = r.fetch(x, y, z_n, w_n); + let x1 = r.fetch(x_n, y_n, z_n, w_n); + let x2 = r.fetch(x, y_n, z_n, w_n); + let x3 = r.fetch(x, y_n, z, w_n); + + let c1 = x0 - c0; + let c2 = x1 - x2; + let c3 = x3 - c0; + let c4 = c0 - x3 - x0 + x2; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + s2.mla(c4, T::from(dg * db)) + } else { + let x0 = r.fetch(x, y, z_n, w_n); + let x1 = r.fetch(x_n, y, z, w_n); + let x2 = r.fetch(x_n, y, z_n, w_n); + let x3 = r.fetch(x_n, y_n, z_n, w_n); + + let c1 = x0 - c0; + let c2 = x1 - c0; + let c3 = x3 - x2; + let c4 = c0 - x1 - x0 + x2; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + s2.mla(c4, T::from(db * dr)) + }; + w0.neg_mla(w0, T::from(dw)).mla(w1, T::from(dw)) + } + + #[cfg(feature = "options")] + #[cfg_attr(docsrs, doc(cfg(feature = "options")))] + #[inline] + pub fn pyramid_vec3(&self, lin_x: f32, lin_y: f32, lin_z: f32, lin_w: f32) -> Vector3f { + self.pyramid( + lin_x, + lin_y, + lin_z, + lin_w, + Fetch4Vec3 { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + z_stride: self.z_stride, + }, + ) + } + + #[cfg(feature = "options")] + #[cfg_attr(docsrs, doc(cfg(feature = "options")))] + #[inline] + pub fn pyramid_vec4(&self, lin_x: f32, lin_y: f32, lin_z: f32, lin_w: f32) -> Vector4f { + self.pyramid( + lin_x, + lin_y, + lin_z, + lin_w, + Fetch4Vec4 { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + z_stride: self.z_stride, + }, + ) + } + + #[cfg(feature = "options")] + #[cfg_attr(docsrs, doc(cfg(feature = "options")))] + #[inline(always)] + fn prism< + T: From + + Add + + Mul + + FusedMultiplyAdd + + Sub + + Copy + + FusedMultiplyNegAdd, + >( + &self, + lin_x: f32, + lin_y: f32, + lin_z: f32, + lin_w: f32, + r: impl Fetcher4, + ) -> T { + let lin_x = lin_x.max(0.0).min(1.0); + let lin_y = lin_y.max(0.0).min(1.0); + let lin_z = lin_z.max(0.0).min(1.0); + let lin_w = lin_w.max(0.0).min(1.0); + + let scale_x = (self.grid_size[0] as i32 - 1) as f32; + let scale_y = (self.grid_size[1] as i32 - 1) as f32; + let scale_z = (self.grid_size[2] as i32 - 1) as f32; + let scale_w = (self.grid_size[3] as i32 - 1) as f32; + + let x = (lin_x * scale_x).floor() as i32; + let y = (lin_y * scale_y).floor() as i32; + let z = (lin_z * scale_z).floor() as i32; + let w = (lin_w * scale_w).floor() as i32; + + let x_n = (lin_x * scale_x).ceil() as i32; + let y_n = (lin_y * scale_y).ceil() as i32; + let z_n = (lin_z * scale_z).ceil() as i32; + let w_n = (lin_w * scale_w).ceil() as i32; + + let dr = lin_x * scale_x - x as f32; + let dg = lin_y * scale_y - y as f32; + let db = lin_z * scale_z - z as f32; + let dw = lin_w * scale_w - w as f32; + + let c0 = r.fetch(x, y, z, w); + + let w0 = if db >= dr { + let x0 = r.fetch(x, y, z_n, w); + let x1 = r.fetch(x_n, y, z_n, w); + let x2 = r.fetch(x, y_n, z, w); + let x3 = r.fetch(x, y_n, z_n, w); + let x4 = r.fetch(x_n, y_n, z_n, w); + + let c1 = x0 - c0; + let c2 = x1 - x0; + let c3 = x2 - c0; + let c4 = c0 - x2 - x0 + x3; + let c5 = x0 - x3 - x1 + x4; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + let s3 = s2.mla(c4, T::from(dg * db)); + s3.mla(c5, T::from(dr * dg)) + } else { + let x0 = r.fetch(x_n, y, z, w); + let x1 = r.fetch(x_n, y, z_n, w); + let x2 = r.fetch(x, y_n, z, w); + let x3 = r.fetch(x_n, y_n, z, w); + let x4 = r.fetch(x_n, y_n, z_n, w); + + let c1 = x1 - x0; + let c2 = x0 - c0; + let c3 = x2 - c0; + let c4 = x0 - x3 - x1 + x4; + let c5 = c0 - x2 - x0 + x3; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + let s3 = s2.mla(c4, T::from(dg * db)); + s3.mla(c5, T::from(dr * dg)) + }; + + let c0 = r.fetch(x, y, z, w_n); + + let w1 = if db >= dr { + let x0 = r.fetch(x, y, z_n, w_n); + let x1 = r.fetch(x_n, y, z_n, w_n); + let x2 = r.fetch(x, y_n, z, w_n); + let x3 = r.fetch(x, y_n, z_n, w_n); + let x4 = r.fetch(x_n, y_n, z_n, w_n); + + let c1 = x0 - c0; + let c2 = x1 - x0; + let c3 = x2 - c0; + let c4 = c0 - x2 - x0 + x3; + let c5 = x0 - x3 - x1 + x4; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + let s3 = s2.mla(c4, T::from(dg * db)); + s3.mla(c5, T::from(dr * dg)) + } else { + let x0 = r.fetch(x_n, y, z, w_n); + let x1 = r.fetch(x_n, y, z_n, w_n); + let x2 = r.fetch(x, y_n, z, w_n); + let x3 = r.fetch(x_n, y_n, z, w_n); + let x4 = r.fetch(x_n, y_n, z_n, w_n); + + let c1 = x1 - x0; + let c2 = x0 - c0; + let c3 = x2 - c0; + let c4 = x0 - x3 - x1 + x4; + let c5 = c0 - x2 - x0 + x3; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + let s3 = s2.mla(c4, T::from(dg * db)); + s3.mla(c5, T::from(dr * dg)) + }; + w0.neg_mla(w0, T::from(dw)).mla(w1, T::from(dw)) + } + + #[cfg(feature = "options")] + #[cfg_attr(docsrs, doc(cfg(feature = "options")))] + #[inline] + pub fn prism_vec3(&self, lin_x: f32, lin_y: f32, lin_z: f32, lin_w: f32) -> Vector3f { + self.prism( + lin_x, + lin_y, + lin_z, + lin_w, + Fetch4Vec3 { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + z_stride: self.z_stride, + }, + ) + } + + #[cfg(feature = "options")] + #[cfg_attr(docsrs, doc(cfg(feature = "options")))] + #[inline] + pub fn prism_vec4(&self, lin_x: f32, lin_y: f32, lin_z: f32, lin_w: f32) -> Vector4f { + self.prism( + lin_x, + lin_y, + lin_z, + lin_w, + Fetch4Vec4 { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + z_stride: self.z_stride, + }, + ) + } + + #[cfg(feature = "options")] + #[cfg_attr(docsrs, doc(cfg(feature = "options")))] + #[inline(always)] + fn tetra< + T: From + + Add + + Mul + + FusedMultiplyAdd + + Sub + + Copy + + FusedMultiplyNegAdd, + >( + &self, + lin_x: f32, + lin_y: f32, + lin_z: f32, + lin_w: f32, + r: impl Fetcher4, + ) -> T { + let lin_x = lin_x.max(0.0).min(1.0); + let lin_y = lin_y.max(0.0).min(1.0); + let lin_z = lin_z.max(0.0).min(1.0); + let lin_w = lin_w.max(0.0).min(1.0); + + let scale_x = (self.grid_size[0] as i32 - 1) as f32; + let scale_y = (self.grid_size[1] as i32 - 1) as f32; + let scale_z = (self.grid_size[2] as i32 - 1) as f32; + let scale_w = (self.grid_size[3] as i32 - 1) as f32; + + let x = (lin_x * scale_x).floor() as i32; + let y = (lin_y * scale_y).floor() as i32; + let z = (lin_z * scale_z).floor() as i32; + let w = (lin_w * scale_w).floor() as i32; + + let x_n = (lin_x * scale_x).ceil() as i32; + let y_n = (lin_y * scale_y).ceil() as i32; + let z_n = (lin_z * scale_z).ceil() as i32; + let w_n = (lin_w * scale_w).ceil() as i32; + + let rx = lin_x * scale_x - x as f32; + let ry = lin_y * scale_y - y as f32; + let rz = lin_z * scale_z - z as f32; + let rw = lin_w * scale_w - w as f32; + + let c0 = r.fetch(x, y, z, w); + let c2; + let c1; + let c3; + if rx >= ry { + if ry >= rz { + //rx >= ry && ry >= rz + c1 = r.fetch(x_n, y, z, w) - c0; + c2 = r.fetch(x_n, y_n, z, w) - r.fetch(x_n, y, z, w); + c3 = r.fetch(x_n, y_n, z_n, w) - r.fetch(x_n, y_n, z, w); + } else if rx >= rz { + //rx >= rz && rz >= ry + c1 = r.fetch(x_n, y, z, w) - c0; + c2 = r.fetch(x_n, y_n, z_n, w) - r.fetch(x_n, y, z_n, w); + c3 = r.fetch(x_n, y, z_n, w) - r.fetch(x_n, y, z, w); + } else { + //rz > rx && rx >= ry + c1 = r.fetch(x_n, y, z_n, w) - r.fetch(x, y, z_n, w); + c2 = r.fetch(x_n, y_n, z_n, w) - r.fetch(x_n, y, z_n, w); + c3 = r.fetch(x, y, z_n, w) - c0; + } + } else if rx >= rz { + //ry > rx && rx >= rz + c1 = r.fetch(x_n, y_n, z, w) - r.fetch(x, y_n, z, w); + c2 = r.fetch(x, y_n, z, w) - c0; + c3 = r.fetch(x_n, y_n, z_n, w) - r.fetch(x_n, y_n, z, w); + } else if ry >= rz { + //ry >= rz && rz > rx + c1 = r.fetch(x_n, y_n, z_n, w) - r.fetch(x, y_n, z_n, w); + c2 = r.fetch(x, y_n, z, w) - c0; + c3 = r.fetch(x, y_n, z_n, w) - r.fetch(x, y_n, z, w); + } else { + //rz > ry && ry > rx + c1 = r.fetch(x_n, y_n, z_n, w) - r.fetch(x, y_n, z_n, w); + c2 = r.fetch(x, y_n, z_n, w) - r.fetch(x, y, z_n, w); + c3 = r.fetch(x, y, z_n, w) - c0; + } + let s0 = c0.mla(c1, T::from(rx)); + let s1 = s0.mla(c2, T::from(ry)); + let w0 = s1.mla(c3, T::from(rz)); + + let c0 = r.fetch(x, y, z, w_n); + let c2; + let c1; + let c3; + if rx >= ry { + if ry >= rz { + //rx >= ry && ry >= rz + c1 = r.fetch(x_n, y, z, w_n) - c0; + c2 = r.fetch(x_n, y_n, z, w_n) - r.fetch(x_n, y, z, w_n); + c3 = r.fetch(x_n, y_n, z_n, w_n) - r.fetch(x_n, y_n, z, w_n); + } else if rx >= rz { + //rx >= rz && rz >= ry + c1 = r.fetch(x_n, y, z, w_n) - c0; + c2 = r.fetch(x_n, y_n, z_n, w_n) - r.fetch(x_n, y, z_n, w_n); + c3 = r.fetch(x_n, y, z_n, w_n) - r.fetch(x_n, y, z, w_n); + } else { + //rz > rx && rx >= ry + c1 = r.fetch(x_n, y, z_n, w_n) - r.fetch(x, y, z_n, w_n); + c2 = r.fetch(x_n, y_n, z_n, w_n) - r.fetch(x_n, y, z_n, w_n); + c3 = r.fetch(x, y, z_n, w_n) - c0; + } + } else if rx >= rz { + //ry > rx && rx >= rz + c1 = r.fetch(x_n, y_n, z, w_n) - r.fetch(x, y_n, z, w_n); + c2 = r.fetch(x, y_n, z, w_n) - c0; + c3 = r.fetch(x_n, y_n, z_n, w_n) - r.fetch(x_n, y_n, z, w_n); + } else if ry >= rz { + //ry >= rz && rz > rx + c1 = r.fetch(x_n, y_n, z_n, w_n) - r.fetch(x, y_n, z_n, w_n); + c2 = r.fetch(x, y_n, z, w_n) - c0; + c3 = r.fetch(x, y_n, z_n, w_n) - r.fetch(x, y_n, z, w_n); + } else { + //rz > ry && ry > rx + c1 = r.fetch(x_n, y_n, z_n, w_n) - r.fetch(x, y_n, z_n, w_n); + c2 = r.fetch(x, y_n, z_n, w_n) - r.fetch(x, y, z_n, w_n); + c3 = r.fetch(x, y, z_n, w_n) - c0; + } + let s0 = c0.mla(c1, T::from(rx)); + let s1 = s0.mla(c2, T::from(ry)); + let w1 = s1.mla(c3, T::from(rz)); + w0.neg_mla(w0, T::from(rw)).mla(w1, T::from(rw)) + } + + #[cfg(feature = "options")] + #[cfg_attr(docsrs, doc(cfg(feature = "options")))] + #[inline] + pub fn tetra_vec3(&self, lin_x: f32, lin_y: f32, lin_z: f32, lin_w: f32) -> Vector3f { + self.tetra( + lin_x, + lin_y, + lin_z, + lin_w, + Fetch4Vec3 { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + z_stride: self.z_stride, + }, + ) + } + + #[cfg(feature = "options")] + #[cfg_attr(docsrs, doc(cfg(feature = "options")))] + #[inline] + pub fn tetra_vec4(&self, lin_x: f32, lin_y: f32, lin_z: f32, lin_w: f32) -> Vector4f { + self.tetra( + lin_x, + lin_y, + lin_z, + lin_w, + Fetch4Vec4 { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + z_stride: self.z_stride, + }, + ) + } +} + +/// 3D CLUT helper +/// +/// Represents hexahedron. +pub struct Cube<'a> { + array: &'a [f32], + x_stride: u32, + y_stride: u32, + grid_size: [u8; 3], +} + +pub(crate) trait ArrayFetch { + fn fetch(&self, x: i32, y: i32, z: i32) -> T; +} + +struct ArrayFetchVector3f<'a> { + array: &'a [f32], + x_stride: u32, + y_stride: u32, +} + +impl ArrayFetch for ArrayFetchVector3f<'_> { + #[inline(always)] + fn fetch(&self, x: i32, y: i32, z: i32) -> Vector3f { + let start = (x as u32 * self.x_stride + y as u32 * self.y_stride + z as u32) as usize * 3; + let k = &self.array[start..start + 3]; + Vector3f { + v: [k[0], k[1], k[2]], + } + } +} + +struct ArrayFetchVector4f<'a> { + array: &'a [f32], + x_stride: u32, + y_stride: u32, +} + +impl ArrayFetch for ArrayFetchVector4f<'_> { + #[inline(always)] + fn fetch(&self, x: i32, y: i32, z: i32) -> Vector4f { + let start = (x as u32 * self.x_stride + y as u32 * self.y_stride + z as u32) as usize * 4; + let k = &self.array[start..start + 4]; + Vector4f { + v: [k[0], k[1], k[2], k[3]], + } + } +} + +impl Cube<'_> { + pub fn new(array: &[f32], grid_size: usize) -> Cube<'_> { + let y_stride = grid_size; + let x_stride = y_stride * y_stride; + Cube { + array, + x_stride: x_stride as u32, + y_stride: y_stride as u32, + grid_size: [grid_size as u8, grid_size as u8, grid_size as u8], + } + } + + pub fn new_cube(array: &[f32], grid_size: [u8; 3]) -> Cube<'_> { + let y_stride = grid_size[1] as u32; + let x_stride = y_stride * grid_size[0] as u32; + Cube { + array, + x_stride, + y_stride, + grid_size, + } + } + + #[inline(always)] + fn trilinear< + T: Copy + + From + + Sub + + Mul + + Add + + FusedMultiplyNegAdd + + FusedMultiplyAdd, + >( + &self, + lin_x: f32, + lin_y: f32, + lin_z: f32, + fetch: impl ArrayFetch, + ) -> T { + let lin_x = lin_x.max(0.0).min(1.0); + let lin_y = lin_y.max(0.0).min(1.0); + let lin_z = lin_z.max(0.0).min(1.0); + + let scale_x = (self.grid_size[0] as i32 - 1) as f32; + let scale_y = (self.grid_size[1] as i32 - 1) as f32; + let scale_z = (self.grid_size[2] as i32 - 1) as f32; + + let x = (lin_x * scale_x).floor() as i32; + let y = (lin_y * scale_y).floor() as i32; + let z = (lin_z * scale_z).floor() as i32; + + let x_n = (lin_x * scale_x).ceil() as i32; + let y_n = (lin_y * scale_y).ceil() as i32; + let z_n = (lin_z * scale_z).ceil() as i32; + + let x_d = T::from(lin_x * scale_x - x as f32); + let y_d = T::from(lin_y * scale_y - y as f32); + let z_d = T::from(lin_z * scale_z - z as f32); + + let c000 = fetch.fetch(x, y, z); + let c100 = fetch.fetch(x_n, y, z); + let c010 = fetch.fetch(x, y_n, z); + let c110 = fetch.fetch(x_n, y_n, z); + let c001 = fetch.fetch(x, y, z_n); + let c101 = fetch.fetch(x_n, y, z_n); + let c011 = fetch.fetch(x, y_n, z_n); + let c111 = fetch.fetch(x_n, y_n, z_n); + + let c00 = c000.neg_mla(c000, x_d).mla(c100, x_d); + let c10 = c010.neg_mla(c010, x_d).mla(c110, x_d); + let c01 = c001.neg_mla(c001, x_d).mla(c101, x_d); + let c11 = c011.neg_mla(c011, x_d).mla(c111, x_d); + + let c0 = c00.neg_mla(c00, y_d).mla(c10, y_d); + let c1 = c01.neg_mla(c01, y_d).mla(c11, y_d); + + c0.neg_mla(c0, z_d).mla(c1, z_d) + } + + #[cfg(feature = "options")] + #[inline] + fn pyramid< + T: Copy + + From + + Sub + + Mul + + Add + + FusedMultiplyAdd, + >( + &self, + lin_x: f32, + lin_y: f32, + lin_z: f32, + fetch: impl ArrayFetch, + ) -> T { + let lin_x = lin_x.max(0.0).min(1.0); + let lin_y = lin_y.max(0.0).min(1.0); + let lin_z = lin_z.max(0.0).min(1.0); + + let scale_x = (self.grid_size[0] as i32 - 1) as f32; + let scale_y = (self.grid_size[1] as i32 - 1) as f32; + let scale_z = (self.grid_size[2] as i32 - 1) as f32; + + let x = (lin_x * scale_x).floor() as i32; + let y = (lin_y * scale_y).floor() as i32; + let z = (lin_z * scale_z).floor() as i32; + + let x_n = (lin_x * scale_x).ceil() as i32; + let y_n = (lin_y * scale_y).ceil() as i32; + let z_n = (lin_z * scale_z).ceil() as i32; + + let dr = lin_x * scale_x - x as f32; + let dg = lin_y * scale_y - y as f32; + let db = lin_z * scale_z - z as f32; + + let c0 = fetch.fetch(x, y, z); + + if dr > db && dg > db { + let x0 = fetch.fetch(x_n, y_n, z_n); + let x1 = fetch.fetch(x_n, y_n, z); + let x2 = fetch.fetch(x_n, y, z); + let x3 = fetch.fetch(x, y_n, z); + + let c1 = x0 - x1; + let c2 = x2 - c0; + let c3 = x3 - c0; + let c4 = c0 - x3 - x2 + x1; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + s2.mla(c4, T::from(dr * dg)) + } else if db > dr && dg > dr { + let x0 = fetch.fetch(x, y, z_n); + let x1 = fetch.fetch(x_n, y_n, z_n); + let x2 = fetch.fetch(x, y_n, z_n); + let x3 = fetch.fetch(x, y_n, z); + + let c1 = x0 - c0; + let c2 = x1 - x2; + let c3 = x3 - c0; + let c4 = c0 - x3 - x0 + x2; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + s2.mla(c4, T::from(dg * db)) + } else { + let x0 = fetch.fetch(x, y, z_n); + let x1 = fetch.fetch(x_n, y, z); + let x2 = fetch.fetch(x_n, y, z_n); + let x3 = fetch.fetch(x_n, y_n, z_n); + + let c1 = x0 - c0; + let c2 = x1 - c0; + let c3 = x3 - x2; + let c4 = c0 - x1 - x0 + x2; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + s2.mla(c4, T::from(db * dr)) + } + } + + #[cfg(feature = "options")] + #[inline] + fn tetra< + T: Copy + + From + + Sub + + Mul + + Add + + FusedMultiplyAdd, + >( + &self, + lin_x: f32, + lin_y: f32, + lin_z: f32, + fetch: impl ArrayFetch, + ) -> T { + let lin_x = lin_x.max(0.0).min(1.0); + let lin_y = lin_y.max(0.0).min(1.0); + let lin_z = lin_z.max(0.0).min(1.0); + + let scale_x = (self.grid_size[0] as i32 - 1) as f32; + let scale_y = (self.grid_size[1] as i32 - 1) as f32; + let scale_z = (self.grid_size[2] as i32 - 1) as f32; + + let x = (lin_x * scale_x).floor() as i32; + let y = (lin_y * scale_y).floor() as i32; + let z = (lin_z * scale_z).floor() as i32; + + let x_n = (lin_x * scale_x).ceil() as i32; + let y_n = (lin_y * scale_y).ceil() as i32; + let z_n = (lin_z * scale_z).ceil() as i32; + + let rx = lin_x * scale_x - x as f32; + let ry = lin_y * scale_y - y as f32; + let rz = lin_z * scale_z - z as f32; + + let c0 = fetch.fetch(x, y, z); + let c2; + let c1; + let c3; + if rx >= ry { + if ry >= rz { + //rx >= ry && ry >= rz + c1 = fetch.fetch(x_n, y, z) - c0; + c2 = fetch.fetch(x_n, y_n, z) - fetch.fetch(x_n, y, z); + c3 = fetch.fetch(x_n, y_n, z_n) - fetch.fetch(x_n, y_n, z); + } else if rx >= rz { + //rx >= rz && rz >= ry + c1 = fetch.fetch(x_n, y, z) - c0; + c2 = fetch.fetch(x_n, y_n, z_n) - fetch.fetch(x_n, y, z_n); + c3 = fetch.fetch(x_n, y, z_n) - fetch.fetch(x_n, y, z); + } else { + //rz > rx && rx >= ry + c1 = fetch.fetch(x_n, y, z_n) - fetch.fetch(x, y, z_n); + c2 = fetch.fetch(x_n, y_n, z_n) - fetch.fetch(x_n, y, z_n); + c3 = fetch.fetch(x, y, z_n) - c0; + } + } else if rx >= rz { + //ry > rx && rx >= rz + c1 = fetch.fetch(x_n, y_n, z) - fetch.fetch(x, y_n, z); + c2 = fetch.fetch(x, y_n, z) - c0; + c3 = fetch.fetch(x_n, y_n, z_n) - fetch.fetch(x_n, y_n, z); + } else if ry >= rz { + //ry >= rz && rz > rx + c1 = fetch.fetch(x_n, y_n, z_n) - fetch.fetch(x, y_n, z_n); + c2 = fetch.fetch(x, y_n, z) - c0; + c3 = fetch.fetch(x, y_n, z_n) - fetch.fetch(x, y_n, z); + } else { + //rz > ry && ry > rx + c1 = fetch.fetch(x_n, y_n, z_n) - fetch.fetch(x, y_n, z_n); + c2 = fetch.fetch(x, y_n, z_n) - fetch.fetch(x, y, z_n); + c3 = fetch.fetch(x, y, z_n) - c0; + } + let s0 = c0.mla(c1, T::from(rx)); + let s1 = s0.mla(c2, T::from(ry)); + s1.mla(c3, T::from(rz)) + } + + #[cfg(feature = "options")] + #[inline] + fn prism< + T: Copy + + From + + Sub + + Mul + + Add + + FusedMultiplyAdd, + >( + &self, + lin_x: f32, + lin_y: f32, + lin_z: f32, + fetch: impl ArrayFetch, + ) -> T { + let lin_x = lin_x.max(0.0).min(1.0); + let lin_y = lin_y.max(0.0).min(1.0); + let lin_z = lin_z.max(0.0).min(1.0); + + let scale_x = (self.grid_size[0] as i32 - 1) as f32; + let scale_y = (self.grid_size[1] as i32 - 1) as f32; + let scale_z = (self.grid_size[2] as i32 - 1) as f32; + + let x = (lin_x * scale_x).floor() as i32; + let y = (lin_y * scale_y).floor() as i32; + let z = (lin_z * scale_z).floor() as i32; + + let x_n = (lin_x * scale_x).ceil() as i32; + let y_n = (lin_y * scale_y).ceil() as i32; + let z_n = (lin_z * scale_z).ceil() as i32; + + let dr = lin_x * scale_x - x as f32; + let dg = lin_y * scale_y - y as f32; + let db = lin_z * scale_z - z as f32; + + let c0 = fetch.fetch(x, y, z); + + if db >= dr { + let x0 = fetch.fetch(x, y, z_n); + let x1 = fetch.fetch(x_n, y, z_n); + let x2 = fetch.fetch(x, y_n, z); + let x3 = fetch.fetch(x, y_n, z_n); + let x4 = fetch.fetch(x_n, y_n, z_n); + + let c1 = x0 - c0; + let c2 = x1 - x0; + let c3 = x2 - c0; + let c4 = c0 - x2 - x0 + x3; + let c5 = x0 - x3 - x1 + x4; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + let s3 = s2.mla(c4, T::from(dg * db)); + s3.mla(c5, T::from(dr * dg)) + } else { + let x0 = fetch.fetch(x_n, y, z); + let x1 = fetch.fetch(x_n, y, z_n); + let x2 = fetch.fetch(x, y_n, z); + let x3 = fetch.fetch(x_n, y_n, z); + let x4 = fetch.fetch(x_n, y_n, z_n); + + let c1 = x1 - x0; + let c2 = x0 - c0; + let c3 = x2 - c0; + let c4 = x0 - x3 - x1 + x4; + let c5 = c0 - x2 - x0 + x3; + + let s0 = c0.mla(c1, T::from(db)); + let s1 = s0.mla(c2, T::from(dr)); + let s2 = s1.mla(c3, T::from(dg)); + let s3 = s2.mla(c4, T::from(dg * db)); + s3.mla(c5, T::from(dr * dg)) + } + } + + pub fn trilinear_vec3(&self, lin_x: f32, lin_y: f32, lin_z: f32) -> Vector3f { + self.trilinear( + lin_x, + lin_y, + lin_z, + ArrayFetchVector3f { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + }, + ) + } + + #[cfg(feature = "options")] + #[cfg_attr(docsrs, doc(cfg(feature = "options")))] + pub fn prism_vec3(&self, lin_x: f32, lin_y: f32, lin_z: f32) -> Vector3f { + self.prism( + lin_x, + lin_y, + lin_z, + ArrayFetchVector3f { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + }, + ) + } + + #[cfg(feature = "options")] + #[cfg_attr(docsrs, doc(cfg(feature = "options")))] + pub fn pyramid_vec3(&self, lin_x: f32, lin_y: f32, lin_z: f32) -> Vector3f { + self.pyramid( + lin_x, + lin_y, + lin_z, + ArrayFetchVector3f { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + }, + ) + } + + #[cfg(feature = "options")] + #[cfg_attr(docsrs, doc(cfg(feature = "options")))] + pub fn tetra_vec3(&self, lin_x: f32, lin_y: f32, lin_z: f32) -> Vector3f { + self.tetra( + lin_x, + lin_y, + lin_z, + ArrayFetchVector3f { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + }, + ) + } + + pub fn trilinear_vec4(&self, lin_x: f32, lin_y: f32, lin_z: f32) -> Vector4f { + self.trilinear( + lin_x, + lin_y, + lin_z, + ArrayFetchVector4f { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + }, + ) + } + + #[cfg(feature = "options")] + pub fn tetra_vec4(&self, lin_x: f32, lin_y: f32, lin_z: f32) -> Vector4f { + self.tetra( + lin_x, + lin_y, + lin_z, + ArrayFetchVector4f { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + }, + ) + } + + #[cfg(feature = "options")] + #[cfg_attr(docsrs, doc(cfg(feature = "options")))] + pub fn pyramid_vec4(&self, lin_x: f32, lin_y: f32, lin_z: f32) -> Vector4f { + self.pyramid( + lin_x, + lin_y, + lin_z, + ArrayFetchVector4f { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + }, + ) + } + + #[cfg(feature = "options")] + #[cfg_attr(docsrs, doc(cfg(feature = "options")))] + pub fn prism_vec4(&self, lin_x: f32, lin_y: f32, lin_z: f32) -> Vector4f { + self.prism( + lin_x, + lin_y, + lin_z, + ArrayFetchVector4f { + array: self.array, + x_stride: self.x_stride, + y_stride: self.y_stride, + }, + ) + } +} diff --git a/deps/moxcms/src/oklab.rs b/deps/moxcms/src/oklab.rs new file mode 100644 index 0000000..d721e36 --- /dev/null +++ b/deps/moxcms/src/oklab.rs @@ -0,0 +1,354 @@ +/* + * // Copyright 2024 (c) the Radzivon Bartoshyk. All rights reserved. + * // + * // Use of this source code is governed by a BSD-style + * // license that can be found in the LICENSE file. + */ +use crate::Rgb; +use crate::mlaf::mlaf; +use num_traits::Pow; +use pxfm::{f_cbrtf, f_powf}; +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +#[repr(C)] +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] +/// Struct that represent *Oklab* colorspace +pub struct Oklab { + /// All values in Oklab intended to be normalized \[0; 1\] + pub l: f32, + /// A value range \[-0.5; 0.5\] + pub a: f32, + /// B value range \[-0.5; 0.5\] + pub b: f32, +} + +impl Oklab { + #[inline] + pub const fn new(l: f32, a: f32, b: f32) -> Oklab { + Oklab { l, a, b } + } + + #[inline] + /// Convert Linear Rgb to [Oklab] + pub fn from_linear_rgb(rgb: Rgb) -> Oklab { + Self::linear_rgb_to_oklab(rgb) + } + + #[inline] + fn linear_rgb_to_oklab(rgb: Rgb) -> Oklab { + let l = mlaf( + mlaf(0.4122214708f32 * rgb.r, 0.5363325363f32, rgb.g), + 0.0514459929f32, + rgb.b, + ); + let m = mlaf( + mlaf(0.2119034982f32 * rgb.r, 0.6806995451f32, rgb.g), + 0.1073969566f32, + rgb.b, + ); + let s = mlaf( + mlaf(0.0883024619f32 * rgb.r, 0.2817188376f32, rgb.g), + 0.6299787005f32, + rgb.b, + ); + + let l_cone = f_cbrtf(l); + let m_cone = f_cbrtf(m); + let s_cone = f_cbrtf(s); + + Oklab { + l: mlaf( + mlaf(0.2104542553f32 * l_cone, 0.7936177850f32, m_cone), + -0.0040720468f32, + s_cone, + ), + a: mlaf( + mlaf(1.9779984951f32 * l_cone, -2.4285922050f32, m_cone), + 0.4505937099f32, + s_cone, + ), + b: mlaf( + mlaf(0.0259040371f32 * l_cone, 0.7827717662f32, m_cone), + -0.8086757660f32, + s_cone, + ), + } + } + + #[inline] + /// Converts to linear RGB + pub fn to_linear_rgb(&self) -> Rgb { + let l_ = mlaf( + mlaf(self.l, 0.3963377774f32, self.a), + 0.2158037573f32, + self.b, + ); + let m_ = mlaf( + mlaf(self.l, -0.1055613458f32, self.a), + -0.0638541728f32, + self.b, + ); + let s_ = mlaf( + mlaf(self.l, -0.0894841775f32, self.a), + -1.2914855480f32, + self.b, + ); + + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + + Rgb::new( + mlaf( + mlaf(4.0767416621f32 * l, -3.3077115913f32, m), + 0.2309699292f32, + s, + ), + mlaf( + mlaf(-1.2684380046f32 * l, 2.6097574011f32, m), + -0.3413193965f32, + s, + ), + mlaf( + mlaf(-0.0041960863f32 * l, -0.7034186147f32, m), + 1.7076147010f32, + s, + ), + ) + } + + #[inline] + pub fn hybrid_distance(&self, other: Self) -> f32 { + let lax = self.l - other.l; + let dax = self.a - other.a; + let bax = self.b - other.b; + (dax * dax + bax * bax).sqrt() + lax.abs() + } +} + +impl Oklab { + pub fn euclidean_distance(&self, other: Self) -> f32 { + let lax = self.l - other.l; + let dax = self.a - other.a; + let bax = self.b - other.b; + (lax * lax + dax * dax + bax * bax).sqrt() + } +} + +impl Oklab { + pub fn taxicab_distance(&self, other: Self) -> f32 { + let lax = self.l - other.l; + let dax = self.a - other.a; + let bax = self.b - other.b; + lax.abs() + dax.abs() + bax.abs() + } +} + +impl Add for Oklab { + type Output = Oklab; + + #[inline] + fn add(self, rhs: Self) -> Oklab { + Oklab::new(self.l + rhs.l, self.a + rhs.a, self.b + rhs.b) + } +} + +impl Add for Oklab { + type Output = Oklab; + + #[inline] + fn add(self, rhs: f32) -> Oklab { + Oklab::new(self.l + rhs, self.a + rhs, self.b + rhs) + } +} + +impl AddAssign for Oklab { + #[inline] + fn add_assign(&mut self, rhs: Oklab) { + self.l += rhs.l; + self.a += rhs.a; + self.b += rhs.b; + } +} + +impl AddAssign for Oklab { + #[inline] + fn add_assign(&mut self, rhs: f32) { + self.l += rhs; + self.a += rhs; + self.b += rhs; + } +} + +impl Mul for Oklab { + type Output = Oklab; + + #[inline] + fn mul(self, rhs: f32) -> Self::Output { + Oklab::new(self.l * rhs, self.a * rhs, self.b * rhs) + } +} + +impl Mul for Oklab { + type Output = Oklab; + + #[inline] + fn mul(self, rhs: Oklab) -> Self::Output { + Oklab::new(self.l * rhs.l, self.a * rhs.a, self.b * rhs.b) + } +} + +impl MulAssign for Oklab { + #[inline] + fn mul_assign(&mut self, rhs: f32) { + self.l *= rhs; + self.a *= rhs; + self.b *= rhs; + } +} + +impl MulAssign for Oklab { + #[inline] + fn mul_assign(&mut self, rhs: Oklab) { + self.l *= rhs.l; + self.a *= rhs.a; + self.b *= rhs.b; + } +} + +impl Sub for Oklab { + type Output = Oklab; + + #[inline] + fn sub(self, rhs: f32) -> Self::Output { + Oklab::new(self.l - rhs, self.a - rhs, self.b - rhs) + } +} + +impl Sub for Oklab { + type Output = Oklab; + + #[inline] + fn sub(self, rhs: Oklab) -> Self::Output { + Oklab::new(self.l - rhs.l, self.a - rhs.a, self.b - rhs.b) + } +} + +impl SubAssign for Oklab { + #[inline] + fn sub_assign(&mut self, rhs: f32) { + self.l -= rhs; + self.a -= rhs; + self.b -= rhs; + } +} + +impl SubAssign for Oklab { + #[inline] + fn sub_assign(&mut self, rhs: Oklab) { + self.l -= rhs.l; + self.a -= rhs.a; + self.b -= rhs.b; + } +} + +impl Div for Oklab { + type Output = Oklab; + + #[inline] + fn div(self, rhs: f32) -> Self::Output { + Oklab::new(self.l / rhs, self.a / rhs, self.b / rhs) + } +} + +impl Div for Oklab { + type Output = Oklab; + + #[inline] + fn div(self, rhs: Oklab) -> Self::Output { + Oklab::new(self.l / rhs.l, self.a / rhs.a, self.b / rhs.b) + } +} + +impl DivAssign for Oklab { + #[inline] + fn div_assign(&mut self, rhs: f32) { + self.l /= rhs; + self.a /= rhs; + self.b /= rhs; + } +} + +impl DivAssign for Oklab { + #[inline] + fn div_assign(&mut self, rhs: Oklab) { + self.l /= rhs.l; + self.a /= rhs.a; + self.b /= rhs.b; + } +} + +impl Neg for Oklab { + type Output = Oklab; + + #[inline] + fn neg(self) -> Self::Output { + Oklab::new(-self.l, -self.a, -self.b) + } +} + +impl Pow for Oklab { + type Output = Oklab; + + #[inline] + fn pow(self, rhs: f32) -> Self::Output { + Oklab::new( + f_powf(self.l, rhs), + f_powf(self.a, rhs), + f_powf(self.b, rhs), + ) + } +} + +impl Pow for Oklab { + type Output = Oklab; + + #[inline] + fn pow(self, rhs: Oklab) -> Self::Output { + Oklab::new( + f_powf(self.l, rhs.l), + f_powf(self.a, rhs.a), + f_powf(self.b, rhs.b), + ) + } +} + +impl Oklab { + #[inline] + pub fn sqrt(&self) -> Oklab { + Oklab::new(self.l.sqrt(), self.a.sqrt(), self.b.sqrt()) + } + + #[inline] + pub fn cbrt(&self) -> Oklab { + Oklab::new(f_cbrtf(self.l), f_cbrtf(self.a), f_cbrtf(self.b)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trip() { + let xyz = Rgb::new(0.1, 0.2, 0.3); + let lab = Oklab::from_linear_rgb(xyz); + let rolled_back = lab.to_linear_rgb(); + let dx = (xyz.r - rolled_back.r).abs(); + let dy = (xyz.g - rolled_back.g).abs(); + let dz = (xyz.b - rolled_back.b).abs(); + assert!(dx < 1e-5); + assert!(dy < 1e-5); + assert!(dz < 1e-5); + } +} diff --git a/deps/moxcms/src/oklch.rs b/deps/moxcms/src/oklch.rs new file mode 100644 index 0000000..18783ab --- /dev/null +++ b/deps/moxcms/src/oklch.rs @@ -0,0 +1,294 @@ +/* + * // Copyright 2024 (c) the Radzivon Bartoshyk. All rights reserved. + * // + * // Use of this source code is governed by a BSD-style + * // license that can be found in the LICENSE file. + */ +use crate::{Oklab, Rgb}; +use num_traits::Pow; +use pxfm::{f_atan2f, f_cbrtf, f_hypotf, f_powf, f_sincosf}; +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +/// Represents *Oklch* colorspace +#[repr(C)] +#[derive(Copy, Clone, PartialOrd, PartialEq)] +pub struct Oklch { + /// Lightness + pub l: f32, + /// Chroma + pub c: f32, + /// Hue + pub h: f32, +} + +impl Oklch { + /// Creates new instance + #[inline] + pub const fn new(l: f32, c: f32, h: f32) -> Oklch { + Oklch { l, c, h } + } + + /// Converts Linear [Rgb] into [Oklch] + /// + /// # Arguments + /// `transfer_function` - Transfer function into linear colorspace and its inverse + #[inline] + pub fn from_linear_rgb(rgb: Rgb) -> Oklch { + let oklab = Oklab::from_linear_rgb(rgb); + Oklch::from_oklab(oklab) + } + + /// Converts [Oklch] into linear [Rgb] + #[inline] + pub fn to_linear_rgb(&self) -> Rgb { + let oklab = self.to_oklab(); + oklab.to_linear_rgb() + } + + /// Converts *Oklab* to *Oklch* + #[inline] + pub fn from_oklab(oklab: Oklab) -> Oklch { + let chroma = f_hypotf(oklab.b, oklab.a); + let hue = f_atan2f(oklab.b, oklab.a); + Oklch::new(oklab.l, chroma, hue) + } + + /// Converts *Oklch* to *Oklab* + #[inline] + pub fn to_oklab(&self) -> Oklab { + let l = self.l; + let sincos = f_sincosf(self.h); + let a = self.c * sincos.1; + let b = self.c * sincos.0; + Oklab::new(l, a, b) + } +} + +impl Oklch { + #[inline] + pub fn euclidean_distance(&self, other: Self) -> f32 { + let dl = self.l - other.l; + let dc = self.c - other.c; + let dh = self.h - other.h; + (dl * dl + dc * dc + dh * dh).sqrt() + } +} + +impl Oklch { + #[inline] + pub fn taxicab_distance(&self, other: Self) -> f32 { + let dl = self.l - other.l; + let dc = self.c - other.c; + let dh = self.h - other.h; + dl.abs() + dc.abs() + dh.abs() + } +} + +impl Add for Oklch { + type Output = Oklch; + + #[inline] + fn add(self, rhs: Self) -> Oklch { + Oklch::new(self.l + rhs.l, self.c + rhs.c, self.h + rhs.h) + } +} + +impl Add for Oklch { + type Output = Oklch; + + #[inline] + fn add(self, rhs: f32) -> Oklch { + Oklch::new(self.l + rhs, self.c + rhs, self.h + rhs) + } +} + +impl AddAssign for Oklch { + #[inline] + fn add_assign(&mut self, rhs: Oklch) { + self.l += rhs.l; + self.c += rhs.c; + self.h += rhs.h; + } +} + +impl AddAssign for Oklch { + #[inline] + fn add_assign(&mut self, rhs: f32) { + self.l += rhs; + self.c += rhs; + self.h += rhs; + } +} + +impl Mul for Oklch { + type Output = Oklch; + + #[inline] + fn mul(self, rhs: f32) -> Self::Output { + Oklch::new(self.l * rhs, self.c * rhs, self.h * rhs) + } +} + +impl Mul for Oklch { + type Output = Oklch; + + #[inline] + fn mul(self, rhs: Oklch) -> Self::Output { + Oklch::new(self.l * rhs.l, self.c * rhs.c, self.h * rhs.h) + } +} + +impl MulAssign for Oklch { + #[inline] + fn mul_assign(&mut self, rhs: f32) { + self.l *= rhs; + self.c *= rhs; + self.h *= rhs; + } +} + +impl MulAssign for Oklch { + #[inline] + fn mul_assign(&mut self, rhs: Oklch) { + self.l *= rhs.l; + self.c *= rhs.c; + self.h *= rhs.h; + } +} + +impl Sub for Oklch { + type Output = Oklch; + + #[inline] + fn sub(self, rhs: f32) -> Self::Output { + Oklch::new(self.l - rhs, self.c - rhs, self.h - rhs) + } +} + +impl Sub for Oklch { + type Output = Oklch; + + #[inline] + fn sub(self, rhs: Oklch) -> Self::Output { + Oklch::new(self.l - rhs.l, self.c - rhs.c, self.h - rhs.h) + } +} + +impl SubAssign for Oklch { + #[inline] + fn sub_assign(&mut self, rhs: f32) { + self.l -= rhs; + self.c -= rhs; + self.h -= rhs; + } +} + +impl SubAssign for Oklch { + #[inline] + fn sub_assign(&mut self, rhs: Oklch) { + self.l -= rhs.l; + self.c -= rhs.c; + self.h -= rhs.h; + } +} + +impl Div for Oklch { + type Output = Oklch; + + #[inline] + fn div(self, rhs: f32) -> Self::Output { + Oklch::new(self.l / rhs, self.c / rhs, self.h / rhs) + } +} + +impl Div for Oklch { + type Output = Oklch; + + #[inline] + fn div(self, rhs: Oklch) -> Self::Output { + Oklch::new(self.l / rhs.l, self.c / rhs.c, self.h / rhs.h) + } +} + +impl DivAssign for Oklch { + #[inline] + fn div_assign(&mut self, rhs: f32) { + self.l /= rhs; + self.c /= rhs; + self.h /= rhs; + } +} + +impl DivAssign for Oklch { + #[inline] + fn div_assign(&mut self, rhs: Oklch) { + self.l /= rhs.l; + self.c /= rhs.c; + self.h /= rhs.h; + } +} + +impl Neg for Oklch { + type Output = Oklch; + + #[inline] + fn neg(self) -> Self::Output { + Oklch::new(-self.l, -self.c, -self.h) + } +} + +impl Pow for Oklch { + type Output = Oklch; + + #[inline] + fn pow(self, rhs: f32) -> Self::Output { + Oklch::new( + f_powf(self.l, rhs), + f_powf(self.c, rhs), + f_powf(self.h, rhs), + ) + } +} + +impl Pow for Oklch { + type Output = Oklch; + + #[inline] + fn pow(self, rhs: Oklch) -> Self::Output { + Oklch::new( + f_powf(self.l, rhs.l), + f_powf(self.c, rhs.c), + f_powf(self.h, rhs.h), + ) + } +} + +impl Oklch { + #[inline] + pub fn sqrt(&self) -> Oklch { + Oklch::new(self.l.sqrt(), self.c.sqrt(), self.h.sqrt()) + } + + #[inline] + pub fn cbrt(&self) -> Oklch { + Oklch::new(f_cbrtf(self.l), f_cbrtf(self.c), f_cbrtf(self.h)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trip() { + let xyz = Rgb::new(0.1, 0.2, 0.3); + let lab = Oklch::from_linear_rgb(xyz); + let rolled_back = lab.to_linear_rgb(); + let dx = (xyz.r - rolled_back.r).abs(); + let dy = (xyz.g - rolled_back.g).abs(); + let dz = (xyz.b - rolled_back.b).abs(); + assert!(dx < 1e-5); + assert!(dy < 1e-5); + assert!(dz < 1e-5); + } +} diff --git a/deps/moxcms/src/profile.rs b/deps/moxcms/src/profile.rs new file mode 100644 index 0000000..8cd79f3 --- /dev/null +++ b/deps/moxcms/src/profile.rs @@ -0,0 +1,1365 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::chad::BRADFORD_D; +use crate::cicp::{ + CicpColorPrimaries, ColorPrimaries, MatrixCoefficients, TransferCharacteristics, +}; +use crate::dat::ColorDateTime; +use crate::err::CmsError; +use crate::matrix::{Matrix3f, Xyz}; +use crate::reader::s15_fixed16_number_to_float; +use crate::safe_math::{SafeAdd, SafeMul}; +use crate::tag::{TAG_SIZE, Tag}; +use crate::trc::ToneReprCurve; +use crate::{Chromaticity, Layout, Matrix3d, Vector3d, XyY, Xyzd, adapt_to_d50_d}; +use std::io::Read; + +const MAX_PROFILE_SIZE: usize = 1024 * 1024 * 10; // 10 MB max, for Fogra39 etc + +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProfileSignature { + Acsp, +} + +impl TryFrom for ProfileSignature { + type Error = CmsError; + #[inline] + fn try_from(value: u32) -> Result { + if value == u32::from_ne_bytes(*b"acsp").to_be() { + return Ok(ProfileSignature::Acsp); + } + Err(CmsError::InvalidProfile) + } +} + +impl From for u32 { + #[inline] + fn from(value: ProfileSignature) -> Self { + match value { + ProfileSignature::Acsp => u32::from_ne_bytes(*b"acsp").to_be(), + } + } +} + +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Ord, PartialOrd)] +pub enum ProfileVersion { + V2_0 = 0x02000000, + V2_1 = 0x02100000, + V2_2 = 0x02200000, + V2_3 = 0x02300000, + V2_4 = 0x02400000, + V4_0 = 0x04000000, + V4_1 = 0x04100000, + V4_2 = 0x04200000, + V4_3 = 0x04300000, + #[default] + V4_4 = 0x04400000, + Unknown, +} + +impl TryFrom for ProfileVersion { + type Error = CmsError; + fn try_from(value: u32) -> Result { + match value { + 0x02000000 => Ok(ProfileVersion::V2_0), + 0x02100000 => Ok(ProfileVersion::V2_1), + 0x02200000 => Ok(ProfileVersion::V2_2), + 0x02300000 => Ok(ProfileVersion::V2_3), + 0x02400000 => Ok(ProfileVersion::V2_4), + 0x04000000 => Ok(ProfileVersion::V4_0), + 0x04100000 => Ok(ProfileVersion::V4_1), + 0x04200000 => Ok(ProfileVersion::V4_2), + 0x04300000 => Ok(ProfileVersion::V4_3), + 0x04400000 => Ok(ProfileVersion::V4_3), + _ => Err(CmsError::InvalidProfile), + } + } +} + +impl From for u32 { + fn from(value: ProfileVersion) -> Self { + match value { + ProfileVersion::V2_0 => 0x02000000, + ProfileVersion::V2_1 => 0x02100000, + ProfileVersion::V2_2 => 0x02200000, + ProfileVersion::V2_3 => 0x02300000, + ProfileVersion::V2_4 => 0x02400000, + ProfileVersion::V4_0 => 0x04000000, + ProfileVersion::V4_1 => 0x04100000, + ProfileVersion::V4_2 => 0x04200000, + ProfileVersion::V4_3 => 0x04300000, + ProfileVersion::V4_4 => 0x04400000, + ProfileVersion::Unknown => 0x02000000, + } + } +} + +#[repr(u32)] +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default, Hash)] +pub enum DataColorSpace { + #[default] + Xyz, + Lab, + Luv, + YCbr, + Yxy, + Rgb, + Gray, + Hsv, + Hls, + Cmyk, + Cmy, + Color2, + Color3, + Color4, + Color5, + Color6, + Color7, + Color8, + Color9, + Color10, + Color11, + Color12, + Color13, + Color14, + Color15, +} + +impl DataColorSpace { + #[inline] + pub fn check_layout(self, layout: Layout) -> Result<(), CmsError> { + let unsupported: bool = match self { + DataColorSpace::Xyz => layout != Layout::Rgb, + DataColorSpace::Lab => layout != Layout::Rgb, + DataColorSpace::Luv => layout != Layout::Rgb, + DataColorSpace::YCbr => layout != Layout::Rgb, + DataColorSpace::Yxy => layout != Layout::Rgb, + DataColorSpace::Rgb => layout != Layout::Rgb && layout != Layout::Rgba, + DataColorSpace::Gray => layout != Layout::Gray && layout != Layout::GrayAlpha, + DataColorSpace::Hsv => layout != Layout::Rgb, + DataColorSpace::Hls => layout != Layout::Rgb, + DataColorSpace::Cmyk => layout != Layout::Rgba, + DataColorSpace::Cmy => layout != Layout::Rgb, + DataColorSpace::Color2 => layout != Layout::GrayAlpha, + DataColorSpace::Color3 => layout != Layout::Rgb, + DataColorSpace::Color4 => layout != Layout::Rgba, + DataColorSpace::Color5 => layout != Layout::Inks5, + DataColorSpace::Color6 => layout != Layout::Inks6, + DataColorSpace::Color7 => layout != Layout::Inks7, + DataColorSpace::Color8 => layout != Layout::Inks8, + DataColorSpace::Color9 => layout != Layout::Inks9, + DataColorSpace::Color10 => layout != Layout::Inks10, + DataColorSpace::Color11 => layout != Layout::Inks11, + DataColorSpace::Color12 => layout != Layout::Inks12, + DataColorSpace::Color13 => layout != Layout::Inks13, + DataColorSpace::Color14 => layout != Layout::Inks14, + DataColorSpace::Color15 => layout != Layout::Inks15, + }; + if unsupported { + Err(CmsError::InvalidLayout) + } else { + Ok(()) + } + } + + pub(crate) fn is_three_channels(self) -> bool { + matches!( + self, + DataColorSpace::Xyz + | DataColorSpace::Lab + | DataColorSpace::Luv + | DataColorSpace::YCbr + | DataColorSpace::Yxy + | DataColorSpace::Rgb + | DataColorSpace::Hsv + | DataColorSpace::Hls + | DataColorSpace::Cmy + | DataColorSpace::Color3 + ) + } +} + +#[repr(u32)] +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default)] +pub enum ProfileClass { + InputDevice, + #[default] + DisplayDevice, + OutputDevice, + DeviceLink, + ColorSpace, + Abstract, + Named, +} + +impl TryFrom for ProfileClass { + type Error = CmsError; + fn try_from(value: u32) -> Result { + if value == u32::from_ne_bytes(*b"scnr").to_be() { + return Ok(ProfileClass::InputDevice); + } else if value == u32::from_ne_bytes(*b"mntr").to_be() { + return Ok(ProfileClass::DisplayDevice); + } else if value == u32::from_ne_bytes(*b"prtr").to_be() { + return Ok(ProfileClass::OutputDevice); + } else if value == u32::from_ne_bytes(*b"link").to_be() { + return Ok(ProfileClass::DeviceLink); + } else if value == u32::from_ne_bytes(*b"spac").to_be() { + return Ok(ProfileClass::ColorSpace); + } else if value == u32::from_ne_bytes(*b"abst").to_be() { + return Ok(ProfileClass::Abstract); + } else if value == u32::from_ne_bytes(*b"nmcl").to_be() { + return Ok(ProfileClass::Named); + } + Err(CmsError::InvalidProfile) + } +} + +impl From for u32 { + fn from(val: ProfileClass) -> Self { + match val { + ProfileClass::InputDevice => u32::from_ne_bytes(*b"scnr").to_be(), + ProfileClass::DisplayDevice => u32::from_ne_bytes(*b"mntr").to_be(), + ProfileClass::OutputDevice => u32::from_ne_bytes(*b"prtr").to_be(), + ProfileClass::DeviceLink => u32::from_ne_bytes(*b"link").to_be(), + ProfileClass::ColorSpace => u32::from_ne_bytes(*b"spac").to_be(), + ProfileClass::Abstract => u32::from_ne_bytes(*b"abst").to_be(), + ProfileClass::Named => u32::from_ne_bytes(*b"nmcl").to_be(), + } + } +} + +#[derive(Debug, Clone)] +pub enum LutStore { + Store8(Vec), + Store16(Vec), +} + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum LutType { + Lut8, + Lut16, + LutMab, + LutMba, +} + +impl TryFrom for LutType { + type Error = CmsError; + fn try_from(value: u32) -> Result { + if value == u32::from_ne_bytes(*b"mft1").to_be() { + return Ok(LutType::Lut8); + } else if value == u32::from_ne_bytes(*b"mft2").to_be() { + return Ok(LutType::Lut16); + } else if value == u32::from_ne_bytes(*b"mAB ").to_be() { + return Ok(LutType::LutMab); + } else if value == u32::from_ne_bytes(*b"mBA ").to_be() { + return Ok(LutType::LutMba); + } + Err(CmsError::InvalidProfile) + } +} + +impl From for u32 { + fn from(val: LutType) -> Self { + match val { + LutType::Lut8 => u32::from_ne_bytes(*b"mft1").to_be(), + LutType::Lut16 => u32::from_ne_bytes(*b"mft2").to_be(), + LutType::LutMab => u32::from_ne_bytes(*b"mAB ").to_be(), + LutType::LutMba => u32::from_ne_bytes(*b"mBA ").to_be(), + } + } +} + +impl TryFrom for DataColorSpace { + type Error = CmsError; + fn try_from(value: u32) -> Result { + if value == u32::from_ne_bytes(*b"XYZ ").to_be() { + return Ok(DataColorSpace::Xyz); + } else if value == u32::from_ne_bytes(*b"Lab ").to_be() { + return Ok(DataColorSpace::Lab); + } else if value == u32::from_ne_bytes(*b"Luv ").to_be() { + return Ok(DataColorSpace::Luv); + } else if value == u32::from_ne_bytes(*b"YCbr").to_be() { + return Ok(DataColorSpace::YCbr); + } else if value == u32::from_ne_bytes(*b"Yxy ").to_be() { + return Ok(DataColorSpace::Yxy); + } else if value == u32::from_ne_bytes(*b"RGB ").to_be() { + return Ok(DataColorSpace::Rgb); + } else if value == u32::from_ne_bytes(*b"GRAY").to_be() { + return Ok(DataColorSpace::Gray); + } else if value == u32::from_ne_bytes(*b"HSV ").to_be() { + return Ok(DataColorSpace::Hsv); + } else if value == u32::from_ne_bytes(*b"HLS ").to_be() { + return Ok(DataColorSpace::Hls); + } else if value == u32::from_ne_bytes(*b"CMYK").to_be() { + return Ok(DataColorSpace::Cmyk); + } else if value == u32::from_ne_bytes(*b"CMY ").to_be() { + return Ok(DataColorSpace::Cmy); + } else if value == u32::from_ne_bytes(*b"2CLR").to_be() { + return Ok(DataColorSpace::Color2); + } else if value == u32::from_ne_bytes(*b"3CLR").to_be() { + return Ok(DataColorSpace::Color3); + } else if value == u32::from_ne_bytes(*b"4CLR").to_be() { + return Ok(DataColorSpace::Color4); + } else if value == u32::from_ne_bytes(*b"5CLR").to_be() { + return Ok(DataColorSpace::Color5); + } else if value == u32::from_ne_bytes(*b"6CLR").to_be() { + return Ok(DataColorSpace::Color6); + } else if value == u32::from_ne_bytes(*b"7CLR").to_be() { + return Ok(DataColorSpace::Color7); + } else if value == u32::from_ne_bytes(*b"8CLR").to_be() { + return Ok(DataColorSpace::Color8); + } else if value == u32::from_ne_bytes(*b"9CLR").to_be() { + return Ok(DataColorSpace::Color9); + } else if value == u32::from_ne_bytes(*b"ACLR").to_be() { + return Ok(DataColorSpace::Color10); + } else if value == u32::from_ne_bytes(*b"BCLR").to_be() { + return Ok(DataColorSpace::Color11); + } else if value == u32::from_ne_bytes(*b"CCLR").to_be() { + return Ok(DataColorSpace::Color12); + } else if value == u32::from_ne_bytes(*b"DCLR").to_be() { + return Ok(DataColorSpace::Color13); + } else if value == u32::from_ne_bytes(*b"ECLR").to_be() { + return Ok(DataColorSpace::Color14); + } else if value == u32::from_ne_bytes(*b"FCLR").to_be() { + return Ok(DataColorSpace::Color15); + } + Err(CmsError::InvalidProfile) + } +} + +impl From for u32 { + fn from(val: DataColorSpace) -> Self { + match val { + DataColorSpace::Xyz => u32::from_ne_bytes(*b"XYZ ").to_be(), + DataColorSpace::Lab => u32::from_ne_bytes(*b"Lab ").to_be(), + DataColorSpace::Luv => u32::from_ne_bytes(*b"Luv ").to_be(), + DataColorSpace::YCbr => u32::from_ne_bytes(*b"YCbr").to_be(), + DataColorSpace::Yxy => u32::from_ne_bytes(*b"Yxy ").to_be(), + DataColorSpace::Rgb => u32::from_ne_bytes(*b"RGB ").to_be(), + DataColorSpace::Gray => u32::from_ne_bytes(*b"GRAY").to_be(), + DataColorSpace::Hsv => u32::from_ne_bytes(*b"HSV ").to_be(), + DataColorSpace::Hls => u32::from_ne_bytes(*b"HLS ").to_be(), + DataColorSpace::Cmyk => u32::from_ne_bytes(*b"CMYK").to_be(), + DataColorSpace::Cmy => u32::from_ne_bytes(*b"CMY ").to_be(), + DataColorSpace::Color2 => u32::from_ne_bytes(*b"2CLR").to_be(), + DataColorSpace::Color3 => u32::from_ne_bytes(*b"3CLR").to_be(), + DataColorSpace::Color4 => u32::from_ne_bytes(*b"4CLR").to_be(), + DataColorSpace::Color5 => u32::from_ne_bytes(*b"5CLR").to_be(), + DataColorSpace::Color6 => u32::from_ne_bytes(*b"6CLR").to_be(), + DataColorSpace::Color7 => u32::from_ne_bytes(*b"7CLR").to_be(), + DataColorSpace::Color8 => u32::from_ne_bytes(*b"8CLR").to_be(), + DataColorSpace::Color9 => u32::from_ne_bytes(*b"9CLR").to_be(), + DataColorSpace::Color10 => u32::from_ne_bytes(*b"ACLR").to_be(), + DataColorSpace::Color11 => u32::from_ne_bytes(*b"BCLR").to_be(), + DataColorSpace::Color12 => u32::from_ne_bytes(*b"CCLR").to_be(), + DataColorSpace::Color13 => u32::from_ne_bytes(*b"DCLR").to_be(), + DataColorSpace::Color14 => u32::from_ne_bytes(*b"ECLR").to_be(), + DataColorSpace::Color15 => u32::from_ne_bytes(*b"FCLR").to_be(), + } + } +} + +#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +pub enum TechnologySignatures { + FilmScanner, + DigitalCamera, + ReflectiveScanner, + InkJetPrinter, + ThermalWaxPrinter, + ElectrophotographicPrinter, + ElectrostaticPrinter, + DyeSublimationPrinter, + PhotographicPaperPrinter, + FilmWriter, + VideoMonitor, + VideoCamera, + ProjectionTelevision, + CathodeRayTubeDisplay, + PassiveMatrixDisplay, + ActiveMatrixDisplay, + LiquidCrystalDisplay, + OrganicLedDisplay, + PhotoCd, + PhotographicImageSetter, + Gravure, + OffsetLithography, + Silkscreen, + Flexography, + MotionPictureFilmScanner, + MotionPictureFilmRecorder, + DigitalMotionPictureCamera, + DigitalCinemaProjector, + Unknown(u32), +} + +impl From for TechnologySignatures { + fn from(value: u32) -> Self { + if value == u32::from_ne_bytes(*b"fscn").to_be() { + return TechnologySignatures::FilmScanner; + } else if value == u32::from_ne_bytes(*b"dcam").to_be() { + return TechnologySignatures::DigitalCamera; + } else if value == u32::from_ne_bytes(*b"rscn").to_be() { + return TechnologySignatures::ReflectiveScanner; + } else if value == u32::from_ne_bytes(*b"ijet").to_be() { + return TechnologySignatures::InkJetPrinter; + } else if value == u32::from_ne_bytes(*b"twax").to_be() { + return TechnologySignatures::ThermalWaxPrinter; + } else if value == u32::from_ne_bytes(*b"epho").to_be() { + return TechnologySignatures::ElectrophotographicPrinter; + } else if value == u32::from_ne_bytes(*b"esta").to_be() { + return TechnologySignatures::ElectrostaticPrinter; + } else if value == u32::from_ne_bytes(*b"dsub").to_be() { + return TechnologySignatures::DyeSublimationPrinter; + } else if value == u32::from_ne_bytes(*b"rpho").to_be() { + return TechnologySignatures::PhotographicPaperPrinter; + } else if value == u32::from_ne_bytes(*b"fprn").to_be() { + return TechnologySignatures::FilmWriter; + } else if value == u32::from_ne_bytes(*b"vidm").to_be() { + return TechnologySignatures::VideoMonitor; + } else if value == u32::from_ne_bytes(*b"vidc").to_be() { + return TechnologySignatures::VideoCamera; + } else if value == u32::from_ne_bytes(*b"pjtv").to_be() { + return TechnologySignatures::ProjectionTelevision; + } else if value == u32::from_ne_bytes(*b"CRT ").to_be() { + return TechnologySignatures::CathodeRayTubeDisplay; + } else if value == u32::from_ne_bytes(*b"PMD ").to_be() { + return TechnologySignatures::PassiveMatrixDisplay; + } else if value == u32::from_ne_bytes(*b"AMD ").to_be() { + return TechnologySignatures::ActiveMatrixDisplay; + } else if value == u32::from_ne_bytes(*b"LCD ").to_be() { + return TechnologySignatures::LiquidCrystalDisplay; + } else if value == u32::from_ne_bytes(*b"OLED").to_be() { + return TechnologySignatures::OrganicLedDisplay; + } else if value == u32::from_ne_bytes(*b"KPCD").to_be() { + return TechnologySignatures::PhotoCd; + } else if value == u32::from_ne_bytes(*b"imgs").to_be() { + return TechnologySignatures::PhotographicImageSetter; + } else if value == u32::from_ne_bytes(*b"grav").to_be() { + return TechnologySignatures::Gravure; + } else if value == u32::from_ne_bytes(*b"offs").to_be() { + return TechnologySignatures::OffsetLithography; + } else if value == u32::from_ne_bytes(*b"silk").to_be() { + return TechnologySignatures::Silkscreen; + } else if value == u32::from_ne_bytes(*b"flex").to_be() { + return TechnologySignatures::Flexography; + } else if value == u32::from_ne_bytes(*b"mpfs").to_be() { + return TechnologySignatures::MotionPictureFilmScanner; + } else if value == u32::from_ne_bytes(*b"mpfr").to_be() { + return TechnologySignatures::MotionPictureFilmRecorder; + } else if value == u32::from_ne_bytes(*b"dmpc").to_be() { + return TechnologySignatures::DigitalMotionPictureCamera; + } else if value == u32::from_ne_bytes(*b"dcpj").to_be() { + return TechnologySignatures::DigitalCinemaProjector; + } + TechnologySignatures::Unknown(value) + } +} + +#[derive(Debug, Clone)] +pub enum LutWarehouse { + Lut(LutDataType), + Multidimensional(LutMultidimensionalType), +} + +#[derive(Debug, Clone)] +pub struct LutDataType { + // used by lut8Type/lut16Type (mft2) only + pub num_input_channels: u8, + pub num_output_channels: u8, + pub num_clut_grid_points: u8, + pub matrix: Matrix3d, + pub num_input_table_entries: u16, + pub num_output_table_entries: u16, + pub input_table: LutStore, + pub clut_table: LutStore, + pub output_table: LutStore, + pub lut_type: LutType, +} + +impl LutDataType { + pub(crate) fn has_same_kind(&self) -> bool { + matches!( + (&self.input_table, &self.clut_table, &self.output_table), + ( + LutStore::Store8(_), + LutStore::Store8(_), + LutStore::Store8(_) + ) | ( + LutStore::Store16(_), + LutStore::Store16(_), + LutStore::Store16(_) + ) + ) + } +} + +#[derive(Debug, Clone)] +pub struct LutMultidimensionalType { + pub num_input_channels: u8, + pub num_output_channels: u8, + pub grid_points: [u8; 16], + pub clut: Option, + pub a_curves: Vec, + pub b_curves: Vec, + pub m_curves: Vec, + pub matrix: Matrix3d, + pub bias: Vector3d, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Default, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub enum RenderingIntent { + AbsoluteColorimetric = 3, + Saturation = 2, + RelativeColorimetric = 1, + #[default] + Perceptual = 0, +} + +impl TryFrom for RenderingIntent { + type Error = CmsError; + + #[inline] + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(RenderingIntent::Perceptual), + 1 => Ok(RenderingIntent::RelativeColorimetric), + 2 => Ok(RenderingIntent::Saturation), + 3 => Ok(RenderingIntent::AbsoluteColorimetric), + _ => Err(CmsError::InvalidRenderingIntent), + } + } +} + +impl From for u32 { + #[inline] + fn from(value: RenderingIntent) -> Self { + match value { + RenderingIntent::AbsoluteColorimetric => 3, + RenderingIntent::Saturation => 2, + RenderingIntent::RelativeColorimetric => 1, + RenderingIntent::Perceptual => 0, + } + } +} + +/// ICC Header +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub(crate) struct ProfileHeader { + pub size: u32, // Size of the profile (computed) + pub cmm_type: u32, // Preferred CMM type (ignored) + pub version: ProfileVersion, // Version (4.3 or 4.4 if CICP is included) + pub profile_class: ProfileClass, // Display device profile + pub data_color_space: DataColorSpace, // RGB input color space + pub pcs: DataColorSpace, // Profile connection space + pub creation_date_time: ColorDateTime, // Date and time + pub signature: ProfileSignature, // Profile signature + pub platform: u32, // Platform target (ignored) + pub flags: u32, // Flags (not embedded, can be used independently) + pub device_manufacturer: u32, // Device manufacturer (ignored) + pub device_model: u32, // Device model (ignored) + pub device_attributes: [u8; 8], // Device attributes (ignored) + pub rendering_intent: RenderingIntent, // Relative colorimetric rendering intent + pub illuminant: Xyz, // D50 standard illuminant X + pub creator: u32, // Profile creator (ignored) + pub profile_id: [u8; 16], // Profile id checksum (ignored) + pub reserved: [u8; 28], // Reserved (ignored) + pub tag_count: u32, // Technically not part of header, but required +} + +impl ProfileHeader { + #[allow(dead_code)] + pub(crate) fn new(size: u32) -> Self { + Self { + size, + cmm_type: 0, + version: ProfileVersion::V4_3, + profile_class: ProfileClass::DisplayDevice, + data_color_space: DataColorSpace::Rgb, + pcs: DataColorSpace::Xyz, + creation_date_time: ColorDateTime::default(), + signature: ProfileSignature::Acsp, + platform: 0, + flags: 0x00000000, + device_manufacturer: 0, + device_model: 0, + device_attributes: [0; 8], + rendering_intent: RenderingIntent::Perceptual, + illuminant: Chromaticity::D50.to_xyz(), + creator: 0, + profile_id: [0; 16], + reserved: [0; 28], + tag_count: 0, + } + } + + /// Creates profile from the buffer + pub(crate) fn new_from_slice(slice: &[u8]) -> Result { + if slice.len() < size_of::() { + return Err(CmsError::InvalidProfile); + } + let mut cursor = std::io::Cursor::new(slice); + let mut buffer = [0u8; size_of::()]; + cursor + .read_exact(&mut buffer) + .map_err(|_| CmsError::InvalidProfile)?; + + let header = Self { + size: u32::from_be_bytes(buffer[0..4].try_into().unwrap()), + cmm_type: u32::from_be_bytes(buffer[4..8].try_into().unwrap()), + version: ProfileVersion::try_from(u32::from_be_bytes( + buffer[8..12].try_into().unwrap(), + ))?, + profile_class: ProfileClass::try_from(u32::from_be_bytes( + buffer[12..16].try_into().unwrap(), + ))?, + data_color_space: DataColorSpace::try_from(u32::from_be_bytes( + buffer[16..20].try_into().unwrap(), + ))?, + pcs: DataColorSpace::try_from(u32::from_be_bytes(buffer[20..24].try_into().unwrap()))?, + creation_date_time: ColorDateTime::new_from_slice(buffer[24..36].try_into().unwrap())?, + signature: ProfileSignature::try_from(u32::from_be_bytes( + buffer[36..40].try_into().unwrap(), + ))?, + platform: u32::from_be_bytes(buffer[40..44].try_into().unwrap()), + flags: u32::from_be_bytes(buffer[44..48].try_into().unwrap()), + device_manufacturer: u32::from_be_bytes(buffer[48..52].try_into().unwrap()), + device_model: u32::from_be_bytes(buffer[52..56].try_into().unwrap()), + device_attributes: buffer[56..64].try_into().unwrap(), + rendering_intent: RenderingIntent::try_from(u32::from_be_bytes( + buffer[64..68].try_into().unwrap(), + ))?, + illuminant: Xyz::new( + s15_fixed16_number_to_float(i32::from_be_bytes(buffer[68..72].try_into().unwrap())), + s15_fixed16_number_to_float(i32::from_be_bytes(buffer[72..76].try_into().unwrap())), + s15_fixed16_number_to_float(i32::from_be_bytes(buffer[76..80].try_into().unwrap())), + ), + creator: u32::from_be_bytes(buffer[80..84].try_into().unwrap()), + profile_id: buffer[84..100].try_into().unwrap(), + reserved: buffer[100..128].try_into().unwrap(), + tag_count: u32::from_be_bytes(buffer[128..132].try_into().unwrap()), + }; + Ok(header) + } +} + +/// A [Coding Independent Code Point](https://en.wikipedia.org/wiki/Coding-independent_code_points). +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct CicpProfile { + pub color_primaries: CicpColorPrimaries, + pub transfer_characteristics: TransferCharacteristics, + pub matrix_coefficients: MatrixCoefficients, + pub full_range: bool, +} + +#[derive(Debug, Clone)] +pub struct LocalizableString { + /// An ISO 639-1 value is expected; any text w. more than two symbols will be truncated + pub language: String, + /// An ISO 3166-1 value is expected; any text w. more than two symbols will be truncated + pub country: String, + pub value: String, +} + +impl LocalizableString { + /// Creates new localizable string + /// + /// # Arguments + /// + /// * `language`: an ISO 639-1 value is expected, any text more than 2 symbols will be truncated + /// * `country`: an ISO 3166-1 value is expected, any text more than 2 symbols will be truncated + /// * `value`: String value + /// + pub fn new(language: String, country: String, value: String) -> Self { + Self { + language, + country, + value, + } + } +} + +#[derive(Debug, Clone)] +pub struct DescriptionString { + pub ascii_string: String, + pub unicode_language_code: u32, + pub unicode_string: String, + pub script_code_code: i8, + pub mac_string: String, +} + +#[derive(Debug, Clone)] +pub enum ProfileText { + PlainString(String), + Localizable(Vec), + Description(DescriptionString), +} + +impl ProfileText { + pub(crate) fn has_values(&self) -> bool { + match self { + ProfileText::PlainString(_) => true, + ProfileText::Localizable(lc) => !lc.is_empty(), + ProfileText::Description(_) => true, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum StandardObserver { + D50, + D65, + Unknown, +} + +impl From for StandardObserver { + fn from(value: u32) -> Self { + if value == 1 { + return StandardObserver::D50; + } else if value == 2 { + return StandardObserver::D65; + } + StandardObserver::Unknown + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ViewingConditions { + pub illuminant: Xyz, + pub surround: Xyz, + pub observer: StandardObserver, +} + +#[derive(Debug, Clone, Copy)] +pub enum MeasurementGeometry { + Unknown, + /// 0°:45° or 45°:0° + D45to45, + /// 0°:d or d:0° + D0to0, +} + +impl From for MeasurementGeometry { + fn from(value: u32) -> Self { + if value == 1 { + Self::D45to45 + } else if value == 2 { + Self::D0to0 + } else { + Self::Unknown + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum StandardIlluminant { + Unknown, + D50, + D65, + D93, + F2, + D55, + A, + EquiPower, + F8, +} + +impl From for StandardIlluminant { + fn from(value: u32) -> Self { + match value { + 1 => StandardIlluminant::D50, + 2 => StandardIlluminant::D65, + 3 => StandardIlluminant::D93, + 4 => StandardIlluminant::F2, + 5 => StandardIlluminant::D55, + 6 => StandardIlluminant::A, + 7 => StandardIlluminant::EquiPower, + 8 => StandardIlluminant::F8, + _ => Self::Unknown, + } + } +} + +impl From for u32 { + fn from(value: StandardIlluminant) -> Self { + match value { + StandardIlluminant::Unknown => 0u32, + StandardIlluminant::D50 => 1u32, + StandardIlluminant::D65 => 2u32, + StandardIlluminant::D93 => 3, + StandardIlluminant::F2 => 4, + StandardIlluminant::D55 => 5, + StandardIlluminant::A => 6, + StandardIlluminant::EquiPower => 7, + StandardIlluminant::F8 => 8, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Measurement { + pub observer: StandardObserver, + pub backing: Xyz, + pub geometry: MeasurementGeometry, + pub flare: f32, + pub illuminant: StandardIlluminant, +} + +/// ICC Profile representation +#[repr(C)] +#[derive(Debug, Clone, Default)] +pub struct ColorProfile { + pub pcs: DataColorSpace, + pub color_space: DataColorSpace, + pub profile_class: ProfileClass, + pub rendering_intent: RenderingIntent, + pub red_colorant: Xyzd, + pub green_colorant: Xyzd, + pub blue_colorant: Xyzd, + pub white_point: Xyzd, + pub black_point: Option, + pub media_white_point: Option, + pub luminance: Option, + pub measurement: Option, + pub red_trc: Option, + pub green_trc: Option, + pub blue_trc: Option, + pub gray_trc: Option, + pub cicp: Option, + pub chromatic_adaptation: Option, + pub lut_a_to_b_perceptual: Option, + pub lut_a_to_b_colorimetric: Option, + pub lut_a_to_b_saturation: Option, + pub lut_b_to_a_perceptual: Option, + pub lut_b_to_a_colorimetric: Option, + pub lut_b_to_a_saturation: Option, + pub gamut: Option, + pub copyright: Option, + pub description: Option, + pub device_manufacturer: Option, + pub device_model: Option, + pub char_target: Option, + pub viewing_conditions: Option, + pub viewing_conditions_description: Option, + pub technology: Option, + pub calibration_date: Option, + /// Version for internal and viewing purposes only. + /// On encoding added value to profile will always be V4. + pub(crate) version_internal: ProfileVersion, +} + +#[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Hash)] +pub struct ParsingOptions { + // Maximum allowed profile size in bytes + pub max_profile_size: usize, + // Maximum allowed CLUT size in bytes + pub max_allowed_clut_size: usize, + // Maximum allowed TRC size in elements count + pub max_allowed_trc_size: usize, +} + +impl Default for ParsingOptions { + fn default() -> Self { + Self { + max_profile_size: MAX_PROFILE_SIZE, + max_allowed_clut_size: 10_000_000, + max_allowed_trc_size: 40_000, + } + } +} + +impl ColorProfile { + /// Returns profile version + pub fn version(&self) -> ProfileVersion { + self.version_internal + } + + pub fn new_from_slice(slice: &[u8]) -> Result { + Self::new_from_slice_with_options(slice, Default::default()) + } + + pub fn new_from_slice_with_options( + slice: &[u8], + options: ParsingOptions, + ) -> Result { + let header = ProfileHeader::new_from_slice(slice)?; + let tags_count = header.tag_count as usize; + if slice.len() >= options.max_profile_size { + return Err(CmsError::InvalidProfile); + } + let tags_end = tags_count + .safe_mul(TAG_SIZE)? + .safe_add(size_of::())?; + if slice.len() < tags_end { + return Err(CmsError::InvalidProfile); + } + let tags_slice = &slice[size_of::()..tags_end]; + let mut profile = ColorProfile { + rendering_intent: header.rendering_intent, + pcs: header.pcs, + profile_class: header.profile_class, + color_space: header.data_color_space, + white_point: header.illuminant.to_xyzd(), + version_internal: header.version, + ..Default::default() + }; + let color_space = profile.color_space; + for tag in tags_slice.chunks_exact(TAG_SIZE) { + let tag_value = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]); + let tag_entry = u32::from_be_bytes([tag[4], tag[5], tag[6], tag[7]]); + let tag_size = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]) as usize; + // Just ignore unknown tags + if let Ok(tag) = Tag::try_from(tag_value) { + match tag { + Tag::RedXyz => { + if color_space == DataColorSpace::Rgb { + profile.red_colorant = + Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; + } + } + Tag::GreenXyz => { + if color_space == DataColorSpace::Rgb { + profile.green_colorant = + Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; + } + } + Tag::BlueXyz => { + if color_space == DataColorSpace::Rgb { + profile.blue_colorant = + Self::read_xyz_tag(slice, tag_entry as usize, tag_size)?; + } + } + Tag::RedToneReproduction => { + if color_space == DataColorSpace::Rgb { + profile.red_trc = Self::read_trc_tag_s( + slice, + tag_entry as usize, + tag_size, + &options, + )?; + } + } + Tag::GreenToneReproduction => { + if color_space == DataColorSpace::Rgb { + profile.green_trc = Self::read_trc_tag_s( + slice, + tag_entry as usize, + tag_size, + &options, + )?; + } + } + Tag::BlueToneReproduction => { + if color_space == DataColorSpace::Rgb { + profile.blue_trc = Self::read_trc_tag_s( + slice, + tag_entry as usize, + tag_size, + &options, + )?; + } + } + Tag::GreyToneReproduction => { + if color_space == DataColorSpace::Gray { + profile.gray_trc = Self::read_trc_tag_s( + slice, + tag_entry as usize, + tag_size, + &options, + )?; + } + } + Tag::MediaWhitePoint => { + profile.media_white_point = + Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?; + } + Tag::Luminance => { + profile.luminance = + Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)?; + } + Tag::Measurement => { + profile.measurement = + Self::read_meas_tag(slice, tag_entry as usize, tag_size)?; + } + Tag::CodeIndependentPoints => { + // This tag may be present when the data colour space in the profile header is RGB, YCbCr, or XYZ, and the + // profile class in the profile header is Input or Display. The tag shall not be present for other data colour spaces + // or profile classes indicated in the profile header. + if (profile.profile_class == ProfileClass::InputDevice + || profile.profile_class == ProfileClass::DisplayDevice) + && (profile.color_space == DataColorSpace::Rgb + || profile.color_space == DataColorSpace::YCbr + || profile.color_space == DataColorSpace::Xyz) + { + profile.cicp = + Self::read_cicp_tag(slice, tag_entry as usize, tag_size)?; + } + } + Tag::ChromaticAdaptation => { + profile.chromatic_adaptation = + Self::read_chad_tag(slice, tag_entry as usize, tag_size)?; + } + Tag::BlackPoint => { + profile.black_point = + Self::read_xyz_tag(slice, tag_entry as usize, tag_size).map(Some)? + } + Tag::DeviceToPcsLutPerceptual => { + profile.lut_a_to_b_perceptual = + Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; + } + Tag::DeviceToPcsLutColorimetric => { + profile.lut_a_to_b_colorimetric = + Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; + } + Tag::DeviceToPcsLutSaturation => { + profile.lut_a_to_b_saturation = + Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; + } + Tag::PcsToDeviceLutPerceptual => { + profile.lut_b_to_a_perceptual = + Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; + } + Tag::PcsToDeviceLutColorimetric => { + profile.lut_b_to_a_colorimetric = + Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; + } + Tag::PcsToDeviceLutSaturation => { + profile.lut_b_to_a_saturation = + Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; + } + Tag::Gamut => { + profile.gamut = Self::read_lut_tag(slice, tag_entry, tag_size, &options)?; + } + Tag::Copyright => { + profile.copyright = + Self::read_string_tag(slice, tag_entry as usize, tag_size)?; + } + Tag::ProfileDescription => { + profile.description = + Self::read_string_tag(slice, tag_entry as usize, tag_size)?; + } + Tag::ViewingConditionsDescription => { + profile.viewing_conditions_description = + Self::read_string_tag(slice, tag_entry as usize, tag_size)?; + } + Tag::DeviceModel => { + profile.device_model = + Self::read_string_tag(slice, tag_entry as usize, tag_size)?; + } + Tag::DeviceManufacturer => { + profile.device_manufacturer = + Self::read_string_tag(slice, tag_entry as usize, tag_size)?; + } + Tag::CharTarget => { + profile.char_target = + Self::read_string_tag(slice, tag_entry as usize, tag_size)?; + } + Tag::Chromaticity => {} + Tag::ObserverConditions => { + profile.viewing_conditions = + Self::read_viewing_conditions(slice, tag_entry as usize, tag_size)?; + } + Tag::Technology => { + profile.technology = + Self::read_tech_tag(slice, tag_entry as usize, tag_size)?; + } + Tag::CalibrationDateTime => { + profile.calibration_date = + Self::read_date_time_tag(slice, tag_entry as usize, tag_size)?; + } + } + } + } + + Ok(profile) + } +} + +impl ColorProfile { + #[inline] + pub fn colorant_matrix(&self) -> Matrix3d { + Matrix3d { + v: [ + [ + self.red_colorant.x, + self.green_colorant.x, + self.blue_colorant.x, + ], + [ + self.red_colorant.y, + self.green_colorant.y, + self.blue_colorant.y, + ], + [ + self.red_colorant.z, + self.green_colorant.z, + self.blue_colorant.z, + ], + ], + } + } + + /// Computes colorants matrix. Returns not transposed matrix. + /// + /// To work on `const` context this method does have restrictions. + /// If invalid values were provided it may return invalid matrix or NaNs. + pub const fn colorants_matrix(white_point: XyY, primaries: ColorPrimaries) -> Matrix3d { + let red_xyz = primaries.red.to_xyzd(); + let green_xyz = primaries.green.to_xyzd(); + let blue_xyz = primaries.blue.to_xyzd(); + + let xyz_matrix = Matrix3d { + v: [ + [red_xyz.x, green_xyz.x, blue_xyz.x], + [red_xyz.y, green_xyz.y, blue_xyz.y], + [red_xyz.z, green_xyz.z, blue_xyz.z], + ], + }; + let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd()); + adapt_to_d50_d(colorants, white_point) + } + + /// Updates RGB triple colorimetry from 3 [Chromaticity] and white point + pub const fn update_rgb_colorimetry(&mut self, white_point: XyY, primaries: ColorPrimaries) { + let red_xyz = primaries.red.to_xyzd(); + let green_xyz = primaries.green.to_xyzd(); + let blue_xyz = primaries.blue.to_xyzd(); + + self.chromatic_adaptation = Some(BRADFORD_D); + self.update_rgb_colorimetry_triplet(white_point, red_xyz, green_xyz, blue_xyz) + } + + /// Updates RGB triple colorimetry from 3 [Xyzd] and white point + /// + /// To work on `const` context this method does have restrictions. + /// If invalid values were provided it may return invalid matrix or NaNs. + pub const fn update_rgb_colorimetry_triplet( + &mut self, + white_point: XyY, + red_xyz: Xyzd, + green_xyz: Xyzd, + blue_xyz: Xyzd, + ) { + let xyz_matrix = Matrix3d { + v: [ + [red_xyz.x, green_xyz.x, blue_xyz.x], + [red_xyz.y, green_xyz.y, blue_xyz.y], + [red_xyz.z, green_xyz.z, blue_xyz.z], + ], + }; + let colorants = ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point.to_xyzd()); + let colorants = adapt_to_d50_d(colorants, white_point); + + self.update_colorants(colorants); + } + + pub(crate) const fn update_colorants(&mut self, colorants: Matrix3d) { + // note: there's a transpose type of operation going on here + self.red_colorant.x = colorants.v[0][0]; + self.red_colorant.y = colorants.v[1][0]; + self.red_colorant.z = colorants.v[2][0]; + self.green_colorant.x = colorants.v[0][1]; + self.green_colorant.y = colorants.v[1][1]; + self.green_colorant.z = colorants.v[2][1]; + self.blue_colorant.x = colorants.v[0][2]; + self.blue_colorant.y = colorants.v[1][2]; + self.blue_colorant.z = colorants.v[2][2]; + } + + /// Updates RGB triple colorimetry from CICP + pub fn update_rgb_colorimetry_from_cicp(&mut self, cicp: CicpProfile) -> bool { + self.cicp = Some(cicp); + if !cicp.color_primaries.has_chromaticity() + || !cicp.transfer_characteristics.has_transfer_curve() + { + return false; + } + let primaries_xy: ColorPrimaries = match cicp.color_primaries.try_into() { + Ok(primaries) => primaries, + Err(_) => return false, + }; + let white_point: Chromaticity = match cicp.color_primaries.white_point() { + Ok(v) => v, + Err(_) => return false, + }; + self.update_rgb_colorimetry(white_point.to_xyyb(), primaries_xy); + + let red_trc: ToneReprCurve = match cicp.transfer_characteristics.try_into() { + Ok(trc) => trc, + Err(_) => return false, + }; + self.green_trc = Some(red_trc.clone()); + self.blue_trc = Some(red_trc.clone()); + self.red_trc = Some(red_trc); + false + } + + pub const fn rgb_to_xyz(&self, xyz_matrix: Matrix3f, wp: Xyz) -> Matrix3f { + let xyz_inverse = xyz_matrix.inverse(); + let s = xyz_inverse.mul_vector(wp.to_vector()); + let mut v = xyz_matrix.mul_row_vector::<0>(s); + v = v.mul_row_vector::<1>(s); + v.mul_row_vector::<2>(s) + } + + ///TODO: make primary instead of [rgb_to_xyz] in the next major version + pub(crate) const fn rgb_to_xyz_static(xyz_matrix: Matrix3f, wp: Xyz) -> Matrix3f { + let xyz_inverse = xyz_matrix.inverse(); + let s = xyz_inverse.mul_vector(wp.to_vector()); + let mut v = xyz_matrix.mul_row_vector::<0>(s); + v = v.mul_row_vector::<1>(s); + v.mul_row_vector::<2>(s) + } + + /// If Primaries is invalid will return invalid matrix on const context. + /// This assumes not transposed matrix and returns not transposed matrix. + pub const fn rgb_to_xyz_d(xyz_matrix: Matrix3d, wp: Xyzd) -> Matrix3d { + let xyz_inverse = xyz_matrix.inverse(); + let s = xyz_inverse.mul_vector(wp.to_vector_d()); + let mut v = xyz_matrix.mul_row_vector::<0>(s); + v = v.mul_row_vector::<1>(s); + v = v.mul_row_vector::<2>(s); + v + } + + pub fn rgb_to_xyz_matrix(&self) -> Matrix3d { + let xyz_matrix = self.colorant_matrix(); + let white_point = Chromaticity::D50.to_xyzd(); + ColorProfile::rgb_to_xyz_d(xyz_matrix, white_point) + } + + /// Computes transform matrix RGB -> XYZ -> RGB + /// Current profile is used as source, other as destination + pub fn transform_matrix(&self, dest: &ColorProfile) -> Matrix3d { + let source = self.rgb_to_xyz_matrix(); + let dst = dest.rgb_to_xyz_matrix(); + let dest_inverse = dst.inverse(); + dest_inverse.mat_mul(source) + } + + /// Returns volume of colors stored in profile + pub fn profile_volume(&self) -> Option { + let red_prim = self.red_colorant; + let green_prim = self.green_colorant; + let blue_prim = self.blue_colorant; + let tetrahedral_vertices = Matrix3d { + v: [ + [red_prim.x, red_prim.y, red_prim.z], + [green_prim.x, green_prim.y, green_prim.z], + [blue_prim.x, blue_prim.y, blue_prim.z], + ], + }; + let det = tetrahedral_vertices.determinant()?; + Some((det / 6.0f64) as f32) + } + + pub(crate) fn has_device_to_pcs_lut(&self) -> bool { + self.lut_a_to_b_perceptual.is_some() + || self.lut_a_to_b_saturation.is_some() + || self.lut_a_to_b_colorimetric.is_some() + } + + pub(crate) fn has_pcs_to_device_lut(&self) -> bool { + self.lut_b_to_a_perceptual.is_some() + || self.lut_b_to_a_saturation.is_some() + || self.lut_b_to_a_colorimetric.is_some() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn test_gray() { + if let Ok(gray_icc) = fs::read("./assets/Generic Gray Gamma 2.2 Profile.icc") { + let f_p = ColorProfile::new_from_slice(&gray_icc).unwrap(); + assert!(f_p.gray_trc.is_some()); + } + } + + #[test] + fn test_perceptual() { + if let Ok(srgb_perceptual_icc) = fs::read("./assets/srgb_perceptual.icc") { + let f_p = ColorProfile::new_from_slice(&srgb_perceptual_icc).unwrap(); + assert_eq!(f_p.pcs, DataColorSpace::Lab); + assert_eq!(f_p.color_space, DataColorSpace::Rgb); + assert_eq!(f_p.version(), ProfileVersion::V4_2); + assert!(f_p.lut_a_to_b_perceptual.is_some()); + assert!(f_p.lut_b_to_a_perceptual.is_some()); + } + } + + #[test] + fn test_us_swop_coated() { + if let Ok(us_swop_coated) = fs::read("./assets/us_swop_coated.icc") { + let f_p = ColorProfile::new_from_slice(&us_swop_coated).unwrap(); + assert_eq!(f_p.pcs, DataColorSpace::Lab); + assert_eq!(f_p.color_space, DataColorSpace::Cmyk); + assert_eq!(f_p.version(), ProfileVersion::V2_0); + + assert!(f_p.lut_a_to_b_perceptual.is_some()); + assert!(f_p.lut_b_to_a_perceptual.is_some()); + + assert!(f_p.lut_a_to_b_colorimetric.is_some()); + assert!(f_p.lut_b_to_a_colorimetric.is_some()); + + assert!(f_p.gamut.is_some()); + + assert!(f_p.copyright.is_some()); + assert!(f_p.description.is_some()); + } + } + + #[test] + fn test_matrix_shaper() { + if let Ok(matrix_shaper) = fs::read("./assets/Display P3.icc") { + let f_p = ColorProfile::new_from_slice(&matrix_shaper).unwrap(); + assert_eq!(f_p.pcs, DataColorSpace::Xyz); + assert_eq!(f_p.color_space, DataColorSpace::Rgb); + assert_eq!(f_p.version(), ProfileVersion::V4_0); + + assert!(f_p.red_trc.is_some()); + assert!(f_p.blue_trc.is_some()); + assert!(f_p.green_trc.is_some()); + + assert_ne!(f_p.red_colorant, Xyzd::default()); + assert_ne!(f_p.blue_colorant, Xyzd::default()); + assert_ne!(f_p.green_colorant, Xyzd::default()); + + assert!(f_p.copyright.is_some()); + assert!(f_p.description.is_some()); + } + } +} diff --git a/deps/moxcms/src/reader.rs b/deps/moxcms/src/reader.rs new file mode 100644 index 0000000..a047eca --- /dev/null +++ b/deps/moxcms/src/reader.rs @@ -0,0 +1,955 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::err::try_vec; +use crate::helpers::{read_matrix_3d, read_vector_3d}; +use crate::profile::LutDataType; +use crate::safe_math::{SafeAdd, SafeMul, SafePowi}; +use crate::tag::{TAG_SIZE, TagTypeDefinition}; +use crate::{ + CicpColorPrimaries, CicpProfile, CmsError, ColorDateTime, ColorProfile, DescriptionString, + LocalizableString, LutMultidimensionalType, LutStore, LutType, LutWarehouse, Matrix3d, + Matrix3f, MatrixCoefficients, Measurement, MeasurementGeometry, ParsingOptions, ProfileText, + StandardIlluminant, StandardObserver, TechnologySignatures, ToneReprCurve, + TransferCharacteristics, Vector3d, ViewingConditions, Xyz, Xyzd, +}; + +/// Produces the nearest float to `a` with a maximum error of 1/1024 which +/// happens for large values like 0x40000040. +#[inline] +pub(crate) const fn s15_fixed16_number_to_float(a: i32) -> f32 { + a as f32 / 65536. +} + +#[inline] +pub(crate) const fn s15_fixed16_number_to_double(a: i32) -> f64 { + a as f64 / 65536. +} + +#[inline] +pub(crate) const fn uint16_number_to_float(a: u32) -> f32 { + a as f32 / 65536. +} + +#[inline] +pub(crate) const fn uint16_number_to_float_fast(a: u32) -> f32 { + a as f32 * (1. / 65536.) +} + +// #[inline] +// pub(crate) fn uint8_number_to_float(a: u8) -> f32 { +// a as f32 / 255.0 +// } + +#[inline] +pub(crate) fn uint8_number_to_float_fast(a: u8) -> f32 { + a as f32 * (1. / 255.0) +} + +fn utf16be_to_utf16(slice: &[u8]) -> Result, CmsError> { + let mut vec = try_vec![0u16; slice.len() / 2]; + for (dst, chunk) in vec.iter_mut().zip(slice.chunks_exact(2)) { + *dst = u16::from_be_bytes([chunk[0], chunk[1]]); + } + Ok(vec) +} + +impl ColorProfile { + #[inline] + pub(crate) fn read_lut_type( + slice: &[u8], + entry: usize, + tag_size: usize, + ) -> Result { + let tag_size = if tag_size == 0 { TAG_SIZE } else { tag_size }; + let last_tag_offset = tag_size.safe_add(entry)?; + if last_tag_offset > slice.len() { + return Err(CmsError::InvalidProfile); + } + let tag = &slice[entry..last_tag_offset]; + if tag.len() < 48 { + return Err(CmsError::InvalidProfile); + } + let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]); + LutType::try_from(tag_type) + } + + #[inline] + pub(crate) fn read_viewing_conditions( + slice: &[u8], + entry: usize, + tag_size: usize, + ) -> Result, CmsError> { + if tag_size < 36 { + return Ok(None); + } + if slice.len() < entry.safe_add(36)? { + return Err(CmsError::InvalidProfile); + } + let tag = &slice[entry..entry.safe_add(36)?]; + let tag_type = + TagTypeDefinition::from(u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]])); + // Ignore unknown + if tag_type != TagTypeDefinition::DefViewingConditions { + return Ok(None); + } + let illuminant_x = i32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]); + let illuminant_y = i32::from_be_bytes([tag[12], tag[13], tag[14], tag[15]]); + let illuminant_z = i32::from_be_bytes([tag[16], tag[17], tag[18], tag[19]]); + + let surround_x = i32::from_be_bytes([tag[20], tag[21], tag[22], tag[23]]); + let surround_y = i32::from_be_bytes([tag[24], tag[25], tag[26], tag[27]]); + let surround_z = i32::from_be_bytes([tag[28], tag[29], tag[30], tag[31]]); + + let illuminant_type = u32::from_be_bytes([tag[32], tag[33], tag[34], tag[35]]); + + let illuminant = Xyz::new( + s15_fixed16_number_to_float(illuminant_x), + s15_fixed16_number_to_float(illuminant_y), + s15_fixed16_number_to_float(illuminant_z), + ); + + let surround = Xyz::new( + s15_fixed16_number_to_float(surround_x), + s15_fixed16_number_to_float(surround_y), + s15_fixed16_number_to_float(surround_z), + ); + + let observer = StandardObserver::from(illuminant_type); + + Ok(Some(ViewingConditions { + illuminant, + surround, + observer, + })) + } + + pub(crate) fn read_string_tag( + slice: &[u8], + entry: usize, + tag_size: usize, + ) -> Result, CmsError> { + let tag_size = if tag_size == 0 { TAG_SIZE } else { tag_size }; + if tag_size < 4 { + return Ok(None); + } + let last_tag_offset = tag_size.safe_add(entry)?; + if last_tag_offset > slice.len() { + return Err(CmsError::InvalidProfile); + } + let tag = &slice[entry..last_tag_offset]; + if tag.len() < 8 { + return Ok(None); + } + let tag_type = + TagTypeDefinition::from(u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]])); + // Ignore unknown + if tag_type == TagTypeDefinition::Text { + let sliced_from_to_end = &tag[8..tag.len()]; + let str = String::from_utf8_lossy(sliced_from_to_end); + return Ok(Some(ProfileText::PlainString(str.to_string()))); + } else if tag_type == TagTypeDefinition::MultiLocalizedUnicode { + if tag.len() < 28 { + return Err(CmsError::InvalidProfile); + } + // let record_size = u32::from_be_bytes([tag[12], tag[13], tag[14], tag[15]]) as usize; + // // Record size is reserved to be 12. + // if record_size != 12 { + // return Err(CmsError::InvalidIcc); + // } + let records_count = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]) as usize; + let primary_language_code = String::from_utf8_lossy(&[tag[16], tag[17]]).to_string(); + let primary_country_code = String::from_utf8_lossy(&[tag[18], tag[19]]).to_string(); + let first_string_record_length = + u32::from_be_bytes([tag[20], tag[21], tag[22], tag[23]]) as usize; + let first_record_offset = + u32::from_be_bytes([tag[24], tag[25], tag[26], tag[27]]) as usize; + + if tag.len() < first_record_offset.safe_add(first_string_record_length)? { + return Ok(None); + } + + let resliced = + &tag[first_record_offset..first_record_offset + first_string_record_length]; + let cvt = utf16be_to_utf16(resliced)?; + let string_record = String::from_utf16_lossy(&cvt); + + let mut records = vec![LocalizableString { + language: primary_language_code, + country: primary_country_code, + value: string_record, + }]; + + for record in 1..records_count { + // Localizable header must be at least 12 bytes + let localizable_header_offset = if record == 1 { + 28 + } else { + 28 + 12 * (record - 1) + }; + if tag.len() < localizable_header_offset + 12 { + return Err(CmsError::InvalidProfile); + } + let choked = &tag[localizable_header_offset..localizable_header_offset + 12]; + + let language_code = String::from_utf8_lossy(&[choked[0], choked[1]]).to_string(); + let country_code = String::from_utf8_lossy(&[choked[2], choked[3]]).to_string(); + let record_length = + u32::from_be_bytes([choked[4], choked[5], choked[6], choked[7]]) as usize; + let string_offset = + u32::from_be_bytes([choked[8], choked[9], choked[10], choked[11]]) as usize; + + if tag.len() < string_offset.safe_add(record_length)? { + return Ok(None); + } + let resliced = &tag[string_offset..string_offset + record_length]; + let cvt = utf16be_to_utf16(resliced)?; + let string_record = String::from_utf16_lossy(&cvt); + records.push(LocalizableString { + country: country_code, + language: language_code, + value: string_record, + }); + } + + return Ok(Some(ProfileText::Localizable(records))); + } else if tag_type == TagTypeDefinition::Description { + if tag.len() < 12 { + return Err(CmsError::InvalidProfile); + } + let ascii_length = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]) as usize; + if tag.len() < 12.safe_add(ascii_length)? { + return Err(CmsError::InvalidProfile); + } + let sliced = &tag[12..12 + ascii_length]; + let ascii_string = String::from_utf8_lossy(sliced).to_string(); + + let mut last_position = 12 + ascii_length; + if tag.len() < last_position + 8 { + return Err(CmsError::InvalidProfile); + } + let uc = &tag[last_position..last_position + 8]; + let unicode_code = u32::from_be_bytes([uc[0], uc[1], uc[2], uc[3]]); + let unicode_length = u32::from_be_bytes([uc[4], uc[5], uc[6], uc[7]]) as usize * 2; + if tag.len() < unicode_length.safe_add(8)?.safe_add(last_position)? { + return Ok(None); + } + + last_position += 8; + let uc = &tag[last_position..last_position + unicode_length]; + let wc = utf16be_to_utf16(uc)?; + let unicode_string = String::from_utf16_lossy(&wc).to_string(); + + // last_position += unicode_length; + // + // if tag.len() < last_position + 2 { + // return Err(CmsError::InvalidIcc); + // } + + // let uc = &tag[last_position..last_position + 2]; + // let script_code = uc[0]; + // let mac_length = uc[1] as usize; + // last_position += 2; + // if tag.len() < last_position + mac_length { + // return Err(CmsError::InvalidIcc); + // } + // + // let uc = &tag[last_position..last_position + unicode_length]; + // let wc = utf16be_to_utf16(uc); + // let mac_string = String::from_utf16_lossy(&wc).to_string(); + + return Ok(Some(ProfileText::Description(DescriptionString { + ascii_string, + unicode_language_code: unicode_code, + unicode_string, + mac_string: "".to_string(), + script_code_code: -1, + }))); + } + Ok(None) + } + + #[inline] + fn read_lut_table_f32(table: &[u8], lut_type: LutType) -> Result { + if lut_type == LutType::Lut16 { + let mut clut = try_vec![0u16; table.len() / 2]; + for (src, dst) in table.chunks_exact(2).zip(clut.iter_mut()) { + *dst = u16::from_be_bytes([src[0], src[1]]); + } + Ok(LutStore::Store16(clut)) + } else if lut_type == LutType::Lut8 { + let mut clut = try_vec![0u8; table.len()]; + for (&src, dst) in table.iter().zip(clut.iter_mut()) { + *dst = src; + } + Ok(LutStore::Store8(clut)) + } else { + unreachable!("This should never happen, report to https://github.com/awxkee/moxcms") + } + } + + #[inline] + fn read_nested_tone_curves( + slice: &[u8], + offset: usize, + length: usize, + options: &ParsingOptions, + ) -> Result>, CmsError> { + let mut curve_offset: usize = offset; + let mut curves = Vec::new(); + for _ in 0..length { + if slice.len() < curve_offset.safe_add(12)? { + return Err(CmsError::InvalidProfile); + } + let mut tag_size = 0usize; + let new_curve = Self::read_trc_tag(slice, curve_offset, 0, &mut tag_size, options)?; + match new_curve { + None => return Err(CmsError::InvalidProfile), + Some(curve) => curves.push(curve), + } + curve_offset += tag_size; + // 4 byte aligned + if curve_offset % 4 != 0 { + curve_offset += 4 - curve_offset % 4; + } + } + Ok(Some(curves)) + } + + #[inline] + pub(crate) fn read_lut_abm_type( + slice: &[u8], + entry: usize, + tag_size: usize, + to_pcs: bool, + options: &ParsingOptions, + ) -> Result, CmsError> { + if tag_size < 48 { + return Ok(None); + } + let last_tag_offset = tag_size.safe_add(entry)?; + if last_tag_offset > slice.len() { + return Err(CmsError::InvalidProfile); + } + let tag = &slice[entry..last_tag_offset]; + if tag.len() < 48 { + return Err(CmsError::InvalidProfile); + } + let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]); + let tag_type_definition = TagTypeDefinition::from(tag_type); + if tag_type_definition != TagTypeDefinition::MabLut + && tag_type_definition != TagTypeDefinition::MbaLut + { + return Ok(None); + } + let in_channels = tag[8]; + let out_channels = tag[9]; + if in_channels > 4 && out_channels > 4 { + return Ok(None); + } + let a_curve_offset = u32::from_be_bytes([tag[28], tag[29], tag[30], tag[31]]) as usize; + let clut_offset = u32::from_be_bytes([tag[24], tag[25], tag[26], tag[27]]) as usize; + let m_curve_offset = u32::from_be_bytes([tag[20], tag[21], tag[22], tag[23]]) as usize; + let matrix_offset = u32::from_be_bytes([tag[16], tag[17], tag[18], tag[19]]) as usize; + let b_curve_offset = u32::from_be_bytes([tag[12], tag[13], tag[14], tag[15]]) as usize; + + let transform: Matrix3d; + let bias: Vector3d; + if matrix_offset != 0 { + let matrix_end = matrix_offset.safe_add(12 * 4)?; + if tag.len() < matrix_end { + return Err(CmsError::InvalidProfile); + } + + let m_tag = &tag[matrix_offset..matrix_end]; + + bias = read_vector_3d(&m_tag[36..48])?; + transform = read_matrix_3d(m_tag)?; + } else { + transform = Matrix3d::IDENTITY; + bias = Vector3d::default(); + } + + let mut grid_points: [u8; 16] = [0; 16]; + + let clut_table: Option = if clut_offset != 0 { + // Check if CLUT formed correctly + if clut_offset.safe_add(20)? > tag.len() { + return Err(CmsError::InvalidProfile); + } + + let clut_sizes_slice = &tag[clut_offset..clut_offset.safe_add(16)?]; + for (&s, v) in clut_sizes_slice.iter().zip(grid_points.iter_mut()) { + *v = s; + } + + let mut clut_size = 1u32; + for &i in grid_points.iter().take(in_channels as usize) { + clut_size *= i as u32; + } + clut_size *= out_channels as u32; + + if clut_size == 0 { + return Err(CmsError::InvalidProfile); + } + + if clut_size > 10_000_000 { + return Err(CmsError::InvalidProfile); + } + + let clut_offset20 = clut_offset.safe_add(20)?; + + let clut_header = &tag[clut_offset..clut_offset20]; + let entry_size = clut_header[16]; + if entry_size != 1 && entry_size != 2 { + return Err(CmsError::InvalidProfile); + } + + let clut_end = + clut_offset20.safe_add(clut_size.safe_mul(entry_size as u32)? as usize)?; + + if tag.len() < clut_end { + return Err(CmsError::InvalidProfile); + } + + let shaped_clut_table = &tag[clut_offset20..clut_end]; + Some(Self::read_lut_table_f32( + shaped_clut_table, + if entry_size == 1 { + LutType::Lut8 + } else { + LutType::Lut16 + }, + )?) + } else { + None + }; + + let a_curves = if a_curve_offset == 0 { + Vec::new() + } else { + Self::read_nested_tone_curves( + tag, + a_curve_offset, + if to_pcs { + in_channels as usize + } else { + out_channels as usize + }, + options, + )? + .ok_or(CmsError::InvalidProfile)? + }; + + let m_curves = if m_curve_offset == 0 { + Vec::new() + } else { + Self::read_nested_tone_curves( + tag, + m_curve_offset, + if to_pcs { + out_channels as usize + } else { + in_channels as usize + }, + options, + )? + .ok_or(CmsError::InvalidProfile)? + }; + + let b_curves = if b_curve_offset == 0 { + Vec::new() + } else { + Self::read_nested_tone_curves( + tag, + b_curve_offset, + if to_pcs { + out_channels as usize + } else { + in_channels as usize + }, + options, + )? + .ok_or(CmsError::InvalidProfile)? + }; + + let wh = LutWarehouse::Multidimensional(LutMultidimensionalType { + num_input_channels: in_channels, + num_output_channels: out_channels, + matrix: transform, + clut: clut_table, + a_curves, + b_curves, + m_curves, + grid_points, + bias, + }); + Ok(Some(wh)) + } + + #[inline] + pub(crate) fn read_lut_a_to_b_type( + slice: &[u8], + entry: usize, + tag_size: usize, + parsing_options: &ParsingOptions, + ) -> Result, CmsError> { + if tag_size < 48 { + return Ok(None); + } + let last_tag_offset = tag_size.safe_add(entry)?; + if last_tag_offset > slice.len() { + return Err(CmsError::InvalidProfile); + } + let tag = &slice[entry..last_tag_offset]; + if tag.len() < 48 { + return Err(CmsError::InvalidProfile); + } + let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]); + let lut_type = LutType::try_from(tag_type)?; + assert!(lut_type == LutType::Lut8 || lut_type == LutType::Lut16); + + if lut_type == LutType::Lut16 && tag.len() < 52 { + return Err(CmsError::InvalidProfile); + } + + let num_input_table_entries: u16 = match lut_type { + LutType::Lut8 => 256, + LutType::Lut16 => u16::from_be_bytes([tag[48], tag[49]]), + _ => unreachable!(), + }; + let num_output_table_entries: u16 = match lut_type { + LutType::Lut8 => 256, + LutType::Lut16 => u16::from_be_bytes([tag[50], tag[51]]), + _ => unreachable!(), + }; + + if !(2..=4096).contains(&num_input_table_entries) + || !(2..=4096).contains(&num_output_table_entries) + { + return Err(CmsError::InvalidProfile); + } + + let input_offset: usize = match lut_type { + LutType::Lut8 => 48, + LutType::Lut16 => 52, + _ => unreachable!(), + }; + let entry_size: usize = match lut_type { + LutType::Lut8 => 1, + LutType::Lut16 => 2, + _ => unreachable!(), + }; + + let in_chan = tag[8]; + let out_chan = tag[9]; + let is_3_to_4 = in_chan == 3 || out_chan == 4; + let is_4_to_3 = in_chan == 4 || out_chan == 3; + if !is_3_to_4 && !is_4_to_3 { + return Err(CmsError::InvalidProfile); + } + let grid_points = tag[10]; + let clut_size = (grid_points as u32).safe_powi(in_chan as u32)? as usize; + + if !(1..=parsing_options.max_allowed_clut_size).contains(&clut_size) { + return Err(CmsError::InvalidProfile); + } + + assert!(tag.len() >= 48); + + let transform = read_matrix_3d(&tag[12..48])?; + + let lut_input_size = num_input_table_entries.safe_mul(in_chan as u16)? as usize; + + let linearization_table_end = lut_input_size + .safe_mul(entry_size)? + .safe_add(input_offset)?; + if tag.len() < linearization_table_end { + return Err(CmsError::InvalidProfile); + } + let shaped_input_table = &tag[input_offset..linearization_table_end]; + let linearization_table = Self::read_lut_table_f32(shaped_input_table, lut_type)?; + + let clut_offset = linearization_table_end; + + let clut_data_size = clut_size + .safe_mul(out_chan as usize)? + .safe_mul(entry_size)?; + + if tag.len() < clut_offset.safe_add(clut_data_size)? { + return Err(CmsError::InvalidProfile); + } + + let shaped_clut_table = &tag[clut_offset..clut_offset + clut_data_size]; + let clut_table = Self::read_lut_table_f32(shaped_clut_table, lut_type)?; + + let output_offset = clut_offset.safe_add(clut_data_size)?; + + let output_size = (num_output_table_entries as usize).safe_mul(out_chan as usize)?; + + let shaped_output_table = + &tag[output_offset..output_offset.safe_add(output_size.safe_mul(entry_size)?)?]; + let gamma_table = Self::read_lut_table_f32(shaped_output_table, lut_type)?; + + let wh = LutWarehouse::Lut(LutDataType { + num_input_table_entries, + num_output_table_entries, + num_input_channels: in_chan, + num_output_channels: out_chan, + num_clut_grid_points: grid_points, + matrix: transform, + input_table: linearization_table, + clut_table, + output_table: gamma_table, + lut_type, + }); + Ok(Some(wh)) + } + + pub(crate) fn read_lut_tag( + slice: &[u8], + tag_entry: u32, + tag_size: usize, + parsing_options: &ParsingOptions, + ) -> Result, CmsError> { + let lut_type = Self::read_lut_type(slice, tag_entry as usize, tag_size)?; + Ok(if lut_type == LutType::Lut8 || lut_type == LutType::Lut16 { + Self::read_lut_a_to_b_type(slice, tag_entry as usize, tag_size, parsing_options)? + } else if lut_type == LutType::LutMba || lut_type == LutType::LutMab { + Self::read_lut_abm_type( + slice, + tag_entry as usize, + tag_size, + lut_type == LutType::LutMab, + parsing_options, + )? + } else { + None + }) + } + + pub(crate) fn read_trc_tag_s( + slice: &[u8], + entry: usize, + tag_size: usize, + options: &ParsingOptions, + ) -> Result, CmsError> { + let mut _empty = 0usize; + Self::read_trc_tag(slice, entry, tag_size, &mut _empty, options) + } + + pub(crate) fn read_trc_tag( + slice: &[u8], + entry: usize, + tag_size: usize, + read_size: &mut usize, + options: &ParsingOptions, + ) -> Result, CmsError> { + if slice.len() < entry.safe_add(4)? { + return Ok(None); + } + let small_tag = &slice[entry..entry + 4]; + // We require always recognize tone curves. + let curve_type = TagTypeDefinition::from(u32::from_be_bytes([ + small_tag[0], + small_tag[1], + small_tag[2], + small_tag[3], + ])); + if tag_size != 0 && tag_size < TAG_SIZE { + return Ok(None); + } + let last_tag_offset = if tag_size != 0 { + tag_size + entry + } else { + slice.len() + }; + if last_tag_offset > slice.len() { + return Err(CmsError::MalformedTrcCurve("Data exhausted".to_string())); + } + let tag = &slice[entry..last_tag_offset]; + if tag.len() < TAG_SIZE { + return Err(CmsError::MalformedTrcCurve("Data exhausted".to_string())); + } + if curve_type == TagTypeDefinition::LutToneCurve { + let entry_count = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]) as usize; + if entry_count == 0 { + return Ok(Some(ToneReprCurve::Lut(vec![]))); + } + if entry_count > options.max_allowed_trc_size { + return Err(CmsError::CurveLutIsTooLarge); + } + let curve_end = entry_count.safe_mul(size_of::())?.safe_add(12)?; + if tag.len() < curve_end { + return Err(CmsError::MalformedTrcCurve( + "Curve end ends to early".to_string(), + )); + } + let curve_sliced = &tag[12..curve_end]; + let mut curve_values = try_vec![0u16; entry_count]; + for (value, curve_value) in curve_sliced.chunks_exact(2).zip(curve_values.iter_mut()) { + let gamma_s15 = u16::from_be_bytes([value[0], value[1]]); + *curve_value = gamma_s15; + } + *read_size = curve_end; + Ok(Some(ToneReprCurve::Lut(curve_values))) + } else if curve_type == TagTypeDefinition::ParametricToneCurve { + let entry_count = u16::from_be_bytes([tag[8], tag[9]]) as usize; + if entry_count > 4 { + return Err(CmsError::MalformedTrcCurve( + "Parametric curve has unknown entries count".to_string(), + )); + } + + const COUNT_TO_LENGTH: [usize; 5] = [1, 3, 4, 5, 7]; //PARAMETRIC_CURVE_TYPE + + if tag.len() < 12 + COUNT_TO_LENGTH[entry_count] * size_of::() { + return Err(CmsError::MalformedTrcCurve( + "Parametric curve has unknown entries count exhaust data too early".to_string(), + )); + } + let curve_sliced = &tag[12..12 + COUNT_TO_LENGTH[entry_count] * size_of::()]; + let mut params = try_vec![0f32; COUNT_TO_LENGTH[entry_count]]; + for (value, param_value) in curve_sliced.chunks_exact(4).zip(params.iter_mut()) { + let parametric_value = i32::from_be_bytes([value[0], value[1], value[2], value[3]]); + *param_value = s15_fixed16_number_to_float(parametric_value); + } + if entry_count == 1 || entry_count == 2 { + // we have a type 1 or type 2 function that has a division by `a` + let a: f32 = params[1]; + if a == 0.0 { + return Err(CmsError::ParametricCurveZeroDivision); + } + } + *read_size = 12 + COUNT_TO_LENGTH[entry_count] * 4; + Ok(Some(ToneReprCurve::Parametric(params))) + } else { + Err(CmsError::MalformedTrcCurve( + "Unknown parametric curve tag".to_string(), + )) + } + } + + #[inline] + pub(crate) fn read_chad_tag( + slice: &[u8], + entry: usize, + tag_size: usize, + ) -> Result, CmsError> { + let last_tag_offset = tag_size.safe_add(entry)?; + if last_tag_offset > slice.len() { + return Err(CmsError::InvalidProfile); + } + if slice[entry..].len() < 8 { + return Err(CmsError::InvalidProfile); + } + if tag_size < 8 { + return Ok(None); + } + if (tag_size - 8) / 4 != 9 { + return Ok(None); + } + let tag0 = &slice[entry..entry.safe_add(8)?]; + let c_type = + TagTypeDefinition::from(u32::from_be_bytes([tag0[0], tag0[1], tag0[2], tag0[3]])); + if c_type != TagTypeDefinition::S15Fixed16Array { + return Err(CmsError::InvalidProfile); + } + if slice.len() < 9 * size_of::() + 8 { + return Err(CmsError::InvalidProfile); + } + let tag = &slice[entry + 8..last_tag_offset]; + if tag.len() != size_of::() { + return Err(CmsError::InvalidProfile); + } + let matrix = read_matrix_3d(tag)?; + Ok(Some(matrix)) + } + + #[inline] + pub(crate) fn read_tech_tag( + slice: &[u8], + entry: usize, + tag_size: usize, + ) -> Result, CmsError> { + if tag_size < TAG_SIZE { + return Err(CmsError::InvalidProfile); + } + let last_tag_offset = tag_size.safe_add(entry)?; + if last_tag_offset > slice.len() { + return Err(CmsError::InvalidProfile); + } + let tag = &slice[entry..entry.safe_add(12)?]; + let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]); + let def = TagTypeDefinition::from(tag_type); + if def == TagTypeDefinition::Signature { + let sig = u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]); + let tech_sig = TechnologySignatures::from(sig); + return Ok(Some(tech_sig)); + } + Ok(None) + } + + #[inline] + pub(crate) fn read_date_time_tag( + slice: &[u8], + entry: usize, + tag_size: usize, + ) -> Result, CmsError> { + if tag_size < 20 { + return Ok(None); + } + let last_tag_offset = tag_size.safe_add(entry)?; + if last_tag_offset > slice.len() { + return Err(CmsError::InvalidProfile); + } + let tag = &slice[entry..entry.safe_add(20)?]; + let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]); + let def = TagTypeDefinition::from(tag_type); + if def == TagTypeDefinition::DateTime { + let tag_value = &slice[8..20]; + let time = ColorDateTime::new_from_slice(tag_value)?; + return Ok(Some(time)); + } + Ok(None) + } + + #[inline] + pub(crate) fn read_meas_tag( + slice: &[u8], + entry: usize, + tag_size: usize, + ) -> Result, CmsError> { + if tag_size < TAG_SIZE { + return Ok(None); + } + let last_tag_offset = tag_size.safe_add(entry)?; + if last_tag_offset > slice.len() { + return Err(CmsError::InvalidProfile); + } + let tag = &slice[entry..entry + 12]; + let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]); + let def = TagTypeDefinition::from(tag_type); + if def != TagTypeDefinition::Measurement { + return Ok(None); + } + if 36 > slice.len() { + return Err(CmsError::InvalidProfile); + } + let tag = &slice[entry..entry + 36]; + let observer = + StandardObserver::from(u32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]])); + let q15_16_x = i32::from_be_bytes([tag[12], tag[13], tag[14], tag[15]]); + let q15_16_y = i32::from_be_bytes([tag[16], tag[17], tag[18], tag[19]]); + let q15_16_z = i32::from_be_bytes([tag[20], tag[21], tag[22], tag[23]]); + let x = s15_fixed16_number_to_float(q15_16_x); + let y = s15_fixed16_number_to_float(q15_16_y); + let z = s15_fixed16_number_to_float(q15_16_z); + let xyz = Xyz::new(x, y, z); + let geometry = + MeasurementGeometry::from(u32::from_be_bytes([tag[24], tag[25], tag[26], tag[27]])); + let flare = + uint16_number_to_float(u32::from_be_bytes([tag[28], tag[29], tag[30], tag[31]])); + let illuminant = + StandardIlluminant::from(u32::from_be_bytes([tag[32], tag[33], tag[34], tag[35]])); + Ok(Some(Measurement { + flare, + illuminant, + geometry, + observer, + backing: xyz, + })) + } + + #[inline] + pub(crate) fn read_xyz_tag( + slice: &[u8], + entry: usize, + tag_size: usize, + ) -> Result { + if tag_size < TAG_SIZE { + return Ok(Xyzd::default()); + } + let last_tag_offset = tag_size.safe_add(entry)?; + if last_tag_offset > slice.len() { + return Err(CmsError::InvalidProfile); + } + let tag = &slice[entry..entry + 12]; + let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]); + let def = TagTypeDefinition::from(tag_type); + if def != TagTypeDefinition::Xyz { + return Ok(Xyzd::default()); + } + + let tag = &slice[entry..last_tag_offset]; + if tag.len() < 20 { + return Err(CmsError::InvalidProfile); + } + let q15_16_x = i32::from_be_bytes([tag[8], tag[9], tag[10], tag[11]]); + let q15_16_y = i32::from_be_bytes([tag[12], tag[13], tag[14], tag[15]]); + let q15_16_z = i32::from_be_bytes([tag[16], tag[17], tag[18], tag[19]]); + let x = s15_fixed16_number_to_double(q15_16_x); + let y = s15_fixed16_number_to_double(q15_16_y); + let z = s15_fixed16_number_to_double(q15_16_z); + Ok(Xyzd { x, y, z }) + } + + #[inline] + pub(crate) fn read_cicp_tag( + slice: &[u8], + entry: usize, + tag_size: usize, + ) -> Result, CmsError> { + if tag_size < TAG_SIZE { + return Ok(None); + } + let last_tag_offset = tag_size.safe_add(entry)?; + if last_tag_offset > slice.len() { + return Err(CmsError::InvalidProfile); + } + let tag = &slice[entry..last_tag_offset]; + if tag.len() < 12 { + return Err(CmsError::InvalidProfile); + } + let tag_type = u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]); + let def = TagTypeDefinition::from(tag_type); + if def != TagTypeDefinition::Cicp { + return Ok(None); + } + let primaries = CicpColorPrimaries::try_from(tag[8])?; + let transfer_characteristics = TransferCharacteristics::try_from(tag[9])?; + let matrix_coefficients = MatrixCoefficients::try_from(tag[10])?; + let full_range = tag[11] == 1; + Ok(Some(CicpProfile { + color_primaries: primaries, + transfer_characteristics, + matrix_coefficients, + full_range, + })) + } +} diff --git a/deps/moxcms/src/rgb.rs b/deps/moxcms/src/rgb.rs new file mode 100644 index 0000000..057e62d --- /dev/null +++ b/deps/moxcms/src/rgb.rs @@ -0,0 +1,723 @@ +/* + * // Copyright 2024 (c) the Radzivon Bartoshyk. All rights reserved. + * // + * // Use of this source code is governed by a BSD-style + * // license that can be found in the LICENSE file. + */ +use crate::math::{FusedMultiplyAdd, m_clamp, m_max, m_min}; +use crate::mlaf::mlaf; +use crate::{Matrix3f, Vector3, Xyz}; +use num_traits::{AsPrimitive, Bounded, Float, Num, Pow, Signed}; +use pxfm::{ + f_exp, f_exp2, f_exp2f, f_exp10, f_exp10f, f_expf, f_log, f_log2, f_log2f, f_log10, f_log10f, + f_logf, f_pow, f_powf, +}; +use std::cmp::Ordering; +use std::ops::{Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub}; + +#[repr(C)] +#[derive(Debug, PartialOrd, PartialEq, Clone, Copy, Default)] +/// Represents any RGB values +pub struct Rgb { + /// Red component + pub r: T, + /// Green component + pub g: T, + /// Blue component + pub b: T, +} + +impl Rgb { + pub fn new(r: T, g: T, b: T) -> Rgb { + Rgb { r, g, b } + } +} + +impl Rgb +where + T: Copy, +{ + pub fn dup(v: T) -> Rgb { + Rgb { r: v, g: v, b: v } + } + + #[inline] + pub const fn to_vector(self) -> Vector3 { + Vector3 { + v: [self.r, self.g, self.b], + } + } +} + +impl Rgb { + #[inline(always)] + pub fn apply(&self, matrix: Matrix3f) -> Rgb { + let new_r = mlaf( + mlaf(self.r * matrix.v[0][0], self.g, matrix.v[0][1]), + self.b, + matrix.v[0][2], + ); + + let new_g = mlaf( + mlaf(self.r * matrix.v[1][0], self.g, matrix.v[1][1]), + self.b, + matrix.v[1][2], + ); + + let new_b = mlaf( + mlaf(self.r * matrix.v[2][0], self.g, matrix.v[2][1]), + self.b, + matrix.v[2][2], + ); + + Rgb { + r: new_r, + g: new_g, + b: new_b, + } + } + + #[inline(always)] + pub fn to_xyz(&self, matrix: Matrix3f) -> Xyz { + let new_self = self.apply(matrix); + Xyz { + x: new_self.r, + y: new_self.g, + z: new_self.b, + } + } + + #[inline(always)] + pub fn is_out_of_gamut(&self) -> bool { + !(0.0..=1.0).contains(&self.r) + || !(0.0..=1.0).contains(&self.g) + || !(0.0..=1.0).contains(&self.b) + } +} + +impl Index for Rgb { + type Output = T; + + fn index(&self, index: usize) -> &T { + match index { + 0 => &self.r, + 1 => &self.g, + 2 => &self.b, + _ => panic!("Index out of bounds for Rgb"), + } + } +} + +impl IndexMut for Rgb { + fn index_mut(&mut self, index: usize) -> &mut T { + match index { + 0 => &mut self.r, + 1 => &mut self.g, + 2 => &mut self.b, + _ => panic!("Index out of bounds for RGB"), + } + } +} + +macro_rules! generated_float_definition_rgb { + ($T: ty) => { + impl Rgb<$T> { + #[inline] + pub fn zeroed() -> Rgb<$T> { + Rgb::<$T>::new(0., 0., 0.) + } + + #[inline] + pub fn ones() -> Rgb<$T> { + Rgb::<$T>::new(1., 1., 1.) + } + + #[inline] + pub fn white() -> Rgb<$T> { + Rgb::<$T>::ones() + } + + #[inline] + pub fn black() -> Rgb<$T> { + Rgb::<$T>::zeroed() + } + } + }; +} + +generated_float_definition_rgb!(f32); +generated_float_definition_rgb!(f64); + +macro_rules! generated_integral_definition_rgb { + ($T: ty) => { + impl Rgb<$T> { + #[inline] + pub fn zeroed() -> Rgb<$T> { + Rgb::<$T>::new(0, 0, 0) + } + + #[inline] + pub fn capped() -> Rgb<$T> { + Rgb::<$T>::new(<$T>::MAX, <$T>::MAX, <$T>::MAX) + } + + #[inline] + pub fn white() -> Rgb<$T> { + Rgb::<$T>::capped() + } + + #[inline] + pub fn black() -> Rgb<$T> { + Rgb::<$T>::new(0, 0, 0) + } + } + }; +} + +generated_integral_definition_rgb!(u8); +generated_integral_definition_rgb!(u16); +generated_integral_definition_rgb!(i8); +generated_integral_definition_rgb!(i16); +generated_integral_definition_rgb!(i32); +generated_integral_definition_rgb!(u32); + +pub trait FusedPow { + fn f_pow(&self, power: T) -> Self; +} + +pub trait FusedLog2 { + fn f_log2(&self) -> Self; +} + +pub trait FusedLog10 { + fn f_log10(&self) -> Self; +} + +pub trait FusedLog { + fn f_log(&self) -> Self; +} + +pub trait FusedExp { + fn f_exp(&self) -> Self; +} + +pub trait FusedExp2 { + fn f_exp2(&self) -> Self; +} + +pub trait FusedExp10 { + fn f_exp10(&self) -> Self; +} + +impl FusedPow> for Rgb { + fn f_pow(&self, power: Rgb) -> Rgb { + Rgb::new( + f_powf(self.r, power.r), + f_powf(self.g, power.g), + f_powf(self.b, power.b), + ) + } +} + +impl FusedPow> for Rgb { + fn f_pow(&self, power: Rgb) -> Rgb { + Rgb::new( + f_pow(self.r, power.r), + f_pow(self.g, power.g), + f_pow(self.b, power.b), + ) + } +} + +impl FusedLog2> for Rgb { + #[inline] + fn f_log2(&self) -> Rgb { + Rgb::new(f_log2f(self.r), f_log2f(self.g), f_log2f(self.b)) + } +} + +impl FusedLog2> for Rgb { + #[inline] + fn f_log2(&self) -> Rgb { + Rgb::new(f_log2(self.r), f_log2(self.g), f_log2(self.b)) + } +} + +impl FusedLog> for Rgb { + #[inline] + fn f_log(&self) -> Rgb { + Rgb::new(f_logf(self.r), f_logf(self.g), f_logf(self.b)) + } +} + +impl FusedLog> for Rgb { + #[inline] + fn f_log(&self) -> Rgb { + Rgb::new(f_log(self.r), f_log(self.g), f_log(self.b)) + } +} + +impl FusedLog10> for Rgb { + #[inline] + fn f_log10(&self) -> Rgb { + Rgb::new(f_log10f(self.r), f_log10f(self.g), f_log10f(self.b)) + } +} + +impl FusedLog10> for Rgb { + #[inline] + fn f_log10(&self) -> Rgb { + Rgb::new(f_log10(self.r), f_log10(self.g), f_log10(self.b)) + } +} + +impl FusedExp> for Rgb { + #[inline] + fn f_exp(&self) -> Rgb { + Rgb::new(f_expf(self.r), f_expf(self.g), f_expf(self.b)) + } +} + +impl FusedExp> for Rgb { + #[inline] + fn f_exp(&self) -> Rgb { + Rgb::new(f_exp(self.r), f_exp(self.g), f_exp(self.b)) + } +} + +impl FusedExp2> for Rgb { + #[inline] + fn f_exp2(&self) -> Rgb { + Rgb::new(f_exp2f(self.r), f_exp2f(self.g), f_exp2f(self.b)) + } +} + +impl FusedExp2> for Rgb { + #[inline] + fn f_exp2(&self) -> Rgb { + Rgb::new(f_exp2(self.r), f_exp2(self.g), f_exp2(self.b)) + } +} + +impl FusedExp10> for Rgb { + #[inline] + fn f_exp10(&self) -> Rgb { + Rgb::new(f_exp10f(self.r), f_exp10f(self.g), f_exp10f(self.b)) + } +} + +impl FusedExp10> for Rgb { + #[inline] + fn f_exp10(&self) -> Rgb { + Rgb::new(f_exp10(self.r), f_exp10(self.g), f_exp10(self.b)) + } +} + +impl Rgb +where + T: Copy + AsPrimitive, +{ + pub fn euclidean_distance(&self, other: Rgb) -> f32 { + let dr = self.r.as_() - other.r.as_(); + let dg = self.g.as_() - other.g.as_(); + let db = self.b.as_() - other.b.as_(); + (dr * dr + dg * dg + db * db).sqrt() + } +} + +impl Rgb +where + T: Copy + AsPrimitive, +{ + pub fn taxicab_distance(&self, other: Self) -> f32 { + let dr = self.r.as_() - other.r.as_(); + let dg = self.g.as_() - other.g.as_(); + let db = self.b.as_() - other.b.as_(); + dr.abs() + dg.abs() + db.abs() + } +} + +impl Add for Rgb +where + T: Add, +{ + type Output = Rgb; + + #[inline] + fn add(self, rhs: Self) -> Self::Output { + Rgb::new(self.r + rhs.r, self.g + rhs.g, self.b + rhs.b) + } +} + +impl Sub for Rgb +where + T: Sub, +{ + type Output = Rgb; + + #[inline] + fn sub(self, rhs: Self) -> Self::Output { + Rgb::new(self.r - rhs.r, self.g - rhs.g, self.b - rhs.b) + } +} + +impl Sub for Rgb +where + T: Sub, +{ + type Output = Rgb; + + #[inline] + fn sub(self, rhs: T) -> Self::Output { + Rgb::new(self.r - rhs, self.g - rhs, self.b - rhs) + } +} + +impl Add for Rgb +where + T: Add, +{ + type Output = Rgb; + + #[inline] + fn add(self, rhs: T) -> Self::Output { + Rgb::new(self.r + rhs, self.g + rhs, self.b + rhs) + } +} + +impl Rgb +where + T: Signed, +{ + #[inline] + pub fn abs(self) -> Self { + Rgb::new(self.r.abs(), self.g.abs(), self.b.abs()) + } +} + +impl Div for Rgb +where + T: Div, +{ + type Output = Rgb; + + #[inline] + fn div(self, rhs: Self) -> Self::Output { + Rgb::new(self.r / rhs.r, self.g / rhs.g, self.b / rhs.b) + } +} + +impl Div for Rgb +where + T: Div, +{ + type Output = Rgb; + + #[inline] + fn div(self, rhs: T) -> Self::Output { + Rgb::new(self.r / rhs, self.g / rhs, self.b / rhs) + } +} + +impl Mul for Rgb +where + T: Mul, +{ + type Output = Rgb; + + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Rgb::new(self.r * rhs.r, self.g * rhs.g, self.b * rhs.b) + } +} + +impl Mul for Rgb +where + T: Mul, +{ + type Output = Rgb; + + #[inline] + fn mul(self, rhs: T) -> Self::Output { + Rgb::new(self.r * rhs, self.g * rhs, self.b * rhs) + } +} + +impl MulAssign for Rgb +where + T: MulAssign, +{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + self.r *= rhs.r; + self.g *= rhs.g; + self.b *= rhs.b; + } +} + +macro_rules! generated_mul_assign_definition_rgb { + ($T: ty) => { + impl MulAssign<$T> for Rgb + where + T: MulAssign<$T>, + { + #[inline] + fn mul_assign(&mut self, rhs: $T) { + self.r *= rhs; + self.g *= rhs; + self.b *= rhs; + } + } + }; +} + +generated_mul_assign_definition_rgb!(i8); +generated_mul_assign_definition_rgb!(u8); +generated_mul_assign_definition_rgb!(u16); +generated_mul_assign_definition_rgb!(i16); +generated_mul_assign_definition_rgb!(u32); +generated_mul_assign_definition_rgb!(i32); +generated_mul_assign_definition_rgb!(f32); +generated_mul_assign_definition_rgb!(f64); + +impl AddAssign for Rgb +where + T: AddAssign, +{ + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.r += rhs.r; + self.g += rhs.g; + self.b += rhs.b; + } +} + +macro_rules! generated_add_assign_definition_rgb { + ($T: ty) => { + impl AddAssign<$T> for Rgb + where + T: AddAssign<$T>, + { + #[inline] + fn add_assign(&mut self, rhs: $T) { + self.r += rhs; + self.g += rhs; + self.b += rhs; + } + } + }; +} + +generated_add_assign_definition_rgb!(i8); +generated_add_assign_definition_rgb!(u8); +generated_add_assign_definition_rgb!(u16); +generated_add_assign_definition_rgb!(i16); +generated_add_assign_definition_rgb!(u32); +generated_add_assign_definition_rgb!(i32); +generated_add_assign_definition_rgb!(f32); +generated_add_assign_definition_rgb!(f64); + +impl DivAssign for Rgb +where + T: DivAssign, +{ + #[inline] + fn div_assign(&mut self, rhs: Self) { + self.r /= rhs.r; + self.g /= rhs.g; + self.b /= rhs.b; + } +} + +macro_rules! generated_div_assign_definition_rgb { + ($T: ty) => { + impl DivAssign<$T> for Rgb + where + T: DivAssign<$T>, + { + #[inline] + fn div_assign(&mut self, rhs: $T) { + self.r /= rhs; + self.g /= rhs; + self.b /= rhs; + } + } + }; +} + +generated_div_assign_definition_rgb!(u8); +generated_div_assign_definition_rgb!(i8); +generated_div_assign_definition_rgb!(u16); +generated_div_assign_definition_rgb!(i16); +generated_div_assign_definition_rgb!(u32); +generated_div_assign_definition_rgb!(i32); +generated_div_assign_definition_rgb!(f32); +generated_div_assign_definition_rgb!(f64); + +impl Neg for Rgb +where + T: Neg, +{ + type Output = Rgb; + + #[inline] + fn neg(self) -> Self::Output { + Rgb::new(-self.r, -self.g, -self.b) + } +} + +impl Rgb +where + T: FusedMultiplyAdd, +{ + pub fn mla(&self, b: Rgb, c: Rgb) -> Rgb { + Rgb::new( + self.r.mla(b.r, c.r), + self.g.mla(b.g, c.g), + self.b.mla(b.b, c.b), + ) + } +} + +impl Rgb +where + T: Num + PartialOrd + Copy + Bounded, +{ + /// Clamp function to clamp each channel within a given range + #[inline] + #[allow(clippy::manual_clamp)] + pub fn clamp(&self, min_value: T, max_value: T) -> Rgb { + Rgb::new( + m_clamp(self.r, min_value, max_value), + m_clamp(self.g, min_value, max_value), + m_clamp(self.b, min_value, max_value), + ) + } + + /// Min function to define min + #[inline] + pub fn min(&self, other_min: T) -> Rgb { + Rgb::new( + m_min(self.r, other_min), + m_min(self.g, other_min), + m_min(self.b, other_min), + ) + } + + /// Max function to define max + #[inline] + pub fn max(&self, other_max: T) -> Rgb { + Rgb::new( + m_max(self.r, other_max), + m_max(self.g, other_max), + m_max(self.b, other_max), + ) + } + + /// Clamp function to clamp each channel within a given range + #[inline] + #[allow(clippy::manual_clamp)] + pub fn clamp_p(&self, min_value: Rgb, max_value: Rgb) -> Rgb { + Rgb::new( + m_clamp(self.r, max_value.r, min_value.r), + m_clamp(self.g, max_value.g, min_value.g), + m_clamp(self.b, max_value.b, min_value.b), + ) + } + + /// Min function to define min + #[inline] + pub fn min_p(&self, other_min: Rgb) -> Rgb { + Rgb::new( + m_min(self.r, other_min.r), + m_min(self.g, other_min.g), + m_min(self.b, other_min.b), + ) + } + + /// Max function to define max + #[inline] + pub fn max_p(&self, other_max: Rgb) -> Rgb { + Rgb::new( + m_max(self.r, other_max.r), + m_max(self.g, other_max.g), + m_max(self.b, other_max.b), + ) + } +} + +impl Rgb +where + T: Float + 'static, + f32: AsPrimitive, +{ + #[inline] + pub fn sqrt(&self) -> Rgb { + let zeros = 0f32.as_(); + Rgb::new( + if self.r.partial_cmp(&zeros).unwrap_or(Ordering::Less) == Ordering::Less { + 0f32.as_() + } else { + self.r.sqrt() + }, + if self.g.partial_cmp(&zeros).unwrap_or(Ordering::Less) == Ordering::Less { + 0f32.as_() + } else { + self.g.sqrt() + }, + if self.b.partial_cmp(&zeros).unwrap_or(Ordering::Less) == Ordering::Less { + 0f32.as_() + } else { + self.b.sqrt() + }, + ) + } + + #[inline] + pub fn cbrt(&self) -> Rgb { + Rgb::new(self.r.cbrt(), self.g.cbrt(), self.b.cbrt()) + } +} + +impl Pow for Rgb +where + T: Float, +{ + type Output = Rgb; + + #[inline] + fn pow(self, rhs: T) -> Self::Output { + Rgb::::new(self.r.powf(rhs), self.g.powf(rhs), self.b.powf(rhs)) + } +} + +impl Pow> for Rgb +where + T: Float, +{ + type Output = Rgb; + + #[inline] + fn pow(self, rhs: Rgb) -> Self::Output { + Rgb::::new(self.r.powf(rhs.r), self.g.powf(rhs.g), self.b.powf(rhs.b)) + } +} + +impl Rgb { + pub fn cast(self) -> Rgb + where + T: AsPrimitive, + V: Copy + 'static, + { + Rgb::new(self.r.as_(), self.g.as_(), self.b.as_()) + } +} + +impl Rgb +where + T: Float + 'static, +{ + pub fn round(self) -> Rgb { + Rgb::new(self.r.round(), self.g.round(), self.b.round()) + } +} diff --git a/deps/moxcms/src/safe_math.rs b/deps/moxcms/src/safe_math.rs new file mode 100644 index 0000000..7380f8a --- /dev/null +++ b/deps/moxcms/src/safe_math.rs @@ -0,0 +1,103 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::CmsError; +use std::ops::Add; + +pub(crate) trait SafeAdd> { + fn safe_add(&self, other: T) -> Result; +} + +pub(crate) trait SafeMul> { + fn safe_mul(&self, other: T) -> Result; +} + +pub(crate) trait SafePowi> { + fn safe_powi(&self, power: u32) -> Result; +} + +macro_rules! safe_add_impl { + ($type_name: ident) => { + impl SafeAdd<$type_name> for $type_name { + #[inline(always)] + fn safe_add(&self, other: $type_name) -> Result<$type_name, CmsError> { + if let Some(result) = self.checked_add(other) { + return Ok(result); + } + Err(CmsError::OverflowingError) + } + } + }; +} + +safe_add_impl!(u16); +safe_add_impl!(u32); +safe_add_impl!(i32); +safe_add_impl!(usize); +safe_add_impl!(isize); + +macro_rules! safe_mul_impl { + ($type_name: ident) => { + impl SafeMul<$type_name> for $type_name { + #[inline(always)] + fn safe_mul(&self, other: $type_name) -> Result<$type_name, CmsError> { + if let Some(result) = self.checked_mul(other) { + return Ok(result); + } + Err(CmsError::OverflowingError) + } + } + }; +} + +safe_mul_impl!(u16); +safe_mul_impl!(u32); +safe_mul_impl!(i32); +safe_mul_impl!(usize); +safe_mul_impl!(isize); + +macro_rules! safe_powi_impl { + ($type_name: ident) => { + impl SafePowi<$type_name> for $type_name { + #[inline(always)] + fn safe_powi(&self, power: u32) -> Result<$type_name, CmsError> { + if let Some(result) = self.checked_pow(power) { + return Ok(result); + } + Err(CmsError::OverflowingError) + } + } + }; +} + +safe_powi_impl!(u8); +safe_powi_impl!(u16); +safe_powi_impl!(u32); +safe_powi_impl!(i32); +safe_powi_impl!(usize); +safe_powi_impl!(isize); diff --git a/deps/moxcms/src/srlab2.rs b/deps/moxcms/src/srlab2.rs new file mode 100644 index 0000000..c8afd8b --- /dev/null +++ b/deps/moxcms/src/srlab2.rs @@ -0,0 +1,102 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 6/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::Xyz; +use crate::mlaf::mlaf; +use pxfm::f_cbrtf; + +#[inline] +fn srlab2_gamma(x: f32) -> f32 { + if x <= 216. / 24389. { + x * (24389. / 2700.) + } else { + 1.16 * f_cbrtf(x) - 0.16 + } +} + +#[inline] +fn srlab2_linearize(x: f32) -> f32 { + if x <= 0.08 { + x * (2700.0 / 24389.0) + } else { + let zx = (x + 0.16) / 1.16; + zx * zx * zx + } +} + +#[derive(Copy, Clone, Debug, Default, PartialOrd, PartialEq)] +pub struct Srlab2 { + pub l: f32, + pub a: f32, + pub b: f32, +} + +impl Srlab2 { + #[inline] + pub const fn new(l: f32, a: f32, b: f32) -> Srlab2 { + Srlab2 { l, a, b } + } + + #[inline] + pub fn from_xyz(xyz: Xyz) -> Srlab2 { + let lx = srlab2_gamma(xyz.x); + let ly = srlab2_gamma(xyz.y); + let lz = srlab2_gamma(xyz.z); + + let l = mlaf(mlaf(0.629054 * ly, -0.000008, lz), 0.37095, lx); + let a = mlaf(mlaf(6.634684 * lx, -7.505078, ly), 0.870328, lz); + let b = mlaf(mlaf(0.639569 * lx, 1.084576, ly), -1.724152, lz); + Srlab2 { l, a, b } + } + + #[inline] + pub fn to_xyz(&self) -> Xyz { + let x = mlaf(mlaf(self.l, 0.09041272, self.a), 0.045634452, self.b); + let y = mlaf(mlaf(self.l, -0.05331593, self.a), -0.026917785, self.b); + let z = mlaf(self.l, -0.58, self.b); + let lx = srlab2_linearize(x); + let ly = srlab2_linearize(y); + let lz = srlab2_linearize(z); + Xyz::new(lx, ly, lz) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_srlab2() { + let xyz = Xyz::new(0.3, 0.65, 0.66); + let srlab2 = Srlab2::from_xyz(xyz); + let r_xyz = srlab2.to_xyz(); + assert!((r_xyz.x - xyz.x).abs() < 1e-5); + assert!((r_xyz.y - xyz.y).abs() < 1e-5); + assert!((r_xyz.z - xyz.z).abs() < 1e-5); + } +} diff --git a/deps/moxcms/src/tag.rs b/deps/moxcms/src/tag.rs new file mode 100644 index 0000000..cbf0f18 --- /dev/null +++ b/deps/moxcms/src/tag.rs @@ -0,0 +1,271 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::CmsError; + +pub(crate) const TAG_SIZE: usize = 12; + +#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)] +pub(crate) enum Tag { + RedXyz, + GreenXyz, + BlueXyz, + RedToneReproduction, + GreenToneReproduction, + BlueToneReproduction, + GreyToneReproduction, + MediaWhitePoint, + CodeIndependentPoints, + ChromaticAdaptation, + BlackPoint, + DeviceToPcsLutPerceptual, + DeviceToPcsLutColorimetric, + DeviceToPcsLutSaturation, + PcsToDeviceLutPerceptual, + PcsToDeviceLutColorimetric, + PcsToDeviceLutSaturation, + ProfileDescription, + Copyright, + ViewingConditionsDescription, + DeviceManufacturer, + DeviceModel, + Gamut, + Luminance, + Measurement, + Chromaticity, + ObserverConditions, + CharTarget, + Technology, + CalibrationDateTime, +} + +impl TryFrom for Tag { + type Error = CmsError; + + fn try_from(value: u32) -> Result { + if value == u32::from_ne_bytes(*b"rXYZ").to_be() { + return Ok(Self::RedXyz); + } else if value == u32::from_ne_bytes(*b"gXYZ").to_be() { + return Ok(Self::GreenXyz); + } else if value == u32::from_ne_bytes(*b"bXYZ").to_be() { + return Ok(Self::BlueXyz); + } else if value == u32::from_ne_bytes(*b"rTRC").to_be() { + return Ok(Self::RedToneReproduction); + } else if value == u32::from_ne_bytes(*b"gTRC").to_be() { + return Ok(Self::GreenToneReproduction); + } else if value == u32::from_ne_bytes(*b"bTRC").to_be() { + return Ok(Self::BlueToneReproduction); + } else if value == u32::from_ne_bytes(*b"kTRC").to_be() { + return Ok(Self::GreyToneReproduction); + } else if value == u32::from_ne_bytes(*b"wtpt").to_be() { + return Ok(Self::MediaWhitePoint); + } else if value == u32::from_ne_bytes(*b"cicp").to_be() { + return Ok(Self::CodeIndependentPoints); + } else if value == u32::from_ne_bytes(*b"chad").to_be() { + return Ok(Self::ChromaticAdaptation); + } else if value == u32::from_ne_bytes(*b"bkpt").to_be() { + return Ok(Self::BlackPoint); + } else if value == u32::from_ne_bytes(*b"A2B0").to_be() { + return Ok(Self::DeviceToPcsLutPerceptual); + } else if value == u32::from_ne_bytes(*b"A2B1").to_be() { + return Ok(Self::DeviceToPcsLutColorimetric); + } else if value == u32::from_ne_bytes(*b"A2B2").to_be() { + return Ok(Self::DeviceToPcsLutSaturation); + } else if value == u32::from_ne_bytes(*b"B2A0").to_be() { + return Ok(Self::PcsToDeviceLutPerceptual); + } else if value == u32::from_ne_bytes(*b"B2A1").to_be() { + return Ok(Self::PcsToDeviceLutColorimetric); + } else if value == u32::from_ne_bytes(*b"B2A2").to_be() { + return Ok(Self::PcsToDeviceLutSaturation); + } else if value == u32::from_ne_bytes(*b"desc").to_be() { + return Ok(Self::ProfileDescription); + } else if value == u32::from_ne_bytes(*b"cprt").to_be() { + return Ok(Self::Copyright); + } else if value == u32::from_ne_bytes(*b"vued").to_be() { + return Ok(Self::ViewingConditionsDescription); + } else if value == u32::from_ne_bytes(*b"dmnd").to_be() { + return Ok(Self::DeviceManufacturer); + } else if value == u32::from_ne_bytes(*b"dmdd").to_be() { + return Ok(Self::DeviceModel); + } else if value == u32::from_ne_bytes(*b"gamt").to_be() { + return Ok(Self::Gamut); + } else if value == u32::from_ne_bytes(*b"lumi").to_be() { + return Ok(Self::Luminance); + } else if value == u32::from_ne_bytes(*b"meas").to_be() { + return Ok(Self::Measurement); + } else if value == u32::from_ne_bytes(*b"chrm").to_be() { + return Ok(Self::Chromaticity); + } else if value == u32::from_ne_bytes(*b"view").to_be() { + return Ok(Self::ObserverConditions); + } else if value == u32::from_ne_bytes(*b"targ").to_be() { + return Ok(Self::CharTarget); + } else if value == u32::from_ne_bytes(*b"tech").to_be() { + return Ok(Self::Technology); + } else if value == u32::from_ne_bytes(*b"calt").to_be() { + return Ok(Self::CalibrationDateTime); + } + Err(CmsError::UnknownTag(value)) + } +} + +impl From for u32 { + fn from(value: Tag) -> Self { + match value { + Tag::RedXyz => u32::from_ne_bytes(*b"rXYZ").to_be(), + Tag::GreenXyz => u32::from_ne_bytes(*b"gXYZ").to_be(), + Tag::BlueXyz => u32::from_ne_bytes(*b"bXYZ").to_be(), + Tag::RedToneReproduction => u32::from_ne_bytes(*b"rTRC").to_be(), + Tag::GreenToneReproduction => u32::from_ne_bytes(*b"gTRC").to_be(), + Tag::BlueToneReproduction => u32::from_ne_bytes(*b"bTRC").to_be(), + Tag::GreyToneReproduction => u32::from_ne_bytes(*b"kTRC").to_be(), + Tag::MediaWhitePoint => u32::from_ne_bytes(*b"wtpt").to_be(), + Tag::CodeIndependentPoints => u32::from_ne_bytes(*b"cicp").to_be(), + Tag::ChromaticAdaptation => u32::from_ne_bytes(*b"chad").to_be(), + Tag::BlackPoint => u32::from_ne_bytes(*b"bkpt").to_be(), + Tag::DeviceToPcsLutPerceptual => u32::from_ne_bytes(*b"A2B0").to_be(), + Tag::DeviceToPcsLutColorimetric => u32::from_ne_bytes(*b"A2B1").to_be(), + Tag::DeviceToPcsLutSaturation => u32::from_ne_bytes(*b"A2B2").to_be(), + Tag::PcsToDeviceLutPerceptual => u32::from_ne_bytes(*b"B2A0").to_be(), + Tag::PcsToDeviceLutColorimetric => u32::from_ne_bytes(*b"B2A1").to_be(), + Tag::PcsToDeviceLutSaturation => u32::from_ne_bytes(*b"B2A2").to_be(), + Tag::ProfileDescription => u32::from_ne_bytes(*b"desc").to_be(), + Tag::Copyright => u32::from_ne_bytes(*b"cprt").to_be(), + Tag::ViewingConditionsDescription => u32::from_ne_bytes(*b"vued").to_be(), + Tag::DeviceManufacturer => u32::from_ne_bytes(*b"dmnd").to_be(), + Tag::DeviceModel => u32::from_ne_bytes(*b"dmdd").to_be(), + Tag::Gamut => u32::from_ne_bytes(*b"gamt").to_be(), + Tag::Luminance => u32::from_ne_bytes(*b"lumi").to_be(), + Tag::Measurement => u32::from_ne_bytes(*b"meas").to_be(), + Tag::Chromaticity => u32::from_ne_bytes(*b"chrm").to_be(), + Tag::ObserverConditions => u32::from_ne_bytes(*b"view").to_be(), + Tag::CharTarget => u32::from_ne_bytes(*b"targ").to_be(), + Tag::Technology => u32::from_ne_bytes(*b"tech").to_be(), + Tag::CalibrationDateTime => u32::from_ne_bytes(*b"calt").to_be(), + } + } +} + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub(crate) enum TagTypeDefinition { + Text, + MultiLocalizedUnicode, + Description, + MabLut, + MbaLut, + ParametricToneCurve, + LutToneCurve, + Xyz, + MultiProcessElement, + DefViewingConditions, + Signature, + Cicp, + DateTime, + S15Fixed16Array, + U8Array, + U16Fixed16Array, + U16Array, + U32Array, + U64Array, + Measurement, + NotAllowed, +} + +impl From for TagTypeDefinition { + fn from(value: u32) -> Self { + if value == u32::from_ne_bytes(*b"mluc").to_be() { + return TagTypeDefinition::MultiLocalizedUnicode; + } else if value == u32::from_ne_bytes(*b"desc").to_be() { + return TagTypeDefinition::Description; + } else if value == u32::from_ne_bytes(*b"text").to_be() { + return TagTypeDefinition::Text; + } else if value == u32::from_ne_bytes(*b"mAB ").to_be() { + return TagTypeDefinition::MabLut; + } else if value == u32::from_ne_bytes(*b"mBA ").to_be() { + return TagTypeDefinition::MbaLut; + } else if value == u32::from_ne_bytes(*b"para").to_be() { + return TagTypeDefinition::ParametricToneCurve; + } else if value == u32::from_ne_bytes(*b"curv").to_be() { + return TagTypeDefinition::LutToneCurve; + } else if value == u32::from_ne_bytes(*b"XYZ ").to_be() { + return TagTypeDefinition::Xyz; + } else if value == u32::from_ne_bytes(*b"mpet").to_be() { + return TagTypeDefinition::MultiProcessElement; + } else if value == u32::from_ne_bytes(*b"view").to_be() { + return TagTypeDefinition::DefViewingConditions; + } else if value == u32::from_ne_bytes(*b"sig ").to_be() { + return TagTypeDefinition::Signature; + } else if value == u32::from_ne_bytes(*b"cicp").to_be() { + return TagTypeDefinition::Cicp; + } else if value == u32::from_ne_bytes(*b"dtim").to_be() { + return TagTypeDefinition::DateTime; + } else if value == u32::from_ne_bytes(*b"meas").to_be() { + return TagTypeDefinition::Measurement; + } else if value == u32::from_ne_bytes(*b"sf32").to_be() { + return TagTypeDefinition::S15Fixed16Array; + } else if value == u32::from_ne_bytes(*b"uf32").to_be() { + return TagTypeDefinition::U16Fixed16Array; + } else if value == u32::from_ne_bytes(*b"ui16").to_be() { + return TagTypeDefinition::U16Array; + } else if value == u32::from_ne_bytes(*b"ui32").to_be() { + return TagTypeDefinition::U32Array; + } else if value == u32::from_ne_bytes(*b"ui64").to_be() { + return TagTypeDefinition::U64Array; + } else if value == u32::from_ne_bytes(*b"ui08").to_be() { + return TagTypeDefinition::U8Array; + } + TagTypeDefinition::NotAllowed + } +} + +impl From for u32 { + fn from(value: TagTypeDefinition) -> Self { + match value { + TagTypeDefinition::MultiLocalizedUnicode => u32::from_ne_bytes(*b"mluc").to_be(), + TagTypeDefinition::Description => u32::from_ne_bytes(*b"desc").to_be(), + TagTypeDefinition::Text => u32::from_ne_bytes(*b"text").to_be(), + TagTypeDefinition::MabLut => u32::from_ne_bytes(*b"mAB ").to_be(), + TagTypeDefinition::MbaLut => u32::from_ne_bytes(*b"mBA ").to_be(), + TagTypeDefinition::ParametricToneCurve => u32::from_ne_bytes(*b"para").to_be(), + TagTypeDefinition::LutToneCurve => u32::from_ne_bytes(*b"curv").to_be(), + TagTypeDefinition::Xyz => u32::from_ne_bytes(*b"XYZ ").to_be(), + TagTypeDefinition::MultiProcessElement => u32::from_ne_bytes(*b"mpet").to_be(), + TagTypeDefinition::DefViewingConditions => u32::from_ne_bytes(*b"view").to_be(), + TagTypeDefinition::Signature => u32::from_ne_bytes(*b"sig ").to_be(), + TagTypeDefinition::Cicp => u32::from_ne_bytes(*b"cicp").to_be(), + TagTypeDefinition::DateTime => u32::from_ne_bytes(*b"dtim").to_be(), + TagTypeDefinition::S15Fixed16Array => u32::from_ne_bytes(*b"sf32").to_be(), + TagTypeDefinition::U16Fixed16Array => u32::from_ne_bytes(*b"uf32").to_be(), + TagTypeDefinition::U8Array => u32::from_ne_bytes(*b"ui08").to_be(), + TagTypeDefinition::U16Array => u32::from_ne_bytes(*b"ui16").to_be(), + TagTypeDefinition::U32Array => u32::from_ne_bytes(*b"ui32").to_be(), + TagTypeDefinition::U64Array => u32::from_ne_bytes(*b"ui64").to_be(), + TagTypeDefinition::Measurement => u32::from_ne_bytes(*b"meas").to_be(), + TagTypeDefinition::NotAllowed => 0, + } + } +} diff --git a/deps/moxcms/src/transform.rs b/deps/moxcms/src/transform.rs new file mode 100644 index 0000000..cb6987e --- /dev/null +++ b/deps/moxcms/src/transform.rs @@ -0,0 +1,1334 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::conversions::{ + LutBarycentricReduction, RgbXyzFactory, RgbXyzFactoryOpt, ToneReproductionRgbToGray, + TransformMatrixShaper, make_gray_to_unfused, make_gray_to_x, make_lut_transform, + make_rgb_to_gray, +}; +use crate::err::CmsError; +use crate::trc::GammaLutInterpolate; +use crate::{ColorProfile, DataColorSpace, LutWarehouse, RenderingIntent, Vector3f, Xyzd}; +use num_traits::AsPrimitive; +use std::marker::PhantomData; + +/// Transformation executor itself +pub trait TransformExecutor { + /// Count of samples always must match. + /// If there is N samples of *Cmyk* source then N samples of *Rgb* is expected as an output. + fn transform(&self, src: &[V], dst: &mut [V]) -> Result<(), CmsError>; +} + +/// Helper for intermediate transformation stages +pub trait Stage { + fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError>; +} + +/// Helper for intermediate transformation stages +pub trait InPlaceStage { + fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError>; +} + +/// Barycentric interpolation weights size. +/// +/// Bigger weights increases precision. +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)] +pub enum BarycentricWeightScale { + #[default] + /// Low scale weights is enough for common case. + /// + /// However, it might crush dark zones and gradients. + /// Weights increasing costs 5% performance. + Low, + #[cfg(feature = "options")] + High, +} + +/// Declares additional transformation options +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct TransformOptions { + pub rendering_intent: RenderingIntent, + /// If set it will try to use Transfer Characteristics from CICP + /// on transform. This might be more precise and faster. + pub allow_use_cicp_transfer: bool, + /// Prefers fixed point where implemented as default. + /// Most of the applications actually do not need floating point. + /// + /// Do not change it if you're not sure that extreme precision is required, + /// in most cases it is a simple way to spend energy to warming up environment + /// a little. + /// + /// Q2.13 for RGB->XYZ->RGB is used. + /// LUT interpolation use Q0.15. + pub prefer_fixed_point: bool, + /// Interpolation method for 3D LUT + /// + /// This parameter has no effect on LAB/XYZ interpolation and scene linear RGB. + /// + /// Technically, it should be assumed to perform cube dividing interpolation: + /// - Source colorspace is gamma-encoded (discards scene linear RGB and XYZ). + /// - Colorspace is uniform. + /// - Colorspace has linear scaling (discards LAB). + /// - Interpolation doesn't shift hues (discards LAB). + /// + /// For LAB, XYZ and scene linear RGB `trilinear/quadlinear` always in force. + pub interpolation_method: InterpolationMethod, + /// Barycentric weights scale. + /// + /// This value controls LUT weights precision. + pub barycentric_weight_scale: BarycentricWeightScale, + /// For floating points transform, it will try to detect gamma function on *Matrix Shaper* profiles. + /// If gamma function is found, then it will be used instead of LUT table. + /// This allows to work with excellent precision with extended range, + /// at a cost of execution time. + pub allow_extended_range_rgb_xyz: bool, + // pub black_point_compensation: bool, +} + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)] +/// Defines the interpolation method. +/// +/// All methods produce very close results that almost not possible to separate without +/// some automation tools. +/// +/// This implementation chooses the fastest method as default. +pub enum InterpolationMethod { + /// General Tetrahedron interpolation. + /// This is used in lcms2 and others CMS. + #[cfg(feature = "options")] + Tetrahedral, + /// Divides cube into a pyramids and interpolate then in the pyramid. + #[cfg(feature = "options")] + Pyramid, + /// Interpolation by dividing cube into prisms. + #[cfg(feature = "options")] + Prism, + /// Trilinear/Quadlinear interpolation + #[default] + Linear, +} + +impl Default for TransformOptions { + fn default() -> Self { + Self { + rendering_intent: RenderingIntent::default(), + allow_use_cicp_transfer: true, + prefer_fixed_point: true, + interpolation_method: InterpolationMethod::default(), + barycentric_weight_scale: BarycentricWeightScale::default(), + allow_extended_range_rgb_xyz: false, + // black_point_compensation: false, + } + } +} + +pub type Transform8BitExecutor = dyn TransformExecutor + Send + Sync; +pub type Transform16BitExecutor = dyn TransformExecutor + Send + Sync; +pub type TransformF32BitExecutor = dyn TransformExecutor + Send + Sync; +pub type TransformF64BitExecutor = dyn TransformExecutor + Send + Sync; + +/// Layout declares a data layout. +/// For RGB it shows also the channel order. +/// To handle different data bit-depth appropriate executor must be used. +/// Cmyk8 uses the same layout as Rgba8. +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum Layout { + Rgb = 0, + Rgba = 1, + Gray = 2, + GrayAlpha = 3, + Inks5 = 4, + Inks6 = 5, + Inks7 = 6, + Inks8 = 7, + Inks9 = 8, + Inks10 = 9, + Inks11 = 10, + Inks12 = 11, + Inks13 = 12, + Inks14 = 13, + Inks15 = 14, +} + +impl Layout { + /// Returns Red channel index + #[inline(always)] + pub const fn r_i(self) -> usize { + match self { + Layout::Rgb => 0, + Layout::Rgba => 0, + Layout::Gray => unimplemented!(), + Layout::GrayAlpha => unimplemented!(), + _ => unimplemented!(), + } + } + + /// Returns Green channel index + #[inline(always)] + pub const fn g_i(self) -> usize { + match self { + Layout::Rgb => 1, + Layout::Rgba => 1, + Layout::Gray => unimplemented!(), + Layout::GrayAlpha => unimplemented!(), + _ => unimplemented!(), + } + } + + /// Returns Blue channel index + #[inline(always)] + pub const fn b_i(self) -> usize { + match self { + Layout::Rgb => 2, + Layout::Rgba => 2, + Layout::Gray => unimplemented!(), + Layout::GrayAlpha => unimplemented!(), + _ => unimplemented!(), + } + } + + #[inline(always)] + pub const fn a_i(self) -> usize { + match self { + Layout::Rgb => unimplemented!(), + Layout::Rgba => 3, + Layout::Gray => unimplemented!(), + Layout::GrayAlpha => 1, + _ => unimplemented!(), + } + } + + #[inline(always)] + pub const fn has_alpha(self) -> bool { + match self { + Layout::Rgb => false, + Layout::Rgba => true, + Layout::Gray => false, + Layout::GrayAlpha => true, + _ => false, + } + } + + #[inline] + pub const fn channels(self) -> usize { + match self { + Layout::Rgb => 3, + Layout::Rgba => 4, + Layout::Gray => 1, + Layout::GrayAlpha => 2, + Layout::Inks5 => 5, + Layout::Inks6 => 6, + Layout::Inks7 => 7, + Layout::Inks8 => 8, + Layout::Inks9 => 9, + Layout::Inks10 => 10, + Layout::Inks11 => 11, + Layout::Inks12 => 12, + Layout::Inks13 => 13, + Layout::Inks14 => 14, + Layout::Inks15 => 15, + } + } + + pub(crate) fn from_inks(inks: usize) -> Self { + match inks { + 1 => Layout::Gray, + 2 => Layout::GrayAlpha, + 3 => Layout::Rgb, + 4 => Layout::Rgba, + 5 => Layout::Inks5, + 6 => Layout::Inks6, + 7 => Layout::Inks7, + 8 => Layout::Inks8, + 9 => Layout::Inks9, + 10 => Layout::Inks10, + 11 => Layout::Inks11, + 12 => Layout::Inks12, + 13 => Layout::Inks13, + 14 => Layout::Inks14, + 15 => Layout::Inks15, + _ => unreachable!("Impossible amount of inks"), + } + } +} + +impl From for Layout { + fn from(value: u8) -> Self { + match value { + 0 => Layout::Rgb, + 1 => Layout::Rgba, + 2 => Layout::Gray, + 3 => Layout::GrayAlpha, + _ => unimplemented!(), + } + } +} + +impl Layout { + #[inline(always)] + pub const fn resolve(value: u8) -> Self { + match value { + 0 => Layout::Rgb, + 1 => Layout::Rgba, + 2 => Layout::Gray, + 3 => Layout::GrayAlpha, + 4 => Layout::Inks5, + 5 => Layout::Inks6, + 6 => Layout::Inks7, + 7 => Layout::Inks8, + 8 => Layout::Inks9, + 9 => Layout::Inks10, + 10 => Layout::Inks11, + 11 => Layout::Inks12, + 12 => Layout::Inks13, + 13 => Layout::Inks14, + 14 => Layout::Inks15, + _ => unimplemented!(), + } + } +} + +#[doc(hidden)] +pub trait PointeeSizeExpressible { + fn _as_usize(self) -> usize; + const FINITE: bool; + const NOT_FINITE_GAMMA_TABLE_SIZE: usize; + const NOT_FINITE_LINEAR_TABLE_SIZE: usize; + const IS_U8: bool; + const IS_U16: bool; +} + +impl PointeeSizeExpressible for u8 { + #[inline(always)] + fn _as_usize(self) -> usize { + self as usize + } + + const FINITE: bool = true; + const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 1; + const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1; + const IS_U8: bool = true; + const IS_U16: bool = false; +} + +impl PointeeSizeExpressible for u16 { + #[inline(always)] + fn _as_usize(self) -> usize { + self as usize + } + + const FINITE: bool = true; + + const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 1; + const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1; + + const IS_U8: bool = false; + const IS_U16: bool = true; +} + +impl PointeeSizeExpressible for f32 { + #[inline(always)] + fn _as_usize(self) -> usize { + const MAX_14_BIT: f32 = ((1 << 14u32) - 1) as f32; + ((self * MAX_14_BIT).max(0f32).min(MAX_14_BIT) as u16) as usize + } + + const FINITE: bool = false; + + const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 32768; + const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1 << 14u32; + const IS_U8: bool = false; + const IS_U16: bool = false; +} + +impl PointeeSizeExpressible for f64 { + #[inline(always)] + fn _as_usize(self) -> usize { + const MAX_16_BIT: f64 = ((1 << 16u32) - 1) as f64; + ((self * MAX_16_BIT).max(0.).min(MAX_16_BIT) as u16) as usize + } + + const FINITE: bool = false; + + const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 65536; + const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1 << 16; + const IS_U8: bool = false; + const IS_U16: bool = false; +} + +impl ColorProfile { + /// Checks if profile is valid *Matrix Shaper* profile + pub fn is_matrix_shaper(&self) -> bool { + self.color_space == DataColorSpace::Rgb + && self.red_colorant != Xyzd::default() + && self.green_colorant != Xyzd::default() + && self.blue_colorant != Xyzd::default() + && self.red_trc.is_some() + && self.green_trc.is_some() + && self.blue_trc.is_some() + } + + /// Creates transform between source and destination profile + /// Use for 16 bit-depth data bit-depth only. + pub fn create_transform_16bit( + &self, + src_layout: Layout, + dst_pr: &ColorProfile, + dst_layout: Layout, + options: TransformOptions, + ) -> Result, CmsError> { + self.create_transform_nbit::(src_layout, dst_pr, dst_layout, options) + } + + /// Creates transform between source and destination profile + /// Use for 12 bit-depth data bit-depth only. + pub fn create_transform_12bit( + &self, + src_layout: Layout, + dst_pr: &ColorProfile, + dst_layout: Layout, + options: TransformOptions, + ) -> Result, CmsError> { + self.create_transform_nbit::(src_layout, dst_pr, dst_layout, options) + } + + /// Creates transform between source and destination profile + /// Use for 10 bit-depth data bit-depth only. + pub fn create_transform_10bit( + &self, + src_layout: Layout, + dst_pr: &ColorProfile, + dst_layout: Layout, + options: TransformOptions, + ) -> Result, CmsError> { + self.create_transform_nbit::(src_layout, dst_pr, dst_layout, options) + } + + /// Creates transform between source and destination profile + /// Data has to be normalized into [0, 1] range. + /// ICC profiles and LUT tables do not exist in infinite precision. + /// Thus, this implementation considers `f32` as 14-bit values. + /// Floating point transformer works in extended mode, that means returned data might be negative + /// or more than 1. + pub fn create_transform_f32( + &self, + src_layout: Layout, + dst_pr: &ColorProfile, + dst_layout: Layout, + options: TransformOptions, + ) -> Result, CmsError> { + self.create_transform_nbit::(src_layout, dst_pr, dst_layout, options) + } + + /// Creates transform between source and destination profile + /// Data has to be normalized into [0, 1] range. + /// ICC profiles and LUT tables do not exist in infinite precision. + /// Thus, this implementation considers `f64` as 16-bit values. + /// Floating point transformer works in extended mode, that means returned data might be negative + /// or more than 1. + pub fn create_transform_f64( + &self, + src_layout: Layout, + dst_pr: &ColorProfile, + dst_layout: Layout, + options: TransformOptions, + ) -> Result, CmsError> { + self.create_transform_nbit::(src_layout, dst_pr, dst_layout, options) + } + + fn create_transform_nbit< + T: Copy + + Default + + AsPrimitive + + PointeeSizeExpressible + + Send + + Sync + + AsPrimitive + + RgbXyzFactory + + RgbXyzFactoryOpt + + GammaLutInterpolate, + const BIT_DEPTH: usize, + const LINEAR_CAP: usize, + const GAMMA_CAP: usize, + >( + &self, + src_layout: Layout, + dst_pr: &ColorProfile, + dst_layout: Layout, + options: TransformOptions, + ) -> Result + Send + Sync>, CmsError> + where + f32: AsPrimitive, + u32: AsPrimitive, + (): LutBarycentricReduction, + (): LutBarycentricReduction, + { + if self.color_space == DataColorSpace::Rgb + && dst_pr.pcs == DataColorSpace::Xyz + && dst_pr.color_space == DataColorSpace::Rgb + && self.pcs == DataColorSpace::Xyz + && self.is_matrix_shaper() + && dst_pr.is_matrix_shaper() + { + if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha { + return Err(CmsError::InvalidLayout); + } + if dst_layout == Layout::Gray || dst_layout == Layout::GrayAlpha { + return Err(CmsError::InvalidLayout); + } + + if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() { + return make_lut_transform::( + src_layout, self, dst_layout, dst_pr, options, + ); + } + + let transform = self.transform_matrix(dst_pr); + + if !T::FINITE && options.allow_extended_range_rgb_xyz { + if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() { + if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() { + use crate::conversions::{ + TransformShaperFloatInOut, make_rgb_xyz_rgb_transform_float_in_out, + }; + let p = TransformShaperFloatInOut { + linear_evaluator, + gamma_evaluator, + adaptation_matrix: transform.to_f32(), + phantom_data: PhantomData, + }; + return make_rgb_xyz_rgb_transform_float_in_out::( + src_layout, dst_layout, p, BIT_DEPTH, + ); + } + + let lin_r = self.build_r_linearize_table::( + options.allow_use_cicp_transfer, + )?; + let lin_g = self.build_g_linearize_table::( + options.allow_use_cicp_transfer, + )?; + let lin_b = self.build_b_linearize_table::( + options.allow_use_cicp_transfer, + )?; + + use crate::conversions::{ + TransformShaperRgbFloat, make_rgb_xyz_rgb_transform_float, + }; + let p = TransformShaperRgbFloat { + r_linear: lin_r, + g_linear: lin_g, + b_linear: lin_b, + gamma_evaluator, + adaptation_matrix: transform.to_f32(), + phantom_data: PhantomData, + }; + return make_rgb_xyz_rgb_transform_float::( + src_layout, dst_layout, p, BIT_DEPTH, + ); + } + } + + if self.are_all_trc_the_same() && dst_pr.are_all_trc_the_same() { + let linear = self.build_r_linearize_table::( + options.allow_use_cicp_transfer, + )?; + + let gamma = dst_pr.build_gamma_table::( + &dst_pr.red_trc, + options.allow_use_cicp_transfer, + )?; + + let profile_transform = crate::conversions::TransformMatrixShaperOptimized { + linear, + gamma, + adaptation_matrix: transform.to_f32(), + }; + + return T::make_optimized_transform::( + src_layout, + dst_layout, + profile_transform, + options, + ); + } + + let lin_r = self.build_r_linearize_table::( + options.allow_use_cicp_transfer, + )?; + let lin_g = self.build_g_linearize_table::( + options.allow_use_cicp_transfer, + )?; + let lin_b = self.build_b_linearize_table::( + options.allow_use_cicp_transfer, + )?; + + let gamma_r = dst_pr.build_gamma_table::( + &dst_pr.red_trc, + options.allow_use_cicp_transfer, + )?; + let gamma_g = dst_pr.build_gamma_table::( + &dst_pr.green_trc, + options.allow_use_cicp_transfer, + )?; + let gamma_b = dst_pr.build_gamma_table::( + &dst_pr.blue_trc, + options.allow_use_cicp_transfer, + )?; + + let profile_transform = TransformMatrixShaper { + r_linear: lin_r, + g_linear: lin_g, + b_linear: lin_b, + r_gamma: gamma_r, + g_gamma: gamma_g, + b_gamma: gamma_b, + adaptation_matrix: transform.to_f32(), + }; + + T::make_transform::( + src_layout, + dst_layout, + profile_transform, + options, + ) + } else if (self.color_space == DataColorSpace::Gray && self.gray_trc.is_some()) + && (dst_pr.color_space == DataColorSpace::Rgb + || (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some())) + && self.pcs == DataColorSpace::Xyz + && dst_pr.pcs == DataColorSpace::Xyz + { + if src_layout != Layout::GrayAlpha && src_layout != Layout::Gray { + return Err(CmsError::InvalidLayout); + } + + if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() { + return make_lut_transform::( + src_layout, self, dst_layout, dst_pr, options, + ); + } + + let gray_linear = self.build_gray_linearize_table::()?; + + if dst_pr.color_space == DataColorSpace::Gray { + if !T::FINITE && options.allow_extended_range_rgb_xyz { + if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() { + if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() { + // Gray -> Gray case extended range + use crate::conversions::make_gray_to_one_trc_extended; + return make_gray_to_one_trc_extended::( + src_layout, + dst_layout, + linear_evaluator, + gamma_evaluator, + BIT_DEPTH, + ); + } + } + } + + // Gray -> Gray case + let gray_gamma = dst_pr.build_gamma_table::( + &dst_pr.gray_trc, + options.allow_use_cicp_transfer, + )?; + + make_gray_to_x::( + src_layout, + dst_layout, + &gray_linear, + &gray_gamma, + BIT_DEPTH, + GAMMA_CAP, + ) + } else { + #[allow(clippy::collapsible_if)] + if dst_pr.are_all_trc_the_same() { + if !T::FINITE && options.allow_extended_range_rgb_xyz { + if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() { + if let Some(linear_evaluator) = + self.try_extended_linearizing_evaluator() + { + // Gray -> RGB where all TRC is the same with extended range + use crate::conversions::make_gray_to_one_trc_extended; + return make_gray_to_one_trc_extended::( + src_layout, + dst_layout, + linear_evaluator, + gamma_evaluator, + BIT_DEPTH, + ); + } + } + } + + // Gray -> RGB where all TRC is the same + let rgb_gamma = dst_pr.build_gamma_table::( + &dst_pr.red_trc, + options.allow_use_cicp_transfer, + )?; + + make_gray_to_x::( + src_layout, + dst_layout, + &gray_linear, + &rgb_gamma, + BIT_DEPTH, + GAMMA_CAP, + ) + } else { + // Gray -> RGB where all TRC is NOT the same + if !T::FINITE && options.allow_extended_range_rgb_xyz { + if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() { + if let Some(linear_evaluator) = + self.try_extended_linearizing_evaluator() + { + // Gray -> RGB where all TRC is NOT the same with extended range + + use crate::conversions::make_gray_to_rgb_extended; + return make_gray_to_rgb_extended::( + src_layout, + dst_layout, + linear_evaluator, + gamma_evaluator, + BIT_DEPTH, + ); + } + } + } + + let red_gamma = dst_pr.build_gamma_table::( + &dst_pr.red_trc, + options.allow_use_cicp_transfer, + )?; + let green_gamma = dst_pr.build_gamma_table::( + &dst_pr.green_trc, + options.allow_use_cicp_transfer, + )?; + let blue_gamma = dst_pr.build_gamma_table::( + &dst_pr.blue_trc, + options.allow_use_cicp_transfer, + )?; + + let mut gray_linear2 = Box::new([0f32; 65536]); + for (dst, src) in gray_linear2.iter_mut().zip(gray_linear.iter()) { + *dst = *src; + } + + make_gray_to_unfused::( + src_layout, + dst_layout, + gray_linear2, + red_gamma, + green_gamma, + blue_gamma, + BIT_DEPTH, + GAMMA_CAP, + ) + } + } + } else if self.color_space == DataColorSpace::Rgb + && (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some()) + && dst_pr.pcs == DataColorSpace::Xyz + && self.pcs == DataColorSpace::Xyz + { + if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha { + return Err(CmsError::InvalidLayout); + } + if dst_layout != Layout::Gray && dst_layout != Layout::GrayAlpha { + return Err(CmsError::InvalidLayout); + } + + if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() { + return make_lut_transform::( + src_layout, self, dst_layout, dst_pr, options, + ); + } + + let transform = self.transform_matrix(dst_pr).to_f32(); + + let vector = Vector3f { + v: [transform.v[1][0], transform.v[1][1], transform.v[1][2]], + }; + + if !T::FINITE && options.allow_extended_range_rgb_xyz { + if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() { + if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() { + use crate::conversions::make_rgb_to_gray_extended; + return Ok(make_rgb_to_gray_extended::( + src_layout, + dst_layout, + linear_evaluator, + gamma_evaluator, + vector, + BIT_DEPTH, + )); + } + } + } + + let lin_r = self.build_r_linearize_table::( + options.allow_use_cicp_transfer, + )?; + let lin_g = self.build_g_linearize_table::( + options.allow_use_cicp_transfer, + )?; + let lin_b = self.build_b_linearize_table::( + options.allow_use_cicp_transfer, + )?; + let gray_linear = dst_pr.build_gamma_table::( + &dst_pr.gray_trc, + options.allow_use_cicp_transfer, + )?; + + let trc_box = ToneReproductionRgbToGray:: { + r_linear: lin_r, + g_linear: lin_g, + b_linear: lin_b, + gray_gamma: gray_linear, + }; + + Ok(make_rgb_to_gray::( + src_layout, dst_layout, trc_box, vector, GAMMA_CAP, BIT_DEPTH, + )) + } else if (self.color_space.is_three_channels() + || self.color_space == DataColorSpace::Cmyk + || self.color_space == DataColorSpace::Color4) + && (dst_pr.color_space.is_three_channels() + || dst_pr.color_space == DataColorSpace::Cmyk + || dst_pr.color_space == DataColorSpace::Color4) + && (dst_pr.pcs == DataColorSpace::Xyz || dst_pr.pcs == DataColorSpace::Lab) + && (self.pcs == DataColorSpace::Xyz || self.pcs == DataColorSpace::Lab) + { + if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha { + return Err(CmsError::InvalidLayout); + } + if dst_layout == Layout::Gray || dst_layout == Layout::GrayAlpha { + return Err(CmsError::InvalidLayout); + } + make_lut_transform::( + src_layout, self, dst_layout, dst_pr, options, + ) + } else { + make_lut_transform::( + src_layout, self, dst_layout, dst_pr, options, + ) + } + } + + /// Creates transform between source and destination profile + /// Only 8 bit is supported. + pub fn create_transform_8bit( + &self, + src_layout: Layout, + dst_pr: &ColorProfile, + dst_layout: Layout, + options: TransformOptions, + ) -> Result, CmsError> { + self.create_transform_nbit::(src_layout, dst_pr, dst_layout, options) + } + + pub(crate) fn get_device_to_pcs(&self, intent: RenderingIntent) -> Option<&LutWarehouse> { + match intent { + RenderingIntent::AbsoluteColorimetric => self.lut_a_to_b_colorimetric.as_ref(), + RenderingIntent::Saturation => self.lut_a_to_b_saturation.as_ref(), + RenderingIntent::RelativeColorimetric => self.lut_a_to_b_colorimetric.as_ref(), + RenderingIntent::Perceptual => self.lut_a_to_b_perceptual.as_ref(), + } + } + + pub(crate) fn get_pcs_to_device(&self, intent: RenderingIntent) -> Option<&LutWarehouse> { + match intent { + RenderingIntent::AbsoluteColorimetric => self.lut_b_to_a_colorimetric.as_ref(), + RenderingIntent::Saturation => self.lut_b_to_a_saturation.as_ref(), + RenderingIntent::RelativeColorimetric => self.lut_b_to_a_colorimetric.as_ref(), + RenderingIntent::Perceptual => self.lut_b_to_a_perceptual.as_ref(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ColorProfile, DataColorSpace, Layout, RenderingIntent, TransformOptions}; + use rand::Rng; + + #[test] + fn test_transform_rgb8() { + let mut srgb_profile = ColorProfile::new_srgb(); + let bt2020_profile = ColorProfile::new_bt2020(); + let random_point_x = rand::rng().random_range(0..255); + let transform = bt2020_profile + .create_transform_8bit( + Layout::Rgb, + &srgb_profile, + Layout::Rgb, + TransformOptions::default(), + ) + .unwrap(); + let src = vec![random_point_x; 256 * 256 * 3]; + let mut dst = vec![random_point_x; 256 * 256 * 3]; + transform.transform(&src, &mut dst).unwrap(); + + let transform = bt2020_profile + .create_transform_8bit( + Layout::Rgb, + &srgb_profile, + Layout::Rgb, + TransformOptions { + ..TransformOptions::default() + }, + ) + .unwrap(); + transform.transform(&src, &mut dst).unwrap(); + srgb_profile.rendering_intent = RenderingIntent::RelativeColorimetric; + let transform = bt2020_profile + .create_transform_8bit( + Layout::Rgb, + &srgb_profile, + Layout::Rgb, + TransformOptions { + ..TransformOptions::default() + }, + ) + .unwrap(); + transform.transform(&src, &mut dst).unwrap(); + srgb_profile.rendering_intent = RenderingIntent::Saturation; + let transform = bt2020_profile + .create_transform_8bit( + Layout::Rgb, + &srgb_profile, + Layout::Rgb, + TransformOptions { + ..TransformOptions::default() + }, + ) + .unwrap(); + transform.transform(&src, &mut dst).unwrap(); + } + + #[test] + fn test_transform_rgba8() { + let srgb_profile = ColorProfile::new_srgb(); + let bt2020_profile = ColorProfile::new_bt2020(); + let random_point_x = rand::rng().random_range(0..255); + let transform = bt2020_profile + .create_transform_8bit( + Layout::Rgba, + &srgb_profile, + Layout::Rgba, + TransformOptions::default(), + ) + .unwrap(); + let src = vec![random_point_x; 256 * 256 * 4]; + let mut dst = vec![random_point_x; 256 * 256 * 4]; + transform.transform(&src, &mut dst).unwrap(); + } + + #[test] + fn test_transform_gray_to_rgb8() { + let gray_profile = ColorProfile::new_gray_with_gamma(2.2f32); + let bt2020_profile = ColorProfile::new_bt2020(); + let random_point_x = rand::rng().random_range(0..255); + let transform = gray_profile + .create_transform_8bit( + Layout::Gray, + &bt2020_profile, + Layout::Rgb, + TransformOptions::default(), + ) + .unwrap(); + let src = vec![random_point_x; 256 * 256]; + let mut dst = vec![random_point_x; 256 * 256 * 3]; + transform.transform(&src, &mut dst).unwrap(); + } + + #[test] + fn test_transform_gray_to_rgba8() { + let srgb_profile = ColorProfile::new_gray_with_gamma(2.2f32); + let bt2020_profile = ColorProfile::new_bt2020(); + let random_point_x = rand::rng().random_range(0..255); + let transform = srgb_profile + .create_transform_8bit( + Layout::Gray, + &bt2020_profile, + Layout::Rgba, + TransformOptions::default(), + ) + .unwrap(); + let src = vec![random_point_x; 256 * 256]; + let mut dst = vec![random_point_x; 256 * 256 * 4]; + transform.transform(&src, &mut dst).unwrap(); + } + + #[test] + fn test_transform_gray_to_gray_alpha8() { + let srgb_profile = ColorProfile::new_gray_with_gamma(2.2f32); + let bt2020_profile = ColorProfile::new_bt2020(); + let random_point_x = rand::rng().random_range(0..255); + let transform = srgb_profile + .create_transform_8bit( + Layout::Gray, + &bt2020_profile, + Layout::GrayAlpha, + TransformOptions::default(), + ) + .unwrap(); + let src = vec![random_point_x; 256 * 256]; + let mut dst = vec![random_point_x; 256 * 256 * 2]; + transform.transform(&src, &mut dst).unwrap(); + } + + #[test] + fn test_transform_rgb10() { + let srgb_profile = ColorProfile::new_srgb(); + let bt2020_profile = ColorProfile::new_bt2020(); + let random_point_x = rand::rng().random_range(0..((1 << 10) - 1)); + let transform = bt2020_profile + .create_transform_10bit( + Layout::Rgb, + &srgb_profile, + Layout::Rgb, + TransformOptions::default(), + ) + .unwrap(); + let src = vec![random_point_x; 256 * 256 * 3]; + let mut dst = vec![random_point_x; 256 * 256 * 3]; + transform.transform(&src, &mut dst).unwrap(); + } + + #[test] + fn test_transform_rgb12() { + let srgb_profile = ColorProfile::new_srgb(); + let bt2020_profile = ColorProfile::new_bt2020(); + let random_point_x = rand::rng().random_range(0..((1 << 12) - 1)); + let transform = bt2020_profile + .create_transform_12bit( + Layout::Rgb, + &srgb_profile, + Layout::Rgb, + TransformOptions::default(), + ) + .unwrap(); + let src = vec![random_point_x; 256 * 256 * 3]; + let mut dst = vec![random_point_x; 256 * 256 * 3]; + transform.transform(&src, &mut dst).unwrap(); + } + + #[test] + fn test_transform_rgb16() { + let srgb_profile = ColorProfile::new_srgb(); + let bt2020_profile = ColorProfile::new_bt2020(); + let random_point_x = rand::rng().random_range(0..((1u32 << 16u32) - 1u32)) as u16; + let transform = bt2020_profile + .create_transform_16bit( + Layout::Rgb, + &srgb_profile, + Layout::Rgb, + TransformOptions::default(), + ) + .unwrap(); + let src = vec![random_point_x; 256 * 256 * 3]; + let mut dst = vec![random_point_x; 256 * 256 * 3]; + transform.transform(&src, &mut dst).unwrap(); + } + + #[test] + fn test_transform_round_trip_rgb8() { + let srgb_profile = ColorProfile::new_srgb(); + let bt2020_profile = ColorProfile::new_bt2020(); + let transform = srgb_profile + .create_transform_8bit( + Layout::Rgb, + &bt2020_profile, + Layout::Rgb, + TransformOptions::default(), + ) + .unwrap(); + let mut src = vec![0u8; 256 * 256 * 3]; + for dst in src.chunks_exact_mut(3) { + dst[0] = 175; + dst[1] = 75; + dst[2] = 13; + } + let mut dst = vec![0u8; 256 * 256 * 3]; + transform.transform(&src, &mut dst).unwrap(); + + let transform_inverse = bt2020_profile + .create_transform_8bit( + Layout::Rgb, + &srgb_profile, + Layout::Rgb, + TransformOptions::default(), + ) + .unwrap(); + + transform_inverse.transform(&dst, &mut src).unwrap(); + + for src in src.chunks_exact_mut(3) { + let diff0 = (src[0] as i32 - 175).abs(); + let diff1 = (src[1] as i32 - 75).abs(); + let diff2 = (src[2] as i32 - 13).abs(); + assert!( + diff0 < 3, + "On channel 0 difference should be less than 3, but it was {diff0}" + ); + assert!( + diff1 < 3, + "On channel 1 difference should be less than 3, but it was {diff1}" + ); + assert!( + diff2 < 3, + "On channel 2 difference should be less than 3, but it was {diff2}" + ); + } + } + + #[test] + fn test_transform_round_trip_rgb10() { + let srgb_profile = ColorProfile::new_srgb(); + let bt2020_profile = ColorProfile::new_bt2020(); + let transform = srgb_profile + .create_transform_10bit( + Layout::Rgb, + &bt2020_profile, + Layout::Rgb, + TransformOptions::default(), + ) + .unwrap(); + let mut src = vec![0u16; 256 * 256 * 3]; + for dst in src.chunks_exact_mut(3) { + dst[0] = 175; + dst[1] = 256; + dst[2] = 512; + } + let mut dst = vec![0u16; 256 * 256 * 3]; + transform.transform(&src, &mut dst).unwrap(); + + let transform_inverse = bt2020_profile + .create_transform_10bit( + Layout::Rgb, + &srgb_profile, + Layout::Rgb, + TransformOptions::default(), + ) + .unwrap(); + + transform_inverse.transform(&dst, &mut src).unwrap(); + + for src in src.chunks_exact_mut(3) { + let diff0 = (src[0] as i32 - 175).abs(); + let diff1 = (src[1] as i32 - 256).abs(); + let diff2 = (src[2] as i32 - 512).abs(); + assert!( + diff0 < 15, + "On channel 0 difference should be less than 15, but it was {diff0}" + ); + assert!( + diff1 < 15, + "On channel 1 difference should be less than 15, but it was {diff1}" + ); + assert!( + diff2 < 15, + "On channel 2 difference should be less than 15, but it was {diff2}" + ); + } + } + + #[test] + fn test_transform_round_trip_rgb12() { + let srgb_profile = ColorProfile::new_srgb(); + let bt2020_profile = ColorProfile::new_bt2020(); + let transform = srgb_profile + .create_transform_12bit( + Layout::Rgb, + &bt2020_profile, + Layout::Rgb, + TransformOptions::default(), + ) + .unwrap(); + let mut src = vec![0u16; 256 * 256 * 3]; + for dst in src.chunks_exact_mut(3) { + dst[0] = 1750; + dst[1] = 2560; + dst[2] = 3143; + } + let mut dst = vec![0u16; 256 * 256 * 3]; + transform.transform(&src, &mut dst).unwrap(); + + let transform_inverse = bt2020_profile + .create_transform_12bit( + Layout::Rgb, + &srgb_profile, + Layout::Rgb, + TransformOptions::default(), + ) + .unwrap(); + + transform_inverse.transform(&dst, &mut src).unwrap(); + + for src in src.chunks_exact_mut(3) { + let diff0 = (src[0] as i32 - 1750).abs(); + let diff1 = (src[1] as i32 - 2560).abs(); + let diff2 = (src[2] as i32 - 3143).abs(); + assert!( + diff0 < 25, + "On channel 0 difference should be less than 25, but it was {diff0}" + ); + assert!( + diff1 < 25, + "On channel 1 difference should be less than 25, but it was {diff1}" + ); + assert!( + diff2 < 25, + "On channel 2 difference should be less than 25, but it was {diff2}" + ); + } + } + + #[test] + fn test_transform_round_trip_rgb16() { + let srgb_profile = ColorProfile::new_srgb(); + let bt2020_profile = ColorProfile::new_bt2020(); + let transform = srgb_profile + .create_transform_16bit( + Layout::Rgb, + &bt2020_profile, + Layout::Rgb, + TransformOptions::default(), + ) + .unwrap(); + let mut src = vec![0u16; 256 * 256 * 3]; + for dst in src.chunks_exact_mut(3) { + dst[0] = 1760; + dst[1] = 2560; + dst[2] = 5120; + } + let mut dst = vec![0u16; 256 * 256 * 3]; + transform.transform(&src, &mut dst).unwrap(); + + let transform_inverse = bt2020_profile + .create_transform_16bit( + Layout::Rgb, + &srgb_profile, + Layout::Rgb, + TransformOptions::default(), + ) + .unwrap(); + + transform_inverse.transform(&dst, &mut src).unwrap(); + + for src in src.chunks_exact_mut(3) { + let diff0 = (src[0] as i32 - 1760).abs(); + let diff1 = (src[1] as i32 - 2560).abs(); + let diff2 = (src[2] as i32 - 5120).abs(); + assert!( + diff0 < 35, + "On channel 0 difference should be less than 35, but it was {diff0}" + ); + assert!( + diff1 < 35, + "On channel 1 difference should be less than 35, but it was {diff1}" + ); + assert!( + diff2 < 35, + "On channel 2 difference should be less than 35, but it was {diff2}" + ); + } + } + + #[test] + fn test_transform_rgb_to_gray_extended() { + let srgb = ColorProfile::new_srgb(); + let mut gray_profile = ColorProfile::new_gray_with_gamma(1.0); + gray_profile.color_space = DataColorSpace::Gray; + gray_profile.gray_trc = srgb.red_trc.clone(); + let mut test_profile = vec![0.; 4]; + test_profile[2] = 1.; + let mut dst = vec![0.; 1]; + + let mut inverse = vec![0.; 4]; + + let cvt0 = srgb + .create_transform_f32( + Layout::Rgba, + &gray_profile, + Layout::Gray, + TransformOptions { + allow_extended_range_rgb_xyz: true, + ..Default::default() + }, + ) + .unwrap(); + cvt0.transform(&test_profile, &mut dst).unwrap(); + assert!((dst[0] - 0.273046) < 1e-4); + + let cvt_inverse = gray_profile + .create_transform_f32( + Layout::Gray, + &srgb, + Layout::Rgba, + TransformOptions { + allow_extended_range_rgb_xyz: false, + ..Default::default() + }, + ) + .unwrap(); + cvt_inverse.transform(&dst, &mut inverse).unwrap(); + assert!((inverse[0] - 0.273002833) < 1e-4); + + let cvt1 = srgb + .create_transform_f32( + Layout::Rgba, + &gray_profile, + Layout::Gray, + TransformOptions { + allow_extended_range_rgb_xyz: false, + ..Default::default() + }, + ) + .unwrap(); + cvt1.transform(&test_profile, &mut dst).unwrap(); + assert!((dst[0] - 0.27307168) < 1e-5); + + inverse.fill(0.); + + let cvt_inverse = gray_profile + .create_transform_f32( + Layout::Gray, + &srgb, + Layout::Rgba, + TransformOptions { + allow_extended_range_rgb_xyz: true, + ..Default::default() + }, + ) + .unwrap(); + cvt_inverse.transform(&dst, &mut inverse).unwrap(); + assert!((inverse[0] - 0.273002833) < 1e-4); + } +} diff --git a/deps/moxcms/src/trc.rs b/deps/moxcms/src/trc.rs new file mode 100644 index 0000000..1170adc --- /dev/null +++ b/deps/moxcms/src/trc.rs @@ -0,0 +1,1583 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::cicp::create_rec709_parametric; +use crate::matan::is_curve_linear16; +use crate::math::m_clamp; +use crate::mlaf::{mlaf, neg_mlaf}; +use crate::transform::PointeeSizeExpressible; +use crate::writer::FloatToFixedU8Fixed8; +use crate::{CmsError, ColorProfile, DataColorSpace, Rgb, TransferCharacteristics}; +use num_traits::AsPrimitive; +use pxfm::{dirty_powf, f_pow, f_powf}; + +#[derive(Clone, Debug)] +pub enum ToneReprCurve { + Lut(Vec), + Parametric(Vec), +} + +impl ToneReprCurve { + pub fn inverse(&self) -> Result { + match self { + ToneReprCurve::Lut(lut) => { + let inverse_length = lut.len().max(256); + Ok(ToneReprCurve::Lut(invert_lut(lut, inverse_length))) + } + ToneReprCurve::Parametric(parametric) => ParametricCurve::new(parametric) + .and_then(|x| x.invert()) + .map(|x| ToneReprCurve::Parametric([x.g, x.a, x.b, x.c, x.d, x.e, x.f].to_vec())) + .ok_or(CmsError::BuildTransferFunction), + } + } + + /// Creates tone curve evaluator + pub fn make_linear_evaluator( + &self, + ) -> Result, CmsError> { + match self { + ToneReprCurve::Lut(lut) => { + if lut.is_empty() { + return Ok(Box::new(ToneCurveEvaluatorLinear {})); + } + if lut.len() == 1 { + let gamma = u8_fixed_8number_to_float(lut[0]); + return Ok(Box::new(ToneCurveEvaluatorPureGamma { gamma })); + } + let converted_curve = lut.iter().map(|&x| x as f32 / 65535.0).collect::>(); + Ok(Box::new(ToneCurveLutEvaluator { + lut: converted_curve, + })) + } + ToneReprCurve::Parametric(parametric) => { + let parametric_curve = + ParametricCurve::new(parametric).ok_or(CmsError::BuildTransferFunction)?; + Ok(Box::new(ToneCurveParametricEvaluator { + parametric: parametric_curve, + })) + } + } + } + + /// Creates tone curve evaluator from transfer characteristics + pub fn make_cicp_linear_evaluator( + transfer_characteristics: TransferCharacteristics, + ) -> Result, CmsError> { + if !transfer_characteristics.has_transfer_curve() { + return Err(CmsError::BuildTransferFunction); + } + Ok(Box::new(ToneCurveCicpLinearEvaluator { + trc: transfer_characteristics, + })) + } + + /// Creates tone curve inverse evaluator + pub fn make_gamma_evaluator( + &self, + ) -> Result, CmsError> { + match self { + ToneReprCurve::Lut(lut) => { + if lut.is_empty() { + return Ok(Box::new(ToneCurveEvaluatorLinear {})); + } + if lut.len() == 1 { + let gamma = 1. / u8_fixed_8number_to_float(lut[0]); + return Ok(Box::new(ToneCurveEvaluatorPureGamma { gamma })); + } + let inverted_lut = invert_lut(lut, 16384); + let converted_curve = inverted_lut + .iter() + .map(|&x| x as f32 / 65535.0) + .collect::>(); + Ok(Box::new(ToneCurveLutEvaluator { + lut: converted_curve, + })) + } + ToneReprCurve::Parametric(parametric) => { + let parametric_curve = ParametricCurve::new(parametric) + .and_then(|x| x.invert()) + .ok_or(CmsError::BuildTransferFunction)?; + Ok(Box::new(ToneCurveParametricEvaluator { + parametric: parametric_curve, + })) + } + } + } + + /// Creates tone curve inverse evaluator from transfer characteristics + pub fn make_cicp_gamma_evaluator( + transfer_characteristics: TransferCharacteristics, + ) -> Result, CmsError> { + if !transfer_characteristics.has_transfer_curve() { + return Err(CmsError::BuildTransferFunction); + } + Ok(Box::new(ToneCurveCicpGammaEvaluator { + trc: transfer_characteristics, + })) + } +} + +struct ToneCurveCicpLinearEvaluator { + trc: TransferCharacteristics, +} + +struct ToneCurveCicpGammaEvaluator { + trc: TransferCharacteristics, +} + +impl ToneCurveEvaluator for ToneCurveCicpLinearEvaluator { + fn evaluate_tristimulus(&self, rgb: Rgb) -> Rgb { + Rgb::new( + self.trc.linearize(rgb.r as f64) as f32, + self.trc.linearize(rgb.g as f64) as f32, + self.trc.linearize(rgb.b as f64) as f32, + ) + } + + fn evaluate_value(&self, value: f32) -> f32 { + self.trc.linearize(value as f64) as f32 + } +} + +impl ToneCurveEvaluator for ToneCurveCicpGammaEvaluator { + fn evaluate_tristimulus(&self, rgb: Rgb) -> Rgb { + Rgb::new( + self.trc.gamma(rgb.r as f64) as f32, + self.trc.gamma(rgb.g as f64) as f32, + self.trc.gamma(rgb.b as f64) as f32, + ) + } + + fn evaluate_value(&self, value: f32) -> f32 { + self.trc.gamma(value as f64) as f32 + } +} + +struct ToneCurveLutEvaluator { + lut: Vec, +} + +impl ToneCurveEvaluator for ToneCurveLutEvaluator { + fn evaluate_value(&self, value: f32) -> f32 { + lut_interp_linear_float(value, &self.lut) + } + + fn evaluate_tristimulus(&self, rgb: Rgb) -> Rgb { + Rgb::new( + lut_interp_linear_float(rgb.r, &self.lut), + lut_interp_linear_float(rgb.g, &self.lut), + lut_interp_linear_float(rgb.b, &self.lut), + ) + } +} + +pub(crate) fn build_trc_table(num_entries: i32, eotf: impl Fn(f64) -> f64) -> Vec { + let mut table = vec![0u16; num_entries as usize]; + + for (i, table_value) in table.iter_mut().enumerate() { + let x: f64 = i as f64 / (num_entries - 1) as f64; + let y: f64 = eotf(x); + let mut output: f64; + output = y * 65535.0 + 0.5; + if output > 65535.0 { + output = 65535.0 + } + if output < 0.0 { + output = 0.0 + } + *table_value = output.floor() as u16; + } + table +} + +/// Creates Tone Reproduction curve from gamma +pub fn curve_from_gamma(gamma: f32) -> ToneReprCurve { + ToneReprCurve::Lut(vec![gamma.to_u8_fixed8()]) +} + +#[derive(Debug)] +struct ParametricCurve { + g: f32, + a: f32, + b: f32, + c: f32, + d: f32, + e: f32, + f: f32, +} + +impl ParametricCurve { + #[allow(clippy::many_single_char_names)] + fn new(params: &[f32]) -> Option { + // convert from the variable number of parameters + // contained in profiles to a unified representation. + let g: f32 = params[0]; + match params[1..] { + [] => Some(ParametricCurve { + g, + a: 1., + b: 0., + c: 1., + d: 0., + e: 0., + f: 0., + }), + [a, b] => Some(ParametricCurve { + g, + a, + b, + c: 0., + d: -b / a, + e: 0., + f: 0., + }), + [a, b, c] => Some(ParametricCurve { + g, + a, + b, + c: 0., + d: -b / a, + e: c, + f: c, + }), + [a, b, c, d] => Some(ParametricCurve { + g, + a, + b, + c, + d, + e: 0., + f: 0., + }), + [a, b, c, d, e, f] => Some(ParametricCurve { + g, + a, + b, + c, + d, + e, + f, + }), + _ => None, + } + } + + fn is_linear(&self) -> bool { + (self.g - 1.0).abs() < 1e-5 + && (self.a - 1.0).abs() < 1e-5 + && self.b.abs() < 1e-5 + && self.c.abs() < 1e-5 + } + + fn eval(&self, x: f32) -> f32 { + if x < self.d { + self.c * x + self.f + } else { + f_powf(self.a * x + self.b, self.g) + self.e + } + } + + #[allow(dead_code)] + #[allow(clippy::many_single_char_names)] + fn invert(&self) -> Option { + // First check if the function is continuous at the cross-over point d. + let d1 = f_powf(self.a * self.d + self.b, self.g) + self.e; + let d2 = self.c * self.d + self.f; + + if (d1 - d2).abs() > 0.1 { + return None; + } + let d = d1; + + // y = (a * x + b)^g + e + // y - e = (a * x + b)^g + // (y - e)^(1/g) = a*x + b + // (y - e)^(1/g) - b = a*x + // (y - e)^(1/g)/a - b/a = x + // ((y - e)/a^g)^(1/g) - b/a = x + // ((1/(a^g)) * y - e/(a^g))^(1/g) - b/a = x + let a = 1. / f_powf(self.a, self.g); + let b = -self.e / f_powf(self.a, self.g); + let g = 1. / self.g; + let e = -self.b / self.a; + + // y = c * x + f + // y - f = c * x + // y/c - f/c = x + let (c, f); + if d <= 0. { + c = 1.; + f = 0.; + } else { + c = 1. / self.c; + f = -self.f / self.c; + } + + // if self.d > 0. and self.c == 0 as is likely with type 1 and 2 parametric function + // then c and f will not be finite. + if !(g.is_finite() + && a.is_finite() + && b.is_finite() + && c.is_finite() + && d.is_finite() + && e.is_finite() + && f.is_finite()) + { + return None; + } + + Some(ParametricCurve { + g, + a, + b, + c, + d, + e, + f, + }) + } +} + +#[inline] +pub(crate) fn u8_fixed_8number_to_float(x: u16) -> f32 { + // 0x0000 = 0. + // 0x0100 = 1. + // 0xffff = 255 + 255/256 + (x as i32 as f64 / 256.0) as f32 +} + +fn passthrough_table() +-> Box<[f32; N]> { + let mut gamma_table = Box::new([0f32; N]); + let max_value = if T::FINITE { + (1 << BIT_DEPTH) - 1 + } else { + T::NOT_FINITE_LINEAR_TABLE_SIZE - 1 + }; + let cap_values = if T::FINITE { + (1u32 << BIT_DEPTH) as usize + } else { + T::NOT_FINITE_LINEAR_TABLE_SIZE + }; + assert!(cap_values <= N, "Invalid lut table construction"); + let scale_value = 1f64 / max_value as f64; + for (i, g) in gamma_table.iter_mut().enumerate().take(cap_values) { + *g = (i as f64 * scale_value) as f32; + } + + gamma_table +} + +fn linear_forward_table( + gamma: u16, +) -> Box<[f32; N]> { + let mut gamma_table = Box::new([0f32; N]); + let gamma_float: f32 = u8_fixed_8number_to_float(gamma); + let max_value = if T::FINITE { + (1 << BIT_DEPTH) - 1 + } else { + T::NOT_FINITE_LINEAR_TABLE_SIZE - 1 + }; + let cap_values = if T::FINITE { + (1u32 << BIT_DEPTH) as usize + } else { + T::NOT_FINITE_LINEAR_TABLE_SIZE + }; + assert!(cap_values <= N, "Invalid lut table construction"); + let scale_value = 1f64 / max_value as f64; + for (i, g) in gamma_table.iter_mut().enumerate().take(cap_values) { + *g = f_pow(i as f64 * scale_value, gamma_float as f64) as f32; + } + + gamma_table +} + +#[inline(always)] +pub(crate) fn lut_interp_linear_float(x: f32, table: &[f32]) -> f32 { + let value = x.min(1.).max(0.) * (table.len() - 1) as f32; + + let upper: i32 = value.ceil() as i32; + let lower: i32 = value.floor() as i32; + + let diff = upper as f32 - value; + let tu = table[upper as usize]; + mlaf(neg_mlaf(tu, tu, diff), table[lower as usize], diff) +} + +/// Lut interpolation float where values is already clamped +#[inline(always)] +#[allow(dead_code)] +pub(crate) fn lut_interp_linear_float_clamped(x: f32, table: &[f32]) -> f32 { + let value = x * (table.len() - 1) as f32; + + let upper: i32 = value.ceil() as i32; + let lower: i32 = value.floor() as i32; + + let diff = upper as f32 - value; + let tu = table[upper as usize]; + mlaf(neg_mlaf(tu, tu, diff), table[lower as usize], diff) +} + +#[inline] +pub(crate) fn lut_interp_linear(input_value: f64, table: &[u16]) -> f32 { + let mut input_value = input_value; + if table.is_empty() { + return input_value as f32; + } + + input_value *= (table.len() - 1) as f64; + + let upper: i32 = input_value.ceil() as i32; + let lower: i32 = input_value.floor() as i32; + let w0 = table[(upper as usize).min(table.len() - 1)] as f64; + let w1 = 1. - (upper as f64 - input_value); + let w2 = table[(lower as usize).min(table.len() - 1)] as f64; + let w3 = upper as f64 - input_value; + let value: f32 = mlaf(w2 * w3, w0, w1) as f32; + value * (1.0 / 65535.0) +} + +fn linear_lut_interpolate( + table: &[u16], +) -> Box<[f32; N]> { + let mut gamma_table = Box::new([0f32; N]); + let max_value = if T::FINITE { + (1 << BIT_DEPTH) - 1 + } else { + T::NOT_FINITE_LINEAR_TABLE_SIZE - 1 + }; + let cap_values = if T::FINITE { + (1u32 << BIT_DEPTH) as usize + } else { + T::NOT_FINITE_LINEAR_TABLE_SIZE + }; + assert!(cap_values <= N, "Invalid lut table construction"); + let scale_value = 1f64 / max_value as f64; + for (i, g) in gamma_table.iter_mut().enumerate().take(cap_values) { + *g = lut_interp_linear(i as f64 * scale_value, table); + } + gamma_table +} + +fn linear_curve_parametric( + params: &[f32], +) -> Option> { + let params = ParametricCurve::new(params)?; + let mut gamma_table = Box::new([0f32; N]); + let max_value = if T::FINITE { + (1 << BIT_DEPTH) - 1 + } else { + T::NOT_FINITE_LINEAR_TABLE_SIZE - 1 + }; + let cap_value = if T::FINITE { + 1 << BIT_DEPTH + } else { + T::NOT_FINITE_LINEAR_TABLE_SIZE + }; + let scale_value = 1f32 / max_value as f32; + for (i, g) in gamma_table.iter_mut().enumerate().take(cap_value) { + let x = i as f32 * scale_value; + *g = m_clamp(params.eval(x), 0.0, 1.0); + } + Some(gamma_table) +} + +fn linear_curve_parametric_s(params: &[f32]) -> Option> { + let params = ParametricCurve::new(params)?; + let mut gamma_table = Box::new([0f32; N]); + let scale_value = 1f32 / (N - 1) as f32; + for (i, g) in gamma_table.iter_mut().enumerate().take(N) { + let x = i as f32 * scale_value; + *g = m_clamp(params.eval(x), 0.0, 1.0); + } + Some(gamma_table) +} + +pub(crate) fn make_gamma_linear_table< + T: Default + Copy + 'static + PointeeSizeExpressible, + const BUCKET: usize, + const N: usize, +>( + bit_depth: usize, +) -> Box<[T; BUCKET]> +where + f32: AsPrimitive, +{ + let mut table = Box::new([T::default(); BUCKET]); + let max_range = if T::FINITE { + (1f64 / ((N - 1) as f64 / (1 << bit_depth) as f64)) as f32 + } else { + (1f64 / ((N - 1) as f64)) as f32 + }; + for (v, output) in table.iter_mut().take(N).enumerate() { + if T::FINITE { + *output = (v as f32 * max_range).round().as_(); + } else { + *output = (v as f32 * max_range).as_(); + } + } + table +} + +#[inline] +fn lut_interp_linear_gamma_impl< + T: Default + Copy + 'static + PointeeSizeExpressible, + const N: usize, + const BIT_DEPTH: usize, +>( + input_value: u32, + table: &[u16], +) -> T +where + u32: AsPrimitive, +{ + // Start scaling input_value to the length of the array: GAMMA_CAP*(length-1). + // We'll divide out the GAMMA_CAP next + let mut value: u32 = input_value * (table.len() - 1) as u32; + let cap_value = N - 1; + // equivalent to ceil(value/GAMMA_CAP) + let upper: u32 = value.div_ceil(cap_value as u32); + // equivalent to floor(value/GAMMA_CAP) + let lower: u32 = value / cap_value as u32; + // interp is the distance from upper to value scaled to 0..GAMMA_CAP + let interp: u32 = value % cap_value as u32; + let lw_value = table[lower as usize]; + let hw_value = table[upper as usize]; + // the table values range from 0..65535 + value = mlaf( + hw_value as u32 * interp, + lw_value as u32, + (N - 1) as u32 - interp, + ); // 0..(65535*GAMMA_CAP) + + // round and scale + let max_colors = if T::FINITE { (1 << BIT_DEPTH) - 1 } else { 1 }; + value += (cap_value * 65535 / max_colors / 2) as u32; // scale to 0...max_colors + value /= (cap_value * 65535 / max_colors) as u32; + value.as_() +} + +#[inline] +fn lut_interp_linear_gamma_impl_f32< + T: Default + Copy + 'static + PointeeSizeExpressible, + const N: usize, + const BIT_DEPTH: usize, +>( + input_value: u32, + table: &[u16], +) -> T +where + f32: AsPrimitive, +{ + // Start scaling input_value to the length of the array: GAMMA_CAP*(length-1). + // We'll divide out the GAMMA_CAP next + let guess: u32 = input_value * (table.len() - 1) as u32; + let cap_value = N - 1; + // equivalent to ceil(value/GAMMA_CAP) + let upper: u32 = guess.div_ceil(cap_value as u32); + // equivalent to floor(value/GAMMA_CAP) + let lower: u32 = guess / cap_value as u32; + // interp is the distance from upper to value scaled to 0..GAMMA_CAP + let interp: u32 = guess % cap_value as u32; + let lw_value = table[lower as usize]; + let hw_value = table[upper as usize]; + // the table values range from 0..65535 + let mut value = mlaf( + hw_value as f32 * interp as f32, + lw_value as f32, + (N - 1) as f32 - interp as f32, + ); // 0..(65535*GAMMA_CAP) + + // round and scale + let max_colors = if T::FINITE { (1 << BIT_DEPTH) - 1 } else { 1 }; + value /= (cap_value * 65535 / max_colors) as f32; + value.as_() +} + +#[doc(hidden)] +pub trait GammaLutInterpolate { + fn gamma_lut_interp< + T: Default + Copy + 'static + PointeeSizeExpressible, + const N: usize, + const BIT_DEPTH: usize, + >( + input_value: u32, + table: &[u16], + ) -> T + where + u32: AsPrimitive, + f32: AsPrimitive; +} + +macro_rules! gamma_lut_interp_fixed { + ($i_type: ident) => { + impl GammaLutInterpolate for $i_type { + #[inline] + fn gamma_lut_interp< + T: Default + Copy + 'static + PointeeSizeExpressible, + const N: usize, + const BIT_DEPTH: usize, + >( + input_value: u32, + table: &[u16], + ) -> T + where + u32: AsPrimitive, + { + lut_interp_linear_gamma_impl::(input_value, table) + } + } + }; +} + +gamma_lut_interp_fixed!(u8); +gamma_lut_interp_fixed!(u16); + +macro_rules! gammu_lut_interp_float { + ($f_type: ident) => { + impl GammaLutInterpolate for $f_type { + #[inline] + fn gamma_lut_interp< + T: Default + Copy + 'static + PointeeSizeExpressible, + const N: usize, + const BIT_DEPTH: usize, + >( + input_value: u32, + table: &[u16], + ) -> T + where + f32: AsPrimitive, + u32: AsPrimitive, + { + lut_interp_linear_gamma_impl_f32::(input_value, table) + } + } + }; +} + +gammu_lut_interp_float!(f32); +gammu_lut_interp_float!(f64); + +pub(crate) fn make_gamma_lut< + T: Default + Copy + 'static + PointeeSizeExpressible + GammaLutInterpolate, + const BUCKET: usize, + const N: usize, + const BIT_DEPTH: usize, +>( + table: &[u16], +) -> Box<[T; BUCKET]> +where + u32: AsPrimitive, + f32: AsPrimitive, +{ + let mut new_table = Box::new([T::default(); BUCKET]); + for (v, output) in new_table.iter_mut().take(N).enumerate() { + *output = T::gamma_lut_interp::(v as u32, table); + } + new_table +} + +#[inline] +pub(crate) fn lut_interp_linear16(input_value: u16, table: &[u16]) -> u16 { + // Start scaling input_value to the length of the array: 65535*(length-1). + // We'll divide out the 65535 next + let mut value: u32 = input_value as u32 * (table.len() as u32 - 1); + let upper: u16 = value.div_ceil(65535) as u16; // equivalent to ceil(value/65535) + let lower: u16 = (value / 65535) as u16; // equivalent to floor(value/65535) + // interp is the distance from upper to value scaled to 0..65535 + let interp: u32 = value % 65535; // 0..65535*65535 + value = (table[upper as usize] as u32 * interp + + table[lower as usize] as u32 * (65535 - interp)) + / 65535; + value as u16 +} + +#[inline] +pub(crate) fn lut_interp_linear16_boxed(input_value: u16, table: &[u16; N]) -> u16 { + // Start scaling input_value to the length of the array: 65535*(length-1). + // We'll divide out the 65535 next + let mut value: u32 = input_value as u32 * (table.len() as u32 - 1); + let upper: u16 = value.div_ceil(65535) as u16; // equivalent to ceil(value/65535) + let lower: u16 = (value / 65535) as u16; // equivalent to floor(value/65535) + // interp is the distance from upper to value scaled to 0..65535 + let interp: u32 = value % 65535; // 0..65535*65535 + value = (table[upper as usize] as u32 * interp + + table[lower as usize] as u32 * (65535 - interp)) + / 65535; + value as u16 +} + +fn make_gamma_pow_table< + T: Default + Copy + 'static + PointeeSizeExpressible, + const BUCKET: usize, + const N: usize, +>( + gamma: f32, + bit_depth: usize, +) -> Box<[T; BUCKET]> +where + f32: AsPrimitive, +{ + let mut table = Box::new([T::default(); BUCKET]); + let scale = 1f32 / (N - 1) as f32; + let cap = ((1 << bit_depth) - 1) as f32; + if T::FINITE { + for (v, output) in table.iter_mut().take(N).enumerate() { + *output = (cap * f_powf(v as f32 * scale, gamma)).round().as_(); + } + } else { + for (v, output) in table.iter_mut().take(N).enumerate() { + *output = (cap * f_powf(v as f32 * scale, gamma)).as_(); + } + } + table +} + +fn make_gamma_parametric_table< + T: Default + Copy + 'static + PointeeSizeExpressible, + const BUCKET: usize, + const N: usize, + const BIT_DEPTH: usize, +>( + parametric_curve: ParametricCurve, +) -> Box<[T; BUCKET]> +where + f32: AsPrimitive, +{ + let mut table = Box::new([T::default(); BUCKET]); + let scale = 1f32 / (N - 1) as f32; + let cap = ((1 << BIT_DEPTH) - 1) as f32; + if T::FINITE { + for (v, output) in table.iter_mut().take(N).enumerate() { + *output = (cap * parametric_curve.eval(v as f32 * scale)) + .round() + .as_(); + } + } else { + for (v, output) in table.iter_mut().take(N).enumerate() { + *output = (cap * parametric_curve.eval(v as f32 * scale)).as_(); + } + } + table +} + +#[inline] +fn compare_parametric(src: &[f32], dst: &[f32]) -> bool { + for (src, dst) in src.iter().zip(dst.iter()) { + if (src - dst).abs() > 1e-4 { + return false; + } + } + true +} + +fn lut_inverse_interp16(value: u16, lut_table: &[u16]) -> u16 { + let mut l: i32 = 1; // 'int' Give spacing for negative values + let mut r: i32 = 0x10000; + let mut x: i32 = 0; + let mut res: i32; + let length = lut_table.len() as i32; + + let mut num_zeroes: i32 = 0; + for &item in lut_table.iter() { + if item == 0 { + num_zeroes += 1 + } else { + break; + } + } + + if num_zeroes == 0 && value as i32 == 0 { + return 0u16; + } + let mut num_of_polys: i32 = 0; + for &item in lut_table.iter().rev() { + if item == 0xffff { + num_of_polys += 1 + } else { + break; + } + } + // Does the curve belong to this case? + if num_zeroes > 1 || num_of_polys > 1 { + let a_0: i32; + let b_0: i32; + // Identify if value fall downto 0 or FFFF zone + if value as i32 == 0 { + return 0u16; + } + // if (Value == 0xFFFF) return 0xFFFF; + // else restrict to valid zone + if num_zeroes > 1 { + a_0 = (num_zeroes - 1) * 0xffff / (length - 1); + l = a_0 - 1 + } + if num_of_polys > 1 { + b_0 = (length - 1 - num_of_polys) * 0xffff / (length - 1); + r = b_0 + 1 + } + } + if r <= l { + // If this happens LutTable is not invertible + return 0u16; + } + + while r > l { + x = (l + r) / 2; + res = lut_interp_linear16((x - 1) as u16, lut_table) as i32; + if res == value as i32 { + // Found exact match. + return (x - 1) as u16; + } + if res > value as i32 { + r = x - 1 + } else { + l = x + 1 + } + } + + // Not found, should we interpolate? + + // Get surrounding nodes + debug_assert!(x >= 1); + + let val2: f64 = (length - 1) as f64 * ((x - 1) as f64 / 65535.0); + let cell0: i32 = val2.floor() as i32; + let cell1: i32 = val2.ceil() as i32; + if cell0 == cell1 { + return x as u16; + } + + let y0: f64 = lut_table[cell0 as usize] as f64; + let x0: f64 = 65535.0 * cell0 as f64 / (length - 1) as f64; + let y1: f64 = lut_table[cell1 as usize] as f64; + let x1: f64 = 65535.0 * cell1 as f64 / (length - 1) as f64; + let a: f64 = (y1 - y0) / (x1 - x0); + let b: f64 = mlaf(y0, -a, x0); + if a.abs() < 0.01f64 { + return x as u16; + } + let f: f64 = (value as i32 as f64 - b) / a; + if f < 0.0 { + return 0u16; + } + if f >= 65535.0 { + return 0xffffu16; + } + (f + 0.5f64).floor() as u16 +} + +fn lut_inverse_interp16_boxed(value: u16, lut_table: &[u16; N]) -> u16 { + let mut l: i32 = 1; // 'int' Give spacing for negative values + let mut r: i32 = 0x10000; + let mut x: i32 = 0; + let mut res: i32; + let length = lut_table.len() as i32; + + let mut num_zeroes: i32 = 0; + for &item in lut_table.iter() { + if item == 0 { + num_zeroes += 1 + } else { + break; + } + } + + if num_zeroes == 0 && value as i32 == 0 { + return 0u16; + } + let mut num_of_polys: i32 = 0; + for &item in lut_table.iter().rev() { + if item == 0xffff { + num_of_polys += 1 + } else { + break; + } + } + // Does the curve belong to this case? + if num_zeroes > 1 || num_of_polys > 1 { + let a_0: i32; + let b_0: i32; + // Identify if value fall downto 0 or FFFF zone + if value as i32 == 0 { + return 0u16; + } + // if (Value == 0xFFFF) return 0xFFFF; + // else restrict to valid zone + if num_zeroes > 1 { + a_0 = (num_zeroes - 1) * 0xffff / (length - 1); + l = a_0 - 1 + } + if num_of_polys > 1 { + b_0 = (length - 1 - num_of_polys) * 0xffff / (length - 1); + r = b_0 + 1 + } + } + if r <= l { + // If this happens LutTable is not invertible + return 0u16; + } + + while r > l { + x = (l + r) / 2; + res = lut_interp_linear16_boxed((x - 1) as u16, lut_table) as i32; + if res == value as i32 { + // Found exact match. + return (x - 1) as u16; + } + if res > value as i32 { + r = x - 1 + } else { + l = x + 1 + } + } + + // Not found, should we interpolate? + + // Get surrounding nodes + debug_assert!(x >= 1); + + let val2: f64 = (length - 1) as f64 * ((x - 1) as f64 / 65535.0); + let cell0: i32 = val2.floor() as i32; + let cell1: i32 = val2.ceil() as i32; + if cell0 == cell1 { + return x as u16; + } + + let y0: f64 = lut_table[cell0 as usize] as f64; + let x0: f64 = 65535.0 * cell0 as f64 / (length - 1) as f64; + let y1: f64 = lut_table[cell1 as usize] as f64; + let x1: f64 = 65535.0 * cell1 as f64 / (length - 1) as f64; + let a: f64 = (y1 - y0) / (x1 - x0); + let b: f64 = mlaf(y0, -a, x0); + if a.abs() < 0.01f64 { + return x as u16; + } + let f: f64 = (value as i32 as f64 - b) / a; + if f < 0.0 { + return 0u16; + } + if f >= 65535.0 { + return 0xffffu16; + } + (f + 0.5f64).floor() as u16 +} + +fn invert_lut(table: &[u16], out_length: usize) -> Vec { + // For now, we invert the lut by creating a lut of size out_length + // and attempting to look up a value for each entry using lut_inverse_interp16 + let mut output = vec![0u16; out_length]; + let scale_value = 65535f64 / (out_length - 1) as f64; + for (i, out) in output.iter_mut().enumerate() { + let x: f64 = i as f64 * scale_value; + let input: u16 = (x + 0.5f64).floor() as u16; + *out = lut_inverse_interp16(input, table); + } + output +} + +fn invert_lut_boxed(table: &[u16; N], out_length: usize) -> Vec { + // For now, we invert the lut by creating a lut of size out_length + // and attempting to look up a value for each entry using lut_inverse_interp16 + let mut output = vec![0u16; out_length]; + let scale_value = 65535f64 / (out_length - 1) as f64; + for (i, out) in output.iter_mut().enumerate() { + let x: f64 = i as f64 * scale_value; + let input: u16 = (x + 0.5f64).floor() as u16; + *out = lut_inverse_interp16_boxed(input, table); + } + output +} + +impl ToneReprCurve { + pub(crate) fn to_clut(&self) -> Result, CmsError> { + match self { + ToneReprCurve::Lut(lut) => { + if lut.is_empty() { + let passthrough_table = passthrough_table::(); + Ok(passthrough_table.to_vec()) + } else { + Ok(lut + .iter() + .map(|&x| x as f32 * (1. / 65535.)) + .collect::>()) + } + } + ToneReprCurve::Parametric(_) => { + let curve = self + .build_linearize_table::() + .ok_or(CmsError::InvalidTrcCurve)?; + let max_value = f32::NOT_FINITE_LINEAR_TABLE_SIZE - 1; + let sliced = &curve[..max_value]; + Ok(sliced.to_vec()) + } + } + } + + pub(crate) fn build_linearize_table< + T: PointeeSizeExpressible, + const N: usize, + const BIT_DEPTH: usize, + >( + &self, + ) -> Option> { + match self { + ToneReprCurve::Parametric(params) => linear_curve_parametric::(params), + ToneReprCurve::Lut(data) => match data.len() { + 0 => Some(passthrough_table::()), + 1 => Some(linear_forward_table::(data[0])), + _ => Some(linear_lut_interpolate::(data)), + }, + } + } + + pub(crate) fn build_gamma_table< + T: Default + Copy + 'static + PointeeSizeExpressible + GammaLutInterpolate, + const BUCKET: usize, + const N: usize, + const BIT_DEPTH: usize, + >( + &self, + ) -> Option> + where + f32: AsPrimitive, + u32: AsPrimitive, + { + match self { + ToneReprCurve::Parametric(params) => { + if params.len() == 5 { + let srgb_params = vec![2.4, 1. / 1.055, 0.055 / 1.055, 1. / 12.92, 0.04045]; + let rec709_params = create_rec709_parametric(); + + let mut lc_params: [f32; 5] = [0.; 5]; + for (dst, src) in lc_params.iter_mut().zip(params.iter()) { + *dst = *src; + } + + if compare_parametric(lc_params.as_slice(), srgb_params.as_slice()) { + return Some( + TransferCharacteristics::Srgb + .make_gamma_table::(BIT_DEPTH), + ); + } + + if compare_parametric(lc_params.as_slice(), rec709_params.as_slice()) { + return Some( + TransferCharacteristics::Bt709 + .make_gamma_table::(BIT_DEPTH), + ); + } + } + + let parametric_curve = ParametricCurve::new(params); + if let Some(v) = parametric_curve? + .invert() + .map(|x| make_gamma_parametric_table::(x)) + { + return Some(v); + } + + let mut gamma_table_uint = Box::new([0; N]); + + let inverted_size: usize = N; + let gamma_table = linear_curve_parametric_s::(params)?; + for (&src, dst) in gamma_table.iter().zip(gamma_table_uint.iter_mut()) { + *dst = (src * 65535f32) as u16; + } + let inverted = invert_lut_boxed(&gamma_table_uint, inverted_size); + Some(make_gamma_lut::(&inverted)) + } + ToneReprCurve::Lut(data) => match data.len() { + 0 => Some(make_gamma_linear_table::(BIT_DEPTH)), + 1 => Some(make_gamma_pow_table::( + 1. / u8_fixed_8number_to_float(data[0]), + BIT_DEPTH, + )), + _ => { + let mut inverted_size = data.len(); + if inverted_size < 256 { + inverted_size = 256 + } + let inverted = invert_lut(data, inverted_size); + Some(make_gamma_lut::(&inverted)) + } + }, + } + } +} + +impl ColorProfile { + /// Produces LUT for 8 bit tone linearization + pub fn build_8bit_lin_table( + &self, + trc: &Option, + ) -> Result, CmsError> { + trc.as_ref() + .and_then(|trc| trc.build_linearize_table::()) + .ok_or(CmsError::BuildTransferFunction) + } + + /// Produces LUT for Gray transfer curve with N depth + pub fn build_gray_linearize_table< + T: PointeeSizeExpressible, + const N: usize, + const BIT_DEPTH: usize, + >( + &self, + ) -> Result, CmsError> { + self.gray_trc + .as_ref() + .and_then(|trc| trc.build_linearize_table::()) + .ok_or(CmsError::BuildTransferFunction) + } + + /// Produces LUT for Red transfer curve with N depth + pub fn build_r_linearize_table< + T: PointeeSizeExpressible, + const N: usize, + const BIT_DEPTH: usize, + >( + &self, + use_cicp: bool, + ) -> Result, CmsError> { + if use_cicp { + if let Some(tc) = self.cicp.as_ref().map(|c| c.transfer_characteristics) { + if tc.has_transfer_curve() { + return Ok(tc.make_linear_table::()); + } + } + } + self.red_trc + .as_ref() + .and_then(|trc| trc.build_linearize_table::()) + .ok_or(CmsError::BuildTransferFunction) + } + + /// Produces LUT for Green transfer curve with N depth + pub fn build_g_linearize_table< + T: PointeeSizeExpressible, + const N: usize, + const BIT_DEPTH: usize, + >( + &self, + use_cicp: bool, + ) -> Result, CmsError> { + if use_cicp { + if let Some(tc) = self.cicp.as_ref().map(|c| c.transfer_characteristics) { + if tc.has_transfer_curve() { + return Ok(tc.make_linear_table::()); + } + } + } + self.green_trc + .as_ref() + .and_then(|trc| trc.build_linearize_table::()) + .ok_or(CmsError::BuildTransferFunction) + } + + /// Produces LUT for Blue transfer curve with N depth + pub fn build_b_linearize_table< + T: PointeeSizeExpressible, + const N: usize, + const BIT_DEPTH: usize, + >( + &self, + use_cicp: bool, + ) -> Result, CmsError> { + if use_cicp { + if let Some(tc) = self.cicp.as_ref().map(|c| c.transfer_characteristics) { + if tc.has_transfer_curve() { + return Ok(tc.make_linear_table::()); + } + } + } + self.blue_trc + .as_ref() + .and_then(|trc| trc.build_linearize_table::()) + .ok_or(CmsError::BuildTransferFunction) + } + + /// Build gamma table for 8 bit depth + /// Only 4092 first bins are used and values scaled in 0..255 + pub fn build_8bit_gamma_table( + &self, + trc: &Option, + use_cicp: bool, + ) -> Result, CmsError> { + self.build_gamma_table::(trc, use_cicp) + } + + /// Build gamma table for 10 bit depth + /// Only 8192 first bins are used and values scaled in 0..1023 + pub fn build_10bit_gamma_table( + &self, + trc: &Option, + use_cicp: bool, + ) -> Result, CmsError> { + self.build_gamma_table::(trc, use_cicp) + } + + /// Build gamma table for 12 bit depth + /// Only 16384 first bins are used and values scaled in 0..4095 + pub fn build_12bit_gamma_table( + &self, + trc: &Option, + use_cicp: bool, + ) -> Result, CmsError> { + self.build_gamma_table::(trc, use_cicp) + } + + /// Build gamma table for 16 bit depth + /// Only 16384 first bins are used and values scaled in 0..65535 + pub fn build_16bit_gamma_table( + &self, + trc: &Option, + use_cicp: bool, + ) -> Result, CmsError> { + self.build_gamma_table::(trc, use_cicp) + } + + /// Builds gamma table checking CICP for Transfer characteristics first. + pub fn build_gamma_table< + T: Default + Copy + 'static + PointeeSizeExpressible + GammaLutInterpolate, + const BUCKET: usize, + const N: usize, + const BIT_DEPTH: usize, + >( + &self, + trc: &Option, + use_cicp: bool, + ) -> Result, CmsError> + where + f32: AsPrimitive, + u32: AsPrimitive, + { + if use_cicp { + if let Some(tc) = self.cicp.as_ref().map(|c| c.transfer_characteristics) { + if tc.has_transfer_curve() { + return Ok(tc.make_gamma_table::(BIT_DEPTH)); + } + } + } + trc.as_ref() + .and_then(|trc| trc.build_gamma_table::()) + .ok_or(CmsError::BuildTransferFunction) + } + + /// Checks if profile gamma can work in extended precision and we have implementation for this + pub(crate) fn try_extended_gamma_evaluator( + &self, + ) -> Option> { + if let Some(tc) = self.cicp.as_ref().map(|c| c.transfer_characteristics) { + if tc.has_transfer_curve() { + return Some(Box::new(ToneCurveCicpEvaluator { + rgb_trc: tc.extended_gamma_tristimulus(), + trc: tc.extended_gamma_single(), + })); + } + } + if !self.are_all_trc_the_same() { + return None; + } + let reference_trc = if self.color_space == DataColorSpace::Gray { + self.gray_trc.as_ref() + } else { + self.red_trc.as_ref() + }; + if let Some(red_trc) = reference_trc { + return Self::make_gamma_evaluator_all_the_same(red_trc); + } + None + } + + fn make_gamma_evaluator_all_the_same( + red_trc: &ToneReprCurve, + ) -> Option> { + match red_trc { + ToneReprCurve::Lut(lut) => { + if lut.is_empty() { + return Some(Box::new(ToneCurveEvaluatorLinear {})); + } + if lut.len() == 1 { + let gamma = 1. / u8_fixed_8number_to_float(lut[0]); + return Some(Box::new(ToneCurveEvaluatorPureGamma { gamma })); + } + None + } + ToneReprCurve::Parametric(params) => { + if params.len() == 5 { + let srgb_params = vec![2.4, 1. / 1.055, 0.055 / 1.055, 1. / 12.92, 0.04045]; + let rec709_params = create_rec709_parametric(); + + let mut lc_params: [f32; 5] = [0.; 5]; + for (dst, src) in lc_params.iter_mut().zip(params.iter()) { + *dst = *src; + } + + if compare_parametric(lc_params.as_slice(), srgb_params.as_slice()) { + return Some(Box::new(ToneCurveCicpEvaluator { + rgb_trc: TransferCharacteristics::Srgb.extended_gamma_tristimulus(), + trc: TransferCharacteristics::Srgb.extended_gamma_single(), + })); + } + + if compare_parametric(lc_params.as_slice(), rec709_params.as_slice()) { + return Some(Box::new(ToneCurveCicpEvaluator { + rgb_trc: TransferCharacteristics::Bt709.extended_gamma_tristimulus(), + trc: TransferCharacteristics::Bt709.extended_gamma_single(), + })); + } + } + + let parametric_curve = ParametricCurve::new(params); + if let Some(v) = parametric_curve?.invert() { + return Some(Box::new(ToneCurveParametricEvaluator { parametric: v })); + } + None + } + } + } + + /// Check if all TRC are the same + pub(crate) fn are_all_trc_the_same(&self) -> bool { + if self.color_space == DataColorSpace::Gray { + return true; + } + if let (Some(red_trc), Some(green_trc), Some(blue_trc)) = + (&self.red_trc, &self.green_trc, &self.blue_trc) + { + if !matches!( + (red_trc, green_trc, blue_trc), + ( + ToneReprCurve::Lut(_), + ToneReprCurve::Lut(_), + ToneReprCurve::Lut(_), + ) | ( + ToneReprCurve::Parametric(_), + ToneReprCurve::Parametric(_), + ToneReprCurve::Parametric(_) + ) + ) { + return false; + } + if let (ToneReprCurve::Lut(lut0), ToneReprCurve::Lut(lut1), ToneReprCurve::Lut(lut2)) = + (red_trc, green_trc, blue_trc) + { + if lut0 == lut1 || lut1 == lut2 { + return true; + } + } + if let ( + ToneReprCurve::Parametric(lut0), + ToneReprCurve::Parametric(lut1), + ToneReprCurve::Parametric(lut2), + ) = (red_trc, green_trc, blue_trc) + { + if lut0 == lut1 || lut1 == lut2 { + return true; + } + } + } + false + } + + /// Checks if profile is matrix shaper, have same TRC and TRC is linear. + pub(crate) fn is_linear_matrix_shaper(&self) -> bool { + if !self.is_matrix_shaper() { + return false; + } + if !self.are_all_trc_the_same() { + return false; + } + if let Some(red_trc) = &self.red_trc { + return match red_trc { + ToneReprCurve::Lut(lut) => { + if lut.is_empty() { + return true; + } + if is_curve_linear16(lut) { + return true; + } + false + } + ToneReprCurve::Parametric(params) => { + if let Some(curve) = ParametricCurve::new(params) { + return curve.is_linear(); + } + false + } + }; + } + false + } + + /// Checks if profile linearization can work in extended precision and we have implementation for this + pub(crate) fn try_extended_linearizing_evaluator( + &self, + ) -> Option> { + if let Some(tc) = self.cicp.as_ref().map(|c| c.transfer_characteristics) { + if tc.has_transfer_curve() { + return Some(Box::new(ToneCurveCicpEvaluator { + rgb_trc: tc.extended_linear_tristimulus(), + trc: tc.extended_linear_single(), + })); + } + } + if !self.are_all_trc_the_same() { + return None; + } + let reference_trc = if self.color_space == DataColorSpace::Gray { + self.gray_trc.as_ref() + } else { + self.red_trc.as_ref() + }; + if let Some(red_trc) = reference_trc { + if let Some(value) = Self::make_linear_curve_evaluator_all_the_same(red_trc) { + return value; + } + } + None + } + + fn make_linear_curve_evaluator_all_the_same( + evaluator_curve: &ToneReprCurve, + ) -> Option>> { + match evaluator_curve { + ToneReprCurve::Lut(lut) => { + if lut.is_empty() { + return Some(Some(Box::new(ToneCurveEvaluatorLinear {}))); + } + if lut.len() == 1 { + let gamma = u8_fixed_8number_to_float(lut[0]); + return Some(Some(Box::new(ToneCurveEvaluatorPureGamma { gamma }))); + } + } + ToneReprCurve::Parametric(params) => { + if params.len() == 5 { + let srgb_params = vec![2.4, 1. / 1.055, 0.055 / 1.055, 1. / 12.92, 0.04045]; + let rec709_params = create_rec709_parametric(); + + let mut lc_params: [f32; 5] = [0.; 5]; + for (dst, src) in lc_params.iter_mut().zip(params.iter()) { + *dst = *src; + } + + if compare_parametric(lc_params.as_slice(), srgb_params.as_slice()) { + return Some(Some(Box::new(ToneCurveCicpEvaluator { + rgb_trc: TransferCharacteristics::Srgb.extended_linear_tristimulus(), + trc: TransferCharacteristics::Srgb.extended_linear_single(), + }))); + } + + if compare_parametric(lc_params.as_slice(), rec709_params.as_slice()) { + return Some(Some(Box::new(ToneCurveCicpEvaluator { + rgb_trc: TransferCharacteristics::Bt709.extended_linear_tristimulus(), + trc: TransferCharacteristics::Bt709.extended_linear_single(), + }))); + } + } + + let parametric_curve = ParametricCurve::new(params); + if let Some(v) = parametric_curve { + return Some(Some(Box::new(ToneCurveParametricEvaluator { + parametric: v, + }))); + } + } + } + None + } +} + +pub(crate) struct ToneCurveCicpEvaluator { + rgb_trc: fn(Rgb) -> Rgb, + trc: fn(f32) -> f32, +} + +pub(crate) struct ToneCurveParametricEvaluator { + parametric: ParametricCurve, +} + +pub(crate) struct ToneCurveEvaluatorPureGamma { + gamma: f32, +} + +pub(crate) struct ToneCurveEvaluatorLinear {} + +impl ToneCurveEvaluator for ToneCurveCicpEvaluator { + fn evaluate_tristimulus(&self, rgb: Rgb) -> Rgb { + (self.rgb_trc)(rgb) + } + + fn evaluate_value(&self, value: f32) -> f32 { + (self.trc)(value) + } +} + +impl ToneCurveEvaluator for ToneCurveParametricEvaluator { + fn evaluate_tristimulus(&self, rgb: Rgb) -> Rgb { + Rgb::new( + self.parametric.eval(rgb.r), + self.parametric.eval(rgb.g), + self.parametric.eval(rgb.b), + ) + } + + fn evaluate_value(&self, value: f32) -> f32 { + self.parametric.eval(value) + } +} + +impl ToneCurveEvaluator for ToneCurveEvaluatorPureGamma { + fn evaluate_tristimulus(&self, rgb: Rgb) -> Rgb { + Rgb::new( + dirty_powf(rgb.r, self.gamma), + dirty_powf(rgb.g, self.gamma), + dirty_powf(rgb.b, self.gamma), + ) + } + + fn evaluate_value(&self, value: f32) -> f32 { + dirty_powf(value, self.gamma) + } +} + +impl ToneCurveEvaluator for ToneCurveEvaluatorLinear { + fn evaluate_tristimulus(&self, rgb: Rgb) -> Rgb { + rgb + } + + fn evaluate_value(&self, value: f32) -> f32 { + value + } +} + +pub trait ToneCurveEvaluator { + fn evaluate_tristimulus(&self, rgb: Rgb) -> Rgb; + fn evaluate_value(&self, value: f32) -> f32; +} diff --git a/deps/moxcms/src/writer.rs b/deps/moxcms/src/writer.rs new file mode 100644 index 0000000..9ecce26 --- /dev/null +++ b/deps/moxcms/src/writer.rs @@ -0,0 +1,889 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::profile::{LutDataType, ProfileHeader}; +use crate::tag::{TAG_SIZE, Tag, TagTypeDefinition}; +use crate::trc::ToneReprCurve; +use crate::{ + CicpProfile, CmsError, ColorDateTime, ColorProfile, DataColorSpace, LocalizableString, + LutMultidimensionalType, LutStore, LutType, LutWarehouse, Matrix3d, ProfileClass, + ProfileSignature, ProfileText, ProfileVersion, Vector3d, Xyzd, +}; + +pub(crate) trait FloatToFixedS15Fixed16 { + fn to_s15_fixed16(self) -> i32; +} + +pub(crate) trait FloatToFixedU8Fixed8 { + fn to_u8_fixed8(self) -> u16; +} + +// pub(crate) trait FloatToFixedU16 { +// fn to_fixed_u16(self) -> u16; +// } + +// impl FloatToFixedU16 for f32 { +// #[inline] +// fn to_fixed_u16(self) -> u16 { +// const SCALE: f64 = (1 << 16) as f64; +// (self as f64 * SCALE + 0.5) +// .floor() +// .clamp(u16::MIN as f64, u16::MAX as f64) as u16 +// } +// } + +impl FloatToFixedS15Fixed16 for f32 { + #[inline] + fn to_s15_fixed16(self) -> i32 { + const SCALE: f64 = (1 << 16) as f64; + (self as f64 * SCALE + 0.5) + .floor() + .clamp(i32::MIN as f64, i32::MAX as f64) as i32 + } +} + +impl FloatToFixedS15Fixed16 for f64 { + #[inline] + fn to_s15_fixed16(self) -> i32 { + const SCALE: f64 = (1 << 16) as f64; + (self * SCALE + 0.5) + .floor() + .clamp(i32::MIN as f64, i32::MAX as f64) as i32 + } +} + +#[inline] +fn write_u32_be(into: &mut Vec, value: u32) { + let bytes = value.to_be_bytes(); + into.push(bytes[0]); + into.push(bytes[1]); + into.push(bytes[2]); + into.push(bytes[3]); +} + +#[inline] +pub(crate) fn write_u16_be(into: &mut Vec, value: u16) { + let bytes = value.to_be_bytes(); + into.push(bytes[0]); + into.push(bytes[1]); +} + +#[inline] +fn write_i32_be(into: &mut Vec, value: i32) { + let bytes = value.to_be_bytes(); + into.push(bytes[0]); + into.push(bytes[1]); + into.push(bytes[2]); + into.push(bytes[3]); +} + +fn first_two_ascii_bytes(s: &String) -> [u8; 2] { + let bytes = s.as_bytes(); + if bytes.len() >= 2 { + bytes[0..2].try_into().unwrap() + } else if bytes.len() == 1 { + let vec = vec![bytes[0], 0u8]; + vec.try_into().unwrap() + } else { + let vec = vec![0u8, 0u8]; + vec.try_into().unwrap() + } +} + +/// Writes Multi Localized Unicode +#[inline] +fn write_mluc(into: &mut Vec, strings: &[LocalizableString]) -> usize { + assert!(!strings.is_empty()); + let start = into.len(); + let tag_def: u32 = TagTypeDefinition::MultiLocalizedUnicode.into(); + write_u32_be(into, tag_def); + write_u32_be(into, 0); + let number_of_records = strings.len(); + write_u32_be(into, number_of_records as u32); + write_u32_be(into, 12); // Record size, must be 12 + let lang = first_two_ascii_bytes(&strings[0].language); + into.extend_from_slice(&lang); + let country = first_two_ascii_bytes(&strings[0].country); + into.extend_from_slice(&country); + let first_string_len = strings[0].value.len() * 2; + write_u32_be(into, first_string_len as u32); + let mut first_string_offset = 16 + 12 * strings.len(); + write_u32_be(into, first_string_offset as u32); + first_string_offset += first_string_len; + for record in strings.iter().skip(1) { + let lang = first_two_ascii_bytes(&record.language); + into.extend_from_slice(&lang); + let country = first_two_ascii_bytes(&record.country); + into.extend_from_slice(&country); + let first_string_len = record.value.len() * 2; + write_u32_be(into, first_string_len as u32); + write_u32_be(into, first_string_offset as u32); + first_string_offset += first_string_len; + } + for record in strings.iter() { + for chunk in record.value.encode_utf16() { + write_u16_be(into, chunk); + } + } + let end = into.len(); + end - start +} + +#[inline] +fn write_string_value(into: &mut Vec, text: &ProfileText) -> usize { + match text { + ProfileText::PlainString(text) => { + let vec = vec![LocalizableString { + language: "en".to_string(), + country: "US".to_string(), + value: text.clone(), + }]; + write_mluc(into, &vec) + } + ProfileText::Localizable(localizable) => { + if localizable.is_empty() { + return 0; + } + write_mluc(into, localizable) + } + ProfileText::Description(description) => { + let vec = vec![LocalizableString { + language: "en".to_string(), + country: "US".to_string(), + value: description.unicode_string.clone(), + }]; + write_mluc(into, &vec) + } + } +} + +#[inline] +fn write_xyz_tag_value(into: &mut Vec, xyz: Xyzd) { + let tag_definition: u32 = TagTypeDefinition::Xyz.into(); + write_u32_be(into, tag_definition); + write_u32_be(into, 0); + let x_fixed = xyz.x.to_s15_fixed16(); + write_i32_be(into, x_fixed); + let y_fixed = xyz.y.to_s15_fixed16(); + write_i32_be(into, y_fixed); + let z_fixed = xyz.z.to_s15_fixed16(); + write_i32_be(into, z_fixed); +} + +#[inline] +fn write_tag_entry(into: &mut Vec, tag: Tag, tag_entry: usize, tag_size: usize) { + let tag_value: u32 = tag.into(); + write_u32_be(into, tag_value); + write_u32_be(into, tag_entry as u32); + write_u32_be(into, tag_size as u32); +} + +fn write_trc_entry(into: &mut Vec, trc: &ToneReprCurve) -> Result { + match trc { + ToneReprCurve::Lut(lut) => { + let curv: u32 = TagTypeDefinition::LutToneCurve.into(); + write_u32_be(into, curv); + write_u32_be(into, 0); + write_u32_be(into, lut.len() as u32); + for item in lut.iter() { + write_u16_be(into, *item); + } + Ok(12 + lut.len() * 2) + } + ToneReprCurve::Parametric(parametric_curve) => { + if parametric_curve.len() > 7 + || parametric_curve.len() == 6 + || parametric_curve.len() == 2 + { + return Err(CmsError::InvalidProfile); + } + let para: u32 = TagTypeDefinition::ParametricToneCurve.into(); + write_u32_be(into, para); + write_u32_be(into, 0); + if parametric_curve.len() == 1 { + write_u16_be(into, 0); + } else if parametric_curve.len() == 3 { + write_u16_be(into, 1); + } else if parametric_curve.len() == 4 { + write_u16_be(into, 2); + } else if parametric_curve.len() == 5 { + write_u16_be(into, 3); + } else if parametric_curve.len() == 7 { + write_u16_be(into, 4); + } + write_u16_be(into, 0); + for item in parametric_curve.iter() { + write_i32_be(into, item.to_s15_fixed16()); + } + Ok(12 + 4 * parametric_curve.len()) + } + } +} + +#[inline] +fn write_cicp_entry(into: &mut Vec, cicp: &CicpProfile) { + let cicp_tag: u32 = TagTypeDefinition::Cicp.into(); + write_u32_be(into, cicp_tag); + write_u32_be(into, 0); + into.push(cicp.color_primaries as u8); + into.push(cicp.transfer_characteristics as u8); + into.push(cicp.matrix_coefficients as u8); + into.push(if cicp.full_range { 1 } else { 0 }); +} + +fn write_chad(into: &mut Vec, matrix: Matrix3d) { + let arr_type: u32 = TagTypeDefinition::S15Fixed16Array.into(); + write_u32_be(into, arr_type); + write_u32_be(into, 0); + write_matrix3d(into, matrix); +} + +#[inline] +fn write_matrix3d(into: &mut Vec, v: Matrix3d) { + write_i32_be(into, v.v[0][0].to_s15_fixed16()); + write_i32_be(into, v.v[0][1].to_s15_fixed16()); + write_i32_be(into, v.v[0][2].to_s15_fixed16()); + + write_i32_be(into, v.v[1][0].to_s15_fixed16()); + write_i32_be(into, v.v[1][1].to_s15_fixed16()); + write_i32_be(into, v.v[1][2].to_s15_fixed16()); + + write_i32_be(into, v.v[2][0].to_s15_fixed16()); + write_i32_be(into, v.v[2][1].to_s15_fixed16()); + write_i32_be(into, v.v[2][2].to_s15_fixed16()); +} + +#[inline] +fn write_vector3d(into: &mut Vec, v: Vector3d) { + write_i32_be(into, v.v[0].to_s15_fixed16()); + write_i32_be(into, v.v[1].to_s15_fixed16()); + write_i32_be(into, v.v[2].to_s15_fixed16()); +} + +#[inline] +fn write_lut_entry(into: &mut Vec, lut: &LutDataType) -> Result { + if !lut.has_same_kind() { + return Err(CmsError::InvalidProfile); + } + let start = into.len(); + let lut16_tag: u32 = match &lut.input_table { + LutStore::Store8(_) => LutType::Lut8.into(), + LutStore::Store16(_) => LutType::Lut16.into(), + }; + write_u32_be(into, lut16_tag); + write_u32_be(into, 0); + into.push(lut.num_input_channels); + into.push(lut.num_output_channels); + into.push(lut.num_clut_grid_points); + into.push(0); + write_matrix3d(into, lut.matrix); + write_u16_be(into, lut.num_input_table_entries); + write_u16_be(into, lut.num_output_table_entries); + match &lut.input_table { + LutStore::Store8(input_table) => { + for &item in input_table.iter() { + into.push(item); + } + } + LutStore::Store16(input_table) => { + for &item in input_table.iter() { + write_u16_be(into, item); + } + } + } + match &lut.clut_table { + LutStore::Store8(input_table) => { + for &item in input_table.iter() { + into.push(item); + } + } + LutStore::Store16(input_table) => { + for &item in input_table.iter() { + write_u16_be(into, item); + } + } + } + match &lut.output_table { + LutStore::Store8(input_table) => { + for &item in input_table.iter() { + into.push(item); + } + } + LutStore::Store16(input_table) => { + for &item in input_table.iter() { + write_u16_be(into, item); + } + } + } + let end = into.len(); + Ok(end - start) +} + +#[inline] +fn write_mab_entry( + into: &mut Vec, + lut: &LutMultidimensionalType, + is_a_to_b: bool, +) -> Result { + let start = into.len(); + let lut16_tag: u32 = if is_a_to_b { + LutType::LutMab.into() + } else { + LutType::LutMba.into() + }; + write_u32_be(into, lut16_tag); + write_u32_be(into, 0); + into.push(lut.num_input_channels); + into.push(lut.num_output_channels); + write_u16_be(into, 0); + let mut working_offset = 32usize; + + let mut data = Vec::new(); + + // Offset to "B curves" + if !lut.b_curves.is_empty() { + while working_offset % 4 != 0 { + data.push(0); + working_offset += 1; + } + + write_u32_be(into, working_offset as u32); + + for trc in lut.b_curves.iter() { + let curve_size = write_trc_entry(&mut data, trc)?; + working_offset += curve_size; + while working_offset % 4 != 0 { + data.push(0); + working_offset += 1; + } + } + } else { + write_u32_be(into, 0); + } + + // Offset to matrix + if !lut.m_curves.is_empty() { + while working_offset % 4 != 0 { + data.push(0); + working_offset += 1; + } + + write_u32_be(into, working_offset as u32); + write_matrix3d(&mut data, lut.matrix); + write_vector3d(&mut data, lut.bias); + working_offset += 9 * 4 + 3 * 4; + // Offset to "M curves" + write_u32_be(into, working_offset as u32); + for trc in lut.m_curves.iter() { + let curve_size = write_trc_entry(&mut data, trc)?; + working_offset += curve_size; + while working_offset % 4 != 0 { + data.push(0); + working_offset += 1; + } + } + } else { + // Offset to matrix + write_u32_be(into, 0); + // Offset to "M curves" + write_u32_be(into, 0); + } + + let mut clut_start = data.len(); + + // Offset to CLUT + if let Some(clut) = &lut.clut { + while working_offset % 4 != 0 { + data.push(0); + working_offset += 1; + } + + clut_start = data.len(); + + write_u32_be(into, working_offset as u32); + + // Writing CLUT + for &pt in lut.grid_points.iter() { + data.push(pt); + } + data.push(match clut { + LutStore::Store8(_) => 1, + LutStore::Store16(_) => 2, + }); // Entry size + data.push(0); + data.push(0); + data.push(0); + match clut { + LutStore::Store8(store) => { + for &element in store.iter() { + data.push(element) + } + } + LutStore::Store16(store) => { + for &element in store.iter() { + write_u16_be(&mut data, element); + } + } + } + } else { + write_u32_be(into, 0); + } + + let clut_size = data.len() - clut_start; + working_offset += clut_size; + + // Offset to "A curves" + if !lut.a_curves.is_empty() { + while working_offset % 4 != 0 { + data.push(0); + working_offset += 1; + } + + write_u32_be(into, working_offset as u32); + + for trc in lut.a_curves.iter() { + let curve_size = write_trc_entry(&mut data, trc)?; + working_offset += curve_size; + while working_offset % 4 != 0 { + data.push(0); + working_offset += 1; + } + } + } else { + write_u32_be(into, 0); + } + + into.extend(data); + + let end = into.len(); + Ok(end - start) +} + +fn write_lut(into: &mut Vec, lut: &LutWarehouse, is_a_to_b: bool) -> Result { + match lut { + LutWarehouse::Lut(lut) => Ok(write_lut_entry(into, lut)?), + LutWarehouse::Multidimensional(mab) => write_mab_entry(into, mab, is_a_to_b), + } +} + +impl ProfileHeader { + fn encode(&self) -> Vec { + let mut encoder: Vec = Vec::with_capacity(size_of::()); + write_u32_be(&mut encoder, self.size); // Size + write_u32_be(&mut encoder, 0); // CMM Type + write_u32_be(&mut encoder, self.version.into()); // Version Number Type + write_u32_be(&mut encoder, self.profile_class.into()); // Profile class + write_u32_be(&mut encoder, self.data_color_space.into()); // Data color space + write_u32_be(&mut encoder, self.pcs.into()); // PCS + self.creation_date_time.encode(&mut encoder); // Date time + write_u32_be(&mut encoder, self.signature.into()); // Profile signature + write_u32_be(&mut encoder, self.platform); + write_u32_be(&mut encoder, self.flags); + write_u32_be(&mut encoder, self.device_manufacturer); + write_u32_be(&mut encoder, self.device_model); + for &i in self.device_attributes.iter() { + encoder.push(i); + } + write_u32_be(&mut encoder, self.rendering_intent.into()); + write_i32_be(&mut encoder, self.illuminant.x.to_s15_fixed16()); + write_i32_be(&mut encoder, self.illuminant.y.to_s15_fixed16()); + write_i32_be(&mut encoder, self.illuminant.z.to_s15_fixed16()); + write_u32_be(&mut encoder, self.creator); + for &i in self.profile_id.iter() { + encoder.push(i); + } + for &i in self.reserved.iter() { + encoder.push(i); + } + write_u32_be(&mut encoder, self.tag_count); + encoder + } +} + +impl ColorProfile { + fn writable_tags_count(&self) -> usize { + let mut tags_count = 0usize; + if self.red_colorant != Xyzd::default() { + tags_count += 1; + } + if self.green_colorant != Xyzd::default() { + tags_count += 1; + } + if self.blue_colorant != Xyzd::default() { + tags_count += 1; + } + if self.red_trc.is_some() { + tags_count += 1; + } + if self.green_trc.is_some() { + tags_count += 1; + } + if self.blue_trc.is_some() { + tags_count += 1; + } + if self.gray_trc.is_some() { + tags_count += 1; + } + if self.cicp.is_some() { + tags_count += 1; + } + if self.media_white_point.is_some() { + tags_count += 1; + } + if self.gamut.is_some() { + tags_count += 1; + } + if self.chromatic_adaptation.is_some() { + tags_count += 1; + } + if self.lut_a_to_b_perceptual.is_some() { + tags_count += 1; + } + if self.lut_a_to_b_colorimetric.is_some() { + tags_count += 1; + } + if self.lut_a_to_b_saturation.is_some() { + tags_count += 1; + } + if self.lut_b_to_a_perceptual.is_some() { + tags_count += 1; + } + if self.lut_b_to_a_colorimetric.is_some() { + tags_count += 1; + } + if self.lut_b_to_a_saturation.is_some() { + tags_count += 1; + } + if self.luminance.is_some() { + tags_count += 1; + } + if let Some(description) = &self.description { + if description.has_values() { + tags_count += 1; + } + } + if let Some(copyright) = &self.copyright { + if copyright.has_values() { + tags_count += 1; + } + } + if let Some(vd) = &self.viewing_conditions_description { + if vd.has_values() { + tags_count += 1; + } + } + if let Some(vd) = &self.device_model { + if vd.has_values() { + tags_count += 1; + } + } + if let Some(vd) = &self.device_manufacturer { + if vd.has_values() { + tags_count += 1; + } + } + tags_count + } + + /// Encodes profile + pub fn encode(&self) -> Result, CmsError> { + let mut entries = Vec::new(); + let tags_count = self.writable_tags_count(); + let mut tags = Vec::with_capacity(TAG_SIZE * tags_count); + let mut base_offset = size_of::() + TAG_SIZE * tags_count; + if self.red_colorant != Xyzd::default() { + write_tag_entry(&mut tags, Tag::RedXyz, base_offset, 20); + write_xyz_tag_value(&mut entries, self.red_colorant); + base_offset += 20; + } + if self.green_colorant != Xyzd::default() { + write_tag_entry(&mut tags, Tag::GreenXyz, base_offset, 20); + write_xyz_tag_value(&mut entries, self.green_colorant); + base_offset += 20; + } + if self.blue_colorant != Xyzd::default() { + write_tag_entry(&mut tags, Tag::BlueXyz, base_offset, 20); + write_xyz_tag_value(&mut entries, self.blue_colorant); + base_offset += 20; + } + if let Some(chad) = self.chromatic_adaptation { + write_tag_entry(&mut tags, Tag::ChromaticAdaptation, base_offset, 8 + 9 * 4); + write_chad(&mut entries, chad); + base_offset += 8 + 9 * 4; + } + if let Some(trc) = &self.red_trc { + let entry_size = write_trc_entry(&mut entries, trc)?; + write_tag_entry(&mut tags, Tag::RedToneReproduction, base_offset, entry_size); + base_offset += entry_size; + } + if let Some(trc) = &self.green_trc { + let entry_size = write_trc_entry(&mut entries, trc)?; + write_tag_entry( + &mut tags, + Tag::GreenToneReproduction, + base_offset, + entry_size, + ); + base_offset += entry_size; + } + if let Some(trc) = &self.blue_trc { + let entry_size = write_trc_entry(&mut entries, trc)?; + write_tag_entry( + &mut tags, + Tag::BlueToneReproduction, + base_offset, + entry_size, + ); + base_offset += entry_size; + } + if let Some(trc) = &self.gray_trc { + let entry_size = write_trc_entry(&mut entries, trc)?; + write_tag_entry( + &mut tags, + Tag::GreyToneReproduction, + base_offset, + entry_size, + ); + base_offset += entry_size; + } + if self.white_point != Xyzd::default() { + write_tag_entry(&mut tags, Tag::MediaWhitePoint, base_offset, 20); + write_xyz_tag_value(&mut entries, self.white_point); + base_offset += 20; + } + + let has_cicp = self.cicp.is_some(); + + // This tag may be present when the data colour space in the profile header is RGB, YCbCr, or XYZ, and the + // profile class in the profile header is Input or Display. The tag shall not be present for other data colour spaces + // or profile classes indicated in the profile header. + + if let Some(cicp) = &self.cicp { + if (self.profile_class == ProfileClass::InputDevice + || self.profile_class == ProfileClass::DisplayDevice) + && (self.color_space == DataColorSpace::Rgb + || self.color_space == DataColorSpace::YCbr + || self.color_space == DataColorSpace::Xyz) + { + write_tag_entry(&mut tags, Tag::CodeIndependentPoints, base_offset, 12); + write_cicp_entry(&mut entries, cicp); + base_offset += 12; + } + } + + if let Some(lut) = &self.lut_a_to_b_perceptual { + let entry_size = write_lut(&mut entries, lut, true)?; + write_tag_entry( + &mut tags, + Tag::DeviceToPcsLutPerceptual, + base_offset, + entry_size, + ); + base_offset += entry_size; + } + + if let Some(lut) = &self.lut_a_to_b_colorimetric { + let entry_size = write_lut(&mut entries, lut, true)?; + write_tag_entry( + &mut tags, + Tag::DeviceToPcsLutColorimetric, + base_offset, + entry_size, + ); + base_offset += entry_size; + } + + if let Some(lut) = &self.lut_a_to_b_saturation { + let entry_size = write_lut(&mut entries, lut, true)?; + write_tag_entry( + &mut tags, + Tag::DeviceToPcsLutSaturation, + base_offset, + entry_size, + ); + base_offset += entry_size; + } + + if let Some(lut) = &self.lut_b_to_a_perceptual { + let entry_size = write_lut(&mut entries, lut, false)?; + write_tag_entry( + &mut tags, + Tag::PcsToDeviceLutPerceptual, + base_offset, + entry_size, + ); + base_offset += entry_size; + } + + if let Some(lut) = &self.lut_b_to_a_colorimetric { + let entry_size = write_lut(&mut entries, lut, false)?; + write_tag_entry( + &mut tags, + Tag::PcsToDeviceLutColorimetric, + base_offset, + entry_size, + ); + base_offset += entry_size; + } + + if let Some(lut) = &self.lut_b_to_a_saturation { + let entry_size = write_lut(&mut entries, lut, false)?; + write_tag_entry( + &mut tags, + Tag::PcsToDeviceLutSaturation, + base_offset, + entry_size, + ); + base_offset += entry_size; + } + + if let Some(lut) = &self.gamut { + let entry_size = write_lut(&mut entries, lut, false)?; + write_tag_entry(&mut tags, Tag::Gamut, base_offset, entry_size); + base_offset += entry_size; + } + + if let Some(luminance) = self.luminance { + write_tag_entry(&mut tags, Tag::Luminance, base_offset, 20); + write_xyz_tag_value(&mut entries, luminance); + base_offset += 20; + } + + if let Some(description) = &self.description { + if description.has_values() { + let entry_size = write_string_value(&mut entries, description); + write_tag_entry(&mut tags, Tag::ProfileDescription, base_offset, entry_size); + base_offset += entry_size; + } + } + + if let Some(copyright) = &self.copyright { + if copyright.has_values() { + let entry_size = write_string_value(&mut entries, copyright); + write_tag_entry(&mut tags, Tag::Copyright, base_offset, entry_size); + base_offset += entry_size; + } + } + + if let Some(vd) = &self.viewing_conditions_description { + if vd.has_values() { + let entry_size = write_string_value(&mut entries, vd); + write_tag_entry( + &mut tags, + Tag::ViewingConditionsDescription, + base_offset, + entry_size, + ); + base_offset += entry_size; + } + } + + if let Some(vd) = &self.device_model { + if vd.has_values() { + let entry_size = write_string_value(&mut entries, vd); + write_tag_entry(&mut tags, Tag::DeviceModel, base_offset, entry_size); + base_offset += entry_size; + } + } + + if let Some(vd) = &self.device_manufacturer { + if vd.has_values() { + let entry_size = write_string_value(&mut entries, vd); + write_tag_entry(&mut tags, Tag::DeviceManufacturer, base_offset, entry_size); + // base_offset += entry_size; + } + } + + tags.extend(entries); + + let profile_header = ProfileHeader { + size: size_of::() as u32 + tags.len() as u32, + pcs: self.pcs, + profile_class: self.profile_class, + rendering_intent: self.rendering_intent, + cmm_type: 0, + version: if has_cicp { + ProfileVersion::V4_3 + } else { + ProfileVersion::V4_0 + }, + data_color_space: self.color_space, + creation_date_time: ColorDateTime::now(), + signature: ProfileSignature::Acsp, + platform: 0u32, + flags: 0u32, + device_manufacturer: 0u32, + device_model: 0u32, + device_attributes: [0u8; 8], + illuminant: self.white_point.to_xyz(), + creator: 0u32, + profile_id: [0u8; 16], + reserved: [0u8; 28], + tag_count: tags_count as u32, + }; + let mut header = profile_header.encode(); + header.extend(tags); + Ok(header) + } +} + +impl FloatToFixedU8Fixed8 for f32 { + #[inline] + fn to_u8_fixed8(self) -> u16 { + if self > 255.0 + 255.0 / 256f32 { + 0xffffu16 + } else if self < 0.0 { + 0u16 + } else { + (self * 256.0 + 0.5).floor() as u16 + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn to_u8_fixed8() { + assert_eq!(0, 0f32.to_u8_fixed8()); + assert_eq!(0x0100, 1f32.to_u8_fixed8()); + assert_eq!(u16::MAX, (255f32 + (255f32 / 256f32)).to_u8_fixed8()); + } + + #[test] + fn to_s15_fixed16() { + assert_eq!(0x80000000u32 as i32, (-32768f32).to_s15_fixed16()); + assert_eq!(0, 0f32.to_s15_fixed16()); + assert_eq!(0x10000, 1.0f32.to_s15_fixed16()); + assert_eq!( + i32::MAX, + (32767f32 + (65535f32 / 65536f32)).to_s15_fixed16() + ); + } +} diff --git a/deps/moxcms/src/xyy.rs b/deps/moxcms/src/xyy.rs new file mode 100644 index 0000000..98cbd2f --- /dev/null +++ b/deps/moxcms/src/xyy.rs @@ -0,0 +1,98 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 8/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::{Xyz, Xyzd}; + +/// Holds CIE XyY representation +#[derive(Clone, Debug, Copy, Default)] +pub struct XyY { + pub x: f64, + pub y: f64, + pub yb: f64, +} + +pub trait XyYRepresentable { + fn to_xyy(self) -> XyY; +} + +impl XyYRepresentable for XyY { + #[inline] + fn to_xyy(self) -> XyY { + self + } +} + +impl XyY { + #[inline] + pub const fn new(x: f64, y: f64, yb: f64) -> Self { + Self { x, y, yb } + } + + #[inline] + pub const fn to_xyz(self) -> Xyz { + let reciprocal = if self.y != 0. { + 1. / self.y * self.yb + } else { + 0. + }; + Xyz { + x: (self.x * reciprocal) as f32, + y: self.yb as f32, + z: ((1. - self.x - self.y) * reciprocal) as f32, + } + } + + #[inline] + pub const fn to_xyzd(self) -> Xyzd { + let reciprocal = if self.y != 0. { + 1. / self.y * self.yb + } else { + 0. + }; + Xyzd { + x: self.x * reciprocal, + y: self.yb, + z: (1. - self.x - self.y) * reciprocal, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_xyzd_xyy() { + let xyy = XyY::new(0.2, 0.4, 0.5); + let xyy = xyy.to_xyzd(); + let r_xyy = xyy.to_xyzd(); + assert!((r_xyy.x - xyy.x).abs() < 1e-5); + assert!((r_xyy.y - xyy.y).abs() < 1e-5); + assert!((r_xyy.z - xyy.z).abs() < 1e-5); + } +} diff --git a/deps/moxcms/src/yrg.rs b/deps/moxcms/src/yrg.rs new file mode 100644 index 0000000..1615013 --- /dev/null +++ b/deps/moxcms/src/yrg.rs @@ -0,0 +1,177 @@ +/* + * // Copyright (c) Radzivon Bartoshyk 3/2025. All rights reserved. + * // + * // Redistribution and use in source and binary forms, with or without modification, + * // are permitted provided that the following conditions are met: + * // + * // 1. Redistributions of source code must retain the above copyright notice, this + * // list of conditions and the following disclaimer. + * // + * // 2. Redistributions in binary form must reproduce the above copyright notice, + * // this list of conditions and the following disclaimer in the documentation + * // and/or other materials provided with the distribution. + * // + * // 3. Neither the name of the copyright holder nor the names of its + * // contributors may be used to endorse or promote products derived from + * // this software without specific prior written permission. + * // + * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +use crate::mlaf::mlaf; +use crate::{Matrix3f, Vector3f, Xyz}; +use pxfm::{f_atan2f, f_hypotf, f_sincosf}; + +/// Structure for Yrg colorspace +/// +/// Kirk Yrg 2021. +#[repr(C)] +#[derive(Default, Debug, PartialOrd, PartialEq, Copy, Clone)] +pub struct Yrg { + pub y: f32, + pub r: f32, + pub g: f32, +} + +/// Structure for cone form of Yrg colorspace +#[repr(C)] +#[derive(Default, Debug, PartialOrd, PartialEq, Copy, Clone)] +pub struct Ych { + pub y: f32, + pub c: f32, + pub h: f32, +} + +const LMS_TO_XYZ: Matrix3f = Matrix3f { + v: [ + [1.8079466, -1.2997167, 0.34785876], + [0.61783963, 0.39595452, -0.041046873], + [-0.12546961, 0.20478038, 1.7427418], + ], +}; +const XYZ_TO_LMS: Matrix3f = Matrix3f { + v: [ + [0.257085, 0.859943, -0.031061], + [-0.394427, 1.175800, 0.106423], + [0.064856, -0.076250, 0.559067], + ], +}; + +impl Yrg { + #[inline] + pub const fn new(y: f32, r: f32, g: f32) -> Yrg { + Yrg { y, r, g } + } + + /// Convert [Xyz] D65 to [Yrg] + /// + /// Yrg defined in D65 white point. Ensure Xyz values is adapted. + /// Yrg use CIE XYZ 2006, adapt CIE XYZ 1931 by using [cie_y_1931_to_cie_y_2006] at first. + #[inline] + pub fn from_xyz(xyz: Xyz) -> Self { + let lms = XYZ_TO_LMS.f_mul_vector(Vector3f { + v: [xyz.x, xyz.y, xyz.z], + }); + let y = mlaf(0.68990272 * lms.v[0], 0.34832189, lms.v[1]); + + let a = lms.v[0] + lms.v[1] + lms.v[2]; + let l = if a == 0. { 0. } else { lms.v[0] / a }; + let m = if a == 0. { 0. } else { lms.v[1] / a }; + let r = mlaf(mlaf(0.02062, -0.6873, m), 1.0671, l); + let g = mlaf(mlaf(-0.05155, -0.0362, l), 1.7182, m); + Yrg { y, r, g } + } + + #[inline] + pub fn to_xyz(&self) -> Xyz { + let l = mlaf(0.95 * self.r, 0.38, self.g); + let m = mlaf(mlaf(0.03, 0.59, self.g), 0.02, self.r); + let den = mlaf(0.68990272 * l, 0.34832189, m); + let a = if den == 0. { 0. } else { self.y / den }; + let l0 = l * a; + let m0 = m * a; + let s0 = (1f32 - l - m) * a; + let v = Vector3f { v: [l0, m0, s0] }; + let x = LMS_TO_XYZ.f_mul_vector(v); + Xyz { + x: x.v[0], + y: x.v[1], + z: x.v[2], + } + } +} + +impl Ych { + #[inline] + pub const fn new(y: f32, c: f32, h: f32) -> Self { + Ych { y, c, h } + } + + #[inline] + pub fn from_yrg(yrg: Yrg) -> Self { + let y = yrg.y; + // Subtract white point. These are the r, g coordinates of + // sRGB (D50 adapted) (1, 1, 1) taken through + // XYZ D50 -> CAT16 D50->D65 adaptation -> LMS 2006 + // -> grading RGB conversion. + let r = yrg.r - 0.21902143; + let g = yrg.g - 0.54371398; + let c = f_hypotf(g, r); + let h = f_atan2f(g, r); + Self { y, c, h } + } + + #[inline] + pub fn to_yrg(&self) -> Yrg { + let y = self.y; + let c = self.c; + let h = self.h; + let sincos = f_sincosf(h); + let r = mlaf(0.21902143, c, sincos.1); + let g = mlaf(0.54371398, c, sincos.0); + Yrg { y, r, g } + } +} + +// Pipeline and ICC luminance is CIE Y 1931 +// Kirk Ych/Yrg uses CIE Y 2006 +// 1 CIE Y 1931 = 1.05785528 CIE Y 2006, so we need to adjust that. +// This also accounts for the CAT16 D50->D65 adaptation that has to be done +// to go from RGB to CIE LMS 2006. +// Warning: only applies to achromatic pixels. +pub const fn cie_y_1931_to_cie_y_2006(x: f32) -> f32 { + 1.05785528 * (x) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_yrg() { + let xyz = Xyz::new(0.95, 1.0, 1.08); + let yrg = Yrg::from_xyz(xyz); + let yrg_to_xyz = yrg.to_xyz(); + assert!((xyz.x - yrg_to_xyz.x) < 1e-5); + assert!((xyz.y - yrg_to_xyz.y) < 1e-5); + assert!((xyz.z - yrg_to_xyz.z) < 1e-5); + } + + #[test] + fn test_ych() { + let xyz = Yrg::new(0.5, 0.4, 0.3); + let yrg = Ych::from_yrg(xyz); + let yrg_to_xyz = yrg.to_yrg(); + assert!((xyz.y - yrg_to_xyz.y) < 1e-5); + assert!((xyz.r - yrg_to_xyz.r) < 1e-5); + assert!((xyz.g - yrg_to_xyz.g) < 1e-5); + } +} diff --git a/deps/simd-adler32/CHANGELOG.md b/deps/simd-adler32/CHANGELOG.md new file mode 100644 index 0000000..95b141e --- /dev/null +++ b/deps/simd-adler32/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## 0.3.3 - 2021-04-14 + +### Features + +- **from_checksum**: add `Adler32::from_checksum` + +### Performance Improvements + +- **scalar**: improve scalar performance by 90-600% + - Defer modulo until right before u16 overflow diff --git a/deps/simd-adler32/Cargo.toml b/deps/simd-adler32/Cargo.toml new file mode 100644 index 0000000..c58dd05 --- /dev/null +++ b/deps/simd-adler32/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "simd-adler32" +authors = ["Marvin Countryman "] +license = "MIT" +version = "0.3.7" +edition = "2018" +keywords = ["simd", "avx2", "ssse3", "adler", "adler32"] +categories = ["algorithms", "no-std"] +repository = "https://github.com/mcountryman/simd-adler32" +description = "A SIMD-accelerated Adler-32 hash algorithm implementation." +exclude = ["bench"] + +[profile.release] +debug = true +opt-level = 2 + +[[bench]] +name = "alts" +path = "bench/alts.rs" +harness = false + +[[bench]] +name = "variants" +path = "bench/variants.rs" +harness = false + +[features] +default = ["std", "const-generics"] +std = [] +nightly = [] +const-generics = [] + +[dev-dependencies] +rand = "0.8" +criterion = "0.3" + +# competition +adler = "1.0.2" +adler32 = "1.2.0" diff --git a/deps/simd-adler32/LICENSE.md b/deps/simd-adler32/LICENSE.md new file mode 100644 index 0000000..9bd65ce --- /dev/null +++ b/deps/simd-adler32/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2021] [Marvin Countryman] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/deps/simd-adler32/README.md b/deps/simd-adler32/README.md new file mode 100644 index 0000000..4eeec46 --- /dev/null +++ b/deps/simd-adler32/README.md @@ -0,0 +1,131 @@ +

simd-adler32

+

+ + docs.rs badge + + + crates.io badge + + + mit license badge + +

+ +A SIMD-accelerated Adler-32 hash algorithm implementation. + +## Features + +- No dependencies +- Support `no_std` (with `default-features = false`) +- Runtime CPU feature detection (when `std` enabled) +- Blazing fast performance on as many targets as possible (currently only x86 and x86_64) +- Default to scalar implementation when simd not available + +## Quick start + +> Cargo.toml + +```toml +[dependencies] +simd-adler32 = "*" +``` + +> example.rs + +```rust +use simd_adler32::Adler32; + +let mut adler = Adler32::new(); +adler.write(b"rust is pretty cool, man"); +let hash = adler.finish(); + +println!("{}", hash); +// 1921255656 +``` + +## Support + +**CPU Features** + +| impl | arch | feature | +| ---- | ---------------- | ------- | +| ✅ | `x86`, `x86_64` | avx512 | +| ✅ | `x86`, `x86_64` | avx2 | +| ✅ | `x86`, `x86_64` | ssse3 | +| ✅ | `x86`, `x86_64` | sse2 | +| 🚧 | `arm`, `aarch64` | neon | +| ✅ | `wasm32` | simd128 | + +**MSRV** `1.36.0`\*\* + +Minimum supported rust version is tested before a new version is published. [**] Feature +`const-generics` needs to disabled to build on rustc versions `<1.51` which can be done +by updating your dependency definition to the following. + +> Cargo.toml + +```toml +[dependencies] +simd-adler32 = { version "*", default-features = false, features = ["std"] } +``` + +## Performance + +Benchmarks listed display number of randomly generated bytes (10k / 100k) and library +name. Benchmarks sources can be found under the [bench](/bench) directory. Crates used for +comparison are [adler](https://crates.io/crates/adler) and +[adler32](https://crates.io/crates/adler32). + +> Windows 10 Pro - Intel i5-8300H @ 2.30GHz + +| name | avg. time | avg. thrpt | +| ----------------------- | --------------- | ------------------ | +| **10k/simd-adler32** | **212.61 ns** | **43.805 GiB/s** | +| 10k/wuffs | 3843 ns | 2.63 GiB/s\* | +| 10k/adler32 | 4.8084 us | 1.9369 GiB/s | +| 10k/adler | 17.979 us | 530.43 MiB/s | +| ----------------------- | --------------- | ------------------ | +| **100k/simd-adler32** | **2.7951 us** | **33.320 GiB/s** | +| 100k/wuffs | 34733 ns | 2.6814 GiB/s\* | +| 100k/adler32 | 48.488 us | 1.9207 GiB/s | +| 100k/adler | 178.36 us | 534.69 MiB/s | + +\* wuffs ran using mingw64/gcc, ran with `wuffs bench -ccompilers=gcc -reps=1 -iterscale=300 std/adler32`. + +> MacBookPro16,1 - Intel i9-9880H CPU @ 2.30GHz + +| name | avg. time | avg. thrpt | +| ----------------------- | --------------- | ------------------ | +| **10k/simd-adler32** | **200.37 ns** | **46.480 GiB/s** | +| 10k/adler32 | 4.1516 us | 2.2433 GiB/s | +| 10k/adler | 10.220 us | 933.15 MiB/s | +| ----------------------- | --------------- | ------------------ | +| **100k/simd-adler32** | **2.3282 us** | **40.003 GiB/s** | +| 100k/adler32 | 41.130 us | 2.2643 GiB/s | +| 100k/adler | 83.776 us | 534.69 MiB/s | + +## Safety + +This crate contains a significant amount of `unsafe` code due to the requirement of `unsafe` +for simd intrinsics. Fuzzing is done on release and debug builds prior to publishing via +`afl`. Fuzzy tests can be found under [fuzz](/fuzz) the directory. + +## Resources + +- [LICENSE](./LICENSE.md) - MIT +- [CHANGELOG](./CHANGELOG.md) + +## Credits + +Thank you to the contributors of the following projects. + +- [adler](https://github.com/jonas-schievink/adler) +- [adler32](https://github.com/remram44/adler32-rs) +- [crc32fast](https://github.com/srijs/rust-crc32fast) +- [wuffs](https://github.com/google/wuffs) +- [chromium](https://bugs.chromium.org/p/chromium/issues/detail?id=762564) +- [zlib](https://zlib.net/) + +## Contributing + +Feel free to submit a issue or pull request. :smile: diff --git a/deps/simd-adler32/src/hash.rs b/deps/simd-adler32/src/hash.rs new file mode 100644 index 0000000..558542b --- /dev/null +++ b/deps/simd-adler32/src/hash.rs @@ -0,0 +1,156 @@ +use crate::{Adler32, Adler32Hash}; + +impl Adler32Hash for &[u8] { + fn hash(&self) -> u32 { + let mut hash = Adler32::new(); + + hash.write(self); + hash.finish() + } +} + +impl Adler32Hash for &str { + fn hash(&self) -> u32 { + let mut hash = Adler32::new(); + + hash.write(self.as_bytes()); + hash.finish() + } +} + +#[cfg(feature = "const-generics")] +impl Adler32Hash for [u8; SIZE] { + fn hash(&self) -> u32 { + let mut hash = Adler32::new(); + + hash.write(self); + hash.finish() + } +} + +macro_rules! array_impl { + ($s:expr, $($size:expr),+) => { + array_impl!($s); + $(array_impl!{$size})* + }; + ($size:expr) => { + #[cfg(not(feature = "const-generics"))] + impl Adler32Hash for [u8; $size] { + fn hash(&self) -> u32 { + let mut hash = Adler32::new(); + + hash.write(self); + hash.finish() + } + } + }; +} + +array_impl!( + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 1024, + 1024 * 1024, + 1024 * 1024 * 1024, + 2048, + 4096 +); diff --git a/deps/simd-adler32/src/imp/mod.rs b/deps/simd-adler32/src/imp/mod.rs new file mode 100644 index 0000000..9f3a19e --- /dev/null +++ b/deps/simd-adler32/src/imp/mod.rs @@ -0,0 +1,13 @@ +pub mod scalar; + +pub type Adler32Imp = fn(u16, u16, &[u8]) -> (u16, u16); + +#[inline] +#[allow(non_snake_case)] +pub const fn _MM_SHUFFLE(z: u32, y: u32, x: u32, w: u32) -> i32 { + ((z << 6) | (y << 4) | (x << 2) | w) as i32 +} + +pub fn get_imp() -> Adler32Imp { + scalar::update +} diff --git a/deps/simd-adler32/src/imp/scalar.rs b/deps/simd-adler32/src/imp/scalar.rs new file mode 100644 index 0000000..558813e --- /dev/null +++ b/deps/simd-adler32/src/imp/scalar.rs @@ -0,0 +1,69 @@ +const MOD: u32 = 65521; +const NMAX: usize = 5552; + +pub fn update(a: u16, b: u16, data: &[u8]) -> (u16, u16) { + let mut a = a as u32; + let mut b = b as u32; + + let chunks = data.chunks_exact(NMAX); + let remainder = chunks.remainder(); + + for chunk in chunks { + for byte in chunk { + a = a.wrapping_add(*byte as _); + b = b.wrapping_add(a); + } + + a %= MOD; + b %= MOD; + } + + for byte in remainder { + a = a.wrapping_add(*byte as _); + b = b.wrapping_add(a); + } + + a %= MOD; + b %= MOD; + + (a as u16, b as u16) +} + +#[cfg(test)] +mod tests { + #[test] + fn zeroes() { + assert_eq!(adler32(&[]), 1); + assert_eq!(adler32(&[0]), 1 | 1 << 16); + assert_eq!(adler32(&[0, 0]), 1 | 2 << 16); + assert_eq!(adler32(&[0; 100]), 0x00640001); + assert_eq!(adler32(&[0; 1024]), 0x04000001); + assert_eq!(adler32(&[0; 1024 * 1024]), 0x00f00001); + } + + #[test] + fn ones() { + assert_eq!(adler32(&[0xff; 1024]), 0x79a6fc2e); + assert_eq!(adler32(&[0xff; 1024 * 1024]), 0x8e88ef11); + } + + #[test] + fn mixed() { + assert_eq!(adler32(&[1]), 2 | 2 << 16); + assert_eq!(adler32(&[40]), 41 | 41 << 16); + + assert_eq!(adler32(&[0xA5; 1024 * 1024]), 0xd5009ab1); + } + + /// Example calculation from https://en.wikipedia.org/wiki/Adler-32. + #[test] + fn wiki() { + assert_eq!(adler32(b"Wikipedia"), 0x11E60398); + } + + fn adler32(data: &[u8]) -> u32 { + let (a, b) = super::update(1, 0, data); + + u32::from(b) << 16 | u32::from(a) + } +} diff --git a/deps/simd-adler32/src/lib.rs b/deps/simd-adler32/src/lib.rs new file mode 100644 index 0000000..721aed7 --- /dev/null +++ b/deps/simd-adler32/src/lib.rs @@ -0,0 +1,309 @@ +//! # simd-adler32 +//! +//! A SIMD-accelerated Adler-32 hash algorithm implementation. +//! +//! ## Features +//! +//! - No dependencies +//! - Support `no_std` (with `default-features = false`) +//! - Runtime CPU feature detection (when `std` enabled) +//! - Blazing fast performance on as many targets as possible (currently only x86 and x86_64) +//! - Default to scalar implementation when simd not available +//! +//! ## Quick start +//! +//! > Cargo.toml +//! +//! ```toml +//! [dependencies] +//! simd-adler32 = "*" +//! ``` +//! +//! > example.rs +//! +//! ```rust +//! use simd_adler32::Adler32; +//! +//! let mut adler = Adler32::new(); +//! adler.write(b"rust is pretty cool, man"); +//! let hash = adler.finish(); +//! +//! println!("{}", hash); +//! // 1921255656 +//! ``` +//! +//! ## Feature flags +//! +//! * `std` - Enabled by default +//! +//! Enables std support, see [CPU Feature Detection](#cpu-feature-detection) for runtime +//! detection support. +//! * `nightly` +//! +//! Enables nightly features required for avx512 support. +//! +//! * `const-generics` - Enabled by default +//! +//! Enables const-generics support allowing for user-defined array hashing by value. See +//! [`Adler32Hash`] for details. +//! +//! ## Support +//! +//! **CPU Features** +//! +//! | impl | arch | feature | +//! | ---- | ---------------- | ------- | +//! | ✅ | `x86`, `x86_64` | avx512 | +//! | ✅ | `x86`, `x86_64` | avx2 | +//! | ✅ | `x86`, `x86_64` | ssse3 | +//! | ✅ | `x86`, `x86_64` | sse2 | +//! | 🚧 | `arm`, `aarch64` | neon | +//! | | `wasm32` | simd128 | +//! +//! **MSRV** `1.36.0`\*\* +//! +//! Minimum supported rust version is tested before a new version is published. [**] Feature +//! `const-generics` needs to disabled to build on rustc versions `<1.51` which can be done +//! by updating your dependency definition to the following. +//! +//! ## CPU Feature Detection +//! simd-adler32 supports both runtime and compile time CPU feature detection using the +//! `std::is_x86_feature_detected` macro when the `Adler32` struct is instantiated with +//! the `new` fn. +//! +//! Without `std` feature enabled simd-adler32 falls back to compile time feature detection +//! using `target-feature` or `target-cpu` flags supplied to rustc. See [https://rust-lang.github.io/packed_simd/perf-guide/target-feature/rustflags.html](https://rust-lang.github.io/packed_simd/perf-guide/target-feature/rustflags.html) +//! for more information. +//! +//! Feature detection tries to use the fastest supported feature first. +#![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(feature = "nightly", feature(stdsimd, avx512_target_feature))] + +#[doc(hidden)] +pub mod hash; +#[doc(hidden)] +pub mod imp; + +use imp::{get_imp, Adler32Imp}; + +/// An adler32 hash generator type. +#[derive(Clone)] +pub struct Adler32 { + a: u16, + b: u16, + update: Adler32Imp, +} + +impl Adler32 { + /// Constructs a new `Adler32`. + /// + /// Potential overhead here due to runtime feature detection although in testing on 100k + /// and 10k random byte arrays it was not really noticeable. + /// + /// # Examples + /// ```rust + /// use simd_adler32::Adler32; + /// + /// let mut adler = Adler32::new(); + /// ``` + pub fn new() -> Self { + Default::default() + } + + /// Constructs a new `Adler32` using existing checksum. + /// + /// Potential overhead here due to runtime feature detection although in testing on 100k + /// and 10k random byte arrays it was not really noticeable. + /// + /// # Examples + /// ```rust + /// use simd_adler32::Adler32; + /// + /// let mut adler = Adler32::from_checksum(0xdeadbeaf); + /// ``` + pub fn from_checksum(checksum: u32) -> Self { + Self { + a: checksum as u16, + b: (checksum >> 16) as u16, + update: get_imp(), + } + } + + /// Computes hash for supplied data and stores results in internal state. + pub fn write(&mut self, data: &[u8]) { + let (a, b) = (self.update)(self.a, self.b, data); + + self.a = a; + self.b = b; + } + + /// Returns the hash value for the values written so far. + /// + /// Despite its name, the method does not reset the hasher’s internal state. Additional + /// writes will continue from the current value. If you need to start a fresh hash + /// value, you will have to use `reset`. + pub fn finish(&self) -> u32 { + (u32::from(self.b) << 16) | u32::from(self.a) + } + + /// Resets the internal state. + pub fn reset(&mut self) { + self.a = 1; + self.b = 0; + } +} + +/// Compute Adler-32 hash on `Adler32Hash` type. +/// +/// # Arguments +/// * `hash` - A Adler-32 hash-able type. +/// +/// # Examples +/// ```rust +/// use simd_adler32::adler32; +/// +/// let hash = adler32(b"Adler-32"); +/// println!("{}", hash); // 800813569 +/// ``` +pub fn adler32(hash: &H) -> u32 { + hash.hash() +} + +/// A Adler-32 hash-able type. +pub trait Adler32Hash { + /// Feeds this value into `Adler32`. + fn hash(&self) -> u32; +} + +impl Default for Adler32 { + fn default() -> Self { + Self { + a: 1, + b: 0, + update: get_imp(), + } + } +} + +#[cfg(feature = "std")] +pub mod read { + //! Reader-based hashing. + //! + //! # Example + //! ```rust + //! use std::io::Cursor; + //! use simd_adler32::read::adler32; + //! + //! let mut reader = Cursor::new(b"Hello there"); + //! let hash = adler32(&mut reader).unwrap(); + //! + //! println!("{}", hash) // 800813569 + //! ``` + use crate::Adler32; + use std::io::{Read, Result}; + + /// Compute Adler-32 hash on reader until EOF. + /// + /// # Example + /// ```rust + /// use std::io::Cursor; + /// use simd_adler32::read::adler32; + /// + /// let mut reader = Cursor::new(b"Hello there"); + /// let hash = adler32(&mut reader).unwrap(); + /// + /// println!("{}", hash) // 800813569 + /// ``` + pub fn adler32(reader: &mut R) -> Result { + let mut hash = Adler32::new(); + let mut buf = [0; 4096]; + + loop { + match reader.read(&mut buf) { + Ok(0) => return Ok(hash.finish()), + Ok(n) => { + hash.write(&buf[..n]); + } + Err(err) => return Err(err), + } + } + } +} + +#[cfg(feature = "std")] +pub mod bufread { + //! BufRead-based hashing. + //! + //! Separate `BufRead` trait implemented to allow for custom buffer size optimization. + //! + //! # Example + //! ```rust + //! use std::io::{Cursor, BufReader}; + //! use simd_adler32::bufread::adler32; + //! + //! let mut reader = Cursor::new(b"Hello there"); + //! let mut reader = BufReader::new(reader); + //! let hash = adler32(&mut reader).unwrap(); + //! + //! println!("{}", hash) // 800813569 + //! ``` + use crate::Adler32; + use std::io::{BufRead, ErrorKind, Result}; + + /// Compute Adler-32 hash on buf reader until EOF. + /// + /// # Example + /// ```rust + /// use std::io::{Cursor, BufReader}; + /// use simd_adler32::bufread::adler32; + /// + /// let mut reader = Cursor::new(b"Hello there"); + /// let mut reader = BufReader::new(reader); + /// let hash = adler32(&mut reader).unwrap(); + /// + /// println!("{}", hash) // 800813569 + /// ``` + pub fn adler32(reader: &mut R) -> Result { + let mut hash = Adler32::new(); + + loop { + let consumed = match reader.fill_buf() { + Ok(buf) => { + if buf.is_empty() { + return Ok(hash.finish()); + } + + hash.write(buf); + buf.len() + } + Err(err) => match err.kind() { + ErrorKind::Interrupted => continue, + ErrorKind::UnexpectedEof => return Ok(hash.finish()), + _ => return Err(err), + }, + }; + + reader.consume(consumed); + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_from_checksum() { + let buf = b"rust is pretty cool man"; + let sum = 0xdeadbeaf; + + let mut simd = super::Adler32::from_checksum(sum); + let mut adler = adler::Adler32::from_checksum(sum); + + simd.write(buf); + adler.write_slice(buf); + + let simd = simd.finish(); + let scalar = adler.checksum(); + + assert_eq!(simd, scalar); + } +} diff --git a/hack/assets/edera-splash.png b/hack/assets/edera-splash.png new file mode 100644 index 0000000000000000000000000000000000000000..2919b9d1ce62b6b3b5d7098e08b1a45ce33c4186 GIT binary patch literal 3751312 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

EIYN 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 z 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�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

goty?fIB(x;^5w?0gpSo*?13tximk* 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

g@P#(Z~!3z#d&a|l^JZizASUh`CPaJYJ* z^q@uVRlFIt3q&*@jbSxPWCM?QuqEAOwabwoQdlEAAc4(CsfOHk@8DQI5SG ztr7~Z%Z(hwF5{mHg>vyWyNaD`n09)}nWnWat;<5d*wBRbF3B+?X`>dl;*B3lKT zZzRLvA<%{93BEK>j3#QC5x9@_toLRWeKsNh_UzIf%-NA9h5%ShouB8z~-}=T@MAi9^MXwJlaA!iBUTO zHnGOA58tM}1=9Ue7eB7c9n2gGzgGtnL$~gKF79VjDn|9BGL0s43ouK{oHT2CBLm~Y z*AoJv00QNjcpTzrdgrwOu-1lOF`tw85&(G&=f|kvZlHp4ExYSuJ(u`74DSNw{iuL zmr_#&NW`dV^K`@%{k|^Q%s&h}I;D(>qU?zlo=l>gblGz4X%Ic`Ly|`eo;cjZ%>=>( zcquvOUgK*X$rB?hHUdrLoDtd2KZhCm?Ah|SQ(glDC3P@7v*0|PIf;yhf=0ry&G+`A zo#F_r(AflLkx43Zs*jKaD9F|os*L#nAVYhX`34Xv;N*Ay_hd0j+zvJe7L(+dibqDz zqEzMT$E3}Y0ury_=NoC_2`U54OZmwszlcW8H3SQ>wx*LqJ|Ey1{Oh0nl^8v&ckX+Z z&#%qf%E#T-PT}cOJ}UdWzvnN%_`

{+ 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 z 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