Make silicon also a lib (#8)

* Make it also a lib

* More comment

* Adjust code structure

* Add the 'bin' feature

* Remove failure dependency from lib

* Update README
This commit is contained in:
Aloxaf 2019-07-20 21:38:34 +08:00 committed by GitHub
parent d6f05ec24c
commit 5775e8a1da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 271 additions and 99 deletions

View File

@ -14,25 +14,42 @@ syntect = "3.2"
image = "0.21"
imageproc = "0.18"
font-kit = "0.3.1"
failure = "0.1.5"
clipboard = "0.5.0"
tempfile = "3.1.0"
conv = "0.3.3"
euclid = "0.19"
log = "0.4.7"
[lib]
name = "silicon"
path = "src/lib.rs"
[[bin]]
name = "silicon"
path = "src/bin.rs"
[dependencies.failure]
version = "0.1.5"
optional = true
[dependencies.structopt]
version = "0.2"
default-features = false
features = [ "color", "wrap_help" ]
optional = true
[dependencies.env_logger]
version = "0.6.2"
default-features = false
features = [ "termcolor", "atty", "humantime" ]
optional = true
[patch.crates-io]
font-kit = { version = "0.3.1", git = "https://github.com/pcwalton/font-kit" }
[features]
default = [ "bin" ]
bin = [ "structopt", "env_logger", "failure" ]
[profile.release]
lto = true

View File

@ -1,8 +1,9 @@
# Silicon
[![crates.io](https://img.shields.io/crates/v/silicon.svg)](https://crates.io/crates/silicon)
[![Crates.io](https://img.shields.io/crates/v/silicon.svg)](https://crates.io/crates/silicon)
[![Documentation](https://docs.rs/silicon/badge.svg)](https://docs.rs/silicon)
[![Build Status](https://travis-ci.org/Aloxaf/silicon.svg?branch=master)](https://travis-ci.org/Aloxaf/silicon)
![license](https://img.shields.io/crates/l/silicon.svg)
![License](https://img.shields.io/crates/l/silicon.svg)
Silicon is an alternative to [Carbon](https://github.com/dawnlabs/carbon) implemented in Rust.

View File

@ -4,26 +4,50 @@ extern crate log;
extern crate failure;
use crate::config::Config;
use crate::utils::{add_window_controls, dump_image_to_clipboard, round_corner};
use crate::utils::*;
use failure::Error;
use image::DynamicImage;
use structopt::StructOpt;
use syntect::dumps;
use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
#[cfg(target_os = "linux")]
use {image::ImageOutputFormat, std::process::Command};
mod blur;
mod config;
mod font;
mod formatter;
mod utils;
pub mod blur;
pub mod config;
pub mod error;
pub mod font;
pub mod formatter;
pub mod utils;
#[cfg(target_os = "linux")]
pub fn dump_image_to_clipboard(image: &DynamicImage) -> Result<(), Error> {
let mut temp = tempfile::NamedTempFile::new()?;
image.write_to(&mut temp, ImageOutputFormat::PNG)?;
Command::new("xclip")
.args(&[
"-sel",
"clip",
"-t",
"image/png",
temp.path().to_str().unwrap(),
])
.status()
.map_err(|e| format_err!("Failed to copy image to clipboard: {}", e))?;
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn dump_image_to_clipboard(_image: &DynamicImage) -> Result<(), Error> {
Err(format_err!(
"This feature hasn't been implemented for your system"
))
}
fn run() -> Result<(), Error> {
let config: Config = Config::from_args();
let ps = dumps::from_binary::<SyntaxSet>(include_bytes!("../assets/syntaxes.bin")); //SyntaxSet::load_defaults_newlines();
let ts = dumps::from_binary::<ThemeSet>(include_bytes!("../assets/themes.bin")); // ThemeSet::load();
let (ps, ts) = init_syntect();
if config.list_themes {
for i in ts.themes.keys() {
@ -43,16 +67,7 @@ fn run() -> Result<(), Error> {
let mut formatter = config.get_formatter()?;
let mut image = formatter.format(&highlight, &theme);
if !config.no_window_controls {
add_window_controls(&mut image);
}
if !config.no_round_corner {
round_corner(&mut image, 12);
}
let image = config.get_shadow_adder().apply_to(&image);
let image = formatter.format(&highlight, &theme);
if config.to_clipboard {
dump_image_to_clipboard(&image)?;

View File

@ -206,26 +206,22 @@ impl Config {
pub fn theme(&self, ts: &ThemeSet) -> Result<Theme, Error> {
if let Some(theme) = ts.themes.get(&self.theme) {
return Ok(theme.clone());
Ok(theme.clone())
} else {
return Ok(ThemeSet::get_theme(&self.theme)?);
Ok(ThemeSet::get_theme(&self.theme)?)
}
// &ts.themes[&self.theme]
}
pub fn get_formatter(&self) -> Result<ImageFormatter, Error> {
let mut formatter = ImageFormatterBuilder::new()
let formatter = ImageFormatterBuilder::new()
.line_pad(self.line_pad)
.window_controls(!self.no_window_controls)
.line_number(!self.no_line_number)
.font(self.font.clone().unwrap_or_else(|| vec![]))
.round_corner(!self.no_round_corner)
.window_controls(!self.no_window_controls)
.shadow_adder(self.get_shadow_adder())
.highlight_lines(self.highlight_lines.clone().unwrap_or_else(|| vec![]));
if let Some(fonts) = &self.font {
formatter = formatter.font(fonts);
}
if self.no_line_number {
formatter = formatter.line_number(false);
}
if self.no_window_controls {
formatter = formatter.code_pad_top(0);
}
Ok(formatter.build()?)
}

32
src/error.rs Normal file
View File

@ -0,0 +1,32 @@
use font_kit::error::{FontLoadingError, SelectionError};
use std::error::Error;
use std::fmt::{self, Display};
#[derive(Debug)]
pub enum FontError {
SelectionError(SelectionError),
FontLoadingError(FontLoadingError),
}
impl Error for FontError {}
impl Display for FontError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
FontError::SelectionError(e) => write!(f, "Font error: {}", e),
FontError::FontLoadingError(e) => write!(f, "Font error: {}", e),
}
}
}
impl From<SelectionError> for FontError {
fn from(e: SelectionError) -> Self {
FontError::SelectionError(e)
}
}
impl From<FontLoadingError> for FontError {
fn from(e: FontLoadingError) -> Self {
FontError::FontLoadingError(e)
}
}

View File

@ -1,6 +1,19 @@
//! A basic font manager with fallback support
//!
//! # Example
//!
//! ```rust
//! use image::{RgbImage, Rgb};
//! use silicon::font::{FontCollection, FontStyle};
//!
//! let mut image = RgbImage::new(200, 100);
//! let font = FontCollection::new(&[("Hack", 27.0), ("FiraCode", 27.0)]).unwrap();
//!
//! font.draw_text_mut(&mut image, Rgb([255, 0, 0]), 0, 0, FontStyle::REGULAR, "Hello, world");
//! ```
use crate::error::FontError;
use conv::ValueInto;
use euclid::{Point2D, Rect, Size2D};
use failure::Error;
use font_kit::canvas::{Canvas, Format, RasterizationOptions};
use font_kit::font::Font;
use font_kit::hinting::HintingOptions;
@ -14,6 +27,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use syntect::highlighting;
/// Font style
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum FontStyle {
REGULAR,
@ -40,6 +54,7 @@ impl From<highlighting::FontStyle> for FontStyle {
use FontStyle::*;
/// A single font with specific size
#[derive(Debug)]
pub struct ImageFont {
pub fonts: HashMap<FontStyle, Font>,
@ -47,6 +62,7 @@ pub struct ImageFont {
}
impl Default for ImageFont {
/// It will use Hack font (size: 26.0) by default
fn default() -> Self {
let l = vec![
(
@ -77,7 +93,7 @@ impl Default for ImageFont {
}
impl ImageFont {
pub fn new(name: &str, size: f32) -> Result<Self, Error> {
pub fn new(name: &str, size: f32) -> Result<Self, FontError> {
// Silicon already contains Hack font
if name == "Hack" {
let mut font = Self::default();
@ -122,17 +138,19 @@ impl ImageFont {
Ok(Self { fonts, size })
}
/// Get a font by style. If there is no such a font, it will return the REGULAR font.
pub fn get_by_style(&self, style: FontStyle) -> &Font {
self.fonts
.get(&style)
.unwrap_or_else(|| self.fonts.get(&REGULAR).unwrap())
}
/// Get the regular font
pub fn get_regular(&self) -> &Font {
self.fonts.get(&REGULAR).unwrap()
}
/// get the height of the font
/// Get the height of the font
pub fn get_font_height(&self) -> u32 {
let font = self.get_regular();
let metrics = font.metrics();
@ -140,6 +158,9 @@ impl ImageFont {
}
}
/// A collection of font
///
/// It can be used to draw text on the image.
#[derive(Debug)]
pub struct FontCollection(Vec<ImageFont>);
@ -150,7 +171,8 @@ impl Default for FontCollection {
}
impl FontCollection {
pub fn new<S: AsRef<str>>(font_list: &[(S, f32)]) -> Result<Self, Error> {
/// Create a FontCollection with several fonts.
pub fn new<S: AsRef<str>>(font_list: &[(S, f32)]) -> Result<Self, FontError> {
let mut fonts = vec![];
for (name, size) in font_list {
let name = name.as_ref();
@ -218,12 +240,14 @@ impl FontCollection {
(glyphs, delta_x)
}
/// Get the width of the given glyph
fn get_glyph_width(font: &Font, id: u32, size: f32) -> u32 {
let metrics = font.metrics();
let advance = font.advance(id).unwrap();
(advance / metrics.units_per_em as f32 * size).x.ceil() as u32
}
/// Get the width of the given text
pub fn get_text_len(&self, text: &str) -> u32 {
self.layout(text, REGULAR).1
}

View File

@ -1,6 +1,7 @@
//! Format the output of syntect into an image
use crate::error::FontError;
use crate::font::{FontCollection, FontStyle};
use crate::utils::{copy_alpha, ToRgba};
use failure::Error;
use crate::utils::*;
use image::{DynamicImage, GenericImageView, Rgba, RgbaImage};
use syntect::highlighting::{Color, Style, Theme};
@ -8,7 +9,7 @@ pub struct ImageFormatter {
/// pad between lines
/// Default: 2
line_pad: u32,
/// pad between code and edge of code area. [top, bottom, left, right]
/// pad between code and edge of code area.
/// Default: 25
code_pad: u32,
/// pad of top of the code area
@ -17,6 +18,9 @@ pub struct ImageFormatter {
/// show line number
/// Default: true
line_number: bool,
/// round corner
/// Default: true
round_corner: bool,
/// pad between code and line number
/// Default: 6
line_number_pad: u32,
@ -28,71 +32,101 @@ pub struct ImageFormatter {
font: FontCollection,
/// Highlight lines
highlight_lines: Vec<u32>,
/// Shadow adder
shadow_adder: Option<ShadowAdder>,
}
pub struct ImageFormatterBuilder<'a, S: AsRef<str>> {
/// pad between lines
#[derive(Default)]
pub struct ImageFormatterBuilder<S> {
/// Pad between lines
line_pad: u32,
/// show line number
/// 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: &'a [(S, f32)],
/// Font of english character, should be mono space font
font: Vec<(S, f32)>,
/// Highlight lines
highlight_lines: Vec<u32>,
/// Whether show the window controls
window_controls: bool,
/// Whether round the corner of the image
round_corner: bool,
/// Shadow adder,
shadow_adder: Option<ShadowAdder>,
}
impl<'a, S: AsRef<str>> ImageFormatterBuilder<'a, S> {
// FXIME: cannot use `ImageFormatterBuilder::new().build()` bacuse cannot infer type for `S`
impl<S: AsRef<str> + Default> ImageFormatterBuilder<S> {
pub fn new() -> Self {
Self {
line_pad: 2,
line_number: true,
code_pad_top: 50,
font: &[],
highlight_lines: vec![],
window_controls: true,
round_corner: true,
..Default::default()
}
}
/// Whether show the line number
pub fn line_number(mut self, show: bool) -> Self {
self.line_number = show;
self
}
/// Set the pad between lines
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
}
pub fn font(mut self, fonts: &'a [(S, f32)]) -> Self {
/// Set the font
pub fn font(mut self, fonts: Vec<(S, f32)>) -> Self {
self.font = fonts;
self
}
/// Whether show the windows controls
pub fn window_controls(mut self, show: bool) -> Self {
self.window_controls = show;
self
}
/// Whether round the corner
pub fn round_corner(mut self, b: bool) -> Self {
self.round_corner = b;
self
}
/// Add the shadow
pub fn shadow_adder(mut self, adder: ShadowAdder) -> Self {
self.shadow_adder = Some(adder);
self
}
/// Set the lines to highlight.
pub fn highlight_lines(mut self, lines: Vec<u32>) -> Self {
self.highlight_lines = lines;
self
}
pub fn build(self) -> Result<ImageFormatter, Error> {
pub fn build(self) -> Result<ImageFormatter, FontError> {
let font = if self.font.is_empty() {
FontCollection::default()
} else {
FontCollection::new(self.font)?
FontCollection::new(&self.font)?
};
let code_pad_top = if self.window_controls { 50 } else { 0 };
Ok(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,
highlight_lines: self.highlight_lines,
round_corner: self.round_corner,
shadow_adder: self.shadow_adder,
code_pad_top,
font,
})
}
@ -242,6 +276,19 @@ impl ImageFormatter {
.draw_text_mut(&mut image, color, x, y, style, &text);
}
image
// draw_window_controls == true
if self.code_pad_top != 0 {
add_window_controls(&mut image);
}
if self.round_corner {
round_corner(&mut image, 12);
}
if let Some(adder) = &self.shadow_adder {
adder.apply_to(&image)
} else {
image
}
}
}

42
src/lib.rs Normal file
View File

@ -0,0 +1,42 @@
//! `silicon` is a tool to create beautiful image of your source code.
//!
//! # Example
//!
//! ```
//! use syntect::easy::HighlightLines;
//! use syntect::util::LinesWithEndings;
//! use silicon::utils::{init_syntect, ShadowAdder};
//! use silicon::formatter::ImageFormatterBuilder;
//!
//! let (ps, ts) = init_syntect();
//! let code = r#"
//! fn main() {
//! println!("Hello, world!");
//! }
//! "#;
//!
//! let syntax = ps.find_syntax_by_token("rs").unwrap();
//! let theme = &ts.themes["Dracula"];
//!
//! let mut h = HighlightLines::new(syntax, theme);
//! let highlight = LinesWithEndings::from(&code)
//! .map(|line| h.highlight(line, &ps))
//! .collect::<Vec<_>>();
//!
//! let mut formatter = ImageFormatterBuilder::new()
//! .font(vec![("Hack", 26.0)])
//! .shadow_adder(ShadowAdder::default())
//! .build()
//! .unwrap();
//! let image = formatter.format(&highlight, theme);
//!
//! image.save("hello.png").unwrap();
//! ```
#[macro_use]
extern crate log;
pub mod blur;
pub mod error;
pub mod font;
pub mod formatter;
pub mod utils;

View File

@ -1,14 +1,21 @@
use failure::Error;
use image::imageops::{crop, resize};
use image::Pixel;
use image::{DynamicImage, FilterType, GenericImage, GenericImageView, Rgba, RgbaImage};
use imageproc::drawing::{draw_filled_rect_mut, draw_line_segment_mut};
use imageproc::rect::Rect;
use syntect::dumps;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;
#[cfg(target_os = "linux")]
use {image::ImageOutputFormat, std::process::Command};
/// Load the default SyntaxSet and ThemeSet.
pub fn init_syntect() -> (SyntaxSet, ThemeSet) {
(
dumps::from_binary(include_bytes!("../assets/syntaxes.bin")),
dumps::from_binary(include_bytes!("../assets/themes.bin")),
)
}
pub trait ToRgba {
pub(crate) trait ToRgba {
type Target;
fn to_rgba(&self) -> Self::Target;
}
@ -33,7 +40,8 @@ impl ToRgba for syntect::highlighting::Color {
}
}
pub fn add_window_controls(image: &mut DynamicImage) {
/// Add the window controls for image
pub(crate) fn add_window_controls(image: &mut DynamicImage) {
let color = [
("#FF5F56", "#E0443E"),
("#FFBD2E", "#DEA123"),
@ -66,6 +74,7 @@ pub fn add_window_controls(image: &mut DynamicImage) {
copy_alpha(&title_bar, image.as_mut_rgba8().unwrap(), 15, 15);
}
/// Add the shadow for image
#[derive(Debug)]
pub struct ShadowAdder {
background: Rgba<u8>,
@ -90,16 +99,19 @@ impl ShadowAdder {
}
}
/// Set the background color
pub fn background(mut self, color: Rgba<u8>) -> Self {
self.background = color;
self
}
/// Set the shadow color
pub fn shadow_color(mut self, color: Rgba<u8>) -> Self {
self.shadow_color = color;
self
}
/// Set the shadow size
pub fn blur_radius(mut self, sigma: f32) -> Self {
self.blur_radius = sigma;
self
@ -159,8 +171,14 @@ impl ShadowAdder {
}
}
impl Default for ShadowAdder {
fn default() -> Self {
ShadowAdder::new()
}
}
/// copy from src to dst, taking into account alpha channels
pub fn copy_alpha(src: &RgbaImage, dst: &mut RgbaImage, x: u32, y: u32) {
pub(crate) fn copy_alpha(src: &RgbaImage, dst: &mut RgbaImage, x: u32, y: u32) {
assert!(src.width() + x <= dst.width());
assert!(src.height() + y <= dst.height());
for j in 0..src.height() {
@ -179,8 +197,8 @@ pub fn copy_alpha(src: &RgbaImage, dst: &mut RgbaImage, x: u32, y: u32) {
}
}
/// round the corner of an image
pub fn round_corner(image: &mut DynamicImage, radius: u32) {
/// Round the corner of the image
pub(crate) fn round_corner(image: &mut DynamicImage, 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
let mut circle =
@ -217,8 +235,12 @@ pub fn round_corner(image: &mut DynamicImage, radius: u32) {
// issue: https://github.com/image-rs/imageproc/issues/328
// PR: https://github.com/image-rs/imageproc/pull/330
/// Draw as much of a circle, including its contents, as lies inside the image bounds.
pub fn draw_filled_circle_mut<I>(image: &mut I, center: (i32, i32), radius: i32, color: I::Pixel)
where
pub(crate) fn draw_filled_circle_mut<I>(
image: &mut I,
center: (i32, i32),
radius: i32,
color: I::Pixel,
) where
I: GenericImage,
I::Pixel: 'static,
{
@ -263,27 +285,3 @@ where
}
}
}
#[cfg(target_os = "linux")]
pub fn dump_image_to_clipboard(image: &DynamicImage) -> Result<(), Error> {
let mut temp = tempfile::NamedTempFile::new()?;
image.write_to(&mut temp, ImageOutputFormat::PNG)?;
Command::new("xclip")
.args(&[
"-sel",
"clip",
"-t",
"image/png",
temp.path().to_str().unwrap(),
])
.status()
.map_err(|e| format_err!("Failed to copy image to clipboard: {}", e))?;
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn dump_image_to_clipboard(_image: &DynamicImage) -> Result<(), Error> {
Err(format_err!(
"This feature hasn't been implemented for your system"
))
}