├── .github └── workflows │ ├── audit.yaml │ ├── ci.yaml │ ├── release.yaml │ └── uud.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── bindings └── bindings.ts ├── deps.ts ├── deps_deprecated.ts ├── deps_test.ts ├── example └── main.ts ├── mod.ts ├── silicon.ts ├── silicon_deprecated.ts ├── silicon_test.ts ├── src └── lib.rs └── testdata └── main.rs /.github/workflows/audit.yaml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | jobs: 6 | audit: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: actions-rs/audit-check@v1 11 | with: 12 | token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - 'README.md' 8 | - '.gitignore' 9 | pull_request: 10 | paths-ignore: 11 | - 'README.md' 12 | - '.gitignore' 13 | jobs: 14 | lint: 15 | name: lint 16 | strategy: 17 | matrix: 18 | toolchain: [stable] 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Cache toolchain 23 | uses: Swatinem/rust-cache@v1 24 | - name: Setup Rust toolchain 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: ${{ matrix.toolchain }} 28 | override: true 29 | components: rustfmt, clippy 30 | - name: Check build 31 | run: | 32 | make check 33 | - name: Lint 34 | run: | 35 | make lint 36 | test: 37 | name: test 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | os: 42 | - windows-latest 43 | - macos-latest 44 | - ubuntu-latest 45 | toolchain: [stable] 46 | deno_version: [v1.x] 47 | runs-on: ${{ matrix.os }} 48 | steps: 49 | - name: Install dependencies 50 | if: runner.os == 'Linux' 51 | run: | 52 | sudo apt update 53 | sudo apt install expat 54 | sudo apt install libxml2-dev 55 | sudo apt install pkg-config libasound2-dev libssl-dev cmake libfreetype6-dev libexpat1-dev libxcb-composite0-dev 56 | - uses: actions/checkout@v2 57 | - name: Cache toolchain 58 | uses: Swatinem/rust-cache@v1 59 | - name: Setup Rust toolchain 60 | uses: actions-rs/toolchain@v1 61 | with: 62 | toolchain: ${{ matrix.toolchain }} 63 | override: true 64 | - uses: denoland/setup-deno@v1 65 | with: 66 | deno-version: ${{ matrix.deno_version }} 67 | - name: Install deno_bindgen 68 | run: | 69 | deno install -Afq -n deno_bindgen https://deno.land/x/deno_bindgen/cli.ts 70 | - name: Test 71 | run: | 72 | make test 73 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | strategy: 12 | matrix: 13 | target: [ "x86_64-pc-windows-msvc", "x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "aarch64-apple-darwin" ] 14 | include: 15 | - target: "x86_64-pc-windows-msvc" 16 | lib: "deno_silicon.dll" 17 | os: "windows-2022" 18 | - target: "x86_64-unknown-linux-gnu" 19 | lib: "libdeno_silicon.so" 20 | os: "ubuntu-22.04" 21 | - target: "x86_64-apple-darwin" 22 | lib: "libdeno_silicon.dylib" 23 | os: "macos-12" 24 | - target: "aarch64-apple-darwin" 25 | lib: "libdeno_silicon_arm64.dylib" 26 | os: "macos-12" 27 | toolchain: [stable] 28 | runs-on: ${{ matrix.os }} 29 | steps: 30 | - uses: actions/checkout@v2 31 | with: 32 | fetch-depth: 0 33 | - uses: Swatinem/rust-cache@v1 34 | with: 35 | key: ${{ matrix.target }} 36 | - name: Setup Rust toolchain 37 | uses: actions-rs/toolchain@v1 38 | with: 39 | toolchain: ${{ matrix.toolchain }} 40 | profile: minimal 41 | override: true 42 | - name: Build aarch64 43 | if: ${{ matrix.target == 'aarch64-apple-darwin' }} 44 | run: | 45 | cargo install cross --git https://github.com/cross-rs/cross 46 | rustup target add ${{ matrix.target }} 47 | cross build --release --target ${{ matrix.target }} 48 | mv target/${{ matrix.target }}/release/libdeno_silicon.dylib target/${{ matrix.target }}/release/${{ matrix.lib }} 49 | - name: Build 50 | if: ${{ matrix.target != 'aarch64-apple-darwin' }} 51 | run: | 52 | rustup target add ${{ matrix.target }} 53 | cargo build --release --target ${{ matrix.target }} 54 | - name: Create release 55 | uses: ncipollo/release-action@v1 56 | with: 57 | omitBody: true 58 | allowUpdates: true 59 | artifacts: 'target/${{ matrix.target }}/release/${{ matrix.lib }}' 60 | token: ${{ secrets.GITHUB_TOKEN }} 61 | -------------------------------------------------------------------------------- /.github/workflows/uud.yaml: -------------------------------------------------------------------------------- 1 | name: Update 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | udd: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: denoland/setup-deno@v1 14 | with: 15 | deno-version: "1.x" 16 | - name: Update dependencies 17 | run: | 18 | make deps > ../output.txt 19 | env: 20 | NO_COLOR: 1 21 | - name: Read ../output.txt 22 | id: log 23 | uses: juliangruber/read-file-action@v1 24 | with: 25 | path: ../output.txt 26 | - name: Diff check 27 | id: diff 28 | run: | 29 | echo "::set-output name=content::$(git diff --name-only | grep ts)" 30 | - name: Commit changes 31 | if: ${{ steps.diff.outputs.content != '' }} 32 | run: | 33 | git config user.name '${{ github.actor }}' 34 | git config user.email '${{ github.actor }}@users.noreply.github.com' 35 | git commit -a -F- < 134 | } 135 | } 136 | | { 137 | ThemeList: { 138 | data: Array 139 | } 140 | } 141 | | { 142 | Image: { 143 | data: string 144 | } 145 | } 146 | | { 147 | Error: { 148 | error: string 149 | } 150 | } 151 | export function font_list() { 152 | const rawResult = symbols.font_list() 153 | const result = readPointer(rawResult) 154 | return JSON.parse(decode(result)) as SiliconResult 155 | } 156 | export function generate(a0: Options) { 157 | const a0_buf = encode(JSON.stringify(a0)) 158 | 159 | const rawResult = symbols.generate(a0_buf, a0_buf.byteLength) 160 | const result = readPointer(rawResult) 161 | return JSON.parse(decode(result)) as SiliconResult 162 | } 163 | export function theme_list() { 164 | const rawResult = symbols.theme_list() 165 | const result = readPointer(rawResult) 166 | return JSON.parse(decode(result)) as SiliconResult 167 | } 168 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export * as base64 from "https://deno.land/std@0.218.0/encoding/base64.ts"; 2 | export * from "https://deno.land/x/unknownutil@v3.16.3/mod.ts"; 3 | -------------------------------------------------------------------------------- /deps_deprecated.ts: -------------------------------------------------------------------------------- 1 | export * as io from "https://deno.land/std@0.205.0/io/mod.ts"; 2 | -------------------------------------------------------------------------------- /deps_test.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.205.0/assert/mod.ts"; 2 | export * as path from "https://deno.land/std@0.205.0/path/mod.ts"; 3 | export { readAll } from "https://deno.land/std@0.205.0/streams/mod.ts"; 4 | -------------------------------------------------------------------------------- /example/main.ts: -------------------------------------------------------------------------------- 1 | import * as silicon from "../mod.ts"; 2 | 3 | const code = `package main 4 | 5 | import { 6 | "fmt" 7 | } 8 | 9 | func main() { 10 | fmt.Println("Hello World") 11 | }`; 12 | 13 | const data = silicon.generate(code, "go", { theme: "Dracula" }); 14 | await Deno.writeFile("out.png", data); 15 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./silicon.ts"; 2 | export * from "./silicon_deprecated.ts"; 3 | -------------------------------------------------------------------------------- /silicon.ts: -------------------------------------------------------------------------------- 1 | import { 2 | font_list, 3 | generate as generate_, 4 | Options as Options_, 5 | theme_list, 6 | } from "./bindings/bindings.ts"; 7 | import { base64, is } from "./deps.ts"; 8 | 9 | const isError = is.ObjectOf({ 10 | Error: is.ObjectOf({ 11 | error: is.String, 12 | }), 13 | }); 14 | 15 | const isFontList = is.ObjectOf({ 16 | FontList: is.ObjectOf({ 17 | data: is.ArrayOf(is.String), 18 | }), 19 | }); 20 | 21 | const isThemeList = is.ObjectOf({ 22 | ThemeList: is.ObjectOf({ 23 | data: is.ArrayOf(is.String), 24 | }), 25 | }); 26 | 27 | const isImage = is.ObjectOf({ 28 | Image: is.ObjectOf({ 29 | data: is.String, 30 | }), 31 | }); 32 | 33 | /** 34 | * Return available font list 35 | */ 36 | export function fontList(): string[] { 37 | const result = font_list(); 38 | if (isError(result)) { 39 | throw new Error(`cannot get font list: ${result.Error.error}`); 40 | } 41 | if (isFontList(result)) { 42 | return result.FontList.data; 43 | } 44 | throw new Error(`unexpected result: ${JSON.stringify(result)}`); 45 | } 46 | 47 | /** 48 | * Return available theme list 49 | */ 50 | export function themeList(): string[] { 51 | const result = theme_list(); 52 | if (isThemeList(result)) { 53 | return result.ThemeList.data; 54 | } 55 | throw new Error(`unexpected result: ${JSON.stringify(result)}`); 56 | } 57 | 58 | export type Options = Omit; 59 | 60 | const defaultOptions: Options = { 61 | font: "", 62 | highlight_lines: "", 63 | no_line_number: false, 64 | no_round_corner: false, 65 | no_window_controls: false, 66 | background_color: "#aaaaff", 67 | line_offset: 1, 68 | line_pad: 2, 69 | pad_horiz: 80, 70 | pad_vert: 100, 71 | shadow_blur_radius: 0, 72 | shadow_color: "#555555", 73 | shadow_offset_x: 0, 74 | shadow_offset_y: 0, 75 | tab_width: 4, 76 | theme: "Solarized (dark)", 77 | }; 78 | 79 | /* 80 | * Generate an image from a code. 81 | */ 82 | export function generate( 83 | code: string, 84 | language: string, 85 | opts: Partial = {}, 86 | ): Uint8Array { 87 | const result = generate_({ 88 | ...defaultOptions, 89 | ...opts, 90 | code: code, 91 | language: language, 92 | }); 93 | if (isError(result)) { 94 | throw new Error(`cannot generate image: ${result.Error.error}`); 95 | } 96 | if (isImage(result)) { 97 | return base64.decodeBase64(result.Image.data); 98 | } 99 | throw new Error(`unexpected result: ${JSON.stringify(result)}`); 100 | } 101 | -------------------------------------------------------------------------------- /silicon_deprecated.ts: -------------------------------------------------------------------------------- 1 | import { io } from "./deps_deprecated.ts"; 2 | import { generate, Options } from "./silicon.ts"; 3 | 4 | /** @deprecated Use `Partial` instead. */ 5 | export type Option = Partial; 6 | 7 | /* 8 | * Generates image from source code. 9 | * 10 | * @deprecated Use `generate` instead. 11 | */ 12 | export async function generateImage( 13 | code: string, 14 | language: string, 15 | opts?: Option, 16 | ): Promise { 17 | const data = generate(code, language, opts); 18 | const buffer = new io.Buffer(); 19 | await buffer.write(data); 20 | return buffer; 21 | } 22 | -------------------------------------------------------------------------------- /silicon_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertNotEquals, 4 | assertThrows, 5 | path, 6 | } from "./deps_test.ts"; 7 | import { generate, themeList } from "./silicon.ts"; 8 | 9 | function assertImage(data: Uint8Array) { 10 | const header = data.slice(0, 8); 11 | const pngHeader = new Uint8Array([ 12 | 0x89, 13 | 0x50, 14 | 0x4e, 15 | 0x47, 16 | 0x0d, 17 | 0x0a, 18 | 0x1a, 19 | 0x0a, 20 | ]); 21 | assertEquals(header, pngHeader); 22 | } 23 | 24 | Deno.test({ 25 | name: "get theme list", 26 | fn: () => { 27 | const got = themeList(); 28 | assertNotEquals(got.length, 0); 29 | }, 30 | }); 31 | 32 | Deno.test({ 33 | name: "generate image", 34 | fn: () => { 35 | const code = `package main 36 | 37 | import { 38 | "fmt" 39 | } 40 | 41 | func main() { 42 | fmt.Println("Hello World") 43 | }`; 44 | const got = generate(code, "go"); 45 | assertImage(got); 46 | }, 47 | }); 48 | 49 | Deno.test({ 50 | name: "generate image with options", 51 | fn: async () => { 52 | const code = new TextDecoder().decode( 53 | await Deno.readFile(path.join("testdata", "main.rs")), 54 | ); 55 | const got = generate(code, "rs", { 56 | no_line_number: true, 57 | no_round_corner: false, 58 | no_window_controls: true, 59 | background_color: "#CCCCCC", 60 | highlight_lines: "5-7", 61 | line_offset: 1, 62 | line_pad: 2, 63 | pad_horiz: 50, 64 | pad_vert: 50, 65 | shadow_blur_radius: 10.5, 66 | shadow_offset_x: 10, 67 | shadow_offset_y: 10, 68 | shadow_color: "#003399", 69 | tab_width: 10, 70 | theme: "Solarized (dark)", 71 | }); 72 | assertImage(got); 73 | }, 74 | }); 75 | 76 | Deno.test({ 77 | name: "invalid options", 78 | fn: () => { 79 | assertThrows( 80 | () => { 81 | generate("", "rs", { theme: "hoge" }); 82 | }, 83 | Error, 84 | "unsupported theme", 85 | ); 86 | 87 | assertThrows( 88 | () => { 89 | generate("", "rs", { background_color: "hoge" }); 90 | }, 91 | Error, 92 | "cannot generate image: invalid color", 93 | ); 94 | 95 | assertThrows( 96 | () => { 97 | generate("", "rs", { highlight_lines: "1;" }); 98 | }, 99 | Error, 100 | "cannot parse integer from empty string", 101 | ); 102 | }, 103 | }); 104 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::not_unsafe_ptr_arg_deref)] 2 | 3 | pub use std::io; 4 | 5 | use anyhow::{anyhow, Context, Result}; 6 | use deno_bindgen::deno_bindgen; 7 | use image::{ImageOutputFormat, Rgba}; 8 | use silicon::formatter::ImageFormatterBuilder; 9 | use silicon::utils::{init_syntect, Background, ShadowAdder, ToRgba}; 10 | use syntect::easy::HighlightLines; 11 | use syntect::util::LinesWithEndings; 12 | 13 | #[deno_bindgen] 14 | pub enum SiliconResult { 15 | FontList { data: Vec }, 16 | ThemeList { data: Vec }, 17 | Image { data: String }, 18 | Error { error: String }, 19 | } 20 | 21 | #[deno_bindgen] 22 | pub fn font_list() -> SiliconResult { 23 | let source = font_kit::source::SystemSource::new(); 24 | match source.all_families() { 25 | Ok(fonts) => SiliconResult::FontList { data: fonts }, 26 | Err(err) => SiliconResult::Error { 27 | error: err.to_string(), 28 | }, 29 | } 30 | } 31 | 32 | #[deno_bindgen] 33 | pub fn theme_list() -> SiliconResult { 34 | let (_, ts) = init_syntect(); 35 | let thems: Vec = ts.themes.keys().cloned().collect(); 36 | SiliconResult::ThemeList { data: thems } 37 | } 38 | 39 | #[deno_bindgen] 40 | pub struct Options { 41 | /// Source code 42 | code: String, 43 | /// full name ("Rust") or file extension ("rs") 44 | language: String, 45 | /// Hide the line number 46 | no_line_number: bool, 47 | /// Don't round the corner 48 | no_round_corner: bool, 49 | /// Hide the window controls 50 | no_window_controls: bool, 51 | /// Background color of the image [default: #aaaaff] 52 | background_color: String, 53 | /// The fallback font list. eg. 'Hack; SimSun=31' 54 | font: String, 55 | /// Lines to high light. rg. '1-3; 4' 56 | highlight_lines: String, 57 | /// Line number offset [default: 1] 58 | line_offset: u32, 59 | /// Pad between lines [default: 2] 60 | line_pad: u32, 61 | /// Pad horiz [default: 80] 62 | pad_horiz: u32, 63 | /// Pad vert [default: 100] 64 | pad_vert: u32, 65 | /// Blur radius of the shadow. (set it to 0 to hide shadow) [default: 0] 66 | shadow_blur_radius: f32, 67 | /// Color of shadow [default: #555555] 68 | shadow_color: String, 69 | /// Shadow's offset in X axis [default: 0] 70 | shadow_offset_x: i32, 71 | /// Shadow's offset in Y axis [default: 0] 72 | shadow_offset_y: i32, 73 | /// Tab width [default: 4] 74 | tab_width: u8, 75 | /// The syntax highlight theme. It can be a theme name or path to a .tmTheme file [default: Dracula] 76 | theme: String, 77 | } 78 | 79 | fn parse_font(s: String) -> Vec<(String, f32)> { 80 | let mut result = vec![]; 81 | for font in s.split(';') { 82 | let tmp = font.split('=').collect::>(); 83 | let font_name = tmp[0].to_owned(); 84 | let font_size = tmp 85 | .get(1) 86 | .map(|s| s.parse::().unwrap()) 87 | .unwrap_or(26.0); 88 | result.push((font_name, font_size)); 89 | } 90 | result 91 | } 92 | 93 | fn parse_line_range(s: String) -> Result> { 94 | let mut result = vec![]; 95 | for range in s.split(';') { 96 | let range: Vec = range 97 | .split('-') 98 | .map(|s| s.parse::()) 99 | .collect::, _>>()?; 100 | if range.len() == 1 { 101 | result.push(range[0]) 102 | } else { 103 | for i in range[0]..=range[1] { 104 | result.push(i); 105 | } 106 | } 107 | } 108 | Ok(result) 109 | } 110 | 111 | fn parse_color(s: String) -> Result> { 112 | let color = s.to_rgba().context("invalid color")?; 113 | Ok(color) 114 | } 115 | 116 | fn run(opts: Options) -> Result> { 117 | let (ps, ts) = init_syntect(); 118 | let code = opts.code; 119 | let lang = opts.language.as_str(); 120 | 121 | let syntax = if let Some(syntax) = ps.find_syntax_by_token(lang) { 122 | syntax 123 | } else { 124 | ps.find_syntax_by_token("txt").unwrap() 125 | }; 126 | 127 | let theme = &ts 128 | .themes 129 | .get(&opts.theme) 130 | .ok_or_else(|| anyhow!("unsupported theme"))?; 131 | 132 | let source = font_kit::source::SystemSource::new(); 133 | 134 | let all_fonts = source 135 | .all_families() 136 | .context("cannot get all font families")?; 137 | let font = if opts.font.is_empty() || !all_fonts.contains(&opts.font) { 138 | vec![] 139 | } else { 140 | parse_font(opts.font) 141 | }; 142 | 143 | let mut h = HighlightLines::new(syntax, theme); 144 | let highlight = LinesWithEndings::from(&code) 145 | .map(|line| h.highlight(line, &ps)) 146 | .collect::>(); 147 | 148 | let mut highlight_lines = Vec::::new(); 149 | if !opts.highlight_lines.is_empty() { 150 | highlight_lines = parse_line_range(opts.highlight_lines)?; 151 | } 152 | 153 | let background = parse_color(opts.background_color)?; 154 | let shadow = parse_color(opts.shadow_color)?; 155 | 156 | let shadow_adder = ShadowAdder::new() 157 | .background(Background::Solid(background)) 158 | .shadow_color(shadow) 159 | .blur_radius(opts.shadow_blur_radius) 160 | .pad_horiz(opts.pad_horiz) 161 | .pad_vert(opts.pad_vert) 162 | .offset_x(opts.shadow_offset_x) 163 | .offset_y(opts.shadow_offset_y); 164 | 165 | let mut formatter = ImageFormatterBuilder::new() 166 | .line_pad(opts.line_pad) 167 | .window_controls(!opts.no_window_controls) 168 | .line_number(!opts.no_line_number) 169 | .font(font) 170 | .round_corner(!opts.no_round_corner) 171 | .shadow_adder(shadow_adder) 172 | .tab_width(opts.tab_width) 173 | .highlight_lines(highlight_lines) 174 | .line_offset(opts.line_offset) 175 | .build()?; 176 | 177 | let image = formatter.format(&highlight, theme); 178 | 179 | let mut out = io::Cursor::new(Vec::new()); 180 | image.write_to(&mut out, ImageOutputFormat::Png)?; 181 | 182 | let result = out.get_ref().to_vec(); 183 | Ok(result) 184 | } 185 | 186 | #[deno_bindgen] 187 | pub fn generate(opts: Options) -> SiliconResult { 188 | match run(opts) { 189 | Ok(raw_data) => { 190 | let b64 = base64::encode(raw_data.as_slice()); 191 | SiliconResult::Image { data: b64 } 192 | } 193 | Err(err) => SiliconResult::Error { 194 | error: err.to_string(), 195 | }, 196 | } 197 | } 198 | 199 | #[cfg(test)] 200 | mod tests { 201 | use std::{fs::File, io::Read, path::Path}; 202 | 203 | use super::*; 204 | 205 | fn assert_image(raw: Vec) { 206 | let png_header: Vec = vec![0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; 207 | let raw_header = &raw[..8]; 208 | assert_eq!(raw_header, png_header); 209 | } 210 | 211 | fn default_opts() -> Options { 212 | Options { 213 | code: "".into(), 214 | language: "rs".into(), 215 | no_line_number: true, 216 | no_round_corner: false, 217 | no_window_controls: true, 218 | background_color: "#CCCCCC".into(), 219 | font: "".into(), 220 | highlight_lines: "5-7".into(), 221 | line_offset: 1, 222 | line_pad: 2, 223 | pad_horiz: 50, 224 | pad_vert: 50, 225 | shadow_blur_radius: 10.5, 226 | shadow_offset_x: 10, 227 | shadow_offset_y: 10, 228 | shadow_color: "#003399".into(), 229 | tab_width: 10, 230 | theme: "Solarized (dark)".into(), 231 | } 232 | } 233 | 234 | #[test] 235 | fn test_run() { 236 | let mut source = File::open(Path::new("testdata/main.rs")).unwrap(); 237 | let mut contents = String::new(); 238 | source.read_to_string(&mut contents).unwrap(); 239 | 240 | let mut opts = default_opts(); 241 | opts.code = contents; 242 | 243 | let got = run(opts).unwrap(); 244 | assert_image(got); 245 | } 246 | 247 | #[test] 248 | fn test_run_without_lang() { 249 | let mut opts = default_opts(); 250 | opts.code = "hoge".into(); 251 | opts.language = "".into(); 252 | 253 | let got = run(opts).unwrap(); 254 | assert_image(got); 255 | } 256 | 257 | #[test] 258 | fn test_run_not_found_font() { 259 | let mut opts = default_opts(); 260 | opts.code = "hoge".into(); 261 | opts.font = "not_found".into(); 262 | 263 | let got = run(opts).unwrap(); 264 | assert_image(got); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /testdata/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::prelude::*; 3 | 4 | fn main() -> std::io::Result<()> { 5 | let mut file = File::create("foo.txt")?; 6 | file.write_all(b"Hello, world!")?; 7 | Ok(()) 8 | } 9 | --------------------------------------------------------------------------------