Compare commits

...

8 Commits

Author SHA1 Message Date
Aloxaf 7837ec85e4
deps: update pathfinder_simd 2024-04-30 10:31:54 +08:00
Jaxydog a59d3554aa
Add missing Ubuntu dependency to README (#244)
* Add missing Ubuntu dependency to README

* Add G++ to required dependencies
2024-04-30 10:20:01 +08:00
Aloxaf 319066e9a1 refactor: add TextLineDrawer trait 2024-03-04 10:28:57 +08:00
Aloxaf a460f0a152 fix: build on nightly 2024-03-04 10:28:12 +08:00
Aloxaf 2b745027c3
deps: remove unnecessary git dependency 2024-02-28 19:39:54 +08:00
Aloxaf a7eb01e513
misc: update deps 2024-02-28 19:22:34 +08:00
Kian-Meng Ang a4ad35338b
misc: Ignore hello.png (#221)
Generated after `cargo test`
2024-02-28 19:20:56 +08:00
Kian-Meng Ang f7584ef672
misc: fix typos (#220)
Found via `typos --format brief`
2024-02-28 18:56:18 +08:00
9 changed files with 433 additions and 298 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
/target
**/*.rs.bk
/.idea
.vscode
.vscode
hello.png

539
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,51 +17,49 @@ bin = ["structopt", "env_logger", "anyhow", "shell-words"]
harfbuzz = ["harfbuzz-sys", "font-kit/loader-freetype-default", "font-kit/source-fontconfig-default"]
[dependencies]
dirs = "4.0"
dirs = "5.0.1"
imageproc = "0.23.0"
clipboard = "0.5.0"
tempfile = "3.8.1"
tempfile = "3.10.1"
conv = "0.3.3"
pathfinder_geometry = "0.5.1"
log = "0.4.20"
lazy_static = "1.4.0"
shell-words = { version = "1.1.0", optional = true }
rayon = "1.8.0"
font-kit = "0.11"
rayon = "1.9.0"
font-kit = "0.12.0"
harfbuzz-sys = { version = "0.5.0", optional = true }
pathfinder_simd = "0.5.3"
[dependencies.image]
version = "0.24"
version = "0.24.9"
default-features = false
features = ["jpeg", "png", "jpeg_rayon"]
[dependencies.syntect]
version = "5.1"
version = "5.2.0"
default-features = false
features = ["parsing", "dump-load", "regex-onig", "plist-load", "yaml-load"]
[dependencies.anyhow]
version = "1.0"
version = "1.0.80"
optional = true
[dependencies.structopt]
version = "0.3"
version = "0.3.26"
default-features = false
features = ["color", "wrap_help"]
optional = true
[dependencies.env_logger]
version = "0.9.3"
version = "0.11.2"
default-features = false
features = ["termcolor", "atty", "humantime"]
features = ["auto-color", "humantime"]
optional = true
[target.'cfg(target_os = "macos")'.dependencies]
pasteboard = "0.1.3"
[target.'cfg(target_os = "windows")'.dependencies]
clipboard-win = "4.5.0"
clipboard-win = "5.2.0"
image = { version = "0.24", default-features = false, features = ["jpeg", "bmp", "jpeg_rayon"] }
[patch.crates-io]
pathfinder_simd = { version = "0.5.0", git = "https://github.com/servo/pathfinder" }

View File

@ -60,7 +60,7 @@ brew install silicon
```bash
sudo apt install expat
sudo apt install libxml2-dev
sudo apt install pkg-config libasound2-dev libssl-dev cmake libfreetype6-dev libexpat1-dev libxcb-composite0-dev libharfbuzz-dev
sudo apt install pkg-config libasound2-dev libssl-dev cmake libfreetype6-dev libexpat1-dev libxcb-composite0-dev libharfbuzz-dev libfontconfig1-dev g++
```
### Fedora

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;
@ -265,11 +266,11 @@ impl Config {
Ok(theme.clone())
} else {
ThemeSet::get_theme(&self.theme)
.context(format!("Canot load the theme: {}", self.theme))
.context(format!("Cannot load the theme: {}", self.theme))
}
}
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,46 +283,47 @@ 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);
}
for i in 0..=lineno {
let line_mumber = format!(
let line_number = format!(
"{:>width$}",
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_mumber,
&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