ScreenshotNeo

BlogHow-to

How to Convert an HTML String from an API into an Image in Angular

Render API HTML safely in Angular, wait for assets, capture it with html2canvas, and export a PNG or Blob without missing styles or images.

By the ScreenshotNeo team1 October 20268 min read

Direct answer: Fetch the API response as a string, bind it to a dedicated Angular element with [innerHTML], wait until Angular and the element’s images and fonts finish rendering, then pass that element to html2canvas. Export the resulting canvas with toDataURL('image/png') for a data URL or toBlob() for a download or upload.

Do not assign an untrusted API string directly with element.innerHTML. Angular’s ordinary [innerHTML] binding sanitizes HTML in the HTML security context. Avoid bypassSecurityTrustHtml unless the content has been validated and sanitized at a trusted boundary.

1. Install html2canvas

npm install html2canvas

html2canvas runs in a browser. It reconstructs supported DOM and CSS into a canvas; it does not take a native browser screenshot. Unsupported CSS, cross-origin frames, and unavailable images can therefore differ from what the browser displays.

2. Build a complete Angular component

The following standalone component fetches HTML, renders it through Angular’s binding, waits for images and fonts, captures the element, and exposes both a preview and a PNG download.

import {
  AfterViewInit,
  Component,
  ElementRef,
  ViewChild,
  inject,
} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import html2canvas from 'html2canvas';

@Component({
  selector: 'app-html-image',
  standalone: true,
  template: `
    <button type="button" (click)="loadAndCapture()" [disabled]="busy">
      {{ busy ? 'Creating image…' : 'Create image' }}
    </button>
    <p *ngIf="error" role="alert">{{ error }}</p>

    <div #capture class="capture" [innerHTML]="apiHtml"></div>

    <img
      *ngIf="imageUrl"
      [src]="imageUrl"
      alt="Rendered API HTML"
    />

    <a *ngIf="downloadUrl" [href]="downloadUrl" download="api-content.png">
      Download PNG
    </a>
  `,
  styles: [`
    .capture {
      width: 900px;
      padding: 24px;
      background: #fff;
      color: #111;
    }
  `],
})
export class HtmlImageComponent implements AfterViewInit {
  private readonly http = inject(HttpClient);

  @ViewChild('capture', { static: false })
  capture!: ElementRef<HTMLElement>;

  apiHtml = '';
  imageUrl = '';
  downloadUrl = '';
  error = '';
  busy = false;

  ngAfterViewInit(): void {
    // The ViewChild is available here. Capture is still triggered after
    // the API value is assigned and Angular has rendered it.
  }

  async loadAndCapture(): Promise<void> {
    this.busy = true;
    this.error = '';
    this.revokeUrls();

    try {
      const html = await firstValueFrom(
        this.http.get('/api/content', { responseType: 'text' }),
      );
      this.apiHtml = html;

      // Let Angular apply [innerHTML] before inspecting the element.
      await this.nextFrame();
      await this.waitForImages(this.capture.nativeElement);
      await this.waitForFonts();

      const element = this.capture.nativeElement;
      const canvas = await html2canvas(element, {
        backgroundColor: '#ffffff',
        useCORS: true,
        scale: window.devicePixelRatio,
        windowWidth: element.scrollWidth,
        windowHeight: element.scrollHeight,
      });

      this.imageUrl = canvas.toDataURL('image/png');
      this.downloadUrl = await this.canvasToObjectUrl(canvas);
    } catch (cause) {
      console.error(cause);
      this.error = 'The HTML could not be rendered into an image.';
    } finally {
      this.busy = false;
    }
  }

  private nextFrame(): Promise<void> {
    return new Promise((resolve) => requestAnimationFrame(() => resolve()));
  }

  private async waitForImages(root: HTMLElement): Promise<void> {
    const images = Array.from(root.querySelectorAll('img'));
    await Promise.all(images.map((image) => {
      if (image.complete) {
        return image.decode?.().catch(() => undefined);
      }
      return new Promise<void>((resolve) => {
        image.addEventListener('load', () => resolve(), { once: true });
        image.addEventListener('error', () => resolve(), { once: true });
      });
    }));
  }

  private async waitForFonts(): Promise<void> {
    if ('fonts' in document) {
      await document.fonts.ready;
    }
  }

  private canvasToObjectUrl(canvas: HTMLCanvasElement): Promise<string> {
    return new Promise((resolve, reject) => {
      canvas.toBlob((blob) => {
        if (!blob) {
          reject(new Error('Canvas conversion returned no Blob'));
          return;
        }
        resolve(URL.createObjectURL(blob));
      }, 'image/png');
    });
  }

  private revokeUrls(): void {
    if (this.imageUrl.startsWith('blob:')) URL.revokeObjectURL(this.imageUrl);
    if (this.downloadUrl.startsWith('blob:')) URL.revokeObjectURL(this.downloadUrl);
    this.imageUrl = '';
    this.downloadUrl = '';
  }
}

For a minimal version, assign canvas.toDataURL('image/png') to an image source. For large captures, prefer toBlob; base64 data URLs keep the complete encoded image in memory.

3. Fetch and validate the API HTML

Request the response as text rather than JSON unless the API wraps the markup in a JSON property.

this.http.get('/api/content', { responseType: 'text' });

If the response is JSON, extract the expected field and validate its shape before assigning it:

const response = await firstValueFrom(
  this.http.get<{ html: string }>('/api/content'),
);
if (typeof response.html !== 'string') {
  throw new Error('API did not return an HTML string');
}
this.apiHtml = response.html;

Keep the accepted markup contract narrow when content can be influenced by users or third parties. Remove scripts, event-handler attributes such as onclick, dangerous URL schemes, unexpected forms, and resource URLs that your application does not permit. Angular’s sanitizer helps protect the binding, but it is not a replacement for validating an untrusted content source.

4. Wait for rendering before capture

Calling html2canvas immediately after changing apiHtml can capture an empty or incomplete element. A button click after the value is assigned is deterministic. For automatic capture, schedule work after Angular has rendered, then wait for dependent assets.

  • Wait at least one animation frame after assigning the string.
  • Wait for images to load or fail.
  • Wait for document.fonts.ready when web fonts affect layout.
  • Use a selector, delay, or network-idle strategy when a component renders content asynchronously.

5. Configure html2canvas for useful output

Option Purpose Practical guidance
backgroundColor Sets the canvas background. Use '#ffffff' for a normal image or null when transparency is required.
useCORS Attempts to load images with CORS. Works only when the image server sends a permissive CORS header.
scale Controls output pixel density. window.devicePixelRatio is sharp but increases memory and file size; use a lower fixed value for very large captures.
windowWidth, windowHeight Controls the rendering viewport. Use the target element’s scroll dimensions for a full element capture.
ignoreElements Excludes matching DOM nodes. Skip animated controls, timestamps, or private UI before capture.
onclone Changes the cloned document only. Hide blinking carets or force print styles without changing the live page.

Capture one element by passing its DOM node. To capture a full page, select a page wrapper and ensure its width and height include all content. Very tall pages can exceed browser canvas limits and produce blank or truncated output; split the content into sections when necessary.

6. Cross-origin images, fonts, and iframes

An exported canvas must remain readable. Same-origin images are simplest. Cross-origin images need a server response that permits your origin and an image request that uses CORS. If the image server does not send the required header, put approved assets behind a controlled same-origin proxy or inline approved assets as data URLs.

Do not set allowTaint: true when you need to read or export the canvas. A tainted canvas blocks operations such as toDataURL and toBlob.

Cross-origin iframes cannot be rendered by html2canvas. Replace them with same-origin content, capture the iframe separately where permitted, or use a real browser renderer.

7. Export, download, and upload the image

Download a PNG

canvas.toBlob((blob) => {
  if (!blob) return;
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = 'api-content.png';
  link.click();
  URL.revokeObjectURL(url);
}, 'image/png');

Upload a Blob

const blob = await new Promise<Blob>((resolve, reject) => {
  canvas.toBlob((value) => value ? resolve(value) : reject(new Error('No blob')), 'image/png');
});
const form = new FormData();
form.append('image', blob, 'api-content.png');
await firstValueFrom(this.http.post('/api/images', form));

8. Security checklist

  • Use [innerHTML], not a direct DOM innerHTML assignment.
  • Do not call bypassSecurityTrustHtml for untrusted API data.
  • Validate the API response type and sanitize content at the trust boundary.
  • Restrict image, font, stylesheet, and iframe origins with your application policy.
  • Do not include secrets in the HTML string or expose private API responses to a public capture route.
  • Remember that rendered HTML can trigger network requests even when scripts are removed.

9. Troubleshooting

Symptom Likely cause Fix
Blank image Capture ran before Angular rendered. Wait for a render frame, images, and fonts before calling html2canvas.
Images missing Images are still loading or are cross-origin without CORS. Wait for image completion and configure CORS or a controlled proxy.
SecurityError during export The canvas was tainted by a cross-origin resource. Fix CORS or proxy the asset; do not use allowTaint.
Styles differ html2canvas supports only part of CSS and reconstructs the DOM. Simplify unsupported styles, use a capture-specific stylesheet, or switch to a real browser renderer.
Fonts shift the layout Web fonts were not ready. Await document.fonts.ready and verify font CORS headers.
Output is cut off The element exceeds browser canvas dimensions. Reduce scale, split the capture, or render on the server.
Iframe content is absent It is cross-origin. Use same-origin content or a browser automation workflow with appropriate access.
API markup appears as text The value was interpolated with {{ apiHtml }}. Use [innerHTML]="apiHtml" on the dedicated capture element.

10. Client-side versus server-side rendering

Client-side html2canvas is convenient when the user already has the Angular page open and the content fits browser limits. It uses the user’s device memory and cannot faithfully reproduce every CSS feature.

For server-side generation, SSR workflows, scheduled jobs, or pixel-accurate output, use a real browser automation renderer such as Puppeteer or Playwright. html2canvas depends on browser APIs and is not suitable for Node.js by itself.

11. Performance, reliability, and cost considerations

  • Capture only the required element instead of the entire document.
  • Lower scale for very large images and use toBlob for uploads.
  • Wait for assets with a bounded application timeout so one broken image does not hold the UI forever.
  • Remove animations and dynamic timestamps in a cloned capture state for repeatable output.
  • Use a server renderer when many users need identical output or when browser memory is constrained.
  • Cache generated files when the source HTML and capture options are unchanged.

12. Or skip the browser setup

If the HTML is available at a URL, ScreenshotNeo can capture the rendered page through one request. Its cleanup step accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for the available options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/rendered-html -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/rendered-html"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/rendered-html' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo supports PNG, JPEG, WebP, and PDF output, full-page and element capture, custom CSS and JavaScript, waits, resource blocking, headers, cookies, user agents, authorization, timezone, geolocation, caching, signed links, asynchronous jobs, webhooks, bulk capture, and a usage API. Its Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

13. FAQ

Can I convert the API string without showing it on screen?

The element must exist in the browser DOM for html2canvas. You can place it in an off-screen capture container, but it still needs dimensions and loaded assets.

Should I use SVG or PNG?

html2canvas exports a canvas, so PNG is the straightforward lossless format. Use JPEG when a smaller photo-oriented file is more useful and transparency is not needed.

Why does the result not match a native screenshot?

html2canvas rebuilds supported DOM and CSS rather than capturing browser pixels. Use Puppeteer, Playwright, or ScreenshotNeo when exact browser rendering is required.

Can Angular Universal create the image during SSR?

Not with html2canvas alone. It requires browser APIs such as the DOM and canvas; perform the capture in a browser or use a server-side browser renderer.