2 # -*- coding: utf-8 -*-
4 # Copyright © 2014 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
29 def __init__(self, filehandle, offset=0):
34 def detect_type(self):
35 self.fh.seek(self.offset, os.SEEK_SET)
37 if mn == b"\x00\x01\x00\x00":
40 if self.fh.read(4) == b"\x00\x01\x00\x00":
44 def read_ttc_head(self):
45 self.fh.seek(self.offset, os.SEEK_SET)
46 if self.fh.read(8) != b"ttcf\x00\x01\x00\x00":
47 raise "TTCファイルヘッダ読み込みエラー"
49 numfn = struct.unpack(">L", self.fh.read(4))[0]
50 for i in range(numfn):
51 self.tttable.append(struct.unpack(">L", self.fh.read(4))[0])
54 def read_ttf_head(self):
55 self.fh.seek(self.offset, os.SEEK_SET)
56 if self.fh.read(4) != b"\x00\x01\x00\x00":
57 raise "TTFファイルヘッダ読み込みエラー"
58 (num_tables, search_range, entry_selector, range_shift) \
59 = struct.unpack(">HHHH", self.fh.read(8))
61 for i in range(num_tables):
62 tag = self.fh.read(4).decode("ascii")
63 (cksum, offset, length) = struct.unpack(">LLL", self.fh.read(12))
64 self.table_list[tag] = {"s": cksum, "o": offset, "l": length}
65 return self.table_list
67 def check_ttf_head(self):
70 except AttributeError:
73 def get_table_head(self):
74 if not "head" in self.tables:
75 self.read_table_head()
76 return self.tables["head"]
78 def read_table_head(self):
80 self.fh.seek(self.table_list["head"]["o"], os.SEEK_SET)
81 (version, font_revidion, check_sum_adjustment, magic_number,
82 flags, units_per_em, created, modified,
83 x_min, y_min, x_max, y_max, mac_style, lowest_rec_ppem,
84 font_direction_hint, index_to_loc_format, glyph_data_format) \
85 = struct.unpack(">llLLHHqqhhhhHHhhh", self.fh.read(54));
87 font_revidion /= 0x10000
88 self.tables["head"] = {
90 "fontRevidion": font_revidion,
91 "checkSumAdjustment": check_sum_adjustment,
92 "magicNumber": magic_number,
94 "unitsPerEm": units_per_em,
101 "macStyle": mac_style,
102 "lowestRecPPEM": lowest_rec_ppem,
103 "fontDirectionHint": font_direction_hint,
104 "indexToLocFormat": index_to_loc_format,
105 "glyphDataFormat": glyph_data_format
107 return self.tables["head"]
109 def get_table_loca(self):
110 if not "loca" in self.tables:
111 self.read_table_loca()
112 return self.tables["loca"]
114 def read_table_loca(self):
115 self.check_ttf_head()
116 if not "head" in self.tables:
117 self.read_table_head()
118 if not "maxp" in self.tables:
119 self.read_table_maxp()
120 self.fh.seek(self.table_list["loca"]["o"], os.SEEK_SET)
121 if self.tables["head"]["indexToLocFormat"] == 0:
122 table_format = struct.Struct(">H")
123 elif self.tables["head"]["indexToLocFormat"] == 1:
124 table_format = struct.Struct(">L")
126 raise "headテーブルのindexToLocFormat値が不正。"
127 self.tables["loca"] = []
128 for i in range(self.tables["maxp"]["numGlyphs"]+1):
129 offset = table_format.unpack(self.fh.read(table_format.size))
130 self.tables["loca"].append(offset)
131 return self.tables["loca"]
133 def get_glyph_from_index(self, index):
134 self.check_ttf_head()
135 self.fh.seek(self.table_list["glyf"]["o"]+index, os.SEEK_SET)
136 (number_of_contours, x_min, y_min, x_max, y_max) \
137 = struct.unpack(">hhhhh", self.fh.read(10))
139 "numberOfContours": number_of_contours,
146 def get_glyph_from_no(self, no):
147 if "glyf" in self.tables and no in self.tables["glyf"]:
148 return self.tables["glyf"][no]
149 if not "glyf" in self.tables:
150 self.tables["glyf"] = {}
151 if not "loca" in self.tables:
152 self.read_table_loca()
153 glyph = self.get_glyph_from_index(self.tables["loca"][no])
154 self.tables["glyf"][no] = glyph
157 def get_glyph_from_char(self, char):
158 if not "cmap" in self.tables:
159 self.read_table_cmap()
160 return self.get_glyph_from_no(self.tables["cmap"][char])
162 def get_table_glyf(self):
163 if not "glyf" in self.tables:
164 self.read_table_glyf()
165 return self.tables["glyf"]
167 def read_table_glyf(self):
168 if not "maxp" in self.tables:
169 self.read_table_maxp()
170 for i in range(self.tables["maxp"]["numGlyphs"]+1):
171 self.get_glyph_from_no(i)
172 return self.tables["glyf"]
174 def get_table_cmap(self):
175 if not "cmap" in self.tables:
176 self.read_table_cmap()
177 return self.tables["cmap"]
179 def read_table_cmap(self): #手抜きとかそういうのじゃない。無理。
180 self.check_ttf_head()
181 self.fh.seek(self.table_list["cmap"]["o"], os.SEEK_SET)
182 (version, num_tables) = struct.unpack(">HH", self.fh.read(4));
184 raise "cmapテーブルのバージョンがあたらしすぎまっす。"
185 encoding_records = []
188 for i in range(num_tables):
189 (platform_id, encoding_id, offset) \
190 = struct.unpack(">HHL", self.fh.read(8))
195 elif encoding_id == 1:
200 encoding_records.append({
201 "platformID": platform_id,
202 "encodingID": encoding_id,
206 raise "文字マップのエンコード方式に非対応のものしかありません"
208 if encoding_records[no]["platformID"] == 3:
209 if encoding_records[no]["encodingID"] == 0:
211 if encoding_records[no]["encodingID"] == 1:
212 encoding = "utf-16be"
213 if encoding_records[no]["encodingID"] == 2:
216 raise "文字マップのエンコード方式に非対応のものしかありません"
217 self.fh.seek(self.table_list["cmap"]["o"]
218 + encoding_records[no]["offset"], os.SEEK_SET)
219 maptype = struct.unpack(">H", self.fh.read(2))[0]
221 if maptype == 0: #要修正: デコードの仕方ダメ
222 (length, language) = struct.unpack(">HH", self.fh.read(4))
224 code = struct.pack(">L", i).decode(encoding, "replace")
225 mapdict[code] = struct.unpack(">B", self.fh.read(1))[0]
227 (length, language) = struct.unpack(">HH", self.fh.read(4))
230 sub_header_keys.append(struct.unpack(">H", self.fh.read(2))[0])
231 start_of_sub_header = self.fh.tell()
233 self.fh.seek(start_of_sub_header + sub_header_keys[i], os.SEEK_SET)
234 (first_code, entry_count, id_delta, id_range_offset) \
235 = struct.unpack(">HHsH", self.fh.read(8))
236 if first_code + entry_count >= 256:
238 self.fh.seek(id_range_offset, SEEK_CUR)
239 for j in range(entry_count):
240 glyph_index = struct.unpack(">H", self.fh.read(2))[0]
243 glyph_index += id_delta
245 code = j + first_code
247 code = (i << 8)|((j+first_code) & 0x0f)
248 code = struct.pack("B", code).decode(encoding, "replace")
249 mapdict[code] = glyph_index
251 (length, language, seg_count_x2, search_range, entry_selector,
252 range_shift) = struct.unpack(">HHHHHH", self.fh.read(12))
253 seg_count = int(seg_count_x2 / 2)
255 for i in range(seg_count):
256 end_count.append(struct.unpack(">H", self.fh.read(2))[0])
257 reserved_pad = struct.unpack(">H", self.fh.read(2))[0]
259 for i in range(seg_count):
260 start_count.append(struct.unpack(">H", self.fh.read(2))[0])
262 for i in range(seg_count):
263 id_delta.append(struct.unpack(">h", self.fh.read(2))[0])
265 for i in range(seg_count):
266 id_range_offset.append(struct.unpack(">H", self.fh.read(2))[0])
267 start_of_glyph_id_array = self.fh.tell()
268 for i in range(seg_count):
269 start = start_count[i]
272 range_offset = id_range_offset[i]
273 if start == 65535 or end == 65535:
275 for j in range(start, end+1):
276 if range_offset == 0:
277 code = struct.pack(">H", j)
278 code = code.decode(encoding, "replace")
279 mapdict[code] = (j + delta) % 65536
282 start_of_glyph_id_array
283 + int((range_offset/2+j-start+i-seg_count)*2),
285 glyph_index = struct.unpack(">H", self.fh.read(2))[0]
288 code = struct.pack(">H", j)
289 code = code.decode(encoding, "replace")
290 mapdict[code] = (glyph_index + delta) % 65536
292 raise "ID{}の文字マップには対応していません".format(maptype)
293 self.tables["cmap"] = mapdict
294 return self.tables["cmap"]
296 def get_table_maxp(self):
297 if not "maxp" in self.tables:
298 self.read_table_maxp()
299 return self.tables["maxp"]
301 def read_table_maxp(self):
302 self.check_ttf_head()
303 self.fh.seek(self.table_list["maxp"]["o"], os.SEEK_SET)
304 self.tables["maxp"] = {}
305 version = struct.unpack(">L", self.fh.read(4))[0]
306 if version == 0x00005000:
307 self.tables["maxp"]["numGlyphs"] \
308 = struct.unpack(">S", self.fh.read(2))[0]
309 elif version == 0x00010000: #手抜き(v1.0で追加されたのを無視する)
310 self.tables["maxp"]["numGlyphs"] \
311 = struct.unpack(">S", self.fh.read(2))[0]
313 raise "maxpテーブルのバージョンが非対応なやつです"
315 def get_table_hhea(self):
316 if not "hhea" in self.tables:
317 self.read_table_hhea()
318 return self.tables["hhea"]
320 def read_table_hhea(self):
321 self.check_ttf_head()
322 self.fh.seek(self.table_list["hhea"]["o"], os.SEEK_SET)
323 (vesion, ascender, descender, line_gap, advance_width_max,
324 min_left_side_bearing, min_right_side_bearing, x_max_extent,
325 caret_slope_rise, caret_slope_run, caret_offset,
326 zero0, zero1, zero2, zero3, metric_data_format, number_of_h_metrics) \
327 = struct.unpack(">LhhhHhhhhhhhhhhhH", self.fh.read(36));
328 self.tables["hhea"] = {
330 "ascender": ascender,
331 "descender": descender,
333 "advanceWidthMax": advance_width_max,
334 "minLeftSideBearing": min_left_side_bearing,
335 "minRightSideBearing": min_right_side_bearing,
336 "xMaxExtent": x_max_extent,
337 "caretSlopeRise": caret_slope_rise,
338 "caretSlopeRun": caret_slope_run,
339 "caretOffset": caret_offset,
340 "metricDataFormat": metric_data_format,
341 "numberOfHMetrics": number_of_h_metrics
343 return self.tables["hhea"]
345 def get_table_hmtx(self):
346 if not "hmtx" in self.tables:
347 self.read_table_hmtx()
348 return self.tables["hmtx"]
350 def read_table_hmtx(self): #手抜き
351 self.check_ttf_head()
352 if not "hhea" in self.tables:
353 self.read_table_hhea()
354 self.fh.seek(self.table_list["hmtx"]["o"], os.SEEK_SET)
356 for i in range(self.tables["hhea"]["numberOfHMetrics"]):
357 (advance_width, lsb) = struct.unpack(">Hh", self.fh.read(4))
358 long_hor_metric.append({"advanceWidth": advance_width, "lsb": lsb})
359 self.tables["hmtx"] = long_hor_metric
360 return long_hor_metric
362 def get_charwidth_from_no(self, no):
363 if not "hmtx" in self.tables:
364 self.read_table_hmtx()
365 if len(self.tables["hmtx"]) <= no:
367 return self.tables["hmtx"][no]["advanceWidth"]
369 def get_charwidth_from_char(self, char):
370 if not "cmap" in self.tables:
371 self.read_table_cmap()
372 return self.get_charwidth_from_no(self.tables["cmap"][char])
374 def get_table_name(self):
375 if not "name" in self.tables:
376 self.read_table_name()
377 return self.tables["name"]
379 def read_table_name(self): #だいぶ手抜き
380 self.check_ttf_head()
381 self.fh.seek(self.table_list["name"]["o"], os.SEEK_SET)
382 (format_num, count, string_offset) \
383 = struct.unpack(">HHH", self.fh.read(6))
385 for i in range(count):
386 (platform_id, encoding_id, language_id, name_id, length, offset) \
387 = struct.unpack(">HHHHHH", self.fh.read(12))
389 "platformID": platform_id,
390 "encodingID": encoding_id,
391 "languageID": language_id,
396 storagepos = self.table_list["name"]["o"] + string_offset
397 self.tables["name"] = []
398 for nr in name_record: #要修正: encID=0はAsciiだよ。雑過ぎ。
400 if nr["platformID"] == 3:
401 if nr["encodingID"] == 0:
402 encoding = "utf-16be"
403 elif nr["encodingID"] == 1:
404 encoding = "utf-16be"
405 elif nr["encodingID"] == 2:
409 self.fh.seek(storagepos + nr["offset"], os.SEEK_SET)
410 string = self.fh.read(nr["length"]).decode(encoding, "replace")
411 nr.update({"string": string})
412 self.tables["name"].append(nr)
413 return self.tables["name"]
415 def get_font_name(self):
416 if not "name" in self.tables:
417 self.read_table_name()
420 for name in self.tables["name"]:
422 if name["nameID"] == 4:
424 if name["languageID"] == 0x0411:
426 if name["encodingID"] == 1:
430 fontname = name["string"]
433 def has_name(self, rqname):
434 if not "name" in self.tables:
435 self.read_table_name()
436 for name in self.tables["name"]:
437 if name["nameID"] == 4:
438 if name["string"] == rqname:
442 # ** フォント優先順位テーブルについて **
443 #適切なフォントが検出できない時のためにいくつか自動検出の候補を用意しておく。
444 #自分の手元にあったフォントからテキトーに選んだだけ。
446 #半角文字用フォントの優先順位。HelveticaというやつはArialと幅が同じらしい。
447 HWalphanumeric_priority = [
448 "Arial", "Helvetica", "DejaVu Sans", "VL PGothic Regular"
450 #ゴシック体用フォントの優先順位。MS Pゴシック以外はテキトーに並べただけ。
452 "MS Pゴシック", "VL PGothic Regular"
455 #明朝体用のフォントの優先順位。SimSun以外はテキトーに並べただけ。最後はゴシック
457 "SimSun", "VL PGothic Regular"
459 mincho_replacer = "\u02c9\u02ca\u02cb\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588\u2589\u258a\u258b\u258c\u258d\u258e\u258f\u311a"
460 #丸文字体用のフォント優先順位。Gulim以外はただのゴシック
461 marumoji_priority = [
462 "Gulim", "MS Pゴシック", "VL PGothic Regular"
464 marumoji_replacer = "\u2660\u2661\u2663\u2664\u2665\u2667\u2668\u2669\u266c\uadf8"
468 fwfont_replacers = {}
470 def gen_fontwidth_py(outputfile):
471 #自動設定はWin以外は今のところ非対応。
474 if os.path.isdir("fonts"):
475 fontdirs.append("fonts")
477 if os.path.isdir("C:\\Windows\\Fonts\\"):
478 fontdirs.append("C:\\Windows\\Fonts\\")
479 for fontdir in fontdirs:
480 infontdir = os.listdir(fontdir)
481 for filename in infontdir:
482 filename = os.path.join(fontdir, filename)
483 if os.path.isfile(filename) and os.access(filename, os.R_OK):
484 fontfiles.append(filename)
486 for fontfile in fontfiles:
487 fh = open(fontfile, "br")
488 headreader = TtfReader(fh)
489 filetype = headreader.detect_type()
490 if filetype == "ttf":
491 fonts.append(headreader)
492 elif filetype == "ttc":
493 for offset in headreader.read_ttc_head():
494 fonts.append(TtfReader(fh, offset))
496 font_can_gothic = None
497 font_can_mincho = None
498 font_can_marumoji = None
499 for rqname in reversed(HWalphanumeric_priority):
501 if font.has_name(rqname):
503 for rqname in reversed(gothic_priority):
505 if font.has_name(rqname):
506 font_can_gothic = font
507 for rqname in reversed(mincho_priority):
509 if font.has_name(rqname):
510 font_can_mincho = font
511 for rqname in reversed(marumoji_priority):
513 if font.has_name(rqname):
514 font_can_marumoji = font
515 if not(font_can_HW and font_can_gothic and
516 font_can_mincho and font_can_marumoji):
517 print("適切なフォントをfonts/に入れてください。(README参照)")
518 raise "必要なフォントを自動検出できんかったよ"
519 hwfont_reader = font_can_HW
520 fwfont_readers = [font_can_gothic, font_can_mincho, font_can_marumoji]
522 font_can_gothic.get_font_name(): gothic_replacer,
523 font_can_mincho.get_font_name(): mincho_replacer,
524 font_can_marumoji.get_font_name(): marumoji_replacer
527 outputfile.write("#Generated by nicovideo-tools")
528 outputfile.write("\n")
529 outputfile.write("font_HWalphanumeric = \"{}\"".format(hwfont_reader.get_font_name()))
530 outputfile.write("\n")
532 for tr in fwfont_readers:
533 nametable.append("\"{}\"".format(tr.get_font_name()))
534 outputfile.write("font_priority = [{}]".format(",".join(nametable)))
535 outputfile.write("\n")
536 outputfile.write("font_change_chars = {")
537 outputfile.write("\n")
538 for key in fwfont_replacers.keys():
539 line = "\t\"{}\": \"".format(key)
540 for char in fwfont_replacers[key]:
541 line += "\\U{:0>8x}".format(ord(char))
543 outputfile.write(line)
544 outputfile.write("\n")
545 outputfile.write("\t\"\": \"\"")
546 outputfile.write("\n")
547 outputfile.write("}")
548 outputfile.write("\n")
549 outputfile.write("fontlist = {")
550 outputfile.write("\n")
551 for tr in fwfont_readers:
552 outputfile.write("\t\"" + tr.get_font_name() + "\": {")
553 outputfile.write("\n")
554 outputfile.write("\t\t\"h\": {},".format(tr.get_table_head()["unitsPerEm"]))
555 outputfile.write("\n")
556 outputfile.write("\t\t\"w\": {")
557 outputfile.write("\n")
558 charmap = tr.get_table_cmap()
559 for char in charmap.keys():
560 width = tr.get_charwidth_from_char(char)
562 outputfile.write("\t\t\t\"\\U{:0>8x}\": {},".format(ord(char), width))
563 outputfile.write("\n")
564 outputfile.write("\t\t\t\"\": 0")
565 outputfile.write("\n")
566 outputfile.write("\t\t}")
567 outputfile.write("\n")
568 outputfile.write("\t},")
569 outputfile.write("\n")
571 outputfile.write("\t\"" + hwfont_reader.get_font_name() + "\": {")
572 outputfile.write("\n")
573 outputfile.write("\t\t\"h\": {},".format(hwfont_reader.get_table_head()["unitsPerEm"]))
574 outputfile.write("\n")
575 outputfile.write("\t\t\"w\": {")
576 outputfile.write("\n")
577 charmap = hwfont_reader.get_table_cmap()
578 for char in charmap.keys():
579 width = hwfont_reader.get_charwidth_from_char(char)
581 outputfile.write("\t\t\t\"\\U{:0>8x}\": {},".format(ord(char), width))
582 outputfile.write("\n")
583 outputfile.write("\t\t\t\"\": 0")
584 outputfile.write("\n")
585 outputfile.write("\t\t}")
586 outputfile.write("\n")
587 outputfile.write("\t}")
588 outputfile.write("\n")
589 outputfile.write("}")
590 outputfile.write("\n")
592 class build_fontwidth_py(distutils.cmd.Command):
593 description = "\"build\" fontwidth.py"
595 user_options = [("build-lib=", "d", "directory to \"build\" to")]
598 def initialize_options(self):
599 self.build_lib = None
601 def finalize_options(self):
602 self.set_undefined_options("build", ('build_lib', 'build_lib'))
605 self.mkpath(os.path.join(self.build_lib, "nicovideo"))
606 outfile = os.path.join(self.build_lib, "nicovideo", "fontwidth.py")
607 outfilehandle = open(outfile, mode="w", encoding="utf-8")
608 outfilehandle.write("# -*- coding: utf-8 -*-\n")
609 gen_fontwidth_py(outfilehandle)
610 outfilehandle.close()
612 class my_build(distutils.command.build.build):
614 self.run_command("build_fontwidth_py")
615 distutils.command.build.build.run(self)
617 distutils.core.setup( name="pylib-nicovideo",
619 description="ニコニコにアクセスしたりできる。",
621 author_email="dyknon@users.sourceforge.jp",
622 url="http://sourceforge.jp/users/dyknon/",
623 packages=["nicovideo"],
626 "build_fontwidth_py": build_fontwidth_py