pandoc-php / pandoc
A native PHP 8.4 port of the Pandoc document converter.
Requires
- php: >=8.4
- latex2mathml/latex2mathml-php: ^1.5
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
- ext-imagick: Shrinks images embedded in EPUB output (optional)
Provides
None
Conflicts
None
Replaces
None
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
readonlyclasses, Enums, and property hooks. - AST-Centric Architecture: Mirrors Pandoc's Abstract Syntax Tree for robust conversions.
- Modular Reader System: Factory pattern and
ReaderInterfacefor 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.jsonsummary of document locale. - PowerPoint (PPTX): Each slide becomes a
slideenvironment, all slides wrapped in asliderenvironment. 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
.zipwith the.texand all media files in the same directory. Plain.texotherwise. - 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
pdflatexand 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) pluslatex2mathml/latex2mathml-phpfor MathML.ext-imagickis 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.xlsfiles 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, andpublisherfields are italicised with\emph{…}. - The
keywordsandabstractfields 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 ababellanguage (\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 generatedthebibliographyenvironment (the contents of the.bblfile) in the source;\citecommands 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/\defdefinitions 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_BINif 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\rceiland nested\boldsymbolsums) 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; aStarMathannotation is ignored). - Recognised conventions:
Title/Author/Dateparagraphs 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 becomethebibliographyand\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:
\labelis written only for ids that something links to; the title block,babel,amsmathandsubcaptiononly when needed.compactWideTables => true(or--compact-tables) shrinks tables of 7 or more columns to fit the page. - Changes to
LatexWriterfor every reader: headings of level 4 and 5 are now\paragraphand\subparagraph(they were written as\section); characters thatpdflatexrejects in text (arrows, relations, Greek letters,✓,⋆, zero-width spaces…) are written as\ensuremath{…}or dropped;\ulis replaced by\underlinewhen the text holds a link or a formula; ordered lists keep their start value and numbering style;descriptionlists,\multicolumn, table and figure captions, footnotes made of paragraphs andQuoted/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),
\multirowrow spans, equation numbers inside multi-row environments, comments and tracked changes (dropped), pictures in EMF, WMF, SVG or GIF (pdflatexcannot 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/.
- Point your web server to the
php-pandoc/web/folder. - Open
index.htmlin your browser. - Upload a
.docx,.xlsx,.pptx,.html,.ipynb,.md, or.bibfile. - Choose Standalone or Fragment output.
- Download the result — a plain
.texif the document has no media, or a.zipif 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/sliderLaTeX environments. - HTML: Full block and inline element support.
- Jupyter: Markdown cells, code blocks, output images.
- BibTeX: Entries rendered as a
thebibliographyenvironment with\bibitemitems; URLs wrapped in\url{…}, title/journal/booktitle/series/publisher fields italicised with\emph{…}, andkeywords/abstractfields 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.