Convert HTML and Webpages to PDF in Rust

This guide shows how to use PDFCrowd's HTML to PDF API from Rust with reqwest. Your program sends an authenticated HTTP request containing a webpage URL, an HTML file, or an HTML string. PDFCrowd performs the conversion and returns the PDF in the response body.

Set up the example

To run the example, you need Rust and Cargo. Download Cargo.toml and main.rs. Put them in a project directory with this layout:

pdfcrowd-example/
├── Cargo.toml
└── src/
    └── main.rs

The manifest enables reqwest's blocking client and multipart form support:

[dependencies]
reqwest = { version = "0.13", features = ["blocking", "multipart"] }

Set your API credentials in the API_USERNAME and API_KEY environment variables. Both can be demo for testing. For example, in a POSIX shell:

export API_USERNAME=demo
export API_KEY=demo

Convert a webpage to PDF

From the project directory, build and run the downloaded program with a URL and an output filename:

cargo run -- url https://example.com/ output.pdf

The request goes to https://api.pdfcrowd.com/convert/24.04/ over HTTPS, using your API username and API key for HTTP Basic authentication. The example sends multipart form fields; reqwest supplies the request's content type and boundary.

The following excerpt shows the request construction. In the complete program, username and api_key come from the environment, and client is a reqwest::blocking::Client configured with connection and request timeouts:

use reqwest::blocking::multipart::Form;

let form = Form::new()
    .text("input_format", "html")
    .text("output_format", "pdf")
    .text("content_viewport_width", "balanced")
    .text("page_size", "A4")
    .text("url", "https://example.com/");

let response = client.post("https://api.pdfcrowd.com/convert/24.04/")
    .basic_auth(username, Some(api_key))
    .multipart(form)
    .send()?;

input_format and output_format select HTML to PDF conversion. The url field specifies the page to convert; it is separate from the API endpoint. content_viewport_width=balanced lets the converter choose a viewport width for the page. After checking for HTTP status 200, the program writes the binary response to output.pdf.

The webpage and its resources must be reachable by PDFCrowd. A URL on localhost refers to the conversion server, not the computer running this program.

Convert an HTML file or string

Use one input field per request: url, file, or text. The authentication and response handling stay the same.

Upload an HTML file

For a local file such as report.html, replace the .text("url", ...) call with:

.file("file", "report.html")?;

Form::file() opens the file and adds it as an upload. The downloadable program exposes this as:

cargo run -- file report.html report.pdf

If the document depends on local images or stylesheets, package them with the HTML in an archive and upload the archive as file. Uploading the HTML alone does not include neighboring files. See the archive input example for supported formats and choosing the main HTML file.

Send an HTML string

For HTML produced by your application, replace the .text("url", ...) call with:

.text("text", "<h1>Monthly report</h1><p>Revenue increased.</p>");

You can also try a short string with the downloaded program:

cargo run -- text '<h1>Monthly report</h1><p>Revenue increased.</p>' report.pdf

Rust strings are UTF-8. Images and stylesheets in uploaded or string HTML can use absolute, publicly reachable URLs. To resolve relative URLs against a public location, include a <base href="https://example.com/assets/"> element in the HTML document's <head>.

Customize the PDF

Conversion options are additional form fields. For example, extend the form before passing it to .multipart() to set the top and bottom margins and use print stylesheets:

let form = form
    .text("margin_top", "20mm")
    .text("margin_bottom", "20mm")
    .text("use_print_media", "true");

The main example already sets page_size to A4; replace that value to change the page format. Use the HTTP parameter names and string values from the parameter reference. Add each option once to the form.

Handle errors

A completed HTTP transfer can still contain an API error. The program checks the HTTP status before opening the output file:

let status = response.status();
if status != reqwest::StatusCode::OK {
    let details = response.text()?;
    return Err(std::io::Error::other(
        format!("PDFCrowd returned HTTP {status}: {details}")
    ).into());
}
let pdf = response.bytes()?;
std::fs::write(output, &pdf)?;

The PDF is read as bytes. Error bodies are read as text so the reported failure includes the API's explanation. The HTTP response documentation explains API errors.

The complete program propagates setup, request, and file-writing errors with ?; main() prints the error and exits with a nonzero status. It uses a 30-second connection timeout and a 120-second total request timeout; adjust these for your application's expected conversion time.

This example buffers the response in memory. For large outputs, a streaming implementation can write to a temporary file and keep it only after the transfer and HTTP status checks succeed.

Use reqwest in an async application

The example uses blocking I/O for a small command-line program. In an async application, use reqwest::Client and reqwest::multipart::Form, awaiting the request and response reads. The async Form::file() also needs .await and the stream feature. The endpoint, authentication, and form fields are the same. Avoid calling the blocking client inside an async runtime; see reqwest's blocking client documentation.