pdfmetrics.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827
  1. #Copyright ReportLab Europe Ltd. 2000-2017
  2. #see license.txt for license details
  3. #history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/pdfbase/pdfmetrics.py
  4. #$Header $
  5. __version__='3.3.0'
  6. __doc__="""This provides a database of font metric information and
  7. efines Font, Encoding and TypeFace classes aimed at end users.
  8. There are counterparts to some of these in pdfbase/pdfdoc.py, but
  9. the latter focus on constructing the right PDF objects. These
  10. classes are declarative and focus on letting the user construct
  11. and query font objects.
  12. The module maintains a registry of font objects at run time.
  13. It is independent of the canvas or any particular context. It keeps
  14. a registry of Font, TypeFace and Encoding objects. Ideally these
  15. would be pre-loaded, but due to a nasty circularity problem we
  16. trap attempts to access them and do it on first access.
  17. """
  18. import string, os, sys, encodings
  19. from reportlab.pdfbase import _fontdata
  20. from reportlab.lib.logger import warnOnce
  21. from reportlab.lib.utils import rl_isfile, rl_glob, rl_isdir, open_and_read, open_and_readlines, findInPaths, isSeq, isStr, isUnicode
  22. from reportlab.rl_config import defaultEncoding, T1SearchPath
  23. from reportlab.lib.rl_accel import unicode2T1, instanceStringWidthT1
  24. from reportlab.pdfbase import rl_codecs
  25. _notdefChar = b'n'
  26. rl_codecs.RL_Codecs.register()
  27. standardFonts = _fontdata.standardFonts
  28. standardEncodings = _fontdata.standardEncodings
  29. _typefaces = {}
  30. _encodings = {}
  31. _fonts = {}
  32. _dynFaceNames = {} #record dynamicFont face names
  33. class FontError(Exception):
  34. pass
  35. class FontNotFoundError(Exception):
  36. pass
  37. def parseAFMFile(afmFileName):
  38. """Quick and dirty - gives back a top-level dictionary
  39. with top-level items, and a 'widths' key containing
  40. a dictionary of glyph names and widths. Just enough
  41. needed for embedding. A better parser would accept
  42. options for what data you wwanted, and preserve the
  43. order."""
  44. lines = open_and_readlines(afmFileName, 'r')
  45. if len(lines)<=1:
  46. #likely to be a MAC file
  47. if lines: lines = lines[0].split('\r')
  48. if len(lines)<=1:
  49. raise ValueError('AFM file %s hasn\'t enough data' % afmFileName)
  50. topLevel = {}
  51. glyphLevel = []
  52. lines = [l.strip() for l in lines]
  53. lines = [l for l in lines if not l.lower().startswith('comment')]
  54. #pass 1 - get the widths
  55. inMetrics = 0 # os 'TOP', or 'CHARMETRICS'
  56. for line in lines:
  57. if line[0:16] == 'StartCharMetrics':
  58. inMetrics = 1
  59. elif line[0:14] == 'EndCharMetrics':
  60. inMetrics = 0
  61. elif inMetrics:
  62. chunks = line.split(';')
  63. chunks = [chunk.strip() for chunk in chunks]
  64. cidChunk, widthChunk, nameChunk = chunks[0:3]
  65. # character ID
  66. l, r = cidChunk.split()
  67. assert l == 'C', 'bad line in font file %s' % line
  68. cid = int(r)
  69. # width
  70. l, r = widthChunk.split()
  71. assert l == 'WX', 'bad line in font file %s' % line
  72. try:
  73. width = int(r)
  74. except ValueError:
  75. width = float(r)
  76. # name
  77. l, r = nameChunk.split()
  78. assert l == 'N', 'bad line in font file %s' % line
  79. name = r
  80. glyphLevel.append((cid, width, name))
  81. # pass 2 font info
  82. inHeader = 0
  83. for line in lines:
  84. if line[0:16] == 'StartFontMetrics':
  85. inHeader = 1
  86. if line[0:16] == 'StartCharMetrics':
  87. inHeader = 0
  88. elif inHeader:
  89. if line[0:7] == 'Comment': pass
  90. try:
  91. left, right = line.split(' ',1)
  92. except:
  93. raise ValueError("Header information error in afm %s: line='%s'" % (afmFileName, line))
  94. try:
  95. right = int(right)
  96. except:
  97. pass
  98. topLevel[left] = right
  99. return (topLevel, glyphLevel)
  100. class TypeFace:
  101. def __init__(self, name):
  102. self.name = name
  103. self.glyphNames = []
  104. self.glyphWidths = {}
  105. self.ascent = 0
  106. self.descent = 0
  107. # all typefaces of whatever class should have these 3 attributes.
  108. # these are the basis for family detection.
  109. self.familyName = None # should set on load/construction if possible
  110. self.bold = 0 # bold faces should set this
  111. self.italic = 0 #italic faces should set this
  112. if name == 'ZapfDingbats':
  113. self.requiredEncoding = 'ZapfDingbatsEncoding'
  114. elif name == 'Symbol':
  115. self.requiredEncoding = 'SymbolEncoding'
  116. else:
  117. self.requiredEncoding = None
  118. if name in standardFonts:
  119. self.builtIn = 1
  120. self._loadBuiltInData(name)
  121. else:
  122. self.builtIn = 0
  123. def _loadBuiltInData(self, name):
  124. """Called for the built in 14 fonts. Gets their glyph data.
  125. We presume they never change so this can be a shared reference."""
  126. name = str(name) #needed for pycanvas&jython/2.1 compatibility
  127. self.glyphWidths = _fontdata.widthsByFontGlyph[name]
  128. self.glyphNames = list(self.glyphWidths.keys())
  129. self.ascent,self.descent = _fontdata.ascent_descent[name]
  130. def getFontFiles(self):
  131. "Info function, return list of the font files this depends on."
  132. return []
  133. def findT1File(self, ext='.pfb'):
  134. possible_exts = (ext.lower(), ext.upper())
  135. if hasattr(self,'pfbFileName'):
  136. r_basename = os.path.splitext(self.pfbFileName)[0]
  137. for e in possible_exts:
  138. if rl_isfile(r_basename + e):
  139. return r_basename + e
  140. try:
  141. r = _fontdata.findT1File(self.name)
  142. except:
  143. afm = bruteForceSearchForAFM(self.name)
  144. if afm:
  145. if ext.lower() == '.pfb':
  146. for e in possible_exts:
  147. pfb = os.path.splitext(afm)[0] + e
  148. if rl_isfile(pfb):
  149. r = pfb
  150. else:
  151. r = None
  152. elif ext.lower() == '.afm':
  153. r = afm
  154. else:
  155. r = None
  156. if r is None:
  157. warnOnce("Can't find %s for face '%s'" % (ext, self.name))
  158. return r
  159. def bruteForceSearchForFile(fn,searchPath=None):
  160. if searchPath is None: from reportlab.rl_config import T1SearchPath as searchPath
  161. if rl_isfile(fn): return fn
  162. bfn = os.path.basename(fn)
  163. for dirname in searchPath:
  164. if not rl_isdir(dirname): continue
  165. tfn = os.path.join(dirname,bfn)
  166. if rl_isfile(tfn): return tfn
  167. return fn
  168. def bruteForceSearchForAFM(faceName):
  169. """Looks in all AFM files on path for face with given name.
  170. Returns AFM file name or None. Ouch!"""
  171. from reportlab.rl_config import T1SearchPath
  172. for dirname in T1SearchPath:
  173. if not rl_isdir(dirname): continue
  174. possibles = rl_glob(dirname + os.sep + '*.[aA][fF][mM]')
  175. for possible in possibles:
  176. try:
  177. topDict, glyphDict = parseAFMFile(possible)
  178. if topDict['FontName'] == faceName:
  179. return possible
  180. except:
  181. t,v,b=sys.exc_info()
  182. v.args = (' '.join(map(str,v.args))+', while looking for faceName=%r' % faceName,)
  183. raise
  184. #for faceName in standardFonts:
  185. # registerTypeFace(TypeFace(faceName))
  186. class Encoding:
  187. """Object to help you create and refer to encodings."""
  188. def __init__(self, name, base=None):
  189. self.name = name
  190. self.frozen = 0
  191. if name in standardEncodings:
  192. assert base is None, "Can't have a base encoding for a standard encoding"
  193. self.baseEncodingName = name
  194. self.vector = _fontdata.encodings[name]
  195. elif base == None:
  196. # assume based on the usual one
  197. self.baseEncodingName = defaultEncoding
  198. self.vector = _fontdata.encodings[defaultEncoding]
  199. elif isStr(base):
  200. baseEnc = getEncoding(base)
  201. self.baseEncodingName = baseEnc.name
  202. self.vector = baseEnc.vector[:]
  203. elif isSeq(base):
  204. self.baseEncodingName = defaultEncoding
  205. self.vector = base[:]
  206. elif isinstance(base, Encoding):
  207. # accept a vector
  208. self.baseEncodingName = base.name
  209. self.vector = base.vector[:]
  210. def __getitem__(self, index):
  211. "Return glyph name for that code point, or None"
  212. # THIS SHOULD BE INLINED FOR SPEED
  213. return self.vector[index]
  214. def __setitem__(self, index, value):
  215. # should fail if they are frozen
  216. assert self.frozen == 0, 'Cannot modify a frozen encoding'
  217. if self.vector[index]!=value:
  218. L = list(self.vector)
  219. L[index] = value
  220. self.vector = tuple(L)
  221. def freeze(self):
  222. self.vector = tuple(self.vector)
  223. self.frozen = 1
  224. def isEqual(self, other):
  225. return self.name==other.name and tuple(self.vector)==tuple(other.vector)
  226. def modifyRange(self, base, newNames):
  227. """Set a group of character names starting at the code point 'base'."""
  228. assert self.frozen == 0, 'Cannot modify a frozen encoding'
  229. idx = base
  230. for name in newNames:
  231. self.vector[idx] = name
  232. idx = idx + 1
  233. def getDifferences(self, otherEnc):
  234. """
  235. Return a compact list of the code points differing between two encodings
  236. This is in the Adobe format: list of
  237. [[b1, name1, name2, name3],
  238. [b2, name4]]
  239. where b1...bn is the starting code point, and the glyph names following
  240. are assigned consecutive code points.
  241. """
  242. ranges = []
  243. curRange = None
  244. for i in range(len(self.vector)):
  245. glyph = self.vector[i]
  246. if glyph==otherEnc.vector[i]:
  247. if curRange:
  248. ranges.append(curRange)
  249. curRange = []
  250. else:
  251. if curRange:
  252. curRange.append(glyph)
  253. elif glyph:
  254. curRange = [i, glyph]
  255. if curRange:
  256. ranges.append(curRange)
  257. return ranges
  258. def makePDFObject(self):
  259. "Returns a PDF Object representing self"
  260. # avoid circular imports - this cannot go at module level
  261. from reportlab.pdfbase import pdfdoc
  262. D = {}
  263. baseEncodingName = self.baseEncodingName
  264. baseEnc = getEncoding(baseEncodingName)
  265. differences = self.getDifferences(baseEnc) #[None] * 256)
  266. # if no differences, we just need the base name
  267. if differences == []:
  268. return pdfdoc.PDFName(baseEncodingName)
  269. else:
  270. #make up a dictionary describing the new encoding
  271. diffArray = []
  272. for range in differences:
  273. diffArray.append(range[0]) # numbers go 'as is'
  274. for glyphName in range[1:]:
  275. if glyphName is not None:
  276. # there is no way to 'unset' a character in the base font.
  277. diffArray.append('/' + glyphName)
  278. #print 'diffArray = %s' % diffArray
  279. D["Differences"] = pdfdoc.PDFArray(diffArray)
  280. if baseEncodingName in ('MacRomanEncoding','MacExpertEncoding','WinAnsiEncoding'):
  281. #https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf page 263
  282. D["BaseEncoding"] = pdfdoc.PDFName(baseEncodingName)
  283. D["Type"] = pdfdoc.PDFName("Encoding")
  284. PD = pdfdoc.PDFDictionary(D)
  285. return PD
  286. #for encName in standardEncodings:
  287. # registerEncoding(Encoding(encName))
  288. standardT1SubstitutionFonts = []
  289. class Font:
  290. """Represents a font (i.e combination of face and encoding).
  291. Defines suitable machinery for single byte fonts. This is
  292. a concrete class which can handle the basic built-in fonts;
  293. not clear yet if embedded ones need a new font class or
  294. just a new typeface class (which would do the job through
  295. composition)"""
  296. _multiByte = 0 # do not want our own stringwidth
  297. _dynamicFont = 0 # do not want dynamic subsetting
  298. def __init__(self, name, faceName, encName, substitutionFonts=None):
  299. self.fontName = name
  300. face = self.face = getTypeFace(faceName)
  301. self.encoding= getEncoding(encName)
  302. self.encName = encName
  303. self.substitutionFonts = (standardT1SubstitutionFonts
  304. if face.builtIn and face.requiredEncoding is None
  305. else substitutionFonts or [])
  306. self._calcWidths()
  307. self._notdefChar = _notdefChar
  308. self._notdefFont = name=='ZapfDingbats' and self or _notdefFont
  309. def stringWidth(self, text, size, encoding='utf8'):
  310. return instanceStringWidthT1(self, text, size, encoding=encoding)
  311. def __repr__(self):
  312. return "<%s %s>" % (self.__class__.__name__, self.face.name)
  313. def _calcWidths(self):
  314. """Vector of widths for stringWidth function"""
  315. #synthesize on first request
  316. w = [0] * 256
  317. gw = self.face.glyphWidths
  318. vec = self.encoding.vector
  319. for i in range(256):
  320. glyphName = vec[i]
  321. if glyphName is not None:
  322. try:
  323. width = gw[glyphName]
  324. w[i] = width
  325. except KeyError:
  326. import reportlab.rl_config
  327. if reportlab.rl_config.warnOnMissingFontGlyphs:
  328. print('typeface "%s" does not have a glyph "%s", bad font!' % (self.face.name, glyphName))
  329. else:
  330. pass
  331. self.widths = w
  332. def _formatWidths(self):
  333. "returns a pretty block in PDF Array format to aid inspection"
  334. text = b'['
  335. for i in range(256):
  336. text = text + b' ' + bytes(str(self.widths[i]),'utf8')
  337. if i == 255:
  338. text = text + b' ]'
  339. if i % 16 == 15:
  340. text = text + b'\n'
  341. return text
  342. def addObjects(self, doc):
  343. """Makes and returns one or more PDF objects to be added
  344. to the document. The caller supplies the internal name
  345. to be used (typically F1, F2... in sequence) """
  346. # avoid circular imports - this cannot go at module level
  347. from reportlab.pdfbase import pdfdoc
  348. # construct a Type 1 Font internal object
  349. internalName = 'F' + repr(len(doc.fontMapping)+1)
  350. pdfFont = pdfdoc.PDFType1Font()
  351. pdfFont.Name = internalName
  352. pdfFont.BaseFont = self.face.name
  353. pdfFont.__Comment__ = 'Font %s' % self.fontName
  354. e = self.encoding.makePDFObject()
  355. if not isStr(e) or e in ('/MacRomanEncoding','/MacExpertEncoding','/WinAnsiEncoding'):
  356. #https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf page 255
  357. pdfFont.Encoding = e
  358. # is it a built-in one? if not, need more stuff.
  359. if not self.face.name in standardFonts:
  360. pdfFont.FirstChar = 0
  361. pdfFont.LastChar = 255
  362. pdfFont.Widths = pdfdoc.PDFArray(self.widths)
  363. pdfFont.FontDescriptor = self.face.addObjects(doc)
  364. # now link it in
  365. ref = doc.Reference(pdfFont, internalName)
  366. # also refer to it in the BasicFonts dictionary
  367. fontDict = doc.idToObject['BasicFonts'].dict
  368. fontDict[internalName] = pdfFont
  369. # and in the font mappings
  370. doc.fontMapping[self.fontName] = '/' + internalName
  371. PFB_MARKER=chr(0x80)
  372. PFB_ASCII=chr(1)
  373. PFB_BINARY=chr(2)
  374. PFB_EOF=chr(3)
  375. def _pfbCheck(p,d,m,fn):
  376. if chr(d[p])!=PFB_MARKER or chr(d[p+1])!=m:
  377. raise ValueError('Bad pfb file\'%s\' expected chr(%d)chr(%d) at char %d, got chr(%d)chr(%d)' % (fn,ord(PFB_MARKER),ord(m),p,d[p],d[p+1]))
  378. if m==PFB_EOF: return
  379. p = p + 2
  380. l = (((((d[p+3])<<8)|(d[p+2])<<8)|(d[p+1]))<<8)|(d[p])
  381. p = p + 4
  382. if p+l>len(d):
  383. raise ValueError('Bad pfb file\'%s\' needed %d+%d bytes have only %d!' % (fn,p,l,len(d)))
  384. return p, p+l
  385. _postScriptNames2Unicode = None
  386. class EmbeddedType1Face(TypeFace):
  387. """A Type 1 font other than one of the basic 14.
  388. Its glyph data will be embedded in the PDF file."""
  389. def __init__(self, afmFileName, pfbFileName):
  390. # ignore afm file for now
  391. TypeFace.__init__(self, None)
  392. #None is a hack, name will be supplied by AFM parse lower done
  393. #in this __init__ method.
  394. afmFileName = findInPaths(afmFileName,T1SearchPath)
  395. pfbFileName = findInPaths(pfbFileName,T1SearchPath)
  396. self.afmFileName = os.path.abspath(afmFileName)
  397. self.pfbFileName = os.path.abspath(pfbFileName)
  398. self.requiredEncoding = None
  399. self._loadGlyphs(pfbFileName)
  400. self._loadMetrics(afmFileName)
  401. def getFontFiles(self):
  402. return [self.afmFileName, self.pfbFileName]
  403. def _loadGlyphs(self, pfbFileName):
  404. """Loads in binary glyph data, and finds the four length
  405. measurements needed for the font descriptor"""
  406. pfbFileName = bruteForceSearchForFile(pfbFileName)
  407. assert rl_isfile(pfbFileName), 'file %s not found' % pfbFileName
  408. d = open_and_read(pfbFileName, 'b')
  409. s1, l1 = _pfbCheck(0,d,PFB_ASCII,pfbFileName)
  410. s2, l2 = _pfbCheck(l1,d,PFB_BINARY,pfbFileName)
  411. s3, l3 = _pfbCheck(l2,d,PFB_ASCII,pfbFileName)
  412. _pfbCheck(l3,d,PFB_EOF,pfbFileName)
  413. self._binaryData = d[s1:l1]+d[s2:l2]+d[s3:l3]
  414. self._length = len(self._binaryData)
  415. self._length1 = l1-s1
  416. self._length2 = l2-s2
  417. self._length3 = l3-s3
  418. def _loadMetrics(self, afmFileName):
  419. """Loads in and parses font metrics"""
  420. #assert os.path.isfile(afmFileName), "AFM file %s not found" % afmFileName
  421. afmFileName = bruteForceSearchForFile(afmFileName)
  422. (topLevel, glyphData) = parseAFMFile(afmFileName)
  423. self.name = topLevel['FontName']
  424. self.familyName = topLevel['FamilyName']
  425. self.ascent = topLevel.get('Ascender', 1000)
  426. self.descent = topLevel.get('Descender', 0)
  427. self.capHeight = topLevel.get('CapHeight', 1000)
  428. self.italicAngle = topLevel.get('ItalicAngle', 0)
  429. self.stemV = topLevel.get('stemV', 0)
  430. self.xHeight = topLevel.get('XHeight', 1000)
  431. strBbox = topLevel.get('FontBBox', [0,0,1000,1000])
  432. tokens = strBbox.split()
  433. self.bbox = []
  434. for tok in tokens:
  435. self.bbox.append(int(tok))
  436. glyphWidths = {}
  437. for (cid, width, name) in glyphData:
  438. glyphWidths[name] = width
  439. self.glyphWidths = glyphWidths
  440. self.glyphNames = list(glyphWidths.keys())
  441. self.glyphNames.sort()
  442. # for font-specific encodings like Symbol, Dingbats, Carta we
  443. # need to make a new encoding as well....
  444. if topLevel.get('EncodingScheme', None) == 'FontSpecific':
  445. global _postScriptNames2Unicode
  446. if _postScriptNames2Unicode is None:
  447. try:
  448. from reportlab.pdfbase._glyphlist import _glyphname2unicode
  449. _postScriptNames2Unicode = _glyphname2unicode
  450. del _glyphname2unicode
  451. except:
  452. _postScriptNames2Unicode = {}
  453. raise ValueError(
  454. "cannot import module reportlab.pdfbase._glyphlist module\n"
  455. "you can obtain a version from here\n"
  456. "https://www.reportlab.com/ftp/_glyphlist.py\n"
  457. )
  458. names = [None] * 256
  459. ex = {}
  460. rex = {}
  461. for (code, width, name) in glyphData:
  462. if 0<=code<=255:
  463. names[code] = name
  464. u = _postScriptNames2Unicode.get(name,None)
  465. if u is not None:
  466. rex[code] = u
  467. ex[u] = code
  468. encName = encodings.normalize_encoding('rl-dynamic-%s-encoding' % self.name)
  469. rl_codecs.RL_Codecs.add_dynamic_codec(encName,ex,rex)
  470. self.requiredEncoding = encName
  471. enc = Encoding(encName, names)
  472. registerEncoding(enc)
  473. def addObjects(self, doc):
  474. """Add whatever needed to PDF file, and return a FontDescriptor reference"""
  475. from reportlab.pdfbase import pdfdoc
  476. fontFile = pdfdoc.PDFStream()
  477. fontFile.content = self._binaryData
  478. #fontFile.dictionary['Length'] = self._length
  479. fontFile.dictionary['Length1'] = self._length1
  480. fontFile.dictionary['Length2'] = self._length2
  481. fontFile.dictionary['Length3'] = self._length3
  482. #fontFile.filters = [pdfdoc.PDFZCompress]
  483. fontFileRef = doc.Reference(fontFile, 'fontFile:' + self.pfbFileName)
  484. fontDescriptor = pdfdoc.PDFDictionary({
  485. 'Type': '/FontDescriptor',
  486. 'Ascent':self.ascent,
  487. 'CapHeight':self.capHeight,
  488. 'Descent':self.descent,
  489. 'Flags': 34,
  490. 'FontBBox':pdfdoc.PDFArray(self.bbox),
  491. 'FontName':pdfdoc.PDFName(self.name),
  492. 'ItalicAngle':self.italicAngle,
  493. 'StemV':self.stemV,
  494. 'XHeight':self.xHeight,
  495. 'FontFile': fontFileRef,
  496. })
  497. fontDescriptorRef = doc.Reference(fontDescriptor, 'fontDescriptor:' + self.name)
  498. return fontDescriptorRef
  499. def registerTypeFace(face):
  500. assert isinstance(face, TypeFace), 'Not a TypeFace: %s' % face
  501. _typefaces[face.name] = face
  502. if not face.name in standardFonts:
  503. # HACK - bold/italic do not apply for type 1, so egister
  504. # all combinations of mappings.
  505. registerFontFamily(face.name)
  506. def registerEncoding(enc):
  507. assert isinstance(enc, Encoding), 'Not an Encoding: %s' % enc
  508. if enc.name in _encodings:
  509. # already got one, complain if they are not the same
  510. if enc.isEqual(_encodings[enc.name]):
  511. enc.freeze()
  512. else:
  513. raise FontError('Encoding "%s" already registered with a different name vector!' % enc.name)
  514. else:
  515. _encodings[enc.name] = enc
  516. enc.freeze()
  517. # have not yet dealt with immutability!
  518. def registerFontFamily(family,normal=None,bold=None,italic=None,boldItalic=None):
  519. from reportlab.lib import fonts
  520. if not normal: normal = family
  521. family = family.lower()
  522. if not boldItalic: boldItalic = italic or bold or normal
  523. if not bold: bold = normal
  524. if not italic: italic = normal
  525. fonts.addMapping(family, 0, 0, normal)
  526. fonts.addMapping(family, 1, 0, bold)
  527. fonts.addMapping(family, 0, 1, italic)
  528. fonts.addMapping(family, 1, 1, boldItalic)
  529. def registerFont(font):
  530. "Registers a font, including setting up info for accelerated stringWidth"
  531. #assert isinstance(font, Font), 'Not a Font: %s' % font
  532. fontName = font.fontName
  533. if font._dynamicFont:
  534. faceName = font.face.name
  535. if fontName not in _fonts:
  536. if faceName in _dynFaceNames:
  537. ofont = _dynFaceNames[faceName]
  538. if not ofont._dynamicFont:
  539. raise ValueError('Attempt to register fonts %r %r for face %r' % (ofont, font, faceName))
  540. else:
  541. _fonts[fontName] = ofont
  542. else:
  543. _dynFaceNames[faceName] = _fonts[fontName] = font
  544. else:
  545. _fonts[fontName] = font
  546. if font._multiByte:
  547. # CID fonts don't need to have typeface registered.
  548. #need to set mappings so it can go in a paragraph even if within
  549. # bold tags
  550. registerFontFamily(font.fontName)
  551. def getTypeFace(faceName):
  552. """Lazily construct known typefaces if not found"""
  553. try:
  554. return _typefaces[faceName]
  555. except KeyError:
  556. # not found, construct it if known
  557. if faceName in standardFonts:
  558. face = TypeFace(faceName)
  559. (face.familyName, face.bold, face.italic) = _fontdata.standardFontAttributes[faceName]
  560. registerTypeFace(face)
  561. ## print 'auto-constructing type face %s with family=%s, bold=%d, italic=%d' % (
  562. ## face.name, face.familyName, face.bold, face.italic)
  563. return face
  564. else:
  565. #try a brute force search
  566. afm = bruteForceSearchForAFM(faceName)
  567. if afm:
  568. for e in ('.pfb', '.PFB'):
  569. pfb = os.path.splitext(afm)[0] + e
  570. if rl_isfile(pfb): break
  571. assert rl_isfile(pfb), 'file %s not found!' % pfb
  572. face = EmbeddedType1Face(afm, pfb)
  573. registerTypeFace(face)
  574. return face
  575. else:
  576. raise
  577. def getEncoding(encName):
  578. """Lazily construct known encodings if not found"""
  579. try:
  580. return _encodings[encName]
  581. except KeyError:
  582. if encName in standardEncodings:
  583. enc = Encoding(encName)
  584. registerEncoding(enc)
  585. #print 'auto-constructing encoding %s' % encName
  586. return enc
  587. else:
  588. raise
  589. def findFontAndRegister(fontName):
  590. '''search for and register a font given its name'''
  591. fontName = str(fontName)
  592. assert type(fontName) is str, 'fontName=%s is not required type str' % ascii(fontName)
  593. #it might have a font-specific encoding e.g. Symbol
  594. # or Dingbats. If not, take the default.
  595. face = getTypeFace(fontName)
  596. if face.requiredEncoding:
  597. font = Font(fontName, fontName, face.requiredEncoding)
  598. else:
  599. font = Font(fontName, fontName, defaultEncoding)
  600. registerFont(font)
  601. return font
  602. def getFont(fontName):
  603. """Lazily constructs known fonts if not found.
  604. Names of form 'face-encoding' will be built if
  605. face and encoding are known. Also if the name is
  606. just one of the standard 14, it will make up a font
  607. in the default encoding."""
  608. try:
  609. return _fonts[fontName]
  610. except KeyError:
  611. return findFontAndRegister(fontName)
  612. _notdefFont = getFont('ZapfDingbats')
  613. standardT1SubstitutionFonts.extend([getFont('Symbol'),_notdefFont])
  614. def getAscentDescent(fontName,fontSize=None):
  615. font = getFont(fontName)
  616. try:
  617. ascent = font.ascent
  618. descent = font.descent
  619. except:
  620. ascent = font.face.ascent
  621. descent = font.face.descent
  622. if fontSize:
  623. norm = fontSize/1000.
  624. return ascent*norm, descent*norm
  625. else:
  626. return ascent, descent
  627. def getAscent(fontName,fontSize=None):
  628. return getAscentDescent(fontName,fontSize)[0]
  629. def getDescent(fontName,fontSize=None):
  630. return getAscentDescent(fontName,fontSize)[1]
  631. def getRegisteredFontNames():
  632. "Returns what's in there"
  633. reg = list(_fonts.keys())
  634. reg.sort()
  635. return reg
  636. def stringWidth(text, fontName, fontSize, encoding='utf8'):
  637. """Compute width of string in points;
  638. not accelerated as fast enough because of instanceStringWidthT1/TTF"""
  639. return getFont(fontName).stringWidth(text, fontSize, encoding=encoding)
  640. def dumpFontData():
  641. print('Registered Encodings:')
  642. keys = list(_encodings.keys())
  643. keys.sort()
  644. for encName in keys:
  645. print(' ',encName)
  646. print()
  647. print('Registered Typefaces:')
  648. faces = list(_typefaces.keys())
  649. faces.sort()
  650. for faceName in faces:
  651. print(' ',faceName)
  652. print()
  653. print('Registered Fonts:')
  654. k = list(_fonts.keys())
  655. k.sort()
  656. for key in k:
  657. font = _fonts[key]
  658. print(' %s (%s/%s)' % (font.fontName, font.face.name, font.encoding.name))
  659. def test3widths(texts):
  660. # checks all 3 algorithms give same answer, note speed
  661. import time
  662. for fontName in standardFonts[0:1]:
  663. ## t0 = time.time()
  664. ## for text in texts:
  665. ## l1 = stringWidth(text, fontName, 10)
  666. ## t1 = time.time()
  667. ## print 'fast stringWidth took %0.4f' % (t1 - t0)
  668. t0 = time.time()
  669. w = getFont(fontName).widths
  670. for text in texts:
  671. l2 = 0
  672. for ch in text:
  673. l2 = l2 + w[ord(ch)]
  674. t1 = time.time()
  675. print('slow stringWidth took %0.4f' % (t1 - t0))
  676. t0 = time.time()
  677. for text in texts:
  678. l3 = getFont(fontName).stringWidth(text, 10)
  679. t1 = time.time()
  680. print('class lookup and stringWidth took %0.4f' % (t1 - t0))
  681. print()
  682. def testStringWidthAlgorithms():
  683. rawdata = open('../../rlextra/rml2pdf/doc/rml_user_guide.prep').read()
  684. print('rawdata length %d' % len(rawdata))
  685. print('test one huge string...')
  686. test3widths([rawdata])
  687. print()
  688. words = rawdata.split()
  689. print('test %d shorter strings (average length %0.2f chars)...' % (len(words), 1.0*len(rawdata)/len(words)))
  690. test3widths(words)
  691. def test():
  692. helv = TypeFace('Helvetica')
  693. registerTypeFace(helv)
  694. print(helv.glyphNames[0:30])
  695. wombat = TypeFace('Wombat')
  696. print(wombat.glyphNames)
  697. registerTypeFace(wombat)
  698. dumpFontData()
  699. #preserve the initial values here
  700. def _reset(
  701. initial_dicts = dict(
  702. _typefaces = _typefaces.copy(),
  703. _encodings = _encodings.copy(),
  704. _fonts = _fonts.copy(),
  705. _dynFaceNames = _dynFaceNames.copy(),
  706. )
  707. ):
  708. for k,v in initial_dicts.items():
  709. d=globals()[k]
  710. d.clear()
  711. d.update(v)
  712. rl_codecs.RL_Codecs.reset_dynamic_codecs()
  713. from reportlab.rl_config import register_reset
  714. register_reset(_reset)
  715. del register_reset
  716. if __name__=='__main__':
  717. test()
  718. testStringWidthAlgorithms()