ScreenshotNeo

BlogHTML to image & PDF

How to Generate Multiple PDFs from HTML Files with iclems iOS-htmltopdf

Generate a batch of PDFs from HTML on iOS without overlapping UIWebView jobs, missing assets, or unexpected pagination.

By the ScreenshotNeo team30 September 20267 min read

How to Generate Multiple PDFs from HTML Files with iclems iOS-htmltopdf

Use a serial completion-driven queue. NDHTMLtoPDF renders asynchronously through UIWebView, so a tight for loop can start overlapping conversions and crash. Retain the active converter, create a unique output path for every document, and start the next conversion only from the previous job’s success or error callback.

The iclems/iOS-htmltopdf project documents URL-based and in-memory HTML conversion, page sizes supplied as CGSize, margins supplied as UIEdgeInsets, and delegate or block callbacks. See the project README for the initializer signatures available in your version.

1. Add iOS-htmltopdf and prepare a batch

Install the library using the method supported by your project, then keep the conversion queue and active converter alive for the entire batch. The README specifically recommends retaining a property such as PDFCreator because conversion is asynchronous.

For URL-backed HTML files, the source can be a local file URL. For HTML generated in memory, use the documented createPDFWithHTML overload and pass a baseURL when relative CSS, images, or fonts need to resolve.

2. Complete Objective-C serial batch implementation

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "NDHTMLtoPDF.h"

@interface HTMLPDFBatchConverter : NSObject
@property (nonatomic, strong) NSMutableArray<NSString *> *pendingHTMLFiles;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSString *> *results;
@property (nonatomic, strong) NDHTMLtoPDF *PDFCreator;
@property (nonatomic, copy) NSString *documentsDirectory;
@property (nonatomic, assign) CGSize paperSize;
@property (nonatomic, assign) UIEdgeInsets margins;
- (void)startWithHTMLFiles:(NSArray<NSString *> *)paths
                paperSize:(CGSize)paperSize
                  margins:(UIEdgeInsets)margins;
@end

@implementation HTMLPDFBatchConverter

- (instancetype)init {
    self = [super init];
    if (self) {
        _pendingHTMLFiles = [NSMutableArray array];
        _results = [NSMutableDictionary dictionary];
        _documentsDirectory = NSSearchPathForDirectoriesInDomains(
            NSDocumentDirectory, NSUserDomainMask, YES
        ).firstObject;
    }
    return self;
}

- (void)startWithHTMLFiles:(NSArray<NSString *> *)paths
                paperSize:(CGSize)paperSize
                  margins:(UIEdgeInsets)margins {
    self.pendingHTMLFiles = [paths mutableCopy];
    self.paperSize = paperSize;
    self.margins = margins;
    [self convertNextHTML];
}

- (void)convertNextHTML {
    if (self.pendingHTMLFiles.count == 0) {
        self.PDFCreator = nil;
        return;
    }

    NSString *htmlPath = self.pendingHTMLFiles.firstObject;
    [self.pendingHTMLFiles removeObjectAtIndex:0];

    NSURL *htmlURL = [NSURL fileURLWithPath:htmlPath];
    NSString *baseName = htmlPath.lastPathComponent.stringByDeletingPathExtension;
    NSString *pdfName = [baseName stringByAppendingPathExtension:@"pdf"];
    NSString *pdfPath = [self.documentsDirectory stringByAppendingPathComponent:pdfName];

    // Ensure each destination is unique if input names can repeat.
    if ([[NSFileManager defaultManager] fileExistsAtPath:pdfPath]) {
        NSString *uniqueName = [NSString stringWithFormat:@"%@-%@.pdf",
                                 baseName, NSUUID.UUID.UUIDString];
        pdfPath = [self.documentsDirectory stringByAppendingPathComponent:uniqueName];
    }

    self.PDFCreator = [NDHTMLtoPDF createPDFWithURL:htmlURL
        pathForPDF:pdfPath
        pageSize:self.paperSize
        margins:self.margins
        successBlock:^(NDHTMLtoPDF *job) {
            self.results[htmlPath] = pdfPath;
            [self convertNextHTML];
        }
        errorBlock:^(NDHTMLtoPDF *job) {
            self.results[htmlPath] = @"FAILED";
            // Record the failure, then keep the queue moving.
            [self convertNextHTML];
        }];
}

@end

// Example:
// CGSize a4 = CGSizeMake(595.2, 841.8);
// UIEdgeInsets margins = UIEdgeInsetsMake(10, 5, 10, 5);
// [converter startWithHTMLFiles:htmlPaths paperSize:a4 margins:margins];

Adapt the exact initializer spelling to the library version in your project. The documented API has both delegate and block forms. The important behavior is unchanged: retain the converter and advance from both completion paths.

Process each HTML file after the previous PDF conversion finishes.
Process each HTML file after the previous PDF conversion finishes.

3. Convert HTML strings already in memory

NSString *html = @"<html><head><link rel='stylesheet' href='css/invoice.css'></head><body><h1>Invoice</h1></body></html>";
NSString *output = [documentsDirectory stringByAppendingPathComponent:@"invoice-0001.pdf"];
NSURL *baseURL = [NSURL fileURLWithPath:temporaryHTMLDirectory isDirectory:YES];

self.PDFCreator = [NDHTMLtoPDF createPDFWithHTML:html
    pathForPDF:output
    pageSize:CGSizeMake(595.2, 841.8)
    margins:UIEdgeInsetsMake(10, 5, 10, 5)
    baseURL:baseURL
    successBlock:^(NDHTMLtoPDF *job) {
        // Continue with the next item.
    }
    errorBlock:^(NDHTMLtoPDF *job) {
        // Record the failure and continue.
    }];

Use the overload exposed by your installed version. A base URL gives relative resources a resolvable origin; without it, local CSS, images, and fonts may be missing.

A base URL gives relative resources an origin during rendering.
A base URL gives relative resources an origin during rendering.

4. Choose paper size and margins explicitly

The README’s A4 example is CGSizeMake(595.2, 841.8). Margins are UIEdgeInsets values. Keep these values consistent throughout a batch unless a document intentionally uses a different layout.

Setting What to decide Why it matters
Paper size A4 example or your required CGSize Controls the printable width and height.
Margins Top, left, bottom, right in UIEdgeInsets Reduces the usable content area and can create an extra page.
Output path Unique path per input Prevents overwrites and stale-file confusion.
Resource base URL Directory or URL containing relative assets Lets CSS, images, and fonts resolve.

5. Prevent unexpected second pages

A document that appears to fit visually can still overflow the printable area. Check the effective height after margins, then inspect:

  • Default body margins.
  • Fixed-height containers and bottom padding.
  • Image dimensions and intrinsic aspect ratios.
  • Line wrapping caused by the selected paper width.
  • Fonts that load late or change text metrics.
  • Explicit UIEdgeInsets margins.

Log the item identifier, paper size, margins, source path, and destination path for every job. That makes a second-page result reproducible.

6. Batch reliability checklist

  1. Build a stable input list before starting.
  2. Assign a unique destination path to every item.
  3. Retain the current NDHTMLtoPDF object in a property or retained per-job collection.
  4. Start exactly one conversion at a time unless you have verified that your target library and device can safely handle more.
  5. Call the queue advance method from both success and error callbacks.
  6. Record success and failure by input path.
  7. Release the converter only after its callback has fired.
  8. Verify that the output file exists before presenting it to the user.

7. Troubleshooting

Crash when processing several files

Cause: a tight loop starts multiple asynchronous UIWebView conversions at once. The Stack Overflow report for this workflow describes that overlap and suggests coordination.

Fix: use a serial completion-driven queue like the implementation above. Start the next conversion only after success or failure, and retain the active converter.

Images, CSS, or fonts are missing

Cause: relative URLs have no resolvable origin.

Fix: use a createPDFWithHTML overload with baseURL:, or make sure a URL-based source can resolve every referenced resource.

The wrong PDF is returned

Cause: duplicate output names, stale files, or an input path whose extension was removed incorrectly.

Fix: expand every destination path explicitly, make names unique, and decide whether an existing file should be overwritten or replaced with a unique name.

A one-page document becomes two pages

Cause: the content exceeds the printable height after margins, often because of body margins, fixed boxes, image sizing, or wrapping.

Fix: inspect the effective CSS box sizes, reduce unintended spacing, size images explicitly, and log the selected paper size and margins.

The queue stops after one failed item

Cause: the next item is started only in the success callback.

Fix: advance from the error callback too, while recording the failed input.

The batch appears stuck

Cause: the converter was deallocated, the callback is not wired for the initializer variant you selected, or a resource never resolves.

Fix: retain the converter, use the callback signature documented for your version, verify local asset URLs, and add per-item logging before and after conversion.

8. Performance, reliability, and cost considerations

The project documentation and the cited workflow report do not publish a throughput benchmark or compatibility matrix. Treat conversion speed as dependent on HTML size, resource loading, device, and page complexity. Serial processing uses more wall-clock time than uncontrolled parallelism, but it bounds memory pressure and avoids overlapping UIWebView work.

On-device conversion can work without a hosted conversion service, but it requires you to manage local files, rendering differences, retries, and queue state. If the source HTML depends on network resources, capture success also depends on those resources being available when the device renders the page.

9. Or skip the browser setup

If your input is a public web page rather than local HTML files, ScreenshotNeo provides a single GET request for a clean page capture and also offers PDF capture through its MCP server. Its documented API and options are at screenshotneo.com/docs.

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, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing result.
  • An MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf.
  • Every plan includes the features; 1,000 screenshots each month are free with no card, and paid plans start at $5 for 3,000.

Start with the free ScreenshotNeo account.

10. FAQ

Can I launch all conversions with a dispatch group?

You can coordinate asynchronous work, but the documented crash report makes overlapping conversions a risk. A serial queue is the simpler default for this library.

Should every file use the same margins?

Use consistent margins for predictable batches. Change them only when a document’s layout requires it, and log the exception.

Do I need a base URL for every HTML string?

No. You need one when the HTML references relative CSS, images, fonts, or other resources that must be resolved.

Does the README provide a performance guarantee?

No. The available documentation does not publish a throughput figure or compatibility matrix.