├── .editorconfig ├── .github └── workflows │ ├── autofix.yml │ └── checks.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── benchs └── index.ts ├── build.config.mjs ├── eslint.config.mjs ├── package.json ├── pnpm-lock.yaml ├── renovate.json ├── src ├── _utils.ts ├── core.ts ├── index.ts └── types.ts ├── test ├── .snapshot │ └── image.webp └── index.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.js] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [{package.json,*.yml,*.cjson}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yml: -------------------------------------------------------------------------------- 1 | name: autofix.ci 2 | on: { push: {}, pull_request: {} } 3 | permissions: { contents: read } 4 | jobs: 5 | autofix: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v5 9 | - run: npm i -fg corepack && corepack enable 10 | - uses: actions/setup-node@v5 11 | with: { node-version: 24, cache: "pnpm" } 12 | - run: pnpm install 13 | - run: pnpm lint:fix 14 | - run: pnpm automd 15 | - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 16 | with: { commit-message: "chore: apply automated updates" } 17 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: checks 2 | on: { push: {}, pull_request: {} } 3 | jobs: 4 | checks: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v5 8 | - run: npm i -fg corepack && corepack enable 9 | - uses: actions/setup-node@v5 10 | with: { node-version: 24, cache: "pnpm" } 11 | - run: pnpm install 12 | - run: pnpm run lint 13 | - run: pnpm vitest --coverage 14 | # - uses: codecov/codecov-action@v5 15 | # with: { token: ${{ secrets.CODECOV_TOKEN }} } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .vscode 5 | .DS_Store 6 | .eslintcache 7 | *.log* 8 | *.env* 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.1.4 4 | 5 | [compare changes](https://github.com/pi0/shiki-image/compare/v0.1.3...v0.1.4) 6 | 7 | ### 🚀 Enhancements 8 | 9 | - Update `takumi` and use builtin `Geist Mono` font ([#9](https://github.com/pi0/shiki-image/pull/9)) 10 | 11 | ### 🩹 Fixes 12 | 13 | - Quick fix for small code rendering ([696d3b0](https://github.com/pi0/shiki-image/commit/696d3b0)) 14 | 15 | ### 💅 Refactors 16 | 17 | - Improve font fetch with error handling ([#5](https://github.com/pi0/shiki-image/pull/5)) 18 | 19 | ### 🏡 Chore 20 | 21 | - Apply automated updates ([198d3f9](https://github.com/pi0/shiki-image/commit/198d3f9)) 22 | - Add benchmark script ([0241d82](https://github.com/pi0/shiki-image/commit/0241d82)) 23 | - Apply automated updates ([aaf37bb](https://github.com/pi0/shiki-image/commit/aaf37bb)) 24 | - Update Takumi ([29fb0d6](https://github.com/pi0/shiki-image/commit/29fb0d6)) 25 | - Apply automated updates ([2d95b1a](https://github.com/pi0/shiki-image/commit/2d95b1a)) 26 | - Update Takumi ([29005e5](https://github.com/pi0/shiki-image/commit/29005e5)) 27 | - Apply automated updates ([656855d](https://github.com/pi0/shiki-image/commit/656855d)) 28 | - **release:** V0.1.3 ([e612ca8](https://github.com/pi0/shiki-image/commit/e612ca8)) 29 | - Add `Alekhya` to showcase ([#3](https://github.com/pi0/shiki-image/pull/3)) 30 | - Add modern-monaco-demo to showcase ([d23ce8f](https://github.com/pi0/shiki-image/commit/d23ce8f)) 31 | 32 | ### ✅ Tests 33 | 34 | - Update snapshot ([7367cdf](https://github.com/pi0/shiki-image/commit/7367cdf)) 35 | - Allow update snapshot in ci ([8417fd5](https://github.com/pi0/shiki-image/commit/8417fd5)) 36 | 37 | ### ❤️ Contributors 38 | 39 | - Pooya Parsa ([@pi0](https://github.com/pi0)) 40 | - Reyalka ([@reyalka](https://github.com/reyalka)) 41 | - JD Solanki 42 | - Kane Wang ([@yeecord](https://github.com/yeecord)) 43 | 44 | ## v0.1.3 45 | 46 | [compare changes](https://github.com/pi0/shiki-image/compare/v0.1.2...v0.1.3) 47 | 48 | ### 🩹 Fixes 49 | 50 | - Quick fix for small code rendering ([305a0ec](https://github.com/pi0/shiki-image/commit/305a0ec)) 51 | 52 | ### 🏡 Chore 53 | 54 | - **release:** V0.1.2 ([2025f11](https://github.com/pi0/shiki-image/commit/2025f11)) 55 | 56 | ### ❤️ Contributors 57 | 58 | - Pooya Parsa ([@pi0](https://github.com/pi0)) 59 | 60 | ## v0.1.2 61 | 62 | [compare changes](https://github.com/pi0/shiki-image/compare/v0.1.1...v0.1.2) 63 | 64 | ### 🚀 Enhancements 65 | 66 | - Options for better font rendering ([2281bb7](https://github.com/pi0/shiki-image/commit/2281bb7)) 67 | - Export `loadFont` from `/core` ([94efe3e](https://github.com/pi0/shiki-image/commit/94efe3e)) 68 | 69 | ### 🏡 Chore 70 | 71 | - Apply automated updates ([80b6143](https://github.com/pi0/shiki-image/commit/80b6143)) 72 | 73 | ### ✅ Tests 74 | 75 | - Avoid updating snapshot in CI ([7cc2f9d](https://github.com/pi0/shiki-image/commit/7cc2f9d)) 76 | 77 | ### ❤️ Contributors 78 | 79 | - Pooya Parsa ([@pi0](https://github.com/pi0)) 80 | 81 | ## v0.1.1 82 | 83 | ### 🚀 Enhancements 84 | 85 | - Support format ([a5f5069](https://github.com/pi0/shiki-image/commit/a5f5069)) 86 | - Built-in font caching ([c9eb78f](https://github.com/pi0/shiki-image/commit/c9eb78f)) 87 | - `/core` subpath to allow customize shiki and takumi ([3bcf403](https://github.com/pi0/shiki-image/commit/3bcf403)) 88 | - Quality option ([89a19e0](https://github.com/pi0/shiki-image/commit/89a19e0)) 89 | - Default font ([9f2c881](https://github.com/pi0/shiki-image/commit/9f2c881)) 90 | 91 | ### 📖 Documentation 92 | 93 | - Add jsdocs for `CodeToImageOptions` ([d7d9e5f](https://github.com/pi0/shiki-image/commit/d7d9e5f)) 94 | 95 | ### 🏡 Chore 96 | 97 | - Update lockfile ([5b9e90d](https://github.com/pi0/shiki-image/commit/5b9e90d)) 98 | - Update pkg ([3a178a4](https://github.com/pi0/shiki-image/commit/3a178a4)) 99 | - Update ci ([be2d1e6](https://github.com/pi0/shiki-image/commit/be2d1e6)) 100 | - Apply automated updates ([6753cdc](https://github.com/pi0/shiki-image/commit/6753cdc)) 101 | - Apply automated updates ([894087a](https://github.com/pi0/shiki-image/commit/894087a)) 102 | - Update readme ([e33fc13](https://github.com/pi0/shiki-image/commit/e33fc13)) 103 | 104 | ### ❤️ Contributors 105 | 106 | - Pooya Parsa ([@pi0](https://github.com/pi0)) 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Pooya Parsa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shiki-image 2 | 3 | 4 | 5 | [![npm version](https://img.shields.io/npm/v/shiki-image?color=yellow)](https://npmjs.com/package/shiki-image) 6 | [![npm downloads](https://img.shields.io/npm/dm/shiki-image?color=yellow)](https://npm.chart.dev/shiki-image) 7 | 8 | 9 | 10 | Convert code snippets into images. Powered by [shiki](https://github.com/shikijs/shiki) and [takumi](https://github.com/kane50613/takumi). Super fast 🚀 11 | 12 | **Example:** 13 | 14 |

15 | Example output 16 |

17 | 18 | > [!NOTE] 19 | > This was a quick experimental project. Contributors needed! 20 | 21 | ## Usage 22 | 23 | ```js 24 | import { writeFile } from "node:fs/promises"; 25 | import { codeToImage } from "shiki-image"; 26 | 27 | const buffer = await codeToImage('console.log("hello, world!");', { 28 | lang: "js", 29 | theme: "github-dark", 30 | format: "webp", 31 | }); 32 | 33 | await writeFile("image.webp", buffer); 34 | ``` 35 | 36 | ## Options 37 | 38 | ### `lang` 39 | 40 | Code language. See [shiki supported languages](https://shiki.style/languages) 41 | 42 | ### `theme` 43 | 44 | Rendering theme. See [shiki supported theems](https://shiki.style/themes). 45 | 46 | ### `style` 47 | 48 | Additional container styles. See [takumi stylesheets](https://takumi.kane.tw/docs/deep-dives/stylesheets). 49 | 50 | ### `format` 51 | 52 | Output format can be either `png`, `webp`, `avif`, or `jpeg` (default is `webp`). 53 | 54 | ### `quality` 55 | 56 | Image quality between `0` to `100` (jpeg format only) 57 | 58 | ### `font` 59 | 60 | Font used to render the code. Can be either a string (remote URL to fetch) or an ArrayBuffer. 61 | 62 | > [!NOTE] 63 | > If no font is specified, it will use the builtin `Geist Mono` font from Takumi. 64 | 65 | > [!TIP] 66 | > If a URL is passed, response will be cached in memory for the next renders. 67 | 68 | ### `fontRatio` 69 | 70 | Font ratio used to compute the final font size. Default is `0.63`. 71 | 72 | ### `width` 73 | 74 | Rendering width. By default is computed as `columns * fontSize * fontRatio`. 75 | 76 | > [!NOTE] 77 | > Default font size is `18` and can be customized using `style.fontSize`. 78 | 79 | ### `height` 80 | 81 | Rendering height. By default is computed as `lines * fontSize * lineHeight`. 82 | 83 | > [!NOTE] 84 | > Default lineHeight is `1.3` and can be customized using `style.lineHeight`. 85 | 86 | ## Development 87 | 88 |
89 | 90 | local development 91 | 92 | - Clone this repository 93 | - Install latest LTS version of [Node.js](https://nodejs.org/en/) 94 | - Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable` 95 | - Install dependencies using `pnpm install` 96 | - Run interactive tests using `pnpm dev` 97 | 98 |
99 | 100 | ## Showcase 101 | 102 | - [Alekhya](https://github.com/jd-solanki/alekhya): Code image generator with API support 103 | - [Modern Monaco Demo](https://github.com/pi0/modern-monaco-demo): Uses shiki-image to generate [og image](https://modern-monaco-demo.vercel.app/og). 104 | 105 | ## License 106 | 107 | Published under the [MIT](https://github.com/unjs/shiki-image/blob/main/LICENSE) license. 108 | -------------------------------------------------------------------------------- /benchs/index.ts: -------------------------------------------------------------------------------- 1 | import { bench, do_not_optimize, run } from "mitata"; 2 | import { codeToImageCore, loadFont } from "../dist/core.mjs"; 3 | import { createHighlighter } from "shiki"; 4 | import { Renderer } from "@takumi-rs/core"; 5 | 6 | const exampleCode = /* js */ ` 7 | import { writeFile } from "node:fs/promises"; 8 | import { codeToImage } from "shiki-image"; 9 | 10 | const buffer = await codeToImage('console.log("hello, world!");', { 11 | lang: "js", 12 | theme: "github-dark", 13 | format: 'webp', 14 | style: { borderRadius: 4 }, 15 | }); 16 | 17 | await writeFile("image.webp", buffer); 18 | `; 19 | 20 | const highlighter = await createHighlighter({ 21 | themes: ["github-dark"], 22 | langs: ["js"], 23 | }); 24 | 25 | // preload the font 26 | const renderer = new Renderer({ 27 | fonts: [await loadFont(undefined)], 28 | }); 29 | 30 | bench("Takumi with default options", async () => { 31 | // to make the benchmark more fair, 32 | // we purge the font rasterization/shaping cache before every run 33 | renderer.purgeFontCache(); 34 | 35 | do_not_optimize( 36 | await codeToImageCore( 37 | exampleCode, 38 | { 39 | lang: "js", 40 | theme: "github-dark", 41 | format: "webp", 42 | style: { borderRadius: 4 }, 43 | }, 44 | { 45 | highlighter, 46 | renderer, 47 | }, 48 | ), 49 | ); 50 | }); 51 | 52 | await run(); 53 | -------------------------------------------------------------------------------- /build.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from "obuild/config"; 2 | 3 | export default defineBuildConfig({ 4 | entries: ["./src/index.ts", "./src/core.ts"], 5 | }); 6 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import unjs from "eslint-config-unjs"; 2 | 3 | export default unjs({ 4 | ignores: [ 5 | // ignore paths 6 | ], 7 | rules: { 8 | // rule overrides 9 | }, 10 | markdown: { 11 | rules: { 12 | // markdown rule overrides 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shiki-image", 3 | "version": "0.1.4", 4 | "description": "Convert code snippets into images. Powered by shiki and takumi. Super fast.", 5 | "repository": "pi0/shiki-image", 6 | "license": "MIT", 7 | "sideEffects": false, 8 | "type": "module", 9 | "exports": { 10 | ".": "./dist/index.mjs", 11 | "./core": "./dist/core.mjs" 12 | }, 13 | "types": "./dist/index.d.mts", 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "build": "obuild", 19 | "dev": "vitest dev", 20 | "lint": "eslint . && prettier -c .", 21 | "lint:fix": "automd && eslint . --fix && prettier -w .", 22 | "bench": "node --expose-gc benchs/index.ts", 23 | "prepack": "pnpm build", 24 | "release": "pnpm test && changelogen --release && npm publish && git push --follow-tags", 25 | "test": "pnpm lint && pnpm test:types && vitest run --coverage", 26 | "test:types": "tsc --noEmit --skipLibCheck" 27 | }, 28 | "dependencies": { 29 | "@takumi-rs/core": "^0.29.5", 30 | "@takumi-rs/helpers": "^0.29.5", 31 | "shiki": "^3.12.2" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "^24.3.1", 35 | "@vitest/coverage-v8": "^3.2.4", 36 | "automd": "^0.4.0", 37 | "changelogen": "^0.6.2", 38 | "eslint": "^9.35.0", 39 | "eslint-config-unjs": "^0.5.0", 40 | "mitata": "^1.0.34", 41 | "obuild": "^0.2.1", 42 | "prettier": "^3.6.2", 43 | "typescript": "^5.9.2", 44 | "vitest": "^3.2.4" 45 | }, 46 | "packageManager": "pnpm@10.14.0" 47 | } 48 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>unjs/renovate-config"] 3 | } 4 | -------------------------------------------------------------------------------- /src/_utils.ts: -------------------------------------------------------------------------------- 1 | import type { ContainerNode } from "@takumi-rs/helpers"; 2 | import { container, text, em, percentage } from "@takumi-rs/helpers"; 3 | import type { CodeToImageCoreOptions, CodeToImageOptions } from "./types"; 4 | 5 | const DEFAULT_FONT_SIZE = 18; 6 | const DEFAULT_FONT_RATIO = 0.63; 7 | const DEFAULT_LINE_HEIGHT = 1.3; 8 | 9 | export function codeToContainer( 10 | code: string, 11 | opts: CodeToImageOptions, 12 | coreOpts: CodeToImageCoreOptions, 13 | ): ContainerNode { 14 | const { tokens, fg, bg } = coreOpts.highlighter.codeToTokens(code, { 15 | theme: opts.theme, 16 | lang: opts.lang, 17 | }); 18 | 19 | const root = container({ 20 | style: { 21 | color: fg, 22 | backgroundColor: bg, 23 | display: "flex", 24 | flexDirection: "column", 25 | width: percentage(100), 26 | height: percentage(100), 27 | padding: em(1), 28 | fontSize: DEFAULT_FONT_SIZE, 29 | lineHeight: DEFAULT_LINE_HEIGHT, 30 | fontFamily: "monospace", 31 | ...opts.style, 32 | }, 33 | children: tokens.map((line) => 34 | container({ 35 | style: { 36 | display: "flex", 37 | minHeight: em(1), 38 | }, 39 | children: line.map((token) => 40 | token.content.trim() === "" 41 | ? container({ 42 | style: { 43 | minWidth: em(0.5 * token.content.length), 44 | minHeight: em(1), 45 | // backgroundColor: "gray", 46 | }, 47 | }) 48 | : // TODO: fontStyle 49 | text(token.content, { color: token.color }), 50 | ), 51 | }), 52 | ), 53 | }); 54 | 55 | return root; 56 | } 57 | 58 | export async function loadFont(font: string | ArrayBuffer | Buffer) { 59 | let fontData: Buffer | ArrayBuffer; 60 | 61 | if (typeof font === "string") { 62 | const fontCache: Map = (( 63 | globalThis as any 64 | ).__shikiImageFontCache__ ||= new Map()); 65 | 66 | const cachedFont = fontCache.get(font); 67 | if (cachedFont) { 68 | fontData = cachedFont; 69 | } else { 70 | const res = await fetch(font); 71 | if (!res.ok) { 72 | throw new Error( 73 | `Font fetch failed (${res.status} ${res.statusText}): ${font}`, 74 | ); 75 | } 76 | fontData = await res.arrayBuffer(); 77 | fontCache.set(font, fontData); 78 | } 79 | return fontData; 80 | } 81 | 82 | if (font instanceof ArrayBuffer) { 83 | fontData = Buffer.from(font); 84 | return fontData; 85 | } 86 | 87 | if (font instanceof Buffer) { 88 | fontData = font; 89 | return fontData; 90 | } 91 | 92 | throw new Error( 93 | "Invalid font type. Expected a string, ArrayBuffer, or Buffer", 94 | ); 95 | } 96 | 97 | export function renderOptions(code: string, opts: CodeToImageOptions) { 98 | const lines = code.split("\n").length; 99 | const columns = Math.max(...code.split("\n").map((l) => l.length)); 100 | 101 | const fontRatio = opts?.fontRatio || DEFAULT_FONT_RATIO; 102 | const fontSize = 103 | Number.parseInt(opts.style?.fontSize as string) || DEFAULT_FONT_SIZE; 104 | const lineHeight = 105 | Number.parseInt(opts.style?.lineHeight as string) || DEFAULT_LINE_HEIGHT; 106 | 107 | const width = opts.width || (columns + 2) * (fontSize * fontRatio); 108 | const height = opts.height || (lines + 2) * (fontSize * lineHeight); 109 | 110 | const format = opts.format || ("webp" as const); 111 | 112 | return { width, height, format }; 113 | } 114 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | import { codeToContainer, renderOptions } from "./_utils"; 2 | import type { CodeToImageCoreOptions, CodeToImageOptions } from "./types"; 3 | 4 | export { loadFont } from "./_utils"; 5 | 6 | export type { CodeToImageOptions } from "./types"; 7 | 8 | export async function codeToImageCore( 9 | code: string, 10 | opts: CodeToImageOptions, 11 | coreOpts: CodeToImageCoreOptions, 12 | ) { 13 | const container = codeToContainer(code, opts, coreOpts); 14 | const { width, height, format } = renderOptions(code, opts); 15 | return await coreOpts.renderer.renderAsync(container, { 16 | width, 17 | height, 18 | format, 19 | quality: opts.quality, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { CodeToImageOptions } from "./types"; 2 | 3 | import { createHighlighter } from "shiki"; 4 | import { Renderer } from "@takumi-rs/core"; 5 | import { codeToImageCore } from "./core"; 6 | import { loadFont } from "./_utils"; 7 | 8 | export type { CodeToImageOptions } from "./types"; 9 | 10 | export async function codeToImage(code: string, options: CodeToImageOptions) { 11 | const highlighter = await createHighlighter({ 12 | themes: [options.theme], 13 | langs: [options.lang], 14 | }); 15 | const fonts = options.font ? [await loadFont(options.font)] : undefined; 16 | const renderer = new Renderer({ fonts }); 17 | return codeToImageCore(code, options, { highlighter, renderer }); 18 | } 19 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { BundledLanguage, BundledTheme, HighlighterCore } from "shiki"; 2 | import type { PartialStyle } from "@takumi-rs/helpers"; 3 | import type { Renderer as NativeRenderer } from "@takumi-rs/core"; 4 | import type { OutputFormat } from "@takumi-rs/core"; 5 | 6 | /** 7 | * Options for rendering code to image. 8 | */ 9 | export interface CodeToImageOptions { 10 | /** 11 | * Code language. See shiki supported languages. 12 | */ 13 | lang: BundledLanguage; 14 | 15 | /** 16 | * Rendering theme. See shiki supported themes. 17 | */ 18 | theme: BundledTheme; 19 | 20 | /** 21 | * Font used to render the code. Can be a remote URL (string) or an ArrayBuffer. 22 | * 23 | * If a URL is passed, it will be cached in memory for next renders. 24 | * 25 | * If no font is specified, it will use the builtin `Geist Mono` font from Takumi. 26 | */ 27 | font?: string | ArrayBuffer; 28 | 29 | /** 30 | * Font ratio used to compute the final font size. Default is `0.63`. 31 | */ 32 | fontRatio?: number; 33 | 34 | /** 35 | * Rendering width. By default is computed as `columns * fontSize * fontRatio`. 36 | * 37 | * Default font size is `18` and can be customized using `style.fontSize`. 38 | */ 39 | width?: number; 40 | 41 | /** 42 | * Rendering height. By default is computed as `lines * fontSize * lineHeight`. 43 | * 44 | * Default `lineHeight` is `1.3` and can be customized using `style.lineHeight`. 45 | */ 46 | height?: number; 47 | 48 | /** 49 | * Additional container styles. See takumi stylesheets. 50 | */ 51 | style?: PartialStyle; 52 | 53 | /** 54 | * Output format: `png`, `webp`, `avif`, or `jpeg`. Default is `webp`. 55 | */ 56 | format?: OutputFormat; 57 | 58 | /** 59 | * Image quality between 0 and 100 (jpeg format only). 60 | */ 61 | quality?: number; 62 | } 63 | 64 | export interface CodeToImageCoreOptions { 65 | highlighter: HighlighterCore; 66 | renderer: NativeRenderer; 67 | } 68 | -------------------------------------------------------------------------------- /test/.snapshot/image.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pi0/shiki-image/7ec449c53c20ce5d3260b76cb4bf2cb32e3938e3/test/.snapshot/image.webp -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { codeToImage } from "../src"; 3 | import { writeFile } from "node:fs/promises"; 4 | import { fileURLToPath } from "node:url"; 5 | 6 | const exampleCode = /* js */ ` 7 | import { writeFile } from "node:fs/promises"; 8 | import { codeToImage } from "shiki-image"; 9 | 10 | const buffer = await codeToImage('console.log("hello, world!");', { 11 | lang: "js", 12 | theme: "github-dark", 13 | format: 'webp', 14 | style: { borderRadius: 4 }, 15 | }); 16 | 17 | await writeFile("image.webp", buffer); 18 | `; 19 | 20 | describe("shiki-image", () => { 21 | it("convert code to image", async () => { 22 | const start = Date.now(); 23 | const img = await codeToImage(exampleCode, { 24 | lang: "js", 25 | theme: "github-dark", 26 | style: { borderRadius: 4 }, 27 | }); 28 | await writeFile( 29 | fileURLToPath(new URL(".snapshot/image.webp", import.meta.url)), 30 | img, 31 | ); 32 | const end = Date.now(); 33 | console.log(`Image generated in ${end - start}ms`); 34 | }); 35 | 36 | it("failed when url returned 404", async () => { 37 | await expect( 38 | codeToImage(exampleCode, { 39 | lang: "js", 40 | theme: "github-dark", 41 | font: "https://www.google.com/404", 42 | }), 43 | ).rejects.toThrowError( 44 | "Font fetch failed (404 Not Found): https://www.google.com/404", 45 | ); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "Preserve", 5 | "moduleResolution": "bundler", 6 | "moduleDetection": "force", 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "allowJs": true, 10 | "resolveJsonModule": true, 11 | "strict": true, 12 | "isolatedModules": true, 13 | "verbatimModuleSyntax": true, 14 | "noUncheckedIndexedAccess": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "allowImportingTsExtensions": true, 17 | "noImplicitOverride": true, 18 | "noEmit": true 19 | } 20 | } 21 | --------------------------------------------------------------------------------