This commit is contained in:
wangjinlei
2024-07-17 09:23:45 +08:00
parent edf9deeb1a
commit 881ac3e056
1001 changed files with 41032 additions and 5452 deletions

View File

@@ -374,7 +374,7 @@ class Converter
*
* @param string $value
*
* @return float
* @return ?float
*/
public static function cssToPoint($value)
{

View File

@@ -0,0 +1,80 @@
<?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
*
* @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
*/
declare(strict_types=1);
namespace PhpOffice\PhpWord\Shared;
class Css
{
/**
* @var string
*/
private $cssContent;
/**
* @var array<string, array<string, string>>
*/
private $styles = [];
public function __construct(string $cssContent)
{
$this->cssContent = $cssContent;
}
public function process(): void
{
$cssContent = str_replace(["\r", "\n"], '', $this->cssContent);
preg_match_all('/(.+?)\s?\{\s?(.+?)\s?\}/', $cssContent, $cssExtracted);
// Check the number of extracted
if (count($cssExtracted) != 3) {
return;
}
// Check if there are x selectors and x rules
if (count($cssExtracted[1]) != count($cssExtracted[2])) {
return;
}
foreach ($cssExtracted[1] as $key => $selector) {
$rules = trim($cssExtracted[2][$key]);
$rules = explode(';', $rules);
foreach ($rules as $rule) {
if (empty($rule)) {
continue;
}
[$key, $value] = explode(':', trim($rule));
$this->styles[$this->sanitize($selector)][$this->sanitize($key)] = $this->sanitize($value);
}
}
}
public function getStyles(): array
{
return $this->styles;
}
public function getStyle(string $selector): array
{
$selector = $this->sanitize($selector);
return $this->styles[$selector] ?? [];
}
private function sanitize(string $value): string
{
return addslashes(trim($value));
}
}

View File

@@ -43,6 +43,11 @@ class Html
protected static $options;
/**
* @var Css
*/
protected static $css;
/**
* Add HTML parts.
*
@@ -61,7 +66,7 @@ class Html
* @todo parse $stylesheet for default styles. Should result in an array based on id, class and element,
* which could be applied when such an element occurs in the parseNode function.
*/
self::$options = $options;
static::$options = $options;
// Preprocess: remove all line ends, decode HTML entity,
// fix ampersand and angle brackets and add body tag for HTML fragments
@@ -82,10 +87,10 @@ class Html
$dom = new DOMDocument();
$dom->preserveWhiteSpace = $preserveWhiteSpace;
$dom->loadXML($html);
self::$xpath = new DOMXPath($dom);
static::$xpath = new DOMXPath($dom);
$node = $dom->getElementsByTagName('body');
self::parseNode($node->item(0), $element);
static::parseNode($node->item(0), $element);
if (\PHP_VERSION_ID < 80000) {
libxml_disable_entity_loader($orignalLibEntityLoader);
}
@@ -104,11 +109,12 @@ class Html
if (XML_ELEMENT_NODE == $node->nodeType) {
$attributes = $node->attributes; // get all the attributes(eg: id, class)
$bidi = ($attributes['dir'] ?? '') === 'rtl';
foreach ($attributes as $attribute) {
$val = $attribute->value;
switch (strtolower($attribute->name)) {
case 'align':
$styles['alignment'] = self::mapAlign(trim($val));
$styles['alignment'] = self::mapAlign(trim($val), $bidi);
break;
case 'lang':
@@ -149,6 +155,19 @@ class Html
}
}
$attributeIdentifier = $attributes->getNamedItem('id');
if ($attributeIdentifier && self::$css) {
$styles = self::parseStyleDeclarations(self::$css->getStyle('#' . $attributeIdentifier->value), $styles);
}
$attributeClass = $attributes->getNamedItem('class');
if ($attributeClass) {
if (self::$css) {
$styles = self::parseStyleDeclarations(self::$css->getStyle('.' . $attributeClass->value), $styles);
}
$styles['className'] = $attributeClass->value;
}
$attributeStyle = $attributes->getNamedItem('style');
if ($attributeStyle) {
$styles = self::parseStyle($attributeStyle, $styles);
@@ -168,6 +187,13 @@ class Html
*/
protected static function parseNode($node, $element, $styles = [], $data = []): void
{
if ($node->nodeName == 'style') {
self::$css = new Css($node->textContent);
self::$css->process();
return;
}
// Populate styles array
$styleTypes = ['font', 'paragraph', 'list', 'table', 'row', 'cell'];
foreach ($styleTypes as $styleType) {
@@ -246,7 +272,7 @@ class Html
* Parse child nodes.
*
* @param DOMNode $node
* @param \PhpOffice\PhpWord\Element\AbstractContainer $element
* @param \PhpOffice\PhpWord\Element\AbstractContainer|Row|Table $element
* @param array $styles
* @param array $data
*/
@@ -389,6 +415,11 @@ class Html
$newElement = $element->addTable($elementStyles);
// Add style name from CSS Class
if (isset($elementStyles['className'])) {
$newElement->getStyle()->setStyleName($elementStyles['className']);
}
$attributes = $node->attributes;
if ($attributes->getNamedItem('border') !== null) {
$border = (int) $attributes->getNamedItem('border')->value;
@@ -414,7 +445,11 @@ class Html
$rowStyles['tblHeader'] = true;
}
return $element->addRow(null, $rowStyles);
// set cell height to control row heights
$height = $rowStyles['height'] ?? null;
unset($rowStyles['height']); // would not apply
return $element->addRow($height, $rowStyles);
}
/**
@@ -635,13 +670,22 @@ class Html
{
$properties = explode(';', trim($attribute->value, " \t\n\r\0\x0B;"));
$selectors = [];
foreach ($properties as $property) {
[$cKey, $cValue] = array_pad(explode(':', $property, 2), 2, null);
$cValue = trim($cValue ?? '');
$cKey = strtolower(trim($cKey));
switch ($cKey) {
$selectors[strtolower(trim($cKey))] = trim($cValue ?? '');
}
return self::parseStyleDeclarations($selectors, $styles);
}
protected static function parseStyleDeclarations(array $selectors, array $styles)
{
$bidi = ($selectors['direction'] ?? '') === 'rtl';
foreach ($selectors as $property => $value) {
switch ($property) {
case 'text-decoration':
switch ($cValue) {
switch ($value) {
case 'underline':
$styles['underline'] = 'single';
@@ -654,44 +698,45 @@ class Html
break;
case 'text-align':
$styles['alignment'] = self::mapAlign($cValue);
$styles['alignment'] = self::mapAlign($value, $bidi);
break;
case 'display':
$styles['hidden'] = $cValue === 'none' || $cValue === 'hidden';
$styles['hidden'] = $value === 'none' || $value === 'hidden';
break;
case 'direction':
$styles['rtl'] = $cValue === 'rtl';
$styles['rtl'] = $value === 'rtl';
$styles['bidi'] = $value === 'rtl';
break;
case 'font-size':
$styles['size'] = Converter::cssToPoint($cValue);
$styles['size'] = Converter::cssToPoint($value);
break;
case 'font-family':
$cValue = array_map('trim', explode(',', $cValue));
$styles['name'] = ucwords($cValue[0]);
$value = array_map('trim', explode(',', $value));
$styles['name'] = ucwords($value[0]);
break;
case 'color':
$styles['color'] = trim($cValue, '#');
$styles['color'] = trim($value, '#');
break;
case 'background-color':
$styles['bgColor'] = trim($cValue, '#');
$styles['bgColor'] = trim($value, '#');
break;
case 'line-height':
$matches = [];
if ($cValue === 'normal') {
if ($value === 'normal') {
$spacingLineRule = \PhpOffice\PhpWord\SimpleType\LineSpacingRule::AUTO;
$spacing = 0;
} elseif (preg_match('/([0-9]+\.?[0-9]*[a-z]+)/', $cValue, $matches)) {
} elseif (preg_match('/([0-9]+\.?[0-9]*[a-z]+)/', $value, $matches)) {
//matches number with a unit, e.g. 12px, 15pt, 20mm, ...
$spacingLineRule = \PhpOffice\PhpWord\SimpleType\LineSpacingRule::EXACT;
$spacing = Converter::cssToTwip($matches[1]);
} elseif (preg_match('/([0-9]+)%/', $cValue, $matches)) {
} elseif (preg_match('/([0-9]+)%/', $value, $matches)) {
//matches percentages
$spacingLineRule = \PhpOffice\PhpWord\SimpleType\LineSpacingRule::AUTO;
//we are subtracting 1 line height because the Spacing writer is adding one line
@@ -700,23 +745,23 @@ class Html
//any other, wich is a multiplier. E.g. 1.2
$spacingLineRule = \PhpOffice\PhpWord\SimpleType\LineSpacingRule::AUTO;
//we are subtracting 1 line height because the Spacing writer is adding one line
$spacing = ($cValue * Paragraph::LINE_HEIGHT) - Paragraph::LINE_HEIGHT;
$spacing = ($value * Paragraph::LINE_HEIGHT) - Paragraph::LINE_HEIGHT;
}
$styles['spacingLineRule'] = $spacingLineRule;
$styles['line-spacing'] = $spacing;
break;
case 'letter-spacing':
$styles['letter-spacing'] = Converter::cssToTwip($cValue);
$styles['letter-spacing'] = Converter::cssToTwip($value);
break;
case 'text-indent':
$styles['indentation']['firstLine'] = Converter::cssToTwip($cValue);
$styles['indentation']['firstLine'] = Converter::cssToTwip($value);
break;
case 'font-weight':
$tValue = false;
if (preg_match('#bold#', $cValue)) {
if (preg_match('#bold#', $value)) {
$tValue = true; // also match bolder
}
$styles['bold'] = $tValue;
@@ -724,52 +769,65 @@ class Html
break;
case 'font-style':
$tValue = false;
if (preg_match('#(?:italic|oblique)#', $cValue)) {
if (preg_match('#(?:italic|oblique)#', $value)) {
$tValue = true;
}
$styles['italic'] = $tValue;
break;
case 'font-variant':
$tValue = false;
if (preg_match('#small-caps#', $value)) {
$tValue = true;
}
$styles['smallCaps'] = $tValue;
break;
case 'margin':
$cValue = Converter::cssToTwip($cValue);
$styles['spaceBefore'] = $cValue;
$styles['spaceAfter'] = $cValue;
$value = Converter::cssToTwip($value);
$styles['spaceBefore'] = $value;
$styles['spaceAfter'] = $value;
break;
case 'margin-top':
// BC change: up to ver. 0.17.0 incorrectly converted to points - Converter::cssToPoint($cValue)
$styles['spaceBefore'] = Converter::cssToTwip($cValue);
// BC change: up to ver. 0.17.0 incorrectly converted to points - Converter::cssToPoint($value)
$styles['spaceBefore'] = Converter::cssToTwip($value);
break;
case 'margin-bottom':
// BC change: up to ver. 0.17.0 incorrectly converted to points - Converter::cssToPoint($cValue)
$styles['spaceAfter'] = Converter::cssToTwip($cValue);
// BC change: up to ver. 0.17.0 incorrectly converted to points - Converter::cssToPoint($value)
$styles['spaceAfter'] = Converter::cssToTwip($value);
break;
case 'border-color':
self::mapBorderColor($styles, $cValue);
self::mapBorderColor($styles, $value);
break;
case 'border-width':
$styles['borderSize'] = Converter::cssToPoint($cValue);
$styles['borderSize'] = Converter::cssToPoint($value);
break;
case 'border-style':
$styles['borderStyle'] = self::mapBorderStyle($cValue);
$styles['borderStyle'] = self::mapBorderStyle($value);
break;
case 'width':
if (preg_match('/([0-9]+[a-z]+)/', $cValue, $matches)) {
if (preg_match('/([0-9]+[a-z]+)/', $value, $matches)) {
$styles['width'] = Converter::cssToTwip($matches[1]);
$styles['unit'] = \PhpOffice\PhpWord\SimpleType\TblWidth::TWIP;
} elseif (preg_match('/([0-9]+)%/', $cValue, $matches)) {
} elseif (preg_match('/([0-9]+)%/', $value, $matches)) {
$styles['width'] = $matches[1] * 50;
$styles['unit'] = \PhpOffice\PhpWord\SimpleType\TblWidth::PERCENT;
} elseif (preg_match('/([0-9]+)/', $cValue, $matches)) {
} elseif (preg_match('/([0-9]+)/', $value, $matches)) {
$styles['width'] = $matches[1];
$styles['unit'] = \PhpOffice\PhpWord\SimpleType\TblWidth::AUTO;
}
break;
case 'height':
$styles['height'] = Converter::cssToTwip($value);
$styles['exactHeight'] = true;
break;
case 'border':
case 'border-top':
@@ -778,9 +836,9 @@ class Html
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]+)/', $cValue, $matches)) {
if (false !== strpos($cKey, '-')) {
$tmp = explode('-', $cKey);
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 {
@@ -803,13 +861,13 @@ class Html
break;
case 'vertical-align':
// https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align
if (preg_match('#(?:top|bottom|middle|sub|baseline)#i', $cValue, $matches)) {
if (preg_match('#(?:top|bottom|middle|sub|baseline)#i', $value, $matches)) {
$styles['valign'] = self::mapAlignVertical($matches[0]);
}
break;
case 'page-break-after':
if ($cValue == 'always') {
if ($value == 'always') {
$styles['isPageBreak'] = true;
}
@@ -910,7 +968,10 @@ class Html
$tmpDir = Settings::getTempDir() . '/';
$match = [];
preg_match('/.+\.(\w+)$/', $src, $match);
$src = $tmpDir . uniqid() . '.' . $match[1];
$src = $tmpDir . uniqid();
if (isset($match[1])) {
$src .= '.' . $match[1];
}
$ifp = fopen($src, 'wb');
@@ -968,20 +1029,21 @@ class Html
* Transforms a HTML/CSS alignment into a \PhpOffice\PhpWord\SimpleType\Jc.
*
* @param string $cssAlignment
* @param bool $bidi
*
* @return null|string
*/
protected static function mapAlign($cssAlignment)
protected static function mapAlign($cssAlignment, $bidi)
{
switch ($cssAlignment) {
case 'right':
return Jc::END;
return $bidi ? Jc::START : Jc::END;
case 'center':
return Jc::CENTER;
case 'justify':
return Jc::BOTH;
default:
return Jc::START;
return $bidi ? Jc::END : Jc::START;
}
}

View File

@@ -61,6 +61,17 @@ class OLERead
public $wrkObjectPool = null;
public $summaryInformation = null;
public $docSummaryInfos = null;
public $numBigBlockDepotBlocks = null;
public $rootStartBlock = null;
public $sbdStartBlock = null;
public $extensionBlock = null;
public $numExtensionBlocks = null;
public $bigBlockChain = null;
public $smallBlockChain = null;
public $entry = null;
public $rootentry = null;
public $wrkObjectPoolelseif = null;
public $props = array();
/**
* Read the file

View File

@@ -1597,7 +1597,7 @@ class PclZip
if (is_string($p_options_list[$i + 1])) {
// ----- Remove spaces
$p_options_list[$i + 1] = strtr($p_options_list[$i + 1], ' ', '');
$p_options_list[$i + 1] = str_replace(' ', '', $p_options_list[$i + 1]);
// ----- Parse items
$v_work_list = explode(",", $p_options_list[$i + 1]);

View File

@@ -138,14 +138,15 @@ class Text
/**
* Return UTF8 encoded value.
*
* @param string $value
* @param null|string $value
*
* @return string
* @return ?string
*/
public static function toUTF8($value = '')
{
if (null !== $value && !self::isUTF8($value)) {
$value = utf8_encode($value);
// PHP8.2 : utf8_encode is deprecated, but mb_convert_encoding always usable
$value = (function_exists('mb_convert_encoding')) ? mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1') : utf8_encode($value);
}
return $value;

View File

@@ -0,0 +1,76 @@
<?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
*
* @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
*/
declare(strict_types=1);
namespace PhpOffice\PhpWord\Shared;
class Validate
{
public const CSS_WHITESPACE = [
'pre-wrap',
'normal',
'nowrap',
'pre',
'pre-line',
'initial',
'inherit',
];
public const CSS_GENERICFONT = [
'serif',
'sans-serif',
'monospace',
'cursive',
'fantasy',
'system-ui',
'math',
'emoji',
'fangsong',
];
/**
* Validate html css white-space value. It is expected that only pre-wrap and normal (default) are useful.
*
* @param string $value CSS White space
*
* @return string value if valid, empty string if not
*/
public static function validateCSSWhiteSpace(?string $value): string
{
if (in_array($value, self::CSS_WHITESPACE)) {
return $value;
}
return '';
}
/**
* Validate generic font for fallback for html.
*
* @param string $value Generic font name
*
* @return string Value if legitimate, empty string if not
*/
public static function validateCSSGenericFont(?string $value): string
{
if (in_array($value, self::CSS_GENERICFONT)) {
return $value;
}
return '';
}
}

View File

@@ -62,7 +62,7 @@ class XMLReader
$zip = new ZipArchive();
$zip->open($zipFile);
$content = $zip->getFromName($xmlFile);
$content = $zip->getFromName(ltrim($xmlFile, '/'));
$zip->close();
if ($content === false) {
@@ -97,24 +97,21 @@ class XMLReader
* Get elements.
*
* @param string $path
* @param DOMElement $contextNode
*
* @return DOMNodeList
* @return DOMNodeList<DOMElement>
*/
public function getElements($path, ?DOMElement $contextNode = null)
{
if ($this->dom === null) {
return [];
return new DOMNodeList(); // @phpstan-ignore-line
}
if ($this->xpath === null) {
$this->xpath = new DOMXpath($this->dom);
}
if (null === $contextNode) {
return $this->xpath->query($path);
}
$result = @$this->xpath->query($path, $contextNode);
return $this->xpath->query($path, $contextNode);
return empty($result) ? new DOMNodeList() : $result; // @phpstan-ignore-line
}
/**
@@ -141,7 +138,6 @@ class XMLReader
* Get element.
*
* @param string $path
* @param DOMElement $contextNode
*
* @return null|DOMElement
*/
@@ -159,7 +155,6 @@ class XMLReader
* Get element attribute.
*
* @param string $attribute
* @param DOMElement $contextNode
* @param string $path
*
* @return null|string
@@ -187,7 +182,6 @@ class XMLReader
* Get element value.
*
* @param string $path
* @param DOMElement $contextNode
*
* @return null|string
*/
@@ -205,7 +199,6 @@ class XMLReader
* Count elements.
*
* @param string $path
* @param DOMElement $contextNode
*
* @return int
*/
@@ -220,7 +213,6 @@ class XMLReader
* Element exists.
*
* @param string $path
* @param DOMElement $contextNode
*
* @return bool
*/

View File

@@ -30,8 +30,8 @@ use PhpOffice\PhpWord\Settings;
*
* @method bool addFile(string $filename, string $localname = null)
* @method bool addFromString(string $localname, string $contents)
* @method string getNameIndex(int $index)
* @method int locateName(string $name)
* @method false|string getNameIndex(int $index)
* @method false|int locateName(string $name)
*
* @since 0.10.0
*/
@@ -396,7 +396,7 @@ class ZipArchive
*
* @param string $filename Filename for the file in zip archive
*
* @return int
* @return false|int
*/
public function pclzipLocateName($filename)
{