Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table Borders Fixes #2535

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/changes/1.x/1.3.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# [1.3.0](https://github.com/PHPOffice/PHPWord/tree/1.3.0) (WIP)

[Full Changelog](https://github.com/PHPOffice/PHPWord/compare/1.2.0...1.3.0)

## Enhancements

### Bug fixes

- MsDoc Reader : Correct Font Size Calculation by [@oleibman](https://github.com/oleibman) Issue [#2526](https://github.com/PHPOffice/PHPWord/issues/2526) PR [#2531](https://github.com/PHPOffice/PHPWord/pull/2531)
- Html Reader : Process Titles as Headings not Paragraphs [@0b10011](https://github.com/0b10011) and [@oleibman](https://github.com/oleibman) Issue [#1692](https://github.com/PHPOffice/PHPWord/issues/1692) PR [#2533](https://github.com/PHPOffice/PHPWord/pull/2533)
- Table Borders Fixes by [@oleibman](https://github.com/oleibman) Issue [#2402](https://github.com/PHPOffice/PHPWord/issues/2402) Issue [#2474](https://github.com/PHPOffice/PHPWord/issues/2474) PR [#2535](https://github.com/PHPOffice/PHPWord/pull/2535)

### Miscellaneous

### BC Breaks
5 changes: 0 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -385,11 +385,6 @@ parameters:
count: 1
path: src/PhpWord/Shared/Html.php

-
message: "#^Cannot call method setBorderSize\\(\\) on PhpOffice\\\\PhpWord\\\\Style\\\\Table\\|string\\.$#"
count: 1
path: src/PhpWord/Shared/Html.php

-
message: "#^Cannot call method setStyleName\\(\\) on PhpOffice\\\\PhpWord\\\\Style\\\\Table\\|string\\.$#"
count: 1
Expand Down
65 changes: 38 additions & 27 deletions src/PhpWord/Reader/Word2007/AbstractPart.php
Original file line number Diff line number Diff line change
Expand Up @@ -744,35 +744,46 @@ protected function readTableStyle(XMLReader $xmlReader, DOMElement $domNode)
$borders = array_merge($margins, ['insideH', 'insideV']);

if ($xmlReader->elementExists('w:tblPr', $domNode)) {
$tblStyleName = '';
if ($xmlReader->elementExists('w:tblPr/w:tblStyle', $domNode)) {
$style = $xmlReader->getAttribute('w:val', $domNode, 'w:tblPr/w:tblStyle');
} else {
$styleNode = $xmlReader->getElement('w:tblPr', $domNode);
$styleDefs = [];
foreach ($margins as $side) {
$ucfSide = ucfirst($side);
$styleDefs["cellMargin$ucfSide"] = [self::READ_VALUE, "w:tblCellMar/w:$side", 'w:w'];
}
foreach ($borders as $side) {
$ucfSide = ucfirst($side);
$styleDefs["border{$ucfSide}Size"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:sz'];
$styleDefs["border{$ucfSide}Color"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:color'];
$styleDefs["border{$ucfSide}Style"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:val'];
}
$styleDefs['layout'] = [self::READ_VALUE, 'w:tblLayout', 'w:type'];
$styleDefs['bidiVisual'] = [self::READ_TRUE, 'w:bidiVisual'];
$styleDefs['cellSpacing'] = [self::READ_VALUE, 'w:tblCellSpacing', 'w:w'];
$style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);

$tablePositionNode = $xmlReader->getElement('w:tblpPr', $styleNode);
if ($tablePositionNode !== null) {
$style['position'] = $this->readTablePosition($xmlReader, $tablePositionNode);
}
$tblStyleName = $xmlReader->getAttribute('w:val', $domNode, 'w:tblPr/w:tblStyle');
}
$styleNode = $xmlReader->getElement('w:tblPr', $domNode);
$styleDefs = [];

$indentNode = $xmlReader->getElement('w:tblInd', $styleNode);
if ($indentNode !== null) {
$style['indent'] = $this->readTableIndent($xmlReader, $indentNode);
}
foreach ($margins as $side) {
$ucfSide = ucfirst($side);
$styleDefs["cellMargin$ucfSide"] = [self::READ_VALUE, "w:tblCellMar/w:$side", 'w:w'];
}
foreach ($borders as $side) {
$ucfSide = ucfirst($side);
$styleDefs["border{$ucfSide}Size"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:sz'];
$styleDefs["border{$ucfSide}Color"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:color'];
$styleDefs["border{$ucfSide}Style"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:val'];
}
$styleDefs['layout'] = [self::READ_VALUE, 'w:tblLayout', 'w:type'];
$styleDefs['bidiVisual'] = [self::READ_TRUE, 'w:bidiVisual'];
$styleDefs['cellSpacing'] = [self::READ_VALUE, 'w:tblCellSpacing', 'w:w'];
$style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);

$tablePositionNode = $xmlReader->getElement('w:tblpPr', $styleNode);
if ($tablePositionNode !== null) {
$style['position'] = $this->readTablePosition($xmlReader, $tablePositionNode);
}

$indentNode = $xmlReader->getElement('w:tblInd', $styleNode);
if ($indentNode !== null) {
$style['indent'] = $this->readTableIndent($xmlReader, $indentNode);
}
if ($xmlReader->elementExists('w:basedOn', $domNode)) {
$style['basedOn'] = $xmlReader->getAttribute('w:val', $domNode, 'w:basedOn');
}
if ($tblStyleName !== '') {
$style['tblStyle'] = $tblStyleName;
}
// this may be unneeded
if ($xmlReader->elementExists('w:name', $domNode)) {
$style['styleName'] = $xmlReader->getAttribute('w:val', $domNode, 'w:name');
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/PhpWord/Reader/Word2007/Styles.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ public function read(PhpWord $phpWord): void
foreach ($nodes as $node) {
$type = $xmlReader->getAttribute('w:type', $node);
$name = $xmlReader->getAttribute('w:val', $node, 'w:name');
$styleId = $xmlReader->getAttribute('w:styleId', $node);
if (null === $name) {
$name = $xmlReader->getAttribute('w:styleId', $node);
$name = $styleId;
}
$headingMatches = [];
preg_match('/Heading\s*(\d)/i', $name, $headingMatches);
Expand Down Expand Up @@ -98,7 +99,8 @@ public function read(PhpWord $phpWord): void
case 'table':
$tStyle = $this->readTableStyle($xmlReader, $node);
if (!empty($tStyle)) {
$phpWord->addTableStyle($name, $tStyle);
$newTable = $phpWord->addTableStyle($styleId, $tStyle);
$newTable->setStyleName($name);
}

break;
Expand Down
83 changes: 53 additions & 30 deletions src/PhpWord/Shared/Html.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use PhpOffice\PhpWord\Element\Row;
use PhpOffice\PhpWord\Element\Table;
use PhpOffice\PhpWord\Settings;
use PhpOffice\PhpWord\SimpleType\Border;
use PhpOffice\PhpWord\SimpleType\Jc;
use PhpOffice\PhpWord\SimpleType\NumberFormat;
use PhpOffice\PhpWord\Style\Paragraph;
Expand All @@ -37,6 +38,8 @@
*/
class Html
{
private const SPECIAL_BORDER_WIDTHS = ['thin' => '0.5pt', 'thick' => '3.5pt', 'medium' => '2.0pt'];

private const RGB_REGEXP = '/^\s*rgb\s*[(]\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*[)]\s*$/';

protected static $listIndex = 0;
Expand Down Expand Up @@ -146,7 +149,7 @@ protected static function parseInlineStyle($node, $styles = [])
break;
case 'bgcolor':
// tables, rows, cells e.g. <tr bgColor="#FF0000">
$styles['bgColor'] = self::convertRgb($val);
HtmlColours::setArrayColour($styles, 'bgColor', self::convertRgb($val));

break;
case 'valign':
Expand Down Expand Up @@ -425,9 +428,10 @@ protected static function parseTable($node, $element, &$styles)
}

$attributes = $node->attributes;
if ($attributes->getNamedItem('border')) {
if ($attributes->getNamedItem('border') !== null && is_object($newElement->getStyle())) {
$border = (int) $attributes->getNamedItem('border')->nodeValue;
$newElement->getStyle()->setBorderSize(Converter::pixelToTwip($border));
$newElement->getStyle()->setBorderSize((int) Converter::pixelToTwip($border));
$newElement->getStyle()->setBorderStyle(($border === 0) ? 'none' : 'single');
}

return $newElement;
Expand Down Expand Up @@ -724,11 +728,11 @@ protected static function parseStyleDeclarations(array $selectors, array $styles

break;
case 'color':
$styles['color'] = self::convertRgb($value);
HtmlColours::setArrayColour($styles, 'color', self::convertRgb($value));

break;
case 'background-color':
$styles['bgColor'] = self::convertRgb($value);
HtmlColours::setArrayColour($styles, 'bgColor', self::convertRgb($value));

break;
case 'line-height':
Expand Down Expand Up @@ -808,7 +812,7 @@ protected static function parseStyleDeclarations(array $selectors, array $styles

break;
case 'border-width':
$styles['borderSize'] = Converter::cssToPoint($value);
$styles['borderSize'] = Converter::cssToPoint(self::SPECIAL_BORDER_WIDTHS[$value] ?? $value);

break;
case 'border-style':
Expand Down Expand Up @@ -838,29 +842,46 @@ protected static function parseStyleDeclarations(array $selectors, array $styles
case 'border-bottom':
case 'border-right':
case 'border-left':
// must have exact order [width color style], e.g. "1px #0011CC solid" or "2pt green solid"
// Word does not accept shortened hex colors e.g. #CCC, only full e.g. #CCCCCC
if (preg_match('/([0-9]+[^0-9]*)\s+(\#[a-fA-F0-9]+|[a-zA-Z]+)\s+([a-z]+)/', $value, $matches)) {
if (false !== strpos($property, '-')) {
$tmp = explode('-', $property);
$which = $tmp[1];
$which = ucfirst($which); // e.g. bottom -> Bottom
} else {
$which = '';
}
// Note - border width normalization:
// Width of border in Word is calculated differently than HTML borders, usually showing up too bold.
// Smallest 1px (or 1pt) appears in Word like 2-3px/pt in HTML once converted to twips.
// Therefore we need to normalize converted twip value to cca 1/2 of value.
// This may be adjusted, if better ratio or formula found.
// BC change: up to ver. 0.17.0 was $size converted to points - Converter::cssToPoint($size)
$size = Converter::cssToTwip($matches[1]);
$stylePattern = '/(^|\\s)(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)(\\s|$)/';
if (!preg_match($stylePattern, $value, $matches)) {
break;
}
$borderStyle = $matches[2];
$value = preg_replace($stylePattern, ' ', $value) ?? '';
$borderSize = $borderColor = null;
$sizePattern = '/(^|\\s)([0-9]+([.][0-9]+)?+(%|[a-z]*)|thick|thin|medium)(\\s|$)/';
if (preg_match($sizePattern, $value, $matches)) {
$borderSize = $matches[2];
$borderSize = self::SPECIAL_BORDER_WIDTHS[$borderSize] ?? $borderSize;
$value = preg_replace($sizePattern, ' ', $value) ?? '';
}
$colorPattern = '/(^|\\s)([#][a-fA-F0-9]{6}|[#][a-fA-F0-9]{3}|[a-z][a-z0-9]+)(\\s|$)/';
if (preg_match($colorPattern, $value, $matches)) {
$borderColor = HtmlColours::convertColour($matches[2]);
}
if (false !== strpos($property, '-')) {
$tmp = explode('-', $property);
$which = $tmp[1];
$which = ucfirst($which); // e.g. bottom -> Bottom
} else {
$which = '';
}
// Note - border width normalization:
// Width of border in Word is calculated differently than HTML borders, usually showing up too bold.
// Smallest 1px (or 1pt) appears in Word like 2-3px/pt in HTML once converted to twips.
// Therefore we need to normalize converted twip value to cca 1/2 of value.
// This may be adjusted, if better ratio or formula found.
// BC change: up to ver. 0.17.0 was $size converted to points - Converter::cssToPoint($size)
if ($borderSize !== null) {
$size = Converter::cssToTwip($borderSize);
$size = (int) ($size / 2);
// valid variants may be e.g. borderSize, borderTopSize, borderLeftColor, etc ..
$styles["border{$which}Size"] = $size; // twips
$styles["border{$which}Color"] = trim($matches[2], '#');
$styles["border{$which}Style"] = self::mapBorderStyle($matches[3]);
}
if (!empty($borderColor)) {
$styles["border{$which}Color"] = $borderColor;
}
$styles["border{$which}Style"] = self::mapBorderStyle($borderStyle);

break;
case 'vertical-align':
Expand Down Expand Up @@ -1032,21 +1053,23 @@ protected static function mapBorderStyle($cssBorderStyle)
case 'dotted':
case 'double':
return $cssBorderStyle;
case 'hidden':
return 'none';
default:
return 'single';
}
}

protected static function mapBorderColor(&$styles, $cssBorderColor): void
{
$numColors = substr_count($cssBorderColor, '#');
$colors = explode(' ', $cssBorderColor);
$numColors = count($colors);
if ($numColors === 1) {
$styles['borderColor'] = trim($cssBorderColor, '#');
} elseif ($numColors > 1) {
$colors = explode(' ', $cssBorderColor);
HtmlColours::setArrayColour($styles, 'borderColor', $cssBorderColor);
} else {
$borders = ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'];
for ($i = 0; $i < min(4, $numColors, count($colors)); ++$i) {
$styles[$borders[$i]] = trim($colors[$i], '#');
HtmlColours::setArrayColour($styles, $borders[$i], $colors[$i]);
}
}
}
Expand Down
Loading
Loading