From: <jo...@us...> - 2007-09-03 21:36:32
|
Revision: 3770 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=3770&view=rev Author: jouni Date: 2007-09-03 14:36:17 -0700 (Mon, 03 Sep 2007) Log Message: ----------- Created new file type1font.py for supporting Type 1 fonts; quite preliminary for now. Started adding Type 1 support to PDF backend for purposes of usetex. Modified Paths: -------------- trunk/matplotlib/API_CHANGES trunk/matplotlib/CHANGELOG trunk/matplotlib/lib/matplotlib/afm.py trunk/matplotlib/lib/matplotlib/backends/backend_pdf.py trunk/matplotlib/lib/matplotlib/dviread.py trunk/matplotlib/src/ft2font.cpp trunk/matplotlib/src/ft2font.h Added Paths: ----------- trunk/matplotlib/lib/matplotlib/type1font.py Modified: trunk/matplotlib/API_CHANGES =================================================================== --- trunk/matplotlib/API_CHANGES 2007-09-03 18:27:22 UTC (rev 3769) +++ trunk/matplotlib/API_CHANGES 2007-09-03 21:36:17 UTC (rev 3770) @@ -1,3 +1,18 @@ + The dviread.py file now has a parser for files like psfonts.map + and pdftex.map, to map TeX font names to external files. + + The file type1font.py contains a new class for Type 1 fonts. + Currently it simply reads pfa and pfb format files and stores the + data in pfa format, which is the format for embedding Type 1 fonts + in postscript and pdf files. In the future the class might + actually parse the font to allow e.g. subsetting. + + FT2Font now supports FT_Attach_File. In practice this can be used + to read an afm file in addition to a pfa/pfb file, to get metrics + and kerning information for a Type 1 font. + + The AFM class now supports querying CapHeight and stem widths. + Changed pcolor default to shading='flat'; but as noted now in the docstring, it is preferable to simply use the edgecolor kwarg. Modified: trunk/matplotlib/CHANGELOG =================================================================== --- trunk/matplotlib/CHANGELOG 2007-09-03 18:27:22 UTC (rev 3769) +++ trunk/matplotlib/CHANGELOG 2007-09-03 21:36:17 UTC (rev 3770) @@ -1,3 +1,9 @@ +2007-09-03 Created type1font.py, added features to AFM and FT2Font + (see API_CHANGES), started work on embedding Type 1 fonts + in pdf files. - JKS + +2007-09-02 Continued work on dviread.py. - JKS + 2007-08-16 Added a set_extent method to AxesImage, allow data extent to be modified after initial call to imshow - DSD Modified: trunk/matplotlib/lib/matplotlib/afm.py =================================================================== --- trunk/matplotlib/lib/matplotlib/afm.py 2007-09-03 18:27:22 UTC (rev 3769) +++ trunk/matplotlib/lib/matplotlib/afm.py 2007-09-03 21:36:17 UTC (rev 3770) @@ -103,7 +103,8 @@ 'Version': _to_str, 'Notice': _to_str, 'EncodingScheme': _to_str, - 'CapHeight': _to_float, + 'CapHeight': _to_float, # Is the second version a mistake, or + 'Capheight': _to_float, # do some AFM files contain 'Capheight'? -JKS 'XHeight': _to_float, 'Ascender': _to_float, 'Descender': _to_float, @@ -112,7 +113,6 @@ 'StartCharMetrics': _to_int, 'CharacterSet': _to_str, 'Characters': _to_int, - 'Capheight': _to_int, } d = {} @@ -446,6 +446,10 @@ "Return the fontangle as float" return self._header['ItalicAngle'] + def get_capheight(self): + "Return the cap height as float" + return self._header['CapHeight'] + def get_xheight(self): "Return the xheight as float" return self._header['XHeight'] @@ -453,6 +457,20 @@ def get_underline_thickness(self): "Return the underline thickness as float" return self._header['UnderlineThickness'] + + def get_horizontal_stem_width(self): + """ + Return the standard horizontal stem width as float, or None if + not specified in AFM file. + """ + return self._header.get('StdHW', None) + + def get_vertical_stem_width(self): + """ + Return the standard vertical stem width as float, or None if + not specified in AFM file. + """ + return self._header.get('StdVW', None) if __name__=='__main__': Modified: trunk/matplotlib/lib/matplotlib/backends/backend_pdf.py =================================================================== --- trunk/matplotlib/lib/matplotlib/backends/backend_pdf.py 2007-09-03 18:27:22 UTC (rev 3769) +++ trunk/matplotlib/lib/matplotlib/backends/backend_pdf.py 2007-09-03 21:36:17 UTC (rev 3770) @@ -26,7 +26,8 @@ from matplotlib.figure import Figure from matplotlib.font_manager import findfont from matplotlib.afm import AFM -from matplotlib.dviread import Dvi +import matplotlib.type1font as type1font +import matplotlib.dviread as dviread from matplotlib.ft2font import FT2Font, FIXED_WIDTH, ITALIC, LOAD_NO_SCALE, \ LOAD_NO_HINTING, KERNING_UNFITTED from matplotlib.mathtext import MathTextParser @@ -367,6 +368,7 @@ # self.fontNames maps filenames to internal font names self.fontNames = {} self.nextFont = 1 # next free internal font name + self.fontInfo = {} # information on fonts: metrics, encoding self.alphaStates = {} # maps alpha values to graphics state objects self.nextAlphaState = 1 @@ -438,6 +440,12 @@ self.currentstream = None def fontName(self, fontprop): + """ + Select a font based on fontprop and return a name suitable for + Op.selectfont. If fontprop is a string, it will be interpreted + as the filename of the font. + """ + if is_string_like(fontprop): filename = fontprop elif rcParams['pdf.use14corefonts']: @@ -458,6 +466,9 @@ for filename, Fx in self.fontNames.items(): if filename.endswith('.afm'): fontdictObject = self._write_afm_font(filename) + elif filename.endswith('.pfb') or filename.endswith('.pfa'): + # a Type 1 font; limited support for now + fontdictObject = self.embedType1(filename, self.fontInfo[Fx]) else: realpath, stat_key = get_realpath_and_stat(filename) chars = self.used_characters.get(stat_key) @@ -480,6 +491,97 @@ self.writeObject(fontdictObject, fontdict) return fontdictObject + def embedType1(self, filename, fontinfo): + fh = open(filename, 'rb') + try: + fontdata = fh.read() + finally: + fh.close() + + fh = open(fontinfo.afmfile, 'rb') + try: + afmdata = AFM(fh) + finally: + fh.close() + + font = FT2Font(filename) + font.attach_file(fontinfo.afmfile) + + widthsObject, fontdescObject, fontdictObject, fontfileObject = \ + [ self.reserveObject(n) for n in + ('font widths', 'font descriptor', + 'font dictionary', 'font file') ] + + _, _, fullname, familyname, weight, italic_angle, fixed_pitch, \ + ul_position, ul_thickness = font.get_ps_font_info() + + differencesArray = [ 0 ] + [ Name(ch) for ch in + dviread.Encoding(fontinfo.encoding) ] + + fontdict = { + 'Type': Name('Font'), + 'Subtype': Name('Type1'), + 'BaseFont': Name(font.postscript_name), + 'FirstChar': 0, + 'LastChar': len(differencesArray) - 2, + 'Widths': widthsObject, + 'FontDescriptor': fontdescObject, + 'Encoding': { 'Type': Name('Encoding'), + 'Differences': differencesArray }, + } + + flags = 0 + if fixed_pitch: flags |= 1 << 0 # fixed width + if 0: flags |= 1 << 1 # TODO: serif + if 0: flags |= 1 << 2 # TODO: symbolic + else: flags |= 1 << 5 # non-symbolic + if italic_angle: flags |= 1 << 6 # italic + if 0: flags |= 1 << 16 # TODO: all caps + if 0: flags |= 1 << 17 # TODO: small caps + if 0: flags |= 1 << 18 # TODO: force bold + + descriptor = { + 'Type': Name('FontDescriptor'), + 'FontName': Name(font.postscript_name), + 'Flags': flags, + 'FontBBox': font.bbox, + 'ItalicAngle': italic_angle, + 'Ascent': font.ascender, + 'Descent': font.descender, + 'CapHeight': afmdata.get_capheight(), + 'XHeight': afmdata.get_xheight(), + 'FontFile': fontfileObject, + 'FontFamily': Name(familyname), + #'FontWeight': a number where 400 = Regular, 700 = Bold + } + + # StemV is obligatory in PDF font descriptors but optional in + # AFM files. The collection of AFM files in my TeX Live 2007 + # collection has values ranging from 22 to 219, with both + # median and mode 50, so if the AFM file is silent, I'm + # guessing 50. -JKS + StemV = afmdata.get_vertical_stem_width() + if StemV is None: StemV = 50 + descriptor['StemV'] = StemV + + # StemH is entirely optional: + StemH = afmdata.get_horizontal_stem_width() + if StemH is not None: + descriptor['StemH'] = StemH + + self.writeObject(fontdictObject, fontdict) + self.writeObject(widthsObject, widths) + self.writeObject(fontdescObject, descriptor) + + fontdata = type1font.Type1Font(filename) + len1, len2, len3 = fontdata.lenghts() + self.beginStream(fontfileObject.id, None, + { 'Length1': len1, + 'Length2': len2, + 'Length3': len3 }) + self.currentstream.write(fontdata.data) + self.endStream() + def _get_xobject_symbol_name(self, filename, symbol_name): return "%s-%s" % ( os.path.splitext(os.path.basename(filename))[0], @@ -1034,6 +1136,7 @@ self.encode_string = self.encode_string_type42 self.mathtext_parser = MathTextParser("Pdf") self.image_magnification = dpi/72.0 + self.tex_font_map = None def finalize(self): self.gc.finalize() @@ -1050,6 +1153,12 @@ # Restore gc to avoid unwanted side effects gc._fillcolor = orig_fill + def tex_font_mapping(self, texfont): + if self.tex_font_map is None: + self.tex_font_map = \ + dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) + return self.tex_font_map[texfont] + def track_characters(self, font, s): """Keeps track of which characters are required from each font.""" @@ -1288,9 +1397,8 @@ texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() dvifile = texmanager.make_dvi(s, fontsize) - dvi = Dvi(dvifile, 72) + dvi = dviread.Dvi(dvifile, 72) text, boxes = iter(dvi).next() - fontdir = os.path.join(get_data_path(), 'fonts', 'ttf') if angle == 0: # avoid rounding errors in common case def mytrans(x1, y1): @@ -1303,14 +1411,17 @@ self.check_gc(gc, gc._rgb) self.file.output(Op.begin_text) - oldfont, oldx, oldy = None, 0, 0 - for x1, y1, font, glyph in text: - if font != oldfont: - fontname, fontsize = dvi.fontinfo(font) - fontfile = os.path.join(fontdir, fontname+'.ttf') - self.file.output(self.file.fontName(fontfile), - fontsize, Op.selectfont) - oldfont = font + oldfontnum, oldx, oldy = None, 0, 0 + for x1, y1, fontnum, glyph in text: + if fontnum != oldfontnum: + texname, fontsize = dvi.fontinfo(fontnum) + fontinfo = self.tex_font_mapping(texname) + pdfname = self.file.fontName(fontinfo.filename) + self.file.fontInfo[pdfname] = Bunch( + encodingfile=fontinfo.encoding, + afmfile=fontinfo.afm) + self.file.output(pdfname, fontsize, Op.selectfont) + oldfontnum = fontnum x1, y1 = mytrans(x1, y1) self._setup_textpos(x1, y1, angle, oldx, oldy) self.file.output(chr(glyph), Op.show) Modified: trunk/matplotlib/lib/matplotlib/dviread.py =================================================================== --- trunk/matplotlib/lib/matplotlib/dviread.py 2007-09-03 18:27:22 UTC (rev 3769) +++ trunk/matplotlib/lib/matplotlib/dviread.py 2007-09-03 21:36:17 UTC (rev 3770) @@ -410,11 +410,11 @@ def __getitem__(self, texname): result = self._font[texname] - if result.filename is not None \ - and not result.filename.startswith('/'): - result.filename = find_tex_file(result.filename) - if result.encoding is not None \ - and not result.encoding.startswith('/'): + fn, enc = result.filename, result.encoding + if fn is not None and not fn.startswith('/'): + result.filename = find_tex_file(fn) + result.afm = find_tex_file(fn[:-4] + '.afm') + if enc is not None and not enc.startswith('/'): result.encoding = find_tex_file(result.encoding) return result @@ -473,6 +473,51 @@ texname=texname, psname=psname, effects=effects, encoding=encoding, filename=filename) +class Encoding(object): + + def __init__(self, filename): + file = open(filename, 'rt') + try: + self.encoding = self._parse(file) + finally: + file.close() + + def __iter__(self): + for name in self.encoding: + yield name + + def _parse(self, file): + result = [] + + state = 0 + for line in file: + comment_start = line.find('%') + if comment_start > -1: + line = line[:comment_start] + line = line.strip() + + if state == 0: + # Expecting something like /FooEncoding [ + if '[' in line: + state = 1 + line = line[line.index('[')+1].strip() + + if state == 1: + words = line.split() + for w in words: + if w.startswith('/'): + # Allow for /abc/def/ghi + subwords = w.split('/') + result.extend(subwords[1:]) + else: + raise ValueError, "Broken name in encoding file: " + w + + # Expecting ] def + if ']' in line: + break + + return result + def find_tex_file(filename, format=None): """ Call kpsewhich to find a file in the texmf tree. Added: trunk/matplotlib/lib/matplotlib/type1font.py =================================================================== --- trunk/matplotlib/lib/matplotlib/type1font.py (rev 0) +++ trunk/matplotlib/lib/matplotlib/type1font.py 2007-09-03 21:36:17 UTC (rev 3770) @@ -0,0 +1,95 @@ +""" +A class representing a Type 1 font. + +This version merely allows reading in pfa and pfb files, and stores +the data in pfa format (which can be embedded in PostScript or PDF +files). A more complete class might support subsetting. + +Usage: font = Type1Font(filename) + somefile.write(font.data) # writes out font in pfa format + len1, len2, len3 = font.lengths() # needed for pdf embedding + +Source: Adobe Technical Note #5040, Supporting Downloadable PostScript +Language Fonts. + +If extending this class, see also: Adobe Type 1 Font Format, Adobe +Systems Incorporated, third printing, v1.1, 1993. ISBN 0-201-57044-0. +""" + +import struct + +class Type1Font(object): + + def __init__(self, filename): + file = open(filename, 'rb') + try: + self._read(file) + finally: + file.close() + + def _read(self, file): + rawdata = file.read() + if not rawdata.startswith(chr(128)): + self.data = rawdata + return + + self.data = '' + while len(rawdata) > 0: + if not rawdata.startswith(chr(128)): + raise RuntimeError, \ + 'Broken pfb file (expected byte 128, got %d)' % \ + ord(rawdata[0]) + type = ord(rawdata[1]) + if type in (1,2): + length, = struct.unpack('<i', rawdata[2:6]) + segment = rawdata[6:6+length] + rawdata = rawdata[6+length:] + + if type == 1: # ASCII text: include verbatim + self.data += segment + elif type == 2: # binary data: encode in hexadecimal + self.data += ''.join(['%02x' % ord(char) + for char in segment]) + elif type == 3: # end of file + break + else: + raise RuntimeError, \ + 'Unknown segment type %d in pfb file' % type + + def lengths(self): + """ + Compute the lengths of the three parts of a Type 1 font. + + The three parts are: (1) the cleartext part, which ends in a + eexec operator; (2) the encrypted part; (3) the fixed part, + which contains 512 ASCII zeros possibly divided on various + lines, a cleartomark operator, and possibly something else. + """ + + # Cleartext part: just find the eexec and skip the eol char(s) + idx = self.data.index('eexec') + idx += len('eexec') + while self.data[idx] in ('\n', '\r'): + idx += 1 + len1 = idx + + # Encrypted part: find the cleartomark operator and count + # zeros backward + idx = self.data.rindex('cleartomark') - 1 + zeros = 512 + while zeros and self.data[idx] in ('0', '\n', '\r'): + if self.data[idx] == '0': + zeros -= 1 + idx -= 1 + if zeros: + raise RuntimeError, 'Insufficiently many zeros in Type 1 font' + + len2 = idx - len1 + len3 = len(self.data) - idx + + return len1, len2, len3 + +if __name__ == '__main__': + import sys + font = Type1Font(sys.argv[1]) + sys.stdout.write(font.data) Modified: trunk/matplotlib/src/ft2font.cpp =================================================================== --- trunk/matplotlib/src/ft2font.cpp 2007-09-03 18:27:22 UTC (rev 3769) +++ trunk/matplotlib/src/ft2font.cpp 2007-09-03 21:36:17 UTC (rev 3770) @@ -1552,11 +1552,11 @@ } Py::Tuple info(9); - info[0] = Py::String(fontinfo.version); - info[1] = Py::String(fontinfo.notice); - info[2] = Py::String(fontinfo.full_name); - info[3] = Py::String(fontinfo.family_name); - info[4] = Py::String(fontinfo.weight); + info[0] = Py::String(fontinfo.version ? fontinfo.version : ""); + info[1] = Py::String(fontinfo.notice ? fontinfo.notice : ""); + info[2] = Py::String(fontinfo.full_name ? fontinfo.full_name : ""); + info[3] = Py::String(fontinfo.family_name ? fontinfo.family_name : ""); + info[4] = Py::String(fontinfo.weight ? fontinfo.weight : ""); info[5] = Py::Long(fontinfo.italic_angle); info[6] = Py::Int(fontinfo.is_fixed_pitch); info[7] = Py::Int(fontinfo.underline_position); @@ -1788,7 +1788,30 @@ return Py::asObject(image); } +char FT2Font::attach_file__doc__ [] = + "attach_file(filename)\n" + "\n" + "Attach a file with extra information on the font\n" + "(in practice, an AFM file with the metrics of a Type 1 font).\n" + "Throws an exception if unsuccessful.\n"; Py::Object +FT2Font::attach_file (const Py::Tuple &args) { + args.verify_length(1); + + std::string filename = Py::String(args[0]); + FT_Error error = + FT_Attach_File(face, filename.c_str()); + + if (error) { + std::ostringstream s; + s << "Could not attach file " << filename + << " (freetype error code " << error << ")" << std::endl; + throw Py::RuntimeError(s.str()); + } + return Py::Object(); +} + +Py::Object ft2font_module::new_ft2image (const Py::Tuple &args) { args.verify_length(2); @@ -1894,6 +1917,8 @@ FT2Font::get_sfnt_table__doc__); add_varargs_method("get_image", &FT2Font::get_image, FT2Font::get_image__doc__); + add_varargs_method("attach_file", &FT2Font::attach_file, + FT2Font::attach_file__doc__); behaviors().supportGetattr(); behaviors().supportSetattr(); @@ -1949,6 +1974,7 @@ " max_advance_height same for vertical layout\n" " underline_position vertical position of the underline bar\n" " underline_thickness vertical thickness of the underline\n" +" postscript_name PostScript name of the font\n" ; #if defined(_MSC_VER) Modified: trunk/matplotlib/src/ft2font.h =================================================================== --- trunk/matplotlib/src/ft2font.h 2007-09-03 18:27:22 UTC (rev 3769) +++ trunk/matplotlib/src/ft2font.h 2007-09-03 21:36:17 UTC (rev 3770) @@ -122,6 +122,7 @@ Py::Object get_ps_font_info(const Py::Tuple & args); Py::Object get_sfnt_table(const Py::Tuple & args); Py::Object get_image(const Py::Tuple & args); + Py::Object attach_file(const Py::Tuple & args); int setattr( const char *_name, const Py::Object &value ); Py::Object getattr( const char *_name ); FT2Image* image; @@ -163,6 +164,7 @@ static char get_ps_font_info__doc__[]; static char get_sfnt_table__doc__[]; static char get_image__doc__[]; + static char attach_file__doc__[]; }; // the extension module This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |