Table of contents
tc-lib-pdf renders a subset of HTML and CSS directly, with no browser engine involved. Markup goes into a placed block on the page, and the engine resolves the cascade, the box model, floats, tables, and page breaks itself.
Table of contents
The Two Entry Points
public addHTMLCell(
string $html,
float $posx = 0,
float $posy = 0,
float $width = 0,
float $height = 0,
?TCellDef $cell = null,
array $styles = [],
): void
public getHTMLCell(/* the same parameters */): string
addHTMLCell() writes the result into the document. It accounts for automatic page and region breaks, appending content to each page stream the block reaches, so a block longer than the remaining space continues onto the following pages.
getHTMLCell() returns the PDF operators as a string and writes nothing. Append them yourself:
$pdf->page->addContent($pdf->getHTMLCell($html, 15, 15, 180));
Use getHTMLCell() when the content fits on the current page and you want control over where the operators land, for instance inside an XObject template or between other page content. Its output stays on the current page. Use addHTMLCell() for anything that may span a page break.
getLastHTMLCellEnd() returns the page and the ordinate, in user units, of the bottom of the last block rendered by either method; the page is -1 before the first call. A block placed at that ordinate on that page starts right below the previous one, so a long document can be written in chunks:
$pdf->addHTMLCell('<h2>Orders</h2>', 15, 10, 180);
foreach ($chunks as $html) {
$end = $pdf->getLastHTMLCellEnd();
$pdf->setCurrentPage($end['page']);
$pdf->addHTMLCell($html, 15, $end['y'], 180);
}
/examples/E093_html_large_table/ writes a large table in one call and in chunks, and reports the page count and the peak memory of each method.
Both take positions and dimensions in the document unit selected on the constructor. The $cell argument overrides the padding and margin for this call alone, in points. $styles carries the cell border styles.
A minimal call:
$pdf->addHTMLCell(
html: '<h1>Invoice 2026-0184</h1><p>Due on 30 September 2026.</p>',
posx: 15,
posy: 20,
width: 180,
);
A font must be inserted and added to the page before any text renders. See /docs/fonts/.
Where the Styles Come From
Three author sources are resolved in the normal cascade order:
- The global stylesheet, registered once and injected into every rendering call as the lowest-priority author source.
<style>blocks inside the markup passed to the call.styleattributes on individual elements.
A document rule beats a global rule at equal specificity. To force a global rule to win, mark it !important.
$pdf->setGlobalCSS('
body { font-family: helvetica; font-size: 10pt; color: #222; }
.card { border: 0.5pt solid #999; padding: 4pt; }
');
$pdf->addGlobalCSS('.badge { background-color: #eef; font-weight: bold; }');
| Method | Effect |
|---|---|
setGlobalCSS(string $css) | Replaces the global stylesheet. The argument has no surrounding <style> tags. |
addGlobalCSS(string $css) | Appends to the current global stylesheet. |
resetGlobalCSS() | Clears it. |
The global CSS goes through the same parser as document CSS, so @import, @media, and @spot rules work there too.
The Supported CSS Subset
The engine covers the parts of CSS that a paged document needs. /examples/E073_css_supported_categories/ enumerates the categories in its own markup and renders them, which makes it the reference for what actually works in the release you have installed.
- Cascade: source order, specificity, shorthand expansion and normalization, and the CSS-wide keywords
inherit(including its shorthand forms),initial,unset,revert, andrevert-layer. - Selectors: type, class, id, descendant, child, the adjacent and general sibling combinators (
+and~), attribute selectors, and a subset of pseudo-classes including:empty. - Pseudo-elements:
::beforeand::afterwith text content. - Box model: margin, padding, border, width and height, and
inline-block. - Flow: floats,
clear, and positioning, plusfieldsetandlegend. - Typography:
font-size-adjust,font-variant,quotes,overflow-wrap,word-break,white-space,text-indentwith theeach-linekeyword,letter-spacing,font-stretch, andline-height. - Paged media:
orphans,widows, page break control, and theprintmedia type. - Colour: every notation tc-lib-color parses, including spot colours addressed from CSS for prepress work.
A font-family that names no inserted font falls back to the base font of the cell. Character references in attribute values (for example " or &) are decoded once before the value is used, including the declarations of a style attribute; an href is decoded when its link annotation is written.
unset resolves to inherit for an inherited property and to initial otherwise. revert and revert-layer drop the declaration, so the element keeps the value of its tag defaults or of its parent, and so does initial on a property that has no initial value in the engine.
A length declaration (sizes, margins, padding, border widths, offsets, spacing, font-size, and line-height) is ignored, as in CSS, when one of its values has no number and is not a keyword the property accepts, or when it has more values than the property takes; the element keeps its inherited or default value. A parenthesized group such as calc(...) counts as one value. width: auto and height: auto restore the automatic size.
The width and height attributes are presentational hints: a width or height declared in CSS takes precedence, and an attribute value with no number is ignored.
CSS flexbox and grid are not implemented. Layout that depends on them needs a different tool; /comparison/ covers where the headless-browser renderers fit.
Defaults You Can Change
The engine applies its own defaults before the cascade runs. These set them:
| Method | What it controls |
|---|---|
setDefaultCSSMargin(float $top, $right, $bottom, $left) | Default element margin. |
setDefaultCSSPadding(float $top, $right, $bottom, $left) | Default element padding. |
setDefaultCSSBorderSpacing(float $vert, float $horiz) | Default table border spacing. |
setDefaultCellMargin(float $top, $right, $bottom, $left) | Default cell margin, stored in points. |
setDefaultCellPadding(float $top, $right, $bottom, $left) | Default cell padding, stored in points. |
setDefaultCellBorderPos(float $borderpos) | BORDERPOS_DEFAULT, BORDERPOS_EXTERNAL, or BORDERPOS_INTERNAL. |
setHtmlVSpace(array $tagvs) | Vertical space before and after each tag, as a fixed height or a number of line heights. |
setHTMLMonospaceFont(string $fontname) | The font used for <pre>, <code>, and <tt>. |
setHtmlImageImportResize(bool $enabled) | Import raster images at a reduced pixel size when they render smaller than their intrinsic size. |
setHTMLMonospaceFont() matters for tagged output. PDF/UA requires every font to be embedded, and the default Courier is a non-embedded standard-14 font, so a tagged document needs an embedded monospace font set here after inserting it with the font stack. See /docs/standards/.
Tables
Tables carry the full structure tags: caption, colgroup, col, thead, tbody, and tfoot. Cells span rows and columns, both border models are supported, and a thead repeats at the top of every page the table crosses.
<table>
<thead>
<tr><th>Item</th><th>Qty</th><th>Amount</th></tr>
</thead>
<tbody>
<tr><td>Consulting</td><td>12</td><td>1,440.00</td></tr>
</tbody>
</table>
Widths accept pixel units as well as the document unit; see /examples/E077_html_table_header_px_unit/ for a header sized in pixels and repeated across pages. Floats and table clearance interact as CSS specifies, which /examples/E043_html_tables/ exercises.
The column grid comes from every row: a row with more cells than the first adds columns inside the table width, and a cell spanning rows keeps its column in the following rows. A height attribute or CSS height on a cell sets the minimum height of its row.
A table with page-break-inside: avoid or nobr="true" is kept together: it moves to the next page when its estimated height does not fit in the remaining space. The estimate includes the captions, nested tables, and cells spanning several columns at the width of all the columns they cover. /examples/E087_html_table_grid/ draws each case against its expected result.
The columns of an automatic layout table are widened when they are narrower than their longest word. On each page the cell borders are drawn after all the cell backgrounds of the table.
Across page breaks:
- A row taller than a page continues on the following pages, with the table head repeated and the cell borders left open at each break.
- The rows spanned by a cell are kept together when they fit on a page; otherwise the spanning cell continues over the break. Nested tables in a split row are drawn, with their borders, on each page they reach.
- When a table moves to the next page, the table head is drawn once there, and it is kept together with the first body row.
- A top
captionis kept with the first rows, and a bottomcaptionwith the last row. - A repeated table head keeps the column widths of the table, including percentage widths set on the head cells, and is drawn without the margins and padding of the table, so it touches the first body row.
/examples/E092_html_table_row_split/ renders each case with its expected result.
Lists
Ordered, unordered, and nested lists are supported with their CSS list-style properties. /examples/E032_html_lists/ renders the variations.
list-style-image: url(...) draws an image marker sized from the current font size. The source is a local path, resolved against the directory of the running script and the working directory and checked against markupAllowedPaths, a remote URL on an allowedHosts host, or a data URI holding PNG, another raster format, or SVG.
TCPDF 7.x converts the legacy list-style-type: img|type|width|height|path bullets of writeHTML() to list-style-image: url(path); the width and height fields are ignored.
Forms
Form controls written as markup are mapped to AcroForm widgets, with labels, field flags, and select precedence carried across. The result is an interactive PDF form rather than a picture of one.
<form>
<label for="name">Name</label>
<input type="text" name="name" value="">
<select name="country"><option value="it">Italy</option></select>
</form>
See /examples/E042_html_form/ for the markup side and /examples/E040_annotation_form/ for the widget API underneath.
Flow Across Pages
addHTMLCell() breaks the block at the page boundary and continues on the next page, honouring orphans and widows. /examples/E018_html_page_span/ shows a block long enough to cross several pages.
page-break-before and page-break-after request a page break at an element, including a paragraph or a table row, and so does the <tcpdf method="pagebreak" /> tag. Open styled blocks and tables are closed on the page left, and the content continues at the top of the region on the next page. A keep-together block taller than a page stays where it starts. /examples/E094_html_page_breaks/ renders each case with its expected result.
Two things interact with the flow and are worth knowing:
- Page margins bound the block. A cell placed inside the header or footer band behaves differently from one in the content area; /examples/E078_htmlcell_page_margins/ covers the cases.
- No-write regions and column regions constrain where text may go, and HTML flow respects both. See /examples/E080_no_write_regions/ and /examples/E019_page_regions/.
Line height is resolved from CSS; /examples/E069_html_line_height/ shows the values and units side by side.
Images and Remote Assets in Markup
An <img> in the markup is loaded through the same file layer as everything else, which means the allowlists apply. An image inside <a href> gets a link annotation over the image. A local path outside markupAllowedPaths and a remote host outside allowedHosts are both refused, and the document renders without the image. No exception is raised. See /docs/remote-resources/.
SVG referenced from markup goes through the same path. For direct SVG placement, use addSVG() or getSetSVG(); see /examples/E030_svg/.
The src of an <img> can also be an RFC 2397 data URI, in base64, plain, or percent-encoded form. The media type is optional and its parameters are ignored, and whitespace inside base64 data is skipped. An image is drawn as SVG when its data is an SVG document (the root element is svg after an optional XML prolog, comments, and DOCTYPE), whatever media type the URI declares, or when a path or URL ends in .svg. /examples/E031_html_features/ draws an SVG image from a base64 and from a percent-encoded data URI.
CSS Inside SVG
An SVG carries a cascade of its own. A <style> sheet in the image competes with the presentation attributes and the style attribute of each element, in the normal order of precedence.
Selector support covers type, class, id and universal selectors, attribute selectors, the descendant, child, adjacent sibling and general sibling combinators, and the structural pseudo-classes that a single forward pass can decide: :root, :first-child, :nth-child(), and :not() over a simple compound. A selector that would have to look ahead of the element being painted, such as :last-child, :only-child, :nth-last-child() or :empty, matches nothing rather than matching wrongly. This is narrower than the HTML cascade above, which resolves the whole tree before painting.
Tagged and Accessible Output
In a PDF/UA mode the HTML renderer generates the structure tree from the markup: heading levels are mapped, text runs are tagged, alt text becomes /Alt on figures, an image with an empty alt becomes an Artifact, and form fields get /TU descriptions. A lang or xml:lang attribute that differs from the language in effect becomes /Lang on the element’s structure element, with a Span or Div added for an element that has no role of its own; an empty value is written as und. /examples/E015_pdfua/, /examples/E016_pdfua1/, and /examples/E017_pdfua2/ render the same document under each profile.
To build the tree by hand instead, use beginStructElem() and endStructElem(); see /examples/E063_manual_tag_tree/. suspendPdfUaTagging() and resumePdfUaTagging() bracket content that should not enter the tree, and the suspensions nest.
Hyphenation
loadTexHyphenPatterns() and setTexHyphenPatterns() load a TeX pattern dictionary that the HTML renderer then applies to wrapped text. Soft hyphens in the markup are honoured on their own. See /examples/E028_text_hyphenation/ and /examples/E064_custom_hyphenation_dictionary/.
enableZeroWidthBreakPoints(true) lets a line break inside an unbroken run that offers no space to break at, such as a long URL. /examples/E083_text_line_wrap/ renders the same string with the setting on and off.
Line Breaking
Chinese, Japanese, and Korean text has no spaces between words. The engine adds a line break opportunity around each ideographic character and Hangul syllable, following the UAX #14 line start and end rules: a line does not start with closing punctuation, a full stop, a prolonged sound mark, or a small kana, and does not end with an opening bracket. enableIdeographicBreaks(false) turns this off. The same rules apply to addTextCell() and getTextCell().
CSS and markup adjust the break opportunities:
| Markup or CSS | Effect |
|---|---|
word-break: keep-all | No line break between ideographic characters. |
white-space: nowrap | No line breaks. |
word-break: break-all | A line break between any two characters. |
word-break: break-word, overflow-wrap: break-word or anywhere | A line break inside a word that does not fit on a line. |
<wbr>, ​ (zero width space) | A break opportunity, without a hyphen. |
­ (soft hyphen) | A break opportunity, with a hyphen when the line breaks there. |
⁠ (word joiner),  (zero width no-break space) | No line break at that position. |
These controls take no width, and they are kept through the bidirectional reordering, so they also apply in mixed-direction text. /examples/E086_cjk_line_break/ renders each case in a frame, and /examples/E085_bidi_line_break/ covers line breaking in mixed-direction text.
A word wider than the line is broken between two characters, keeping combining marks and joiners with their base character. enableWordBreakFallback(false) lets such a word overflow the line instead. The text fit modes measure the whole word. A line can also break after a hyphen that joins two letters (well-known), as UAX #14 specifies; enableHyphenBreaks(false) turns this off. Both settings are on by default.
text-align aligns lines made of several inline fragments, in both directions: a centered or right-aligned line that wraps before an inline element, the last line of a wrapped fragment together with the inline content that follows it, and right-to-left runs with inline elements. The unjustified lines of right-to-left justified text start at the right. /examples/E091_html_inline_alignment/ frames each case with its center guide and expected result.
text-indent indents the first line of a block; with the each-line keyword it also indents the first line after each forced line break. The value is inherited.
Malformed Markup
tidyHTML(string $html, string $defcss) runs the input through Tidy and returns corrected markup, raising an exception when Tidy cannot parse it. Markup arriving from a CMS or from user input is worth passing through it before rendering.
Related Guides
- /docs/fonts/ : the font setup every rendering call depends on.
- /docs/remote-resources/ : the allowlists that govern images, fonts, and SVG referenced from markup.
- /docs/standards/ : PDF/UA tagging, and the conformance modes that change what the renderer emits.
- /features/ : the full capability index.
- /examples/ : the 14 HTML and CSS examples, and 81 others.