1<?php
2/**
3 * @package php-font-lib
4 * @link    https://github.com/PhenX/php-font-lib
5 * @author  Fabien Ménager <fabien.menager@gmail.com>
6 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
7 */
8
9namespace FontLib\Table\Type;
10use FontLib\Table\Table;
11
12/**
13 * `cmap` font table.
14 *
15 * @package php-font-lib
16 */
17class cmap extends Table {
18  private static $header_format = array(
19    "version"         => self::uint16,
20    "numberSubtables" => self::uint16,
21  );
22
23  private static $subtable_header_format = array(
24    "platformID"         => self::uint16,
25    "platformSpecificID" => self::uint16,
26    "offset"             => self::uint32,
27  );
28
29  private static $subtable_v4_format = array(
30    "length"        => self::uint16,
31    "language"      => self::uint16,
32    "segCountX2"    => self::uint16,
33    "searchRange"   => self::uint16,
34    "entrySelector" => self::uint16,
35    "rangeShift"    => self::uint16,
36  );
37
38  private static $subtable_v12_format = array(
39    "length"        => self::uint32,
40    "language"      => self::uint32,
41    "ngroups"    => self::uint32
42  );
43
44  protected function _parse() {
45    $font = $this->getFont();
46
47    $cmap_offset = $font->pos();
48
49    $data = $font->unpack(self::$header_format);
50
51    $subtables = array();
52    for ($i = 0; $i < $data["numberSubtables"]; $i++) {
53      $subtables[] = $font->unpack(self::$subtable_header_format);
54    }
55
56    $data["subtables"] = $subtables;
57
58    foreach ($data["subtables"] as $i => &$subtable) {
59      $font->seek($cmap_offset + $subtable["offset"]);
60
61      $subtable["format"] = $font->readUInt16();
62
63      // @todo Only CMAP version 4 and 12
64      if (($subtable["format"] != 4) && ($subtable["format"] != 12)) {
65        unset($data["subtables"][$i]);
66        $data["numberSubtables"]--;
67        continue;
68      }
69
70      if ($subtable["format"] == 12) {
71
72        $font->readUInt16();
73
74        $subtable += $font->unpack(self::$subtable_v12_format);
75
76        $glyphIndexArray = array();
77        $endCodes = array();
78        $startCodes = array();
79
80        for ($p = 0; $p < $subtable['ngroups']; $p++) {
81
82          $startCode = $startCodes[] = $font->readUInt32();
83          $endCode = $endCodes[] = $font->readUInt32();
84          $startGlyphCode = $font->readUInt32();
85
86          for ($c = $startCode; $c <= $endCode; $c++) {
87            $glyphIndexArray[$c] = $startGlyphCode;
88            $startGlyphCode++;
89          }
90        }
91
92        $subtable += array(
93          "startCode" => $startCodes,
94          "endCode" => $endCodes,
95          "glyphIndexArray" => $glyphIndexArray,
96        );
97
98      }
99      else if ($subtable["format"] == 4) {
100
101        $subtable += $font->unpack(self::$subtable_v4_format);
102
103        $segCount             = $subtable["segCountX2"] / 2;
104        $subtable["segCount"] = $segCount;
105
106        $endCode = $font->readUInt16Many($segCount);
107
108        $font->readUInt16(); // reservedPad
109
110        $startCode = $font->readUInt16Many($segCount);
111        $idDelta   = $font->readInt16Many($segCount);
112
113        $ro_start      = $font->pos();
114        $idRangeOffset = $font->readUInt16Many($segCount);
115
116        $glyphIndexArray = array();
117        for ($i = 0; $i < $segCount; $i++) {
118          $c1 = $startCode[$i];
119          $c2 = $endCode[$i];
120          $d  = $idDelta[$i];
121          $ro = $idRangeOffset[$i];
122
123          if ($ro > 0) {
124            $font->seek($subtable["offset"] + 2 * $i + $ro);
125          }
126
127          for ($c = $c1; $c <= $c2; $c++) {
128            if ($ro == 0) {
129              $gid = ($c + $d) & 0xFFFF;
130            }
131            else {
132              $offset = ($c - $c1) * 2 + $ro;
133              $offset = $ro_start + 2 * $i + $offset;
134
135              $font->seek($offset);
136              $gid = $font->readUInt16();
137
138              if ($gid != 0) {
139                $gid = ($gid + $d) & 0xFFFF;
140              }
141            }
142
143            if ($gid > 0) {
144              $glyphIndexArray[$c] = $gid;
145            }
146          }
147        }
148
149        $subtable += array(
150          "endCode"         => $endCode,
151          "startCode"       => $startCode,
152          "idDelta"         => $idDelta,
153          "idRangeOffset"   => $idRangeOffset,
154          "glyphIndexArray" => $glyphIndexArray,
155        );
156      }
157    }
158
159    $this->data = $data;
160  }
161
162  function _encode() {
163    $font = $this->getFont();
164
165    $subset          = $font->getSubset();
166    $glyphIndexArray = $font->getUnicodeCharMap();
167
168    $newGlyphIndexArray = array();
169    foreach ($glyphIndexArray as $code => $gid) {
170      $new_gid = array_search($gid, $subset);
171      if ($new_gid !== false) {
172        $newGlyphIndexArray[$code] = $new_gid;
173      }
174    }
175
176    ksort($newGlyphIndexArray); // Sort by char code
177
178    $segments = array();
179
180    $i        = -1;
181    $prevCode = 0xFFFF;
182    $prevGid  = 0xFFFF;
183
184    foreach ($newGlyphIndexArray as $code => $gid) {
185      if (
186        $prevCode + 1 != $code ||
187        $prevGid + 1 != $gid
188      ) {
189        $i++;
190        $segments[$i] = array();
191      }
192
193      $segments[$i][] = array($code, $gid);
194
195      $prevCode = $code;
196      $prevGid  = $gid;
197    }
198
199    $segments[][] = array(0xFFFF, 0xFFFF);
200
201    $startCode = array();
202    $endCode   = array();
203    $idDelta   = array();
204
205    foreach ($segments as $codes) {
206      $start = reset($codes);
207      $end   = end($codes);
208
209      $startCode[] = $start[0];
210      $endCode[]   = $end[0];
211      $idDelta[]   = $start[1] - $start[0];
212    }
213
214    $segCount      = count($startCode);
215    $idRangeOffset = array_fill(0, $segCount, 0);
216
217    $searchRange   = 1;
218    $entrySelector = 0;
219    while ($searchRange * 2 <= $segCount) {
220      $searchRange *= 2;
221      $entrySelector++;
222    }
223    $searchRange *= 2;
224    $rangeShift = $segCount * 2 - $searchRange;
225
226    $subtables = array(
227      array(
228        // header
229        "platformID"         => 3, // Unicode
230        "platformSpecificID" => 1,
231        "offset"             => null,
232
233        // subtable
234        "format"             => 4,
235        "length"             => null,
236        "language"           => 0,
237        "segCount"           => $segCount,
238        "segCountX2"         => $segCount * 2,
239        "searchRange"        => $searchRange,
240        "entrySelector"      => $entrySelector,
241        "rangeShift"         => $rangeShift,
242        "startCode"          => $startCode,
243        "endCode"            => $endCode,
244        "idDelta"            => $idDelta,
245        "idRangeOffset"      => $idRangeOffset,
246        "glyphIndexArray"    => $newGlyphIndexArray,
247      )
248    );
249
250    $data = array(
251      "version"         => 0,
252      "numberSubtables" => count($subtables),
253      "subtables"       => $subtables,
254    );
255
256    $length = $font->pack(self::$header_format, $data);
257
258    $subtable_headers_size   = $data["numberSubtables"] * 8; // size of self::$subtable_header_format
259    $subtable_headers_offset = $font->pos();
260
261    $length += $font->write(str_repeat("\0", $subtable_headers_size), $subtable_headers_size);
262
263    // write subtables data
264    foreach ($data["subtables"] as $i => $subtable) {
265      $length_before                   = $length;
266      $data["subtables"][$i]["offset"] = $length;
267
268      $length += $font->writeUInt16($subtable["format"]);
269
270      $before_subheader = $font->pos();
271      $length += $font->pack(self::$subtable_v4_format, $subtable);
272
273      $segCount = $subtable["segCount"];
274      $length += $font->w(array(self::uint16, $segCount), $subtable["endCode"]);
275      $length += $font->writeUInt16(0); // reservedPad
276      $length += $font->w(array(self::uint16, $segCount), $subtable["startCode"]);
277      $length += $font->w(array(self::int16, $segCount), $subtable["idDelta"]);
278      $length += $font->w(array(self::uint16, $segCount), $subtable["idRangeOffset"]);
279      $length += $font->w(array(self::uint16, $segCount), array_values($subtable["glyphIndexArray"]));
280
281      $after_subtable = $font->pos();
282
283      $subtable["length"] = $length - $length_before;
284      $font->seek($before_subheader);
285      $length += $font->pack(self::$subtable_v4_format, $subtable);
286
287      $font->seek($after_subtable);
288    }
289
290    // write subtables headers
291    $font->seek($subtable_headers_offset);
292    foreach ($data["subtables"] as $subtable) {
293      $font->pack(self::$subtable_header_format, $subtable);
294    }
295
296    return $length;
297  }
298}
299