initial commit ✨
This commit is contained in:
commit
71d91f02e6
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "inkify"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4"
|
||||
silicon = { git = "https://github.com/Aloxaf/silicon.git" }
|
||||
lazy_static = "1.4.0"
|
||||
serde = { version = "1.0.130", features = ["derive"] }
|
||||
structopt = "0.3.26"
|
||||
image = "0.24.7"
|
||||
anyhow = "1.0.75"
|
||||
thiserror = "1.0.49"
|
||||
syntect = "5.1.0"
|
||||
font-kit = "0.11.0"
|
||||
reqwest = "0.11.22"
|
|
@ -0,0 +1,59 @@
|
|||
FROM rust:1.73.0-buster as builder
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libssl-dev \
|
||||
pkg-config \
|
||||
libharfbuzz-dev
|
||||
|
||||
# Copy files
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
RUN cargo build --release
|
||||
|
||||
FROM debian:buster-slim as fonts
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
wget \
|
||||
xz-utils
|
||||
|
||||
WORKDIR /data/fonts
|
||||
|
||||
COPY ./docker/download_nerd_fonts.sh .
|
||||
|
||||
RUN ls -la
|
||||
RUN chmod +x download_nerd_fonts.sh
|
||||
RUN bash ./download_nerd_fonts.sh
|
||||
|
||||
RUN mkdir -p /usr/share/fonts/truetype
|
||||
RUN mv *.ttf /usr/share/fonts/truetype
|
||||
|
||||
FROM debian:buster-slim
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libssl-dev \
|
||||
libharfbuzz-dev \
|
||||
libfontconfig1 \
|
||||
fontconfig
|
||||
|
||||
# Copy fonts
|
||||
COPY --from=fonts /usr/share/fonts/truetype /usr/share/fonts/truetype/
|
||||
RUN fc-cache -fv
|
||||
|
||||
# Copy binary
|
||||
COPY --from=builder /usr/src/app/target/release/inkify /usr/local/bin/inkify
|
||||
|
||||
ARG PORT=8080
|
||||
ARG HOST=0.0.0.0
|
||||
|
||||
ENV PORT=$PORT
|
||||
ENV HOST=$HOST
|
||||
|
||||
EXPOSE $PORT
|
||||
|
||||
# Run
|
||||
ENTRYPOINT ["/usr/local/bin/inkify"]
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Chris Watson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,62 @@
|
|||
# Inkify
|
||||
|
||||
Unfortunately [Carbon](https://carbon.now.sh) has been without an API for too long, and I've run into a few cases where one would be useful for a project. So I present to you, Inkify, an API for generating beautiful pictures of your code.
|
||||
|
||||
## Usage
|
||||
|
||||
Inkify relies on the [silicon](https://github.com/Aloxaf/silicon) library for generating photos, and takes much the same arguments as the silicon CLI does. Arguments are passed as query parameters to the `/generate` route, and are as follows:
|
||||
|
||||
- code: The code to generate an image from. Required.
|
||||
- language: The language to use for syntax highlighting. Optional, will attempt to guess if not provided.
|
||||
- theme: The theme to use for syntax highlighting. Optional, defaults to Dracula.
|
||||
- font: The font to use. Optional, defaults to Fira Code.
|
||||
- shadow_color: The color of the shadow. Optional, defaults to transparent.
|
||||
- background: The background color. Optional, defaults to transparent.
|
||||
- tab_width: The tab width. Optional, defaults to 4.
|
||||
- line_pad: The line padding. Optional, defaults to 2.
|
||||
- line_offset: The line offset. Optional, defaults to 1.
|
||||
- window_title: The window title. Optional, defaults to \"Inkify\".
|
||||
- no_line_number: Whether to hide the line numbers. Optional, defaults to false.
|
||||
- no_round_corner: Whether to round the corners. Optional, defaults to false.
|
||||
- no_window_controls: Whether to hide the window controls. Optional, defaults to false.
|
||||
- shadow_blur_radius: The shadow blur radius. Optional, defaults to 0.
|
||||
- shadow_offset_x: The shadow offset x. Optional, defaults to 0.
|
||||
- shadow_offset_y: The shadow offset y. Optional, defaults to 0.
|
||||
- pad_horiz: The horizontal padding. Optional, defaults to 80.
|
||||
- pad_vert: The vertical padding. Optional, defaults to 100.
|
||||
- highlight_lines: The lines to highlight. Optional, defaults to none.
|
||||
- background_image: The background image for the padding area as a URL. Optional, defaults to none.
|
||||
|
||||
### Routes
|
||||
|
||||
#### `GET /`
|
||||
|
||||
The index route is used as a help/ping route. It will always return a 200 response if the API is live, and the body is a JSON object containing a message and a list of routes.
|
||||
|
||||
#### `GET /generate`
|
||||
|
||||
The generate route is used to generate images. It takes the arguments listed above as query parameters, and returns a PNG image.
|
||||
|
||||
#### `GET /themes`
|
||||
|
||||
The themes route is used to get a list of available themes. It takes no arguments, and returns a JSON object containing a list of themes.
|
||||
|
||||
#### `GET /fonts`
|
||||
|
||||
The fonts route is used to get a list of available fonts. It takes no arguments, and returns a JSON object containing a list of fonts.
|
||||
|
||||
#### `GET /languages`
|
||||
|
||||
The languages route is used to get a list of available languages. It takes no arguments, and returns a JSON object containing a list of languages supported by the [syntect](https://github.com/trishume/syntect) library (which is used by silicon under the hood).
|
||||
|
||||
## Deployment
|
||||
|
||||
Inkify is written in Rust using the [actix-web](https://actix.rs) framework, and can be deployed as a standalone binary. It can also be deployed as a Docker container, and a Dockerfile is provided for this purpose. The Dockerfile also installs all nerd fonts by default, allowing you to use any of them as the font for your code.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome, and can be made by opening a pull request. Please make sure to lint your code using `cargo clippy` before submitting a pull request.
|
||||
|
||||
## License
|
||||
|
||||
Inkify is licensed under the MIT license. See the [LICENSE](LICENSE) file for more information.
|
|
@ -0,0 +1,68 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
all_nerd_fonts=(
|
||||
"3270"
|
||||
"Agave"
|
||||
"AnonymousPro"
|
||||
"Arimo"
|
||||
"AurulentSansMono"
|
||||
"BigBlueTerminal"
|
||||
"BitstreamVeraSansMono"
|
||||
"CascadiaCode"
|
||||
"CodeNewRoman"
|
||||
"ComicShannsMono"
|
||||
"Cousine"
|
||||
"DaddyTimeMono"
|
||||
"DejaVuSansMono"
|
||||
"DroidSansMono"
|
||||
"EnvyCodeR"
|
||||
"FantasqueSansMono"
|
||||
"FiraCode"
|
||||
"FiraMono"
|
||||
"Go-Mono"
|
||||
"Gohu"
|
||||
"Hack"
|
||||
"Hasklig"
|
||||
"HeavyData"
|
||||
"Hermit"
|
||||
"iA-Writer"
|
||||
"IBMPlexMono"
|
||||
"Inconsolata"
|
||||
"InconsolataGo"
|
||||
"InconsolataLGC"
|
||||
"IntelOneMono"
|
||||
"Iosevka"
|
||||
"IosevkaTerm"
|
||||
"JetBrainsMono"
|
||||
"Lekton"
|
||||
"LiberationMono"
|
||||
"Lilex"
|
||||
"Meslo"
|
||||
"Monofur"
|
||||
"Monoid"
|
||||
"Mononoki"
|
||||
"MPlus"
|
||||
"NerdFontsSymbolsOnly"
|
||||
"Noto"
|
||||
"OpenDyslexic"
|
||||
"Overpass"
|
||||
"ProFont"
|
||||
"ProggyClean"
|
||||
"RobotoMono"
|
||||
"ShareTechMono"
|
||||
"SourceCodePro"
|
||||
"SpaceMono"
|
||||
"Terminus"
|
||||
"Tinos"
|
||||
"Ubuntu"
|
||||
"UbuntuMono"
|
||||
"VictorMono"
|
||||
)
|
||||
|
||||
# Download each font, un-tar it, and install it
|
||||
for font in "${all_nerd_fonts[@]}"; do
|
||||
echo "Downloading $font..."
|
||||
wget "https://github.com/ryanoasis/nerd-fonts/releases/download/v3.0.2/$font.tar.xz"
|
||||
tar -xf "./$font.tar.xz"
|
||||
rm "$font.tar.xz"
|
||||
done
|
|
@ -0,0 +1,222 @@
|
|||
use silicon::formatter::{ImageFormatter, ImageFormatterBuilder};
|
||||
use silicon::utils::{Background, ShadowAdder};
|
||||
use std::path::PathBuf;
|
||||
use anyhow::Error;
|
||||
use syntect::highlighting::{Theme, ThemeSet};
|
||||
use syntect::parsing::{SyntaxReference, SyntaxSet};
|
||||
|
||||
use crate::rgba::{Rgba, ImageRgba};
|
||||
|
||||
type FontList = Vec<(String, f32)>;
|
||||
type Lines = Vec<u32>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct Config {
|
||||
/// Background image URL
|
||||
pub background_image: Option<Vec<u8>>,
|
||||
|
||||
/// Background color of the image
|
||||
pub background: Rgba,
|
||||
|
||||
/// The code to highlight.
|
||||
pub code: String,
|
||||
|
||||
/// The fallback font list. eg. 'Hack; SimSun=31'
|
||||
pub font: Option<FontList>,
|
||||
|
||||
/// Lines to high light. rg. '1-3; 4'
|
||||
pub highlight_lines: Option<Lines>,
|
||||
|
||||
/// The language for syntax highlighting. You can use full name ("Rust") or file extension ("rs").
|
||||
pub language: Option<String>,
|
||||
|
||||
/// Pad between lines
|
||||
pub line_pad: u32,
|
||||
|
||||
/// Line number offset
|
||||
pub line_offset: u32,
|
||||
|
||||
/// Hide the window controls.
|
||||
pub no_window_controls: bool,
|
||||
|
||||
/// Show window title
|
||||
pub window_title: Option<String>,
|
||||
|
||||
/// Hide the line number.
|
||||
pub no_line_number: bool,
|
||||
|
||||
/// Don't round the corner
|
||||
pub no_round_corner: bool,
|
||||
|
||||
/// Pad horiz
|
||||
pub pad_horiz: u32,
|
||||
|
||||
/// Pad vert
|
||||
pub pad_vert: u32,
|
||||
|
||||
/// Color of shadow
|
||||
pub shadow_color: Rgba,
|
||||
|
||||
/// Blur radius of the shadow. (set it to 0 to hide shadow)
|
||||
pub shadow_blur_radius: f32,
|
||||
|
||||
/// Shadow's offset in Y axis
|
||||
pub shadow_offset_y: i32,
|
||||
|
||||
/// Shadow's offset in X axis
|
||||
pub shadow_offset_x: i32,
|
||||
|
||||
/// Tab width
|
||||
pub tab_width: u8,
|
||||
|
||||
/// The syntax highlight theme. It can be a theme name or path to a .tmTheme file.
|
||||
pub theme: String
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn default() -> Self {
|
||||
Config {
|
||||
background_image: None,
|
||||
background: Rgba(ImageRgba([0, 0, 0, 0])),
|
||||
code: "".to_owned(),
|
||||
font: None,
|
||||
highlight_lines: None,
|
||||
language: None,
|
||||
line_pad: 2,
|
||||
line_offset: 1,
|
||||
no_window_controls: false,
|
||||
window_title: None,
|
||||
no_line_number: false,
|
||||
no_round_corner: false,
|
||||
pad_horiz: 80,
|
||||
pad_vert: 100,
|
||||
shadow_color: Rgba(ImageRgba([0, 0, 0, 0])),
|
||||
shadow_blur_radius: 0.0,
|
||||
shadow_offset_y: 0,
|
||||
shadow_offset_x: 0,
|
||||
tab_width: 4,
|
||||
theme: "Dracula".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn language<'a>(&self, ps: &'a SyntaxSet) -> Result<&'a SyntaxReference, Error> {
|
||||
let possible_language = self.language.as_ref().map(|language| {
|
||||
ps.find_syntax_by_token(language)
|
||||
.ok_or_else(|| format_err!("Unable to determine language, please provide one explicitly"))
|
||||
});
|
||||
|
||||
let language = possible_language.unwrap_or_else(|| {
|
||||
ps.find_syntax_by_first_line(self.code.as_ref())
|
||||
.ok_or_else(|| format_err!("Unable to determine language, please provide one explicitly"))
|
||||
})?;
|
||||
|
||||
Ok(language)
|
||||
}
|
||||
|
||||
pub fn theme(&self, ts: &ThemeSet) -> Result<Theme, Error> {
|
||||
if let Some(theme) = ts.themes.get(&self.theme) {
|
||||
Ok(theme.clone())
|
||||
} else {
|
||||
ThemeSet::get_theme(PathBuf::from(&self.theme))
|
||||
.map_err(|e| Error::msg(format!("Invalid theme: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn get_formatter(&self) -> Result<ImageFormatter, Error> {
|
||||
let formatter = ImageFormatterBuilder::new()
|
||||
.line_pad(self.line_pad)
|
||||
.window_controls(!self.no_window_controls)
|
||||
.window_title(self.window_title.clone())
|
||||
.line_number(!self.no_line_number)
|
||||
.font(self.font.clone().unwrap_or_default())
|
||||
.round_corner(!self.no_round_corner)
|
||||
.shadow_adder(self.get_shadow_adder()?)
|
||||
.tab_width(self.tab_width)
|
||||
.highlight_lines(self.highlight_lines.clone().unwrap_or_default())
|
||||
.line_offset(self.line_offset);
|
||||
|
||||
Ok(formatter.build()?)
|
||||
}
|
||||
|
||||
pub fn get_shadow_adder(&self) -> Result<ShadowAdder, Error> {
|
||||
Ok(ShadowAdder::new()
|
||||
.background(match &self.background_image {
|
||||
Some(path) => Background::Image(image::load_from_memory(path)?.to_rgba8()),
|
||||
None => Background::Solid(self.background.to_rgba()),
|
||||
})
|
||||
.shadow_color(self.shadow_color.to_rgba())
|
||||
.blur_radius(self.shadow_blur_radius)
|
||||
.pad_horiz(self.pad_horiz)
|
||||
.pad_vert(self.pad_vert)
|
||||
.offset_x(self.shadow_offset_x)
|
||||
.offset_y(self.shadow_offset_y))
|
||||
}
|
||||
}
|
||||
|
||||
/// Query parameters for the /generate endpoint, using Option to make all options
|
||||
/// with defaults optional.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ConfigQuery {
|
||||
/// Background image URL
|
||||
pub background_image: Option<String>,
|
||||
|
||||
/// Background color of the image
|
||||
pub background: Option<String>,
|
||||
|
||||
/// The code to highlight.
|
||||
pub code: String,
|
||||
|
||||
/// The fallback font list. eg. 'Hack; SimSun=31'
|
||||
pub font: Option<String>,
|
||||
|
||||
/// Lines to high light. rg. '1-3; 4'
|
||||
pub highlight_lines: Option<String>,
|
||||
|
||||
/// The language for syntax highlighting. You can use full name ("Rust") or file extension ("rs").
|
||||
pub language: Option<String>,
|
||||
|
||||
/// Pad between lines
|
||||
pub line_pad: Option<u32>,
|
||||
|
||||
/// Line number offset
|
||||
pub line_offset: Option<u32>,
|
||||
|
||||
/// Hide the window controls.
|
||||
pub no_window_controls: Option<bool>,
|
||||
|
||||
/// Show window title
|
||||
pub window_title: Option<String>,
|
||||
|
||||
/// Hide the line number.
|
||||
pub no_line_number: Option<bool>,
|
||||
|
||||
/// Don't round the corner
|
||||
pub no_round_corner: Option<bool>,
|
||||
|
||||
/// Pad horiz
|
||||
pub pad_horiz: Option<u32>,
|
||||
|
||||
/// Pad vert
|
||||
pub pad_vert: Option<u32>,
|
||||
|
||||
/// Color of shadow
|
||||
pub shadow_color: Option<String>,
|
||||
|
||||
/// Blur radius of the shadow. (set it to 0 to hide shadow)
|
||||
pub shadow_blur_radius: Option<f32>,
|
||||
|
||||
/// Shadow's offset in Y axis
|
||||
pub shadow_offset_y: Option<i32>,
|
||||
|
||||
/// Shadow's offset in X axis
|
||||
pub shadow_offset_x: Option<i32>,
|
||||
|
||||
/// Tab width
|
||||
pub tab_width: Option<u8>,
|
||||
|
||||
/// The syntax highlight theme. It can be a theme name or path to a .tmTheme file.
|
||||
pub theme: Option<String>
|
||||
}
|
|
@ -0,0 +1,287 @@
|
|||
#[macro_use]
|
||||
extern crate anyhow;
|
||||
|
||||
use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
|
||||
use anyhow::Error;
|
||||
use lazy_static::lazy_static;
|
||||
use silicon as si;
|
||||
use silicon::utils::ToRgba;
|
||||
use std::collections::HashSet;
|
||||
use std::io::Cursor;
|
||||
use std::num::ParseIntError;
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::util::LinesWithEndings;
|
||||
|
||||
mod config;
|
||||
mod rgba;
|
||||
|
||||
lazy_static! {
|
||||
static ref HIGHLIGHTING_ASSETS: si::assets::HighlightingAssets =
|
||||
silicon::assets::HighlightingAssets::new();
|
||||
}
|
||||
|
||||
macro_rules! unwrap_or_return {
|
||||
( $e:expr, $r:expr ) => {
|
||||
match $e {
|
||||
Ok(x) => x,
|
||||
Err(_) => return $r,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_font_str(s: &str) -> Vec<(String, f32)> {
|
||||
let mut result = vec![];
|
||||
for font in s.split(';') {
|
||||
let tmp = font.split('=').collect::<Vec<_>>();
|
||||
let font_name = tmp[0].to_owned();
|
||||
let font_size = tmp
|
||||
.get(1)
|
||||
.map(|s| s.parse::<f32>().unwrap())
|
||||
.unwrap_or(26.0);
|
||||
result.push((font_name, font_size));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn parse_line_range(s: &str) -> Result<Vec<u32>, ParseIntError> {
|
||||
let mut result = vec![];
|
||||
for range in s.split(';') {
|
||||
let range: Vec<u32> = range
|
||||
.split('-')
|
||||
.map(|s| s.parse::<u32>())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
if range.len() == 1 {
|
||||
result.push(range[0])
|
||||
} else {
|
||||
for i in range[0]..=range[1] {
|
||||
result.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn parse_str_color(s: &str) -> Result<rgba::Rgba, Error> {
|
||||
let res = s
|
||||
.to_rgba()
|
||||
.map_err(|_| format_err!("Invalid color: `{}`", s));
|
||||
Ok(rgba::Rgba(res?))
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn help() -> impl Responder {
|
||||
// Respond with some help text for how to use the API,
|
||||
// formatted as JSON since this is an API.
|
||||
let json = r#"
|
||||
{
|
||||
"message": "Hello, world! Welcome to Inkify, a simple API for generating images from code. Think of it like Carbon in API form.",
|
||||
"routes": {
|
||||
"GET /": "This help text. Will always return 200, so you can use it to check if the server is up.",
|
||||
"GET /themes": "Return a list of available syntax themes.",
|
||||
"GET /languages": "Retuns a list of languages which can be parsed.",
|
||||
"GET /fonts": "Returns a list of available fonts.",
|
||||
"GET /generate": {
|
||||
"description": "Generate an image from the given code.",
|
||||
"parameters": {
|
||||
"code": "The code to generate an image from. Required.",
|
||||
"language": "The language to use for syntax highlighting. Optional, will attempt to guess if not provided.",
|
||||
"theme": "The theme to use for syntax highlighting. Optional, defaults to Dracula.",
|
||||
"font": "The font to use. Optional, defaults to Fira Code.",
|
||||
"shadow_color": "The color of the shadow. Optional, defaults to transparent.",
|
||||
"background": "The background color. Optional, defaults to transparent.",
|
||||
"tab_width": "The tab width. Optional, defaults to 4.",
|
||||
"line_pad": "The line padding. Optional, defaults to 2.",
|
||||
"line_offset": "The line offset. Optional, defaults to 1.",
|
||||
"window_title": "The window title. Optional, defaults to \"Inkify\".",
|
||||
"no_line_number": "Whether to hide the line numbers. Optional, defaults to false.",
|
||||
"no_round_corner": "Whether to round the corners. Optional, defaults to false.",
|
||||
"no_window_controls": "Whether to hide the window controls. Optional, defaults to false.",
|
||||
"shadow_blur_radius": "The shadow blur radius. Optional, defaults to 0.",
|
||||
"shadow_offset_x": "The shadow offset x. Optional, defaults to 0.",
|
||||
"shadow_offset_y": "The shadow offset y. Optional, defaults to 0.",
|
||||
"pad_horiz": "The horizontal padding. Optional, defaults to 80.",
|
||||
"pad_vert": "The vertical padding. Optional, defaults to 100.",
|
||||
"highlight_lines": "The lines to highlight. Optional, defaults to none.",
|
||||
"background_image": "The background image for the padding area as a URL. Optional, defaults to none."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(json)
|
||||
}
|
||||
|
||||
#[get("/themes")]
|
||||
async fn themes() -> impl Responder {
|
||||
let ha = &*HIGHLIGHTING_ASSETS;
|
||||
let themes = &ha.theme_set.themes;
|
||||
let theme_keys: Vec<String> = themes.keys().map(|s| s.to_string()).collect();
|
||||
HttpResponse::Ok().json(theme_keys)
|
||||
}
|
||||
|
||||
#[get("/languages")]
|
||||
async fn languages() -> impl Responder {
|
||||
let ha = &*HIGHLIGHTING_ASSETS;
|
||||
let syntaxes = &ha.syntax_set.syntaxes();
|
||||
let mut languages = syntaxes
|
||||
.iter()
|
||||
.map(|s| s.name.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
let unique_languages: HashSet<String> = languages.drain(..).collect();
|
||||
let mut unique_languages: Vec<String> = unique_languages.into_iter().collect();
|
||||
unique_languages.sort();
|
||||
HttpResponse::Ok().json(unique_languages)
|
||||
}
|
||||
|
||||
#[get("/fonts")]
|
||||
async fn fonts() -> impl Responder {
|
||||
let source = font_kit::source::SystemSource::new();
|
||||
let fonts = source.all_families().unwrap_or_default();
|
||||
HttpResponse::Ok().json(fonts)
|
||||
}
|
||||
|
||||
#[get("/generate")]
|
||||
async fn generate(info: web::Query<config::ConfigQuery>) -> impl Responder {
|
||||
let ha = &*HIGHLIGHTING_ASSETS;
|
||||
let (ps, ts) = (&ha.syntax_set, &ha.theme_set);
|
||||
|
||||
let mut conf = config::Config::default();
|
||||
conf.code = info.code.clone();
|
||||
if conf.code.is_empty() {
|
||||
return HttpResponse::BadRequest()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(r#"{"error": "code parameter is required"}"#);
|
||||
}
|
||||
|
||||
conf.language = info.language.clone();
|
||||
if let Some(theme) = info.theme.clone() {
|
||||
conf.theme = theme;
|
||||
}
|
||||
if let Some(font) = info.font.clone() {
|
||||
conf.font = Some(parse_font_str(&font));
|
||||
}
|
||||
if let Some(shadow_color) = info.shadow_color.clone() {
|
||||
conf.shadow_color = parse_str_color(shadow_color.as_str()).unwrap();
|
||||
}
|
||||
if let Some(background) = info.background.clone() {
|
||||
conf.background = parse_str_color(background.as_str()).unwrap();
|
||||
}
|
||||
if let Some(tab_width) = info.tab_width {
|
||||
conf.tab_width = tab_width;
|
||||
}
|
||||
if let Some(line_pad) = info.line_pad {
|
||||
conf.line_pad = line_pad;
|
||||
}
|
||||
if let Some(line_offset) = info.line_offset {
|
||||
conf.line_offset = line_offset;
|
||||
}
|
||||
if let Some(window_title) = info.window_title.clone() {
|
||||
conf.window_title = Some(window_title);
|
||||
}
|
||||
if let Some(no_line_number) = info.no_line_number {
|
||||
conf.no_line_number = no_line_number;
|
||||
}
|
||||
if let Some(no_round_corner) = info.no_round_corner {
|
||||
conf.no_round_corner = no_round_corner;
|
||||
}
|
||||
if let Some(no_window_controls) = info.no_window_controls {
|
||||
conf.no_window_controls = no_window_controls;
|
||||
}
|
||||
if let Some(shadow_blur_radius) = info.shadow_blur_radius {
|
||||
conf.shadow_blur_radius = shadow_blur_radius;
|
||||
}
|
||||
if let Some(shadow_offset_x) = info.shadow_offset_x {
|
||||
conf.shadow_offset_x = shadow_offset_x;
|
||||
}
|
||||
if let Some(shadow_offset_y) = info.shadow_offset_y {
|
||||
conf.shadow_offset_y = shadow_offset_y;
|
||||
}
|
||||
if let Some(pad_horiz) = info.pad_horiz {
|
||||
conf.pad_horiz = pad_horiz;
|
||||
}
|
||||
if let Some(pad_vert) = info.pad_vert {
|
||||
conf.pad_vert = pad_vert;
|
||||
}
|
||||
if let Some(highlight_lines) = info.highlight_lines.clone() {
|
||||
conf.highlight_lines = Some(parse_line_range(highlight_lines.as_str()).unwrap());
|
||||
}
|
||||
if let Some(background_image) = info.background_image.clone() {
|
||||
// If a background image is provided, it will be as a URL. We need
|
||||
// to download it and add it to the config as a Vec<u8>.
|
||||
let res = reqwest::get(background_image.as_str()).await;
|
||||
if let Ok(mut res) = res {
|
||||
let mut buf = vec![];
|
||||
while let Ok(Some(chunk)) = res.chunk().await {
|
||||
buf.extend_from_slice(&chunk);
|
||||
}
|
||||
conf.background_image = Some(buf);
|
||||
}
|
||||
}
|
||||
|
||||
let syntax = unwrap_or_return!(
|
||||
conf.language(ps),
|
||||
HttpResponse::BadRequest()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(r#"{"error": "Unable to determine language, please provide one explicitly"}"#)
|
||||
);
|
||||
let theme = unwrap_or_return!(
|
||||
conf.theme(ts),
|
||||
HttpResponse::BadRequest()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(r#"{"error": "Invalid theme"}"#)
|
||||
);
|
||||
|
||||
let mut h = HighlightLines::new(syntax, &theme);
|
||||
let highlight = unwrap_or_return!(
|
||||
LinesWithEndings::from(conf.code.as_ref())
|
||||
.map(|line| h.highlight_line(line, ps))
|
||||
.collect::<Result<Vec<_>, _>>(),
|
||||
HttpResponse::InternalServerError()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(r#"{"error": "Failed to highlight code"}"#)
|
||||
);
|
||||
|
||||
let mut formatter = unwrap_or_return!(
|
||||
conf.get_formatter(),
|
||||
HttpResponse::InternalServerError()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(r#"{"error": "Failed to get formatter"}"#)
|
||||
);
|
||||
|
||||
let image = formatter.format(&highlight, &theme);
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
unwrap_or_return!(
|
||||
image.write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png),
|
||||
HttpResponse::InternalServerError()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(r#"{"error": "Failed to write image"}"#)
|
||||
);
|
||||
|
||||
// Return the image as a PNG.
|
||||
HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "image/png"))
|
||||
.body(buffer)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_owned());
|
||||
let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_owned());
|
||||
let server = HttpServer::new(|| {
|
||||
App::new()
|
||||
.service(help)
|
||||
.service(themes)
|
||||
.service(languages)
|
||||
.service(fonts)
|
||||
.service(generate)
|
||||
})
|
||||
.bind((host.clone(), port.parse::<u16>().unwrap()))?
|
||||
.run();
|
||||
|
||||
println!("Inkify listening on {}:{}", host, port);
|
||||
println!("Visit http://{}:{}/ to get started.", host, port);
|
||||
server.await
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
use silicon::utils::ToRgba;
|
||||
use anyhow::Error;
|
||||
pub use image::{Rgba as ImageRgba, Pixel};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rgba(pub ImageRgba<u8>);
|
||||
|
||||
impl Rgba {
|
||||
pub fn to_rgba(&self) -> ImageRgba<u8> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ImageRgba<u8>> for Rgba {
|
||||
fn from(rgba: ImageRgba<u8>) -> Self {
|
||||
Rgba(rgba)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Rgba {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Rgba, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
parse_str_color(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Rgba {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let channels = self.0.channels();
|
||||
let r = channels[0];
|
||||
let g = channels[1];
|
||||
let b = channels[2];
|
||||
let a = channels[3];
|
||||
write!(f, "#{:02x}{:02x}{:02x}{:02x}", r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_str_color(s: &str) -> Result<Rgba, Error> {
|
||||
let rgba = s.to_rgba()
|
||||
.map_err(|e| Error::msg(format!("Invalid color: {}", e)))?;
|
||||
Ok(Rgba(rgba))
|
||||
}
|
Loading…
Reference in New Issue