PHPackages                             paperdoc-dev/paperdoc-lib - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [PDF &amp; Document Generation](/categories/documents)
4. /
5. paperdoc-dev/paperdoc-lib

ActiveLibrary[PDF &amp; Document Generation](/categories/documents)

paperdoc-dev/paperdoc-lib
=========================

A zero-dependency PHP library for generating, parsing and converting documents — PDF, DOCX, XLSX, PPTX, HTML, Markdown, CSV and legacy Office formats

v1.0.1(3w ago)1315.4kMITPHPPHP ^8.2

Since Mar 1Pushed 5mo agoCompare

[ Source](https://github.com/paperdoc-dev/paperdoc-lib)[ Packagist](https://packagist.org/packages/paperdoc-dev/paperdoc-lib)[ Docs](https://paperdoc.dev)[ GitHub Sponsors](https://github.com/AkramZerarka)[ RSS](/packages/paperdoc-dev-paperdoc-lib/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (8)Dependencies (6)Versions (26)Used By (0)

Paperdoc Library
================

[](#paperdoc-library)

[![Latest Version](https://camo.githubusercontent.com/05c34c5abe56b9b1631caf4fcf79ec3fc987ff564096e1e0d2c535055600a789/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7061706572646f632d6465762f7061706572646f632d6c69622e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/paperdoc-dev/paperdoc-lib)[![Stable](https://camo.githubusercontent.com/c70215c1f3c5e0d65407123b5e5f739cf3ed3d244ae79ec3a7b9a12fe0a7e90a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f73746162696c6974792d737461626c652d627269676874677265656e3f7374796c653d666c61742d737175617265)](https://github.com/paperdoc-dev/paperdoc-lib/releases)[![PHP Version](https://camo.githubusercontent.com/0137db7634f2bfb58931a1788223a4a9136100008a2ee84e758b20b9c6ec21a0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253545382e322d626c75653f7374796c653d666c61742d737175617265)](https://www.php.net)[![License](https://camo.githubusercontent.com/422db9fd40f5831c765cf6530b6750c081b696bd18d904cf89554df98c676277/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e3f7374796c653d666c61742d737175617265)](LICENSE)[![Tests](https://camo.githubusercontent.com/6d5c5924dd90006b028137004cc3ff582b3d9b780098c73f9566afb0bd0615b3/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7061706572646f632d6465762f7061706572646f632d6c69622f74657374732e796d6c3f6c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/paperdoc-dev/paperdoc-lib/actions)[![PHPStan](https://camo.githubusercontent.com/c719e95cfc6c92181aa0ec445e9c1aef560df1b376b886d3abf5ed3c4c35d91b/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7061706572646f632d6465762f7061706572646f632d6c69622f74657374732e796d6c3f6c6162656c3d7068707374616e267374796c653d666c61742d737175617265)](https://github.com/paperdoc-dev/paperdoc-lib/actions)

> A zero-dependency PHP library for generating, parsing and converting documents — PDF, HTML, CSV, DOCX, XLSX, PPTX, Markdown and more.

---

Features
--------

[](#features)

- **Generate** documents from scratch — PDF, HTML, CSV, DOCX, XLSX, PPTX, Markdown
- **Parse** existing documents into a unified in-memory model
- **Convert** between any supported format in one call
- **Rich document model** — headings, nested lists, bookmarks, code blocks, blockquotes, images, tables, page breaks, metadata
- **Layout &amp; typography** — per-page setup, multi-column layout, backgrounds, text zones, headers/footers, footnotes, vertical alignment, first-line indent, letter spacing, horizontal rules
- **Native rendering** — block elements render cleanly to DOCX, PDF, HTML and Markdown
- **Hyperlinks** — parse from DOCX and round-trip to HTML, Markdown, DOCX and PDF (external URLs and internal anchors)
- **PDF outline &amp; table of contents** — navigation panel and linked TOC across all formats
- **Watermark &amp; rich text styles** — strikethrough, highlight, underline across formats
- **String I/O** — parse and convert content in memory, no file required
- **Typed Format enum** — use strings or `Paperdoc\Enum\Format` everywhere
- **Metadata** — title, author, subject, dates mapped to each format's native properties
- **Batch processing** — open and process multiple files at once
- **Laravel integration** — ServiceProvider and Facade included
- **AI-powered OCR** — post-correction and structured extraction via built-in HTTP providers (OpenAI, Anthropic, Gemini, Ollama)
- **Typed exceptions** — clear error hierarchy for parsers, renderers and LLM calls
- **Zero-dependency** — no third-party packages, no native binaries

---

Requirements
------------

[](#requirements)

DependencyVersionPHP^8.2ext-dom\*ext-mbstring\*ext-zip\*ext-zlib\***Optional**

PackageVersionEnablesilluminate/support^11.0 | ^12.0Laravel integration (ServiceProvider, Facade)ext-curl\*Preferred HTTP transport for the built-in LLM providers (PHP streams fallback otherwise)---

Installation
------------

[](#installation)

```
composer require paperdoc-dev/paperdoc-lib
```

### Laravel auto-discovery

[](#laravel-auto-discovery)

The `PaperdocServiceProvider` and `Paperdoc` facade are registered automatically via Laravel's package auto-discovery.

---

Quick Start
-----------

[](#quick-start)

`DocumentManager` uses **static** methods (`create`, `save`, `open`, …). A document is built from **`Section`** instances: use `addSection($section)` or `addSection()` to append an empty section, or **`openSection()`** when you want a fluent chain (`addParagraph`, `addHeading`, …) on the new section. Bold and other run styles live on **`TextStyle`**. For advanced layouts (custom page size, full-page backgrounds, absolutely-positioned text zones, running headers/footers), see [Page layout](#page-layout-text-zones-headers--footers) below.

### Standalone PHP

[](#standalone-php)

```
use Paperdoc\Support\DocumentManager;
use Paperdoc\Document\Style\TextStyle;

$doc = DocumentManager::create('pdf', 'My Report');

$doc->openSection()
    ->addParagraph('Hello, Paperdoc!', TextStyle::make()->setBold());

DocumentManager::save($doc, 'output/report.pdf');
```

### Laravel (via Facade)

[](#laravel-via-facade)

```
use Paperdoc\Facades\Paperdoc;

// Create
$doc = Paperdoc::create('docx', 'Invoice #1042');
$doc->openSection()->addParagraph('Amount due: $500');
Paperdoc::save($doc, storage_path('invoices/1042.docx'));

// Parse an existing file
$doc = Paperdoc::open('uploads/report.xlsx');

// Convert directly
Paperdoc::convert('report.docx', 'report.pdf', 'pdf');

// Render as string
$html = Paperdoc::renderAs($doc, 'html');

// Batch open
$docs = Paperdoc::openBatch([
    'file1.pdf',
    'file2.docx',
    'file3.xlsx',
]);
```

---

Supported Formats
-----------------

[](#supported-formats)

FormatParseRender/GeneratePDF✅✅HTML✅✅DOCX✅✅XLSX✅✅PPTX✅✅CSV✅✅Markdown✅✅DOC✅✅XLS✅✅PPT✅✅---

Document Model
--------------

[](#document-model)

Every format shares the same strongly-typed in-memory structure:

```
Document (format, title, ?Metadata, metadata[])
└── Section[]
    ├── Heading (level 1-6, runs, ?id)
    ├── Paragraph (TextRun[], ?ParagraphStyle)
    │   └── TextRun (text, ?TextStyle, ?TextLink)
    ├── ListBlock (bullet | ordered, start)
    │   └── ListItem (runs, blocks → nested ListBlock…)
    ├── Blockquote (nested DocumentElement[])
    ├── CodeBlock (code, ?language)
    ├── Bookmark (id) — link target for TextLink anchors
    ├── Table → TableRow[] → TableCell[]
    ├── Image (src | embedded data + mimeType)
    └── PageBreak

```

All block elements implement `Paperdoc\Contracts\BlockElementInterface`. Styles live in `Document/Style/` (`ParagraphStyle`, `TextStyle`, `TableStyle`), links in `Document/Link/TextLink`, typed document properties in `Document/Metadata`.

### Example — build a richly-typed document

[](#example--build-a-richly-typed-document)

```
use Paperdoc\Document\{Document, Section, Metadata, ListBlock};
use Paperdoc\Document\Style\TextStyle;

$doc = Document::make('md', 'Release notes v0.5.0')
    ->setProperties(
        Metadata::make()
            ->setAuthor('Alice')
            ->setKeywords('release, changelog, paperdoc')
            ->setLanguage('en-US')
    );

$section = $doc->openSection();

$section->addElement(\Paperdoc\Document\Heading::make('Getting started', 2, 'intro'));

$section->addBulletList()
    ->addText('Install the library')
    ->addText('Run the quick start')
    ->addText('Read the docs');

$section->addCodeBlock("composer require paperdoc-dev/paperdoc-lib", 'bash');

$section->addBookmark('ready-to-go');

$section->addBlockquote()
    ->addText('You are all set.', TextStyle::make()->setItalic());
```

---

Page layout, text zones, headers &amp; footers
----------------------------------------------

[](#page-layout-text-zones-headers--footers)

Each section can declare its own page geometry through a `PageSetup`value object, and place absolutely-positioned `TextZone` blocks anywhere on the page. Combine several sections (each with its own `PageSetup`) to build documents where every page has a different size and background. Add a global `RunningElement` to the `Document` to draw a header/footer on every page.

### Configure a page

[](#configure-a-page)

```
use Paperdoc\Document\{Image, Section};
use Paperdoc\Document\Style\PageSetup;
use Paperdoc\Enum\PageSize;

$cover = Section::make('cover')->setPageSetup(
    PageSetup::fromSize(PageSize::A4)
        ->setPadding(0)                                // 1, 2, 3 or 4 values (CSS shorthand)
        ->setBackgroundImage(Image::make('cover.jpg')) // full-bleed image
);

$body = Section::make('body')->setPageSetup(
    PageSetup::fromSize(PageSize::A4, PageSetup::ORIENTATION_LANDSCAPE)
        ->setPadding(50)
        ->setBackgroundColor('#F8F5EC')                // solid color
);

$square = Section::make('back-cover')->setPageSetup(
    PageSetup::custom(500, 500)                        // any width × height in pt
        ->setBackgroundImage(Image::make('back.jpg'))
);
```

`Section` exposes shortcut setters (`setPageSize()`, `setPageDimensions()`, `setPagePadding()`, `setPageBackgroundImage()`, `setPageBackgroundColor()`, `setColumnCount()`, `setColumnGap()`) that delegate to a lazily-created `PageSetup`.

Setter / FactoryPurpose`PageSetup::fromSize(PageSize, $orientation = 'portrait')`Use a standard format (A3/A4/A5/A6/Letter/Legal/Tabloid/Executive)`PageSetup::custom($width, $height)`Any dimensions in PDF points`landscape()` / `portrait()`Flip the active orientation`setPadding(...)` (1–4 values)CSS-style shorthand for top/right/bottom/left padding`setBackgroundColor($hex)`Solid full-bleed background color`setBackgroundImage(Image)`Full-bleed image (on-disk or `Image::fromData()`)`setBackgroundSize(string)``cover` (default), `contain`, `auto`, `stretch`, or any CSS string`setBackgroundPosition(string)`CSS string, default `'center center'``setBackgroundRepeat(string)`CSS string, default `'no-repeat'``setColumnCount(int)` / `setColumnGap(float)`Multi-column body layout (PDF, HTML, DOCX)### Multi-column layout

[](#multi-column-layout)

```
$section->setPageSetup(
    PageSetup::fromSize(PageSize::A4)
        ->setColumnCount(2)
        ->setColumnGap(18.0) // points
);
```

Text flows down each column, then onto the next page. Tables and full-width images still use the full content width.

### Footnotes

[](#footnotes)

Attach a footnote to any `TextRun`. Markers (`[n]`) appear inline. PDF places the note at the bottom of the page; HTML/Markdown list notes at section end; DOCX appends a notes block at the end of the document.

```
use Paperdoc\Document\Footnote;

$paragraph->addRun(TextRun::make(
    'See the specification',
    null,
    null,
    Footnote::make('ISO 32000-1:2008, §7.5'),
));
```

### Background image fit

[](#background-image-fit)

PDF and HTML share the same modes. `cover` and `auto` clip overflow.

```
use Paperdoc\Document\Style\PageSetup;

$page->setPageSetup(
    PageSetup::fromSize(PageSize::A4)
        ->setBackgroundImage(Image::make('hero.jpg'))
        ->setBackgroundSize(PageSetup::BG_SIZE_COVER)   // default
);
```

`BG_SIZE_*` constantCSS equivalentBehaviour`BG_SIZE_COVER``cover`Fills the page, preserves aspect ratio, **overflow is clipped** (default)`BG_SIZE_CONTAIN``contain`Fits inside the page, preserves aspect ratio (may leave empty bands)`BG_SIZE_AUTO``auto`Image at its natural size, centred, clipped if larger than the page`BG_SIZE_STRETCH``100% 100%`Stretches to fill the page; aspect ratio is **not** preserved (legacy mode)Any other CSS-valid string (`'50% auto'`, `'300pt 200pt'`, …) is accepted as-is in HTML output.

### Place text precisely with `TextZone`

[](#place-text-precisely-with-textzone)

```
use Paperdoc\Document\TextZone;
use Paperdoc\Document\Style\{ParagraphStyle, TextStyle};
use Paperdoc\Enum\Alignment;

$cover->addTextZone(x: 40, y: 40, width: 515, height: 90)
    ->setBackgroundColor('#0B1437')
    ->setBorder('#FFFFFF', 0.8)
    ->setPadding(16)
    ->addText(
        'Paperdoc — Cover title',
        TextStyle::make()->setBold()->setFontSize(20)->setColor('#FFFFFF'),
        ParagraphStyle::make()->setAlignment(Alignment::LEFT),
    );

// Long lorem with the ellipsis strategy: text is truncated to fit
// exactly the visible height and the last visible line ends with "…".
$cover->addTextZone(x: 40, y: 160, width: 250, height: 260)
    ->setPadding(12)
    ->setBackgroundColor('#FFFFFF')
    ->setBorder('#1F2937', 0.5)
    ->setOverflow(TextZone::OVERFLOW_ELLIPSIS)
    ->addText($veryLongText,
        TextStyle::make()->setFontSize(10)->setColor('#111827'),
        ParagraphStyle::make()->setLineSpacing(1.25),
    );
```

Overflow strategyBehaviour`TextZone::OVERFLOW_CLIP`(Default) Silently truncates content that doesn't fit`TextZone::OVERFLOW_ELLIPSIS`Truncates and ends the last visible line with `…` (PDF: native; HTML: pseudo-element)`TextZone::OVERFLOW_VISIBLE`No clipping — content may flow outside the box (kept for parity with CSS)Coordinates use the top-left convention (`x=0, y=0` is the top-left of the page) for both PDF and HTML — the `PdfRenderer` flips to PDF's bottom-left origin internally.

#### Per-paragraph alignment

[](#per-paragraph-alignment)

Each paragraph of a `TextZone` carries its own `ParagraphStyle`, so you can mix several alignments in the same zone (centred title, justified body, right-aligned signature, …):

```
use Paperdoc\Enum\Alignment;

$zone = $page->addTextZone(40, 80, 515, 380)
    ->setBackgroundColor('#FFFFFF')
    ->setOverflow(TextZone::OVERFLOW_ELLIPSIS);

$zone->addText('Quarterly report',
    TextStyle::make()->setBold()->setFontSize(18),
    ParagraphStyle::make()->setAlignment(Alignment::CENTER));

$zone->addText($longLorem,
    TextStyle::make()->setFontSize(11),
    ParagraphStyle::make()->setAlignment(Alignment::JUSTIFY)->setLineSpacing(1.3));

$zone->addText('— J. Doe',
    TextStyle::make()->setItalic(),
    ParagraphStyle::make()->setAlignment(Alignment::RIGHT));
```

In the PDF, justification is implemented with the native PDF word-spacing operator (`Tw`); the last line of a paragraph is intentionally left-aligned to avoid stretched short lines.

### Document-wide headers and footers

[](#document-wide-headers-and-footers)

```
use Paperdoc\Document\Style\{RunningElement, TextStyle};
use Paperdoc\Enum\Alignment;
use Paperdoc\Support\DocumentManager;

$doc = DocumentManager::create('pdf', 'Quarterly report');

$doc->setHeader(
    RunningElement::make('{title}')
        ->setAlignment(Alignment::LEFT)
        ->setStyle(TextStyle::make()->setFontSize(9)->setItalic()->setColor('#FFFFFF'))
);

$doc->setFooter(
    RunningElement::make('Page {page} / {pages}  ·  {date}')
        ->setAlignment(Alignment::CENTER)
        ->setStyle(TextStyle::make()->setFontSize(9)->setColor('#FFFFFF'))
);
```

Supported placeholders in the template: `{page}` (1-indexed current page), `{pages}` (total pages), `{title}` (the document title), `{date}` (`Y-m-d`) and `{datetime}` (`Y-m-d H:i`). The renderer resolves them per page so you don't need to update the template between pages.

Since **v1.0.0**, the **DOCX renderer** honours document-level running elements too: it writes native `header1.xml` / `footer1.xml` parts and emits `{page}` / `{pages}` as live Word `PAGE` / `NUMPAGES` fields, so page numbers stay correct when the document is edited in Word. (Per-section overrides below remain PDF/HTML-only — Word's section model differs.)

The HTML renderer adds a translucent `rgba(255, 255, 255, 0.85)`backdrop with a `backdrop-filter: blur(2px)` behind the running elements so they remain legible on top of any background image. The library does not automatically reserve vertical space for the header/footer — keep that in mind when positioning a `TextZone` close to a page edge.

### Per-section header / footer override

[](#per-section-header--footer-override)

A document-level header/footer applies uniformly to every page by default. Often that's not what you want — a cover page should NOT carry the page-number footer (it would either disappear under the artwork or fight with the imagery for legibility), and a colophon on the last page might want a different label. Sections can override or suppress the document-level running elements:

```
use Paperdoc\Document\Style\RunningElement;

// Document-level: every page gets this footer by default.
$doc->setFooter(RunningElement::make('Page {page} / {pages}'));

// Cover page: NO footer at all.
$cover = $doc->openSection('cover')->hideFooter();

// Body pages: inherit the document footer.
$body = $doc->openSection('body');
// (nothing to do — automatic fallback)

// Colophon: per-section override.
$colophon = $doc->openSection('colophon')
    ->setFooter(RunningElement::make('— Fin —'));
```

Resolution rule — for every page, the renderer picks (in order) :

1. If `Section::hideHeader()` / `hideFooter()` was called → no header/footer is drawn.
2. If `Section::setHeader()` / `setFooter()` was called with a non-null element → that element is drawn.
3. Otherwise → the document-level header/footer is drawn (if any).

Both PDF and HTML renderers honour this resolution.

### Vertical alignment

[](#vertical-alignment)

By default the content of a section flows from the top padding downwards. For pages that should breathe vertically — chapter openers, colophons, frontispieces, single-paragraph "blank" pages — you can centre or bottom-anchor the content :

```
use Paperdoc\Enum\VerticalAlignment;

$opener = $doc->openSection('chapter-1-opener')
    ->setPageSize(PageSize::A5)
    ->setVerticalAlignment(VerticalAlignment::CENTER);
$opener->addText('CHAPITRE 1', TextStyle::make()->setFontSize(10)->setColor('#888'));
$opener->addText('Le Signal sur le Balcon', TextStyle::make()->setFontSize(28)->setBold());

$colophon = $doc->openSection('colophon')
    ->setVerticalAlignment(VerticalAlignment::BOTTOM);
$colophon->addText('© 2026 — All rights reserved.');
```

Implementation notes :

- The PDF renderer captures the section's content slice, measures its rendered height, then wraps it in a native PDF `q ... 1 0 0 1 0 dy cm ... Q` translation block — no overhead, no pre-render measurement pass.
- The HTML renderer applies the same semantics via flexbox (`display:flex; justify-content:center` for `CENTER`, `flex-end`for `BOTTOM`).
- Sections that overflow onto a second page automatically fall back to TOP alignment to avoid a stale CTM bleeding across pages. If you need centring on a section with lots of content, reduce its content first.

### Per-side padding shortcuts

[](#per-side-padding-shortcuts)

`Section::setPagePadding(...$values)` already accepts CSS-shorthand 1-/2-/3-/4-value forms. When only ONE side needs tweaking — typical for a frontispiece title that should sit ~110pt from the top — the new per-side shortcuts are clearer :

```
$frontispiece = $doc->openSection('frontispiece')
    ->setPageSize(PageSize::A5)
    ->setPagePaddingTop(110.0)
    ->setPagePaddingBottom(60.0);
$frontispiece->addText('La Lumière des Autres', TextStyle::make()->setFontSize(32)->setBold());
```

Available : `setPagePaddingTop()`, `setPagePaddingRight()`, `setPagePaddingBottom()`, `setPagePaddingLeft()`. They all return `$this` for chaining.

---

First-line indent and letter-spacing
------------------------------------

[](#first-line-indent-and-letter-spacing)

Two style-level additions that previously had to be hacked at the application layer :

### Paragraph first-line indent

[](#paragraph-first-line-indent)

`ParagraphStyle::setFirstLineIndent(float $points)` mirrors the CSS `text-indent` property : only the first line of the paragraph starts further to the right. Negative values produce a hanging indent (first line jutting OUT to the left of the block — a typographic device used in lists or dictionary entries).

```
use Paperdoc\Document\Style\ParagraphStyle;

$body = ParagraphStyle::make()
    ->setLineSpacing(1.4)
    ->setFirstLineIndent(18.0); // ~6mm — classic book body indent

$paragraph->setStyle($body);
```

The PDF wrap engine sees a tighter budget for the first line so wrapping accounts for the indent correctly. The HTML renderer emits `text-indent: Xpt` on the paragraph's inline style.

### Run letter-spacing

[](#run-letter-spacing)

`TextStyle::setLetterSpacing(float $points)` opens out a run of text by the requested number of points between every pair of adjacent glyphs. Common uses : an opened-out section heading (`setLetterSpacing(1.5)`), or marking an acronym in small-caps style. Negative values pull glyphs closer.

```
use Paperdoc\Document\Style\TextStyle;

$eyebrow = TextStyle::make()
    ->setFontSize(10)
    ->setBold()
    ->setColor('#888')
    ->setLetterSpacing(2.0); // wide tracking for the eyebrow

$paragraph->addRun(new TextRun('CHAPITRE PREMIER', $eyebrow));
```

PDF emits the native `Tc` operator and resets to 0 after the run, so copy-paste from the rendered file gives back the original (un-spaced) text — a property the previous "insert thin spaces between every glyph" workarounds did not have. HTML emits `letter-spacing: Xpt`. `measureTextWidth()` and `wrapText()`correctly account for letter-spacing.

---

Horizontal rule
---------------

[](#horizontal-rule)

A first-class block element for visual separators :

```
use Paperdoc\Document\HorizontalRule;
use Paperdoc\Enum\Alignment;

// Quick: full-width default-styled rule.
$section->addRule();

// Customised: 50%-width centred grey hairline.
$section->addRule()
    ->setWidth('50%')
    ->setThickness(0.75)
    ->setColor('#aaaaaa')
    ->setAlignment(Alignment::CENTER)
    ->setMargins(8.0, 12.0);

// Pure absolute pt width.
$section->addRule()->setWidth(140.0)->setColor('#1F3763');
```

PropertyTypeDefaultNotes`width``string|float``'100%'`Either a CSS-style percentage (`'50%'`) or absolute pt.`thickness``float` (pt)`0.5`Word-style "thin rule" by default.`color``string` hex`'#999999'`CSS hex (`#rgb` or `#rrggbb`).`alignment``Alignment``CENTER`LEFT / CENTER / RIGHT for partial-width rules.`marginTop``float` (pt)`6.0`Vertical breathing space above the rule.`marginBottom``float` (pt)`6.0`Below.Renderers :

- **PDF** : a stroked horizontal line, drawn with the requested thickness and stroke colour.
- **HTML** : `` with inline CSS (`border-top:Wpt solid C ; width:X ; margin:T 0 B`).
- **Markdown** : `---` thematic break (CommonMark).
- **DOCX** : the canonical Word "horizontal line" — an empty paragraph carrying a `` with the requested colour and thickness in eighths-of-a-point.

---

Rendering
---------

[](#rendering)

Every element of the document model is natively rendered by **all four** core renderers — no element is silently dropped, every output is a valid file format.

ElementDOCXPDFHTMLMarkdown`Heading` (1–6)`` + bookmark anchortyped font sizes (24/20/16/14/13/12 pt) + navy``…`` with `id``#`…`######`, optional `{#id}``Paragraph``` + run stylingwrapped text + inline run styles`` + inline ``plain text + emphasis`ListBlock``` + `word/numbering.xml`, nested ```•` / `1.` markers, depth-based indent`` / ``, nested`-` / `1.`, two-space indent`Blockquote``` + indentindented italic muted-grey`` (nested children)`> ` prefixed lines`CodeBlock``` + Consolas + ``Courier, dedicated spacing``fenced ````lang` block`Bookmark``` / ``named destination — internal link target *(v1.0.0)*``inline ```TextLink``` (external rels + `w:anchor` + tooltip)clickable Link annotation (URI / GoTo) *(v1.0.0)*`` with safe `target/rel`safe `[label](url "title")``Image``` + `word/media/imageN.ext` relXObject DCT (JPEG/PNG/GIF via GD re-encode)`` or `data:` URI`![alt](path)` or `data:` URI`Table``` with header rows + `gridSpan`drawn cells with header bg`` + striped rows`|` rows`PageBreak````newPage()``.page-break` dividerblank line`HorizontalRule`bordered empty `` (``)stroked PDF line`` with inline CSS`---` thematic break`Metadata``docProps/core.xml`full `/Info` dict *(v1.0.0)*`` meta + `lang` *(v1.0.0)*YAML frontmatter *(v1.0.0)*`Heading` → outline(outline levels via `pStyle`)`/Outlines` bookmarks panel *(v1.0.0)*`id` anchors`{#id}` anchorsBoth `Image::make($path)` (on-disk) and `Image::fromData($bytes, $mimeType)` (in-memory) are accepted everywhere; HTML and Markdown automatically inline embedded images as `data:` URIs, DOCX writes them to `word/media/`, and PDF embeds them as DCT XObjects (re-encoding GIF/PNG/WebP through GD when needed).

---

Typed Exceptions
----------------

[](#typed-exceptions)

All library errors extend a single base so consumers can catch them uniformly:

ExceptionThrown when…`Paperdoc\Exceptions\PaperdocException`Base (extends `RuntimeException`)`Paperdoc\Exceptions\ParserException`A parser cannot read/decode a file (`::forFile($path, $reason, $previous)`)`Paperdoc\Exceptions\RendererException`A renderer cannot serialise a document (`::forFormat($fmt, $reason, $previous)`)`Paperdoc\Exceptions\UnsupportedFormatException`Unknown format or extension (`::forFormat()` / `::forExtension()`)`Paperdoc\Exceptions\InvalidDocumentException`Document is used in an invalid state (e.g. invalid heading level)`Paperdoc\Exceptions\LlmException`An LLM provider request fails (transport, HTTP error, unusable response)```
use Paperdoc\Exceptions\PaperdocException;

try {
    $doc = Paperdoc::open('report.docx');
} catch (PaperdocException $e) {
    // Any Paperdoc error ends up here.
}
```

---

Hyperlinks
----------

[](#hyperlinks)

Every `TextRun` can carry an optional `Paperdoc\Document\Link\TextLink`. Links survive the full round-trip: they're parsed from DOCX (``) and rendered natively by the HTML, Markdown, DOCX **and PDF** renderers.

### Clickable links in PDF (v1.0.0)

[](#clickable-links-in-pdf-v100)

The native PDF renderer emits real Link annotations:

- **External URLs** become `/A >` actions — clicking opens the browser.
- **Internal anchors** (`TextLink::make('', 'my-anchor')`) become GoTo destinations pointing at the page and position of the matching `Bookmark` or `Heading` id. Forward references work: a table of contents on page 1 can target a heading on page 12.
- Wrapped links get one clickable rectangle per line; links crossing an automatic page break attach each rectangle to its own page. Links inside `TextZone` blocks are clickable too.
- A link to an anchor that is never declared renders as styled text without an annotation (nothing breaks).

```
$section->addText('See chapter 2', null, TextLink::make('', 'chap-2'));
// … later, possibly pages away:
$section->addElement(Heading::make('Chapter 2', 1, 'chap-2'));
```

Headings also feed the **PDF outline** (the "bookmarks" side panel): every `Heading` becomes a navigable outline entry, nested by level, and the document opens with the panel visible (`/PageMode /UseOutlines`).

---

Table of contents, watermark &amp; rich styles (v1.0.0)
-------------------------------------------------------

[](#table-of-contents-watermark--rich-styles-v100)

```
use Paperdoc\Document\Style\{TextStyle, Watermark};
use Paperdoc\Enum\Format;
use Paperdoc\Support\DocumentManager;

$doc = DocumentManager::create(Format::PDF, 'Rapport');
$doc->setWatermark(Watermark::make('CONFIDENTIEL')->setOpacity(0.2)->setAngle(-45));

$section = $doc->openSection();
$section->addTableOfContents(maxLevel: 3, title: 'Sommaire'); // clickable in PDF/HTML/MD, native field in DOCX
$section->addElement(\Paperdoc\Document\Heading::make('Introduction', 1));

$section->addText('ancien prix', TextStyle::make()->setStrikethrough());
$section->addText('à retenir', TextStyle::make()->setHighlight('#FFF3B0'));
```

```
// Parse & convert without touching the filesystem:
$html = DocumentManager::convertString("# Titre\n\nCorps.", 'md', 'html');
$doc  = DocumentManager::openString($csvContent, Format::CSV);
```

### Add a link programmatically

[](#add-a-link-programmatically)

```
use Paperdoc\Support\DocumentManager;
use Paperdoc\Document\Section;
use Paperdoc\Document\Link\TextLink;

$doc = DocumentManager::create('md', 'Release notes');
$section = Section::make('main');

$section->addText(
    'See the full changelog',
    null,
    TextLink::make('https://github.com/paperdoc-dev/paperdoc-lib/blob/main/CHANGELOG.md', '', 'Changelog')
);

$doc->addSection($section);
echo DocumentManager::renderAs($doc, 'md');
// [See the full changelog](https://github.com/paperdoc-dev/paperdoc-lib/blob/main/CHANGELOG.md "Changelog")
```

### Supported link flavours

[](#supported-link-flavours)

KindConstructionHTML outputMarkdown outputExternal URL`TextLink::make('https://x.com')``…``[label](url)`Internal anchor`TextLink::make('', 'section-2')``…``[label](#section-2)`URL + fragment`TextLink::make('https://x.com', 'sect-2')``…``[label](url#sect-2)`Tooltip / title`TextLink::make('https://x.com', '', 'Open site')``…``[label](url "Open site")`External schemes (`http`, `https`, `mailto`, `tel`, `ftp`) automatically get `target="_blank" rel="noopener noreferrer"` in HTML to prevent tabnabbing. Run styling (bold, italic, color, font) is preserved when combined with a link.

### Convert DOCX with hyperlinks to Markdown

[](#convert-docx-with-hyperlinks-to-markdown)

```
use Paperdoc\Support\DocumentManager;

//  elements are parsed and attached to their TextRun
$doc = DocumentManager::open('report.docx');

// Links are rendered as safe [label](url) — labels with ] and URLs with spaces
// or parentheses are escaped/wrapped automatically.
file_put_contents('report.md', DocumentManager::renderAs($doc, 'md'));
```

---

Configuration
-------------

[](#configuration)

Publish the config (Laravel):

```
php artisan vendor:publish --tag=paperdoc-config
```

This creates `config/paperdoc.php` where you can set the default format, text styles, storage paths, and AI/OCR settings.

---

Testing
-------

[](#testing)

```
composer test
# or
./vendor/bin/phpunit
```

Integration tests live in `tests/Integration/`, unit tests in `tests/Unit/`.

Static Analysis
---------------

[](#static-analysis)

```
composer phpstan
```

PHPStan runs at `level: max` on `src/`. The initial adoption uses `phpstan-baseline.neon` to freeze the current legacy debt so new type regressions fail CI immediately.

---

Architecture
------------

[](#architecture)

```
src/
├── Concerns/          # Shared traits
├── Console/           # Artisan commands
├── Contracts/         # DocumentInterface, ParserInterface, BlockElementInterface…
├── Document/          # Core model (Document, Section, Paragraph, Heading, ListBlock, Bookmark, CodeBlock, Blockquote, Metadata…)
├── Enum/              # Format enums
├── Exceptions/        # PaperdocException + typed exceptions
├── Facades/           # Laravel Facade
├── Factory/           # Document/Parser factories
├── Llm/               # Native AI/LLM layer (built-in OpenAI/Anthropic/Gemini/Ollama HTTP providers)
├── Ocr/               # OCR integration
├── Parsers/           # Format-specific parsers
├── Renderers/         # Format-specific renderers
├── Support/           # DocumentManager and helpers
└── PaperdocServiceProvider.php

```

---

Contributing
------------

[](#contributing)

We welcome contributions! Please read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.

### Contributors

[](#contributors)

Thanks to everyone who has contributed to **paperdoc-lib**.

[ ![Contributors](https://camo.githubusercontent.com/348b495746261b86d63b4c102990260b4f3177c16dd87dc71a4820c8db7b5631/68747470733a2f2f636f6e747269622e726f636b732f696d6167653f7265706f3d7061706572646f632d6465762f7061706572646f632d6c6962)](https://github.com/paperdoc-dev/paperdoc-lib/graphs/contributors)Made with [contrib.rocks](https://contrib.rocks). Detailed credits: [CONTRIBUTORS.md](CONTRIBUTORS.md).

Want to be listed? Open a PR that gets merged into `main` — see [CONTRIBUTING.md](CONTRIBUTING.md).

---

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md) for release history.

---

License
-------

[](#license)

Paperdoc Library is released under the **[MIT License](LICENSE)** — free to use, modify and distribute, commercial or not.

© Paperdoc — [paperdoc.dev](https://paperdoc.dev)

###  Health Score

50

—

FairBetter than 95% of packages

Maintenance82

Actively maintained with recent releases

Popularity37

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity57

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 93.9% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~6 days

Recently: every ~18 days

Total

24

Last Release

23d ago

Major Versions

v0.8.3 → v1.0.02026-07-16

### Community

Maintainers

![](https://www.gravatar.com/avatar/46071ff41a6620ef4019427bf6338efb59394b4f6342e9879cef0541df4cc878?d=identicon)[AkramZerarka](/maintainers/AkramZerarka)

---

Top Contributors

[![AkramZerarka](https://avatars.githubusercontent.com/u/14928910?v=4)](https://github.com/AkramZerarka "AkramZerarka (31 commits)")[![olivM](https://avatars.githubusercontent.com/u/855?v=4)](https://github.com/olivM "olivM (2 commits)")

---

Tags

laravelpdfparserhtmlxlsxcsvconvertermarkdowndocxwriterdocumentpptx

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/paperdoc-dev-paperdoc-lib/health.svg)

```
[![Health](https://phpackages.com/badges/paperdoc-dev-paperdoc-lib/health.svg)](https://phpackages.com/packages/paperdoc-dev-paperdoc-lib)
```

###  Alternatives

[gotenberg/gotenberg-php

A PHP client for interacting with Gotenberg, a developer-friendly API for converting numerous document formats into PDF files, and more!

3906.6M32](/packages/gotenberg-gotenberg-php)[faisalman/simple-excel-php

Easily parse / convert / write between Microsoft Excel XML / CSV / TSV / HTML / JSON / etc formats

557614.2k1](/packages/faisalman-simple-excel-php)[nilgems/laravel-textract

A Laravel package to extract text from files like DOC, XL, Image, Pdf and more. I've developed this package by inspiring "npm textract".

196.0k](/packages/nilgems-laravel-textract)[mnvx/lowrapper

PHP wrapper over LibreOffice converter

129205.2k](/packages/mnvx-lowrapper)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
