use alpha2_media::{EmojiPolicy, TextAlign};
use alpha2_text::{TextFont, TextRasterRequest, TextRasterResult};
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub(crate) struct TextRequestWire {
text: String,
font_size: f32,
#[serde(default)]
max_width: f32,
#[serde(default = "default_line_height")]
line_height: f32,
#[serde(default)]
align: AlignWire,
color: [u8; 4],
#[serde(default)]
emoji_policy: EmojiWire,
#[serde(default)]
outline: Option<OutlineWire>,
#[serde(default)]
background: Option<BackgroundWire>,
#[serde(default)]
font_family: Option<String>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct OutlineWire {
color: [u8; 4],
width: f32,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct BackgroundWire {
color: [u8; 4],
padding: f32,
}
const fn default_line_height() -> f32 {
1.2
}
#[derive(Default, Deserialize)]
#[serde(rename_all = "lowercase")]
enum AlignWire {
#[default]
Start,
Center,
End,
}
#[derive(Default, Deserialize)]
#[serde(rename_all = "lowercase")]
enum EmojiWire {
#[default]
Color,
Monochrome,
Replace,
}
impl TextRequestWire {
pub(crate) fn into_request(self, content_version: u64) -> TextRasterRequest {
TextRasterRequest {
text: self.text,
font_size: self.font_size,
max_width: self.max_width,
line_height: self.line_height,
align: match self.align {
AlignWire::Start => TextAlign::Start,
AlignWire::Center => TextAlign::Center,
AlignWire::End => TextAlign::End,
},
color: self.color,
emoji_policy: match self.emoji_policy {
EmojiWire::Color => EmojiPolicy::Color,
EmojiWire::Monochrome => EmojiPolicy::Monochrome,
EmojiWire::Replace => EmojiPolicy::Replace,
},
content_version,
outline: self.outline.map(|outline| alpha2_text::TextOutline {
color: outline.color,
width: outline.width,
}),
background: self
.background
.map(|background| alpha2_text::TextBackground {
color: background.color,
padding: background.padding,
}),
font_family: self.font_family,
}
}
}
pub(crate) fn descriptor_json(result: &TextRasterResult) -> String {
let descriptor = &result.descriptor;
serde_json::json!({
"width": descriptor.texture.width,
"height": descriptor.texture.height,
"layoutX": descriptor.layout_x,
"layoutY": descriptor.layout_y,
"layoutWidth": descriptor.layout_width,
"layoutHeight": descriptor.layout_height,
"firstBaseline": descriptor.first_baseline,
"ascent": descriptor.ascent,
"descent": descriptor.descent,
"lineCount": descriptor.line_count,
})
.to_string()
}
pub(crate) fn font(family: String, bytes: Vec<u8>, face_index: u32) -> TextFont {
TextFont {
family,
bytes: bytes.into(),
face_index,
}
}
#[cfg(target_arch = "wasm32")]
mod web {
use std::sync::Arc;
use super::{TextRasterResult, TextRequestWire, descriptor_json, font};
use alpha2_text::TextRasterizer;
use wasm_bindgen::prelude::*;
use web_sys::js_sys::Uint8Array;
use crate::web::{invalid_input, typed_error};
#[wasm_bindgen]
pub struct WasmTextRasterizer {
inner: TextRasterizer,
}
#[wasm_bindgen]
pub struct RasterizedText {
descriptor: String,
raster: Option<Arc<TextRasterResult>>,
}
#[wasm_bindgen]
impl RasterizedText {
#[wasm_bindgen(getter)]
pub fn descriptor(&self) -> String {
self.descriptor.clone()
}
#[wasm_bindgen(js_name = takePixels)]
pub fn take_pixels(&mut self) -> Uint8Array {
self.raster.take().map_or_else(
|| Uint8Array::new_with_length(0),
|raster| Uint8Array::from(raster.pixels.rgba8.as_slice()),
)
}
}
impl From<Arc<TextRasterResult>> for RasterizedText {
fn from(result: Arc<TextRasterResult>) -> Self {
Self {
descriptor: descriptor_json(&result),
raster: Some(result),
}
}
}
#[wasm_bindgen]
impl WasmTextRasterizer {
#[wasm_bindgen(constructor)]
#[must_use]
pub fn new() -> Self {
Self {
inner: TextRasterizer::empty(),
}
}
#[wasm_bindgen(js_name = addFont)]
pub fn add_font(
&mut self,
family: String,
bytes: Vec<u8>,
face_index: u32,
) -> Result<(), JsValue> {
self.inner
.add_font(font(family, bytes, face_index))
.map_err(|error| typed_error(&error))
}
pub fn rasterize(
&mut self,
request_json: &str,
content_version: u64,
) -> Result<RasterizedText, JsValue> {
let wire: TextRequestWire = serde_json::from_str(request_json)
.map_err(|error| invalid_input(format!("invalid text request: {error}")))?;
self.inner
.rasterize(&wire.into_request(content_version))
.map(RasterizedText::from)
.map_err(|error| typed_error(&error))
}
pub fn invalidate(&mut self, content_version: u64) {
self.inner.invalidate(content_version);
}
pub fn clear(&mut self) {
self.inner.clear();
}
#[wasm_bindgen(getter, js_name = cacheSize)]
#[must_use]
pub fn cache_size(&self) -> usize {
self.inner.cache_len()
}
}
impl Default for WasmTextRasterizer {
fn default() -> Self {
Self::new()
}
}
}
#[cfg(test)]
#[path = "text_tests.rs"]
mod tests;