1<?php
2/*******************************************************************************
3* Utility to generate font definition files                                    *
4*                                                                              *
5* Version: 1.3                                                                 *
6* Date:    2015-11-29                                                          *
7* Author:  Olivier PLATHEY                                                     *
8*******************************************************************************/
9
10require('ttfparser.php');
11
12function Message($txt, $severity='')
13{
14	if(PHP_SAPI=='cli')
15	{
16		if($severity)
17			echo "$severity: ";
18		echo "$txt\n";
19	}
20	else
21	{
22		if($severity)
23			echo "<b>$severity</b>: ";
24		echo "$txt<br>";
25	}
26}
27
28function Notice($txt)
29{
30	Message($txt, 'Notice');
31}
32
33function Warning($txt)
34{
35	Message($txt, 'Warning');
36}
37
38function Error($txt)
39{
40	Message($txt, 'Error');
41	exit;
42}
43
44function LoadMap($enc)
45{
46	$file = dirname(__FILE__).'/'.strtolower($enc).'.map';
47	$a = file($file);
48	if(empty($a))
49		Error('Encoding not found: '.$enc);
50	$map = array_fill(0, 256, array('uv'=>-1, 'name'=>'.notdef'));
51	foreach($a as $line)
52	{
53		$e = explode(' ', rtrim($line));
54		$c = hexdec(substr($e[0],1));
55		$uv = hexdec(substr($e[1],2));
56		$name = $e[2];
57		$map[$c] = array('uv'=>$uv, 'name'=>$name);
58	}
59	return $map;
60}
61
62function GetInfoFromTrueType($file, $embed, $subset, $map)
63{
64	// Return information from a TrueType font
65	try
66	{
67		$ttf = new TTFParser($file);
68		$ttf->Parse();
69	}
70	catch(Exception $e)
71	{
72		Error($e->getMessage());
73	}
74	if($embed)
75	{
76		if(!$ttf->embeddable)
77			Error('Font license does not allow embedding');
78		if($subset)
79		{
80			$chars = array();
81			foreach($map as $v)
82			{
83				if($v['name']!='.notdef')
84					$chars[] = $v['uv'];
85			}
86			$ttf->Subset($chars);
87			$info['Data'] = $ttf->Build();
88		}
89		else
90			$info['Data'] = file_get_contents($file);
91		$info['OriginalSize'] = strlen($info['Data']);
92	}
93	$k = 1000/$ttf->unitsPerEm;
94	$info['FontName'] = $ttf->postScriptName;
95	$info['Bold'] = $ttf->bold;
96	$info['ItalicAngle'] = $ttf->italicAngle;
97	$info['IsFixedPitch'] = $ttf->isFixedPitch;
98	$info['Ascender'] = round($k*$ttf->typoAscender);
99	$info['Descender'] = round($k*$ttf->typoDescender);
100	$info['UnderlineThickness'] = round($k*$ttf->underlineThickness);
101	$info['UnderlinePosition'] = round($k*$ttf->underlinePosition);
102	$info['FontBBox'] = array(round($k*$ttf->xMin), round($k*$ttf->yMin), round($k*$ttf->xMax), round($k*$ttf->yMax));
103	$info['CapHeight'] = round($k*$ttf->capHeight);
104	$info['MissingWidth'] = round($k*$ttf->glyphs[0]['w']);
105	$widths = array_fill(0, 256, $info['MissingWidth']);
106	foreach($map as $c=>$v)
107	{
108		if($v['name']!='.notdef')
109		{
110			if(isset($ttf->chars[$v['uv']]))
111			{
112				$id = $ttf->chars[$v['uv']];
113				$w = $ttf->glyphs[$id]['w'];
114				$widths[$c] = round($k*$w);
115			}
116			else
117				Warning('Character '.$v['name'].' is missing');
118		}
119	}
120	$info['Widths'] = $widths;
121	return $info;
122}
123
124function GetInfoFromType1($file, $embed, $map)
125{
126	// Return information from a Type1 font
127	if($embed)
128	{
129		$f = fopen($file, 'rb');
130		if(!$f)
131			Error('Can\'t open font file');
132		// Read first segment
133		$a = unpack('Cmarker/Ctype/Vsize', fread($f,6));
134		if($a['marker']!=128)
135			Error('Font file is not a valid binary Type1');
136		$size1 = $a['size'];
137		$data = fread($f, $size1);
138		// Read second segment
139		$a = unpack('Cmarker/Ctype/Vsize', fread($f,6));
140		if($a['marker']!=128)
141			Error('Font file is not a valid binary Type1');
142		$size2 = $a['size'];
143		$data .= fread($f, $size2);
144		fclose($f);
145		$info['Data'] = $data;
146		$info['Size1'] = $size1;
147		$info['Size2'] = $size2;
148	}
149
150	$afm = substr($file, 0, -3).'afm';
151	if(!file_exists($afm))
152		Error('AFM font file not found: '.$afm);
153	$a = file($afm);
154	if(empty($a))
155		Error('AFM file empty or not readable');
156	foreach($a as $line)
157	{
158		$e = explode(' ', rtrim($line));
159		if(count($e)<2)
160			continue;
161		$entry = $e[0];
162		if($entry=='C')
163		{
164			$w = $e[4];
165			$name = $e[7];
166			$cw[$name] = $w;
167		}
168		elseif($entry=='FontName')
169			$info['FontName'] = $e[1];
170		elseif($entry=='Weight')
171			$info['Weight'] = $e[1];
172		elseif($entry=='ItalicAngle')
173			$info['ItalicAngle'] = (int)$e[1];
174		elseif($entry=='Ascender')
175			$info['Ascender'] = (int)$e[1];
176		elseif($entry=='Descender')
177			$info['Descender'] = (int)$e[1];
178		elseif($entry=='UnderlineThickness')
179			$info['UnderlineThickness'] = (int)$e[1];
180		elseif($entry=='UnderlinePosition')
181			$info['UnderlinePosition'] = (int)$e[1];
182		elseif($entry=='IsFixedPitch')
183			$info['IsFixedPitch'] = ($e[1]=='true');
184		elseif($entry=='FontBBox')
185			$info['FontBBox'] = array((int)$e[1], (int)$e[2], (int)$e[3], (int)$e[4]);
186		elseif($entry=='CapHeight')
187			$info['CapHeight'] = (int)$e[1];
188		elseif($entry=='StdVW')
189			$info['StdVW'] = (int)$e[1];
190	}
191
192	if(!isset($info['FontName']))
193		Error('FontName missing in AFM file');
194	if(!isset($info['Ascender']))
195		$info['Ascender'] = $info['FontBBox'][3];
196	if(!isset($info['Descender']))
197		$info['Descender'] = $info['FontBBox'][1];
198	$info['Bold'] = isset($info['Weight']) && preg_match('/bold|black/i', $info['Weight']);
199	if(isset($cw['.notdef']))
200		$info['MissingWidth'] = $cw['.notdef'];
201	else
202		$info['MissingWidth'] = 0;
203	$widths = array_fill(0, 256, $info['MissingWidth']);
204	foreach($map as $c=>$v)
205	{
206		if($v['name']!='.notdef')
207		{
208			if(isset($cw[$v['name']]))
209				$widths[$c] = $cw[$v['name']];
210			else
211				Warning('Character '.$v['name'].' is missing');
212		}
213	}
214	$info['Widths'] = $widths;
215	return $info;
216}
217
218function MakeFontDescriptor($info)
219{
220	// Ascent
221	$fd = "array('Ascent'=>".$info['Ascender'];
222	// Descent
223	$fd .= ",'Descent'=>".$info['Descender'];
224	// CapHeight
225	if(!empty($info['CapHeight']))
226		$fd .= ",'CapHeight'=>".$info['CapHeight'];
227	else
228		$fd .= ",'CapHeight'=>".$info['Ascender'];
229	// Flags
230	$flags = 0;
231	if($info['IsFixedPitch'])
232		$flags += 1<<0;
233	$flags += 1<<5;
234	if($info['ItalicAngle']!=0)
235		$flags += 1<<6;
236	$fd .= ",'Flags'=>".$flags;
237	// FontBBox
238	$fbb = $info['FontBBox'];
239	$fd .= ",'FontBBox'=>'[".$fbb[0].' '.$fbb[1].' '.$fbb[2].' '.$fbb[3]."]'";
240	// ItalicAngle
241	$fd .= ",'ItalicAngle'=>".$info['ItalicAngle'];
242	// StemV
243	if(isset($info['StdVW']))
244		$stemv = $info['StdVW'];
245	elseif($info['Bold'])
246		$stemv = 120;
247	else
248		$stemv = 70;
249	$fd .= ",'StemV'=>".$stemv;
250	// MissingWidth
251	$fd .= ",'MissingWidth'=>".$info['MissingWidth'].')';
252	return $fd;
253}
254
255function MakeWidthArray($widths)
256{
257	$s = "array(\n\t";
258	for($c=0;$c<=255;$c++)
259	{
260		if(chr($c)=="'")
261			$s .= "'\\''";
262		elseif(chr($c)=="\\")
263			$s .= "'\\\\'";
264		elseif($c>=32 && $c<=126)
265			$s .= "'".chr($c)."'";
266		else
267			$s .= "chr($c)";
268		$s .= '=>'.$widths[$c];
269		if($c<255)
270			$s .= ',';
271		if(($c+1)%22==0)
272			$s .= "\n\t";
273	}
274	$s .= ')';
275	return $s;
276}
277
278function MakeFontEncoding($map)
279{
280	// Build differences from reference encoding
281	$ref = LoadMap('cp1252');
282	$s = '';
283	$last = 0;
284	for($c=32;$c<=255;$c++)
285	{
286		if($map[$c]['name']!=$ref[$c]['name'])
287		{
288			if($c!=$last+1)
289				$s .= $c.' ';
290			$last = $c;
291			$s .= '/'.$map[$c]['name'].' ';
292		}
293	}
294	return rtrim($s);
295}
296
297function MakeUnicodeArray($map)
298{
299	// Build mapping to Unicode values
300	$ranges = array();
301	foreach($map as $c=>$v)
302	{
303		$uv = $v['uv'];
304		if($uv!=-1)
305		{
306			if(isset($range))
307			{
308				if($c==$range[1]+1 && $uv==$range[3]+1)
309				{
310					$range[1]++;
311					$range[3]++;
312				}
313				else
314				{
315					$ranges[] = $range;
316					$range = array($c, $c, $uv, $uv);
317				}
318			}
319			else
320				$range = array($c, $c, $uv, $uv);
321		}
322	}
323	$ranges[] = $range;
324
325	foreach($ranges as $range)
326	{
327		if(isset($s))
328			$s .= ',';
329		else
330			$s = 'array(';
331		$s .= $range[0].'=>';
332		$nb = $range[1]-$range[0]+1;
333		if($nb>1)
334			$s .= 'array('.$range[2].','.$nb.')';
335		else
336			$s .= $range[2];
337	}
338	$s .= ')';
339	return $s;
340}
341
342function SaveToFile($file, $s, $mode)
343{
344	$f = fopen($file, 'w'.$mode);
345	if(!$f)
346		Error('Can\'t write to file '.$file);
347	fwrite($f, $s);
348	fclose($f);
349}
350
351function MakeDefinitionFile($file, $type, $enc, $embed, $subset, $map, $info)
352{
353	$s = "<?php\n";
354	$s .= '$type = \''.$type."';\n";
355	$s .= '$name = \''.$info['FontName']."';\n";
356	$s .= '$desc = '.MakeFontDescriptor($info).";\n";
357	$s .= '$up = '.$info['UnderlinePosition'].";\n";
358	$s .= '$ut = '.$info['UnderlineThickness'].";\n";
359	$s .= '$cw = '.MakeWidthArray($info['Widths']).";\n";
360	$s .= '$enc = \''.$enc."';\n";
361	$diff = MakeFontEncoding($map);
362	if($diff)
363		$s .= '$diff = \''.$diff."';\n";
364	$s .= '$uv = '.MakeUnicodeArray($map).";\n";
365	if($embed)
366	{
367		$s .= '$file = \''.$info['File']."';\n";
368		if($type=='Type1')
369		{
370			$s .= '$size1 = '.$info['Size1'].";\n";
371			$s .= '$size2 = '.$info['Size2'].";\n";
372		}
373		else
374		{
375			$s .= '$originalsize = '.$info['OriginalSize'].";\n";
376			if($subset)
377				$s .= "\$subsetted = true;\n";
378		}
379	}
380	$s .= "?>\n";
381	SaveToFile($file, $s, 't');
382}
383
384function MakeFont($fontfile, $enc='cp1252', $embed=true, $subset=true)
385{
386	// Generate a font definition file
387	if(get_magic_quotes_runtime())
388		@set_magic_quotes_runtime(false);
389	ini_set('auto_detect_line_endings', '1');
390
391	if(!file_exists($fontfile))
392		Error('Font file not found: '.$fontfile);
393	$ext = strtolower(substr($fontfile,-3));
394	if($ext=='ttf' || $ext=='otf')
395		$type = 'TrueType';
396	elseif($ext=='pfb')
397		$type = 'Type1';
398	else
399		Error('Unrecognized font file extension: '.$ext);
400
401	$map = LoadMap($enc);
402
403	if($type=='TrueType')
404		$info = GetInfoFromTrueType($fontfile, $embed, $subset, $map);
405	else
406		$info = GetInfoFromType1($fontfile, $embed, $map);
407
408	$basename = substr(basename($fontfile), 0, -4);
409	if($embed)
410	{
411		if(function_exists('gzcompress'))
412		{
413			$file = $basename.'.z';
414			SaveToFile($file, gzcompress($info['Data']), 'b');
415			$info['File'] = $file;
416			Message('Font file compressed: '.$file);
417		}
418		else
419		{
420			$info['File'] = basename($fontfile);
421			$subset = false;
422			Notice('Font file could not be compressed (zlib extension not available)');
423		}
424	}
425
426	MakeDefinitionFile($basename.'.php', $type, $enc, $embed, $subset, $map, $info);
427	Message('Font definition file generated: '.$basename.'.php');
428}
429
430if(PHP_SAPI=='cli')
431{
432	// Command-line interface
433	ini_set('log_errors', '0');
434	if($argc==1)
435		die("Usage: php makefont.php fontfile [encoding] [embed] [subset]\n");
436	$fontfile = $argv[1];
437	if($argc>=3)
438		$enc = $argv[2];
439	else
440		$enc = 'cp1252';
441	if($argc>=4)
442		$embed = ($argv[3]=='true' || $argv[3]=='1');
443	else
444		$embed = true;
445	if($argc>=5)
446		$subset = ($argv[4]=='true' || $argv[4]=='1');
447	else
448		$subset = true;
449	MakeFont($fontfile, $enc, $embed, $subset);
450}
451?>
452