Some assorted fixes for html2pdf (converted line endings) and the patient report.
[openemr.git] / library / html2pdf / parsingHTML.class.php
blob196c0a77587d42d6424e9e736ac2fa9b78a6370e
1 <?php
2 /**
3 * Logiciel : HTML2PDF - classe ParsingHTML
5 * Convertisseur HTML => PDF
6 * Distribué sous la licence LGPL.
8 * @author Laurent MINGUET <webmaster@html2pdf.fr>
9 * @version 3.31
12 class parsingHTML
14 var $html = ''; // code HTML à parser
15 var $num = 0; // numéro de table
16 var $level = 0; // niveaux de table
17 var $encoding = ''; // encodage
18 var $code = array(); // code HTML parsé
20 /**
21 * Constructeur
23 * @return null
25 function parsingHTML($encoding = 'ISO-8859-15')
27 $this->num = 0;
28 $this->level = array($this->num);
29 $this->html = '';
30 $this->code = array();
31 $this->setEncoding($encoding);
34 function setEncoding($encoding)
36 $this->encoding = $encoding;
39 /**
40 * Définir le code HTML à parser
42 * @param string code html
43 * @return null
45 function setHTML($html)
47 $html = preg_replace('/<!--(.*)-->/isU', '', $html);
48 $this->html = $html;
51 /**
52 * parser le code HTML
54 * @return null
56 function parse()
58 $parents = array();
60 // chercher les balises HTML du code
61 $tmp = array();
62 $this->searchCode($tmp);
64 // identifier les balises une à une
65 $pre_in = false;
66 $pre_br = array(
67 'name' => 'br',
68 'close' => false,
69 'param' => array(
70 'style' => array(),
71 'num' => 0
75 $balises_no_closed = array(
76 'br', 'hr', 'img', 'col',
77 'input', 'link', 'option',
78 'circle', 'ellipse', 'path', 'rect', 'line', 'polygon', 'polyline'
80 $todos = array();
81 foreach($tmp as $part)
83 // si c'est un code
84 if ($part[0]=='code')
86 $res = $this->analiseCode($part[1]);
88 // si le code est bien un code analisable
89 if ($res)
91 $res['html_pos'] = $part[2];
92 if (!in_array($res['name'], $balises_no_closed))
94 if ($res['close'])
96 if (count($parents)<1)
97 HTML2PDF::makeError(3, __FILE__, __LINE__, $res['name'], $this->getHtmlErrorCode($res['html_pos']));
98 else if ($parents[count($parents)-1]!=$res['name'])
99 HTML2PDF::makeError(4, __FILE__, __LINE__, $parents, $this->getHtmlErrorCode($res['html_pos']));
100 else
101 unset($parents[count($parents)-1]);
103 else
105 if ($res['autoclose'])
107 $todos[] = $res;
108 $res['params'] = array();
109 $res['close'] = true;
111 else
112 $parents[count($parents)] = $res['name'];
115 if (($res['name']=='pre' || $res['name']=='code') && !$res['autoclose'])
116 $pre_in = !$res['close'];
119 $todos[] = $res;
121 // sinon (code non analisable) => on le transforme en texte
122 else
124 $part[0]='txt';
127 // sinon si c'est un texte
128 if ($part[0]=='txt')
130 // enregistrer l'action correspondante
131 if (!$pre_in)
133 // remplacer tous les espaces, tabulations, saufs de ligne multiples par de simples espaces
134 $todos[] = array(
135 'name' => 'write',
136 'close' => false,
137 'param' => array('txt' => $this->prepareTxt($part[1])),
140 else
142 $part[1] = str_replace("\r", '', $part[1]);
143 $part[1] = explode("\n", $part[1]);
145 foreach($part[1] as $k => $txt)
147 $txt = str_replace("\t", ' ', $txt);
148 $txt = str_replace(' ', '&nbsp;', $txt);
149 if ($k>0) $todos[] = $pre_br;
151 $todos[] = array(
152 'name' => 'write',
153 'close' => false,
154 'param' => array('txt' => $this->prepareTxt($txt, false)),
161 // pour chaque action identifiée, il faut nettoyer le début et la fin des textes
162 // en fonction des balises qui l'entourent.
163 $balises_clean = array('page', 'page_header', 'page_footer', 'form',
164 'table', 'thead', 'tfoot', 'tr', 'td', 'th', 'br',
165 'div', 'hr', 'p', 'ul', 'ol', 'li',
166 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
167 'bookmark', 'fieldset', 'legend',
168 'draw', 'circle', 'ellipse', 'path', 'rect', 'line', 'g', 'polygon', 'polyline');
169 $nb = count($todos);
170 for($k=0; $k<$nb; $k++)
172 //si c'est un texte
173 if ($todos[$k]['name']=='write')
175 // et qu'une balise spécifique le précède => on nettoye les espaces du début du texte
176 if ($k>0 && in_array($todos[$k-1]['name'], $balises_clean))
177 $todos[$k]['param']['txt'] = ltrim($todos[$k]['param']['txt']);
179 // et qu'une balise spécifique le suit => on nettoye les espaces de la fin du texte
180 if ($k<$nb-1 && in_array($todos[$k+1]['name'], $balises_clean))
181 $todos[$k]['param']['txt'] = rtrim($todos[$k]['param']['txt']);
183 if (!strlen($todos[$k]['param']['txt']))
184 unset($todos[$k]);
187 if (count($parents)) HTML2PDF::makeError(5, __FILE__, __LINE__, $parents);
189 // liste des actions sauvée
190 $this->code = array_values($todos);
194 * preparer le texte une seule fois pour gagner du temps. vient de o_WRITE
196 * @param string texte
197 * @return string texte
199 function prepareTxt($txt, $spaces = true)
201 if ($spaces) $txt = preg_replace('/\s+/is', ' ', $txt);
202 $txt = str_replace('&euro;', '€', $txt);
203 $txt = html_entity_decode($txt, ENT_QUOTES, $this->encoding);
204 return $txt;
208 * parser le code HTML
210 * @param &array tableau de retour des données
211 * @return null
213 function searchCode(&$tmp)
215 // séparer les balises du texte
216 $tmp = array();
217 $reg = '/(<[^>]+>)|([^<]+)+/isU';
219 // pour chaque élément trouvé :
220 $str = '';
221 $offset = 0;
222 while(preg_match($reg, $this->html, $parse, PREG_OFFSET_CAPTURE, $offset))
224 // si une balise a été détectée
225 if ($parse[1][0])
227 // sauvegarde du texte précédent si il existe
228 if ($str!=='') $tmp[] = array('txt',$str);
230 // sauvegarde de la balise
231 $tmp[] = array('code',trim($parse[1][0]), $offset);
233 // initialisation du texte suivant
234 $str = '';
236 else
238 // ajout du texte à la fin de celui qui est déjà détecté
239 $str.= $parse[2][0];
241 // Update offset to the end of the match
242 $offset = $parse[0][1] + strlen($parse[0][0]);
243 unset($parse);
245 // si un texte est présent à la fin, on l'enregistre
246 if ($str!='') $tmp[] = array('txt',$str);
247 unset($str);
251 * analyse une balise HTML
253 * @param string code HTML à identifier
254 * @return array action correspondante
256 function analiseCode($code)
258 // nom de la balise et ouverture ou fermeture
259 $balise = '<([\/]{0,1})([_a-z0-9]+)([\/>\s]+)';
260 if (!preg_match('/'.$balise.'/isU', $code, $match))
261 return null;
263 $close = ($match[1]=='/' ? true : false);
264 $autoclose = preg_match('/\/>$/isU', $code);
265 $name = strtolower($match[2]);
267 // paramètres obligatoires en fonction du nom de la balise
268 $param = array();
269 $param['style'] = '';
270 if ($name=='img') { $param['alt'] = ''; $param['src'] = ''; }
271 if ($name=='a') { $param['href'] = ''; }
273 // lecture des paramétres du type nom=valeur
274 $prop = '([a-zA-Z0-9_]+)=([^"\'\s>]+)';
275 preg_match_all('/'.$prop.'/is', $code, $match);
276 for($k=0; $k<count($match[0]); $k++)
277 $param[trim(strtolower($match[1][$k]))] = trim($match[2][$k]);
279 // lecture des paramétres du type nom="valeur"
280 $prop = '([a-zA-Z0-9_]+)=["]([^"]*)["]';
281 preg_match_all('/'.$prop.'/is', $code, $match);
282 for($k=0; $k<count($match[0]); $k++)
283 $param[trim(strtolower($match[1][$k]))] = trim($match[2][$k]);
285 // lecture des paramétres du type nom='valeur'
286 $prop = "([a-zA-Z0-9_]+)=[']([^']*)[']";
287 preg_match_all('/'.$prop.'/is', $code, $match);
288 for($k=0; $k<count($match[0]); $k++)
289 $param[trim(strtolower($match[1][$k]))] = trim($match[2][$k]);
291 // mise en conformité en style de chaque paramètre
292 $color = "#000000";
293 $border = null;
294 foreach($param as $key => $val)
296 $key = strtolower($key);
297 switch($key)
299 case 'width':
300 unset($param[$key]);
301 $param['style'] .= 'width: '.$val.'px; ';
302 break;
304 case 'align':
305 if ($name==='img')
307 unset($param[$key]);
308 $param['style'] .= 'float: '.$val.'; ';
310 elseif ($name!=='table')
312 unset($param[$key]);
313 $param['style'] .= 'text-align: '.$val.'; ';
315 break;
317 case 'valign':
318 unset($param[$key]);
319 $param['style'] .= 'vertical-align: '.$val.'; ';
320 break;
322 case 'height':
323 unset($param[$key]);
324 $param['style'] .= 'height: '.$val.'px; ';
325 break;
327 case 'bgcolor':
328 unset($param[$key]);
329 $param['style'] .= 'background: '.$val.'; ';
330 break;
332 case 'bordercolor':
333 unset($param[$key]);
334 $color = $val;
335 break;
337 case 'border':
338 unset($param[$key]);
339 if (preg_match('/^[0-9]+$/isU', $val)) $val = $val.'px';
340 $border = $val;
341 break;
343 case 'cellpadding':
344 case 'cellspacing':
345 if (preg_match('/^([0-9]+)$/isU', $val)) $param[$key] = $val.'px';
346 break;
348 case 'colspan':
349 case 'rowspan':
350 $val = preg_replace('/[^0-9]/isU', '', $val);
351 if (!$val) $val = 1;
352 $param[$key] = $val;
353 break;
356 if ($border!==null)
358 if ($border) $border = 'border: solid '.$border.' '.$color;
359 else $border = 'border: none';
361 $param['style'] .= $border.'; ';
362 $param['border'] = $border;
365 // lecture des styles - décomposition
366 $styles = explode(';', $param['style']);
367 $param['style'] = array();
368 foreach($styles as $style)
370 $tmp = explode(':', $style);
371 if (count($tmp)>1)
373 $cod = $tmp[0]; unset($tmp[0]); $tmp = implode(':', $tmp);
374 $param['style'][trim(strtolower($cod))] = preg_replace('/[\s]+/isU', ' ', trim($tmp));
378 // détermination du niveau de table pour les ouverture, avec ajout d'un level
379 if (in_array($name, array('ul', 'ol', 'table')) && !$close)
381 $this->num++;
382 $this->level[count($this->level)] = $this->num;
385 // attribution du niveau de table où se trouve l'élément
386 if (!isset($param['num'])) $param['num'] = $this->level[count($this->level)-1];
388 // pour les fins de table : suppression d'un level
389 if (in_array($name, array('ul', 'ol', 'table')) && $close)
391 unset($this->level[count($this->level)-1]);
394 if (isset($param['value'])) $param['value'] = $this->prepareTxt($param['value']);
395 if (isset($param['alt'])) $param['alt'] = $this->prepareTxt($param['alt']);
396 if (isset($param['title'])) $param['title'] = $this->prepareTxt($param['title']);
397 if (isset($param['class'])) $param['class'] = $this->prepareTxt($param['class']);
399 // retour de l'action identifiée
400 return array('name' => $name, 'close' => $close ? 1 : 0, 'autoclose' => $autoclose, 'param' => $param);
403 // récupérer un niveau complet d'HTML entre une ouverture de balise et la fermeture correspondante
404 function getLevel($k)
406 // si le code n'existe pas : fin
407 if (!isset($this->code[$k])) return array();
409 // quelle balise faudra-t-il détecter
410 $detect = $this->code[$k]['name'];
412 $level = 0; // niveau de profondeur
413 $end = false; // etat de fin de recherche
414 $code = array(); // code extrait
416 // tant que c'est pas fini, on boucle
417 while (!$end)
419 // action courante
420 $row = $this->code[$k];
422 // si write => on ajoute le texte
423 if ($row['name']=='write')
425 $code[] = $row;
427 // sinon, c'est une balise html
428 else
430 $not = false; // indicateur de non prise en compte de la balise courante
432 // si c'est la balise que l'on cherche
433 if ($row['name']==$detect)
435 if ($level==0) { $not = true; } // si on est à la premiere balise : on l'ignore
436 $level+= ($row['close'] ? -1 : 1); // modification du niveau en cours en fonction de l'ouvertre / fermeture
437 if ($level==0) { $not = true; $end = true; } // si on est au niveau 0 : on a fini
440 // si on doit prendre en compte la balise courante
441 if (!$not)
443 if (isset($row['style']['text-align'])) unset($row['style']['text-align']);
444 $code[] = $row;
448 // on continue tant qu'il y a du code à analyser...
449 if (isset($this->code[$k+1]))
450 $k++;
451 else
452 $end = true;
455 // retourne le code extrait
456 return $code;
459 function getHtmlErrorCode($pos)
461 return substr($this->html, $pos-30, 70);