diff --git a/.gitea/workflows/skia.yml b/.gitea/workflows/skia.yml new file mode 100644 index 0000000..0bc0a8e --- /dev/null +++ b/.gitea/workflows/skia.yml @@ -0,0 +1,50 @@ +name: Skia feature + +on: + push: + pull_request: + +jobs: + linux: + runs-on: ubuntu-latest + env: + FORCE_SKIA_BINARIES_DOWNLOAD: "1" + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Install native link prerequisites + run: | + sudo apt-get update + sudo apt-get install --yes curl pkg-config libfontconfig1-dev libfreetype6-dev + - name: Test default and Skia feature builds + run: | + cargo test -p libremetaverse-imaging-skia --no-default-features + cargo test -p libremetaverse-imaging-skia --features skia + cargo package -p libremetaverse-imaging-skia --list + + macos: + runs-on: macos-latest + env: + FORCE_SKIA_BINARIES_DOWNLOAD: "1" + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Test default and Skia feature builds + run: | + cargo test -p libremetaverse-imaging-skia --no-default-features + cargo test -p libremetaverse-imaging-skia --features skia + cargo package -p libremetaverse-imaging-skia --list + + windows: + runs-on: windows-latest + env: + FORCE_SKIA_BINARIES_DOWNLOAD: "1" + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Test default and Skia feature builds + shell: pwsh + run: | + cargo test -p libremetaverse-imaging-skia --no-default-features + cargo test -p libremetaverse-imaging-skia --features skia + cargo package -p libremetaverse-imaging-skia --list diff --git a/Cargo.lock b/Cargo.lock index c19b87a..9ea1d73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # 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 = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + [[package]] name = "base64" version = "0.22.1" @@ -14,6 +29,32 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f09c37bc0e9f0924b7dae9988265ef3c76c88538f41a3b06caf4bed07cee5226" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + [[package]] name = "block-buffer" version = "0.10.4" @@ -38,12 +79,42 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "const-oid" version = "0.10.2" @@ -68,6 +139,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -108,6 +188,54 @@ dependencies = [ "crypto-common 0.2.2", ] +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "futures-core" version = "0.3.33" @@ -153,6 +281,24 @@ dependencies = [ "r-efi", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hybrid-array" version = "0.4.14" @@ -162,6 +308,25 @@ dependencies = [ "typenum", ] +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -185,6 +350,16 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libremetaverse" version = "0.0.1" @@ -228,6 +403,7 @@ version = "0.0.1" dependencies = [ "libremetaverse-imaging", "libremetaverse-types", + "skia-safe", ] [[package]] @@ -340,6 +516,18 @@ dependencies = [ "libremetaverse-types", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + [[package]] name = "md-5" version = "0.10.6" @@ -356,12 +544,44 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[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 = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -374,6 +594,16 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -398,6 +628,54 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.23" @@ -446,6 +724,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "sha1" version = "0.10.7" @@ -468,6 +755,65 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "skia-bindings" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2d1c3ebd697c0cbded0145e9204a38fa6b268446051b7196d0a096414ea7f3" +dependencies = [ + "bindgen", + "cc", + "flate2", + "heck", + "pkg-config", + "regex", + "serde_json", + "tar", + "toml", +] + +[[package]] +name = "skia-safe" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f512ac418a64194842dd05566320805dad1c957c521039db2486fd6368865bc" +dependencies = [ + "base64", + "bitflags", + "percent-encoding", + "skia-bindings", + "skia-svg-macros", +] + +[[package]] +name = "skia-svg-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "044dd2233c9717a74f75197f3e7f0a966db2127c0ffb5e05013b480a9b75b2c7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "slab" version = "0.4.12" @@ -502,6 +848,56 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + [[package]] name = "typenum" version = "1.20.1" @@ -582,6 +978,37 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/README.md b/README.md index 7133a35..63e6263 100644 --- a/README.md +++ b/README.md @@ -150,5 +150,13 @@ including its alpha-only and opaque-alpha substitutions. See [`crates/libremetaverse-openjpeg/README.md`](crates/libremetaverse-openjpeg/README.md) for OpenJPEG prerequisites, licensing, and deployment details. Default builds do not discover or link OpenJPEG. +The optional Skia adapter decodes BMP, GIF, ICO, JPEG, PNG, WBMP, and WebP into +the same checked planar representation. Its `skia` feature uses pinned +rust-skia binary-cache configurations on macOS, Linux, and Windows, normalizes +premultiplied color at the imaging boundary, and keeps encoded input, decoded +dimensions, strides, and allocations bounded. See +[`crates/libremetaverse-imaging-skia/README.md`](crates/libremetaverse-imaging-skia/README.md) +for native prerequisites, cache/source-build controls, licenses, and packaging +details. Default workspace builds do not compile or link Skia. The controlled audit aggregates every expected failure by standardized C# member ID and rejects unrelated fixture, assertion, compile, or symbol errors. diff --git a/RUSTREWRITE.md b/RUSTREWRITE.md index 3799c23..02af450 100644 --- a/RUSTREWRITE.md +++ b/RUSTREWRITE.md @@ -414,7 +414,7 @@ MSRV, features, and licenses at adoption time. | compression and tar archives | [`flate2` 1.1.9](https://crates.io/crates/flate2/1.1.9), [`tar` 0.4.46](https://crates.io/crates/tar/0.4.46) | Enforce decompressed-size, path traversal, and entry-count limits on untrusted OAR data. | | XML and URL | [`quick-xml` 0.41.0](https://crates.io/crates/quick-xml/0.41.0), [`url` 2.5.8](https://crates.io/crates/url/2.5.8), [`base64` 0.23.1](https://crates.io/crates/base64/0.23.1) | Streaming XML for LLSD/login; retain exact URL escaping behavior with fixtures. | | CoreJ2K 2.3.3.91 | system [OpenJPEG](https://www.openjpeg.org/) 2.5.4+ through the private `libremetaverse-openjpeg` adapter | Opt-in `jpeg2000` feature; checked-in minimal bindings avoid a libclang build dependency. Bounded memory streams and header validation isolate native code. Golden tests cover channel order, alpha, 16-bit precision, dimensions, discard levels, lossless/lossy modes, and malformed input. | -| SkiaSharp 4.150.1 | [`skia-safe` 0.99.0](https://crates.io/crates/skia-safe/0.99.0) | MSRV 1.85 and native/binary-cache build. Optional adapter only; core image APIs must not leak Skia types. | +| SkiaSharp 4.150.1 | [`skia-safe` 0.99.0](https://crates.io/crates/skia-safe/0.99.0) | Adopted behind the opt-in `skia` feature. Target-specific official binary-cache feature sets provide BMP/GIF/ICO/JPEG/PNG/WBMP/WebP decoding on macOS, Linux, and Windows; checked owned buffers keep Skia types out of the core image API. | | Pfim 0.11.4 | [`image` 0.25.10](https://crates.io/crates/image/0.25.10), [`ddsfile` 0.6.0](https://crates.io/crates/ddsfile/0.6.0) | `image` covers TGA and common DDS decoding; `ddsfile` exposes DDS container details. Golden files decide whether both are needed. | | OggVorbisEncoder 1.2.2 | [`vorbis_rs` 0.5.6](https://crates.io/crates/vorbis_rs/0.5.6) | BSD-3-Clause, MSRV 1.82, backed by C libraries. Feature-gate native audio encoding. | | SIPSorcery 8.0.23 | [`webrtc` 0.20.0](https://crates.io/crates/webrtc/0.20.0), [`cpal` 0.18.1](https://crates.io/crates/cpal/0.18.1), [`opus` 0.3.1](https://crates.io/crates/opus/0.3.1) | Validate SDP, ICE, data-channel framing, audio formats, device hotplug, and native libopus deployment separately. Do not claim parity from successful compilation. | diff --git a/api/SHIM-COVERAGE.md b/api/SHIM-COVERAGE.md index e3bbfae..c523751 100644 --- a/api/SHIM-COVERAGE.md +++ b/api/SHIM-COVERAGE.md @@ -6,7 +6,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand. |---|---:|---:|---| | `LibreMetaverse` | 2,711 | 27,281 | native implementation: 3 types / 7 members; remaining surface is callable failure-only shims | | `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain | -| `LibreMetaverse.Imaging.Skia` | 1 | 3 | callable failure-only shim | +| `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain | | `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim | | `LibreMetaverse.PrimMesher` | 17 | 207 | callable failure-only shim | | `LibreMetaverse.RLV` | 28 | 499 | callable failure-only shim | diff --git a/crates/libremetaverse-imaging-skia/Cargo.toml b/crates/libremetaverse-imaging-skia/Cargo.toml index 0c21248..b23a889 100644 --- a/crates/libremetaverse-imaging-skia/Cargo.toml +++ b/crates/libremetaverse-imaging-skia/Cargo.toml @@ -5,11 +5,27 @@ edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true -description = "Skia codec shims for the MetaCrate LibreMetaverse rewrite" +description = "Optional Skia image decoder for the MetaCrate LibreMetaverse rewrite" + +[features] +default = [] +skia = ["dep:skia-safe"] [dependencies] -libremetaverse-imaging = { path = "../libremetaverse-imaging" } -libremetaverse-types = { path = "../libremetaverse-types" } +libremetaverse-imaging = { version = "0.0.1", path = "../libremetaverse-imaging" } +libremetaverse-types = { version = "0.0.1", path = "../libremetaverse-types" } + +# rust-skia publishes WebP-capable binary caches with Vulkan on Linux/Windows +# and without Vulkan on macOS. Matching those sets avoids an hours-long Skia +# source build on every supported desktop target. +[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies] +skia-safe = { version = "0.99.0", default-features = false, features = ["binary-cache", "jpeg", "pdf", "svg", "textlayout", "vulkan", "webp"], optional = true } + +[target.'cfg(target_os = "macos")'.dependencies] +skia-safe = { version = "0.99.0", default-features = false, features = ["binary-cache", "jpeg", "pdf", "svg", "textlayout", "webp"], optional = true } + +[target.'cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))'.dependencies] +skia-safe = { version = "0.99.0", default-features = false, features = ["binary-cache", "jpeg", "pdf", "webp"], optional = true } [lints] workspace = true diff --git a/crates/libremetaverse-imaging-skia/README.md b/crates/libremetaverse-imaging-skia/README.md new file mode 100644 index 0000000..368d6b9 --- /dev/null +++ b/crates/libremetaverse-imaging-skia/README.md @@ -0,0 +1,68 @@ +# MetaCrate Skia adapter + +This crate implements the pinned `SkiaTextureCodec` behavior at a bounded, +project-owned image boundary. The core `libremetaverse-imaging` crate never +exposes a `skia-safe` type and never discovers or links Skia. + +## Features and formats + +The default feature set has no Skia dependency. `SkiaTextureCodec::decode` +returns a typed `InvalidOperation` error in that configuration, while conversion +from the checked project-owned `backend::SKBitmap` remains available for tests +and callers that already own decoded pixels. + +Enable native decoding with: + +```sh +cargo test -p libremetaverse-imaging-skia --features skia +``` + +The `skia` feature decodes the CPU codec formats supported by the pinned +rust-skia release: BMP, GIF, ICO, JPEG, PNG, WBMP, and WebP. Input is buffered +to at most 64 MiB. Dimensions are rejected before native pixel allocation when +they exceed the core 16,777,216-pixel limit, and decoded storage uses checked +strides and owned Rust buffers. + +The mapped C# adapter exposes decoding and `SKBitmap`-to-`ManagedImage` +conversion only; it has no encoding or resize method. Encoding selection stays +with the format-specific imaging APIs, and callers use `ManagedImage`'s checked +resize methods after decoding. Premultiplied pixels are converted to straight +alpha at this boundary. RGB565, BGRA8888, RGBA8888, RGBA/BGRA1010102, Gray8, +Alpha8, row padding, and the reference byte-width fallback retain the pinned +C# channel and rounding rules. + +## Binary cache and source builds + +`skia-safe` 0.99.0 downloads an official prebuilt Skia archive when the target +and Cargo feature set match a published archive. MetaCrate deliberately selects +the published WebP-capable feature sets: + +- Linux and Windows: JPEG, PDF, SVG, text layout, Vulkan, and WebP; +- macOS: JPEG, PDF, SVG, text layout, and WebP. + +Vulkan is selected on Linux and Windows only to match the published CPU/WebP +archive; this adapter does not create a GPU context or call a platform graphics +API. The same feature is therefore usable on macOS, Linux, and Windows without +leaking platform-specific behavior. + +Build prerequisites for the supported desktop targets are: + +- Linux: a C++ linker/runtime, `curl`, `pkg-config`, FreeType, and Fontconfig; +- macOS: Xcode command-line tools and `curl`; +- Windows MSVC: the Rust MSVC toolchain, Visual Studio C++ build tools, and + `curl`. + +The build script downloads cache archives from the `rust-skia/skia-binaries` +GitHub releases. Pin or mirror `SKIA_BINARIES_URL` in offline/reproducible build +environments. `FORCE_SKIA_BINARIES_DOWNLOAD=1` makes a missing archive fail +instead of compiling Skia. If no archive matches and that variable is absent, +rust-skia falls back to a source build, which additionally requires Python 3, +Ninja, and an LLVM/Clang toolchain. `FORCE_SKIA_BUILD=1` selects that path +explicitly. + +## Licensing and redistribution + +`skia-safe` and rust-skia's bindings are MIT licensed; the linked Skia library +is BSD-3-Clause licensed. Official binary-cache archives contain compiled Skia. +Products that redistribute the resulting native artifacts must preserve the +applicable MIT and BSD notices and audit the exact archive they ship. diff --git a/crates/libremetaverse-imaging-skia/src/backend.rs b/crates/libremetaverse-imaging-skia/src/backend.rs new file mode 100644 index 0000000..7425958 --- /dev/null +++ b/crates/libremetaverse-imaging-skia/src/backend.rs @@ -0,0 +1,134 @@ +//! Project-owned representation of the mapped `SkiaSharp.SKBitmap` boundary. + +use crate::Error; +use libremetaverse_imaging::DEFAULT_MAX_PIXELS; + +/// Pixel formats used by the pinned `SkiaTextureCodec` implementation. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum SKColorType { + Rgb565, + Bgra8888, + Rgba8888, + Rgba1010102, + Bgra1010102, + Gray8, + Alpha8, + /// Another Skia format handled by the reference's byte-width fallback. + Other { + bytes_per_pixel: usize, + }, +} + +impl SKColorType { + const fn storage_bytes(self) -> usize { + match self { + Self::Rgb565 => 2, + Self::Bgra8888 | Self::Rgba8888 | Self::Rgba1010102 | Self::Bgra1010102 => 4, + Self::Gray8 | Self::Alpha8 => 1, + Self::Other { bytes_per_pixel } => bytes_per_pixel, + } + } +} + +/// Alpha representation attached to bitmap samples. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum SKAlphaType { + Opaque, + Premul, + Unpremul, + Unknown, +} + +/// Owned, checked bitmap used by the fixed mapped signature. +/// +/// This deliberately does not expose a Skia allocation or pointer. Feature- +/// enabled decoding copies the native pixmap into this bounded Rust buffer +/// before conversion to the core imaging abstraction. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SKBitmap { + width: i32, + height: i32, + row_bytes: usize, + color_type: SKColorType, + alpha_type: SKAlphaType, + pixels: Vec, +} + +impl SKBitmap { + /// Creates a bitmap with an explicit row stride and owned pixels. + /// + /// # Errors + /// + /// Returns [`Error::Argument`] for invalid dimensions, formats, strides, + /// lengths, or over-limit storage. + pub fn new( + width: i32, + height: i32, + row_bytes: usize, + color_type: SKColorType, + alpha_type: SKAlphaType, + pixels: Vec, + ) -> Result { + let width_usize = usize::try_from(width).map_err(|_| Error::Argument)?; + let height_usize = usize::try_from(height).map_err(|_| Error::Argument)?; + let pixel_count = width_usize + .checked_mul(height_usize) + .filter(|count| *count > 0 && *count <= DEFAULT_MAX_PIXELS) + .ok_or(Error::Argument)?; + let storage_bytes = color_type.storage_bytes(); + if storage_bytes == 0 || storage_bytes > 16 { + return Err(Error::Argument); + } + let packed_row = width_usize + .checked_mul(storage_bytes) + .ok_or(Error::Argument)?; + if row_bytes < packed_row { + return Err(Error::Argument); + } + let required = row_bytes + .checked_mul(height_usize.saturating_sub(1)) + .and_then(|prefix| prefix.checked_add(packed_row)) + .ok_or(Error::Argument)?; + if pixels.len() < required + || pixels.len() + > pixel_count + .checked_mul(16) + .and_then(|size| size.checked_add(row_bytes)) + .ok_or(Error::Argument)? + { + return Err(Error::Argument); + } + Ok(Self { + width, + height, + row_bytes, + color_type, + alpha_type, + pixels, + }) + } + + pub(crate) const fn width(&self) -> i32 { + self.width + } + + pub(crate) const fn height(&self) -> i32 { + self.height + } + + pub(crate) const fn row_bytes(&self) -> usize { + self.row_bytes + } + + pub(crate) const fn color_type(&self) -> SKColorType { + self.color_type + } + + pub(crate) const fn alpha_type(&self) -> SKAlphaType { + self.alpha_type + } + + pub(crate) fn pixels(&self) -> &[u8] { + &self.pixels + } +} diff --git a/crates/libremetaverse-imaging-skia/src/generated.rs b/crates/libremetaverse-imaging-skia/src/generated.rs index 7122e4b..017d47b 100644 --- a/crates/libremetaverse-imaging-skia/src/generated.rs +++ b/crates/libremetaverse-imaging-skia/src/generated.rs @@ -6,29 +6,7 @@ #![allow(non_snake_case)] /// C# type: `T:LibreMetaverse.Imaging.Skia.SkiaTextureCodec`. -pub struct SkiaTextureCodec; -impl SkiaTextureCodec { - /// C# member: `M:LibreMetaverse.Imaging.Skia.SkiaTextureCodec.#ctor`. - pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Imaging.Skia.SkiaTextureCodec.#ctor", - ) - } - /// C# member: `M:LibreMetaverse.Imaging.Skia.SkiaTextureCodec.Decode(System.IO.Stream)`. - pub fn decode( - &self, - stream: Box, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Imaging.Skia.SkiaTextureCodec.Decode(System.IO.Stream)", - ) - } - /// C# member: `M:LibreMetaverse.Imaging.Skia.SkiaTextureCodec.ToManagedImage(SkiaSharp.SKBitmap)`. - pub fn to_managed_image( - bitmap: libremetaverse_imaging_skia::backend::SKBitmap, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Imaging.Skia.SkiaTextureCodec.ToManagedImage(SkiaSharp.SKBitmap)", - ) - } -} +/// C# member: `M:LibreMetaverse.Imaging.Skia.SkiaTextureCodec.#ctor`. +/// C# member: `M:LibreMetaverse.Imaging.Skia.SkiaTextureCodec.Decode(System.IO.Stream)`. +/// C# member: `M:LibreMetaverse.Imaging.Skia.SkiaTextureCodec.ToManagedImage(SkiaSharp.SKBitmap)`. +pub use crate::skia_codec::SkiaTextureCodec; diff --git a/crates/libremetaverse-imaging-skia/src/lib.rs b/crates/libremetaverse-imaging-skia/src/lib.rs index 86956fa..50b9fec 100644 --- a/crates/libremetaverse-imaging-skia/src/lib.rs +++ b/crates/libremetaverse-imaging-skia/src/lib.rs @@ -2,20 +2,15 @@ extern crate self as libremetaverse_imaging_skia; -pub mod backend { - /// Project-owned bitmap boundary; no `SkiaSharp` API is copied. - pub struct SKBitmap; -} +pub mod backend; mod generated; +mod skia_codec; pub use generated::*; pub use libremetaverse_imaging as imaging; pub use libremetaverse_types::Error; -// The native codec abstraction requires an explicit implementation. Until the -// optional Skia adapter is ported, forward through its cataloged typed-failure -// method rather than manufacturing an image or silently accepting input. impl libremetaverse_imaging::ITextureCodec for SkiaTextureCodec { fn decode( &self, diff --git a/crates/libremetaverse-imaging-skia/src/skia_codec.rs b/crates/libremetaverse-imaging-skia/src/skia_codec.rs new file mode 100644 index 0000000..f7c9d01 --- /dev/null +++ b/crates/libremetaverse-imaging-skia/src/skia_codec.rs @@ -0,0 +1,579 @@ +//! Native implementation of the pinned `SkiaTextureCodec` behavior. + +use crate::Error; +use crate::backend::{SKAlphaType, SKBitmap, SKColorType}; +#[cfg(feature = "skia")] +use libremetaverse_imaging::{DEFAULT_MAX_ENCODED_BYTES, DEFAULT_MAX_PIXELS}; +use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels}; +use libremetaverse_types::compat::ReadWrite; +#[cfg(feature = "skia")] +use std::io::Read; + +/// Optional Skia-backed decoder and format-neutral bitmap converter. +#[derive(Clone, Copy, Debug, Default)] +pub struct SkiaTextureCodec; + +impl SkiaTextureCodec { + /// Creates the stateless codec adapter. + /// + /// # Errors + /// + /// This fixed compatibility constructor cannot fail. + pub const fn new() -> Result { + Ok(Self) + } + + /// Decodes a bounded compressed-image stream through Skia. + /// + /// # Errors + /// + /// With the `skia` feature, returns a typed argument, parse, or operation + /// error for oversized or malformed input and unsupported native output. + /// Without the feature it returns [`Error::InvalidOperation`]. + pub fn decode(&self, mut stream: Box) -> Result { + #[cfg(feature = "skia")] + { + let mut encoded = Vec::new(); + Read::by_ref(&mut stream) + .take((DEFAULT_MAX_ENCODED_BYTES + 1) as u64) + .read_to_end(&mut encoded) + .map_err(|_| Error::InvalidOperation)?; + if encoded.is_empty() { + return Err(Error::InvalidOperation); + } + if encoded.len() > DEFAULT_MAX_ENCODED_BYTES { + return Err(Error::Argument); + } + decode_with_skia(&encoded) + } + #[cfg(not(feature = "skia"))] + { + let _ = &mut stream; + Err(Error::InvalidOperation) + } + } + + /// Converts an owned mapped bitmap using the exact reference channel rules. + /// + /// # Errors + /// + /// Returns a typed error for invalid dimensions, storage, or an unsupported + /// fallback byte width. + #[allow(clippy::needless_pass_by_value)] // fixed mapped C# signature + pub fn to_managed_image(bitmap: SKBitmap) -> Result { + bitmap_to_managed(&bitmap) + } +} + +// Keeping the format branches together makes comparison with the pinned C# +// color switch auditable and prevents subtly different indexing paths. +#[allow(clippy::too_many_lines)] +fn bitmap_to_managed(bitmap: &SKBitmap) -> Result { + let width = usize::try_from(bitmap.width()).map_err(|_| Error::Argument)?; + let height = usize::try_from(bitmap.height()).map_err(|_| Error::Argument)?; + let reference_step = bitmap.row_bytes() / width.max(1); + let channels = match bitmap.color_type() { + SKColorType::Rgb565 => ManagedImageImageChannels::COLOR, + SKColorType::Bgra8888 + | SKColorType::Rgba8888 + | SKColorType::Rgba1010102 + | SKColorType::Bgra1010102 => { + ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA + } + SKColorType::Gray8 => ManagedImageImageChannels::GRAY, + SKColorType::Alpha8 => ManagedImageImageChannels::ALPHA, + SKColorType::Other { .. } if reference_step == 1 => ManagedImageImageChannels::GRAY, + SKColorType::Other { .. } if reference_step == 3 => ManagedImageImageChannels::COLOR, + SKColorType::Other { .. } if reference_step == 4 => { + ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA + } + SKColorType::Other { .. } => return Err(Error::InvalidOperation), + }; + let mut image = ManagedImage::new(bitmap.width(), bitmap.height(), channels)?; + for y in 0..height { + let row = y.checked_mul(bitmap.row_bytes()).ok_or(Error::Argument)?; + for x in 0..width { + let pixel = y + .checked_mul(width) + .and_then(|offset| offset.checked_add(x)) + .ok_or(Error::Argument)?; + match bitmap.color_type() { + SKColorType::Rgb565 => { + let offset = checked_offset(row, x, 2, bitmap.pixels().len())?; + let packed = + u16::from_le_bytes([bitmap.pixels()[offset], bitmap.pixels()[offset + 1]]); + image.red[pixel] = scale_bits(u32::from((packed >> 11) & 0x1f), 31); + image.green[pixel] = scale_bits(u32::from((packed >> 5) & 0x3f), 63); + image.blue[pixel] = scale_bits(u32::from(packed & 0x1f), 31); + } + SKColorType::Bgra8888 | SKColorType::Rgba8888 => { + let offset = checked_offset(row, x, reference_step, bitmap.pixels().len())?; + let source = &bitmap.pixels()[offset..offset + 4]; + let (red, green, blue, alpha) = if bitmap.color_type() == SKColorType::Rgba8888 + { + (source[0], source[1], source[2], source[3]) + } else { + (source[2], source[1], source[0], source[3]) + }; + let (red, green, blue) = + normalize_8bit_alpha(red, green, blue, alpha, bitmap.alpha_type()); + image.red[pixel] = red; + image.green[pixel] = green; + image.blue[pixel] = blue; + image.alpha[pixel] = alpha; + } + SKColorType::Rgba1010102 | SKColorType::Bgra1010102 => { + let offset = checked_offset(row, x, 4, bitmap.pixels().len())?; + let packed = u32::from_le_bytes( + bitmap.pixels()[offset..offset + 4] + .try_into() + .map_err(|_| Error::Argument)?, + ); + let first = packed & 0x3ff; + let green = (packed >> 10) & 0x3ff; + let third = (packed >> 20) & 0x3ff; + let alpha = (packed >> 30) & 0x3; + let (red, blue) = if bitmap.color_type() == SKColorType::Rgba1010102 { + (first, third) + } else { + (third, first) + }; + let (red, green, blue) = + normalize_10bit_alpha(red, green, blue, alpha, bitmap.alpha_type()); + image.red[pixel] = scale_bits(red, 1023); + image.green[pixel] = scale_bits(green, 1023); + image.blue[pixel] = scale_bits(blue, 1023); + image.alpha[pixel] = u8::try_from(alpha * 85).map_err(|_| Error::Argument)?; + } + SKColorType::Gray8 => { + let offset = checked_offset(row, x, reference_step, bitmap.pixels().len())?; + image.red[pixel] = bitmap.pixels()[offset]; + } + SKColorType::Alpha8 => { + let offset = checked_offset(row, x, reference_step, bitmap.pixels().len())?; + image.alpha[pixel] = bitmap.pixels()[offset]; + } + SKColorType::Other { .. } => { + let offset = checked_offset(row, x, reference_step, bitmap.pixels().len())?; + match reference_step { + 4 => { + let alpha = bitmap.pixels()[offset + 3]; + let (red, green, blue) = normalize_8bit_alpha( + bitmap.pixels()[offset + 2], + bitmap.pixels()[offset + 1], + bitmap.pixels()[offset], + alpha, + bitmap.alpha_type(), + ); + image.red[pixel] = red; + image.green[pixel] = green; + image.blue[pixel] = blue; + image.alpha[pixel] = alpha; + } + 3 => { + image.blue[pixel] = bitmap.pixels()[offset]; + image.green[pixel] = bitmap.pixels()[offset + 1]; + image.red[pixel] = bitmap.pixels()[offset + 2]; + } + 1 => image.red[pixel] = bitmap.pixels()[offset], + _ => return Err(Error::InvalidOperation), + } + } + } + } + } + Ok(image) +} + +fn checked_offset(row: usize, x: usize, step: usize, length: usize) -> Result { + let offset = x + .checked_mul(step) + .and_then(|offset| row.checked_add(offset)) + .ok_or(Error::Argument)?; + offset + .checked_add(step) + .filter(|end| *end <= length) + .map(|_| offset) + .ok_or(Error::Argument) +} + +fn scale_bits(value: u32, maximum: u32) -> u8 { + u8::try_from((value * 255 + maximum / 2) / maximum).unwrap_or(u8::MAX) +} + +fn normalize_8bit_alpha( + red: u8, + green: u8, + blue: u8, + alpha: u8, + alpha_type: SKAlphaType, +) -> (u8, u8, u8) { + if alpha_type != SKAlphaType::Premul || alpha == u8::MAX { + return (red, green, blue); + } + if alpha == 0 { + return (0, 0, 0); + } + let normalize = |value: u8| { + u8::try_from((u32::from(value) * 255 + u32::from(alpha) / 2) / u32::from(alpha)) + .unwrap_or(u8::MAX) + }; + (normalize(red), normalize(green), normalize(blue)) +} + +fn normalize_10bit_alpha( + red: u32, + green: u32, + blue: u32, + alpha: u32, + alpha_type: SKAlphaType, +) -> (u32, u32, u32) { + if alpha_type != SKAlphaType::Premul || alpha == 3 { + return (red, green, blue); + } + if alpha == 0 { + return (0, 0, 0); + } + let normalize = |value: u32| (value * 3 + alpha / 2) / alpha; + (normalize(red), normalize(green), normalize(blue)) +} + +#[cfg(feature = "skia")] +fn decode_with_skia(encoded: &[u8]) -> Result { + use skia_safe::Data; + use skia_safe::codec::{Codec, Options, Result as CodecResult}; + + let mut codec = Codec::from_data(Data::new_copy(encoded)).ok_or(Error::InvalidOperation)?; + let source_info = codec.info(); + let width = usize::try_from(source_info.width()).map_err(|_| Error::Argument)?; + let height = usize::try_from(source_info.height()).map_err(|_| Error::Argument)?; + width + .checked_mul(height) + .filter(|pixels| *pixels > 0 && *pixels <= DEFAULT_MAX_PIXELS) + .ok_or(Error::Argument)?; + // Match SKBitmap.Decode: decode into Skia's native alpha representation, + // then normalize premultiplied channels at the abstraction boundary. + let target_info = source_info.clone(); + let row_bytes = target_info.min_row_bytes(); + let length = target_info.compute_byte_size(row_bytes); + if length == usize::MAX || length > DEFAULT_MAX_PIXELS * 16 { + return Err(Error::Argument); + } + let mut pixels = Vec::new(); + pixels + .try_reserve_exact(length) + .map_err(|_| Error::InvalidOperation)?; + pixels.resize(length, 0); + let result = codec.get_pixels_with_options( + &target_info, + &mut pixels, + row_bytes, + Some(&Options { + max_decode_memory: Some(DEFAULT_MAX_PIXELS * 16), + ..Options::default() + }), + ); + if result != CodecResult::Success { + return Err(Error::InvalidOperation); + } + let bitmap = SKBitmap::new( + source_info.width(), + source_info.height(), + row_bytes, + map_color_type(target_info.color_type(), target_info.bytes_per_pixel()), + map_alpha_type(target_info.alpha_type()), + pixels, + )?; + bitmap_to_managed(&bitmap) +} + +#[cfg(feature = "skia")] +const fn map_color_type(color: skia_safe::ColorType, bytes_per_pixel: usize) -> SKColorType { + use skia_safe::ColorType; + match color { + ColorType::RGB565 => SKColorType::Rgb565, + ColorType::BGRA8888 => SKColorType::Bgra8888, + ColorType::RGBA8888 => SKColorType::Rgba8888, + ColorType::RGBA1010102 => SKColorType::Rgba1010102, + ColorType::BGRA1010102 => SKColorType::Bgra1010102, + ColorType::Gray8 => SKColorType::Gray8, + ColorType::Alpha8 => SKColorType::Alpha8, + _ => SKColorType::Other { bytes_per_pixel }, + } +} + +#[cfg(feature = "skia")] +const fn map_alpha_type(alpha: skia_safe::AlphaType) -> SKAlphaType { + use skia_safe::AlphaType; + match alpha { + AlphaType::Opaque => SKAlphaType::Opaque, + AlphaType::Premul => SKAlphaType::Premul, + AlphaType::Unpremul => SKAlphaType::Unpremul, + AlphaType::Unknown => SKAlphaType::Unknown, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mapped_bitmap_formats_preserve_reference_channel_rules() { + let rgba = SKBitmap::new( + 2, + 1, + 8, + SKColorType::Rgba8888, + SKAlphaType::Unpremul, + vec![1, 2, 3, 4, 5, 6, 7, 8], + ) + .unwrap(); + let image = SkiaTextureCodec::to_managed_image(rgba).unwrap(); + assert_eq!(image.red, [1, 5]); + assert_eq!(image.green, [2, 6]); + assert_eq!(image.blue, [3, 7]); + assert_eq!(image.alpha, [4, 8]); + + let rgb565 = SKBitmap::new( + 3, + 1, + 6, + SKColorType::Rgb565, + SKAlphaType::Opaque, + vec![0x00, 0xf8, 0xe0, 0x07, 0x1f, 0x00], + ) + .unwrap(); + let image = SkiaTextureCodec::to_managed_image(rgb565).unwrap(); + assert_eq!(image.red, [255, 0, 0]); + assert_eq!(image.green, [0, 255, 0]); + assert_eq!(image.blue, [0, 0, 255]); + + // The reference fallback derives its byte width from rowBytes / width, + // even when the declared native format has a different packed width. + let padded_unknown = SKBitmap::new( + 2, + 1, + 8, + SKColorType::Other { bytes_per_pixel: 3 }, + SKAlphaType::Unpremul, + vec![3, 2, 1, 4, 7, 6, 5, 8], + ) + .unwrap(); + let image = SkiaTextureCodec::to_managed_image(padded_unknown).unwrap(); + assert_eq!( + image.channels, + ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA + ); + assert_eq!(image.red, [1, 5]); + assert_eq!(image.green, [2, 6]); + assert_eq!(image.blue, [3, 7]); + assert_eq!(image.alpha, [4, 8]); + } + + #[test] + fn grayscale_alpha_ten_bit_and_premul_are_converted() { + let gray = SKBitmap::new( + 2, + 1, + 2, + SKColorType::Gray8, + SKAlphaType::Opaque, + vec![11, 239], + ) + .unwrap(); + let gray = SkiaTextureCodec::to_managed_image(gray).unwrap(); + assert_eq!(gray.channels, ManagedImageImageChannels::GRAY); + assert_eq!(gray.red, [11, 239]); + + let premul = SKBitmap::new( + 1, + 1, + 4, + SKColorType::Bgra8888, + SKAlphaType::Premul, + vec![25, 50, 100, 128], + ) + .unwrap(); + let premul = SkiaTextureCodec::to_managed_image(premul).unwrap(); + assert_eq!(premul.red, [199]); + assert_eq!(premul.green, [100]); + assert_eq!(premul.blue, [50]); + assert_eq!(premul.alpha, [128]); + + let transparent_premul = SKBitmap::new( + 1, + 1, + 4, + SKColorType::Rgba8888, + SKAlphaType::Premul, + vec![100, 50, 25, 0], + ) + .unwrap(); + let transparent_premul = SkiaTextureCodec::to_managed_image(transparent_premul).unwrap(); + assert_eq!(transparent_premul.red, [0]); + assert_eq!(transparent_premul.green, [0]); + assert_eq!(transparent_premul.blue, [0]); + assert_eq!(transparent_premul.alpha, [0]); + + let packed = 1023_u32 | (512 << 10) | (1 << 20) | (3 << 30); + let ten_bit = SKBitmap::new( + 1, + 1, + 4, + SKColorType::Rgba1010102, + SKAlphaType::Unpremul, + packed.to_le_bytes().to_vec(), + ) + .unwrap(); + let ten_bit = SkiaTextureCodec::to_managed_image(ten_bit).unwrap(); + assert_eq!(ten_bit.red, [255]); + assert_eq!(ten_bit.green, [128]); + assert_eq!(ten_bit.blue, [0]); + assert_eq!(ten_bit.alpha, [255]); + + let packed = 1023_u32 | (512 << 10) | (1 << 20) | (2 << 30); + let ten_bit = SKBitmap::new( + 1, + 1, + 4, + SKColorType::Bgra1010102, + SKAlphaType::Unpremul, + packed.to_le_bytes().to_vec(), + ) + .unwrap(); + let ten_bit = SkiaTextureCodec::to_managed_image(ten_bit).unwrap(); + assert_eq!(ten_bit.red, [0]); + assert_eq!(ten_bit.green, [128]); + assert_eq!(ten_bit.blue, [255]); + assert_eq!(ten_bit.alpha, [170]); + } + + #[test] + fn alpha_and_reference_fallback_layouts_are_converted_or_rejected() { + let alpha = SKBitmap::new( + 2, + 1, + 2, + SKColorType::Alpha8, + SKAlphaType::Unpremul, + vec![17, 241], + ) + .unwrap(); + let alpha = SkiaTextureCodec::to_managed_image(alpha).unwrap(); + assert_eq!(alpha.channels, ManagedImageImageChannels::ALPHA); + assert_eq!(alpha.alpha, [17, 241]); + + let bgr = SKBitmap::new( + 2, + 1, + 6, + SKColorType::Other { bytes_per_pixel: 3 }, + SKAlphaType::Opaque, + vec![3, 2, 1, 6, 5, 4], + ) + .unwrap(); + let bgr = SkiaTextureCodec::to_managed_image(bgr).unwrap(); + assert_eq!(bgr.channels, ManagedImageImageChannels::COLOR); + assert_eq!(bgr.red, [1, 4]); + assert_eq!(bgr.green, [2, 5]); + assert_eq!(bgr.blue, [3, 6]); + + let gray = SKBitmap::new( + 2, + 1, + 2, + SKColorType::Other { bytes_per_pixel: 1 }, + SKAlphaType::Opaque, + vec![23, 229], + ) + .unwrap(); + let gray = SkiaTextureCodec::to_managed_image(gray).unwrap(); + assert_eq!(gray.channels, ManagedImageImageChannels::GRAY); + assert_eq!(gray.red, [23, 229]); + + let unsupported = SKBitmap::new( + 1, + 1, + 2, + SKColorType::Other { bytes_per_pixel: 2 }, + SKAlphaType::Opaque, + vec![0, 0], + ) + .unwrap(); + assert_eq!( + SkiaTextureCodec::to_managed_image(unsupported), + Err(Error::InvalidOperation) + ); + } + + #[test] + fn bitmap_bounds_and_feature_disabled_decode_fail_typed() { + assert_eq!( + SKBitmap::new(0, 1, 0, SKColorType::Gray8, SKAlphaType::Opaque, Vec::new(),), + Err(Error::Argument) + ); + assert_eq!( + SKBitmap::new( + 2, + 2, + 4, + SKColorType::Rgba8888, + SKAlphaType::Unpremul, + vec![0; 16], + ), + Err(Error::Argument) + ); + #[cfg(not(feature = "skia"))] + assert_eq!( + SkiaTextureCodec.decode(Box::new(std::io::Cursor::new(Vec::new()))), + Err(Error::InvalidOperation) + ); + } + + #[cfg(feature = "skia")] + #[test] + fn feature_decodes_known_png_with_straight_alpha() { + const PNG: &[u8] = &[ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, + 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, + 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41, 0x54, 0x78, + 0x9c, 0x63, 0xf8, 0xcf, 0xc0, 0xf0, 0x1f, 0x00, 0x05, 0x00, 0x01, 0xff, 0x89, 0x99, + 0x3d, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, + ]; + let image = SkiaTextureCodec + .decode(Box::new(std::io::Cursor::new(PNG.to_vec()))) + .expect("decode PNG"); + assert_eq!((image.width, image.height), (1, 1)); + assert_eq!(image.red, [255]); + assert_eq!(image.green, [0]); + assert_eq!(image.blue, [0]); + assert_eq!(image.alpha, [255]); + + assert_eq!( + SkiaTextureCodec.decode(Box::new(std::io::Cursor::new(Vec::new()))), + Err(Error::InvalidOperation) + ); + assert_eq!( + SkiaTextureCodec.decode(Box::new(std::io::Cursor::new(b"not an image".to_vec()))), + Err(Error::InvalidOperation) + ); + } + + #[cfg(feature = "skia")] + #[test] + fn feature_decodes_webp_from_the_cross_platform_cache_configuration() { + const WEBP: &[u8] = &[ + 0x52, 0x49, 0x46, 0x46, 0x1c, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, + 0x38, 0x4c, 0x0f, 0x00, 0x00, 0x00, 0x2f, 0x01, 0x40, 0x00, 0x00, 0x07, 0x10, 0xf5, + 0x8f, 0xfe, 0x07, 0x22, 0xa2, 0xff, 0x01, 0x00, + ]; + let image = SkiaTextureCodec + .decode(Box::new(std::io::Cursor::new(WEBP.to_vec()))) + .expect("decode WebP"); + assert_eq!((image.width, image.height), (2, 2)); + assert_eq!(image.red, [254; 4]); + assert_eq!(image.green, [0; 4]); + assert_eq!(image.blue, [0; 4]); + assert_eq!(image.alpha, [255; 4]); + } +} diff --git a/tests/red-suite-baseline.json b/tests/red-suite-baseline.json index c2ee820..218544d 100644 --- a/tests/red-suite-baseline.json +++ b/tests/red-suite-baseline.json @@ -101,5 +101,5 @@ "LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUUID::test", "LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUndef::test" ], - "support_passes": 115 + "support_passes": 119 } diff --git a/tools/generate_api_shims.py b/tools/generate_api_shims.py index a32df96..416c091 100644 --- a/tools/generate_api_shims.py +++ b/tools/generate_api_shims.py @@ -38,6 +38,7 @@ TARGETS = { # implementations. The generated module keeps catalog markers and re-exports # the hand-written type so coverage remains deterministic. NATIVE_TYPES = { + "T:LibreMetaverse.Imaging.Skia.SkiaTextureCodec": "crate::skia_codec::SkiaTextureCodec", "T:LibreMetaverse.Imaging.ManagedImageCreator": "crate::j2k::ManagedImageCreator", "T:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions": "crate::j2k::ManagedImageInterleavedExtensions", "T:LibreMetaverse.Imaging.Targa": "crate::targa::Targa",