ScreenshotNeo

BlogHow-to

How to Convert HTML Code to an Image Without Watermarks

Render HTML in a browser and capture clean PNG, JPEG, or WebP pixels without a converter watermark, with complete Playwright examples.

By the ScreenshotNeo team1 October 20267 min read

Short answer: render the HTML in a real browser, then capture the page or element you need. Playwright and Puppeteer save the rendered pixels directly as PNG, JPEG, or WebP, so the conversion step does not add a watermark.

This preserves CSS layout, fonts, images, JavaScript, and responsive breakpoints. You can capture the visible viewport, the complete scrollable page, or one CSS-selected component.

1. Choose the capture scope

Goal Capture Typical use
Fixed canvas Viewport screenshot Social card, hero, or thumbnail
Every section Full-page screenshot Documentation, invoices, long designs
One component Element screenshot Card, chart, table, or logo

Set the CSS viewport before rendering. A device scale factor of 2 produces a sharper, larger raster for the same CSS dimensions, but increases memory use and file size. For transparency, use PNG or WebP and omit the browser background; JPEG cannot preserve alpha.

2. Convert HTML to an image with Playwright

Playwright documents page.setContent(), page and element screenshots, full-page capture, PNG/JPEG/WebP output, buffers, and omitBackground in its Page API and screenshot guide.

Install Playwright

npm init -y
npm install playwright
npx playwright install chromium

Complete Node.js example

const { chromium } = require('playwright');

(async () => {
  const html = `<!doctype html>
<html>
  <head>
    <meta charset='utf-8'>
    <style>
      * { box-sizing: border-box; }
      body { margin: 0; font-family: system-ui, sans-serif; background: #eef2ff; }
      .canvas { width: 1200px; margin: 0 auto; padding: 64px; }
      .card { padding: 48px; border-radius: 24px; background: white; color: #111827;
              box-shadow: 0 20px 60px rgb(31 41 55 / 18%); }
      h1 { margin: 0 0 16px; font-size: 56px; }
      p { margin: 0; font-size: 22px; line-height: 1.5; }
    </style>
  </head>
  <body>
    <main class='canvas'>
      <article class='card' id='card'>
        <h1>HTML to image</h1>
        <p>A browser renders the layout; Playwright captures the pixels.</p>
      </article>
    </main>
  </body>
</html>`;

  const browser = await chromium.launch();
  const page = await browser.newPage({
    viewport: { width: 1200, height: 800 },
    deviceScaleFactor: 2
  });

  await page.setContent(html, { waitUntil: 'load' });
  await page.evaluate(() => document.fonts.ready);
  await page.waitForLoadState('networkidle');

  await page.screenshot({ path: 'viewport.webp', type: 'webp', quality: 90 });
  await page.screenshot({ path: 'full-page.png', fullPage: true });
  await page.locator('#card').screenshot({
    path: 'card.png', type: 'png', omitBackground: true
  });

  await browser.close();
})();

The first capture is clipped to the viewport. fullPage: true expands to the document’s scrollable height. The locator screenshot isolates the element’s bounding box.

3. Capture an existing file or URL

Local HTML file

const { chromium } = require('playwright');
const path = require('node:path');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
  await page.goto(`file://${path.resolve('design.html')}`, { waitUntil: 'networkidle' });
  await page.screenshot({ path: 'design.png', fullPage: true });
  await browser.close();
})();

Hosted page

await page.goto('https://example.com/page', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'page.webp', type: 'webp', quality: 85 });

Pages that poll, stream, or keep a socket open may never reach network idle. Prefer an application-specific readiness selector or a bounded delay in that case.

4. Wait for fonts, images, and dynamic content

Markup can exist before its visual assets are ready. If you control the page, expose a readiness marker:

await page.setContent(html, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-render-ready=true]');
await page.evaluate(() => document.fonts.ready);
await page.waitForFunction(() => [...document.images].every(img => img.complete));
await page.screenshot({ path: 'ready.png' });

Set data-render-ready='true' only after asynchronous data and charts finish. A short page.waitForTimeout(500) can cover a known animation, but an explicit state is more reliable.

5. Formats, dimensions, and transparency

  • PNG: lossless, the documented default, and suitable for text, UI edges, and transparency.
  • JPEG: smaller for photographs; set quality from 0 to 100. It is always opaque.
  • WebP: compact web output with a configurable quality value.
await page.screenshot({ path: 'image.jpg', type: 'jpeg', quality: 82 });
await page.screenshot({ path: 'image.webp', type: 'webp', quality: 88 });
const bytes = await page.screenshot({ type: 'png', omitBackground: true });
require('node:fs').writeFileSync('transparent.png', bytes);

omitBackground removes the default page background for formats that support alpha. If your CSS sets a background color, remove or override that rule too.

6. Capture one element

const card = page.locator('.pricing-card');
await card.waitFor();
await card.screenshot({ path: 'pricing-card.png' });

Element screenshots are useful for cards and charts. Overflow, sticky positioning, and animations can change the bounding box, so use deterministic dimensions and freeze motion when pixel consistency matters.

7. Puppeteer alternative

Puppeteer exposes the same browser approach through Page.screenshot() and element screenshots in its official screenshot guide.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setViewport({ width: 1200, height: 800, deviceScaleFactor: 2 });
  await page.setContent('<h1 id="title">HTML to image</h1>');
  await page.screenshot({ path: 'page.png', fullPage: true });
  const title = await page.$('#title');
  await title.screenshot({ path: 'title.png' });
  await browser.close();
})();

8. Python and PHP workflows

Python can drive Playwright with the same viewport, wait, and screenshot options:

from pathlib import Path
from playwright.sync_api import sync_playwright

html = Path('design.html').read_text(encoding='utf-8')
with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={'width': 1200, 'height': 800}, device_scale_factor=2)
    page.set_content(html, wait_until='load')
    page.evaluate('document.fonts.ready')
    page.screenshot(path='design.png', full_page=True)
    browser.close()

For PHP, Browsershot wraps Puppeteer and supports rendering a URL or arbitrary HTML. Check the current project documentation for installation and browser requirements.

9. Remove page branding from an authorized preview

A screenshot operation does not add a converter watermark. It also cannot remove branding that is part of the source page or image asset. For a page you own, inject CSS to hide known overlays:

await page.addStyleTag({ content: `
  .cookie-banner, .newsletter-modal, .chat-widget { display: none !important; }
` });
await page.screenshot({ path: 'clean.png' });

If a watermark is baked into an image, CSS cannot remove those pixels; replace the asset with an authorized clean version.

10. Make automated captures repeatable

  • Pin the browser version and use the same operating-system fonts.
  • Set viewport, device scale, timezone, locale, and color scheme explicitly.
  • Disable or freeze animations.
  • Use local assets or wait for every remote asset that affects the result.
  • Use fixture data for charts and timestamps.
  • Reuse a browser process for batches, but isolate pages or contexts.

Very long full-page images consume more memory. Split long documents when the destination allows it. Save the URL, viewport, browser version, and options with each artifact so differences can be reproduced.

11. Troubleshooting

Symptom Cause Fix
Blank or half-rendered image Capture ran before async content finished Wait for a readiness selector, fonts, images, or a bounded delay.
Fallback font Font was loading or unavailable Await document.fonts.ready and verify the font URL.
Bottom is missing Viewport capture was used Use fullPage: true.
Selector fails Wrong selector, iframe, or late rendering Wait for the selector and inspect the correct frame.
Transparent output is white JPEG or an opaque CSS background Use PNG/WebP, omitBackground: true, and remove the CSS background.
Images are missing Lazy loading, blocked request, or failed origin Check requests and wait until images are complete.
CI pixels differ Fonts, browser, viewport, timezone, or animation differs Pin those inputs and disable motion.
Navigation timeout Open requests or unreachable page Use a realistic timeout and wait for a specific state.

12. Performance, reliability, and cost

Browser startup is the expensive step in a one-off script. For a service, keep one browser process alive and create isolated contexts or pages. Limit concurrency to available memory and close pages in a finally block. Cache identical HTML and capture settings when the design is immutable.

PNG uses more bytes than compressed JPEG or WebP. Retries help with transient failures, but do not retry deterministic 404s or invalid markup indefinitely. Local automation avoids a hosted conversion fee, but still requires compute, browser storage, CI minutes, and maintenance.

13. Or skip the browser setup

ScreenshotNeo turns a URL into a clean PNG, JPEG, WebP, or PDF with one GET request. It supports full-page capture with lazy images, CSS element selection, custom CSS and JavaScript, waits, device presets or any viewport, retina scale, dark mode, headers, cookies, user agents, geolocation, blocking rules, resizing, caching, signed links, async jobs, bulk capture, and usage reporting.

See the ScreenshotNeo documentation for the API:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are never billed; response headers report the page verdict and whether it was billed. An MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000, and every feature is on every plan.

Create a free ScreenshotNeo account and start with the 1,000 monthly shots.

14. FAQ

Can HTML become a PNG without a watermark?

Yes. Render it in Playwright or Puppeteer and save a PNG; the screenshot operation does not add a converter branding layer.

Should I capture the viewport or full page?

Use the viewport for a fixed composition and fullPage: true when content below the fold belongs in the image.

Why is the output blurry?

Increase deviceScaleFactor, then check whether the destination is resizing or compressing the image.

Can a screenshot remove a watermark already in the source?

No. The browser reproduces visible pixels. Replace the asset or use an authorized clean source.

Can I return bytes instead of writing a file?

Yes. Omit path; Playwright returns a screenshot buffer that you can upload or process.