refactor: add TextLineDrawer trait

This commit is contained in:
Aloxaf 2024-03-02 13:37:51 +08:00
parent a460f0a152
commit 319066e9a1
5 changed files with 96 additions and 59 deletions

View File

@ -2,6 +2,7 @@ use anyhow::{Context, Error};
use clipboard::{ClipboardContext, ClipboardProvider};
use image::Rgba;
use silicon::directories::PROJECT_DIRS;
use silicon::font::FontCollection;
use silicon::formatter::{ImageFormatter, ImageFormatterBuilder};
use silicon::utils::{Background, ShadowAdder, ToRgba};
use std::ffi::OsString;
@ -269,7 +270,7 @@ impl Config {
}
}
pub fn get_formatter(&self) -> Result<ImageFormatter, Error> {
pub fn get_formatter(&self) -> Result<ImageFormatter<FontCollection>, Error> {
let formatter = ImageFormatterBuilder::new()
.line_pad(self.line_pad)
.window_controls(!self.no_window_controls)

View File

@ -148,6 +148,7 @@ fn run() -> Result<(), Error> {
let mut formatter = config.get_formatter()?;
let image = formatter.format(&highlight, &theme);
let image = DynamicImage::ImageRgba8(image);
if config.to_clipboard {
dump_image_to_clipboard(&image)?;

View File

@ -21,7 +21,7 @@ use font_kit::font::Font;
use font_kit::hinting::HintingOptions;
use font_kit::properties::{Properties, Style, Weight};
use font_kit::source::SystemSource;
use image::{GenericImage, Pixel};
use image::{GenericImage, Pixel, Rgba, RgbaImage};
use imageproc::definitions::Clamp;
use imageproc::pixelops::weighted_sum;
use pathfinder_geometry::transform2d::Transform2F;
@ -29,6 +29,46 @@ use std::collections::HashMap;
use std::sync::Arc;
use syntect::highlighting;
/// a single line text drawer
pub trait TextLineDrawer {
/// get the height of the text
fn height(&mut self, text: &str) -> u32;
/// get the width of the text
fn width(&mut self, text: &str) -> u32;
/// draw the text
fn draw_text(
&mut self,
image: &mut RgbaImage,
color: Rgba<u8>,
x: u32,
y: u32,
font_style: FontStyle,
text: &str,
);
}
impl TextLineDrawer for FontCollection {
fn height(&mut self, _text: &str) -> u32 {
self.get_font_height()
}
fn width(&mut self, text: &str) -> u32 {
self.layout(text, REGULAR).1
}
fn draw_text(
&mut self,
image: &mut RgbaImage,
color: Rgba<u8>,
x: u32,
y: u32,
font_style: FontStyle,
text: &str,
) {
self.draw_text_mut(image, color, x, y, font_style, text);
}
}
/// Font style
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum FontStyle {
@ -172,11 +212,15 @@ impl ImageFont {
///
/// It can be used to draw text on the image.
#[derive(Debug)]
pub struct FontCollection(Vec<ImageFont>);
pub struct FontCollection {
fonts: Vec<ImageFont>,
}
impl Default for FontCollection {
fn default() -> Self {
Self(vec![ImageFont::default()])
Self {
fonts: vec![ImageFont::default()],
}
}
}
@ -191,11 +235,11 @@ impl FontCollection {
Err(err) => eprintln!("[error] Error occurs when load font `{}`: {}", name, err),
}
}
Ok(Self(fonts))
Ok(Self { fonts })
}
fn glyph_for_char(&self, c: char, style: FontStyle) -> Option<(u32, &ImageFont, &Font)> {
for font in &self.0 {
for font in &self.fonts {
let result = font.get_by_style(style);
if let Some(id) = result.glyph_for_char(c) {
return Some((id, font, result));
@ -207,7 +251,7 @@ impl FontCollection {
/// get max height of all the fonts
pub fn get_font_height(&self) -> u32 {
self.0
self.fonts
.iter()
.map(|font| font.get_font_height())
.max()
@ -350,9 +394,9 @@ impl FontCollection {
I: GenericImage,
<I::Pixel as Pixel>::Subpixel: ValueInto<f32> + Clamp<f32>,
{
let metrics = self.0[0].get_regular().metrics();
let metrics = self.fonts[0].get_regular().metrics();
let offset =
(metrics.descent / metrics.units_per_em as f32 * self.0[0].size).round() as i32;
(metrics.descent / metrics.units_per_em as f32 * self.fonts[0].size).round() as i32;
let (glyphs, width) = self.layout(text, style);
@ -372,6 +416,7 @@ impl FontCollection {
}
}
#[derive(Debug)]
struct PositionedGlyph {
id: u32,
font: Font,

View File

@ -1,11 +1,11 @@
//! Format the output of syntect into an image
use crate::error::FontError;
use crate::font::{FontCollection, FontStyle};
use crate::font::{FontCollection, FontStyle, TextLineDrawer};
use crate::utils::*;
use image::{DynamicImage, GenericImageView, Rgba, RgbaImage};
use image::{Rgba, RgbaImage};
use syntect::highlighting::{Color, Style, Theme};
pub struct ImageFormatter {
pub struct ImageFormatter<T> {
/// pad between lines
/// Default: 2
line_pad: u32,
@ -42,7 +42,7 @@ pub struct ImageFormatter {
line_number_chars: u32,
/// font of english character, should be mono space font
/// Default: Hack (builtin)
font: FontCollection,
font: T,
/// Highlight lines
highlight_lines: Vec<u32>,
/// Shadow adder
@ -151,7 +151,7 @@ impl<S: AsRef<str> + Default> ImageFormatterBuilder<S> {
self
}
pub fn build(self) -> Result<ImageFormatter, FontError> {
pub fn build(self) -> Result<ImageFormatter<FontCollection>, FontError> {
let font = if self.font.is_empty() {
FontCollection::default()
} else {
@ -191,19 +191,19 @@ struct Drawable {
drawables: Vec<(u32, u32, Option<Color>, FontStyle, String)>,
}
impl ImageFormatter {
impl<T: TextLineDrawer> ImageFormatter<T> {
/// calculate the height of a line
fn get_line_height(&self) -> u32 {
self.font.get_font_height() + self.line_pad
fn get_line_height(&mut self) -> u32 {
self.font.height(" ") + self.line_pad
}
/// calculate the Y coordinate of a line
fn get_line_y(&self, lineno: u32) -> u32 {
fn get_line_y(&mut self, lineno: u32) -> u32 {
lineno * self.get_line_height() + self.code_pad + self.code_pad_top
}
/// calculate the size of code area
fn get_image_size(&self, max_width: u32, lineno: u32) -> (u32, u32) {
fn get_image_size(&mut self, max_width: u32, lineno: u32) -> (u32, u32) {
(
(max_width + self.code_pad).max(150),
self.get_line_y(lineno + 1) + self.code_pad,
@ -211,18 +211,18 @@ impl ImageFormatter {
}
/// Calculate where code start
fn get_left_pad(&self) -> u32 {
fn get_left_pad(&mut self) -> u32 {
self.code_pad
+ if self.line_number {
let tmp = format!("{:>width$}", 0, width = self.line_number_chars as usize);
2 * self.line_number_pad + self.font.get_text_len(&tmp)
2 * self.line_number_pad + self.font.width(&tmp)
} else {
0
}
}
/// create
fn create_drawables(&self, v: &[Vec<(Style, &str)>]) -> Drawable {
fn create_drawables(&mut self, v: &[Vec<(Style, &str)>]) -> Drawable {
// tab should be replaced to whitespace so that it can be rendered correctly
let tab = " ".repeat(self.tab_width as usize);
let mut drawables = vec![];
@ -246,7 +246,7 @@ impl ImageFormatter {
text.to_owned(),
));
width += self.font.get_text_len(&text);
width += self.font.width(&text);
max_width = max_width.max(width);
}
@ -255,7 +255,7 @@ impl ImageFormatter {
if self.window_title.is_some() {
let title = self.window_title.as_ref().unwrap();
let title_width = self.font.get_text_len(title);
let title_width = self.font.width(title);
let ctrls_offset = if self.window_controls {
self.window_controls_width + self.title_bar_pad
@ -266,7 +266,7 @@ impl ImageFormatter {
drawables.push((
ctrls_offset + self.title_bar_pad,
self.title_bar_pad + ctrls_center - self.font.get_font_height() / 2,
self.title_bar_pad + ctrls_center - self.font.height(" ") / 2,
None,
FontStyle::BOLD,
title.to_string(),
@ -283,7 +283,7 @@ impl ImageFormatter {
}
}
fn draw_line_number(&self, image: &mut DynamicImage, lineno: u32, mut color: Rgba<u8>) {
fn draw_line_number(&mut self, image: &mut RgbaImage, lineno: u32, mut color: Rgba<u8>) {
for i in color.0.iter_mut() {
*i = (*i).saturating_sub(20);
}
@ -293,36 +293,37 @@ impl ImageFormatter {
i + self.line_offset,
width = self.line_number_chars as usize
);
self.font.draw_text_mut(
let y = self.get_line_y(i);
self.font.draw_text(
image,
color,
self.code_pad,
self.get_line_y(i),
y,
FontStyle::REGULAR,
&line_number,
);
}
}
fn highlight_lines<I: IntoIterator<Item = u32>>(&self, image: &mut DynamicImage, lines: I) {
fn highlight_lines<I: IntoIterator<Item = u32>>(&mut self, image: &mut RgbaImage, lines: I) {
let width = image.width();
let height = self.font.get_font_height() + self.line_pad;
let mut color = image.get_pixel(20, 20);
let height = self.get_line_height();
let color = image.get_pixel_mut(20, 20);
for i in color.0.iter_mut() {
*i = (*i).saturating_add(40);
}
let shadow = RgbaImage::from_pixel(width, height, color);
let shadow = RgbaImage::from_pixel(width, height, *color);
for i in lines {
let y = self.get_line_y(i - 1);
copy_alpha(&shadow, image.as_mut_rgba8().unwrap(), 0, y);
copy_alpha(&shadow, image, 0, y);
}
}
// TODO: use &T instead of &mut T ?
pub fn format(&mut self, v: &[Vec<(Style, &str)>], theme: &Theme) -> DynamicImage {
pub fn format(&mut self, v: &[Vec<(Style, &str)>], theme: &Theme) -> RgbaImage {
if self.line_number {
self.line_number_chars =
(((v.len() + self.line_offset as usize) as f32).log10() + 1.0).floor() as u32;
@ -338,15 +339,15 @@ impl ImageFormatter {
let foreground = theme.settings.foreground.unwrap();
let background = theme.settings.background.unwrap();
let mut image =
DynamicImage::ImageRgba8(RgbaImage::from_pixel(size.0, size.1, background.to_rgba()));
let mut image = RgbaImage::from_pixel(size.0, size.1, background.to_rgba());
if !self.highlight_lines.is_empty() {
let highlight_lines = self
.highlight_lines
.iter()
.cloned()
.filter(|&n| n >= 1 && n <= drawables.max_lineno + 1);
.filter(|&n| n >= 1 && n <= drawables.max_lineno + 1)
.collect::<Vec<_>>();
self.highlight_lines(&mut image, highlight_lines);
}
if self.line_number {
@ -355,8 +356,7 @@ impl ImageFormatter {
for (x, y, color, style, text) in drawables.drawables {
let color = color.unwrap_or(foreground).to_rgba();
self.font
.draw_text_mut(&mut image, color, x, y, style, &text);
self.font.draw_text(&mut image, color, x, y, style, &text);
}
if self.window_controls {

View File

@ -1,7 +1,7 @@
use crate::error::ParseColorError;
use image::imageops::{crop_imm, resize, FilterType};
use image::Pixel;
use image::{DynamicImage, GenericImage, GenericImageView, Rgba, RgbaImage};
use image::{GenericImage, GenericImageView, Rgba, RgbaImage};
use imageproc::drawing::{draw_filled_rect_mut, draw_line_segment_mut};
use imageproc::rect::Rect;
@ -74,17 +74,17 @@ pub struct WindowControlsParams {
}
/// Add the window controls for image
pub(crate) fn add_window_controls(image: &mut DynamicImage, params: &WindowControlsParams) {
pub(crate) fn add_window_controls(image: &mut RgbaImage, params: &WindowControlsParams) {
let color = [
("#FF5F56", "#E0443E"),
("#FFBD2E", "#DEA123"),
("#27C93F", "#1AAB29"),
];
let mut background = image.get_pixel(37, 37);
let background = image.get_pixel_mut(37, 37);
background.0[3] = 0;
let mut title_bar = RgbaImage::from_pixel(params.width * 3, params.height * 3, background);
let mut title_bar = RgbaImage::from_pixel(params.width * 3, params.height * 3, *background);
let step = (params.radius * 2) as i32;
let spacer = step * 2;
let center_y = (params.height / 2) as i32;
@ -112,12 +112,7 @@ pub(crate) fn add_window_controls(image: &mut DynamicImage, params: &WindowContr
FilterType::Triangle,
);
copy_alpha(
&title_bar,
image.as_mut_rgba8().unwrap(),
params.padding,
params.padding,
);
copy_alpha(&title_bar, image, params.padding, params.padding);
}
#[derive(Clone, Debug)]
@ -204,7 +199,7 @@ impl ShadowAdder {
self
}
pub fn apply_to(&self, image: &DynamicImage) -> DynamicImage {
pub fn apply_to(&self, image: &RgbaImage) -> RgbaImage {
// the size of the final image
let width = image.width() + self.pad_horiz * 2;
let height = image.height() + self.pad_vert * 2;
@ -226,14 +221,9 @@ impl ShadowAdder {
// shadow = blur(&shadow, self.blur_radius);
// copy the original image to the top of it
copy_alpha(
image.as_rgba8().unwrap(),
&mut shadow,
self.pad_horiz,
self.pad_vert,
);
copy_alpha(image, &mut shadow, self.pad_horiz, self.pad_vert);
DynamicImage::ImageRgba8(shadow)
shadow
}
}
@ -266,7 +256,7 @@ pub(crate) fn copy_alpha(src: &RgbaImage, dst: &mut RgbaImage, x: u32, y: u32) {
}
/// Round the corner of the image
pub(crate) fn round_corner(image: &mut DynamicImage, radius: u32) {
pub(crate) fn round_corner(image: &mut RgbaImage, radius: u32) {
// draw a circle with given foreground on given background
// then split it into four pieces and paste them to the four corner of the image
//
@ -287,7 +277,7 @@ pub(crate) fn round_corner(image: &mut DynamicImage, radius: u32) {
&mut circle,
(((radius + 1) * 2) as i32, ((radius + 1) * 2) as i32),
radius as i32 * 2,
foreground,
*foreground,
);
// scale down the circle to the correct size