use image::{Rgba, RgbaImage};
use super::SurfaceHelpers;
impl SurfaceHelpers {
pub(crate) fn surface_block_height(block_heights: impl Iterator<Item = u32>) -> u32 {
let content_height = block_heights.sum::<u32>() + super::PAGE_PADDING * 2;
content_height.max(super::SURFACE_MIN_HEIGHT)
}
pub(crate) fn parse_color(value: &str) -> Rgba<u8> {
let value = value.trim_start_matches('#');
if value.len() != HEX_COLOR_TEXT_LENGTH {
return DEFAULT_WHITE;
}
let red = u8::from_str_radix(&value[COLOR_RED_START..COLOR_GREEN_START], HEX_RADIX)
.unwrap_or(DEFAULT_CHANNEL);
let green = u8::from_str_radix(&value[COLOR_GREEN_START..COLOR_BLUE_START], HEX_RADIX)
.unwrap_or(DEFAULT_CHANNEL);
let blue = u8::from_str_radix(&value[COLOR_BLUE_START..HEX_COLOR_TEXT_LENGTH], HEX_RADIX)
.unwrap_or(DEFAULT_CHANNEL);
Rgba([red, green, blue, MAX_ALPHA_U8])
}
pub(crate) fn draw_quote_bars(
image: &mut RgbaImage,
depth: u32,
y: u32,
height: u32,
color: Rgba<u8>,
) {
for index in 0..depth {
let x = super::PAGE_PADDING + index * super::QUOTE_INDENT;
for dy in 0..height {
for dx in 0..SUPER_SMALL_ICON_SPAN {
Self::put_pixel_if_inside(image, x + dx, y + dy, color);
}
}
}
}
pub(crate) fn fill_rect(
image: &mut RgbaImage,
x: u32,
y: u32,
width: u32,
height: u32,
color: Rgba<u8>,
) {
for dy in 0..height {
for dx in 0..width {
Self::put_pixel_if_inside(image, x + dx, y + dy, color);
}
}
}
pub(crate) fn stroke_rect(
image: &mut RgbaImage,
x: u32,
y: u32,
width: u32,
height: u32,
color: Rgba<u8>,
) {
if width == 0 || height == 0 {
return;
}
for dx in 0..width {
Self::put_pixel_if_inside(image, x + dx, y, color);
Self::put_pixel_if_inside(image, x + dx, y + height - 1, color);
}
for dy in 0..height {
Self::put_pixel_if_inside(image, x, y + dy, color);
Self::put_pixel_if_inside(image, x + width - 1, y + dy, color);
}
}
pub(crate) fn paste_rgba(image: &mut RgbaImage, source: &RgbaImage, x: u32, y: u32) {
for source_y in 0..source.height() {
for source_x in 0..source.width() {
let target_x = x + source_x;
let target_y = y + source_y;
if target_x >= image.width() || target_y >= image.height() {
continue;
}
let foreground = source.get_pixel(source_x, source_y);
let alpha = foreground[ALPHA_CHANNEL_INDEX] as u16;
if alpha == 0 {
continue;
}
let background = image.get_pixel(target_x, target_y);
let inverse_alpha = MAX_ALPHA_U16 - alpha;
let red = Self::blend_channel(foreground[0], background[0], alpha, inverse_alpha);
let green = Self::blend_channel(foreground[1], background[1], alpha, inverse_alpha);
let blue = Self::blend_channel(foreground[2], background[2], alpha, inverse_alpha);
image.put_pixel(target_x, target_y, Rgba([red, green, blue, MAX_ALPHA_U8]));
}
}
}
pub(crate) fn draw_fallback_text(
image: &mut RgbaImage,
x: u32,
y: u32,
text: &str,
color: Rgba<u8>,
) {
let width = (text.chars().count() as u32)
.saturating_mul(FALLBACK_CHAR_WIDTH)
.min(FALLBACK_MAX_WIDTH);
for dy in 0..FALLBACK_LINE_HEIGHT {
for dx in 0..width {
Self::put_pixel_if_inside(image, x + dx, y + dy, color);
}
}
}
fn blend_channel(foreground: u8, background: u8, alpha: u16, inverse_alpha: u16) -> u8 {
let blended = foreground as u16 * alpha + background as u16 * inverse_alpha;
(blended / MAX_ALPHA_U16) as u8
}
fn put_pixel_if_inside(image: &mut RgbaImage, x: u32, y: u32, color: Rgba<u8>) {
if x < image.width() && y < image.height() {
image.put_pixel(x, y, color);
}
}
}
#[cfg(test)]
#[path = "export_surface_helpers_canvas_tests.rs"]
mod tests;
const DEFAULT_WHITE: image::Rgba<u8> = Rgba([255, 255, 255, 255]);
const DEFAULT_CHANNEL: u8 = 255;
const MAX_ALPHA_U8: u8 = 255;
const MAX_ALPHA_U16: u16 = 255;
const HEX_COLOR_TEXT_LENGTH: usize = 6;
const HEX_RADIX: u32 = 16;
const COLOR_RED_START: usize = 0;
const COLOR_GREEN_START: usize = 2;
const COLOR_BLUE_START: usize = 4;
const ALPHA_CHANNEL_INDEX: usize = 3;
const SUPER_SMALL_ICON_SPAN: u32 = 4;
const FALLBACK_CHAR_WIDTH: u32 = 10;
const FALLBACK_MAX_WIDTH: u32 = 720;
const FALLBACK_LINE_HEIGHT: u32 = 18;