A successful generation request gives you a file. It does not tell you whether a price symbol changed, a required label disappeared, or a table broke where you expected it to.

We will first look at a real catalog export with a branded image, a product illustration, prices, and component rows. Then three focused teaching samples isolate text, fonts, and page breaks. ComPDF generates the PDFs; PDF.js reads them for acceptance checks.

I work on ComPDF content. The results below come from this API run and these teaching samples.

What the generation step actually produced

The ComPDF generation page describes HTML-based document generation with text, images, tables, and structured data. To check the part relevant to this tutorial, I used one original catalog layout with two teaching datasets: four component rows and 32 component rows.

Node.js fills the values and rows into HTML. The request then sends generationType=html, html, pageWidth=595, and pageHeight=842 to ComPDF. The API performs the HTML-to-PDF generation; the local data interpolation is separate application code.

Actual ComPDF catalog PDFs generated from the short and long HTML inputs

Figure: the actual downloaded PDFs, rendered for inspection. The desk-stand artwork and catalog data are teaching examples. The long output is shown with both pages.

Input Actual output Checked in this run
MODEL-Q7, four rows, €128.50 One page Model, price, all four item IDs, headings, end marker, and visible images
MODEL-T9, 32 rows, €206.40 Two pages Model, price, all 32 item IDs, headings, end marker, and visible images

Every page measured 595 × 842 pt, matching the request. The longer input continued its table onto a second page; the final row and end marker remained present. I inspected all three output pages. Repeated column headings on later pages are still a separate requirement.

This demonstrates a specific use of ComPDF: generating PDFs from business HTML that includes both text and images. It does not establish arbitrary CSS compatibility or certify the full document from a text check. It also does not validate the API's separate server-side template mode; these successful requests used HTML already filled in Node.js.

Now make the checks smaller, so a failed requirement points to a useful next step.

Start with requirements you can name

For the short sheet, keep MODEL-Q7, 24 V, and 128.50 USD. For multilingual labels, retain the Chinese text, Français, 23 °C, and €128.50. For the parts list, check the beginning, middle, end, column headings, and final marker.

Page limits belong to these samples. A 2–6 page range catches an implausible result for this parts list; it is not a rule for every export.

Tested locally: Node.js 24.18.0 and pdfjs-dist 5.6.205. Create a directory with samples/ and results/, then install the reader:

npm init -y npm install --save-exact pdfjs-dist@5.6.205 mkdir -p samples results 

Use .mjs filenames for the scripts below. Obtain your public API key as described in ComPDF API authentication, and keep it in a server-side environment variable:

export COMPDF_API_KEY='YOUR_PUBLIC_API_KEY' 

Build three small HTML samples

Each file starts with this document structure. Save the first as samples/simple.html:

<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Product sheet teaching sample</title> <style> body { font-family: Arial, sans-serif; color: #232b43; font-size: 12pt; line-height: 1.5; } h1 { font-size: 25pt; } table { width: 100%; border-collapse: collapse; } td { border: 1px solid #cbd4e1; padding: 8px; } tr { break-inside: avoid; } </style> </head> <body> <h1>Model Q7 product sheet</h1> <p>MODEL-Q7</p> <p>Supply voltage: 24 V</p> <p>Unit price: 128.50 USD</p> <p>Teaching sample. No product performance claims.</p> </body> </html> 

For samples/multilingual.html, use this body and set the body font family to "DejaVu Sans", "Noto Sans CJK SC", sans-serif:

<h1>Language labels</h1> <p>MODEL-M2</p> <p>中文:产品说明 · 教学示例</p> <p>Français : température 23 °C</p> <p>Price: €128.50</p> 

That font selection passed this run. It is not a promise that a matching font is installed in every rendering environment. If a font is a delivery requirement, verify its availability and the actual output; naming it in CSS alone is not enough.

For samples/long-table.html, keep the document structure and replace the body with a title, a table, and an end marker. This small setup script creates the rows without hand-copying 60 entries:

// make-table.mjs import fs from 'node:fs/promises'; const source = await fs.readFile('samples/simple.html', 'utf8'); const rows = Array.from({ length: 60 }, (_, i) => { const n = i + 1; return `<tr><td>PART-${String(n).padStart(3, '0')}</td><td>Teaching component ${n}</td><td>${n % 5 + 1}</td></tr>`; }).join(''); const body = `<h1>Model T9 parts list</h1><p>MODEL-T9 — teaching sample</p> <table><tbody><tr><td>Part</td><td>Description</td><td>Quantity</td></tr>${rows}</tbody></table> <p>END-OF-PARTS</p>`; await fs.writeFile('samples/long-table.html', source.replace(/<body>[\s\S]*<\/body>/u, `<body>${body}</body>`)); 

Run node make-table.mjs. The tested exports embed the ComPDF logo as a data URI, so the image has no external download dependency. The snippets above focus on the content under test.

Save these requirements as cases.json:

{ "simple": {"expect":["MODEL-Q7","24 V","128.50 USD"],"minPages":1,"maxPages":1}, "multilingual": {"expect":["MODEL-M2","产品说明","教学示例","Français","23 °C","€128.50"],"minPages":1,"maxPages":1}, "long-table": {"expect":["MODEL-T9","PART-001","PART-030","PART-060","END-OF-PARTS","Part","Description","Quantity"],"minPages":2,"maxPages":6} } 

Generate and download the files

The PDF Generation API accepts HTML input. This script checks the HTTP response, the business code, the returned status, and the downloaded file header. Save it as generate.mjs:

import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const root=path.dirname(fileURLToPath(import.meta.url)); const names=process.argv.slice(2); const cases=JSON.parse(await fs.readFile(path.join(root,'cases.json'),'utf8')); const key=process.env.COMPDF_API_KEY; if(!key)throw new Error('Set COMPDF_API_KEY to your public API key.'); await fs.mkdir(path.join(root,'results'),{recursive:true}); for(const name of names.length?names:Object.keys(cases).filter(k=>!cases[k].negative)) { if(!(name in cases))throw new Error(`Unknown case: ${name}`); const form=new FormData(); form.append('generationType','html'); form.append('html',await fs.readFile(path.join(root,'samples',`${name}.html`),'utf8')); form.append('outputFileName',`${name}.pdf`); const response=await fetch('https://api-server.compdf.com/server/v2/process/pdf/generate',{ method:'POST',headers:{'x-api-key':key},body:form,signal:AbortSignal.timeout(120000) }); if(!response.ok)throw new Error(`${name}: generation HTTP ${response.status}. Not automatically resubmitting.`); const body=await response.json(); if(String(body.code)!=='200'||body.data?.status!=='success'||!body.data?.downloadUrl) throw new Error(`${name}: generation not successful (code ${body.code}, status ${body.data?.status}).`); const download=await fetch(body.data.downloadUrl,{signal:AbortSignal.timeout(30000)}); if(!download.ok)throw new Error(`${name}: download HTTP ${download.status}.`); const bytes=new Uint8Array(await download.arrayBuffer()); if(Buffer.from(bytes.slice(0,5)).toString()!=='%PDF-')throw new Error(`${name}: response is not a PDF.`); await fs.writeFile(path.join(root,'results',`${name}.pdf`),bytes); console.log(`${name}: saved PDF (${bytes.length} bytes)`); } 

Run node generate.mjs. The API key is sent only to the generation endpoint, not forwarded to the result-download URL. The script saves the files promptly and does not print the download URLs or key.

If a submission times out, investigate the request before resubmitting it. A missing client response does not prove that generation never started.

Check text and page counts

Save this as check.mjs:

import fs from 'node:fs/promises'; import path from 'node:path'; import {fileURLToPath} from 'node:url'; import {getDocument} from 'pdfjs-dist/legacy/build/pdf.mjs'; const root=path.dirname(fileURLToPath(import.meta.url)); export const normalize=text=>text.normalize('NFKC').replace(/\s+/gu,''); export async function inspect(file,rule) { const data=new Uint8Array(await fs.readFile(file)); const pdf=await getDocument({data,useSystemFonts:true,disableFontFace:true}).promise; try { let text=''; for(let i=1;i<=pdf.numPages;i++) { const page=await pdf.getPage(i);const content=await page.getTextContent(); text+=content.items.filter(x=>'str' in x).map(x=>x.str).join(' ')+'\n'; } const missing=rule.expect.filter(t=>!normalize(text).includes(normalize(t))); const pagesOK=pdf.numPages>=rule.minPages&&pdf.numPages<=rule.maxPages; return {ok:missing.length===0&&pagesOK,pages:pdf.numPages,missing,pagesOK}; } finally {await pdf.destroy();} } if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)) { const cases=JSON.parse(await fs.readFile(path.join(root,'cases.json'),'utf8')); const report={};let failed=false; for(const [name,rule] of Object.entries(cases)) { if(rule.negative)continue; try {report[name]=await inspect(path.join(root,'results',`${name}.pdf`),rule);} catch(error){report[name]={ok:false,error:error.message};} failed||=!report[name].ok; } await fs.writeFile(path.join(root,'results','report.json'),JSON.stringify(report,null,2)); console.log(JSON.stringify(report,null,2)); process.exitCode=failed?1:0; } 

Then run node check.mjs. It writes results/report.json and exits with code 1 if a file cannot be read or a requirement fails.

The text normalization removes whitespace and applies Unicode NFKC normalization. That makes the check tolerant of line wrapping and some compatibility characters. It also means this check does not verify exact typography, whitespace, reading order, or every line of a table.

For stricter rules, change the normalization and add checks that match the document's purpose. Do not compare PDF binary hashes as a layout test: metadata changes can alter the bytes without altering the page.

A real font correction from this run

The first multilingual export used an Arial-based fallback stack. It failed the requirements for Français, 23 °C, and €128.50. Its rendered output also showed incorrect characters. Changing the font selection to DejaVu Sans and generating again produced the expected text and visible glyphs in this sample.

Actual PDF excerpts before and after the font selection change

Figure: excerpts of the actual generated PDFs. The text was not recreated for the illustration. This is a sample-specific correction, not a finding about all fonts or the underlying cause.

A text check alone would not distinguish a PDF text-mapping issue from a visual glyph issue. Inspecting the rendered page gave a second piece of evidence. Conversely, a page can look readable while its extractable text is wrong, which matters when users copy, search, or further process the PDF.

Inspect page breaks and headings separately

The corrected parts list contained 60 rows across 3 pages, including its final marker. Review every page to see where rows end, whether characters are clipped, and whether the next page still makes sense.

The first version placed the column headings in <thead> and requested display: table-header-group. Those headings did not appear in the tested output. Moving the initial labels into a <tbody> row restored the first heading row; this example has not established repeated headings on later pages.

If repeating headings or a particular page margin is required, treat it as a separate acceptance condition. Do not infer it from the fact that the PDF has three pages. CSS rules are input intentions until the generated document shows that they took effect.

Results and the distinction between text and visual checks

Figure: checks after correcting the font selection and initial table headings. Passing text checks does not certify the entire layout.

Sample Pages in this run Required text Further review
Short sheet 1 Passed Look for clipping and check the price
Multilingual labels 1 Passed after font change Inspect accents, Chinese glyphs, degree and euro symbols
Parts list 3 Passed after heading change Check all rows and page boundaries; repeated headings remain a separate requirement

Prove that the check rejects a bad export

Remove <p>MODEL-Q7</p> from a copy of simple.html and save it as missing-model.html. Add this entry to cases.json:

"missing-model": {"expect":["MODEL-Q7","24 V","128.50 USD"],"minPages":1,"maxPages":1,"negative":true} 

Generate the altered input explicitly with node generate.mjs missing-model. The normal report excludes cases marked negative; check the altered PDF against the original requirements:

// test-negative.mjs import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import { inspect } from './check.mjs'; const rules = JSON.parse(await fs.readFile('cases.json', 'utf8')); const missing = await inspect('results/missing-model.pdf', rules.simple); assert.equal(missing.ok, false); assert.deepEqual(missing.missing, ['MODEL-Q7']); const pages = await inspectSource:https://dev.to/derek-compdf/testing-html-to-pdf-exports-fonts-page-breaks-and-missing-text-1bch

Related posts