Skip to content
Advertisement

PhpWord output doesn’t work in LibreOffice but works fine in MS Word

This relates to a semi-known issue listed on the GitHub page [link], where the way PhpWord generates tables causes it to set the cell width to “”, which isn’t valid in LibreOffice, but is fine in Word.

The issue on GitHub lists html-to-word conversion specifically, but I’m getting this issue when just going by the regular object-oriented interface also.

I’ve tried manually setting the cell width, but it doesn’t seem to do anything.

I’m asking here to see if anyone has any workaround to this issue, as it makes it nearly impossible to debug my output on a Linux box without access to MS Word. I need to export .docx for a client.

Why not just export to .odt to test it instead? If I do that it won’t render the list I have on the page either…..

Here’s a rough outline of the document I’m working with (I’m not at liberty to share the exact code due to NDAs):

$document = new PhpWordPhpWord();
$section = $document->addSection();

// This list doesn't get rendered in the ODT file
$section->addListItem('Employee ID: 98765');
$section->addListItem('First Name: John');
$section->addListItem('Last Name: Doe');
$section->addListItem('SSN: 12345');

// This table has errors in LibreOffice when exported to .docx
if ($show_adv_details) {
    $table = $section->addTable();
    $table->addRow();
    $table->addCell()->addText('SSN');
    $table->addCell()->addText('Email');
    $table->addCell()->addText('Phone No');
    
    $table->addRow();
    $table->addCell()->addText('12345');
    $table->addCell()->addText('jd@example.com');
    $table->addCell()->addText('999 1234');

    // repeat ad nauseam
}

$section->addTitle('Comment');
$section->addTitle('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.');

I’m using PHP 7.0 (ancient legacy system, can’t do anything about it) on Ubuntu.

So just to reiterate the question here at the end:

Is there something I can do to make this document render correctly on all the different outputs? The documentation doesn’t seem to be much help.

Advertisement

Answer

Well, I’m not a maintainer from the PHPOffice package but here is a workaround which I made digging in the package code and fixing your two issues:

  1. ODT List does not work;
  2. Tables on Word2007 Writer when opening with LibreOffice does not work.

I realized that the ListItem.php class did not exist on ODText writer, and I suppose that this is why you can not add lists to .odt files.

You need to add the widths manually to columns to make it work on LibreOffice (as Nigel said, you can go through each addCell() call and add some number, even “1” works).

How to fix both issues

We will “override” the original files with some updates that fix the issues. In order to make it work, you should have installed PHPWord using Composer. (This is the only option available on the installation guide, but there are other ways to do that).

Without Composer, require the custom files after PHPWord require (I think that it works). Or change directly the files on their locations (a bad idea).

Files

Create a structure like that:

Custom
├── Element
│   └── Table.php
└── Writer
    └── ODText
        └── Element
            └── ListItem.php

Of course, you can use any other, but we’re going to “override” the original package files, so I kept its structure.

We will use Autoload Files to get those files:

composer.json:

...
"autoload": {
    "files": [
        "Custom/Element/Table.php",
        "Custom/Writer/ODText/Element/ListItem.php"
    ]
},
...

If there’s no “autoload” key on your composer.json, you have to add it, and the same goes for the “files” key.

Run composer dump-autoload to update the changes.

Custom classes

Now, we have to add the code to our custom files.

Custom/Writer/ODText/Element/ListItem.php:

<?php
namespace PhpOfficePhpWordWriterODTextElement;

/**
 * ListItem element writer
 */
class ListItem extends AbstractElement
{
    /**
     * Write list item element.
     */
    public function write()
    {
        $xmlWriter = $this->getXmlWriter();
        $element = $this->getElement();
        if (!$element instanceof PhpOfficePhpWordElementListItem) {
            return;
        }
        
        $textObject = $element->getTextObject();

        $xmlWriter->startElement('text:list');
        $xmlWriter->writeAttribute('text:style-name', 'L1');

        $xmlWriter->startElement('text:list-item');

        $xmlWriter->startElement('text:p');
        $xmlWriter->writeAttribute('text:style-name', 'P1');

        $elementWriter = new Text($xmlWriter, $textObject, true);
        $elementWriter->write();

        $xmlWriter->endElement(); // text:list
        $xmlWriter->endElement(); // text:p
        $xmlWriter->endElement(); // text:list-item
    }
}

The file has been adapted from the Word2007 version and fixes your first issue. Now lists will work on ODText.

Custom/Element/Table.php:

<?php
namespace PhpOfficePhpWordElement;

use PhpOfficePhpWordStyleTable as TableStyle;

/**
 * Table element writer
 */
class Table extends AbstractElement
{
    /**
     * Table style
     *
     * @var PhpOfficePhpWordStyleTable
     */
    private $style;

    /**
     * Table rows
     *
     * @var PhpOfficePhpWordElementRow[]
     */
    private $rows = array();

    /**
     * Table width
     *
     * @var int
     */
    private $width = null;

    /**
     * Create a new table
     *
     * @param mixed $style
     */
    public function __construct($style = null)
    {
        $this->style = $this->setNewStyle(new TableStyle(), $style);
    }

    /**
     * Add a row
     *
     * @param int $height
     * @param mixed $style
     * @return PhpOfficePhpWordElementRow
     */
    public function addRow($height = null, $style = null)
    {
        $row = new Row($height, $style);
        $row->setParentContainer($this);
        $this->rows[] = $row;

        return $row;
    }

    /**
     * Add a cell
     *
     * @param int $width
     * @param mixed $style
     * @return PhpOfficePhpWordElementCell
     */
    public function addCell($width = 1, $style = null)
    {
        $index = count($this->rows) - 1;
        $row = $this->rows[$index];
        $cell = $row->addCell($width, $style);

        return $cell;
    }

    /**
     * Get all rows
     *
     * @return PhpOfficePhpWordElementRow[]
     */
    public function getRows()
    {
        return $this->rows;
    }

    /**
     * Get table style
     *
     * @return PhpOfficePhpWordStyleTable
     */
    public function getStyle()
    {
        return $this->style;
    }

    /**
     * Get table width
     *
     * @return int
     */
    public function getWidth()
    {
        return $this->width;
    }

    /**
     * Set table width.
     *
     * @param int $width
     */
    public function setWidth($width)
    {
        $this->width = $width;
    }

    /**
     * Get column count
     *
     * @return int
     */
    public function countColumns()
    {
        $columnCount = 0;

        $rowCount = count($this->rows);
        for ($i = 0; $i < $rowCount; $i++) {
            /** @var PhpOfficePhpWordElementRow $row Type hint */
            $row = $this->rows[$i];
            $cellCount = count($row->getCells());
            if ($columnCount < $cellCount) {
                $columnCount = $cellCount;
            }
        }

        return $columnCount;
    }

    /**
     * The first declared cell width for each column
     *
     * @return int[]
     */
    public function findFirstDefinedCellWidths()
    {
        $cellWidths = array();

        foreach ($this->rows as $row) {
            $cells = $row->getCells();
            if (count($cells) <= count($cellWidths)) {
                continue;
            }
            $cellWidths = array();
            foreach ($cells as $cell) {
                $cellWidths[] = $cell->getWidth();
            }
        }

        return $cellWidths;
    }
}

As we are “overwriting” the file, we cannot reuse the original file extending it (at least, I don’t know how). The only change here is on public function addCell($width = 1, $style = null) that makes always “1” as default value to the function. This fixes your second issue, and now you can call $table->addCell() without a value.

DEMO

require __DIR__ . '/vendor/autoload.php';

$document = new PhpOfficePhpWordPhpWord();

$section = $document->addSection();

// This list **does** get rendered in the ODT file
$section->addListItem('Employee ID: 98765');
$section->addListItem('First Name: John');
$section->addListItem('Last Name: Doe');
$section->addListItem('SSN: 12345');

$table = $section->addTable();
$table->addRow();
$table->addCell()->addText('SSN');
$table->addCell()->addText('Email');
$table->addCell()->addText('Phone No');

$table->addRow();
$table->addCell()->addText('12345');
$table->addCell()->addText('jd@example.com');
$table->addCell()->addText('999 1234');

// Save the document as DOCX
$objWriter = PhpOfficePhpWordIOFactory::createWriter($document, 'Word2007');
$objWriter->save('document.docx');

// Save the document as ODT
$objWriter = PhpOfficePhpWordIOFactory::createWriter($document, 'ODText');
$objWriter->save('document.odt');
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement