Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

You can turn HTML into a PNG or JPEG by running a headless browser behind a small HTTP endpoint: accept HTML in a POST request, load it in Chromium, capture the page with Playwright or Puppeteer, then return the image bytes with the matching content type. A GitHub repository can provide the API wrapper’s source code, but the example pattern is code you run and operate yourself—not a screenshot endpoint hosted by GitHub.

This guide builds a minimal Node.js and Playwright service, shows how to call it from cURL, Python, and Node.js, and explains the settings, security boundaries, and operating limits that matter before exposing it to other users.

What a GitHub HTML-to-image API does

In this context, “GitHub API” means an open-source project on GitHub that implements an HTTP API; it does not mean GitHub offers a general-purpose endpoint that accepts arbitrary HTML and returns screenshots. The repository example described here uses a POST /api/screenshot route with an html field, plus optional width and height, and responds with PNG bytes. To use that pattern, deploy the service yourself or adapt its code.

The rendering work is done by a browser engine. Playwright and Puppeteer expose screenshot methods for a page, and their APIs can return bytes in memory rather than requiring a file. That makes the browser output straightforward to send as an HTTP response, store, or pass to another service. The browser—not an HTML-to-image shortcut—lays out the document and paints it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall
Digital Image Processing, 4Th Edition
  • Brand: Pearson India Education Services Pvt. Ltd.
  • Language: english

Build a minimal Node.js screenshot endpoint

The following example uses Express and Playwright. It accepts JSON containing HTML, optional viewport dimensions, an image type, and a full-page flag. It returns a PNG or JPEG response body. The sample is a starting point for a trusted or controlled workload, not a public service ready to accept arbitrary user input.

1. Install the runtime and browser

Use a supported Node.js release, then create a project and install the dependencies. Playwright’s browser installation is separate from installing its package:

mkdir html-shot-api
cd html-shot-api
npm init -y
npm install express playwright
npx playwright install chromium

2. Create the endpoint

Save this as server.mjs. The route caps the JSON body and viewport dimensions, rejects invalid input, blocks page-initiated network requests, uses a navigation timeout, and closes each browser context after capture. Those controls reduce common risks but do not replace container isolation, operating-system limits, monitoring, or a security review.

import express from 'express';
import { chromium } from 'playwright';

const app = express();
app.use(express.json({ limit: '1mb' }));

const browser = await chromium.launch();
const maxDimension = 2400;

app.post('/api/screenshot', async (req, res) => {
  const { html, width = 1280, height = 800, type = 'png', fullPage = false } = req.body ?? {};

  if (typeof html !== 'string' || html.length === 0) {
    return res.status(400).json({ error: 'html must be a non-empty string' });
  }
  if (!Number.isInteger(width) || !Number.isInteger(height) ||
      width < 1 || height < 1 || width > maxDimension || height > maxDimension) {
    return res.status(400).json({ error: `width and height must be integers from 1 to ${maxDimension}` });
  }
  if (!['png', 'jpeg'].includes(type)) {
    return res.status(400).json({ error: 'type must be png or jpeg' });
  }
  if (typeof fullPage !== 'boolean') {
    return res.status(400).json({ error: 'fullPage must be a boolean' });
  }

  let context;
  try {
    context = await browser.newContext({
      viewport: { width, height },
      serviceWorkers: 'block'
    });
    const page = await context.newPage();
    page.setDefaultTimeout(15000);
    await page.route('**/*', route => {
      const url = route.request().url();
      if (url.startsWith('data:') || url.startsWith('about:')) return route.continue();
      return route.abort();
    });

    await page.setContent(html, { waitUntil: 'domcontentloaded', timeout: 15000 });
    await page.evaluate(() => document.fonts.ready);
    const image = await page.screenshot({ type, fullPage });
    res.status(200).type(type === 'jpeg' ? 'image/jpeg' : 'image/png').send(image);
  } catch (error) {
    console.error('Screenshot failed:', error);
    if (!res.headersSent) res.status(500).json({ error: 'Could not render the supplied HTML' });
  } finally {
    if (context) await context.close().catch(() => {});
  }
});

const server = app.listen(3000, () => {
  console.log('HTML screenshot API listening on http://localhost:3000');
});

async function shutdown() {
  server.close();
  await browser.close();
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);

Start it with node server.mjs. The browser is launched once and reused, while each request gets its own context; this avoids launching a fresh Chromium process for every image and separates the page state between requests. A production deployment should also impose a request concurrency limit and shut down gracefully when work is still in flight.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

3. Send HTML and save the response

Send JSON to the local endpoint. For example, cURL writes the returned image bytes to shot.png:

curl -X POST http://localhost:3000/api/screenshot 
  -H 'Content-Type: application/json' 
  -d '{"html":"<!doctype html><html><body><h1>Hello, image</h1></body></html>","width":1200,"height":800,"type":"png"}' 
  --output shot.png

Python can submit the same payload and save the binary response:

import requests

payload = {
    "html": "<!doctype html><html><body><h1>Hello, image</h1></body></html>",
    "width": 1200,
    "height": 800,
    "type": "png",
}
r = requests.post("http://localhost:3000/api/screenshot", json=payload, timeout=30)
r.raise_for_status()
with open("shot.png", "wb") as image:
    image.write(r.content)

Here is the equivalent client call with Node’s built-in fetch:

const payload = {
  html: '<!doctype html><html><body><h1>Hello, image</h1></body></html>',
  width: 1200,
  height: 800,
  type: 'png'
};
const res = await fetch('http://localhost:3000/api/screenshot', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(payload)
});
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.png', image));

The response is binary, not JSON: use the response body as bytes and retain the server’s MIME type when returning, storing, or forwarding it. If your client requires JSON, base64-encode the byte array and return an explicit JSON field such as imageBase64; base64 increases payload size and requires the client to decode it.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose capture settings deliberately

Viewport or full page

width and height define the browser viewport, which affects responsive CSS, line breaks, and layout. A normal screenshot captures the visible viewport. Set fullPage: true to include the complete scrollable document; this is useful for long receipts or reports, but the resulting image can be much taller and more expensive in memory to encode and transmit. Keep viewport dimensions and total document height within limits appropriate to your workload.

Whole page or one element

For a card, chart, or other component, capture a locator or element instead of the entire page. Playwright supports locator screenshots, so an endpoint can accept a selector and call the locator screenshot method after verifying that the element exists and is visible. Treat a selector as input: validate its length and handle “not found” and timeout cases as client errors rather than returning an unexplained server failure.

PNG, JPEG, and quality

PNG is lossless and a sensible default for text, UI elements, and sharp edges. JPEG is lossy and can be smaller for photographic content. Playwright’s screenshot API includes image-format and quality controls; quality is relevant to JPEG, while Puppeteer’s documented screenshot options state that PNG ignores the quality parameter. The sample limits formats to PNG and JPEG rather than assuming every browser library supports the same set.

Clipping, transparency, and in-memory output

A clip rectangle can capture a specific region by coordinates when the target is not naturally a single element. Both clip coordinates and element bounds need validation if clients supply them. Puppeteer’s screenshot options include clip, omitBackground, path, encoding, quality, and type; Playwright can return a buffer for post-processing or forwarding. Keep bytes in memory for an API response, and use a file path only when you need a persisted artifact or a later file-based workflow.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Wait for the content that matters

domcontentloaded means the initial HTML has been parsed; it does not guarantee that application data, animations, or every image is ready. In the example, fonts are awaited explicitly. If your page needs more, add an application-specific readiness condition, such as waiting for a known selector or a JavaScript flag. Avoid blindly waiting for all network activity to stop: long polling and analytics can prevent a page from reaching an idle state. For an endpoint that accepts only raw HTML, prefer deterministic markup and inline or controlled assets so the capture is repeatable.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Security and production boundaries

Rendering untrusted HTML is not safe simply because it runs in a headless browser. HTML can attempt to load remote resources, consume CPU or memory, create very large documents, or exploit browser behavior. The sample blocks non-data network requests, but that can also prevent external images, stylesheets, and fonts from appearing. If external resources are a requirement, use strict allowlists and network-level egress controls; do not let user HTML reach internal services or cloud metadata endpoints.

  • Run browser workers in isolated containers or equivalent sandboxes, with an unprivileged user and resource limits.
  • Set maximum request-body size, viewport width and height, render timeout, output size, and concurrency.
  • Limit or disable navigation, downloads, popups, and external requests according to the product’s needs.
  • Do not expose internal credentials, authenticated browser profiles, or sensitive filesystem mounts to the rendering process.
  • Use authentication and rate limits on the HTTP endpoint, and avoid logging submitted HTML if it may contain private content.
  • Return controlled error messages to clients while recording detailed diagnostics server-side.

One browser process per request is simpler to reason about but adds startup overhead and uses more resources. Reusing a browser with a fresh context, as above, is more efficient for repeated captures, but requires process supervision and safeguards against accumulated load. Measure the service under your own HTML, output dimensions, and concurrency; the cited browser documentation does not establish a general speed or fidelity winner between Playwright and Puppeteer.

Troubleshooting common failures

  • HTTP 400, “html must be a non-empty string”: Send a JSON object with an html string, and set Content-Type: application/json. Check that your client is not sending form data or an already-stringified JSON value in the wrong shape.
  • HTTP 400 for dimensions or type: Dimensions must be positive integers no greater than the sample’s 2400-pixel limit; the sample accepts only png or jpeg. Change the request or deliberately revise the server’s validation and operational caps.
  • External images, styles, or fonts are missing: The sample aborts network requests other than data and about URLs. Inline needed assets or replace the blanket block with a narrowly defined allowlist and network egress protection.
  • Text or charts are incomplete: The page may need application data or a specific element to finish rendering. Add a selector or app-level readiness check before capture; do not assume that parsing the HTML means all dynamic work is done.
  • Capture times out or the process runs out of memory: Reduce document complexity and dimensions, avoid unbounded full-page captures, set stricter timeouts and concurrency, and inspect worker resource usage. A full-page image may be much larger than the initial viewport.
  • Client cannot open the returned content: Save the response body as bytes rather than decoding it as text, and preserve the image/png or image/jpeg content type. If you return base64 inside JSON, decode it on the client before saving.

Or skip the browser setup

If the page is already available at a URL and you would rather not run Chromium workers, ScreenshotNeo offers a one-request screenshot API. Its URL-based endpoint captures a webpage; it is not a drop-in way to submit arbitrary HTML strings to the local /api/screenshot route above. For the API parameters and options, see the ScreenshotNeo documentation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo can accept cookie or consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents and MCP clients. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo free to get 1,000 screenshots a month with no card.

Frequently Asked Questions

Does GitHub itself host the screenshot endpoint?

No. The GitHub reference is an example of source code for an API wrapper; you run the browser-rendering service on infrastructure you control.

Can the same service return a PDF?

Yes, a browser automation service can expose a separate PDF route using the browser’s PDF capability, but the sample endpoint here is intentionally limited to PNG and JPEG responses.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Quick Recap

Bestseller No. 1
Digital Image Processing, 4Th Edition
Digital Image Processing, 4Th Edition
Brand: Pearson India Education Services Pvt. Ltd.; Language: english
$38.50
SaleBestseller No. 2
SaleBestseller No. 4

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.