1010use ModStart \Core \Provider \FontProvider ;
1111use ModStart \Core \Util \ColorUtil ;
1212use ModStart \Core \Util \FileUtil ;
13+ use ModStart \Core \Util \LogUtil ;
1314use ModStart \Core \Util \QrcodeUtil ;
14- use ModStart \Core \Util \SerializeUtil ;
1515
1616class ImageDesignUtil
1717{
@@ -66,24 +66,131 @@ public static function configSaveCheck($imageConfig)
6666 BizException::throwsIf ('背景图和背景色同时为空 ' , empty ($ imageConfig ['backgroundImage ' ]) && empty ($ imageConfig ['backgroundColor ' ]));
6767 }
6868
69- public static function render ( $ imageConfig , $ variables = [] )
69+ private static function getTextWidth ( $ text , $ fontPath , $ fontSize )
7070 {
71- BizException::throwsIfEmpty ('imageConfig 为空 ' , $ imageConfig );
71+ $ box = @imagettfbbox ($ fontSize , 0 , $ fontPath , $ text );
72+ $ width = abs ($ box [2 ] - $ box [0 ]);
73+ return $ width ;
74+ }
75+
76+ private static function ttfHasChar ($ fontFile , $ char )
77+ {
78+ $ fp = fopen ($ fontFile , 'rb ' );
79+ if (!$ fp ) return false ;
80+ fseek ($ fp , 4 );
81+ $ numTables = unpack ('n ' , fread ($ fp , 2 ))[1 ];
82+ fseek ($ fp , 12 );
83+ $ cmapOffset = null ;
84+ for ($ i = 0 ; $ i < $ numTables ; $ i ++) {
85+ $ record = unpack ('a4tag/NcheckSum/Noffset/Nlength ' , fread ($ fp , 16 ));
86+ if ($ record ['tag ' ] === 'cmap ' ) {
87+ $ cmapOffset = $ record ['offset ' ];
88+ break ;
89+ }
90+ }
91+
92+ if ($ cmapOffset === null ) return false ;
93+
94+ // 读取 cmap 表头
95+ fseek ($ fp , $ cmapOffset );
96+ $ cmapHeader = unpack ('nversion/nnumTables ' , fread ($ fp , 4 ));
97+
98+ $ unicodeOffset = null ;
99+ for ($ i = 0 ; $ i < $ cmapHeader ['numTables ' ]; $ i ++) {
100+ $ entry = unpack ('nplatformID/nencodingID/Noffset ' , fread ($ fp , 8 ));
101+ if ($ entry ['platformID ' ] == 3 && $ entry ['encodingID ' ] == 1 ) { // Windows Unicode BMP
102+ $ unicodeOffset = $ entry ['offset ' ];
103+ break ;
104+ }
105+ }
106+
107+ if ($ unicodeOffset === null ) return false ;
108+ fseek ($ fp , $ cmapOffset + $ unicodeOffset );
109+ $ format = unpack ('nformat ' , fread ($ fp , 2 ))['format ' ];
110+
111+ if ($ format != 4 ) {
112+ fclose ($ fp );
113+ return false ;
114+ }
115+
116+ // 解析 format 4
117+ fseek ($ fp , -2 , SEEK_CUR );
118+ $ fmt4 = unpack ('nformat/nlength/nlanguage/nsegCountX2/nsearchRange/nentrySelector/nrangeShift ' , fread ($ fp , 14 ));
119+ $ segCount = $ fmt4 ['segCountX2 ' ] / 2 ;
120+
121+ $ endCodes = unpack ("n $ segCount " , fread ($ fp , $ segCount * 2 ));
122+ fseek ($ fp , 2 , SEEK_CUR ); // skip reservedPad
123+ $ startCodes = unpack ("n $ segCount " , fread ($ fp , $ segCount * 2 ));
124+ $ idDeltas = unpack ("n $ segCount " , fread ($ fp , $ segCount * 2 ));
125+
126+ fclose ($ fp );
127+
128+ $ code = mb_ord ($ char , 'UTF-8 ' );
129+ for ($ i = 1 ; $ i <= $ segCount ; $ i ++) {
130+ if ($ code >= $ startCodes [$ i ] && $ code <= $ endCodes [$ i ]) {
131+ return true ;
132+ }
133+ }
134+ return false ;
135+ }
136+
137+ private static function getTtfFont ($ fontFile , $ fallbackFontFile , $ text )
138+ {
139+ $ chars = preg_split ('//u ' , $ text , -1 , PREG_SPLIT_NO_EMPTY );
140+ foreach ($ chars as $ char ) {
141+ if (!self ::ttfHasChar ($ fontFile , $ char )) {
142+ return $ fallbackFontFile ;
143+ }
144+ }
145+ return $ fontFile ;
146+ }
147+
148+ private static function replaceParam ($ data , $ variables )
149+ {
150+ if (empty ($ variables )) {
151+ return $ data ;
152+ }
153+ if (is_array ($ data )) {
154+ $ newData = [];
155+ foreach ($ data as $ k => $ v ) {
156+ $ newData [$ k ] = self ::replaceParam ($ v , $ variables );
157+ }
158+ return $ newData ;
159+ }
160+ if (is_string ($ data )) {
161+ foreach ($ variables as $ k => $ v ) {
162+ $ data = str_replace ('${ ' . $ k . '} ' , $ v , $ data );
163+ }
164+ return $ data ;
165+ }
166+ return $ data ;
167+ }
168+
169+ public static function render ($ imageConfigJson , $ variables = [])
170+ {
171+ BizException::throwsIfEmpty ('imageConfig 为空 ' , $ imageConfigJson );
72172 $ configParam = [];
73173 foreach ($ variables as $ k => $ v ) {
74174 $ configParam ['${ ' . $ k . '} ' ] = $ v ;
75175 }
76- $ imageConfig = SerializeUtil::jsonEncode ($ imageConfig , JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
77- $ imageConfig = str_replace (array_keys ($ configParam ), array_values ($ configParam ), $ imageConfig );
78- $ imageConfig = json_decode ($ imageConfig , true );
176+ //$imageConfig = SerializeUtil::jsonEncode($imageConfigJson, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
177+ //$imageConfig = str_replace(array_keys($configParam), array_values($configParam), $imageConfig);
178+ //LogUtil::info('xxxxx', [
179+ // '$imageConfigJson' => $imageConfigJson,
180+ // '$imageConfig' => $imageConfig,
181+ // '$imageConfigNew' => self::replaceParam($imageConfigJson, $variables)
182+ //]);
183+ //$imageConfig = json_decode($imageConfig, true);
184+ $ imageConfig = self ::replaceParam ($ imageConfigJson , $ variables );
79185
80186 BizException::throwsIf ('width empty ' , empty ($ imageConfig ['width ' ]));
81187 BizException::throwsIf ('height empty ' , empty ($ imageConfig ['height ' ]));
82188 BizException::throwsIf ('backgroundImage 和 backgroundColor 为空 ' , empty ($ imageConfig ['backgroundImage ' ]) && empty ($ imageConfig ['backgroundColor ' ]));
83189 BizException::throwsIf ('blocks empty ' , !isset ($ imageConfig ['blocks ' ]));
84190
191+ $ systemFontPath = FontProvider::firstLocalPathOrFail ();
85192 if (empty ($ imageConfig ['font ' ])) {
86- $ fontPath = FontProvider:: firstLocalPathOrFail () ;
193+ $ fontPath = $ systemFontPath ;
87194 } else {
88195 $ fontPath = FileUtil::savePathToLocalTemp ($ imageConfig ['font ' ], 'ttf ' , true );
89196 }
@@ -101,7 +208,44 @@ public static function render($imageConfig, $variables = [])
101208 switch ($ item ['type ' ]) {
102209 case 'text ' :
103210 $ lineHeight = isset ($ item ['data ' ]['lineHeight ' ]) ? $ item ['data ' ]['lineHeight ' ] : 1.2 ;
104- $ lines = explode (self ::LINE_BREAK , $ item ['data ' ]['text ' ]);
211+ $ textFontPath = empty ($ item ['data ' ]['font ' ]) ? null : $ item ['data ' ]['font ' ];
212+ if ($ textFontPath ) {
213+ $ textFontPath = FileUtil::savePathToLocalTemp ($ textFontPath , 'ttf ' , true );
214+ }
215+ if (empty ($ textFontPath )) {
216+ $ textFontPath = $ fontPath ;
217+ }
218+ $ textFontPath = self ::getTtfFont ($ textFontPath , $ systemFontPath , $ item ['data ' ]['text ' ]);
219+ $ linesForBreak = explode (self ::LINE_BREAK , $ item ['data ' ]['text ' ]);
220+ $ lines = [];
221+ foreach ($ linesForBreak as $ line ) {
222+ $ parts = explode ("\n" , $ line );
223+ foreach ($ parts as $ part ) {
224+ $ lines [] = $ part ;
225+ }
226+ }
227+ $ lines = array_filter (array_map ('trim ' , $ lines ));
228+ if (!empty ($ item ['data ' ]['width ' ])) {
229+ $ newLines = [];
230+ foreach ($ lines as $ text ) {
231+ $ currentLine = '' ;
232+ $ words = preg_split ('//u ' , $ text , -1 , PREG_SPLIT_NO_EMPTY );
233+ foreach ($ words as $ char ) {
234+ $ testLine = $ currentLine . $ char ;
235+ $ lineWidth = self ::getTextWidth ($ testLine , $ textFontPath , $ item ['data ' ]['size ' ]);
236+ if ($ lineWidth > $ item ['data ' ]['width ' ] && $ currentLine !== '' ) {
237+ $ newLines [] = $ currentLine ;
238+ $ currentLine = $ char ;
239+ } else {
240+ $ currentLine = $ testLine ;
241+ }
242+ }
243+ if ($ currentLine !== '' ) {
244+ $ newLines [] = $ currentLine ;
245+ }
246+ }
247+ $ lines = $ newLines ;
248+ }
105249 $ offsets = [];
106250 if (!empty ($ item ['data ' ]['shadowOffset ' ])) {
107251 if (empty ($ item ['data ' ]['shadowColor ' ])) {
@@ -139,13 +283,22 @@ public static function render($imageConfig, $variables = [])
139283 if (empty ($ line )) {
140284 continue ;
141285 }
142- $ image ->text ($ line , $ item ['x ' ] + $ offset ['x ' ], $ y + $ offset ['y ' ], function ($ font ) use ($ item , $ offset , $ fontPath ) {
143- $ font ->file ($ fontPath );
144- $ font ->size ($ item ['data ' ]['size ' ]);
145- $ font ->color ($ offset ['color ' ]);
146- $ font ->align ($ item ['data ' ]['align ' ]);
147- $ font ->valign ('top ' );
148- });
286+ //LogUtil::info('xxxx', [
287+ // '$line' => $line,
288+ // '$x' => $item['x'] + $offset['x'],
289+ // '$y' => $y + $offset['y'],
290+ // 'align' => $item['data']['align'],
291+ //]);
292+ $ image ->text (
293+ $ line , $ item ['x ' ] + $ offset ['x ' ], $ y + $ offset ['y ' ],
294+ function ($ font ) use ($ item , $ offset , $ textFontPath ) {
295+ $ font ->file ($ textFontPath );
296+ $ font ->size ($ item ['data ' ]['size ' ]);
297+ $ font ->color ($ offset ['color ' ]);
298+ $ font ->align ($ item ['data ' ]['align ' ]);
299+ $ font ->valign ('top ' );
300+ }
301+ );
149302 $ y += $ item ['data ' ]['size ' ] * $ lineHeight ;
150303 }
151304 }
0 commit comments