// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Rendered glyph image.
///
/// Ported from upstream `swash/src/scale/image.rs` (Apache-2.0 OR MIT).
///|
/// Content of a scaled glyph image.
pub(all) enum Content {
/// 8-bit alpha mask.
Mask
/// 32-bit RGBA subpixel mask.
SubpixelMask
/// 32-bit RGBA bitmap.
Color
}
///|
pub fn Content::default() -> Content {
Content::Mask
}
///|
/// Scaled glyph image.
pub struct Image {
/// Source of the image.
mut source : Source
/// Content of the image.
mut content : Content
/// Offset and size of the image.
mut placement : Placement
/// Raw image data.
mut data : Array[Byte]
}
///|
///|
pub fn Image::Image() -> Image {
Image::{
source: Source::default(),
content: Content::default(),
placement: Placement::{ left: 0, top: 0, width: 0U, height: 0U },
data: [],
}
}
///|
pub fn Image::clear(self : Image) -> Unit {
self.source = Source::default()
self.content = Content::default()
self.placement = Placement::{ left: 0, top: 0, width: 0U, height: 0U }
self.data.clear()
}
///|
pub fn Image::source(self : Image) -> Source {
self.source
}
///|
pub fn Image::content(self : Image) -> Content {
self.content
}
///|
pub fn Image::placement(self : Image) -> Placement {
self.placement
}
///|
/// Zero-copy view of the raw image data.
///
/// This is preferred over `Image::data()` when integrating with renderers that
/// can upload from an `ArrayView[Byte]` directly.
pub fn Image::data_view(self : Image) -> ArrayView[Byte] {
self.data
}
///|
/// Copying accessor for image data.
///
/// Note: this clones the underlying bytes. Prefer `Image::data_view()` for
/// zero-copy access.
pub fn Image::data(self : Image) -> Bytes {
Bytes::from_array(self.data)
}
///|
/// Converts `Mask` / `SubpixelMask` content into an RGBA8 buffer in-place.
///
/// - `base_color` is `[r, g, b, a]` (0..255). The output is *premultiplied*
/// RGBA8, suitable for standard premultiplied-alpha blending.
/// - For `SubpixelMask`, the input stores per-channel coverage in RGB (alpha
/// channel is unused by the rasterizer). We multiply each channel by its
/// corresponding coverage and set output alpha to `max(r, g, b)` coverage.
///
/// Returns false if the image buffer/placement is inconsistent or
/// `base_color` is invalid.
pub fn Image::to_rgba8(self : Image, base_color : Array[Byte]) -> Bool {
if base_color.length() < 4 {
return false
}
let w = self.placement.width
let h = self.placement.height
let pixels = (w * h).reinterpret_as_int()
if pixels <= 0 {
// Empty image; normalize to RGBA8 for convenience.
self.data.clear()
self.content = Content::Color
return true
}
let br = base_color[0].to_int().reinterpret_as_uint()
let bg = base_color[1].to_int().reinterpret_as_uint()
let bb = base_color[2].to_int().reinterpret_as_uint()
let ba = base_color[3].to_int().reinterpret_as_uint()
match self.content {
Content::Color => true
Content::Mask => {
if self.data.length() < pixels {
return false
}
let out_len = pixels * 4
// Grow in-place if possible; write backwards to avoid clobbering input.
if self.data.capacity() < out_len {
let out = Array::makei(out_len, _ => b'\x00')
for i in 0.. out_len {
self.data.truncate(out_len)
}
let mut i = pixels - 1
while i >= 0 {
let m = self.data[i].to_int().reinterpret_as_uint()
let a = m * ba / 255U
let r = br * a / 255U
let g = bg * a / 255U
let b = bb * a / 255U
let o = i * 4
self.data[o + 0] = r.reinterpret_as_int().to_byte()
self.data[o + 1] = g.reinterpret_as_int().to_byte()
self.data[o + 2] = b.reinterpret_as_int().to_byte()
self.data[o + 3] = a.reinterpret_as_int().to_byte()
i = i - 1
}
}
self.content = Content::Color
true
}
Content::SubpixelMask => {
if self.data.length() < pixels * 4 {
return false
}
// In-place transform (same buffer size).
for i in 0.. ag {
if ar > ab {
ar
} else {
ab
}
} else if ag > ab {
ag
} else {
ab
}
self.data[o + 0] = (br * ar / 255U).reinterpret_as_int().to_byte()
self.data[o + 1] = (bg * ag / 255U).reinterpret_as_int().to_byte()
self.data[o + 2] = (bb * ab / 255U).reinterpret_as_int().to_byte()
self.data[o + 3] = a.reinterpret_as_int().to_byte()
}
self.content = Content::Color
true
}
}
}
///|
test "Image::new produces empty image" {
let img = Image::Image()
inspect(
match img.source() {
Outline => true
_ => false
},
content="true",
)
inspect(
match img.content() {
Mask => true
_ => false
},
content="true",
)
inspect(img.placement().width, content="0")
inspect(img.data().length(), content="0")
}
///|
test "Image::clear resets fields" {
let img = Image::Image()
img.clear()
inspect(
match img.source() {
Outline => true
_ => false
},
content="true",
)
inspect(
match img.content() {
Mask => true
_ => false
},
content="true",
)
inspect(img.data().length(), content="0")
}
///|
test "Image::data_view is zero-copy length view" {
let img = Image::Image()
inspect(img.data_view().length(), content="0")
}
///|
test "Image::to_rgba8 converts Mask to premultiplied RGBA8" {
let img = Image::Image()
img.placement = Placement::{ left: 0, top: 0, width: 2U, height: 1U }
img.content = Content::Mask
img.data = [b'\x00', b'\xFF']
let ok = img.to_rgba8([
(10).to_byte(),
(20).to_byte(),
(30).to_byte(),
(255).to_byte(),
])
inspect(ok, content="true")
inspect(
match img.content() {
Color => true
_ => false
},
content="true",
)
let d = img.data_view()
inspect(d.length(), content="8")
// Pixel 0: alpha=0 => premultiplied => all zeros.
inspect(d[0].to_int(), content="0")
inspect(d[1].to_int(), content="0")
inspect(d[2].to_int(), content="0")
inspect(d[3].to_int(), content="0")
// Pixel 1: alpha=255 => premultiplied => base RGB.
inspect(d[4].to_int(), content="10")
inspect(d[5].to_int(), content="20")
inspect(d[6].to_int(), content="30")
inspect(d[7].to_int(), content="255")
}
///|
test "Image::to_rgba8 converts SubpixelMask using per-channel coverage" {
let img = Image::Image()
img.placement = Placement::{ left: 0, top: 0, width: 1U, height: 1U }
img.content = Content::SubpixelMask
// Coverage only on the red subpixel.
img.data = [(255).to_byte(), (0).to_byte(), (0).to_byte(), (0).to_byte()]
let ok = img.to_rgba8([
(100).to_byte(),
(200).to_byte(),
(50).to_byte(),
(255).to_byte(),
])
inspect(ok, content="true")
let d = img.data_view()
inspect(d[0].to_int(), content="100")
inspect(d[1].to_int(), content="0")
inspect(d[2].to_int(), content="0")
inspect(d[3].to_int(), content="255")
}