Search by

pandoc-php / pandoc

snorky22

A native PHP 8.4 port of the Pandoc document converter.

7.5.0 2026-10-27 12:29 UTC

This package is auto-updated.

Last update: 2026-10-27 12:30:04 UTC


README

A native PHP 8.4 port of the Pandoc document converter. This library converts documents between formats (Word .docx, Excel .xlsx, PowerPoint .pptx, HTML .html, Markdown .md, Jupyter .ipynb, BibTeX .bib → LaTeX) and exports LaTeX projects to EPUB 3 (see Converting LaTeX to EPUB) and OpenDocument text (.odt) (see Converting LaTeX to ODT), and reads .odt files back into LaTeX (see Converting ODT to LaTeX) without requiring the system-level Pandoc binary.

Features

  • Native PHP 8.4: Uses readonly classes, Enums, and property hooks.
  • AST-Centric Architecture: Mirrors Pandoc's Abstract Syntax Tree for robust conversions.
  • Modular Reader System: Factory pattern and ReaderInterface for easy format expansion.
  • Deep Docx Parsing: Paragraphs, headers, tables, lists, images, bold/italic/underline/strikeout, superscript/subscript, text and background colors, hyperlinks (external \href/\url, internal \hyperref), footnotes and endnotes (\footnote), math equations (Office Math/OMML → LaTeX, inline $…$ and display \[…\]), automatic run-merging (consecutive runs with identical styling are collapsed into one command), and black-color suppression (spurious \textcolor[HTML]{000000} commands are dropped).
  • Excel (XLSX): All sheets as booktabs tables, shared strings, bold/italic, embedded images, chart extraction (JSON metadata + CSV data for Chart.js), per-sheet CSV export with locale-aware separators, and a metadata.json summary of document locale.
  • PowerPoint (PPTX): Each slide becomes a slide environment, all slides wrapped in a slider environment. Images, embedded videos (\begin{video}...\end{video}), and audio (\begin{audio}...\end{audio}) extracted to MediaBag.
  • LaTeX Generation: Standalone documents or body fragments.
  • Automatic ZIP Bundling: When a document contains images or chart data, output is a .zip with the .tex and all media files in the same directory. Plain .tex otherwise.
  • Full UTF-8: End-to-end UTF-8, supporting CJK, Cyrillic, Arabic, Thai, and all Latin-extended scripts.
  • LaTeX → EPUB 3: Reads vanilla LaTeX (LatexReader) and writes a valid EPUB 3 with MathML math, numbered sections/figures/tables/theorems with working cross-references, footnotes, bibliography, algorithms, embedded images and an optional cover (see below).
  • LaTeX → ODT: The same reader feeds an OpenDocument writer for LibreOffice and other ODF applications: real headings, footnotes, tables, figures with captions, and editable equations (embedded MathML). Pure PHP, no LibreOffice needed on the server.
  • ODT → LaTeX: Reads OpenDocument text from LibreOffice (or from this library) into standard LaTeX that compiles with pdflatex and uses no exotic packages: headings, lists, footnotes, tables, figures with captions, cross-references and citations, and equations converted from MathML. The result can be fed straight back into the LaTeX → EPUB/ODT converters.
  • Minimal Dependencies: Pure PHP 8.4+ (ext-dom, ext-zip) plus latex2mathml/latex2mathml-php for MathML. ext-imagick is optional.

Installation

Requires PHP 8.4 or higher.

composer require pandoc-php/pandoc

Basic Usage

Converting a Word Document to LaTeX

use Pandoc\Reader\DocxReader;
use Pandoc\Writer\LatexWriter;

$reader = new DocxReader();
$writer = new LatexWriter();

$doc   = $reader->read('document.docx');
$latex = $writer->write($doc, standalone: true);

file_put_contents('document.tex', $latex);

Word equations (Office Math/OMML, Insert → Equation) are converted to LaTeX automatically — no extra setup required. An inline equation becomes a Math AST node of type InlineMath ($…$), and a standalone equation paragraph becomes DisplayMath (\[…\]):

// document.docx contains the equation: y = ∫₀ᵗ x(s) ds
$latex = $writer->write($reader->read('document.docx'), standalone: false);
// → "\[y=\int_{0}^{t}{x\left(s\right)ds}\]"

Superscripts/subscripts, fractions, radicals, n-ary operators (∫, ∑, ∏, ⋃, …), delimiters, functions (sin, log, lim, …), matrices, and common Greek letters/operator symbols are all supported. See SUPPORTED_STRUCTURES.md for the full list and known limitations.

Converting Markdown to a LaTeX Fragment

use Pandoc\Reader\MarkdownReader;
use Pandoc\Writer\LatexWriter;

$reader   = new MarkdownReader();
$writer   = new LatexWriter();
$markdown = "# Hello World\nThis is a paragraph.";
$doc      = $reader->read($markdown);

// standalone: false → body only, no \documentclass preamble
$fragment = $writer->write($doc, standalone: false);

Converting HTML to LaTeX

use Pandoc\Reader\HtmlReader;
use Pandoc\Writer\LatexWriter;

$reader = new HtmlReader();
$writer = new LatexWriter();

$doc   = $reader->read("<h1>Hello</h1><p>World</p>");
$latex = $writer->write($doc);

Converting an Excel Spreadsheet to LaTeX

use Pandoc\Reader\XlsxReader;
use Pandoc\Writer\LatexWriter;

$reader = new XlsxReader();
$writer = new LatexWriter();

$doc   = $reader->read('spreadsheet.xlsx');
$latex = $writer->write($doc);

Each sheet produces a level-2 header followed by a booktabs table. If the spreadsheet contains embedded images or charts, use the ZIP output pattern below.

Note: Only .xlsx (OOXML) is supported. Legacy .xls files must be converted first (e.g. via LibreOffice).

Chart extraction: Charts are exported as two companion files added to the MediaBag:

chart1.json — Chart.js-ready metadata:

{
  "type": "bar",
  "title": "Sales by Quarter",
  "dataFile": "chart1.csv",
  "options": {
    "indexAxis": "x",
    "scales": {
      "x": { "title": { "display": true, "text": "Quarter" }, "stacked": false },
      "y": { "title": { "display": true, "text": "Revenue" }, "stacked": false }
    }
  },
  "series": [
    { "label": "Product A" },
    { "label": "Product B" }
  ]
}

chart1.csv — the data (categories + one column per series):

Category,Product A,Product B
Q1,120,85
Q2,135,90
Q3,128,95
Q4,145,110

A comment marker is inserted in the LaTeX at the chart's position:

% [pandoc-chart: chart1.json]

Your app reads the marker → loads the JSON → finds dataFile → loads the CSV → renders with Chart.js.

Per-sheet CSV export: Each worksheet is also exported as a standalone CSV file (e.g. sheet-Sales.csv) added to the MediaBag. Trailing empty rows and columns are stripped automatically.

Locale detection: The reader inspects docProps/core.xml for a <dc:language> tag and selects separators accordingly:

Language group Decimal sep. Thousands sep. Column delim.
en, ja, zh, pt-BR, … . , ,
fr, de, it, es, nl, pl, ru, … , . ;

When no language tag is present the file falls back to en-US conventions.

metadata.json: Always added to the MediaBag alongside the CSVs:

{
    "language": "fr-FR",
    "decimalSeparator": ",",
    "thousandsSeparator": ".",
    "columnDelimiter": ";",
    "quoteCharacter": "\"",
    "sheets": ["Sheet1", "Sheet2"]
}

Utility script: export_xlsx_media.php converts any .xlsx file to a ZIP containing its CSVs and metadata.json:

php export_xlsx_media.php spreadsheet.xlsx output.zip

Converting a PowerPoint Presentation to LaTeX

use Pandoc\Reader\PptxReader;
use Pandoc\Writer\LatexWriter;

$reader = new PptxReader();
$writer = new LatexWriter();

$doc   = $reader->read('presentation.pptx');
$latex = $writer->write($doc, standalone: true);

Each slide is wrapped in a slide environment (with the slide title as argument), and all slides are enclosed in a slider environment:

\begin{slider}

\begin{slide}{Slide Title}
Paragraph content here.
\end{slide}

\begin{slide}{Second Slide}
More content.
\end{slide}

\end{slider}

These are custom environments — define them in your LaTeX preamble to control rendering. All images (including slide master/template graphics) are extracted into the MediaBag.

Embedded videos are exported as a video environment:

\begin{video}
\url{media1.mp4}
\type{mp4}
\end{video}

Embedded audio is exported as an audio environment:

\begin{audio}
\url{recording.mp3}
\end{audio}

All media files (images, video, audio) are included in the ZIP output alongside the .tex.

Converting BibTeX to LaTeX

use Pandoc\Reader\BibtexReader;
use Pandoc\Writer\LatexWriter;

$reader  = new BibtexReader();
$writer  = new LatexWriter();

$content = file_get_contents('references.bib');
$doc     = $reader->read($content);

// standalone: false → bibliography block only, no \documentclass preamble
$fragment = $writer->write($doc, standalone: false);
file_put_contents('references.tex', $fragment);

The output is a self-contained thebibliography block:

\begin{thebibliography}{99}

\bibitem{Smith2020}
\emph{A Great Title}, John Smith, Journal of Examples, 2020

\end{thebibliography}
  • HTTP/HTTPS URLs are automatically wrapped in \url{…}.
  • The title, booktitle, journal, series, and publisher fields are italicised with \emph{…}.
  • The keywords and abstract fields are silently omitted from the output.
  • BibTeX output is always produced as a fragment (standalone: false); the web interface enforces this automatically.

Converting Jupyter Notebooks to LaTeX

use Pandoc\Reader\IpynbReader;
use Pandoc\Writer\LatexWriter;

$reader = new IpynbReader();
$writer = new LatexWriter();

$json  = file_get_contents('notebook.ipynb');
$doc   = $reader->read($json);
$latex = $writer->write($doc);

Converting LaTeX to EPUB

Turns a LaTeX source (an article, report or book using common packages) into an EPUB 3 file with real MathML equations, numbered sections, figures, tables and theorems, working cross-references (Fig. 3, Section A.1), footnotes, a bibliography and embedded images.

Requirements: PHP 8.4 with ext-zip and ext-dom. latex2mathml-php (MathML) is installed automatically by Composer.

From PHP

require 'vendor/autoload.php';

use Pandoc\Reader\LatexReader;
use Pandoc\Writer\EpubWriter;

// Pass the path of the .tex file: images, \input files and \cover are looked up next to it.
$doc = (new LatexReader())->read('/projects/paper/paper.tex');

(new EpubWriter())->writeToFile($doc, '/projects/paper/paper.epub');

To send the file from a web script instead of saving it:

$epub = (new EpubWriter())->write($doc);          // binary string

header('Content-Type: application/epub+zip');
header('Content-Disposition: attachment; filename="paper.epub"');
header('Content-Length: ' . strlen($epub));
echo $epub;

If you give read() a string of LaTeX instead of a path, there is no folder to search; pass the image folder to the writer: new EpubWriter(['resourcePath' => ['/projects/paper']]).

From the command line

php latex2epub.php paper.tex                       # writes paper.epub next to it
php latex2epub.php paper.tex out.epub --cover=cover.png --split=2 --toc-depth=2

The project must be unzipped first (the reader does not open archives), with the images beside the .tex file or in a \graphicspath folder. The web upload form in web/ does not offer EPUB yet.

Preparing the LaTeX source

  • Metadata: \title{…}, \author{A \and B}, \date{…} and a babel language (\usepackage[french]{babel}) fill the EPUB metadata and title page.
  • Cover: add \cover{file.png} to the preamble (extension optional, resolved like \includegraphics). The image is shown centred at the top of the title page and declared as the EPUB cover. No \cover, no cover. If the same source is also compiled with LaTeX, add \providecommand{\cover}[1]{} so LaTeX ignores the command.
  • Bibliography: BibTeX is not run and \bibliography{…} is ignored. Put the generated thebibliography environment (the contents of the .bbl file) in the source; \cite commands then become links to the entries.
  • Images: PNG, JPEG, GIF, SVG and WebP are embedded unchanged. PDF/EPS figures cannot be used in an EPUB; convert them to PNG or SVG.
  • Macros: \newcommand / \def definitions are expanded; packages are not loaded, and unknown commands are skipped rather than failing.

Options

Pass them to the writer: new EpubWriter(['splitLevel' => 2, 'tocDepth' => 2]).

Option Default Meaning
splitLevel 1 Headings of this level or higher start a new chapter file
tocDepth 3 Deepest heading level in the table of contents
numberSections true Show section numbers in headings and the table of contents
titlePage true Generate a title page (title, authors, date, cover)
coverImage none Path of a cover image; overrides the document's \cover
language from babel, else en-US BCP 47 language tag
identifier derived UUID dc:identifier of the book
css built-in Replacement stylesheet contents
resourcePath ['.'] Folders searched for images not found next to the .tex
mathml true Convert math to MathML (falls back to the TeX source if a formula cannot be converted)
optimizeImages false Opt-in image compression with ext-imagick (CPU-heavy: about 12 s for 40 MB of images). Uses maxImageWidth (1600) and jpegQuality (85)

What to expect

Measured on an Apple M4 Max with images embedded unchanged: about 1 second for a 3,000-line survey with 20 figures (the EPUB is about the size of the images). Peak memory is 2 to 3 times the total image size, so allow headroom in memory_limit for very large projects.

The output has been checked for well-formed XML, working internal links and MathML, but not with a strict validator such as epubcheck; run it before distributing a book. See SUPPORTED_STRUCTURES.md for the full list of supported and unsupported LaTeX constructs.

Converting LaTeX to ODT

The same LatexReader output can be written as an OpenDocument Text file, which opens in LibreOffice, Apache OpenOffice and other ODF applications. It is produced in pure PHP (ext-zip, ext-dom); LibreOffice is not needed on the server.

use Pandoc\Reader\LatexReader;
use Pandoc\Writer\OdtWriter;

$doc = (new LatexReader())->read('/projects/paper/paper.tex');
(new OdtWriter())->writeToFile($doc, '/projects/paper/paper.odt');

$odt = (new OdtWriter())->write($doc);   // or get the binary string, to send as a download:
// header('Content-Type: application/vnd.oasis.opendocument.text');

From the command line: php latex2odt.php paper.tex [paper.odt] [--cover=cover.png] [--no-numbers].

What you get: outline-level headings (so LibreOffice's Navigator works) with section numbers, paragraph and character styles, bulleted and numbered lists, footnotes as real footnotes, tables with column widths fitted to their content and booktabs-style rules, figures with numbered captions (sub-figures are laid out side by side, as in the PDF), algorithms as indented pseudocode, internal links and citations as working links, and equations as embedded MathML objects, which stay editable in LibreOffice. \cover{file} places the cover image centred above the title, as in the EPUB.

Option Default Meaning
numberSections true Write the section numbers in headings
titlePage true Title, authors and date at the top (with the cover, if any)
coverImage none Path of a cover image; overrides the document's \cover
language from babel, else en-US BCP 47 language tag of the document
resourcePath ['.'] Folders searched for images not found next to the .tex
mathml true Embed equations as MathML objects (otherwise the TeX source is written as text)

Notes:

  • The output is checked against LibreOffice itself (opened and rendered headlessly in the tests when it is installed; set LIBREOFFICE_BIN if it is not in a standard place). It has not been checked in Microsoft Word, Pages or Google Docs, which handle embedded equation objects less consistently than LibreOffice.
  • LibreOffice cannot parse a few TeX-math constructs. The writer rewrites the common ones (aligned equations, |x|, [a,b), J^{*}, empty cells) so that they display, but about 0.6% of the distinct formulas of our four test papers (for example some \bigl\lfloor … \bigr\rceil and nested \boldsymbol sums) still show a red ¿ marker in LibreOffice. The EPUB output is not affected.
  • Page layout is fixed (A4, 2.5 cm margins, 11 pt serif); there is no table of contents field yet (use LibreOffice's Insert → Table of Contents, which works because headings carry their outline level).

Converting ODT to LaTeX

use Pandoc\Reader\OdtReader;
use Pandoc\Writer\LatexWriter;

$doc = (new OdtReader())->read('/path/to/paper.odt');          // a file path, or the binary contents
$tex = (new LatexWriter())->write($doc);                        // a standalone document; ->write($doc, false) for the body only

file_put_contents('/path/to/paper.tex', $tex);
foreach ($doc->mediaBag->getAll() as $path => $media) {         // the pictures the .tex refers to ("Pictures/image1.png", ...)
    @mkdir(dirname("/path/to/$path"), 0777, true);
    file_put_contents("/path/to/$path", $media['contents']);
}

Or from the command line: php odt2latex.php paper.odt [paper.tex] [--fragment] [--compact-tables] (writes the pictures next to the .tex). The same OdtReader is used when you upload an .odt to web/convert.php.

The LaTeX uses the kernel plus amsmath/amssymb, graphicx, hyperref, booktabs, xcolor, soul, and subcaption or babel only when the document needs them; pdflatex is enough. It is also readable by this library's LatexReader, so ODT → LaTeX → EPUB works.

  • Read from the ODT: outline-level headings, paragraphs and character styles (bold, italic, underline, strikeout, small caps, super/subscript, monospace, colours, resolved through the style chain), lists with their numbering format, links and bookmarks, footnotes, tables with spans, header rows and alignment, pictures (embedded into the MediaBag), captions, and formulas (MathML converted to LaTeX; a StarMath annotation is ignored).
  • Recognised conventions: Title/Author/Date paragraphs become the title block; a leading picture described as "Cover" becomes \cover{…}; "Figure 3: …" and "Table 2: …" captions are attached to their picture or table and lose their label (LaTeX numbers them); a row of pictures becomes side-by-side sub-figures; bibliography entries and citation links become thebibliography and \cite; links that point to a bookmark and show a number become \ref.
  • Manual heading numbers ("1 Introduction", "2.1 Method", "A.1 Details") are removed, and the heading level is taken from the depth of the number, when the numbering is systematic across the document (this also repairs Word files whose headings are all "Heading 1"). Appendix letters must form a sequence, so a title such as "A Study of X" is never mistaken for one.
  • Word-style documents: a list whose items are headings (how Word numbers them) is dissolved into ordinary headings and paragraphs, because LaTeX forbids sectioning commands inside lists; a link to a bookmark that does not exist (an unresolved cross-reference in the source) keeps its text and gets no \\ref.
  • Writer behaviour: \label is written only for ids that something links to; the title block, babel, amsmath and subcaption only when needed. compactWideTables => true (or --compact-tables) shrinks tables of 7 or more columns to fit the page.
  • Changes to LatexWriter for every reader: headings of level 4 and 5 are now \paragraph and \subparagraph (they were written as \section); characters that pdflatex rejects in text (arrows, relations, Greek letters, ✓, ⋆, zero-width spaces…) are written as \ensuremath{…} or dropped; \ul is replaced by \underline when the text holds a link or a formula; ordered lists keep their start value and numbering style; description lists, \multicolumn, table and figure captions, footnotes made of paragraphs and Quoted/SmallCaps/line-break nodes are now written.
  • Not supported: theorem-like environments (a "Theorem 2.1." run-in heading stays bold text), algorithm captions and keywords (pseudo-code lines are kept as indented text), \multirow row spans, equation numbers inside multi-row environments, comments and tracked changes (dropped), pictures in EMF, WMF, SVG or GIF (pdflatex cannot include them, so a visible [image: name.emf] box is written instead of a compile error), and shapes other than text boxes.

Output: Plain .tex or .zip

When a document contains images, charts, or other media, you need to bundle them alongside the .tex file. The MediaBag tells you whether there are any attachments:

use Pandoc\Reader\ReaderFactory;
use Pandoc\Writer\LatexWriter;

$reader = ReaderFactory::createForExtension('docx'); // or xlsx, pptx, etc.
$doc    = $reader->read($filePath);
$latex  = (new LatexWriter())->write($doc, standalone: true);

if (!$doc->mediaBag->isEmpty()) {
    // Bundle .tex + all media into a ZIP
    $zip = new ZipArchive();
    $zip->open('output.zip', ZipArchive::CREATE | ZipArchive::OVERWRITE);
    $zip->addFromString('document.tex', $latex);
    foreach ($doc->mediaBag->getAll() as $filename => $media) {
        $zip->addFromString($filename, $media['contents']);
    }
    $zip->close();
    // → distribute output.zip
} else {
    // No media — plain .tex is sufficient
    file_put_contents('document.tex', $latex);
}

All media files (images, chart JSON/CSV) are stored at the root of the ZIP, so \includegraphics{image.png} and chart references resolve correctly when the .tex is compiled or processed from the same directory.

Web Interface

The project includes a web-based demonstration tool in web/.

  1. Point your web server to the php-pandoc/web/ folder.
  2. Open index.html in your browser.
  3. Upload a .docx, .xlsx, .pptx, .html, .ipynb, .md, or .bib file.
  4. Choose Standalone or Fragment output.
  5. Download the result — a plain .tex if the document has no media, or a .zip if it does.

Supported Structures

See SUPPORTED_STRUCTURES.md for a full feature list. Highlights:

  • Word: Headers (H1–H6, Title), bold/italic/underline/strikeout/color, lists, tables, images, headers & footers, hyperlinks, footnotes/endnotes, math equations (OMML → LaTeX), automatic run-merging.
  • Excel: Multi-sheet tables, cell formatting, embedded images, Chart.js-ready chart extraction, per-sheet CSV export with locale-aware separators.
  • PowerPoint: Slide titles, body text, bullet/ordered lists, images, tables, slide/slider LaTeX environments.
  • HTML: Full block and inline element support.
  • Jupyter: Markdown cells, code blocks, output images.
  • BibTeX: Entries rendered as a thebibliography environment with \bibitem items; URLs wrapped in \url{…}, title/journal/booktitle/series/publisher fields italicised with \emph{…}, and keywords/abstract fields omitted.

Development and Testing

./vendor/bin/phpunit

Credits

This project is a port of Pandoc, originally created by John MacFarlane.

License

GPL v2 or later, mirroring the original Pandoc license.