@@ -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 \\1§ ' , $ 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}
0 commit comments