-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Open
Labels
Description
Describe the problem
When generating an HTML file from a DOCX using:
\PhpOffice\PhpWord\Settings::setTempDir(CMS_FOLDER . TMP_DIR);
$phpWord = \PhpOffice\PhpWord\IOFactory::load($this->_sourceDocx);
$htmlWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'HTML');
$docProps = $phpWord->getDocInfo();
$docProps->setTitle($this->_title);
$this->_filePath = tempnam(CMS_FOLDER . TMP_DIR, 'HTM');
$htmlWriter->save($this->_filePath);
If the source DOCX contains complex tables with row and column merging, the following issues occur:
- It incorrectly calculates the rowspan for merged cells.
- It applies styles only from the first cell, which can cause the loss of styles, such as cell borders, in merged cells.
Describe the expected behavior
Fixed \PHPOffice\PhpWord\Writer\HTML\Element\Table.php
<?php
/**
* This file is part of PHPWord - A pure PHP library for reading and writing
* word processing documents.
*
* PHPWord is free software distributed under the terms of the GNU Lesser
* General Public License version 3 as published by the Free Software Foundation.
*
* For the full copyright and license information, please read the LICENSE
* file that was distributed with this source code. For the full list of
* contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
*
* @see https://github.com/PHPOffice/PHPWord
* @copyright 2010-2018 PHPWord contributors
* @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
*/
namespace PhpOffice\PhpWord\Writer\HTML\Element;
use PhpOffice\PhpWord\Writer\HTML\Style\Table as TableStyleWriter;
use PhpOffice\PhpWord\Writer\HTML\Style\Cell as CellStyleWriter;
use PhpOffice\PhpWord\Writer\HTML\Style\Paragraph as ParagraphStyleWriter;
/**
* Table element HTML writer
*
* @since 0.10.0
*/
class Table extends AbstractElement
{
/**
* Write table
*
* @return string
*/
public function write()
{
if (!$this->element instanceof \PhpOffice\PhpWord\Element\Table) {
return '';
}
$content = '';
$rows = $this->element->getRows();
$rowCount = count($rows);
if ($rowCount > 0) {
$tableStyle = self::getTableStyle($this->element->getStyle());
$content .= '<table cellpadding="0" cellspacing="0"';
if (is_object($tableStyle)) {
$styleWriter = new TableStyleWriter($tableStyle);
$content .= ' style="' . $styleWriter->write() . '"';
} else {
$content .= ' class="' . htmlspecialchars($tableStyle) . '"';
}
$content .= '>' . PHP_EOL;
// Массив для отслеживания объединенных ячеек
$mergedCells = [];
for ($i = 0; $i < $rowCount; $i++) {
/** @var $row \PhpOffice\PhpWord\Element\Row Type hint */
$rowStyle = $rows[$i]->getStyle();
$height = $rows[$i]->getHeight();
$tblHeader = $rowStyle->isTblHeader();
$sTrStyle = '';
$height && $sTrStyle .= 'height: ' . intval($height * 0.2 / 3) . 'px';
$content .= '<tr style="' . $sTrStyle . '">' . PHP_EOL;
$rowCells = $rows[$i]->getCells();
$rowCellCount = count($rowCells);
// Учитываем смещение из-за объединенных ячеек из предыдущих строк
$colOffset = 0;
for ($j = 0; $j < $rowCellCount; $j++) {
$cellStyle = $rowCells[$j]->getStyle();
$width = $rowCells[$j]->getWidth();
$cellColSpan = $cellStyle->getGridSpan();
$cellRowSpan = 1;
$cellVMerge = $cellStyle->getVMerge();
// Проверяем, не является ли текущая ячейка продолжением объединения из предыдущих строк
$isMergedFromAbove = false;
foreach ($mergedCells as $mergedCell) {
if ($mergedCell['row'] < $i &&
$mergedCell['col'] == ($j + $colOffset) &&
$mergedCell['rowSpan'] > ($i - $mergedCell['row'])) {
$isMergedFromAbove = true;
break;
}
}
if ($isMergedFromAbove) {
// Пропускаем ячейку, так как она уже объединена с ячейкой выше
continue;
}
if ($cellVMerge === 'restart') {
$cellRowSpan = $this->calculateCellRowSpan($rows, $i, $j);
// Запоминаем объединенную ячейку
$mergedCells[] = [
'row' => $i,
'col' => $j + $colOffset,
'rowSpan' => $cellRowSpan
];
}
if ($cellVMerge !== 'continue' && $cellVMerge !== '') {
$cellTag = $tblHeader ? 'th' : 'td';
$cellColSpanAttr = (is_numeric($cellColSpan) && ($cellColSpan > 1) ? " colspan=\"{$cellColSpan}\"" : '');
$cellRowSpanAttr = ($cellRowSpan > 1 ? " rowspan=\"{$cellRowSpan}\"" : '');
$sTdStyle = '';
$width && $sTdStyle .= 'width: ' . intval($width * 0.2 / 3) . 'px; ';
$mergedStyles = [];
if ($cellRowSpan > 1) {
for ($k = $i; $k < $i + $cellRowSpan; $k++) {
if (isset($rows[$k])) {
$kRowCells = $rows[$k]->getCells();
$currentColIndex = 0;
foreach ($kRowCells as $kCell) {
if ($currentColIndex == $j + $colOffset) {
$mergedCellStyle = $kCell->getStyle();
$mergedStyles[] = $mergedCellStyle;
break;
}
$currentColIndex += max(1, $kCell->getStyle()->getGridSpan());
}
}
}
} else {
$mergedStyles[] = $cellStyle;
}
// Объединяем стили границ
$finalCellStyle = clone $cellStyle;
$hasBottomBorder = false;
foreach ($mergedStyles as $style) {
if ($style->getBorderBottomSize() > 0 ||
$style->getBorderBottomColor() !== null ||
$style->getBorderBottomStyle() !== null) {
$hasBottomBorder = true;
// Копируем нижнюю границу из любой ячейки, где она есть
$finalCellStyle->setBorderBottomSize($style->getBorderBottomSize());
$finalCellStyle->setBorderBottomColor($style->getBorderBottomColor());
$finalCellStyle->setBorderBottomStyle($style->getBorderBottomStyle());
}
// Также проверяем другие границы
if ($style->getBorderTopSize() > 0) {
$finalCellStyle->setBorderTopSize($style->getBorderTopSize());
$finalCellStyle->setBorderTopColor($style->getBorderTopColor());
$finalCellStyle->setBorderTopStyle($style->getBorderTopStyle());
}
if ($style->getBorderLeftSize() > 0) {
$finalCellStyle->setBorderLeftSize($style->getBorderLeftSize());
$finalCellStyle->setBorderLeftColor($style->getBorderLeftColor());
$finalCellStyle->setBorderLeftStyle($style->getBorderLeftStyle());
}
if ($style->getBorderRightSize() > 0) {
$finalCellStyle->setBorderRightSize($style->getBorderRightSize());
$finalCellStyle->setBorderRightColor($style->getBorderRightColor());
$finalCellStyle->setBorderRightStyle($style->getBorderRightStyle());
}
}
// Если в объединенных ячейках не было нижней границы, но нам нужно ее добавить
if ($cellRowSpan > 1 && !$hasBottomBorder) {
// Проверяем последнюю объединенную ячейку
$lastRowIndex = $i + $cellRowSpan - 1;
if (isset($rows[$lastRowIndex])) {
$lastRowCells = $rows[$lastRowIndex]->getCells();
$currentColIndex = 0;
foreach ($lastRowCells as $lastCell) {
if ($currentColIndex == $j + $colOffset) {
$lastCellStyle = $lastCell->getStyle();
if ($lastCellStyle->getBorderBottomSize() > 0) {
$finalCellStyle->setBorderBottomSize($lastCellStyle->getBorderBottomSize());
$finalCellStyle->setBorderBottomColor($lastCellStyle->getBorderBottomColor());
$finalCellStyle->setBorderBottomStyle($lastCellStyle->getBorderBottomStyle());
}
break;
}
$currentColIndex += max(1, $lastCell->getStyle()->getGridSpan());
}
}
}
$styleWriter = new CellStyleWriter($finalCellStyle);
$sTdStyle .= $styleWriter->write();
// Добавляем стили параграфа
$x = $rowCells[$j]->getElement(0);
if ($x) {
$paragraphStyle = $x->getParagraphStyle();
$ParagraphStyleWriter = new ParagraphStyleWriter($paragraphStyle);
$sTdStyle .= $ParagraphStyleWriter->write();
}
$content .= "<{$cellTag}{$cellColSpanAttr}{$cellRowSpanAttr} style=\"" . $sTdStyle . "\">" . PHP_EOL;
$writer = new Container($this->parentWriter, $rowCells[$j]);
$insideContent = $writer->write();
if ($cellRowSpan > 1) {
// Добавляем контент из объединенных ячеек
for ($k = $i + 1; $k < $i + $cellRowSpan; $k++) {
if (isset($rows[$k])) {
$kRowCells = $rows[$k]->getCells();
$currentColIndex = 0;
foreach ($kRowCells as $kCell) {
if ($currentColIndex == $j + $colOffset) {
$writer = new Container($this->parentWriter, $kCell);
$insideContent .= $writer->write();
break;
}
$currentColIndex += max(1, $kCell->getStyle()->getGridSpan());
}
}
}
}
// HostCMS
//if (trim($insideContent) !== '<p> </p>')
//{
$insideContent = preg_replace('/(?:<p> <\/p>\s*)+$/i', '', $insideContent);
//}
// Default content for empty cell without defined height
$content .= $insideContent != '' || $height
? $insideContent
: '<br />';
$content .= "</{$cellTag}>" . PHP_EOL;
}
// Увеличиваем смещение для учета colspan
$colOffset += max(1, $cellColSpan) - 1;
}
$content .= '</tr>' . PHP_EOL;
}
$content .= '</table>' . PHP_EOL;
}
return $content;
}
/**
* Translates Table style in CSS equivalent
*
* @param string|\PhpOffice\PhpWord\Style\Table|null $tableStyle
* @return string
*/
private function getTableStyle($tableStyle = null)
{
if ($tableStyle == null) {
return '';
}
if (is_string($tableStyle)) {
$style = ' class="' . $tableStyle;
} else {
$style = ' style="';
if ($tableStyle->getLayout() == \PhpOffice\PhpWord\Style\Table::LAYOUT_FIXED) {
$style .= 'table-layout: fixed;';
} elseif ($tableStyle->getLayout() == \PhpOffice\PhpWord\Style\Table::LAYOUT_AUTO) {
$style .= 'table-layout: auto;';
}
}
return $style . '"';
}
/**
* Calculates cell rowspan.
*
* @param \PhpOffice\PhpWord\Element\Row[] $rows
*/
private function calculateCellRowSpan(array $rows, int $rowIndex, int $colIndex): int
{
$currentRow = $rows[$rowIndex];
$currentRowCells = $currentRow->getCells();
$shiftedColIndex = 0;
// Вычисляем смещенный индекс с учетом colspan в текущей строке
for ($i = 0; $i < $colIndex; $i++) {
$cell = $currentRowCells[$i];
$colSpan = 1;
if ($cell->getStyle()->getGridSpan() !== null) {
$colSpan = $cell->getStyle()->getGridSpan();
}
$shiftedColIndex += $colSpan;
}
$rowCount = count($rows);
$rowSpan = 1;
for ($i = $rowIndex + 1; $i < $rowCount; ++$i) {
$rowCells = $rows[$i]->getCells();
$currentColIndex = 0;
$found = false;
foreach ($rowCells as $cell) {
if ($currentColIndex === $shiftedColIndex) {
$vMerge = $cell->getStyle()->getVMerge();
if ($vMerge === 'continue' || $vMerge === '') {
++$rowSpan;
$found = true;
}
break;
}
$colSpan = 1;
if ($cell->getStyle()->getGridSpan() !== null) {
$colSpan = $cell->getStyle()->getGridSpan();
}
$currentColIndex += $colSpan;
}
if (!$found) {
break;
}
}
return $rowSpan;
}
}
Priority
- I want to crowdfund the feature (with @algora-io) and fund a community developer.
- I want to pay the feature and fund a maintainer for that. (Contact @Progi1984)
Reactions are currently unavailable