update lock clucknut
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / build-and-push (push) Successful in 3m14s
Build, Push and Deploy / deploy-staging (push) Successful in 25s
Build, Push and Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-18 20:32:18 +07:00
parent 4554035227
commit dcaf267458
3359 changed files with 153185 additions and 205489 deletions

View File

@@ -0,0 +1,56 @@
# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
name: "Snapshot Tests"
on:
pull_request:
push:
branches:
- "master"
- "development"
- "test"
jobs:
tests:
name: "Tests"
runs-on: ubuntu-latest
steps:
- name: "Checkout"
uses: "actions/checkout@v4"
- name: Update sources and install ghostscript
uses: awalsh128/cache-apt-pkgs-action@master
with:
packages: ghostscript
version: 1.0
refresh: true
- name: Change ImageMagick policy to allow pdf->png conversion.
run: |
sudo sed -i 's/none/read|write/' /etc/ImageMagick-6/policy.xml
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
php-version: "8.5"
extensions: "mbstring, gd, bcmath, bz2, imagick"
tools: composer:v2
ini-values: error_reporting=-1
- name: "Install dependencies"
run: "composer install --no-interaction --no-progress"
- name: "Snapshot Tests"
run: composer test -- --group=snapshot
- uses: actions/upload-artifact@v4
if: ${{ failure() }}
with:
name: "phpunit-artifact"
path: tmp/artifacts/
if-no-files-found: ignore

View File

@@ -7,6 +7,7 @@
* Added support for `psr/http-message` v2 without dropping v1. (@markdorison, @apotek, @greg-1-anderson, @NigelCunningham #1907)
* PHP 8.3 support in mPDF 8.2.1
* Add support for `page-break-before: avoid;` and `page-break-after: avoid;` for tr elements inside tables
* Add support for custom `AssetFetcher` via the service container (@splitbrain, #2165)
Bugfixes
--------
@@ -14,6 +15,8 @@
* Replace character entities with characters when processing the `code` attribute in the `<barcode />` tag
* Escape XML predefined entities in XMP metadata (Fix for #2090)
* Enable Font Subsetting by Default (Fix for #1315)
* Fix `TypeError` with non-numeric `rotate` values like `none` or `90deg` on tables (@derrabus, #2178)
* Small change to better support list-style-type on list items within tables
mPDF 8.1.x
===========================

View File

@@ -38,12 +38,13 @@
"squizlabs/php_codesniffer": "^3.5.0",
"tracy/tracy": "~2.5",
"yoast/phpunit-polyfills": "^1.0"
},
},
"suggest": {
"ext-bcmath": "Needed for generation of some types of barcodes",
"ext-zlib": "Needed for compression of embedded resources, such as fonts",
"ext-xml": "Needed mainly for SVG manipulation"
},
"ext-xml": "Needed mainly for SVG manipulation",
"ext-imagick": "Needed if developing the Mpdf library"
},
"autoload": {
"psr-4": {
"Mpdf\\": "src/"
@@ -54,7 +55,9 @@
},
"autoload-dev": {
"psr-4": {
"Mpdf\\": "tests/Mpdf"
"Mpdf\\": "tests/Mpdf",
"Snapshots\\": "tests/Snapshots",
"Issues\\": "tests/Issues"
},
"files": [
"src/functions-dev.php"
@@ -65,6 +68,7 @@
"php -r \"chmod('./tmp', 0777);\""
],
"cs": "@php vendor/bin/phpcs --report-width=160 --standard=ruleset.xml --severity=1 --warning-severity=0 --extensions=php src utils tests",
"cs:fix": "@php vendor/bin/phpcbf --standard=ruleset.xml --extensions=php src utils tests",
"test": "@php vendor/bin/phpunit",
"coverage": "@php vendor/bin/phpunit --coverage-text"
},

View File

@@ -36,18 +36,6 @@ parameters:
count: 1
path: src/Color/ColorSpaceRestrictor.php
-
message: '#^Binary operation "\+" between non\-empty\-string and 0 results in an error\.$#'
identifier: binaryOp.invalid
count: 1
path: src/CssManager.php
-
message: '#^Variable \$tag might not be defined\.$#'
identifier: variable.undefined
count: 1
path: src/CssManager.php
-
message: '#^Method Mpdf\\Mpdf\:\:GetJspacing\(\) invoked with 4 parameters, 5 required\.$#'
identifier: arguments.count
@@ -885,13 +873,13 @@ parameters:
-
message: '#^Offset int on array\{\} in isset\(\) does not exist\.$#'
identifier: isset.offset
count: 2
count: 1
path: src/Otl.php
-
message: '#^Offset int\<0, max\> on array\{\} in isset\(\) does not exist\.$#'
identifier: isset.offset
count: 4
count: 1
path: src/Otl.php
-

View File

@@ -1,5 +1,6 @@
<phpunit
bootstrap="tests/bootstrap.php"
cacheResultFile="tmp/.phpunit.result.cache"
colors="true"
backupGlobals="false"
beStrictAboutTestsThatDoNotTestAnything="false">
@@ -10,6 +11,12 @@
</testsuite>
</testsuites>
<groups>
<exclude>
<group>snapshot</group>
</exclude>
</groups>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>

View File

@@ -10,7 +10,7 @@
use Mpdf\PsrLogAwareTrait\PsrLogAwareTrait;
use Psr\Log\LoggerInterface;
class AssetFetcher implements \Psr\Log\LoggerAwareInterface
class AssetFetcher implements \Psr\Log\LoggerAwareInterface, \Mpdf\AssetFetcherInterface
{
use PsrLogAwareTrait;

View File

@@ -0,0 +1,16 @@
<?php
namespace Mpdf;
interface AssetFetcherInterface
{
/**
* Fetch data from a given path, either local or remote.
*
* @param string $path The path to fetch data from.
* @param string|null $originalSrc The original source path, if applicable.
* @return string The fetched data.
* @throws \Mpdf\Exception\AssetFetchingException If fetching fails.
*/
public function fetchDataFromPath($path, $originalSrc = null);
}

76
vendor/mpdf/mpdf/src/Buffer.php vendored Normal file
View File

@@ -0,0 +1,76 @@
<?php
namespace Mpdf;
class Buffer
{
/** @var array<int, string> */
private $contents = [];
/** @var int */
private $length = 0;
public function __construct()
{
}
public function append($content, $newLine = false)
{
if ($content === null || $content === '') {
return;
}
$content = (string) $content;
$contentLength = strlen($content);
// Do not create an additional buffer entry if the content is relatively small.
if ($newLine && $contentLength < 1000000) {
$content .= "\n";
++$contentLength;
}
$this->contents[] = $content;
$this->length += $contentLength;
if ($newLine && $contentLength >= 1000000) {
$this->contents[] = "\n";
++$this->length;
}
}
public function getLength()
{
return $this->length;
}
public function writeToFile($handle)
{
foreach ($this->contents as $content) {
fwrite($handle, $content);
}
}
public function writeToOutput()
{
foreach ($this->contents as $content) {
echo $content;
}
}
public function writeToString()
{
return implode('', $this->contents);
}
public function getHash()
{
$hash = '';
foreach ($this->contents as $content) {
$hash = md5($hash.$content);
}
return $hash;
}
}

View File

@@ -42,11 +42,25 @@ protected function createBasePath($basePath)
protected function createDirectory($basePath)
{
$permissions = $this->getPermission($this->getExistingParentDirectory($basePath));
if (! mkdir($basePath, $permissions, true)) {
$parentPath = $this->getExistingParentDirectory($basePath);
$permissions = $this->getPermission($parentPath);
if (!mkdir($basePath, $permissions, true)) {
return false;
}
/* Check if umask modified the permissions and reset any created directories */
if (($permissions & ~umask()) !== $permissions) {
$basePath = realpath($basePath);
$folders = explode('/', substr($basePath, strlen($parentPath) + 1));
for ($i = 1, $total = count($folders); $i <= $total; $i++) {
$path = $parentPath . '/';
$path .= implode('/', array_slice($folders, 0, $i));
chmod($path, $permissions);
}
}
return true;
}

View File

@@ -0,0 +1,142 @@
<?php
namespace Mpdf\Css;
use Mpdf\Exception\InvalidArgumentException;
class BorderMerger
{
/**
* @var array<int> Border dominance levels for cell borders (top/right/bottom/left)
*/
private $borderDominance = [
'T' => 0,
'R' => 0,
'B' => 0,
'L' => 0,
];
/**
* Merge borders into CSS properties.
*
* @param array $newProperties properties to merge from
* @param array $cssProperties current CSS properties (passed by reference)
* @return void
*/
public function mergeBorderProperties($newProperties, &$cssProperties)
{
foreach (['TOP', 'RIGHT', 'BOTTOM', 'LEFT'] as $side) {
$this->mergeSideBorder($side, $newProperties, $cssProperties);
}
}
/**
* Merge border properties for a specific side.
*
* Helper method for mergeBorderProperties to handle merging of individual side properties
* (style, width, color) into the shorthand border property.
*
* @param string $side Side to merge (TOP, RIGHT, BOTTOM, LEFT)
* @param array $properties Source border properties
* @param array $cssProperties Target CSS properties (passed by reference)
* @return void
*/
protected function mergeSideBorder($side, $properties, &$cssProperties)
{
// Merges $a['BORDER-TOP-STYLE'] to $cssProperties['BORDER-TOP'] etc.
$defaults = [
'WIDTH' => '0px',
'STYLE' => 'none',
'COLOR' => '#000000'
];
$borderKey = 'BORDER-' . $side;
$currentBorder = isset($cssProperties[$borderKey]) ? trim($cssProperties[$borderKey]) : '';
foreach (['STYLE', 'WIDTH', 'COLOR'] as $el) {
$propertyKey = $borderKey . '-' . $el;
if (!isset($properties[$propertyKey])) {
continue;
}
$value = trim($properties[$propertyKey]);
if ($currentBorder) {
// Update existing border value
if ($el === 'STYLE') {
$cssProperties[$borderKey] = preg_replace('/(\S+)\s+(\S+)\s+(\S+)/', '\\1 ' . $value . ' \\3', $currentBorder);
} elseif ($el === 'WIDTH') {
$cssProperties[$borderKey] = preg_replace('/(\S+)\s+(\S+)\s+(\S+)/', $value . ' \\2 \\3', $currentBorder);
} else { // COLOR
$cssProperties[$borderKey] = preg_replace('/(\S+)\s+(\S+)\s+(\S+)/', '\\1 \\2 ' . $value, $currentBorder);
}
$currentBorder = $cssProperties[$borderKey]; // Update current border for next iteration
} else {
// Build new border from scratch with defaults
if (!isset($borderParts)) {
$borderParts = $defaults;
}
$borderParts[$el] = $value;
$cssProperties[$borderKey] = $borderParts['WIDTH'] . ' ' . $borderParts['STYLE'] . ' ' . $borderParts['COLOR'];
$currentBorder = $cssProperties[$borderKey];
}
}
}
/**
* Set border dominance level for table cells.
*
* Used in table rendering to determine which cell borders take
* precedence when cells share borders.
*
* @param array $prop CSS properties containing border definitions
* @param int $val Dominance level value
* @return void
*/
public function setDominanceFromProperties($prop, $val)
{
if (!empty($prop['BORDER-TOP'])) {
$this->setBorderDominance('T', $val);
}
if (!empty($prop['BORDER-RIGHT'])) {
$this->setBorderDominance('R', $val);
}
if (!empty($prop['BORDER-BOTTOM'])) {
$this->setBorderDominance('B', $val);
}
if (!empty($prop['BORDER-LEFT'])) {
$this->setBorderDominance('L', $val);
}
}
/**
* Set border dominance level for a specific side.
*
* @param string $side T|R|B|L
* @param int $val Dominance value
* @throws InvalidArgumentException
*/
public function setBorderDominance($side, $val)
{
if (!isset($this->borderDominance[$side])) {
throw new InvalidArgumentException('Invalid border dominance value:' . $side);
}
$this->borderDominance[$side] = (int) $val;
}
/**
* Get border dominance level for a specific side.
*
* @param string $side T|R|B|L
* @return int Dominance value
*/
public function getBorderDominance($side)
{
return isset($this->borderDominance[$side]) ? $this->borderDominance[$side] : 0;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Mpdf\Css;
class CommentParser
{
/**
* Remove mPDF-specific and general HTML comments from content.
*
* Removes <!--mpdf and mpdf--> markers and all HTML comments.
*
* @param string $html HTML content to clean
* @return string HTML with comments removed
*/
public function removeHtmlComments($html)
{
$html = preg_replace('/<!--mpdf/i', '', $html);
$html = preg_replace('/mpdf-->/i', '', $html);
$html = preg_replace('/<!--.*?-->/s', ' ', $html);
return $html;
}
/**
* Remove HTML and CSS comments from style blocks.
*
* Removes both HTML comments (<!-- -->) and CSS comments from
* <style> tag contents while preserving the structure.
*
* @param string $html HTML content with style tags
* @return string HTML with cleaned style blocks
*/
public function removeCommentsFromStyleBlocks($html)
{
preg_match_all('/<style.*?>(.*?)<\/style>/si', $html, $m);
if (count($m[1]) === 0) {
return $html;
}
foreach ($m[1] as $style) {
$sub = str_replace(['<!--', '-->'], ' ', $style);
$sub = '>' . preg_replace('|/\*.*?\*/|s', ' ', $sub) . '</style>';
$html = str_replace('>' . $style . '</style>', $sub, $html);
}
return $html;
}
}

184
vendor/mpdf/mpdf/src/Css/CssLoader.php vendored Normal file
View File

@@ -0,0 +1,184 @@
<?php
namespace Mpdf\Css;
use Mpdf\AssetFetcher;
use Mpdf\Cache;
use Mpdf\Exception\AssetFetchingException;
use Mpdf\Mpdf;
use Mpdf\MpdfException;
use Mpdf\Utils\Path;
class CssLoader
{
/**
* @var Mpdf
*/
private $mpdf;
/**
* @var AssetFetcher
*/
private $assetFetcher;
/**
* @var Cache
*/
private $cache;
public function __construct(Mpdf $mpdf, AssetFetcher $assetFetcher, Cache $cache)
{
$this->mpdf = $mpdf;
$this->assetFetcher = $assetFetcher;
$this->cache = $cache;
}
/**
* Fetch and return the CSS from $path
*
* @param string $path
* @return string
* @throws MpdfException If asset fetching issue, is through when $mpdf->debug = true
*/
public function loadStylesheet($path)
{
$path = preg_replace('/\.css\?.*$/', '.css', $path);
try {
$data = $this->assetFetcher->fetchDataFromPath($path);
if (!$data) {
$path = !$this->mpdf->basepathIsLocal ? Path::normalizeLocalFilePath($path) : $path;
$data = $this->assetFetcher->fetchDataFromPath($path);
}
} catch (AssetFetchingException $e) {
$data = ''; // do nothing
if ($this->mpdf->debug) {
throw new MpdfException($e->getMessage(), 0, E_ERROR, null, null, $e);
}
}
return $data;
}
/**
* Extract external stylesheet URLs from HTML.
*
* Finds all external CSS file references including:
* - <link rel="stylesheet" href="...">
* - <link href="..." rel="stylesheet">
* - @import url(...)
* - @import "..."
*
* @param string $html HTML content to scan
* @return array Array of CSS file URLs
*/
public function extractExternalStylesheetUrls($html)
{
$cssUrls = [];
// <link rel="stylesheet" href="...">
if (preg_match_all('/<link[^>]*rel=["\']stylesheet["\'][^>]*href=["\']([^>"\']*)["\'].*?>/si', $html, $cxt)) {
$cssUrls = $cxt[1];
}
// <link href="..." rel="stylesheet">
if (preg_match_all('/<link[^>]*href=["\']([^>"\']*)["\'][^>]*?rel=["\']stylesheet["\'].*?>/si', $html, $cxt)) {
$cssUrls = array_merge($cssUrls, $cxt[1]);
}
// @import url(...)
if (preg_match_all('/@import url\([\'\"]{0,1}(\S*?\.css(\?[^\s\'\"]+)?)[\'\"]{0,1}\)\;?/si', $html, $cxt)) {
$cssUrls = array_merge($cssUrls, $cxt[1]);
}
// @import "..."
if (preg_match_all('/@import (?!url)[\'\"]{0,1}(\S*?\.css(\?[^\s\'\"]+)?)[\'\"]{0,1}\;?/si', $html, $cxt)) {
$cssUrls = array_merge($cssUrls, $cxt[1]);
}
return $cssUrls;
}
/**
* Locate embedded @import stylesheets in other stylesheets and fix url paths
* (including background-images) relative to stylesheet
*
* @param string $stylesheetCss
* @param string $path
* @param array $externalCss
* @param int $externalCssCount
* @return string
*/
public function processExternalCssImports($stylesheetCss, $path, &$externalCss, &$externalCssCount)
{
$css = '';
$cssBasePath = preg_replace('/\/[^\/]*$/', '', $path) . '/';
if (preg_match_all('/@import url\([\'\"]{0,1}(.*?\.css(\?\S+)?)[\'\"]{0,1}\)/si', $stylesheetCss, $cxtem)) {
foreach ($cxtem[1] as $cxtembedded) {
// path is relative to original stylesheet!!
$externalCss[] = Path::relativeToAbsolutePath($cxtembedded, $cssBasePath);
$externalCssCount++;
}
}
$css .= ' ' . $this->resolveBackgroundUrls($stylesheetCss, $cssBasePath);
return $css;
}
/**
* Resolve background image URLs in CSS.
*
* Converts relative URLs to absolute paths using Path::relativeToAbsolute.
* Skips data URIs which are already absolute.
*
* @param string $cssStr CSS string potentially containing background URLs
* @param string|null $basePath Optional base path for resolving relative URLs
* @return string CSS string with resolved URLs
*/
public function resolveBackgroundUrls($cssStr, $basePath = null)
{
if (!preg_match_all('/(background[^;]*url\s*\(\s*[\'"]{0,1})([^)\'"]*)([\'"]{0,1}\s*\))/si', $cssStr, $cxtem)) {
return $cssStr;
}
$basePath = $basePath ?: $this->mpdf->basepath;
foreach ($cxtem[0] as $i => $value) {
$embedded = $cxtem[2][$i];
if (!preg_match('/^data:image/i', $embedded)) {
$newPath = Path::relativeToAbsolutePath($embedded, $basePath);
$cssStr = str_replace($cxtem[0][$i], ($cxtem[1][$i] . $newPath . $cxtem[3][$i]), $cssStr);
}
}
return $cssStr;
}
/**
* Process data URI images in CSS.
*
* Converts data URI images to temporary files for processing.
* Example: url(data:image/png;base64,...) becomes url("tempfile.png")
*
* @param string $cssStr CSS string potentially containing data URIs
* @return string CSS string with data URIs replaced by temp file references
* @throws \Random\RandomException
*/
public function processDataUriImages($cssStr)
{
preg_match_all("/(url\(data:image\/(jpeg|gif|png);base64,(.*?)\))/si", $cssStr, $idata);
if (count($idata[0]) === 0) {
return $cssStr;
}
foreach ($idata[0] as $i => $value) {
$file = $this->cache->write('_tempCSSidata' . random_int(1, 10000) . '_' . $i . '.' . $idata[2][$i], base64_decode($idata[3][$i]));
$cssStr = str_replace($idata[0][$i], 'url("' . $file . '")', $cssStr);
}
return $cssStr;
}
}

1060
vendor/mpdf/mpdf/src/Css/CssMerger.php vendored Normal file

File diff suppressed because it is too large Load Diff

374
vendor/mpdf/mpdf/src/Css/CssParser.php vendored Normal file
View File

@@ -0,0 +1,374 @@
<?php
namespace Mpdf\Css;
use Mpdf\Utils\Arrays;
use Mpdf\Utils\Path;
use Mpdf\Mpdf;
use Mpdf\Cache;
use Mpdf\SizeConverter;
use Mpdf\Color\ColorConverter;
use Mpdf\AssetFetcher;
class CssParser
{
/**
* @var Mpdf
*/
private $mpdf;
/**
* @var CssLoader
*/
private $cssLoader;
/**
* @var MediaQueryProcessor
*/
private $mediaQueryProcessor;
/**
* @var CommentParser
*/
private $commentParser;
/**
* @var InlineStyleParser
*/
private $inlineStyleParser;
/**
* @var SelectorParser
*/
private $selectorParser;
/**
* @var NormalizeProperties
*/
private $normalizeProperties;
/**
* @var ShadowParser
*/
private $shadowParser;
/**
* CSS for simple selectors.
*
* Stores CSS properties for simple selectors (depth 1).
* Format:
* [
* 'P' => [
* 'COLOR' => '#FF0000',
* 'FONT-SIZE' => '12pt',
* ],
* 'CLASS>>MYCLASS' => [
* 'BORDER' => '1px solid black',
* ],
* ...
* ]
*
* @var array
*/
private $css = [];
/**
* CSS for cascaded selectors.
*
* Stores CSS properties for nested/cascaded selectors (depth > 1).
* Format is a nested array mirroring the selector hierarchy.
* Example for "DIV.myclass P":
* [
* 'DIV' => [
* 'CLASS>>MYCLASS' => [
* 'P' => [
* 'COLOR' => '#0000FF',
* 'depth' => 3
* ]
* ]
* ]
* ]
*
* @var array
*/
private $cascadeCSS = [];
/**
* @var array An index used to filter redundant class names before passing to Arrays::allUniqueSortedCombinations
*/
private $usedClassNames = [];
/**
* @var int Maximum number of classes found in a single selector
*/
private $maxClassDepth = 1;
public function __construct(
Mpdf $mpdf,
Cache $cache,
SizeConverter $sizeConverter,
ColorConverter $colorConverter,
AssetFetcher $assetFetcher
) {
$this->mpdf = $mpdf;
$this->normalizeProperties = new NormalizeProperties($mpdf, $sizeConverter, $colorConverter);
$this->cssLoader = new CssLoader($mpdf, $assetFetcher, $cache);
$this->mediaQueryProcessor = new MediaQueryProcessor($mpdf);
$this->commentParser = new CommentParser();
$this->inlineStyleParser = new InlineStyleParser($this->normalizeProperties);
$this->selectorParser = new SelectorParser($mpdf);
$this->shadowParser = new ShadowParser($mpdf, $sizeConverter, $colorConverter);
}
/**
* Read and parse CSS from HTML content.
*
* @param string $html HTML content containing CSS
* @return string
*/
public function parse($html)
{
$this->css = [];
$this->cascadeCSS = [];
$ind = 0;
$css = '';
$html = $this->mediaQueryProcessor->filterByMediaQuery($html, '/<style[^>]*media=["\']([^"\'>]*)["\'].*?<\/style>/is');
$html = $this->mediaQueryProcessor->filterByMediaQuery($html, '/<link[^>]*media=["\']([^"\'>]*)["\'].*?>/is');
$html = $this->commentParser->removeCommentsFromStyleBlocks($html);
$html = $this->commentParser->removeHtmlComments($html);
$externalCss = $this->cssLoader->extractExternalStylesheetUrls($html);
$externalCssCount = count($externalCss);
while ($externalCssCount) {
$path = htmlspecialchars_decode($externalCss[$ind]);
$path = Path::relativeToAbsolutePath($path, $this->mpdf->basepath);
if (strpos($path, '//') === false) { // mPDF 5.7.3
$path = preg_replace('/\.css\?.*$/', '.css', $path);
}
$stylesheetCss = $this->cssLoader->loadStylesheet($path);
if ($stylesheetCss) {
$css .= $this->cssLoader->processExternalCssImports($stylesheetCss, $path, $externalCss, $externalCssCount);
}
$externalCssCount--;
$ind++;
}
// CSS as <style> in HTML document
$regexp = '/<style.*?>(.*?)<\/style>/si';
if (preg_match_all($regexp, $html, $cssBlock)) {
$css .= ' ' . $this->cssLoader->resolveBackgroundUrls(implode(' ', $cssBlock[1]));
}
$css = preg_replace('|/\*.*?\*/|s', ' ', $css);
$css = preg_replace('/[\s\n\r\t\f]/s', ' ', $css);
$css = $this->mediaQueryProcessor->processMediaQueries($css);
$css = $this->cssLoader->processDataUriImages($css);
$css = preg_replace('/(<\!\-\-|\-\->)/s', ' ', $css);
$css = $this->inlineStyleParser->processUrlsInCss($css);
$this->processCssString($css);
// Remove CSS (tags and content), if any (it can be <style> or <style type="txt/css">)
$html = preg_replace('/<style.*?>(.*?)<\/style>/si', '', $html);
return $html;
}
/**
* @return array
*/
public function getCss()
{
return $this->css;
}
/**
* @return array
*/
public function getCascadeCss()
{
return $this->cascadeCSS;
}
/**
* @return array
*/
public function getUsedClassNames()
{
return array_keys($this->usedClassNames);
}
/**
* @return int
*/
public function getMaxClassDepth()
{
return $this->maxClassDepth;
}
/**
* @param string $css
* @return void
*/
private function processCssString($css)
{
preg_match_all('/(.*?)\{(.*?)\}/', $css, $styles);
$count = count($styles[1]);
for ($i = 0; $i < $count; $i++) {
$classProperties = $this->parseCssProperties($styles[2][$i]);
$tagName = strtoupper(trim($styles[1][$i]));
$tags = explode(',', $tagName);
foreach ($tags as $tag) {
$this->processCssSelector($tag, $classProperties);
}
}
}
/**
* Process a CSS selector.
*
* @param string $selector Selector string
* @param array $classProperties CSS properties
* @return void
*/
private function processCssSelector($selector, $classProperties)
{
// store classes in an index for faster lookups
if (strpos($selector, '.') !== false && preg_match_all('/\.([a-zA-Z0-9_\-]+)/', $selector, $matches)) {
foreach ($matches[1] as $className) {
$this->usedClassNames[$className] = true;
}
$classCount = count($matches[1]);
if ($classCount > $this->maxClassDepth) {
$this->maxClassDepth = $classCount;
}
}
if (preg_match('/NTH-CHILD\((\s*(([\-+]?\d*)N(\s*[\-+]\s*\d+)?|[\-+]?\d+|ODD|EVEN)\s*)\)/', $selector, $m)) {
$selector = preg_replace('/NTH-CHILD\(.*\)/', 'NTH-CHILD(' . str_replace(' ', '', $m[1]) . ')', $selector);
}
$tags = preg_split('/\s+/', trim($selector));
$level = count($tags);
if (trim($tags[0]) === '@PAGE') {
$tag = $this->selectorParser->parsePageSelector($tags);
if ($tag && isset($this->css[$tag])) {
$this->css[$tag] = Arrays::uniqueRecursiveMerge($this->css[$tag], $classProperties);
} elseif ($tag) {
$this->css[$tag] = $classProperties;
}
return;
}
if ($level === 1) {
$tag = $this->selectorParser->parseSimpleSelector($tags);
if ($tag && isset($this->css[$tag])) {
$this->css[$tag] = Arrays::uniqueRecursiveMerge($this->css[$tag], $classProperties);
} elseif ($tag) {
$this->css[$tag] = $classProperties;
}
return;
}
$cascade = $this->selectorParser->parseCascadedSelector($tags);
if (empty($cascade)) {
return;
}
$cascadeCSS = &$this->cascadeCSS;
foreach ($cascade as $tag) {
$cascadeCSS = &$cascadeCSS[$tag];
}
$cascadeCSS = Arrays::uniqueRecursiveMerge($cascadeCSS, $classProperties);
$cascadeCSS['depth'] = $level;
}
/**
* Parse CSS property string into an array.
*
* @param string $rawStyles CSS style string (e.g. "color: red; font-size: 12px")
* @return array Associative array of CSS properties
*/
public function parseCssProperties($rawStyles)
{
$classProperties = [];
$styles = explode(';', trim($rawStyles));
foreach ($styles as $style) {
if (empty(trim($style))) {
continue;
}
// Changed to allow style="background: url('http://www.bpm1.com/bg.jpg')"
$tmp = explode(':', $style, 2);
$property = strtoupper(trim($tmp[0]));
$value = isset($tmp[1]) ? $tmp[1] : '';
$value = str_replace('%ZZ', ';', $value); // restore URL placeholder
$value = preg_replace('/\s*!important/i', '', $value);
$value = trim($value);
if (empty($property) || strlen($value) === 0) {
continue;
}
// Ignores -webkit-gradient so doesn't override -moz-
if (($property === 'BACKGROUND-IMAGE' || $property === 'BACKGROUND') &&
stripos($value, '-webkit-gradient') !== false
) {
continue;
}
$classProperties[$property] = $value;
}
return $this->normalizeProperties->normalize($classProperties);
}
/**
* Parse inline CSS style attribute.
*
* @param string $html CSS string from style attribute
* @return array Parsed CSS properties
*/
public function parseInlineCss($html)
{
return $this->inlineStyleParser->parse($html);
}
/**
* Parse box-shadow CSS property.
*
* Converts box-shadow CSS property string into array format used internally.
* Handles multiple shadows, inset shadows, blur, spread, and colors.
*
* @param string $value Box-shadow property value
* @return array Array of shadow definitions
*/
public function parseBoxShadow($value)
{
return $this->shadowParser->parseBoxShadow($value);
}
/**
* Parse text-shadow CSS property.
*
* Converts text-shadow CSS property string into array format used internally.
* Handles multiple shadows, blur, and colors.
*
* @param string $value Text-shadow property value
* @return array Array of text shadow definitions
*/
public function parseTextShadow($value)
{
return $this->shadowParser->parseTextShadow($value);
}
}

View File

@@ -0,0 +1,189 @@
<?php
namespace Mpdf\Css;
use Mpdf\Color\ColorConverter;
class InlinePropertyConverter
{
/**
* @var ColorConverter
*/
private $colorConverter;
public function __construct(ColorConverter $colorConverter)
{
$this->colorConverter = $colorConverter;
}
/**
* Convert inline properties back to CSS.
*
* Transforms internal inline property format (used in TextVars) back into
* CSS property array. Used for property inheritance and cascading.
*
* @param array $properties Inline properties array
* @return array Converted CSS properties
*/
public function convert($properties)
{
$css = [];
if (!empty($properties['family'])) {
$css['FONT-FAMILY'] = $properties['family'];
}
if (!empty($properties['I'])) {
$css['FONT-STYLE'] = 'italic';
}
if (!empty($properties['sizePt'])) {
$css['FONT-SIZE'] = $properties['sizePt'] . 'pt';
}
if (!empty($properties['B'])) {
$css['FONT-WEIGHT'] = 'bold';
}
if (!empty($properties['colorarray'])) {
$css['COLOR'] = $this->colorConverter->colAtoString($properties['colorarray']);
}
if (!empty($properties['lSpacingCSS'])) {
$css['LETTER-SPACING'] = $properties['lSpacingCSS'];
}
if (!empty($properties['wSpacingCSS'])) {
$css['WORD-SPACING'] = $properties['wSpacingCSS'];
}
if (!empty($properties['textparam'])) {
if (isset($properties['textparam']['hyphens'])) {
$hyphens = (int) $properties['textparam']['hyphens'];
switch ($hyphens) {
case 1:
$css['HYPHENS'] = 'auto';
break;
case 2:
$css['HYPHENS'] = 'none';
break;
default:
$css['HYPHENS'] = 'manual';
}
}
if (isset($properties['textparam']['outline-s']) && !$properties['textparam']['outline-s']) {
$css['TEXT-OUTLINE'] = 'none';
}
if (!empty($properties['textparam']['outline-COLOR'])) {
$css['TEXT-OUTLINE-COLOR'] = $this->colorConverter->colAtoString($properties['textparam']['outline-COLOR']);
}
if (!empty($properties['textparam']['outline-WIDTH'])) {
$css['TEXT-OUTLINE-WIDTH'] = $properties['textparam']['outline-WIDTH'] . 'mm';
}
}
if (!empty($properties['textvar'])) {
// CSS says text-decoration is not inherited, but IE7 does??
if ($properties['textvar'] & TextVars::FD_LINETHROUGH) {
if ($properties['textvar'] & TextVars::FD_UNDERLINE) {
$css['TEXT-DECORATION'] = 'underline line-through';
} else {
$css['TEXT-DECORATION'] = 'line-through';
}
} elseif ($properties['textvar'] & TextVars::FD_UNDERLINE) {
$css['TEXT-DECORATION'] = 'underline';
} else {
$css['TEXT-DECORATION'] = 'none';
}
if ($properties['textvar'] & TextVars::FA_SUPERSCRIPT) {
$css['VERTICAL-ALIGN'] = 'super';
} elseif ($properties['textvar'] & TextVars::FA_SUBSCRIPT) {
$css['VERTICAL-ALIGN'] = 'sub';
} else {
$css['VERTICAL-ALIGN'] = 'baseline';
}
if ($properties['textvar'] & TextVars::FT_CAPITALIZE) {
$css['TEXT-TRANSFORM'] = 'capitalize';
} elseif ($properties['textvar'] & TextVars::FT_UPPERCASE) {
$css['TEXT-TRANSFORM'] = 'uppercase';
} elseif ($properties['textvar'] & TextVars::FT_LOWERCASE) {
$css['TEXT-TRANSFORM'] = 'lowercase';
} else {
$css['TEXT-TRANSFORM'] = 'none';
}
if ($properties['textvar'] & TextVars::FC_KERNING) {
$css['FONT-KERNING'] = 'normal';
} else {
$css['FONT-KERNING'] = 'none';
} // ignore 'auto' as default already applied
if ($properties['textvar'] & TextVars::FA_SUPERSCRIPT) {
$css['FONT-VARIANT-POSITION'] = 'super';
} elseif ($properties['textvar'] & TextVars::FA_SUBSCRIPT) {
$css['FONT-VARIANT-POSITION'] = 'sub';
} else {
$css['FONT-VARIANT-POSITION'] = 'normal';
}
if ($properties['textvar'] & TextVars::FC_SMALLCAPS) {
$css['FONT-VARIANT-CAPS'] = 'small-caps';
}
}
if (isset($properties['fontLanguageOverride'])) {
if ($properties['fontLanguageOverride']) {
$css['FONT-LANGUAGE-OVERRIDE'] = $properties['fontLanguageOverride'];
} else {
$css['FONT-LANGUAGE-OVERRIDE'] = 'normal';
}
}
// All the variations of font-variant-* we are going to set as font-feature-settings...
if (!empty($properties['OTLtags'])) {
$fontFeature = [];
if (!empty($properties['OTLtags']['Minus'])) {
$f = preg_split('/\s+/', trim($properties['OTLtags']['Minus']));
foreach ($f as $ff) {
$fontFeature[] = "'" . $ff . "' 0";
}
}
if (!empty($properties['OTLtags']['FFMinus'])) {
$f = preg_split('/\s+/', trim($properties['OTLtags']['FFMinus']));
foreach ($f as $ff) {
$fontFeature[] = "'" . $ff . "' 0";
}
}
if (!empty($properties['OTLtags']['Plus'])) {
$f = preg_split('/\s+/', trim($properties['OTLtags']['Plus']));
foreach ($f as $ff) {
$fontFeature[] = "'" . $ff . "' 1";
}
}
if (!empty($properties['OTLtags']['FFPlus'])) { // May contain numeric value e.g. salt4
$f = preg_split('/\s+/', trim($properties['OTLtags']['FFPlus']));
foreach ($f as $ff) {
if (strlen($ff) > 4) {
$fontFeature[] = "'" . substr($ff, 0, 4) . "' " . substr($ff, 4);
} else {
$fontFeature[] = "'" . $ff . "' 1";
}
}
}
$css['FONT-FEATURE-SETTINGS'] = implode(', ', $fontFeature);
}
return $css;
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Mpdf\Css;
class InlineStyleParser
{
/**
* @var NormalizeProperties
*/
private $normalizeProperties;
public function __construct(NormalizeProperties $normalizeProperties)
{
$this->normalizeProperties = $normalizeProperties;
}
/**
* Parse inline CSS style attribute.
*
* Parses a CSS string from an HTML style attribute and returns
* an array of CSS properties.
*
* @param string $html CSS string from style attribute
* @return array Parsed CSS properties
*/
public function parse($html)
{
$html = htmlspecialchars_decode($html); // mPDF 5.7.4 URLs
// mPDF 5.7.4 URLs
// Characters "(", ")", and ";" in url() e.g. background-image, cause problems parsing the CSS string
// URLencode ( and ), but change ";" to a code which can be converted back after parsing (so as not to confuse ;
// with a segment delimiter in the URI)
$html = $this->processUrlsInCss($html);
// Fix incomplete CSS code
$size = strlen($html) - 1;
if (substr($html, $size, 1) !== ';') {
$html .= ';';
}
// Make CSS[Name-of-the-class] = array(key => value)
$regexp = '|\\s*?(\\S+?):(.+?);|i';
preg_match_all($regexp, $html, $styleinfo);
$properties = $styleinfo[1];
$values = $styleinfo[2];
// Array-properties and Array-values must have the SAME SIZE!
$classproperties = [];
$properties_count = count($properties);
for ($i = 0; $i < $properties_count; $i++) {
// Ignores -webkit-gradient so doesn't override -moz-
if ((strtoupper($properties[$i]) === 'BACKGROUND-IMAGE' || strtoupper($properties[$i]) === 'BACKGROUND') && false !== stripos($values[$i], '-webkit-gradient')) {
continue;
}
$values[$i] = str_replace('%ZZ', ';', $values[$i]); // mPDF 5.7.4 URLs
$classproperties[strtoupper($properties[$i])] = trim($values[$i]);
}
return $this->normalizeProperties->normalize($classproperties);
}
/**
* Process URLs in CSS strings by encoding special characters.
*
* Characters "(", ")", and ";" in url() can cause problems parsing CSS.
* This method URLencodes ( and ), and temporarily encodes ";" to prevent
* confusion with CSS segment delimiters.
*
* @param string $css CSS string containing url() references
* @return string CSS string with processed URLs
*/
public function processUrlsInCss($css)
{
if (strpos($css, 'url(') === false) {
return $css;
}
// Process urls with double quotes
preg_match_all('/url\(\"(.*?)\"\)/', $css, $m);
foreach ($m[1] as $i => $url) {
$tmp = str_replace(['(', ')', ';'], ['%28', '%29', '%ZZ'], $m[1][$i]);
$css = str_replace($m[0][$i], 'url(\'' . $tmp . '\')', $css);
}
// Process urls with single quotes
preg_match_all('/url\(\'(.*?)\'\)/', $css, $m);
foreach ($m[1] as $i => $url) {
$tmp = str_replace(['(', ')', ';'], ['%28', '%29', '%ZZ'], $m[1][$i]);
$css = str_replace($m[0][$i], 'url(\'' . $tmp . '\')', $css);
}
// Process urls without quotes
preg_match_all('/url\(([^\'\"].*?[^\'\"])\)/', $css, $m);
foreach ($m[1] as $i => $url) {
$tmp = str_replace(['(', ')', ';'], ['%28', '%29', '%ZZ'], $m[1][$i]);
$css = str_replace($m[0][$i], 'url(\'' . $tmp . '\')', $css);
}
return $css;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Mpdf\Css;
use Mpdf\Mpdf;
class MediaQueryProcessor
{
/**
* @var \Mpdf\Mpdf
*/
private $mpdf;
public function __construct(Mpdf $mpdf)
{
$this->mpdf = $mpdf;
}
/**
* Filter HTML elements by media query.
*
* Removes elements (style or link tags) that don't match the configured media type.
*
* @param string $html HTML content to filter
* @param string $pattern Regex pattern to match elements
* @return string Filtered HTML
*/
public function filterByMediaQuery($html, $pattern)
{
preg_match_all($pattern, $html, $m);
foreach ($m[0] as $i => $url) {
if (!$this->mpdf->CSSselectMedia || !preg_match('/(' . trim($this->mpdf->CSSselectMedia) . '|all)/i', $m[1][$i])) {
$html = str_replace($m[0][$i], '', $html);
}
}
return $html;
}
/**
* Process @media queries in CSS.
*
* Filters or unwraps @media blocks based on configured media type.
* If media doesn't match CSSselectMedia, the entire block is removed.
* If it matches, the contents are unwrapped.
*
* @param string $cssStr CSS string potentially containing @media rules
* @return string CSS string with media queries processed
*/
public function processMediaQueries($cssStr)
{
if (!preg_match('/@media/', $cssStr)) {
return $cssStr;
}
preg_match_all('/@media(.*?)\{(([^\{\}]*\{[^\{\}]*\})+)\s*\}/is', $cssStr, $m);
foreach ($m[0] as $i => $value) {
if ($this->mpdf->CSSselectMedia && !preg_match('/(' . trim($this->mpdf->CSSselectMedia) . '|all)/i', $m[1][$i])) {
$cssStr = str_replace($m[0][$i], '', $cssStr);
} else {
$cssStr = str_replace($m[0][$i], ' ' . $m[2][$i] . ' ', $cssStr);
}
}
return $cssStr;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,225 @@
<?php
namespace Mpdf\Css;
use Mpdf\Mpdf;
class SelectorParser
{
/**
* @var Mpdf
*/
private $mpdf;
public function __construct(Mpdf $mpdf)
{
$this->mpdf = $mpdf;
}
/**
* Parse @PAGE selector.
*
* @param array $tags Selector tags array
* @return string Matches tag
*/
public function parsePageSelector($tags)
{
$level = count($tags);
$t = '';
$t2 = '';
$t3 = '';
if (isset($tags[0])) {
$t = trim($tags[0]);
}
if (isset($tags[1])) {
$t2 = trim($tags[1]);
}
if (isset($tags[2])) {
$t3 = trim($tags[2]);
}
$tag = '';
if ($level === 1) {
$tag = $t;
} elseif ($level === 2 && preg_match('/^[:](.*)$/', $t2, $m)) {
$tag = $t . '>>PSEUDO>>' . $m[1];
if ($m[1] === 'LEFT' || $m[1] === 'RIGHT') {
$this->mpdf->mirrorMargins = true;
}
} elseif ($level === 2) {
$tag = $t . '>>NAMED>>' . $t2;
} elseif ($level === 3 && preg_match('/^[:](.*)$/', $t3, $m)) {
$tag = $t . '>>NAMED>>' . $t2 . '>>PSEUDO>>' . $m[1];
if ($m[1] === 'LEFT' || $m[1] === 'RIGHT') {
$this->mpdf->mirrorMargins = true;
}
}
return $tag;
}
/**
* Parse simple selector (depth 1).
*
* @param array $tags Selector tags array
* @return string|null Parsed tag key or null if invalid
*/
public function parseSimpleSelector($tags)
{
$t = isset($tags[0]) ? trim($tags[0]) : '';
if (empty($t)) {
return null;
}
$tag = '';
if (preg_match('/^[.](.*)$/', $t, $m)) {
$classes = explode('.', $m[1]);
sort($classes);
$tag = 'CLASS>>' . implode('.', $classes);
} elseif (preg_match('/^[#](.*)$/', $t, $m)) {
$tag = 'ID>>' . $m[1];
} elseif (preg_match('/^\[LANG=[\'\"]{0,1}([A-Z\-]{2,11})[\'\"]{0,1}\]$/', $t, $m)) {
$tag = 'LANG>>' . strtolower($m[1]);
} elseif (preg_match('/^:LANG\([\'\"]{0,1}([A-Z\-]{2,11})[\'\"]{0,1}\)$/', $t, $m)) { // mPDF 6 Special case for lang as attribute selector
$tag = 'LANG>>' . strtolower($m[1]);
} elseif (preg_match('/^(' . $this->mpdf->allowedCSStags . ')[.](.*)$/', $t, $m)) { // mPDF 6 Special case for lang as attribute selector
$classes = explode('.', $m[2]);
sort($classes);
$tag = $m[1] . '>>CLASS>>' . implode('.', $classes);
} elseif (preg_match('/^(' . $this->mpdf->allowedCSStags . ')\s*:NTH-CHILD\((.*)\)$/', $t, $m)) {
$tag = $m[1] . '>>SELECTORNTHCHILD>>' . $m[2];
} elseif (preg_match('/^(' . $this->mpdf->allowedCSStags . ')[#](.*)$/', $t, $m)) {
$tag = $m[1] . '>>ID>>' . $m[2];
} elseif (preg_match('/^(' . $this->mpdf->allowedCSStags . ')\[LANG=[\'\"]{0,1}([A-Z\-]{2,11})[\'\"]{0,1}\]$/', $t, $m)) {
$tag = $m[1] . '>>LANG>>' . strtolower($m[2]);
} elseif (preg_match('/^(' . $this->mpdf->allowedCSStags . '):LANG\([\'\"]{0,1}([A-Z\-]{2,11})[\'\"]{0,1}\)$/', $t, $m)) { // mPDF 6 Special case for lang as attribute selector
$tag = $m[1] . '>>LANG>>' . strtolower($m[2]);
} elseif (preg_match('/^(' . $this->mpdf->allowedCSStags . ')$/', $t)) { // mPDF 6 Special case for lang as attribute selector
$tag = $t;
}
return $tag ?: null;
}
/**
* Parse cascaded selector (depth > 1).
*
* @param array $tags Selector tags array
* @return array Array of tag levels for cascade
*/
public function parseCascadedSelector($tags)
{
$tmp = [];
$level = count($tags);
for ($n = 0; $n < $level; $n++) {
$tag = '';
$t = isset($tags[$n]) ? trim($tags[$n]) : '';
if (empty($t)) {
continue;
}
if (preg_match('/^[.](.*)$/', $t, $m)) {
$classes = explode('.', $m[1]);
sort($classes);
$tag = 'CLASS>>' . join('.', $classes);
} elseif (preg_match('/^[#](.*)$/', $t, $m)) {
$tag = 'ID>>' . $m[1];
} elseif (preg_match('/^\[LANG=[\'\"]{0,1}([A-Z\-]{2,11})[\'\"]{0,1}\]$/', $t, $m)) {
$tag = 'LANG>>' . strtolower($m[1]);
} elseif (preg_match('/^:LANG\([\'\"]{0,1}([A-Z\-]{2,11})[\'\"]{0,1}\)$/', $t, $m)) { // mPDF 6 Special case for lang as attribute selector
$tag = 'LANG>>' . strtolower($m[1]);
} elseif (preg_match('/^(' . $this->mpdf->allowedCSStags . ')[.](.*)$/', $t, $m)) { // mPDF 6 Special case for lang as attribute selector
$classes = explode('.', $m[2]);
sort($classes);
$tag = $m[1] . '>>CLASS>>' . join('.', $classes);
} elseif (preg_match('/^(' . $this->mpdf->allowedCSStags . ')\s*:NTH-CHILD\((.*)\)$/', $t, $m)) {
$tag = $m[1] . '>>SELECTORNTHCHILD>>' . $m[2];
} elseif (preg_match('/^(' . $this->mpdf->allowedCSStags . ')[#](.*)$/', $t, $m)) {
$tag = $m[1] . '>>ID>>' . $m[2];
} elseif (preg_match('/^(' . $this->mpdf->allowedCSStags . ')\[LANG=[\'\"]{0,1}([A-Z\-]{2,11})[\'\"]{0,1}\]$/', $t, $m)) {
$tag = $m[1] . '>>LANG>>' . strtolower($m[2]);
} elseif (preg_match('/^(' . $this->mpdf->allowedCSStags . '):LANG\([\'\"]{0,1}([A-Z\-]{2,11})[\'\"]{0,1}\)$/', $t, $m)) { // mPDF 6 Special case for lang as attribute selector
$tag = $m[1] . '>>LANG>>' . strtolower($m[2]);
} elseif (preg_match('/^(' . $this->mpdf->allowedCSStags . ')$/', $t)) { // mPDF 6 Special case for lang as attribute selector
$tag = $t;
}
if (!$tag) {
break;
}
$tmp[] = $tag;
}
return $tmp;
}
/**
* Evaluate nth-child CSS selector.
*
* Determines if a given element index matches an nth-child selector formula.
* Supports formulas like "2n+1", "odd", "even", or specific numbers.
*
* @param array $nthComponents Formula components from preg_match (e.g. 2N+1 split into a preg_match array))
* @param int $index Current element index (e.g row or column number)
* @return bool True if element matches the nth-child selector
*/
public function matchesNthChild($nthComponents, $index)
{
++$index;
$select = false;
$numOfComponents = count($nthComponents);
if ($nthComponents[0] === 'ODD') {
$a = 2;
$b = 1;
} elseif ($nthComponents[0] === 'EVEN') {
$a = 2;
$b = 0;
} elseif ($numOfComponents === 2) {
$a = 0;
$b = $nthComponents[1] + 0;
} // e.g. (+6)
elseif ($numOfComponents === 3) { // e.g. (2N)
if ($nthComponents[2] === '') {
$a = 1;
} elseif ($nthComponents[2] === '-') {
$a = -1;
} else {
$a = $nthComponents[2] + 0;
}
$b = 0;
} elseif ($numOfComponents === 4) { // e.g. (2N+6)
if ($nthComponents[2] === '') {
$a = 1;
} elseif ($nthComponents[2] === '-') {
$a = -1;
} else {
$a = $nthComponents[2] + 0;
}
$b = $nthComponents[3] + 0;
} else {
return false;
}
if ($a > 0) {
if (((($index % $a) - $b) % $a) === 0 && $index >= $b) {
$select = true;
}
} elseif ($a === 0) {
if ($index === $b) {
$select = true;
}
} else { // if ($a<0)
if (((($index % $a) - $b) % $a) === 0 && $index <= $b) {
$select = true;
}
}
return $select;
}
}

View File

@@ -0,0 +1,281 @@
<?php
namespace Mpdf\Css;
use Mpdf\Color\ColorConverter;
use Mpdf\Mpdf;
use Mpdf\SizeConverter;
class ShadowParser
{
/**
* @var Mpdf
*/
private $mpdf;
/**
* @var SizeConverter
*/
private $sizeConverter;
/**
* @var ColorConverter
*/
private $colorConverter;
public function __construct(Mpdf $mpdf, SizeConverter $sizeConverter, ColorConverter $colorConverter)
{
$this->mpdf = $mpdf;
$this->sizeConverter = $sizeConverter;
$this->colorConverter = $colorConverter;
}
/**
* Normalize shadow colors.
*
* Replaces commas in color functions (rgb, hsl, etc.) with placeholders
* to prevent splitting multiple shadows on those commas.
*
* @param string $value Shadow property value
* @return string Normalized shadow property value
*/
public function normalizeShadowColors($value)
{
$c = preg_match_all('/(rgba|rgb|device-cmyka|cmyka|device-cmyk|cmyk|hsla|hsl)\(.*?\)/', $value, $x); // mPDF 5.6.05
for ($i = 0; $i < $c; $i++) {
$col = preg_replace('/,\s/', '*', $x[0][$i]);
$value = str_replace($x[0][$i], $col, $value);
}
return $value;
}
/**
* Parse box-shadow CSS property.
*
* Converts box-shadow CSS property string into array format used internally.
* Handles multiple shadows, inset shadows, blur, spread, and colors.
*
* @param string $value Box-shadow property value
* @return array Array of shadow definitions
*/
public function parseBoxShadow($value)
{
$sh = [];
$ss = explode(',', $this->normalizeShadowColors($value));
foreach ($ss as $s) {
$boxShadow = $this->parseSingleBoxShadow($s);
if ($boxShadow) {
array_unshift($sh, $boxShadow);
}
}
return $sh;
}
/**
* Parse a single box-shadow definition.
*
* Helper method for setCSSboxshadow to parse individual shadow components
* (inset, x, y, blur, spread, color).
*
* @param string $s Shadow definition string
* @return array|null Parsed shadow array or null if invalid
*/
protected function parseSingleBoxShadow($s)
{
$boxShadow = [
'inset' => false,
'blur' => 0,
'spread' => 0
];
if (stripos($s, 'inset') !== false) {
$boxShadow['inset'] = true;
$s = preg_replace('/\s*inset\s*/', '', $s);
}
$p = explode(' ', trim($s));
if (isset($p[0])) {
$parentWidth = 0;
if (isset($this->mpdf->blk[$this->mpdf->blklvl - 1]['inner_width'])) {
$parentWidth = isset($this->mpdf->blk[$this->mpdf->blklvl - 1]['inner_width']);
} elseif (isset($this->mpdf->blk[0]['inner_width'])) {
$parentWidth = $this->mpdf->blk[0]['inner_width'];
}
$boxShadow['x'] = $this->sizeConverter->convert(
trim($p[0]),
$parentWidth,
$this->mpdf->FontSize,
false
);
}
if (isset($p[1])) {
$parentWidth = 0;
if (isset($this->mpdf->blk[$this->mpdf->blklvl - 1]['inner_width'])) {
$parentWidth = isset($this->mpdf->blk[$this->mpdf->blklvl - 1]['inner_width']);
} elseif (isset($this->mpdf->blk[0]['inner_width'])) {
$parentWidth = $this->mpdf->blk[0]['inner_width'];
}
$boxShadow['y'] = $this->sizeConverter->convert(
trim($p[1]),
$parentWidth,
$this->mpdf->FontSize,
false
);
}
if (isset($p[2])) {
if (preg_match('/^\s*[\.\-0-9]/', $p[2])) {
$parentWidth = 0;
if (isset($this->mpdf->blk[$this->mpdf->blklvl - 1]['inner_width'])) {
$parentWidth = isset($this->mpdf->blk[$this->mpdf->blklvl - 1]['inner_width']);
} elseif (isset($this->mpdf->blk[0]['inner_width'])) {
$parentWidth = $this->mpdf->blk[0]['inner_width'];
}
$boxShadow['blur'] = $this->sizeConverter->convert(
trim($p[2]),
$parentWidth,
$this->mpdf->FontSize,
false
);
} else {
$boxShadow['col'] = $this->colorConverter->convert(
preg_replace('/\*/', ',', $p[2]),
$this->mpdf->PDFAXwarnings
);
}
}
if (isset($p[3])) {
if (preg_match('/^\s*[\.\-0-9]/', $p[3])) {
$parentWidth = 0;
if (isset($this->mpdf->blk[$this->mpdf->blklvl - 1]['inner_width'])) {
$parentWidth = isset($this->mpdf->blk[$this->mpdf->blklvl - 1]['inner_width']);
} elseif (isset($this->mpdf->blk[0]['inner_width'])) {
$parentWidth = $this->mpdf->blk[0]['inner_width'];
}
$boxShadow['spread'] = $this->sizeConverter->convert(
trim($p[3]),
$parentWidth,
$this->mpdf->FontSize,
false
);
} else {
$boxShadow['col'] = $this->colorConverter->convert(
preg_replace('/\*/', ',', $p[3]),
$this->mpdf->PDFAXwarnings
);
}
}
if (isset($p[4])) {
$boxShadow['col'] = $this->colorConverter->convert(
preg_replace('/\*/', ',', $p[4]),
$this->mpdf->PDFAXwarnings
);
}
if (empty($boxShadow['col'])) {
$boxShadow['col'] = $this->colorConverter->convert('#888888', $this->mpdf->PDFAXwarnings);
}
return isset($boxShadow['y']) ? $boxShadow : null;
}
/**
* Parse text-shadow CSS property.
*
* Converts text-shadow CSS property string into array format used internally.
* Handles multiple shadows, blur, and colors.
*
* @param string $value Text-shadow property value
* @return array Array of text shadow definitions
*/
public function parseTextShadow($value)
{
$sh = [];
$ss = explode(',', $this->normalizeShadowColors($value));
foreach ($ss as $s) {
$textShadow = $this->parseSingleTextShadow($s);
if ($textShadow) {
array_unshift($sh, $textShadow);
}
}
return $sh;
}
/**
* Parse a single text-shadow definition.
*
* Helper method for setCSStextshadow to parse individual shadow components
* (x, y, blur, color).
*
* @param string $s Shadow definition string
* @return array|null Parsed shadow array or null if invalid
*/
protected function parseSingleTextShadow($s)
{
$textShadow = ['blur' => 0];
$p = explode(' ', trim($s));
if (isset($p[0])) {
$textShadow['x'] = $this->sizeConverter->convert(
trim($p[0]),
$this->mpdf->FontSize,
$this->mpdf->FontSize,
false
);
}
if (isset($p[1])) {
$textShadow['y'] = $this->sizeConverter->convert(
trim($p[1]),
$this->mpdf->FontSize,
$this->mpdf->FontSize,
false
);
}
if (isset($p[2])) {
if (preg_match('/^\s*[\.\-0-9]/', $p[2])) {
$textShadow['blur'] = $this->sizeConverter->convert(
trim($p[2]),
isset($this->mpdf->blk[$this->mpdf->blklvl]['inner_width']) ? $this->mpdf->blk[$this->mpdf->blklvl]['inner_width'] : 0,
$this->mpdf->FontSize,
false
);
} else {
$textShadow['col'] = $this->colorConverter->convert(
preg_replace('/\*/', ',', $p[2]),
$this->mpdf->PDFAXwarnings
);
}
}
if (isset($p[3])) {
$textShadow['col'] = $this->colorConverter->convert(
preg_replace('/\*/', ',', $p[3]),
$this->mpdf->PDFAXwarnings
);
}
if (empty($textShadow['col'])) {
$textShadow['col'] = $this->colorConverter->convert(
'#888888',
$this->mpdf->PDFAXwarnings
);
}
return isset($textShadow['y']) ? $textShadow : null;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -332,11 +332,7 @@ public function getImportedPages()
protected function _put($s, $newLine = true)
{
if ($newLine) {
$this->buffer .= $s . "\n";
} else {
$this->buffer .= $s;
}
$this->buffer->append($s, $newLine);
}
/**

View File

@@ -278,7 +278,7 @@ function svgImage($attribs)
$orig_srcpath = '';
if (trim($srcpath) != '' && substr($srcpath, 0, 4) == 'var:') {
$orig_srcpath = $srcpath;
$srcpath = $this->mpdf->GetFullPath($srcpath);
$this->mpdf->GetFullPath($srcpath);
}
// Image file (does not allow vector images i.e. WMF/SVG)

View File

@@ -15,6 +15,7 @@
use Mpdf\Utils\Arrays;
use Mpdf\Utils\NumericString;
use Mpdf\Utils\UtfString;
use Mpdf\Utils\Path;
use Psr\Log\NullLogger;
/**
@@ -32,7 +33,7 @@ class Mpdf implements \Psr\Log\LoggerAwareInterface
use FpdiTrait;
use MpdfPsrLogAwareTrait;
const VERSION = '8.2.7';
const VERSION = '8.3.1';
const SCALE = 72 / 25.4;
@@ -332,10 +333,6 @@ class Mpdf implements \Psr\Log\LoggerAwareInterface
var $lastblocklevelchange;
var $nestedtablejustfinished;
var $linebreakjustfinished;
var $cell_border_dominance_L;
var $cell_border_dominance_R;
var $cell_border_dominance_T;
var $cell_border_dominance_B;
var $table_keep_together;
var $plainCell_properties;
var $shrin_k1;
@@ -851,6 +848,10 @@ class Mpdf implements \Psr\Log\LoggerAwareInterface
*/
private $cssManager;
/**
* @var ShadowParser
private $shadowParser;
/**
* @var \Mpdf\Gradient
*/
@@ -1109,7 +1110,7 @@ public function __construct(array $config = [], $container = null)
$this->BMPonly = [];
$this->page = 0;
$this->n = 2;
$this->buffer = '';
$this->buffer = new Buffer();
$this->objectbuffer = [];
$this->pages = [];
$this->OrientationChanges = [];
@@ -9554,7 +9555,7 @@ function Output($name = '', $dest = '')
$this->logger->debug(sprintf('Compiled in %.6F seconds', microtime(true) - $this->time0), ['context' => LogContext::STATISTICS]);
$this->logger->debug(sprintf('Peak Memory usage %s MB', number_format(memory_get_peak_usage(true) / (1024 * 1024), 2)), ['context' => LogContext::STATISTICS]);
$this->logger->debug(sprintf('PDF file size %s kB', number_format(strlen($this->buffer) / 1024)), ['context' => LogContext::STATISTICS]);
$this->logger->debug(sprintf('PDF file size %s kB', number_format($this->buffer->getLength() / 1024)), ['context' => LogContext::STATISTICS]);
$this->logger->debug(sprintf('%d fonts used', count($this->fonts)), ['context' => LogContext::STATISTICS]);
if (is_bool($dest)) {
@@ -9591,7 +9592,7 @@ function Output($name = '', $dest = '')
if (!isset($_SERVER['HTTP_ACCEPT_ENCODING']) || empty($_SERVER['HTTP_ACCEPT_ENCODING'])) {
// don't use length if server using compression
header('Content-Length: ' . strlen($this->buffer));
header('Content-Length: ' . $this->buffer->getLength());
}
header('Content-disposition: inline; filename="' . $name . '"');
@@ -9602,7 +9603,7 @@ function Output($name = '', $dest = '')
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
}
echo $this->buffer;
$this->buffer->writeToOutput();
break;
@@ -9623,12 +9624,12 @@ function Output($name = '', $dest = '')
if (!isset($_SERVER['HTTP_ACCEPT_ENCODING']) || empty($_SERVER['HTTP_ACCEPT_ENCODING'])) {
// don't use length if server using compression
header('Content-Length: ' . strlen($this->buffer));
header('Content-Length: ' . $this->buffer->getLength());
}
header('Content-Disposition: attachment; filename="' . $name . '"');
echo $this->buffer;
$this->buffer->writeToOutput();
break;
@@ -9639,14 +9640,15 @@ function Output($name = '', $dest = '')
throw new \Mpdf\MpdfException(sprintf('Unable to create output file %s', $name));
}
fwrite($f, $this->buffer, strlen($this->buffer));
$this->buffer->writeToFile($f);
fclose($f);
break;
case Destination::STRING_RETURN:
$this->cache->clearOld();
return $this->buffer;
return $this->buffer->writeToString();
default:
throw new \Mpdf\MpdfException(sprintf('Incorrect output destination %s', $dest));
@@ -10110,7 +10112,7 @@ function _enddoc()
$this->writer->write('endobj');
// Cross-ref
$o = strlen($this->buffer);
$o = $this->buffer->getLength();
$this->writer->write('xref');
$this->writer->write('0 ' . ($this->n + 1));
$this->writer->write('0000000000 65535 f ');
@@ -10129,7 +10131,7 @@ function _enddoc()
$this->writer->write('startxref');
$this->writer->write($o);
$this->buffer .= '%%EOF';
$this->buffer->append('%%EOF');
$this->state = 3;
}
@@ -11535,84 +11537,8 @@ function SetBasePath($str = '')
public function GetFullPath(&$path, $basepath = '')
{
// @todo make return, remove reference
// When parsing CSS need to pass temporary basepath - so links are relative to current stylesheet
if (!$basepath) {
$basepath = $this->basepath;
}
// Fix path value
$path = str_replace("\\", '/', $path); // If on Windows
// mPDF 5.7.2
if (strpos($path, '//') === 0) {
$scheme = parse_url($basepath, PHP_URL_SCHEME);
$scheme = $scheme ?: 'http';
$path = $scheme . ':' . $path;
}
$path = preg_replace('|^./|', '', $path); // Inadvertently corrects "./path/etc" and "//www.domain.com/etc"
if (strpos($path, '#') === 0) {
return;
}
// Skip schemes not supported by installed stream wrappers
$wrappers = stream_get_wrappers();
$pattern = sprintf('@^(?!%s)[a-z0-9\.\-+]+:.*@i', implode('|', $wrappers));
if (preg_match($pattern, $path)) {
return;
}
if (strpos($path, '../') === 0) { // It is a relative link
$backtrackamount = substr_count($path, '../');
$maxbacktrack = substr_count($basepath, '/') - 3;
$filepath = str_replace('../', '', $path);
$path = $basepath;
// If it is an invalid relative link, then make it go to directory root
if ($backtrackamount > $maxbacktrack) {
$backtrackamount = $maxbacktrack;
}
// Backtrack some directories
for ($i = 0; $i < $backtrackamount + 1; $i++) {
$path = substr($path, 0, strrpos($path, "/"));
}
$path .= '/' . $filepath; // Make it an absolute path
return;
}
if ((strpos($path, ":/") === false || strpos($path, ":/") > 10) && !@is_file($path)) { // It is a local link. Ignore potential file errors
if (strpos($path, '/') === 0) {
$tr = parse_url($basepath);
// mPDF 5.7.2
$root = '';
if (!empty($tr['scheme'])) {
$root .= $tr['scheme'] . '://';
}
$root .= isset($tr['host']) ? $tr['host'] : '';
$root .= ((isset($tr['port']) && $tr['port']) ? (':' . $tr['port']) : ''); // mPDF 5.7.3
$path = $root . $path;
return;
}
$path = $basepath . $path;
}
// Do nothing if it is an Absolute Link
$basepath = !empty($basepath) ? $basepath : $this->basepath;
$path = Path::relativeToAbsolutePath($path, $basepath);
}
function docPageNum($num = 0, $extras = false)
@@ -13347,7 +13273,7 @@ function WriteHTML($html, $mode = HTMLParserMode::DEFAULT_MODE, $init = true, $c
}
$properties = $this->cssManager->MergeCSS('BLOCK', 'BODY', '');
if ($zproperties) {
$properties = $this->cssManager->array_merge_recursive_unique($properties, $zproperties);
$properties = Arrays::uniqueRecursiveMerge($properties, $zproperties);
}
if (isset($properties['DIRECTION']) && $properties['DIRECTION']) {

View File

@@ -17,6 +17,9 @@ public function __construct()
];
}
/**
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
@@ -27,12 +30,18 @@ public function offsetSet($offset, $value)
$this->container[$offset] = $value;
}
/**
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return array_key_exists($offset, $this->container);
}
/**
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
@@ -43,6 +52,9 @@ public function offsetUnset($offset)
$this->container[$offset] = null;
}
/**
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{

View File

@@ -5,6 +5,14 @@
use Mpdf\Color\ColorConverter;
use Mpdf\Color\ColorModeConverter;
use Mpdf\Color\ColorSpaceRestrictor;
use Mpdf\Css\BorderMerger;
use Mpdf\Css\CssMerger;
use Mpdf\Css\CssParser;
use Mpdf\Css\InlinePropertyConverter;
use Mpdf\Css\InlineStyleParser;
use Mpdf\Css\NormalizeProperties;
use Mpdf\Css\SelectorParser;
use Mpdf\Css\ShadowParser;
use Mpdf\File\LocalContentLoader;
use Mpdf\Fonts\FontCache;
use Mpdf\Fonts\FontFileFinder;
@@ -81,9 +89,29 @@ public function getServices(
? $this->container->get('localContentLoader')
: new LocalContentLoader();
$assetFetcher = new AssetFetcher($mpdf, $localContentLoader, $httpClient, $logger);
$assetFetcher = $this->container && $this->container->has('assetFetcher')
? $this->container->get('assetFetcher')
: new AssetFetcher($mpdf, $localContentLoader, $httpClient, $logger);
$cssManager = new CssManager($mpdf, $cache, $sizeConverter, $colorConverter, $assetFetcher);
$normalizeProperties = new NormalizeProperties($mpdf, $sizeConverter, $colorConverter);
$selectorParser = new SelectorParser($mpdf);
$inlineStyleParser = new InlineStyleParser($normalizeProperties);
$inlinePropertyConverter = new InlinePropertyConverter($colorConverter);
$borderMerger = new BorderMerger();
$cssParser = new CssParser($mpdf, $cache, $sizeConverter, $colorConverter, $assetFetcher);
$cssMerger = new CssMerger(
$mpdf,
$normalizeProperties,
$inlineStyleParser,
$selectorParser,
$inlinePropertyConverter,
$colorConverter,
$borderMerger
);
$cssManager = new CssManager($cssParser, $cssMerger);
$otl = new Otl($mpdf, $fontCache);

View File

@@ -42,7 +42,7 @@ public function __construct($dpi, $defaultFontSize, Mpdf $mpdf, LoggerInterface
*/
public function convert($size = 5, $maxsize = 0, $fontsize = false, $usefontsize = true)
{
$size = trim(strtolower($size));
$size = trim(strtolower((string) $size));
$res = preg_match('/^(?P<size>[-0-9.,]+([eE]\-?[0-9]+)?)?(?P<unit>[%a-z-]+)?$/', $size, $parts);
if (!$res) {
// ignore definition

View File

@@ -3,6 +3,9 @@
namespace Mpdf\Tag;
use Mpdf\Conversion\DecToAlpha;
use Mpdf\Conversion\DecToCjk;
use Mpdf\Conversion\DecToHebrew;
use Mpdf\Conversion\DecToOther;
use Mpdf\Conversion\DecToRoman;
use Mpdf\Mpdf;
use Mpdf\Utils\Arrays;
@@ -155,6 +158,34 @@ public function open($attr, &$ahtml, &$ihtml)
$this->mpdf->listtype[$this->mpdf->listlvl] = 'square';
}
}
// Override with HTML TYPE attribute (lower specificity than CSS)
if (!empty($attr['TYPE'])) {
$listtype = $attr['TYPE'];
switch ($listtype) {
case 'A':
$listtype = 'upper-latin';
break;
case 'a':
$listtype = 'lower-latin';
break;
case 'I':
$listtype = 'upper-roman';
break;
case 'i':
$listtype = 'lower-roman';
break;
case '1':
$listtype = 'decimal';
break;
}
$this->mpdf->listtype[$this->mpdf->listlvl] = $listtype;
}
// Override with CSS list-style-type if specified (highest specificity)
if (!empty($properties['LIST-STYLE-TYPE'])) {
$this->mpdf->listtype[$this->mpdf->listlvl] = strtolower($properties['LIST-STYLE-TYPE']);
}
}
// mPDF 6 Lists - in Tables
@@ -170,54 +201,161 @@ public function open($attr, &$ahtml, &$ihtml)
//if in table - output here as a tabletextbuffer
//position:inside OR position:outside (always output in table as position:inside)
$currentListType = $this->mpdf->listtype[$this->mpdf->listlvl];
// Allow individual LI to override list type via HTML TYPE attribute
if (!empty($attr['TYPE'])) {
$liType = $attr['TYPE'];
switch ($liType) {
case 'A':
$liType = 'upper-latin';
break;
case 'a':
$liType = 'lower-latin';
break;
case 'I':
$liType = 'upper-roman';
break;
case 'i':
$liType = 'lower-roman';
break;
case '1':
$liType = 'decimal';
break;
}
$currentListType = $liType;
}
// Allow individual LI to override list type via CSS (highest specificity)
if (!empty($properties['LIST-STYLE-TYPE'])) {
$currentListType = strtolower($properties['LIST-STYLE-TYPE']);
}
$decToAlpha = new DecToAlpha();
$decToRoman = new DecToRoman();
$counter = $this->mpdf->listcounter[$this->mpdf->listlvl];
$list_item_color = '';
switch ($this->mpdf->listtype[$this->mpdf->listlvl]) {
switch ($currentListType) {
case 'upper-alpha':
case 'upper-latin':
case 'A':
$blt = $decToAlpha->convert($this->mpdf->listcounter[$this->mpdf->listlvl]) . $this->mpdf->list_number_suffix;
$blt = $decToAlpha->convert($counter) . $this->mpdf->list_number_suffix;
break;
case 'lower-alpha':
case 'lower-latin':
case 'a':
$blt = $decToAlpha->convert($this->mpdf->listcounter[$this->mpdf->listlvl], false) . $this->mpdf->list_number_suffix;
$blt = $decToAlpha->convert($counter, false) . $this->mpdf->list_number_suffix;
break;
case 'upper-roman':
case 'I':
$blt = $decToRoman->convert($this->mpdf->listcounter[$this->mpdf->listlvl]) . $this->mpdf->list_number_suffix;
$blt = $decToRoman->convert($counter) . $this->mpdf->list_number_suffix;
break;
case 'lower-roman':
case 'i':
$blt = $decToRoman->convert($this->mpdf->listcounter[$this->mpdf->listlvl]) . $this->mpdf->list_number_suffix;
$blt = $decToRoman->convert($counter, false) . $this->mpdf->list_number_suffix;
break;
case 'decimal':
case '1':
$blt = $this->mpdf->listcounter[$this->mpdf->listlvl] . $this->mpdf->list_number_suffix;
$blt = $counter . $this->mpdf->list_number_suffix;
break;
case 'hebrew':
$decToHebrew = new DecToHebrew();
$blt = $decToHebrew->convert($counter) . $this->mpdf->list_number_suffix;
break;
case 'cjk-decimal':
$decToCjk = new DecToCjk();
$blt = $decToCjk->convert($counter) . $this->mpdf->list_number_suffix;
break;
case 'arabic-indic':
case 'bengali':
case 'cambodian':
case 'devanagari':
case 'gujarati':
case 'gurmukhi':
case 'kannada':
case 'khmer':
case 'lao':
case 'malayalam':
case 'myanmar':
case 'oriya':
case 'persian':
case 'tamil':
case 'telugu':
case 'thai':
case 'urdu':
$decToOther = new DecToOther($this->mpdf);
$cp = $decToOther->getCodePage($currentListType);
$blt = $decToOther->convert($counter, $cp, true) . $this->mpdf->list_number_suffix;
break;
case 'disc':
$blt = '-';
if ($this->mpdf->_charDefined($this->mpdf->CurrentFont['cw'], 8226)) {
$blt = "\xe2\x80\xa2"; // U+2022 BULLET
}
break;
case 'circle':
$blt = '-';
if ($this->mpdf->_charDefined($this->mpdf->CurrentFont['cw'], 9900)) {
$blt = "\xe2\x9a\xac"; // U+26AC
}
break;
case 'square':
$blt = '-';
if ($this->mpdf->_charDefined($this->mpdf->CurrentFont['cw'], 9642)) {
$blt = "\xe2\x96\xaa"; // U+25AA
}
break;
case 'none':
$blt = '';
break;
default:
$blt = '-';
if ($this->mpdf->listlvl % 3 == 1 && $this->mpdf->_charDefined($this->mpdf->CurrentFont['cw'], 8226)) {
$blt = "\xe2\x80\xa2";
} // &#8226;
elseif ($this->mpdf->listlvl % 3 == 2 && $this->mpdf->_charDefined($this->mpdf->CurrentFont['cw'], 9900)) {
$blt = "\xe2\x9a\xac";
} // &#9900;
elseif ($this->mpdf->listlvl % 3 == 0 && $this->mpdf->_charDefined($this->mpdf->CurrentFont['cw'], 9642)) {
$blt = "\xe2\x96\xaa";
} // &#9642;
if (preg_match('/U\+([a-fA-F0-9]+)/i', $currentListType, $m)) {
$blt = '-';
if ($this->mpdf->_charDefined($this->mpdf->CurrentFont['cw'], hexdec($m[1]))) {
$blt = UtfString::codeHex2utf($m[1]);
}
if (preg_match('/rgb\(.*?\)/', $currentListType, $cm)) {
$list_item_color = $this->colorConverter->convert($cm[0], $this->mpdf->PDFAXwarnings);
}
} else {
$blt = '-';
if ($this->mpdf->_charDefined($this->mpdf->CurrentFont['cw'], 8226)) {
$blt = "\xe2\x80\xa2";
}
}
break;
}
// change to &nbsp; spaces
if ($this->mpdf->usingCoreFont) {
$ls = str_repeat(chr(160) . chr(160), ($this->mpdf->listlvl - 1) * 2) . $blt . ' ';
} else {
$ls = str_repeat("\xc2\xa0\xc2\xa0", ($this->mpdf->listlvl - 1) * 2) . $blt . ' ';
if ($currentListType !== 'none') {
if ($this->mpdf->usingCoreFont) {
$indent = str_repeat(chr(160) . chr(160), ($this->mpdf->listlvl - 1) * 2);
} else {
$indent = str_repeat("\xc2\xa0\xc2\xa0", ($this->mpdf->listlvl - 1) * 2);
}
if (!empty($list_item_color)) {
// Write indentation without color
if ($indent !== '') {
$this->mpdf->_saveCellTextBuffer($indent);
$this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['s'] += $this->mpdf->GetStringWidth($indent);
}
// Write marker with color
$save_colorarray = $this->mpdf->colorarray;
$this->mpdf->colorarray = $list_item_color;
$this->mpdf->_saveCellTextBuffer($blt);
$this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['s'] += $this->mpdf->GetStringWidth($blt);
$this->mpdf->colorarray = $save_colorarray;
// Write trailing space without color
$this->mpdf->_saveCellTextBuffer(' ');
$this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['s'] += $this->mpdf->GetStringWidth(' ');
} else {
$ls = $indent . $blt . ' ';
$this->mpdf->_saveCellTextBuffer($ls);
$this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['s'] += $this->mpdf->GetStringWidth($ls);
}
}
$this->mpdf->_saveCellTextBuffer($ls);
$this->mpdf->cell[$this->mpdf->row][$this->mpdf->col]['s'] += $this->mpdf->GetStringWidth($ls);
}
return;

View File

@@ -212,7 +212,7 @@ public function open($attr, &$ahtml, &$ihtml)
}
}
if (!empty($properties['ROTATE']) && $this->mpdf->tableLevel == 1) {
$this->mpdf->table_rotate = $properties['ROTATE'];
$this->mpdf->table_rotate = $this->parseTableRotate($properties['ROTATE']);
}
if (isset($properties['TOPNTAIL'])) {
$table['topntail'] = $properties['TOPNTAIL'];
@@ -496,7 +496,7 @@ public function open($attr, &$ahtml, &$ihtml)
}
}
if (isset($attr['ROTATE']) && $this->mpdf->tableLevel == 1) {
$this->mpdf->table_rotate = $attr['ROTATE'];
$this->mpdf->table_rotate = $this->parseTableRotate($attr['ROTATE']);
}
//++++++++++++++++++++++++++++
@@ -1278,4 +1278,28 @@ private function tbsqrt($val, $iteration = 3)
return $ret;
}
/**
* @param string $rotate
* @return int
*/
private function parseTableRotate($rotate)
{
if (1 !== preg_match('/^(-?[0-9]+)(?:deg)?$/', $rotate, $matches)) {
return 0;
}
$rotationDegrees = (int) $matches[1] % 360;
if ($rotationDegrees > 180) {
$rotationDegrees -= 360;
} elseif ($rotationDegrees < -180) {
$rotationDegrees += 360;
}
// Only 90 and -90 are supported
if ($rotationDegrees !== 90 && $rotationDegrees !== -90) {
$rotationDegrees = 0;
}
return $rotationDegrees;
}
}

View File

@@ -4,6 +4,7 @@
use Mpdf\Css\Border;
use Mpdf\Css\TextVars;
use Mpdf\Utils\Arrays;
use Mpdf\Utils\UtfString;
class Td extends Tag
@@ -167,14 +168,14 @@ public function open($attr, &$ahtml, &$ihtml)
$c['R'] = $this->mpdf->trow_text_rotate;
}
$this->mpdf->cell_border_dominance_L = 0;
$this->mpdf->cell_border_dominance_R = 0;
$this->mpdf->cell_border_dominance_T = 0;
$this->mpdf->cell_border_dominance_B = 0;
$this->cssManager->setBorderDominance('L', 0);
$this->cssManager->setBorderDominance('R', 0);
$this->cssManager->setBorderDominance('T', 0);
$this->cssManager->setBorderDominance('B', 0);
$properties = $this->cssManager->MergeCSS('TABLE', $tag, $attr);
$properties = $this->cssManager->array_merge_recursive_unique($this->mpdf->base_table_properties, $properties);
$properties = Arrays::uniqueRecursiveMerge($this->mpdf->base_table_properties, $properties);
$this->mpdf->Reset(); // mPDF 6 ?????????????????????
@@ -282,10 +283,10 @@ public function open($attr, &$ahtml, &$ihtml)
$c['border_details']['L'] = $bord;
$c['border_details']['T'] = $bord;
$c['border_details']['B'] = $bord;
$c['border_details']['L']['dom'] = $this->mpdf->cell_border_dominance_L;
$c['border_details']['R']['dom'] = $this->mpdf->cell_border_dominance_R;
$c['border_details']['T']['dom'] = $this->mpdf->cell_border_dominance_T;
$c['border_details']['B']['dom'] = $this->mpdf->cell_border_dominance_B;
$c['border_details']['L']['dom'] = $this->cssManager->getBorderDominance('L');
$c['border_details']['R']['dom'] = $this->cssManager->getBorderDominance('R');
$c['border_details']['T']['dom'] = $this->cssManager->getBorderDominance('T');
$c['border_details']['B']['dom'] = $this->cssManager->getBorderDominance('B');
} elseif ($this->mpdf->simpleTables && $this->mpdf->row == 0 && $this->mpdf->col == 0) {
$table['simple']['border'] = Border::ALL;
$table['simple']['border_details']['R'] = $bord;
@@ -299,22 +300,22 @@ public function open($attr, &$ahtml, &$ihtml)
if (!empty($properties['BORDER-RIGHT'])) {
$c['border_details']['R'] = $this->mpdf->border_details($properties['BORDER-RIGHT']);
$this->mpdf->setBorder($c['border'], Border::RIGHT, $c['border_details']['R']['s']);
$c['border_details']['R']['dom'] = $this->mpdf->cell_border_dominance_R;
$c['border_details']['R']['dom'] = $this->cssManager->getBorderDominance('R');
}
if (!empty($properties['BORDER-LEFT'])) {
$c['border_details']['L'] = $this->mpdf->border_details($properties['BORDER-LEFT']);
$this->mpdf->setBorder($c['border'], Border::LEFT, $c['border_details']['L']['s']);
$c['border_details']['L']['dom'] = $this->mpdf->cell_border_dominance_L;
$c['border_details']['L']['dom'] = $this->cssManager->getBorderDominance('L');
}
if (!empty($properties['BORDER-BOTTOM'])) {
$c['border_details']['B'] = $this->mpdf->border_details($properties['BORDER-BOTTOM']);
$this->mpdf->setBorder($c['border'], Border::BOTTOM, $c['border_details']['B']['s']);
$c['border_details']['B']['dom'] = $this->mpdf->cell_border_dominance_B;
$c['border_details']['B']['dom'] = $this->cssManager->getBorderDominance('B');
}
if (!empty($properties['BORDER-TOP'])) {
$c['border_details']['T'] = $this->mpdf->border_details($properties['BORDER-TOP']);
$this->mpdf->setBorder($c['border'], Border::TOP, $c['border_details']['T']['s']);
$c['border_details']['T']['dom'] = $this->mpdf->cell_border_dominance_T;
$c['border_details']['T']['dom'] = $this->cssManager->getBorderDominance('T');
}
} elseif ($this->mpdf->simpleTables && $this->mpdf->row == 0 && $this->mpdf->col == 0) {
if (!empty($properties['BORDER-LEFT'])) {

View File

@@ -29,9 +29,10 @@ public static function get($array, $key, $default = null)
* [one, two]
* ]
* @param array $array
* @param int|null $maxSize Max depth of the combinations
* @return array
*/
public static function allUniqueSortedCombinations($array)
public static function allUniqueSortedCombinations($array, $maxSize = null)
{
$input = array_unique($array);
if (count($input) <= 1) {
@@ -46,6 +47,9 @@ public static function allUniqueSortedCombinations($array)
$n = count($input);
for ($k = 2; $k <= $n; $k++) {
if ($maxSize && $k > $maxSize) {
break;
}
$combinations = array_merge($combinations, self::combinations($input, $k));
}
@@ -97,4 +101,29 @@ public static function combinations($array, $k)
return $combinations;
}
/**
* Merge arrays recursively, appending integer-like keys and merge string keys.
*
* @param array ...$arrays Arrays to merge
* @return array Merged array
*/
public static function uniqueRecursiveMerge(...$arrays)
{
$results = array_shift($arrays);
foreach ($arrays as $array) {
foreach ($array as $key => $value) {
if ((string) $key === (string) ((int) $key)) {
$results[] = $value;
} elseif (is_array($value) && isset($results[$key]) && is_array($results[$key])) {
$results[$key] = self::uniqueRecursiveMerge($results[$key], $value);
} else {
$results[$key] = $value;
}
}
}
return $results;
}
}

117
vendor/mpdf/mpdf/src/Utils/Path.php vendored Normal file
View File

@@ -0,0 +1,117 @@
<?php
namespace Mpdf\Utils;
class Path
{
/**
* Convert a relative path to an absolute path
*
* @param string $relPath
* @param string $basePath The absolute path to prefix to the relative path (can be a path or URL)
* @return string
*/
public static function relativeToAbsolutePath($relPath, $basePath)
{
// Fix Windows paths
$relPath = str_replace("\\", '/', $relPath);
// mPDF 5.7.2
if (strpos($relPath, '//') === 0) {
$scheme = parse_url($basePath, PHP_URL_SCHEME);
$scheme = $scheme ?: 'http';
$relPath = $scheme . ':' . $relPath;
}
// Inadvertently corrects "./path/etc" and "//www.domain.com/etc"
$relPath = preg_replace('|^./|', '', $relPath);
if (strpos($relPath, '#') === 0) {
return $relPath;
}
// Skip schemes not supported by installed stream wrappers
$wrappers = stream_get_wrappers();
$pattern = sprintf('@^(?!%s)[a-z0-9\.\-+]+:.*@i', implode('|', $wrappers));
if (preg_match($pattern, $relPath)) {
return $relPath;
}
// It is a relative link
if (strpos($relPath, '../') === 0) {
$backtrackamount = substr_count($relPath, '../');
$maxbacktrack = substr_count($basePath, '/') - 3;
$filepath = str_replace('../', '', $relPath);
$relPath = $basePath;
// If it is an invalid relative link, then make it go to directory root
if ($backtrackamount > $maxbacktrack) {
$backtrackamount = $maxbacktrack;
}
// Backtrack some directories
for ($i = 0; $i < $backtrackamount + 1; $i++) {
$relPath = substr($relPath, 0, strrpos($relPath, "/"));
}
// Make it an absolute path
$relPath .= '/' . $filepath;
return $relPath;
}
// It is a local link. Ignore potential file errors
if ((strpos($relPath, ":/") === false || strpos($relPath, ":/") > 10) &&
!@is_file($relPath)
) {
if (strpos($relPath, '/') !== 0) {
return $basePath . $relPath;
}
$tr = parse_url($basePath);
// mPDF 5.7.2
$root = '';
if (!empty($tr['scheme'])) {
$root .= $tr['scheme'] . '://';
}
$root .= !empty($tr['host']) ? $tr['host'] : '';
$root .= !empty($tr['port']) ? ':' . $tr['port'] : ''; // mPDF 5.7.3
$relPath = $root . $relPath;
}
return $relPath;
}
/**
* Normalize file path for local file system access.
*
* Converts URLs to local file paths when the base path is local.
* Handles DOCUMENT_ROOT and relative paths.
*
* @param string $path File path or URL
* @return string Normalized path
*/
public static function normalizeLocalFilePath($path)
{
$tr = parse_url($path);
$lp = __FILE__;
$ap = realpath($lp);
$ap = str_replace("\\", '/', $ap);
$docroot = substr($ap, 0, strpos($ap, $lp));
// WriteHTML parses all paths to full URLs; may be local file name
// DOCUMENT_ROOT is not returned on IIS
if (!empty($tr['scheme']) && !empty($tr['host']) && !empty($_SERVER['DOCUMENT_ROOT'])) {
return $_SERVER['DOCUMENT_ROOT'] . $tr['path'];
}
if ($docroot) {
return $docroot . $tr['path'];
}
return $path;
}
}

View File

@@ -32,7 +32,7 @@ public function write($s, $ln = true)
if ($this->mpdf->state === 2) {
$this->endPage($s, $ln);
} else {
$this->mpdf->buffer .= $s . ($ln ? "\n" : '');
$this->mpdf->buffer->append($s, $ln);
}
}
@@ -53,7 +53,7 @@ public function object($obj_id = false, $onlynewobj = false)
// Begin a new object
if (!$onlynewobj) {
$this->mpdf->offsets[$obj_id] = strlen($this->mpdf->buffer);
$this->mpdf->offsets[$obj_id] = $this->mpdf->buffer->getLength();
$this->write($obj_id . ' 0 obj');
$this->mpdf->currentObjectNumber = $obj_id; // for later use with encryption
}

View File

@@ -801,7 +801,7 @@ public function writeTrailer() // _puttrailer
$this->writer->write('/Encrypt ' . $this->mpdf->enc_obj_id . ' 0 R');
$this->writer->write('/ID [<' . $this->protection->getUniqid() . '> <' . $this->protection->getUniqid() . '>]');
} else {
$uniqid = md5(time() . $this->mpdf->buffer);
$uniqid = md5(time() . $this->mpdf->buffer->getHash());
$this->writer->write('/ID [<' . $uniqid . '> <' . $uniqid . '>]');
}
}

View File

@@ -265,7 +265,7 @@ public function writePages() // _putpages
$this->metadataWriter->writeAnnotations(); // mPDF 5.7.2
// Pages root
$this->mpdf->offsets[1] = strlen($this->mpdf->buffer);
$this->mpdf->offsets[1] = $this->mpdf->buffer->getLength();
$this->writer->write('1 0 obj');
$this->writer->write('<</Type /Pages');

View File

@@ -121,7 +121,7 @@ public function writeResources() // _putresources
$this->backgroundWriter->writePatterns();
// Resource dictionary
$this->mpdf->offsets[2] = strlen($this->mpdf->buffer);
$this->mpdf->offsets[2] = $this->mpdf->buffer->getLength();
$this->writer->write('2 0 obj');
$this->writer->write('<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');