@@ -30,22 +30,42 @@ public function layout(array $nodes, float $width, float $height): LayoutResult
3030
3131 foreach ($ this ->walk ($ nodes ) as $ node ) {
3232 $ style = $ this ->styleParser ->parse ($ node ->attributes ['style ' ] ?? '' );
33+ $ margin = $ this ->parseBoxSpacing ((string ) $ style ->get ('margin ' , '0 ' ));
34+ $ padding = $ this ->parseBoxSpacing ((string ) $ style ->get ('padding ' , '0 ' ));
35+
36+ $ cursorY -= ($ margin ['top ' ] + $ padding ['top ' ]);
37+
3338 $ fontSize = $ this ->toPoints ($ style ->get ('font-size ' , '10 ' ));
3439 $ lineHeight = max ($ fontSize * 1.2 , $ this ->toPoints ($ style ->get ('line-height ' , (string ) ($ fontSize * 1.2 ))));
40+ $ fontAlias = $ this ->resolveFontAlias ((string ) $ style ->get ('font-family ' , 'helvetica ' ), (string ) $ style ->get ('font-weight ' , 'normal ' ));
41+
42+ $ boxWidth = $ this ->toPoints ((string ) $ style ->get ('width ' , '0 ' ));
43+ if ($ boxWidth <= 0 ) {
44+ $ boxWidth = max ($ width - $ margin ['left ' ] - $ margin ['right ' ] - $ padding ['left ' ] - $ padding ['right ' ], 0 );
45+ }
46+ $ leftBase = $ margin ['left ' ] + $ padding ['left ' ];
47+ $ rightBase = $ leftBase + $ boxWidth ;
3548
3649 if ($ node ->tag === 'img ' ) {
37- $ imgWidth = $ this ->toPoints ($ style ->get ('width ' , '32 ' ));
38- $ imgHeight = $ this ->toPoints ($ style ->get ('height ' , '32 ' ));
50+ $ imgWidth = $ this ->toPoints ((string ) $ style ->get ('width ' , '32 ' ));
51+ $ imgHeight = $ this ->toPoints ((string ) $ style ->get ('height ' , '32 ' ));
52+ if ($ imgWidth <= 0 ) {
53+ $ imgWidth = 32.0 ;
54+ }
55+ if ($ imgHeight <= 0 ) {
56+ $ imgHeight = 32.0 ;
57+ }
58+
3959 $ images [] = new LayoutImage (
4060 alias: 'Im ' . $ imageCount ,
41- x: 4.0 ,
61+ x: $ leftBase ,
4262 y: max ($ cursorY - $ imgHeight , 0 ),
4363 width: min ($ imgWidth , $ width ),
4464 height: min ($ imgHeight , $ height ),
4565 source: $ node ->attributes ['src ' ] ?? '' ,
4666 );
4767 ++$ imageCount ;
48- $ cursorY -= ($ imgHeight + 2.0 );
68+ $ cursorY -= ($ imgHeight + 2.0 + $ margin [ ' bottom ' ] + $ padding [ ' bottom ' ] );
4969 continue ;
5070 }
5171
@@ -61,21 +81,21 @@ public function layout(array $nodes, float $width, float $height): LayoutResult
6181
6282 $ align = strtolower ((string ) $ style ->get ('text-align ' , 'left ' ));
6383 $ x = match ($ align ) {
64- 'center ' => $ width / 2.0 ,
65- 'right ' => max ($ width - 8.0 , 0 ),
66- default => 8.0 ,
84+ 'center ' => $ leftBase + ( $ boxWidth / 2.0 ) ,
85+ 'right ' => max ($ rightBase - 8.0 , 0 ),
86+ default => $ leftBase + 8.0 ,
6787 };
6888
6989 $ lines [] = new LayoutLine (
7090 text: $ text ,
7191 x: $ x ,
7292 y: max ($ cursorY , 0 ),
7393 fontSize: $ fontSize ,
74- fontAlias: ' F1 ' ,
94+ fontAlias: $ fontAlias ,
7595 rgbColor: (string ) $ style ->get ('color ' , '#000000 ' ),
7696 );
7797
78- $ cursorY -= $ lineHeight ;
98+ $ cursorY -= ( $ lineHeight + $ margin [ ' bottom ' ] + $ padding [ ' bottom ' ]) ;
7999 }
80100
81101 return new LayoutResult (lines: $ lines , images: $ images );
@@ -112,4 +132,64 @@ private function toPoints(string $value): float
112132
113133 return $ number ;
114134 }
135+
136+ /**
137+ * @return array{top: float, right: float, bottom: float, left: float}
138+ */
139+ private function parseBoxSpacing (string $ value ): array
140+ {
141+ $ tokens = preg_split ('/\s+/ ' , trim ($ value ));
142+ $ tokens = array_values (array_filter ($ tokens ?: [], static fn (string $ token ): bool => $ token !== '' ));
143+
144+ if ($ tokens === []) {
145+ return ['top ' => 0.0 , 'right ' => 0.0 , 'bottom ' => 0.0 , 'left ' => 0.0 ];
146+ }
147+
148+ $ points = array_map (fn (string $ token ): float => $ this ->toPoints ($ token ), $ tokens );
149+ $ count = count ($ points );
150+
151+ if ($ count === 1 ) {
152+ return ['top ' => $ points [0 ], 'right ' => $ points [0 ], 'bottom ' => $ points [0 ], 'left ' => $ points [0 ]];
153+ }
154+
155+ if ($ count === 2 ) {
156+ return ['top ' => $ points [0 ], 'right ' => $ points [1 ], 'bottom ' => $ points [0 ], 'left ' => $ points [1 ]];
157+ }
158+
159+ if ($ count === 3 ) {
160+ return ['top ' => $ points [0 ], 'right ' => $ points [1 ], 'bottom ' => $ points [2 ], 'left ' => $ points [1 ]];
161+ }
162+
163+ return ['top ' => $ points [0 ], 'right ' => $ points [1 ], 'bottom ' => $ points [2 ], 'left ' => $ points [3 ]];
164+ }
165+
166+ private function resolveFontAlias (string $ fontFamily , string $ fontWeight ): string
167+ {
168+ $ primary = strtolower (trim (explode (', ' , $ fontFamily )[0 ], " \t\n\r\0\x0B' \"" ));
169+ $ isBold = $ this ->isBoldWeight ($ fontWeight );
170+
171+ if (str_contains ($ primary , 'times ' )) {
172+ return $ isBold ? 'F4 ' : 'F3 ' ;
173+ }
174+
175+ if (str_contains ($ primary , 'courier ' )) {
176+ return $ isBold ? 'F6 ' : 'F5 ' ;
177+ }
178+
179+ return $ isBold ? 'F2 ' : 'F1 ' ;
180+ }
181+
182+ private function isBoldWeight (string $ fontWeight ): bool
183+ {
184+ $ normalized = strtolower (trim ($ fontWeight ));
185+ if ($ normalized === 'bold ' || $ normalized === 'bolder ' ) {
186+ return true ;
187+ }
188+
189+ if (is_numeric ($ normalized )) {
190+ return (int ) $ normalized >= 600 ;
191+ }
192+
193+ return false ;
194+ }
115195}
0 commit comments