Implement optional Skia codec adapter (#41)
Some checks failed
JPEG 2000 feature / linux (push) Has been cancelled
JPEG 2000 feature / macos (push) Has been cancelled
JPEG 2000 feature / windows (push) Has been cancelled
Skia feature / linux (push) Has been cancelled
Skia feature / macos (push) Has been cancelled
Skia feature / windows (push) Has been cancelled

This commit is contained in:
2026-08-09 04:13:49 +00:00
parent 16501f2331
commit 4849bbda7b
13 changed files with 1295 additions and 39 deletions

50
.gitea/workflows/skia.yml Normal file
View File

@@ -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

427
Cargo.lock generated
View File

@@ -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"

View File

@@ -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.

View File

@@ -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. |

View File

@@ -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 |

View File

@@ -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

View File

@@ -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.

View File

@@ -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<u8>,
}
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<u8>,
) -> Result<Self, Error> {
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
}
}

View File

@@ -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<Self, crate::Error> {
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<dyn libremetaverse_types::compat::ReadWrite + Send>,
) -> Result<libremetaverse_imaging::ManagedImage, crate::Error> {
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_imaging::ManagedImage, crate::Error> {
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;

View File

@@ -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,

View File

@@ -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<Self, Error> {
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<dyn ReadWrite + Send>) -> Result<ManagedImage, Error> {
#[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<ManagedImage, Error> {
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<ManagedImage, Error> {
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<usize, Error> {
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<ManagedImage, Error> {
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]);
}
}

View File

@@ -101,5 +101,5 @@
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUUID::test",
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUndef::test"
],
"support_passes": 115
"support_passes": 119
}

View File

@@ -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",