2 # -*- coding: utf-8 -*-
4 # Copyright © 2014,2016 dyknon
6 # This file is part of Pylib-nicovideo.
8 # Pylib-nicovideo is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23 import distutils.command.build
28 class TtfError(Exception):
29 def __init__(self, val=""):
32 return repr(self.value)
35 def __init__(self, filehandle, offset=0):
40 def detect_type(self):
41 self.fh.seek(self.offset, os.SEEK_SET)
43 if mn == b"\x00\x01\x00\x00":
46 if self.fh.read(4) == b"\x00\x01\x00\x00":
50 def read_ttc_head(self):
51 self.fh.seek(self.offset, os.SEEK_SET)
52 if self.fh.read(8) != b"ttcf\x00\x01\x00\x00":
53 raise TtfError("TTCファイルヘッダ読み込みエラー")
55 numfn = struct.unpack(">L", self.fh.read(4))[0]
56 for i in range(numfn):
57 self.tttable.append(struct.unpack(">L", self.fh.read(4))[0])
60 def read_ttf_head(self):
61 self.fh.seek(self.offset, os.SEEK_SET)
62 if self.fh.read(4) != b"\x00\x01\x00\x00":
63 raise TtfError("TTFファイルヘッダ読み込みエラー")
64 (num_tables, search_range, entry_selector, range_shift) \
65 = struct.unpack(">HHHH", self.fh.read(8))
67 for i in range(num_tables):
68 tag = self.fh.read(4).decode("ascii")
69 (cksum, offset, length) = struct.unpack(">LLL", self.fh.read(12))
70 self.table_list[tag] = {"s": cksum, "o": offset, "l": length}
71 return self.table_list
73 def check_ttf_head(self):
76 except AttributeError:
79 def get_table_head(self):
80 if not "head" in self.tables:
81 self.read_table_head()
82 return self.tables["head"]
84 def read_table_head(self):
86 self.fh.seek(self.table_list["head"]["o"], os.SEEK_SET)
87 (version, font_revidion, check_sum_adjustment, magic_number,
88 flags, units_per_em, created, modified,
89 x_min, y_min, x_max, y_max, mac_style, lowest_rec_ppem,
90 font_direction_hint, index_to_loc_format, glyph_data_format) \
91 = struct.unpack(">llLLHHqqhhhhHHhhh", self.fh.read(54));
93 font_revidion /= 0x10000
94 self.tables["head"] = {
96 "fontRevidion": font_revidion,
97 "checkSumAdjustment": check_sum_adjustment,
98 "magicNumber": magic_number,
100 "unitsPerEm": units_per_em,
102 "modified": modified,
107 "macStyle": mac_style,
108 "lowestRecPPEM": lowest_rec_ppem,
109 "fontDirectionHint": font_direction_hint,
110 "indexToLocFormat": index_to_loc_format,
111 "glyphDataFormat": glyph_data_format
113 return self.tables["head"]
115 def get_table_loca(self):
116 if not "loca" in self.tables:
117 self.read_table_loca()
118 return self.tables["loca"]
120 def read_table_loca(self):
121 self.check_ttf_head()
122 if not "head" in self.tables:
123 self.read_table_head()
124 if not "maxp" in self.tables:
125 self.read_table_maxp()
126 self.fh.seek(self.table_list["loca"]["o"], os.SEEK_SET)
127 if self.tables["head"]["indexToLocFormat"] == 0:
128 table_format = struct.Struct(">H")
129 elif self.tables["head"]["indexToLocFormat"] == 1:
130 table_format = struct.Struct(">L")
132 raise TtfError("headテーブルのindexToLocFormat値が不正。")
133 self.tables["loca"] = []
134 for i in range(self.tables["maxp"]["numGlyphs"]+1):
135 offset = table_format.unpack(self.fh.read(table_format.size))
136 self.tables["loca"].append(offset)
137 return self.tables["loca"]
139 def get_glyph_from_index(self, index):
140 self.check_ttf_head()
141 self.fh.seek(self.table_list["glyf"]["o"]+index, os.SEEK_SET)
142 (number_of_contours, x_min, y_min, x_max, y_max) \
143 = struct.unpack(">hhhhh", self.fh.read(10))
145 "numberOfContours": number_of_contours,
152 def get_glyph_from_no(self, no):
153 if "glyf" in self.tables and no in self.tables["glyf"]:
154 return self.tables["glyf"][no]
155 if not "glyf" in self.tables:
156 self.tables["glyf"] = {}
157 if not "loca" in self.tables:
158 self.read_table_loca()
159 glyph = self.get_glyph_from_index(self.tables["loca"][no])
160 self.tables["glyf"][no] = glyph
163 def get_glyph_from_char(self, char):
164 if not "cmap" in self.tables:
165 self.read_table_cmap()
166 return self.get_glyph_from_no(self.tables["cmap"][char])
168 def get_table_glyf(self):
169 if not "glyf" in self.tables:
170 self.read_table_glyf()
171 return self.tables["glyf"]
173 def read_table_glyf(self):
174 if not "maxp" in self.tables:
175 self.read_table_maxp()
176 for i in range(self.tables["maxp"]["numGlyphs"]+1):
177 self.get_glyph_from_no(i)
178 return self.tables["glyf"]
180 def get_table_cmap(self):
181 if not "cmap" in self.tables:
182 self.read_table_cmap()
183 return self.tables["cmap"]
185 def read_table_cmap(self): #手抜きとかそういうのじゃない。無理。
186 self.check_ttf_head()
187 self.fh.seek(self.table_list["cmap"]["o"], os.SEEK_SET)
188 (version, num_tables) = struct.unpack(">HH", self.fh.read(4));
190 raise TtfError("cmapテーブルのバージョンがあたらしすぎまっす。")
191 encoding_records = []
194 for i in range(num_tables):
195 (platform_id, encoding_id, offset) \
196 = struct.unpack(">HHL", self.fh.read(8))
201 elif encoding_id == 1:
206 encoding_records.append({
207 "platformID": platform_id,
208 "encodingID": encoding_id,
212 raise TtfError("文字マップのエンコード方式に非対応のものしかありません")
214 if encoding_records[no]["platformID"] == 3:
215 if encoding_records[no]["encodingID"] == 0:
217 if encoding_records[no]["encodingID"] == 1:
218 encoding = "utf-16be"
219 if encoding_records[no]["encodingID"] == 2:
222 raise TtfError("文字マップのエンコード方式に非対応のものしかありません")
223 self.fh.seek(self.table_list["cmap"]["o"]
224 + encoding_records[no]["offset"], os.SEEK_SET)
225 maptype = struct.unpack(">H", self.fh.read(2))[0]
227 if maptype == 0: #要修正: デコードの仕方ダメ
228 (length, language) = struct.unpack(">HH", self.fh.read(4))
230 code = struct.pack(">L", i).decode(encoding, "replace")
231 mapdict[code] = struct.unpack(">B", self.fh.read(1))[0]
233 (length, language) = struct.unpack(">HH", self.fh.read(4))
236 sub_header_keys.append(struct.unpack(">H", self.fh.read(2))[0])
237 start_of_sub_header = self.fh.tell()
239 self.fh.seek(start_of_sub_header + sub_header_keys[i], os.SEEK_SET)
240 (first_code, entry_count, id_delta, id_range_offset) \
241 = struct.unpack(">HHsH", self.fh.read(8))
242 if first_code + entry_count >= 256:
244 self.fh.seek(id_range_offset, SEEK_CUR)
245 for j in range(entry_count):
246 glyph_index = struct.unpack(">H", self.fh.read(2))[0]
249 glyph_index += id_delta
251 code = j + first_code
253 code = (i << 8)|((j+first_code) & 0x0f)
254 code = struct.pack("B", code).decode(encoding, "replace")
255 mapdict[code] = glyph_index
257 (length, language, seg_count_x2, search_range, entry_selector,
258 range_shift) = struct.unpack(">HHHHHH", self.fh.read(12))
259 seg_count = int(seg_count_x2 / 2)
261 for i in range(seg_count):
262 end_count.append(struct.unpack(">H", self.fh.read(2))[0])
263 reserved_pad = struct.unpack(">H", self.fh.read(2))[0]
265 for i in range(seg_count):
266 start_count.append(struct.unpack(">H", self.fh.read(2))[0])
268 for i in range(seg_count):
269 id_delta.append(struct.unpack(">h", self.fh.read(2))[0])
271 for i in range(seg_count):
272 id_range_offset.append(struct.unpack(">H", self.fh.read(2))[0])
273 start_of_glyph_id_array = self.fh.tell()
274 for i in range(seg_count):
275 start = start_count[i]
278 range_offset = id_range_offset[i]
279 if start == 65535 or end == 65535:
281 for j in range(start, end+1):
282 if range_offset == 0:
283 code = struct.pack(">H", j)
284 code = code.decode(encoding, "replace")
285 mapdict[code] = (j + delta) % 65536
288 start_of_glyph_id_array
289 + int((range_offset/2+j-start+i-seg_count)*2),
291 glyph_index = struct.unpack(">H", self.fh.read(2))[0]
294 code = struct.pack(">H", j)
295 code = code.decode(encoding, "replace")
296 mapdict[code] = (glyph_index + delta) % 65536
298 raise TtfError("ID{}の文字マップには対応していません".format(maptype))
299 self.tables["cmap"] = mapdict
300 return self.tables["cmap"]
302 def get_table_maxp(self):
303 if not "maxp" in self.tables:
304 self.read_table_maxp()
305 return self.tables["maxp"]
307 def read_table_maxp(self):
308 self.check_ttf_head()
309 self.fh.seek(self.table_list["maxp"]["o"], os.SEEK_SET)
310 self.tables["maxp"] = {}
311 version = struct.unpack(">L", self.fh.read(4))[0]
312 if version == 0x00005000:
313 self.tables["maxp"]["numGlyphs"] \
314 = struct.unpack(">S", self.fh.read(2))[0]
315 elif version == 0x00010000: #手抜き(v1.0で追加されたのを無視する)
316 self.tables["maxp"]["numGlyphs"] \
317 = struct.unpack(">S", self.fh.read(2))[0]
319 raise TtfError("maxpテーブルのバージョンが非対応なやつです")
321 def get_table_hhea(self):
322 if not "hhea" in self.tables:
323 self.read_table_hhea()
324 return self.tables["hhea"]
326 def read_table_hhea(self):
327 self.check_ttf_head()
328 self.fh.seek(self.table_list["hhea"]["o"], os.SEEK_SET)
329 (vesion, ascender, descender, line_gap, advance_width_max,
330 min_left_side_bearing, min_right_side_bearing, x_max_extent,
331 caret_slope_rise, caret_slope_run, caret_offset,
332 zero0, zero1, zero2, zero3, metric_data_format, number_of_h_metrics) \
333 = struct.unpack(">LhhhHhhhhhhhhhhhH", self.fh.read(36));
334 self.tables["hhea"] = {
336 "ascender": ascender,
337 "descender": descender,
339 "advanceWidthMax": advance_width_max,
340 "minLeftSideBearing": min_left_side_bearing,
341 "minRightSideBearing": min_right_side_bearing,
342 "xMaxExtent": x_max_extent,
343 "caretSlopeRise": caret_slope_rise,
344 "caretSlopeRun": caret_slope_run,
345 "caretOffset": caret_offset,
346 "metricDataFormat": metric_data_format,
347 "numberOfHMetrics": number_of_h_metrics
349 return self.tables["hhea"]
351 def get_table_hmtx(self):
352 if not "hmtx" in self.tables:
353 self.read_table_hmtx()
354 return self.tables["hmtx"]
356 def read_table_hmtx(self): #手抜き
357 self.check_ttf_head()
358 if not "hhea" in self.tables:
359 self.read_table_hhea()
360 self.fh.seek(self.table_list["hmtx"]["o"], os.SEEK_SET)
362 for i in range(self.tables["hhea"]["numberOfHMetrics"]):
363 (advance_width, lsb) = struct.unpack(">Hh", self.fh.read(4))
364 long_hor_metric.append({"advanceWidth": advance_width, "lsb": lsb})
365 self.tables["hmtx"] = long_hor_metric
366 return long_hor_metric
368 def get_charwidth_from_no(self, no):
369 if not "hmtx" in self.tables:
370 self.read_table_hmtx()
371 if len(self.tables["hmtx"]) <= no:
373 return self.tables["hmtx"][no]["advanceWidth"]
375 def get_charwidth_from_char(self, char):
376 if not "cmap" in self.tables:
377 self.read_table_cmap()
378 return self.get_charwidth_from_no(self.tables["cmap"][char])
380 def get_table_name(self):
381 if not "name" in self.tables:
382 self.read_table_name()
383 return self.tables["name"]
385 def read_table_name(self): #だいぶ手抜き
386 self.check_ttf_head()
387 self.fh.seek(self.table_list["name"]["o"], os.SEEK_SET)
388 (format_num, count, string_offset) \
389 = struct.unpack(">HHH", self.fh.read(6))
391 for i in range(count):
392 (platform_id, encoding_id, language_id, name_id, length, offset) \
393 = struct.unpack(">HHHHHH", self.fh.read(12))
395 "platformID": platform_id,
396 "encodingID": encoding_id,
397 "languageID": language_id,
402 storagepos = self.table_list["name"]["o"] + string_offset
403 self.tables["name"] = []
404 for nr in name_record: #要修正: encID=0はAsciiだよ。雑過ぎ。
406 if nr["platformID"] == 3:
407 if nr["encodingID"] == 0:
408 encoding = "utf-16be"
409 elif nr["encodingID"] == 1:
410 encoding = "utf-16be"
411 elif nr["encodingID"] == 2:
415 self.fh.seek(storagepos + nr["offset"], os.SEEK_SET)
416 string = self.fh.read(nr["length"]).decode(encoding, "replace")
417 nr.update({"string": string})
418 self.tables["name"].append(nr)
419 return self.tables["name"]
421 def get_font_name(self):
422 if not "name" in self.tables:
423 self.read_table_name()
426 for name in self.tables["name"]:
428 if name["nameID"] == 4:
430 if name["languageID"] == 0x0411:
432 if name["encodingID"] == 1:
436 fontname = name["string"]
439 def has_name(self, rqname):
440 if not "name" in self.tables:
441 self.read_table_name()
442 for name in self.tables["name"]:
443 if name["nameID"] == 4:
444 if name["string"] == rqname:
448 # ** フォント優先順位テーブルについて **
449 #適切なフォントが検出できない時のためにいくつか自動検出の候補を用意しておく。
450 #自分の手元にあったフォントからテキトーに選んだだけ。
452 #半角文字用フォントの優先順位。HelveticaというやつはArialと幅が同じらしい。
453 HWalphanumeric_priority = [
454 "Arial", "Helvetica", "DejaVu Sans", "VL PGothic Regular"
456 #ゴシック体用フォントの優先順位。MS Pゴシック以外はテキトーに並べただけ。
458 "MS Pゴシック", "VL PGothic Regular"
461 #明朝体用のフォントの優先順位。SimSun以外はテキトーに並べただけ。最後はゴシック
463 "SimSun", "VL PGothic Regular"
465 mincho_replacer = "\u02c9\u02ca\u02cb\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588\u2589\u258a\u258b\u258c\u258d\u258e\u258f\u311a"
466 #丸文字体用のフォント優先順位。Gulim以外はただのゴシック
467 marumoji_priority = [
468 "Gulim", "MS Pゴシック", "VL PGothic Regular"
470 marumoji_replacer = "\u2660\u2661\u2663\u2664\u2665\u2667\u2668\u2669\u266c\uadf8"
474 fwfont_replacers = {}
476 def gen_fontwidth_py(outputfile):
477 #自動設定はWin以外は今のところ非対応。
480 if os.path.isdir("fonts"):
481 fontdirs.append("fonts")
483 if os.path.isdir("C:\\Windows\\Fonts\\"):
484 fontdirs.append("C:\\Windows\\Fonts\\")
485 for fontdir in fontdirs:
486 infontdir = os.listdir(fontdir)
487 for filename in infontdir:
488 filename = os.path.join(fontdir, filename)
489 if os.path.isfile(filename) and os.access(filename, os.R_OK):
490 fontfiles.append(filename)
492 for fontfile in fontfiles:
493 fh = open(fontfile, "br")
494 headreader = TtfReader(fh)
495 filetype = headreader.detect_type()
496 if filetype == "ttf":
497 fonts.append(headreader)
498 elif filetype == "ttc":
499 for offset in headreader.read_ttc_head():
500 fonts.append(TtfReader(fh, offset))
502 font_can_gothic = None
503 font_can_mincho = None
504 font_can_marumoji = None
505 for rqname in reversed(HWalphanumeric_priority):
507 if font.has_name(rqname):
509 for rqname in reversed(gothic_priority):
511 if font.has_name(rqname):
512 font_can_gothic = font
513 for rqname in reversed(mincho_priority):
515 if font.has_name(rqname):
516 font_can_mincho = font
517 for rqname in reversed(marumoji_priority):
519 if font.has_name(rqname):
520 font_can_marumoji = font
521 if not(font_can_HW and font_can_gothic and
522 font_can_mincho and font_can_marumoji):
523 print("適切なフォントをfonts/に入れてください。(README参照)")
524 raise TtfError("必要なフォントを自動検出できんかったよ")
525 hwfont_reader = font_can_HW
526 fwfont_readers = [font_can_gothic, font_can_mincho, font_can_marumoji]
528 font_can_gothic.get_font_name(): gothic_replacer,
529 font_can_mincho.get_font_name(): mincho_replacer,
530 font_can_marumoji.get_font_name(): marumoji_replacer
533 outputfile.write("#Generated by nicovideo-tools")
534 outputfile.write("\n")
535 outputfile.write("font_HWalphanumeric = \"{}\"".format(hwfont_reader.get_font_name()))
536 outputfile.write("\n")
538 for tr in fwfont_readers:
539 nametable.append("\"{}\"".format(tr.get_font_name()))
540 outputfile.write("font_priority = [{}]".format(",".join(nametable)))
541 outputfile.write("\n")
542 outputfile.write("font_change_chars = {")
543 outputfile.write("\n")
544 for key in fwfont_replacers.keys():
545 line = "\t\"{}\": \"".format(key)
546 for char in fwfont_replacers[key]:
547 line += "\\U{:0>8x}".format(ord(char))
549 outputfile.write(line)
550 outputfile.write("\n")
551 outputfile.write("\t\"\": \"\"")
552 outputfile.write("\n")
553 outputfile.write("}")
554 outputfile.write("\n")
555 outputfile.write("fontlist = {")
556 outputfile.write("\n")
557 for tr in fwfont_readers:
558 outputfile.write("\t\"" + tr.get_font_name() + "\": {")
559 outputfile.write("\n")
560 outputfile.write("\t\t\"h\": {},".format(tr.get_table_head()["unitsPerEm"]))
561 outputfile.write("\n")
562 outputfile.write("\t\t\"w\": {")
563 outputfile.write("\n")
564 charmap = tr.get_table_cmap()
565 for char in charmap.keys():
566 width = tr.get_charwidth_from_char(char)
568 outputfile.write("\t\t\t\"\\U{:0>8x}\": {},".format(ord(char), width))
569 outputfile.write("\n")
570 outputfile.write("\t\t\t\"\": 0")
571 outputfile.write("\n")
572 outputfile.write("\t\t}")
573 outputfile.write("\n")
574 outputfile.write("\t},")
575 outputfile.write("\n")
577 outputfile.write("\t\"" + hwfont_reader.get_font_name() + "\": {")
578 outputfile.write("\n")
579 outputfile.write("\t\t\"h\": {},".format(hwfont_reader.get_table_head()["unitsPerEm"]))
580 outputfile.write("\n")
581 outputfile.write("\t\t\"w\": {")
582 outputfile.write("\n")
583 charmap = hwfont_reader.get_table_cmap()
584 for char in charmap.keys():
585 width = hwfont_reader.get_charwidth_from_char(char)
587 outputfile.write("\t\t\t\"\\U{:0>8x}\": {},".format(ord(char), width))
588 outputfile.write("\n")
589 outputfile.write("\t\t\t\"\": 0")
590 outputfile.write("\n")
591 outputfile.write("\t\t}")
592 outputfile.write("\n")
593 outputfile.write("\t}")
594 outputfile.write("\n")
595 outputfile.write("}")
596 outputfile.write("\n")
598 class build_fontwidth_py(distutils.cmd.Command):
599 description = "\"build\" fontwidth.py"
601 user_options = [("build-lib=", "d", "directory to \"build\" to")]
604 def initialize_options(self):
605 self.build_lib = None
607 def finalize_options(self):
608 self.set_undefined_options("build", ('build_lib', 'build_lib'))
611 self.mkpath(os.path.join(self.build_lib, "nicovideo"))
612 outfile = os.path.join(self.build_lib, "nicovideo", "fontwidth.py")
613 outfilehandle = open(outfile, mode="w", encoding="utf-8")
614 outfilehandle.write("# -*- coding: utf-8 -*-\n")
615 gen_fontwidth_py(outfilehandle)
616 outfilehandle.close()
618 class my_build(distutils.command.build.build):
620 self.run_command("build_fontwidth_py")
621 distutils.command.build.build.run(self)
623 distutils.core.setup( name="pylib-nicovideo",
625 description="ニコニコにアクセスしたりできる。",
627 author_email="dyknon@users.sourceforge.jp",
628 url="http://sourceforge.jp/users/dyknon/",
629 packages=["nicovideo"],
632 "build_fontwidth_py": build_fontwidth_py