Skip to content

Commit 072f91b

Browse files
authored
draft CSS support methods (#129)
* add implodeCSSData method * change visibility to protected and add css methods * add intToRoman method
1 parent 3221a5b commit 072f91b

File tree

4 files changed

+227
-7
lines changed

4 files changed

+227
-7
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
8.2.3
1+
8.2.4

src/Base.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ abstract class Base
180180
/**
181181
* TCPDF version.
182182
*/
183-
protected string $version = '8.2.3';
183+
protected string $version = '8.2.4';
184184

185185
/**
186186
* Time is seconds since EPOCH when the document was created.

src/CSS.php

Lines changed: 224 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ abstract class CSS extends \Com\Tecnick\Pdf\SVG
4949
*
5050
* @const TCSSBorderSpacing
5151
*/
52-
public const ZEROBORDERSPACE = [
52+
protected const ZEROBORDERSPACE = [
5353
'H' => 0,
5454
'V' => 0,
5555
];
@@ -75,6 +75,45 @@ abstract class CSS extends \Com\Tecnick\Pdf\SVG
7575
*/
7676
protected $defCSSBorderSpacing = self::ZEROBORDERSPACE;
7777

78+
/**
79+
* Maximum value that can be represented in Roman notation.
80+
*
81+
* @var int
82+
*/
83+
protected const ROMAN_LIMIT = 3_999_999_999;
84+
85+
/**
86+
* Maps Roman Vinculum symbols to number multipliers.
87+
*
88+
* @var array<string, int>
89+
*/
90+
protected const ROMAN_VINCULUM = [
91+
'\u{033F}' => 1_000_000,
92+
'\u{0305}' => 1_000,
93+
'' => 1,
94+
];
95+
96+
/**
97+
* Maps Roman symbols to numbers.
98+
*
99+
* @var array<string, int>
100+
*/
101+
protected const ROMAN_SYMBOL = [
102+
// standard notation
103+
'M' => 1_000,
104+
'CM' => 900,
105+
'D' => 500,
106+
'CD' => 400,
107+
'C' => 100,
108+
'XC' => 90,
109+
'L' => 50,
110+
'XL' => 40,
111+
'X' => 10,
112+
'IX' => 9,
113+
'V' => 5,
114+
'IV' => 4,
115+
];
116+
78117
/**
79118
* Set the default CSS margin in user units.
80119
*
@@ -265,7 +304,7 @@ protected function getCSSBorderStyle(string $cssborder): array
265304
*
266305
* @return TCellBound cell paddings.
267306
*/
268-
public function getCSSPadding(string $csspadding, float $width = 0.0): array
307+
protected function getCSSPadding(string $csspadding, float $width = 0.0): array
269308
{
270309
/** @var TCellBound $cellpad */
271310
$cellpad = $this->defCSSCellPadding;
@@ -322,7 +361,7 @@ public function getCSSPadding(string $csspadding, float $width = 0.0): array
322361
*
323362
* @return TCellBound cell margins.
324363
*/
325-
public function getCSSMargin(string $cssmargin, float $width = 0.0): array
364+
protected function getCSSMargin(string $cssmargin, float $width = 0.0): array
326365
{
327366
/** @var TCellBound $cellmrg */
328367
$cellmrg = $this->defCSSCellMargin;
@@ -383,7 +422,7 @@ public function getCSSMargin(string $cssmargin, float $width = 0.0): array
383422
*
384423
* @return TCSSBorderSpacing of border spacings.
385424
*/
386-
public function getCSSBorderMargin(string $cssbspace, float $width = 0.0): array
425+
protected function getCSSBorderMargin(string $cssbspace, float $width = 0.0): array
387426
{
388427
/** @var TCSSBorderSpacing $bsp */
389428
$bsp = $this->defCSSBorderSpacing;
@@ -413,4 +452,185 @@ public function getCSSBorderMargin(string $cssbspace, float $width = 0.0): array
413452
$bsp['V'] = $this->toUnit($this->getUnitValuePoints($bsp['V'], $ref));
414453
return $bsp;
415454
}
455+
456+
/**
457+
* Implode CSS data array into a single string.
458+
*
459+
* @param array<string, string> $css array of CSS properties.
460+
*
461+
* @return string merged CSS properties.
462+
*/
463+
protected function implodeCSSData(array $css): string
464+
{
465+
$out = '';
466+
foreach ($css as $style) {
467+
if (!\is_array($style) || empty($style['c']) || (!\is_string($style['c']))) {
468+
continue;
469+
}
470+
$csscmds = \explode(';', $style['c']);
471+
foreach ($csscmds as $cmd) {
472+
if (empty($cmd)) {
473+
continue;
474+
}
475+
$pos = \strpos($cmd, ':');
476+
if ($pos === false) {
477+
continue;
478+
}
479+
$cmd = \substr($cmd, 0, ($pos + 1));
480+
if (\strpos($out, $cmd) !== false) {
481+
// remove duplicate commands (last commands have high priority)
482+
$out = \preg_replace('/' . $cmd . '[^;]+/i', '', $out) ?? '';
483+
}
484+
}
485+
$out .= ';' . $style['c'];
486+
}
487+
// remove multiple semicolons
488+
$out = \preg_replace('/[;]+/', ';', $out) ?? '';
489+
return $out;
490+
}
491+
492+
/**
493+
* Tidy up the CSS string by removing unsupported properties.
494+
*
495+
* @param string $css string containing CSS definitions.
496+
*
497+
* @return string
498+
*/
499+
protected function tidyCSS($css): string
500+
{
501+
if (empty($css)) {
502+
return '';
503+
}
504+
// remove comments
505+
$css = \preg_replace('/\/\*[^\*]*\*\//', '', $css) ?? '';
506+
// remove newlines and multiple spaces
507+
$css = \preg_replace('/[\s]+/', ' ', $css) ?? '';
508+
// remove some spaces
509+
$css = \preg_replace('/[\s]*([;:\{\}]{1})[\s]*/', '\\1', $css) ?? '';
510+
// remove empty blocks
511+
$css = \preg_replace('/([^\}\{]+)\{\}/', '', $css) ?? '';
512+
// replace media type parenthesis
513+
$css = \preg_replace('/@media[\s]+([^\{]*)\{/i', '@media \\', $css) ?? '';
514+
$css = \preg_replace('/\}\}/si', '', $css) ?? '';
515+
// find media blocks (all, braille, embossed, handheld, print, projection, screen, speech, tty, tv)
516+
$blk = [];
517+
$matches = [];
518+
if (\preg_match_all('/@media[\s]+([^\§]*)§([^§]*)§/i', $css, $matches) > 0) {
519+
foreach ($matches[1] as $key => $type) {
520+
$blk[$type] = $matches[2][$key];
521+
}
522+
// remove media blocks
523+
$css = \preg_replace('/@media[\s]+([^\§]*)§([^§]*)§/i', '', $css) ?? '';
524+
}
525+
// keep 'all' and 'print' media, other media types are discarded
526+
if (!empty($blk['all'])) {
527+
$css .= $blk['all'];
528+
}
529+
if (!empty($blk['print'])) {
530+
$css .= $blk['print'];
531+
}
532+
return \trim($css);
533+
}
534+
535+
/**
536+
* Extracts the CSS properties from a CSS string.
537+
*
538+
* @param string $css string containing CSS definitions.
539+
*
540+
* @return array<string, string> CSS properties.
541+
*/
542+
protected function extractCSSproperties($css): array
543+
{
544+
$css = $this->tidyCSS($css);
545+
if (empty($css)) {
546+
return [];
547+
}
548+
$blk = [];
549+
$matches = [];
550+
// explode css data string into array
551+
if (\substr($css, -1) == '}') {
552+
// remove last parethesis
553+
$css = \substr($css, 0, -1);
554+
}
555+
$matches = \explode('}', $css);
556+
foreach ($matches as $key => $block) {
557+
// index 0 contains the CSS selector, index 1 contains CSS properties
558+
$blk[$key] = \explode('{', $block);
559+
if (!isset($blk[$key][1])) {
560+
// remove empty definitions
561+
unset($blk[$key]);
562+
}
563+
}
564+
// split groups of selectors (comma-separated list of selectors)
565+
foreach ($blk as $key => $block) {
566+
if (\strpos($block[0], ',') > 0) {
567+
$selectors = \explode(',', $block[0]);
568+
foreach ($selectors as $sel) {
569+
$blk[] = [0 => \trim($sel), 1 => $block[1]];
570+
}
571+
unset($blk[$key]);
572+
}
573+
}
574+
// covert array to selector => properties
575+
$out = [];
576+
foreach ($blk as $block) {
577+
$selector = $block[0];
578+
// calculate selector's specificity
579+
$matches = [];
580+
$sta = 0; // the declaration is not from is a 'style' attribute
581+
// number of ID attributes
582+
$stb = \intval(\preg_match_all('/[\#]/', $selector, $matches));
583+
// number of other attributes
584+
$stc = \intval(\preg_match_all('/[\[\.]/', $selector, $matches));
585+
// number of pseudo-classes
586+
$stc += \intval(\preg_match_all(
587+
'/[\:]link|visited|hover|active|focus|target|lang|enabled|disabled'
588+
. '|checked|indeterminate|root|nth|first|last|only|empty|contains|not/i',
589+
$selector,
590+
$matches,
591+
));
592+
// number of element names
593+
$std = \intval(\preg_match_all('/[\>\+\~\s]{1}[a-zA-Z0-9]+/', " $selector", $matches));
594+
// number of pseudo-elements
595+
$std += \intval(\preg_match_all('/[\:][\:]/', $selector, $matches));
596+
$specificity = $sta . $stb . $stc . $std;
597+
// add specificity to the beginning of the selector
598+
$out["$specificity $selector"] = $block[1];
599+
}
600+
// sort selectors alphabetically to account for specificity
601+
\ksort($out, SORT_STRING);
602+
return $out;
603+
}
604+
605+
/**
606+
* Returns the Roman representation of an integer number.
607+
* Roman standard notation can represent numbers up to 3,999.
608+
* For bigger numbers, up to two layers of the "vinculum" notation
609+
* are used for a max value of 3,999,999,999.
610+
*
611+
* @param int $num number to convert.
612+
*
613+
* @return string roman representation of the specified number.
614+
*/
615+
protected function intToRoman(int $num): string
616+
{
617+
if ($num > self::ROMAN_LIMIT) {
618+
return \strval($num);
619+
}
620+
$rmn = '';
621+
foreach (self::ROMAN_VINCULUM as $sfx => $mul) {
622+
foreach (self::ROMAN_SYMBOL as $sym => $val) {
623+
$limit = (int)($mul * $val);
624+
while ($num >= $limit) {
625+
$rmn .= $sym[0] . $sfx . (!empty($sym[1]) ? $sym[1] . $sfx : '');
626+
$num -= $limit;
627+
}
628+
}
629+
}
630+
while ($num >= 1) {
631+
$rmn .= 'I';
632+
$num--;
633+
}
634+
return $rmn;
635+
}
416636
}

src/HTML.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ abstract class HTML extends \Com\Tecnick\Pdf\CSS
4343
*
4444
* @return string XHTML code cleaned up.
4545
*/
46-
public static function tidyHTML(
46+
protected function tidyHTML(
4747
string $html,
4848
string $defcss,
4949
): string {

0 commit comments

Comments
 (0)