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

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]);
}
}