A tool that can be used to convert Markdown or HTML format text to an image.
First, the script calls marked to parse Markdown into a HTML document. Next, use Puppeteer to start a headless browser and render the document with HTML and CSS templates. Finally, export our image through Puppeteer's screenshot API.
Rendering results:
This tool requires a LTS Node version (v20.0.0+).
CLI:
npm install -g mdimgIn Node.js project:
npm install mdimgExample:
mdimg -i path/to/input.md -o path/to/output.png -w 600 --css githubmdimg will read text from path/to/input.md and convert it to an image file path/to/output.png.
When using the command, you must specify either -i (input file, recommended) or -t (directly input text).
When using -t to input Markdown text directly, escape characters will not be available. To fix this, for example, you should replace \n with <br>.
You can always call mdimg -h to get complete help.
Import mdimg to your project:
import { mdimg } from "mdimg";Convert markdown file to an image:
const convertRes = await mdimg({
inputFilename: "path/to/input.md",
outputFilename: "path/to/output.png",
width: 600,
cssTemplate: "github",
theme: "light",
// or with dark theme
// cssTemplate: "githubDark",
// theme: "dark",
});
console.log(
`Convert to image successfully!\nImage has been saved as \`${convertRes.path}\``,
);Convert markdown text to blob:
const convertRes = await mdimg({
inputText: "# Hello world",
encoding: "blob",
});
// import { writeFileSync } from "fs";
// writeFileSync("path/to/output.png", convertRes.data);When using mdimg() method, you must specify either inputFilename (input file) or inputText (directly input text).
Here are all available options:
| Argument | Type | Default | Notes |
|---|---|---|---|
| inputText | String |
undefined |
Input Markdown or HTML text directly. This option has no effect if inputFilename is specified |
| inputFilename | String |
undefined |
Read Markdown or HTML text from a file |
| outputFilename | String |
./mdimg_output/mdimg_${new Date()}.${type} |
Output binary image filename. Available file extensions: jpeg, png, webp. Available when encoding option is binary |
| type | "jpeg" | "png" | "webp" |
png |
File type of the image. Type will be inferred from outputFilename if specified |
| width | Number |
800 |
Width in pixel of output image |
| height | Number |
100 |
Min-height in pixel of output image. No less than 100 |
| encoding | "base64" | "binary" | "blob" |
binary |
Encode type of output image |
| quality | Number |
100 |
Quality of the image, between 0-100. Not applicable to png image |
| htmlText | String |
undefined |
HTML rendering text |
| cssText | String |
undefined |
CSS rendering text |
| htmlTemplate | String |
default |
HTML rendering template. Available presets can be found in template/html. If ends with .html, the mdimg will try to read local file. This option has no effect if htmlText is specified |
| cssTemplate | String |
default |
CSS rendering template. Available presets can be found in template/css. If ends with .css, the mdimg will try to read local file. This option has no effect if cssText is specified |
| theme | light | dark |
light |
Rendering color theme, will impact styles of code block and so on |
| extensions | Boolean | IExtensionOptions |
true |
Configurations for extensions |
| plugins | IPlugin[] |
[] |
List of plugins to apply during the conversion pipeline |
| log | Boolean |
false |
Print execution logs via stderr |
| debug | Boolean |
false |
Whether to keep temporary HTML file after rendering |
| puppeteerProps | LaunchOptions |
undefined |
Launch options of Puppeteer |
Returns: Promise<object>
| Key | Value Type | Notes |
|---|---|---|
| data | string | Uint8Array |
BASE64 encoded string (encoding is base64) or Uint8Array blob (encoding is binary or blob) of the output image |
| path | string |
Path of output image. Available when encoding is binary |
| html | string |
Rendered HTML document |
😍 Contribute to template presets via pull requests is welcomed!
Template presets are stored in the template directory.
If you execute the following command:
mdimg --html custom --css customOr in Node.js project:
await mdimg({
htmlTemplate: "custom",
cssTemplate: "custom",
});The mdimg will read template/html/custom.html as HTML template and template/css/custom.css as CSS template in the mdimg directory to render the image.
Create a new .html file in template/html directory.
There is only one rule you need to follow: an element with id mdimg-body wrapping an element with class markdown-body.
The simplest example:
<div id="mdimg-body">
<div class="markdown-body"></div>
</div>The mdimg will put the parsed HTML content in the element which class="markdown-body" (elements inside will be replaced), and finally generate the image for the whole element which id="mdimg-body".
Nothing to note, create a new .css file in template/css directory and then make your style!
For further development, it is recommended that write .scss or .sass files in the template/scss directory, and use the following command to generate CSS templates:
# Build .scss and .sass files
pnpm run rollup:sassCSS templates with the corresponding name will be generated in template/css directory.
Template presets may not often meet your needs. If you already know the specifications of HTML template and CSS template, you can pass the template directly. There are two methods:
- Using local template file. Pass a local filepath with the file extension
.htmland.cssthrough options--htmland--csswith CLI (htmlTemplateandcssTemplatewith Node.js). - Using template text. Pass template text through
--htmlTextand--cssTextwith CLI (htmlTextandcssTextwith Node.js).
CLI:
# use local file
mdimg --html path/to/custom.html --css path/to/custom.css
# use text directly
mdimg --htmlText '<div id="mdimg-body"><div class="markdown-body"></div></div>' --cssText '@import "https://unpkg.com/normalize.css/normalize.css"; .markdown-body { padding: 6rem 4rem; }'Or in Node.js project:
// use local file
await mdimg({
htmlTemplate: "path/to/custom.html",
cssTemplate: "path/to/custom.css",
});
// use text directly
await mdimg({
htmlText: `<div id="mdimg-body">
<div class="markdown-body"></div>
</div>`,
cssText: `@import "https://unpkg.com/normalize.css/normalize.css";
.markdown-body {
padding: 6rem 4rem;
}`,
});Extensions are default enabled. You can easily configuration them in Node.js:
await mdimg({
extensions: false, // disable all extensions
});
await mdimg({
extensions: {
highlightJs: false, // disable highlight.js
mathJax: {
// further configuration for MathJax
// ...
},
mermaid: true, // enable mermaid (by default)
},
});In CLI, you can only enable or disable extensions globally:
mdimg --extensions false # disable all extensionsSome extended syntaxes, such as LaTeX, can't be parsed by pure marked correctly. To solve this problem, the mdimg introduces some third-party libraries to enhance rendering capabilities. Below are introduced libraries:
MathJax is an open-source JavaScript display engine for LaTeX, MathML, and AsciiMath notation.
$ is not enabled by default to render inline LaTeX. Because It is used too frequently in normal text, so if you want to use it for math delimiters, you must specify it explicitly. In Node.js project:
await mdimg({
extensions: {
mathJax: {
tex: {
inlineMath: [
["$", "$"],
["\\(", "\\)"],
],
},
},
},
});CLI doesn't support to configuration extensions, so you need to override MathJax options in HTML template directly:
<!-- path/to/template.html -->
<div id="mdimg-body">
<div class="markdown-body"></div>
</div>
<script>
MathJax = {
tex: {
inlineMath: [
["$", "$"],
["\\(", "\\)"],
],
},
};
</script>mdimg --html path/to/template.html<div> block. Example:
<div>$$
A_{m,n} =
\begin{pmatrix}
a_{1,1} & a_{1,2} & \cdots & a_{1,n} \\
a_{2,1} & a_{2,2} & \cdots & a_{2,n} \\
\vdots & \vdots & \ddots & \vdots \\
a_{m,1} & a_{m,2} & \cdots & a_{m,n}
\end{pmatrix}
$$</div>Mermaid is a JavaScript-based diagramming and charting tool that uses Markdown-inspired text definitions and a renderer to create and modify complex diagrams.
Sequence diagram example:
```mermaid
sequenceDiagram
Alice->>Bob: Hello Bob, how are you ?
Bob->>Alice: Fine, thank you. And you?
create participant Carl
Alice->>Carl: Hi Carl!
create actor D as Donald
Carl->>D: Hi!
destroy Carl
Alice-xCarl: We are too many
destroy Bob
Bob->>Alice: I agree
```Highlight.js is a syntax highlighter.
The plugin system lets you hook into every stage of the conversion pipeline and register custom extensions — all without modifying mdimg's source code.
A plugin can define any combination of the four lifecycle hooks:
| Hook | When it runs | Signature |
|---|---|---|
beforeParse |
Before Markdown is parsed | (text: string) => string | Promise<string> |
afterParse |
After Markdown → HTML fragment, before template splicing | (html: string) => string | Promise<string> |
afterSplice |
After the full HTML document is assembled | (html: string) => string | Promise<string> |
afterRender |
After Puppeteer renders, before the image is written to disk | (result: IConvertResponse) => IConvertResponse | Promise<IConvertResponse> |
Multiple plugins run in registration order.
import { mdimg } from "mdimg";
import type { IPlugin } from "mdimg";
const myPlugin: IPlugin = {
name: "myPlugin",
hooks: {
// Transform raw Markdown before parsing
beforeParse: (text) => text.replace(/\[\[(\w+)\]\]/g, "**$1**"),
// Modify the rendered HTML fragment
afterParse: (html) => `<div class="wrapper">${html}</div>`,
// Post-process the final HTML document
afterSplice: (html) => html.replace("</title>", " — my brand</title>"),
// Transform or inspect the output image bytes before they are saved
afterRender: async (result) => {
console.log(`Image size: ${result.data.length} bytes`);
return result;
},
},
};
await mdimg({
inputText: "# Hello [[World]]",
outputFilename: "output.png",
plugins: [myPlugin],
});A plugin can also contribute one or more extensions — each extension injects HTML fragments into the <head> or <body> of the rendered page.
import type { IPlugin, IExtension } from "mdimg";
const fontExtension: IExtension = {
name: "googleFont",
inject({ theme }) {
const family = theme === "dark" ? "JetBrains+Mono" : "Inter";
return {
head: `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=${family}&display=swap">`,
};
},
};
const fontPlugin: IPlugin = {
name: "fontPlugin",
extensions: [fontExtension],
};
await mdimg({
inputText: "# Hello",
encoding: "base64",
plugins: [fontPlugin],
});If a plugin extension has the same name as a built-in extension (highlightJs, mathJax, or mermaid), it replaces the built-in. This lets you fully control how a dependency is configured or injected.
import type { IPlugin } from "mdimg";
// Replace built-in highlight.js with a custom CDN version
const customHljsPlugin: IPlugin = {
name: "customHighlightJs",
extensions: [
{
name: "highlightJs", // same name → replaces the built-in
inject({ theme }) {
const cssHref =
theme === "dark"
? "https://cdn.example.com/hljs/atom-one-dark.min.css"
: "https://cdn.example.com/hljs/atom-one-light.min.css";
return {
head: `<link rel="stylesheet" href="${cssHref}">`,
body: `<script src="https://cdn.example.com/hljs/highlight.min.js"></script>
<script>hljs.highlightAll();</script>`,
};
},
},
],
};
await mdimg({
inputText: "# Hello",
encoding: "base64",
plugins: [customHljsPlugin],
});Any extension — built-in or plugin-contributed — can be suppressed via the extensions option using its name as the key:
await mdimg({
inputText: "# Hello",
encoding: "base64",
// Disable mermaid and a plugin-contributed extension named "googleFont"
extensions: { mermaid: false, googleFont: false },
plugins: [fontPlugin],
});extensions: false still disables every extension globally.
git clone https://github.com/LolipopJ/mdimg.git
cd mdimg
pnpm install
npx puppeteer browsers install chrome# Check lint rules
pnpm run lint
# Check lint rules and fix resolvable errors
pnpm run lint:fix# Build .js, .scss and .sass files
pnpm run build# Build productions before testing
pnpm run build
# Run test cases
pnpm run test- md2img. Provided me the idea and a complete feasible solution.














