An attempt on svg support

This commit is contained in:
Aloxaf 2019-07-15 23:18:49 +08:00
parent 9401ed0d49
commit 2bc7625cb1
4 changed files with 376 additions and 241 deletions

View File

@ -1,241 +1,5 @@
use crate::font::{FontCollection, FontStyle};
use crate::utils::{copy_alpha, ToRgba};
use failure::Error;
use image::{DynamicImage, GenericImageView, Rgba, RgbaImage};
use syntect::highlighting::{Color, Style, Theme};
mod image;
mod svg;
pub struct ImageFormatter {
/// pad between lines
/// Default: 2
line_pad: u32,
/// pad between code and edge of code area. [top, bottom, left, right]
/// Default: 25
code_pad: u32,
/// pad of top of the code area
/// Default: 50
code_pad_top: u32,
/// show line number
/// Default: true
line_number: bool,
/// pad between code and line number
/// Default: 6
line_number_pad: u32,
/// number of columns of line number area
/// Default: Auto detect
line_number_chars: u32,
/// font of english character, should be mono space font
/// Default: Hack (builtin)
font: FontCollection,
/// Highlight lines
highlight_lines: Vec<u32>,
}
pub struct ImageFormatterBuilder {
/// pad between lines
line_pad: u32,
/// show line number
line_number: bool,
/// pad of top of the code area
code_pad_top: u32,
/// font of english character, should be mono space font
font: FontCollection,
/// Highlight lines
highlight_lines: Vec<u32>,
}
impl<'a> ImageFormatterBuilder {
pub fn new() -> Self {
Self {
line_pad: 2,
line_number: true,
code_pad_top: 50,
font: FontCollection::default(),
highlight_lines: vec![],
}
}
pub fn line_number(mut self, show: bool) -> Self {
self.line_number = show;
self
}
pub fn line_pad(mut self, pad: u32) -> Self {
self.line_pad = pad;
self
}
pub fn code_pad_top(mut self, pad: u32) -> Self {
self.code_pad_top = pad;
self
}
// TODO: move this Result to `build`
pub fn font<S: AsRef<str>>(mut self, fonts: &[(S, f32)]) -> Result<Self, Error> {
let font = FontCollection::new(fonts)?;
self.font = font;
Ok(self)
}
pub fn highlight_lines(mut self, lines: Vec<u32>) -> Self {
self.highlight_lines = lines;
self
}
pub fn build(self) -> ImageFormatter {
ImageFormatter {
line_pad: self.line_pad,
code_pad: 25,
code_pad_top: self.code_pad_top,
line_number: self.line_number,
line_number_pad: 6,
line_number_chars: 0,
font: self.font,
highlight_lines: self.highlight_lines,
}
}
}
struct Drawable {
/// max width of the picture
max_width: u32,
/// max number of line of the picture
max_lineno: u32,
/// arguments for draw_text_mut
drawables: Vec<(u32, u32, Color, FontStyle, String)>,
}
impl ImageFormatter {
/// calculate the height of a line
fn get_line_height(&self) -> u32 {
self.font.get_font_height() + self.line_pad
}
/// calculate the Y coordinate of a line
fn get_line_y(&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) {
(
(max_width + self.code_pad).max(150),
self.get_line_y(lineno + 1) + self.code_pad,
)
}
/// Calculate where code start
fn get_left_pad(&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)
} else {
0
}
}
/// create
fn create_drawables(&self, v: &[Vec<(Style, &str)>]) -> Drawable {
let mut drawables = vec![];
let (mut max_width, mut max_lineno) = (0, 0);
for (i, tokens) in v.iter().enumerate() {
let height = self.get_line_y(i as u32);
let mut width = self.get_left_pad();
for (style, text) in tokens {
let text = text.trim_end_matches('\n');
if text.is_empty() {
continue;
}
drawables.push((
width,
height,
style.foreground,
style.font_style.into(),
text.to_owned(),
));
width += self.font.get_text_len(text);
max_width = max_width.max(width);
}
max_lineno = i as u32;
}
Drawable {
max_width,
max_lineno,
drawables,
}
}
fn draw_line_number(&self, image: &mut DynamicImage, lineno: u32, color: Rgba<u8>) {
for i in 0..=lineno {
let line_mumber = format!("{:>width$}", i + 1, width = self.line_number_chars as usize);
self.font.draw_text_mut(
image,
color,
self.code_pad,
self.get_line_y(i),
FontStyle::REGULAR,
&line_mumber,
);
}
}
fn highlight_lines(&self, image: &mut DynamicImage, lines: &[u32]) {
let width = image.width();
let height = self.font.get_font_height() + self.line_pad;
let mut color = image.get_pixel(20, 20);
color
.data
.iter_mut()
.for_each(|n| *n = (*n).saturating_add(20));
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);
}
}
// TODO: &mut ?
pub fn format(&mut self, v: &[Vec<(Style, &str)>], theme: &Theme) -> DynamicImage {
if self.line_number {
self.line_number_chars = ((v.len() as f32).log10() + 1.0).floor() as u32;
} else {
self.line_number_chars = 0;
self.line_number_pad = 0;
}
let drawables = self.create_drawables(v);
let size = self.get_image_size(drawables.max_width, drawables.max_lineno);
let foreground = theme.settings.foreground.unwrap();
let background = theme.settings.background.unwrap();
let foreground = foreground.to_rgba();
let background = background.to_rgba();
let mut image = DynamicImage::ImageRgba8(RgbaImage::from_pixel(size.0, size.1, background));
if !self.highlight_lines.is_empty() {
self.highlight_lines(&mut image, &self.highlight_lines);
}
if self.line_number {
self.draw_line_number(&mut image, drawables.max_lineno, foreground);
}
for (x, y, color, style, text) in drawables.drawables {
let color = color.to_rgba();
self.font
.draw_text_mut(&mut image, color, x, y, style, &text);
}
image
}
}
pub use crate::formatter::image::*;
pub use crate::formatter::svg::*;

241
src/formatter/image.rs Normal file
View File

@ -0,0 +1,241 @@
use crate::font::{FontCollection, FontStyle};
use crate::utils::{copy_alpha, ToRgba};
use failure::Error;
use image::{DynamicImage, GenericImageView, Rgba, RgbaImage};
use syntect::highlighting::{Color, Style, Theme};
pub struct ImageFormatter {
/// pad between lines
/// Default: 2
line_pad: u32,
/// pad between code and edge of code area. [top, bottom, left, right]
/// Default: 25
code_pad: u32,
/// pad of top of the code area
/// Default: 50
code_pad_top: u32,
/// show line number
/// Default: true
line_number: bool,
/// pad between code and line number
/// Default: 6
line_number_pad: u32,
/// number of columns of line number area
/// Default: Auto detect
line_number_chars: u32,
/// font of english character, should be mono space font
/// Default: Hack (builtin)
font: FontCollection,
/// Highlight lines
highlight_lines: Vec<u32>,
}
pub struct ImageFormatterBuilder {
/// pad between lines
line_pad: u32,
/// show line number
line_number: bool,
/// pad of top of the code area
code_pad_top: u32,
/// font of english character, should be mono space font
font: FontCollection,
/// Highlight lines
highlight_lines: Vec<u32>,
}
impl<'a> ImageFormatterBuilder {
pub fn new() -> Self {
Self {
line_pad: 2,
line_number: true,
code_pad_top: 50,
font: FontCollection::default(),
highlight_lines: vec![],
}
}
pub fn line_number(mut self, show: bool) -> Self {
self.line_number = show;
self
}
pub fn line_pad(mut self, pad: u32) -> Self {
self.line_pad = pad;
self
}
pub fn code_pad_top(mut self, pad: u32) -> Self {
self.code_pad_top = pad;
self
}
// TODO: move this Result to `build`
pub fn font<S: AsRef<str>>(mut self, fonts: &[(S, f32)]) -> Result<Self, Error> {
let font = FontCollection::new(fonts)?;
self.font = font;
Ok(self)
}
pub fn highlight_lines(mut self, lines: Vec<u32>) -> Self {
self.highlight_lines = lines;
self
}
pub fn build(self) -> ImageFormatter {
ImageFormatter {
line_pad: self.line_pad,
code_pad: 25,
code_pad_top: self.code_pad_top,
line_number: self.line_number,
line_number_pad: 6,
line_number_chars: 0,
font: self.font,
highlight_lines: self.highlight_lines,
}
}
}
struct Drawable {
/// max width of the picture
max_width: u32,
/// max number of line of the picture
max_lineno: u32,
/// arguments for draw_text_mut
drawables: Vec<(u32, u32, Color, FontStyle, String)>,
}
impl ImageFormatter {
/// calculate the height of a line
fn get_line_height(&self) -> u32 {
self.font.get_font_height() + self.line_pad
}
/// calculate the Y coordinate of a line
fn get_line_y(&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) {
(
(max_width + self.code_pad).max(150),
self.get_line_y(lineno + 1) + self.code_pad,
)
}
/// Calculate where code start
fn get_left_pad(&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)
} else {
0
}
}
/// create
fn create_drawables(&self, v: &[Vec<(Style, &str)>]) -> Drawable {
let mut drawables = vec![];
let (mut max_width, mut max_lineno) = (0, 0);
for (i, tokens) in v.iter().enumerate() {
let height = self.get_line_y(i as u32);
let mut width = self.get_left_pad();
for (style, text) in tokens {
let text = text.trim_end_matches('\n');
if text.is_empty() {
continue;
}
drawables.push((
width,
height,
style.foreground,
style.font_style.into(),
text.to_owned(),
));
width += self.font.get_text_len(text);
max_width = max_width.max(width);
}
max_lineno = i as u32;
}
Drawable {
max_width,
max_lineno,
drawables,
}
}
fn draw_line_number(&self, image: &mut DynamicImage, lineno: u32, color: Rgba<u8>) {
for i in 0..=lineno {
let line_mumber = format!("{:>width$}", i + 1, width = self.line_number_chars as usize);
self.font.draw_text_mut(
image,
color,
self.code_pad,
self.get_line_y(i),
FontStyle::REGULAR,
&line_mumber,
);
}
}
fn highlight_lines(&self, image: &mut DynamicImage, lines: &[u32]) {
let width = image.width();
let height = self.font.get_font_height() + self.line_pad;
let mut color = image.get_pixel(20, 20);
color
.data
.iter_mut()
.for_each(|n| *n = (*n).saturating_add(20));
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);
}
}
// TODO: &mut ?
pub fn format(&mut self, v: &[Vec<(Style, &str)>], theme: &Theme) -> DynamicImage {
if self.line_number {
self.line_number_chars = ((v.len() as f32).log10() + 1.0).floor() as u32;
} else {
self.line_number_chars = 0;
self.line_number_pad = 0;
}
let drawables = self.create_drawables(v);
let size = self.get_image_size(drawables.max_width, drawables.max_lineno);
let foreground = theme.settings.foreground.unwrap();
let background = theme.settings.background.unwrap();
let foreground = foreground.to_rgba();
let background = background.to_rgba();
let mut image = DynamicImage::ImageRgba8(RgbaImage::from_pixel(size.0, size.1, background));
if !self.highlight_lines.is_empty() {
self.highlight_lines(&mut image, &self.highlight_lines);
}
if self.line_number {
self.draw_line_number(&mut image, drawables.max_lineno, foreground);
}
for (x, y, color, style, text) in drawables.drawables {
let color = color.to_rgba();
self.font
.draw_text_mut(&mut image, color, x, y, style, &text);
}
image
}
}

128
src/formatter/svg.rs Normal file
View File

@ -0,0 +1,128 @@
use syntect::highlighting::{FontStyle, Style, Theme};
use crate::font::FontCollection;
trait ToHtml {
fn to_html(&self) -> String;
}
impl ToHtml for syntect::highlighting::Color {
fn to_html(&self) -> String {
format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
}
fn get_style(style: FontStyle) -> &'static str {
if style.contains(FontStyle::ITALIC) {
"italic"
} else {
"normal"
}
}
fn get_weight(style: FontStyle) -> &'static str {
if style.contains(FontStyle::BOLD) {
"bold"
} else {
"normal"
}
}
// TODO: Create a Formatter trait ??
pub struct SVGFormatter {
width: u32,
height: u32,
}
impl SVGFormatter {
fn get_svg_size(v: &[&str]) -> (u32, u32) {
let font = FontCollection::new(&[("monospace", 17.0)]).unwrap();
let height = font.get_font_height() * (v.len() + 1) as u32;
let width = v.iter().map(|s| font.get_text_len(s)).max().unwrap();
(width, height)
}
// TODO: don't concat string...
pub fn format(&mut self, v: &[Vec<(Style, &str)>], theme: &Theme) -> String {
let mut svg = format!(
r#"<svg width="{}" height="{}" style="border: 0px solid black" xmlns="http://www.w3.org/2000/svg">"#,
self.width + 20 * 2, self.height + 20 * 2
);
svg.push_str(&format!(
r#"<rect width="100%" height="100%" fill="{}"/>"#,
"#aaaaff"
));
svg.push_str(&format!(
r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}"/>"#,
20, 20, self.width, self.height,
theme.settings.background.unwrap().to_html())
);
let line_height = self.height as usize / v.len();
for (i, line) in v.iter().enumerate() {
let mut text = format!(
r#"<text x="{}" y="{}" font-family="monospace" font-size="17px">"#,
20, (i + 1) * line_height + 20
);
for (style, content) in line {
let tspan = format!(
r#"<tspan fill="{fill}" font-style="{font_style}" font-weight="{font_weight}">{content}</tspan>"#,
fill = style.foreground.to_html(),
font_style = get_style(style.font_style),
font_weight = get_weight(style.font_style),
content = content.replace(' ', "&#160;"),
);
text.push_str(&tspan);
}
text.push_str("</text>");
svg.push_str(&text);
}
svg.push_str("</svg>");
svg
}
}
#[cfg(test)]
mod tests {
use crate::formatter::SVGFormatter;
#[test]
fn test() {
use syntect::easy::HighlightLines;
use syntect::parsing::SyntaxSet;
use syntect::highlighting::ThemeSet;
use syntect::util::LinesWithEndings;
use syntect::dumps::from_binary;
let code = r#"fn factorial(n: u64) -> u64 {
match n {
0 => 1,
_ => n * factorial(n - 1),
}
}
fn main() {
println!("10! = {}", factorial(10));
}
"#;
let ps = from_binary::<SyntaxSet>(include_bytes!("../../assets/syntaxes.bin"));
let ts = from_binary::<ThemeSet>(include_bytes!("../../assets/themes.bin"));
let syntax = ps.find_syntax_by_extension("rs").unwrap();
let mut h = HighlightLines::new(syntax, &ts.themes["Dracula"]);
let (width, height) = SVGFormatter::get_svg_size(&code.split('\n').collect::<Vec<_>>());
let highlight = LinesWithEndings::from(&code)
.map(|line| h.highlight(line, &ps))
.collect::<Vec<_>>();
let x = (SVGFormatter { width, height }).format(&highlight, &ts.themes["Dracula"]);
std::fs::write("test.svg", x).unwrap();
}
}

View File

@ -281,5 +281,7 @@ pub fn dump_image_to_clipboard(image: &DynamicImage) -> Result<(), Error> {
#[cfg(not(target_os = "linux"))]
pub fn dump_image_to_clipboard(image: &DynamicImage) -> Result<(), Error> {
Err(format_err!("This feature hasn't been implemented in your system"))
Err(format_err!(
"This feature hasn't been implemented in your system"
))
}