[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/se3master/var/www/se3/html2pdf/_tcpdf_5.0.002/fonts/utils/ -> makefont.php (source)

   1  <?php
   2  //============================================================+
   3  // File name   : makefont.php
   4  // Begin       : 2004-12-31
   5  // Last Update : 2010-03-19
   6  // Version     : 1.2.006
   7  // License     : GNU LGPL (http://www.gnu.org/copyleft/lesser.html)
   8  //     ----------------------------------------------------------------------------
   9  //     Copyright (C) 2008  Nicola Asuni - Tecnick.com S.r.l.
  10  //     
  11  //     This program is free software: you can redistribute it and/or modify
  12  //     it under the terms of the GNU Lesser General Public License as published by
  13  //     the Free Software Foundation, either version 2.1 of the License, or
  14  //     (at your option) any later version.
  15  //     
  16  //     This program is distributed in the hope that it will be useful,
  17  //     but WITHOUT ANY WARRANTY; without even the implied warranty of
  18  //     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  19  //     GNU Lesser General Public License for more details.
  20  //     
  21  //     You should have received a copy of the GNU Lesser General Public License
  22  //     along with this program.  If not, see <http://www.gnu.org/licenses/>.
  23  //     
  24  //     See LICENSE.TXT file for more information.
  25  //  ----------------------------------------------------------------------------
  26  //
  27  // Description : Utility to generate font definition files fot TCPDF
  28  //
  29  // Authors: Nicola Asuni, Olivier Plathey, Steven Wittens
  30  //
  31  // (c) Copyright:
  32  //               Nicola Asuni
  33  //               Tecnick.com S.r.l.
  34  //               Via della Pace, 11
  35  //               09044 Quartucciu (CA)
  36  //               ITALY
  37  //               www.tecnick.com
  38  //               info@tecnick.com
  39  //============================================================+
  40  
  41  /**
  42   * Utility to generate font definition files fot TCPDF.
  43   * @author Nicola Asuni, Olivier Plathey, Steven Wittens
  44   * @copyright 2004-2008 Nicola Asuni - Tecnick.com S.r.l (www.tecnick.com) Via Della Pace, 11 - 09044 - Quartucciu (CA) - ITALY - www.tecnick.com - info@tecnick.com
  45   * @package com.tecnick.tcpdf
  46   * @link http://www.tcpdf.org
  47   * @license http://www.gnu.org/copyleft/lesser.html LGPL
  48  */
  49  
  50  /**
  51   * 
  52   * @param string $fontfile path to font file (TTF, OTF or PFB).
  53   * @param string $fmfile font metrics file (UFM or AFM).
  54   * @param boolean $embedded Set to false to not embed the font, true otherwise (default).
  55   * @param string $enc Name of the encoding table to use. Omit this parameter for TrueType Unicode, OpenType Unicode and symbolic fonts like Symbol or ZapfDingBats.
  56   * @param array $patch Optional modification of the encoding
  57   */
  58  function MakeFont($fontfile, $fmfile, $embedded=true, $enc='cp1252', $patch=array()) {
  59      //Generate a font definition file
  60      set_magic_quotes_runtime(0);
  61      ini_set('auto_detect_line_endings', '1');
  62      if (!file_exists($fontfile)) {
  63          die('Error: file not found: '.$fontfile);
  64      }
  65      if (!file_exists($fmfile)) {
  66          die('Error: file not found: '.$fmfile);
  67      }
  68      $cidtogidmap = '';
  69      $map = array();
  70      $diff = '';
  71      $dw = 0; // default width
  72      $ffext = strtolower(substr($fontfile, -3));
  73      $fmext = strtolower(substr($fmfile, -3));
  74      if ($fmext == 'afm') {
  75          if (($ffext == 'ttf') OR ($ffext == 'otf')) {
  76              $type = 'TrueType';
  77          } elseif ($ffext == 'pfb') {
  78              $type = 'Type1';
  79          } else {
  80              die('Error: unrecognized font file extension: '.$ffext);
  81          }
  82          if ($enc) {
  83              $map = ReadMap($enc);
  84              foreach ($patch as $cc => $gn) {
  85                  $map[$cc] = $gn;
  86              }
  87          }
  88          $fm = ReadAFM($fmfile, $map);
  89          if (isset($widths['.notdef'])) {
  90              $dw = $widths['.notdef'];
  91          }
  92          if ($enc) {
  93              $diff = MakeFontEncoding($map);
  94          }
  95          $fd = MakeFontDescriptor($fm, empty($map));
  96      } elseif ($fmext == 'ufm') {
  97          $enc = '';
  98          if (($ffext == 'ttf') OR ($ffext == 'otf')) {
  99              $type = 'TrueTypeUnicode';
 100          } else {
 101              die('Error: not a TrueType font: '.$ffext);
 102          }
 103          $fm = ReadUFM($fmfile, $cidtogidmap);
 104          $dw = $fm['MissingWidth'];
 105          $fd = MakeFontDescriptor($fm, false);
 106      }
 107      //Start generation
 108      $s = '<?php'."\n";
 109      $s .= '$type=\''.$type."';\n";
 110      $s .= '$name=\''.$fm['FontName']."';\n";
 111      $s .= '$desc='.$fd.";\n";
 112      if (!isset($fm['UnderlinePosition'])) {
 113          $fm['UnderlinePosition'] = -100;
 114      }
 115      if (!isset($fm['UnderlineThickness'])) {
 116          $fm['UnderlineThickness'] = 50;
 117      }
 118      $s .= '$up='.$fm['UnderlinePosition'].";\n";
 119      $s .= '$ut='.$fm['UnderlineThickness'].";\n";
 120      if ($dw <= 0) {
 121          if (isset($fm['Widths'][32]) AND ($fm['Widths'][32] > 0)) {
 122              // assign default space width
 123              $dw = $fm['Widths'][32];
 124          } else {
 125              $dw = 600;
 126          }
 127      }
 128      $s .= '$dw='.$dw.";\n";
 129      $w = MakeWidthArray($fm);
 130      $s .= '$cw='.$w.";\n";
 131      $s .= '$enc=\''.$enc."';\n";
 132      $s .= '$diff=\''.$diff."';\n";
 133      $basename = substr(basename($fmfile), 0, -4);
 134      if ($embedded) {
 135          //Embedded font
 136          if (($type == 'TrueType') OR ($type == 'TrueTypeUnicode')) {
 137              CheckTTF($fontfile);
 138          }
 139          $f = fopen($fontfile,'rb');
 140          if (!$f) {
 141              die('Error: Unable to open '.$fontfile);
 142          }
 143          $file = fread($f, filesize($fontfile));
 144          fclose($f);
 145          if ($type == 'Type1') {
 146              //Find first two sections and discard third one
 147              $header = (ord($file{0}) == 128);
 148              if ($header) {
 149                  //Strip first binary header
 150                  $file = substr($file, 6);
 151              }
 152              $pos = strpos($file, 'eexec');
 153              if (!$pos) {
 154                  die('Error: font file does not seem to be valid Type1');
 155              }
 156              $size1 = $pos + 6;
 157              if ($header AND (ord($file{$size1}) == 128)) {
 158                  //Strip second binary header
 159                  $file = substr($file, 0, $size1).substr($file, $size1+6);
 160              }
 161              $pos = strpos($file, '00000000');
 162              if (!$pos) {
 163                  die('Error: font file does not seem to be valid Type1');
 164              }
 165              $size2 = $pos - $size1;
 166              $file = substr($file, 0, ($size1 + $size2));
 167          }
 168          $basename = strtolower($basename);
 169          if (function_exists('gzcompress')) {
 170              $cmp = $basename.'.z';
 171              SaveToFile($cmp, gzcompress($file, 9), 'b');
 172              $s .= '$file=\''.$cmp."';\n";
 173              print "Font file compressed (".$cmp.")\n";
 174              if (!empty($cidtogidmap)) {
 175                  $cmp = $basename.'.ctg.z';
 176                  SaveToFile($cmp, gzcompress($cidtogidmap, 9), 'b');
 177                  print "CIDToGIDMap created and compressed (".$cmp.")\n";
 178                  $s .= '$ctg=\''.$cmp."';\n";
 179              }
 180          } else {
 181              $s .= '$file=\''.basename($fontfile)."';\n";
 182              print "Notice: font file could not be compressed (zlib extension not available)\n";
 183              if (!empty($cidtogidmap)) {
 184                  $cmp = $basename.'.ctg';
 185                  $f = fopen($cmp, 'wb');
 186                  fwrite($f, $cidtogidmap);
 187                  fclose($f);
 188                  print "CIDToGIDMap created (".$cmp.")\n";
 189                  $s .= '$ctg=\''.$cmp."';\n";
 190              }
 191          }
 192          if($type == 'Type1') {
 193              $s .= '$size1='.$size1.";\n";
 194              $s .= '$size2='.$size2.";\n";
 195          } else {
 196              $s.='$originalsize='.filesize($fontfile).";\n";
 197          }
 198      } else {
 199          //Not embedded font
 200          $s .= '$file='."'';\n";
 201      }
 202      $s .= "?>";
 203      SaveToFile($basename.'.php',$s);
 204      print "Font definition file generated (".$basename.".php)\n";
 205  }
 206  
 207  /**
 208   * Read the specified encoding map.
 209   * @param string $enc map name (see /enc/ folder for valid names).
 210   */
 211  function ReadMap($enc) {
 212      //Read a map file
 213      $file = dirname(__FILE__).'/enc/'.strtolower($enc).'.map';
 214      $a = file($file);
 215      if (empty($a)) {
 216          die('Error: encoding not found: '.$enc);
 217      }
 218      $cc2gn = array();
 219      foreach ($a as $l) {
 220          if ($l{0} == '!') {
 221              $e = preg_split('/[ \\t]+/',rtrim($l));
 222              $cc = hexdec(substr($e[0],1));
 223              $gn = $e[2];
 224              $cc2gn[$cc] = $gn;
 225          }
 226      }
 227      for($i = 0; $i <= 255; $i++) {
 228          if(!isset($cc2gn[$i])) {
 229              $cc2gn[$i] = '.notdef';
 230          }
 231      }
 232      return $cc2gn;
 233  }
 234  
 235  /**
 236   * Read UFM file
 237   */
 238  function ReadUFM($file, &$cidtogidmap) {
 239      //Prepare empty CIDToGIDMap
 240      $cidtogidmap = str_pad('', (256 * 256 * 2), "\x00");
 241      //Read a font metric file
 242      $a = file($file);
 243      if (empty($a)) {
 244          die('File not found');
 245      }
 246      $widths = array();
 247      $fm = array();
 248      foreach($a as $l) {
 249          $e = explode(' ',chop($l));
 250          if(count($e) < 2) {
 251              continue;
 252          }
 253          $code = $e[0];
 254          $param = $e[1];
 255          if($code == 'U') {
 256              // U 827 ; WX 0 ; N squaresubnosp ; G 675 ;
 257              //Character metrics
 258              $cc = (int)$e[1];
 259              if ($cc != -1) {
 260              $gn = $e[7];
 261              $w = $e[4];
 262              $glyph = $e[10];
 263              $widths[$cc] = $w;
 264              if($cc == ord('X')) {
 265                  $fm['CapXHeight'] = $e[13];
 266              }
 267              // Set GID
 268              if (($cc >= 0) AND ($cc < 0xFFFF) AND $glyph) {
 269                  $cidtogidmap{($cc * 2)} = chr($glyph >> 8);
 270                  $cidtogidmap{(($cc * 2) + 1)} = chr($glyph & 0xFF);
 271              }
 272          }
 273          if((isset($gn) AND ($gn == '.notdef')) AND (!isset($fm['MissingWidth']))) {
 274              $fm['MissingWidth'] = $w;
 275          }
 276          } elseif($code == 'FontName') {
 277              $fm['FontName'] = $param;
 278          } elseif($code == 'Weight') {
 279              $fm['Weight'] = $param;
 280          } elseif($code == 'ItalicAngle') {
 281              $fm['ItalicAngle'] = (double)$param;
 282          } elseif($code == 'Ascender') {
 283              $fm['Ascender'] = (int)$param;
 284          } elseif($code == 'Descender') {
 285              $fm['Descender'] = (int)$param;
 286          } elseif($code == 'UnderlineThickness') {
 287              $fm['UnderlineThickness'] = (int)$param;
 288          } elseif($code == 'UnderlinePosition') {
 289              $fm['UnderlinePosition'] = (int)$param;
 290          } elseif($code == 'IsFixedPitch') {
 291              $fm['IsFixedPitch'] = ($param == 'true');
 292          } elseif($code == 'FontBBox') {
 293              $fm['FontBBox'] = array($e[1], $e[2], $e[3], $e[4]);
 294          } elseif($code == 'CapHeight') {
 295              $fm['CapHeight'] = (int)$param;
 296          } elseif($code == 'StdVW') {
 297              $fm['StdVW'] = (int)$param;
 298          }
 299      }
 300      if(!isset($fm['MissingWidth'])) {
 301          $fm['MissingWidth'] = 600;
 302      }
 303      if(!isset($fm['FontName'])) {
 304          die('FontName not found');
 305      }
 306      $fm['Widths'] = $widths;
 307      return $fm;
 308  }
 309  
 310  /**
 311   * Read AFM file
 312   */
 313  function ReadAFM($file,&$map) {
 314      //Read a font metric file
 315      $a = file($file);
 316      if(empty($a)) {
 317          die('File not found');
 318      }
 319      $widths = array();
 320      $fm = array();
 321      $fix = array(
 322          'Edot'=>'Edotaccent',
 323          'edot'=>'edotaccent',
 324          'Idot'=>'Idotaccent',
 325          'Zdot'=>'Zdotaccent',
 326          'zdot'=>'zdotaccent',
 327          'Odblacute' => 'Ohungarumlaut',
 328          'odblacute' => 'ohungarumlaut',
 329          'Udblacute'=>'Uhungarumlaut',
 330          'udblacute'=>'uhungarumlaut',
 331          'Gcedilla'=>'Gcommaaccent'
 332          ,'gcedilla'=>'gcommaaccent',
 333          'Kcedilla'=>'Kcommaaccent',
 334          'kcedilla'=>'kcommaaccent',
 335          'Lcedilla'=>'Lcommaaccent',
 336          'lcedilla'=>'lcommaaccent',
 337          'Ncedilla'=>'Ncommaaccent',
 338          'ncedilla'=>'ncommaaccent',
 339          'Rcedilla'=>'Rcommaaccent',
 340          'rcedilla'=>'rcommaaccent',
 341          'Scedilla'=>'Scommaaccent',
 342          'scedilla'=>'scommaaccent',
 343          'Tcedilla'=>'Tcommaaccent',
 344          'tcedilla'=>'tcommaaccent',
 345          'Dslash'=>'Dcroat',
 346          'dslash'=>'dcroat',
 347          'Dmacron'=>'Dcroat',
 348          'dmacron'=>'dcroat',
 349          'combininggraveaccent'=>'gravecomb',
 350          'combininghookabove'=>'hookabovecomb',
 351          'combiningtildeaccent'=>'tildecomb',
 352          'combiningacuteaccent'=>'acutecomb',
 353          'combiningdotbelow'=>'dotbelowcomb',
 354          'dongsign'=>'dong'
 355          );
 356      foreach($a as $l) {
 357          $e = explode(' ', rtrim($l));
 358          if (count($e) < 2) {
 359              continue;
 360          }
 361          $code = $e[0];
 362          $param = $e[1];
 363          if ($code == 'C') {
 364              //Character metrics
 365              $cc = (int)$e[1];
 366              $w = $e[4];
 367              $gn = $e[7];
 368              if (substr($gn, -4) == '20AC') {
 369                  $gn = 'Euro';
 370              }
 371              if (isset($fix[$gn])) {
 372                  //Fix incorrect glyph name
 373                  foreach ($map as $c => $n) {
 374                      if ($n == $fix[$gn]) {
 375                          $map[$c] = $gn;
 376                      }
 377                  }
 378              }
 379              if (empty($map)) {
 380                  //Symbolic font: use built-in encoding
 381                  $widths[$cc] = $w;
 382              } else {
 383                  $widths[$gn] = $w;
 384                  if($gn == 'X') {
 385                      $fm['CapXHeight'] = $e[13];
 386                  }
 387              }
 388              if($gn == '.notdef') {
 389                  $fm['MissingWidth'] = $w;
 390              }
 391          } elseif($code == 'FontName') {
 392              $fm['FontName'] = $param;
 393          } elseif($code == 'Weight') {
 394              $fm['Weight'] = $param;
 395          } elseif($code == 'ItalicAngle') {
 396              $fm['ItalicAngle'] = (double)$param;
 397          } elseif($code == 'Ascender') {
 398              $fm['Ascender'] = (int)$param;
 399          } elseif($code == 'Descender') {
 400              $fm['Descender'] = (int)$param;
 401          } elseif($code == 'UnderlineThickness') {
 402              $fm['UnderlineThickness'] = (int)$param;
 403          } elseif($code == 'UnderlinePosition') {
 404              $fm['UnderlinePosition'] = (int)$param;
 405          } elseif($code == 'IsFixedPitch') {
 406              $fm['IsFixedPitch'] = ($param == 'true');
 407          } elseif($code == 'FontBBox') {
 408              $fm['FontBBox'] = array($e[1], $e[2], $e[3], $e[4]);
 409          } elseif($code == 'CapHeight') {
 410              $fm['CapHeight'] = (int)$param;
 411          } elseif($code == 'StdVW') {
 412              $fm['StdVW'] = (int)$param;
 413          }
 414      }
 415      if (!isset($fm['FontName'])) {
 416          die('FontName not found');
 417      }
 418      if (!empty($map)) {
 419          if (!isset($widths['.notdef'])) {
 420              $widths['.notdef'] = 600;
 421          }
 422          if (!isset($widths['Delta']) AND isset($widths['increment'])) {
 423              $widths['Delta'] = $widths['increment'];
 424          }
 425          //Order widths according to map
 426          for ($i = 0; $i <= 255; $i++) {
 427              if (!isset($widths[$map[$i]])) {
 428                  print "Warning: character ".$map[$i]." is missing\n";
 429                  $widths[$i] = $widths['.notdef'];
 430              } else {
 431                  $widths[$i] = $widths[$map[$i]];
 432              }
 433          }
 434      }
 435      $fm['Widths'] = $widths;
 436      return $fm;
 437  }
 438  
 439  function MakeFontDescriptor($fm, $symbolic=false) {
 440      //Ascent
 441      $asc = (isset($fm['Ascender']) ? $fm['Ascender'] : 1000);
 442      $fd = "array('Ascent'=>".$asc;
 443      //Descent
 444      $desc = (isset($fm['Descender']) ? $fm['Descender'] : -200);
 445      $fd .= ",'Descent'=>".$desc;
 446      //CapHeight
 447      if (isset($fm['CapHeight'])) {
 448          $ch = $fm['CapHeight'];
 449      } elseif (isset($fm['CapXHeight'])) {
 450          $ch = $fm['CapXHeight'];
 451      } else {
 452          $ch = $asc;
 453      }
 454      $fd .= ",'CapHeight'=>".$ch;
 455      //Flags
 456      $flags = 0;
 457      if (isset($fm['IsFixedPitch']) AND $fm['IsFixedPitch']) {
 458          $flags += 1<<0;
 459      }
 460      if ($symbolic) {
 461          $flags += 1<<2;
 462      } else {
 463          $flags += 1<<5;
 464      }
 465      if (isset($fm['ItalicAngle']) AND ($fm['ItalicAngle'] != 0)) {
 466          $flags += 1<<6;
 467      }
 468      $fd .= ",'Flags'=>".$flags;
 469      //FontBBox
 470      if (isset($fm['FontBBox'])) {
 471          $fbb = $fm['FontBBox'];
 472      } else {
 473          $fbb = array(0, ($desc - 100), 1000, ($asc + 100));
 474      }
 475      $fd .= ",'FontBBox'=>'[".$fbb[0].' '.$fbb[1].' '.$fbb[2].' '.$fbb[3]."]'";
 476      //ItalicAngle
 477      $ia = (isset($fm['ItalicAngle']) ? $fm['ItalicAngle'] : 0);
 478      $fd .= ",'ItalicAngle'=>".$ia;
 479      //StemV
 480      if (isset($fm['StdVW'])) {
 481          $stemv = $fm['StdVW'];
 482      } elseif (isset($fm['Weight']) AND preg_match('/(bold|black)/i', $fm['Weight'])) {
 483          $stemv = 120;
 484      } else {
 485          $stemv = 70;
 486      }
 487      $fd .= ",'StemV'=>".$stemv;
 488      //MissingWidth
 489      if(isset($fm['MissingWidth'])) {
 490          $fd .= ",'MissingWidth'=>".$fm['MissingWidth'];
 491      }
 492      $fd .= ')';
 493      return $fd;
 494  }
 495  
 496  function MakeWidthArray($fm) {
 497      //Make character width array
 498      $s = 'array(';
 499      $cw = $fm['Widths'];
 500      $els = array();
 501      $c = 0;
 502      foreach ($cw as $i => $w) {
 503          if (is_numeric($i)) {
 504              $els[] = (((($c++)%10) == 0) ? "\n" : '').$i.'=>'.$w;
 505          }
 506      }
 507      $s .= implode(',', $els);
 508      $s .= ')';
 509      return $s;
 510  }
 511  
 512  function MakeFontEncoding($map) {
 513      //Build differences from reference encoding
 514      $ref = ReadMap('cp1252');
 515      $s = '';
 516      $last = 0;
 517      for ($i = 32; $i <= 255; $i++) {
 518          if ($map[$i] != $ref[$i]) {
 519              if ($i != $last+1) {
 520                  $s .= $i.' ';
 521              }
 522              $last = $i;
 523              $s .= '/'.$map[$i].' ';
 524          }
 525      }
 526      return rtrim($s);
 527  }
 528  
 529  function SaveToFile($file, $s, $mode='t') {
 530      $f = fopen($file, 'w'.$mode);
 531      if(!$f) {
 532          die('Can\'t write to file '.$file);
 533      }
 534      fwrite($f, $s, strlen($s));
 535      fclose($f);
 536  }
 537  
 538  function ReadShort($f) {
 539      $a = unpack('n1n', fread($f, 2));
 540      return $a['n'];
 541  }
 542  
 543  function ReadLong($f) {
 544      $a = unpack('N1N', fread($f, 4));
 545      return $a['N'];
 546  }
 547  
 548  function CheckTTF($file) {
 549      //Check if font license allows embedding
 550      $f = fopen($file, 'rb');
 551      if (!$f) {
 552          die('Error: unable to open '.$file);
 553      }
 554      //Extract number of tables
 555      fseek($f, 4, SEEK_CUR);
 556      $nb = ReadShort($f);
 557      fseek($f, 6, SEEK_CUR);
 558      //Seek OS/2 table
 559      $found = false;
 560      for ($i = 0; $i < $nb; $i++) {
 561          if (fread($f, 4) == 'OS/2') {
 562              $found = true;
 563              break;
 564          }
 565          fseek($f, 12, SEEK_CUR);
 566      }
 567      if (!$found) {
 568          fclose($f);
 569          return;
 570      }
 571      fseek($f, 4, SEEK_CUR);
 572      $offset = ReadLong($f);
 573      fseek($f, $offset, SEEK_SET);
 574      //Extract fsType flags
 575      fseek($f, 8, SEEK_CUR);
 576      $fsType = ReadShort($f);
 577      $rl = ($fsType & 0x02) != 0;
 578      $pp = ($fsType & 0x04) != 0;
 579      $e = ($fsType & 0x08) != 0;
 580      fclose($f);
 581      if($rl AND (!$pp) AND (!$e)) {
 582          print "Warning: font license does not allow embedding\n";
 583      }
 584  }
 585  
 586  $arg = $GLOBALS['argv'];
 587  if (count($arg) >= 3) {
 588      ob_start();
 589      array_shift($arg);
 590      if (sizeof($arg) == 3) {
 591          $arg[3] = $arg[2];
 592          $arg[2] = true;
 593      } else {
 594          if (!isset($arg[2])) {
 595              $arg[2] = true;
 596          }
 597          if (!isset($arg[3])) {
 598              $arg[3] = 'cp1252';
 599          }
 600      }
 601      if (!isset($arg[4])) {
 602          $arg[4] = array();
 603      }
 604      MakeFont($arg[0], $arg[1], $arg[2], $arg[3], $arg[4]);
 605      $t = ob_get_clean();
 606      print preg_replace('!<BR( /)?>!i', "\n", $t);
 607  } else {
 608      print "Usage: makefont.php <ttf/otf/pfb file> <afm/ufm file> <encoding> <patch>\n";
 609  }
 610  ?>


Generated: Tue Mar 17 22:47:18 2015 Cross-referenced by PHPXref 0.7.1