def findStyle(self, fn): """ Find the absolute file name for a given style filename. Given a style filename, searches for it in StyleSearchPath and returns the real file name. """ def innerFind(path, fn): if os.path.isabs(fn): if os.path.isfile(fn): return fn else: for D in path: tfn = os.path.join(D, fn) if os.path.isfile(tfn): return tfn return None for ext in ['', '.style', '.json']: result = innerFind(self.StyleSearchPath, fn + ext) if result: break if result is None: log.warning("Can't find stylesheet %s" % fn) return result
def wrap(self, availWidth, availHeight): """ If we need more width than we have, complain, keep a scale. """ self.pad = self.border_padding(True, 0.1) maxWidth = float( min( styles.adjustUnits(self.maxWidth, availWidth) or availWidth, availWidth)) self.maxWidth = maxWidth maxWidth -= (self.pad[1] + self.pad[3]) # Paragraphs always say they can fit, so check the dims instead dims = [] _listWrapOn(self.content, maxWidth, None, dims=dims) self.width, self.height = min(dims, key=lambda i: i[0]) if self.width > maxWidth: if self.mode != 'shrink': self.scale = 1.0 log.warning( "BoundByWidth too wide to fit in frame (%s > %s): %s", self.width, maxWidth, self.identity()) if self.mode == 'shrink' and not self.scale: self.scale = ((maxWidth + self.pad[1] + self.pad[3]) / (self.width + self.pad[1] + self.pad[3])) else: self.scale = 1.0 self.height *= self.scale self.width *= self.scale return self.width, self.height + (self.pad[0] + self.pad[2]) * self.scale
def gather_elements(self, client, node, style): # Based on the graphviz extension global graphviz_warn try: # Is vectorpdf enabled? if hasattr(VectorPdf, 'load_xobj'): # Yes, we have vectorpdf fname, outfn = sphinx.ext.graphviz.render_dot( node['builder'], node['code'], node['options'], 'pdf') else: # Use bitmap if not graphviz_warn: log.warning( 'Using graphviz with PNG output. You get much better results if you enable the vectorpdf extension.' ) graphviz_warn = True fname, outfn = sphinx.ext.graphviz.render_dot( node['builder'], node['code'], node['options'], 'png') if outfn: client.to_unlink.append(outfn) client.to_unlink.append(outfn + '.map') else: # Something went very wrong with graphviz, and # sphinx should have given an error already return [] except sphinx.ext.graphviz.GraphvizError as exc: log.error('dot code %r: ' % node['code'] + str(exc)) return [Paragraph(node['code'], client.styles['code'])] return [MyImage(filename=outfn, client=client)]
def wrap(self, availWidth, availHeight): """ If we need more width than we have, complain, keep a scale. """ self.pad = self.border_padding(True, 0.1) maxWidth = float(min( styles.adjustUnits(self.maxWidth, availWidth) or availWidth, availWidth)) self.maxWidth = maxWidth maxWidth -= (self.pad[1] + self.pad[3]) # Paragraphs always say they can fit, so check the dims instead dims = [] _listWrapOn(self.content, maxWidth, None, dims=dims) self.width, self.height = min(dims, key=lambda i: i[0]) if self.width > maxWidth: if self.mode != 'shrink': self.scale = 1.0 log.warning("BoundByWidth too wide to fit in frame (%s > %s): %s", self.width, maxWidth, self.identity()) if self.mode == 'shrink' and not self.scale: self.scale = ((maxWidth + self.pad[1] + self.pad[3]) / (self.width + self.pad[1] + self.pad[3])) else: self.scale = 1.0 self.height *= self.scale self.width *= self.scale return self.width, self.height + (self.pad[0] + self.pad[2]) * self.scale
def log_unknown(self, node, during): if not hasattr(self, 'unkn_node'): self.unkn_node = set() cln = self.getclassname(node) if not cln in self.unkn_node: self.unkn_node.add(cln) log.warning("Unkn. node (self.%s): %s [%s]", during, cln, nodeid(node)) try: log.debug(node) except (UnicodeDecodeError, UnicodeEncodeError): log.debug(repr(node))
def wrap(self, availWidth, availHeight): if self.__kind == 'percentage_of_container': w, h = self.__width, self.__height if not w: log.warning('Scaling image as % of container with w unset.' 'This should not happen, setting to 100') w = 100 scale = w / 100. w = availWidth * scale h = w / self.__ratio self.image.drawWidth, self.image.drawHeight = w, h return w, h else: if self.image.drawHeight > availHeight: if not getattr(self, '_atTop', True): return self.image.wrap(availWidth, availHeight) else: # It's the first thing in the frame, probably # Wrapping it will not make it work, so we # adjust by height # FIXME get rst file info (line number) # here for better error message log.warning('image %s is too tall for the '\ 'frame, rescaling' % \ self.filename) self.image.drawHeight = availHeight self.image.drawWidth = availHeight * self.__ratio elif self.image.drawWidth > availWidth: log.warning('image %s is too wide for the frame, rescaling' % \ self.filename) self.image.drawWidth = availWidth self.image.drawHeight = availWidth / self.__ratio return self.image.wrap(availWidth, availHeight)
def __getitem__(self, key): """ This 'normalizes' the key. For example, if the key is todo_node (like sphinx uses), it will be converted to 'todo-node' which is a valid docutils class name. """ if not re.match("^[a-z](-?[a-z0-9]+)*$", key): key = docutils.nodes.make_id(key) if key in self.StyleSheet: return self.StyleSheet[key] else: if key.startswith('pygments'): alias = 'code' else: alias = 'normal' log.warning("Using undefined style " + "'%s', aliased to style '%s'.", key, alias) newst = copy.copy(self.StyleSheet[alias]) newst.name = key self.StyleSheet.add(newst) return newst
def __getitem__(self, key): """ This 'normalizes' the key. For example, if the key is todo_node (like sphinx uses), it will be converted to 'todo-node' which is a valid docutils class name. """ if not re.match("^[a-z](-?[a-z0-9]+)*$", key): key = docutils.nodes.make_id(key) if key in self.StyleSheet: return self.StyleSheet[key] else: if key.startswith('pygments'): alias = 'code' else: alias = 'normal' log.warning( "Using undefined style " + "'%s', aliased to style '%s'.", key, alias) newst = copy.copy(self.StyleSheet[alias]) newst.name = key self.StyleSheet.add(newst) return newst
def __init__(self, flist, font_path=None, style_path=None, def_dpi=300): log.info('Using stylesheets: %s' % ','.join(flist)) # find base path if hasattr(sys, 'frozen'): self.PATH = abspath(dirname(sys.executable)) else: self.PATH = abspath(dirname(__file__)) # flist is a list of stylesheet filenames. # They will be loaded and merged in order. # but the two default stylesheets will always # be loaded first flist = [ join(self.PATH, 'styles', 'styles.style'), join(self.PATH, 'styles', 'default.style') ] + flist self.def_dpi = def_dpi if font_path is None: font_path = [] font_path += ['.', os.path.join(self.PATH, 'fonts')] self.FontSearchPath = [os.path.expanduser(p) for p in font_path] if style_path is None: style_path = [] style_path += [ '.', os.path.join(self.PATH, 'styles'), '~/.rst2pdf/styles' ] self.StyleSearchPath = [os.path.expanduser(p) for p in style_path] self.FontSearchPath = list(set(self.FontSearchPath)) self.StyleSearchPath = list(set(self.StyleSearchPath)) log.info('FontPath:%s' % self.FontSearchPath) log.info('StylePath:%s' % self.StyleSearchPath) findfonts.flist = self.FontSearchPath # Page width, height self.pw = 0 self.ph = 0 # Page size [w,h] self.ps = None # Margins (top,bottom,left,right,gutter) self.tm = 0 self.bm = 0 self.lm = 0 self.rm = 0 self.gm = 0 # text width self.tw = 0 # Default emsize, later it will be the fontSize of the base style self.emsize = 10 self.languages = [] ssdata = self.readSheets(flist) # Get pageSetup data from all stylessheets in order: self.ps = pagesizes.A4 self.page = {} for data, ssname in ssdata: page = data.get('pageSetup', {}) if page: self.page.update(page) pgs = page.get('size', None) if pgs: # A standard size pgs = pgs.upper() if pgs in pagesizes.__dict__: self.ps = list(pagesizes.__dict__[pgs]) self.psname = pgs if 'width' in self.page: del (self.page['width']) if 'height' in self.page: del (self.page['height']) elif pgs.endswith('-LANDSCAPE'): self.psname = pgs.split('-')[0] self.ps = list( pagesizes.landscape( pagesizes.__dict__[self.psname])) if 'width' in self.page: del (self.page['width']) if 'height' in self.page: del (self.page['height']) else: log.critical('Unknown page size %s in stylesheet %s' % (page['size'], ssname)) continue else: # A custom size if 'size' in self.page: del (self.page['size']) # The sizes are expressed in some unit. # For example, 2cm is 2 centimeters, and we need # to do 2*cm (cm comes from reportlab.lib.units) if 'width' in page: self.ps[0] = self.adjustUnits(page['width']) if 'height' in page: self.ps[1] = self.adjustUnits(page['height']) self.pw, self.ph = self.ps if 'margin-left' in page: self.lm = self.adjustUnits(page['margin-left']) if 'margin-right' in page: self.rm = self.adjustUnits(page['margin-right']) if 'margin-top' in page: self.tm = self.adjustUnits(page['margin-top']) if 'margin-bottom' in page: self.bm = self.adjustUnits(page['margin-bottom']) if 'margin-gutter' in page: self.gm = self.adjustUnits(page['margin-gutter']) if 'spacing-header' in page: self.ts = self.adjustUnits(page['spacing-header']) if 'spacing-footer' in page: self.bs = self.adjustUnits(page['spacing-footer']) if 'firstTemplate' in page: self.firstTemplate = page['firstTemplate'] # tw is the text width. # We need it to calculate header-footer height # and compress literal blocks. self.tw = self.pw - self.lm - self.rm - self.gm # Get page templates from all stylesheets self.pageTemplates = {} for data, ssname in ssdata: templates = data.get('pageTemplates', {}) # templates is a dictionary of pageTemplates for key in templates: template = templates[key] # template is a dict. # template[´frames'] is a list of frames if key in self.pageTemplates: self.pageTemplates[key].update(template) else: self.pageTemplates[key] = template # Get font aliases from all stylesheets in order self.fontsAlias = {} for data, ssname in ssdata: self.fontsAlias.update(data.get('fontsAlias', {})) embedded_fontnames = [] self.embedded = [] # Embed all fonts indicated in all stylesheets for data, ssname in ssdata: embedded = data.get('embeddedFonts', []) for font in embedded: try: # Just a font name, try to embed it if isinstance(font, str): # See if we can find the font fname, pos = findfonts.guessFont(font) if font in embedded_fontnames: pass else: fontList = findfonts.autoEmbed(font) if fontList: embedded_fontnames.append(font) if not fontList: if (fname, pos) in embedded_fontnames: fontList = None else: fontList = findfonts.autoEmbed(fname) if fontList is not None: self.embedded += fontList # Maybe the font we got is not called # the same as the one we gave # so check that out suff = ["", "-Oblique", "-Bold", "-BoldOblique"] if not fontList[0].startswith(font): # We need to create font aliases, and use them for fname, aliasname in zip( fontList, [font + suffix for suffix in suff]): self.fontsAlias[aliasname] = fname continue # Each "font" is a list of four files, which will be # used for regular / bold / italic / bold+italic # versions of the font. # If your font doesn't have one of them, just repeat # the regular font. # Example, using the Tuffy font from # http://tulrich.com/fonts/ # "embeddedFonts" : [ # ["Tuffy.ttf", # "Tuffy_Bold.ttf", # "Tuffy_Italic.ttf", # "Tuffy_Bold_Italic.ttf"] # ], # The fonts will be registered with the file name, # minus the extension. if font[0].lower().endswith('.ttf'): # A True Type font for variant in font: location = self.findFont(variant) pdfmetrics.registerFont( TTFont(str(variant.split('.')[0]), location)) log.info('Registering font: %s from %s' % (str(variant.split('.')[0]), location)) self.embedded.append(str(variant.split('.')[0])) # And map them all together regular, bold, italic, bolditalic = [ variant.split('.')[0] for variant in font ] addMapping(regular, 0, 0, regular) addMapping(regular, 0, 1, italic) addMapping(regular, 1, 0, bold) addMapping(regular, 1, 1, bolditalic) else: # A Type 1 font # For type 1 fonts we require # [FontName,regular,italic,bold,bolditalic] # where each variant is a (pfbfile,afmfile) pair. # For example, for the URW palladio from TeX: # ["Palatino",("uplr8a.pfb","uplr8a.afm"), # ("uplri8a.pfb","uplri8a.afm"), # ("uplb8a.pfb","uplb8a.afm"), # ("uplbi8a.pfb","uplbi8a.afm")] regular = pdfmetrics.EmbeddedType1Face(*font[1]) italic = pdfmetrics.EmbeddedType1Face(*font[2]) bold = pdfmetrics.EmbeddedType1Face(*font[3]) bolditalic = pdfmetrics.EmbeddedType1Face(*font[4]) except Exception as e: try: if isinstance(font, list): fname = font[0] else: fname = font log.error("Error processing font %s: %s", os.path.splitext(fname)[0], str(e)) log.error("Registering %s as Helvetica alias", fname) self.fontsAlias[fname] = 'Helvetica' except Exception as e: log.critical("Error processing font %s: %s", fname, str(e)) continue # Go though all styles in all stylesheets and find all fontNames. # Then decide what to do with them for data, ssname in ssdata: for [skey, style] in self.stylepairs(data): for key in style: if key == 'fontName' or key.endswith('FontName'): # It's an alias, replace it if style[key] in self.fontsAlias: style[key] = self.fontsAlias[style[key]] # Embedded already, nothing to do if style[key] in self.embedded: continue # Standard font, nothing to do if style[key] in ("Courier", "Courier-Bold", "Courier-BoldOblique", "Courier-Oblique", "Helvetica", "Helvetica-Bold", "Helvetica-BoldOblique", "Helvetica-Oblique", "Symbol", "Times-Bold", "Times-BoldItalic", "Times-Italic", "Times-Roman", "ZapfDingbats"): continue # Now we need to do something # See if we can find the font fname, pos = findfonts.guessFont(style[key]) if style[key] in embedded_fontnames: pass else: fontList = findfonts.autoEmbed(style[key]) if fontList: embedded_fontnames.append(style[key]) if not fontList: if (fname, pos) in embedded_fontnames: fontList = None else: fontList = findfonts.autoEmbed(fname) if fontList: embedded_fontnames.append((fname, pos)) if fontList: self.embedded += fontList # Maybe the font we got is not called # the same as the one we gave so check that out suff = ["", "-Bold", "-Oblique", "-BoldOblique"] if not fontList[0].startswith(style[key]): # We need to create font aliases, and use them basefname = style[key].split('-')[0] for fname, aliasname in zip( fontList, [basefname + suffix for suffix in suff]): self.fontsAlias[aliasname] = fname style[key] = self.fontsAlias[basefname + suff[pos]] else: log.error( 'Unknown font: "%s",' "replacing with Helvetica", style[key]) style[key] = "Helvetica" # log.info('FontList: %s'%self.embedded) # log.info('FontAlias: %s'%self.fontsAlias) # Get styles from all stylesheets in order self.stylesheet = {} self.styles = [] self.linkColor = 'navy' # FIXME: linkColor should probably not be a global # style, and tocColor should probably not # be a special case, but for now I'm going # with the flow... self.tocColor = None for data, ssname in ssdata: self.linkColor = data.get('linkColor') or self.linkColor self.tocColor = data.get('tocColor') or self.tocColor for [skey, style] in self.stylepairs(data): sdict = {} # FIXME: this is done completely backwards for key in style: # Handle color references by name if key == 'color' or key.endswith('Color') and style[key]: style[key] = formatColor(style[key]) # Yet another workaround for the unicode bug in # reportlab's toColor elif key == 'commands': style[key] = validateCommands(style[key]) # for command in style[key]: # c=command[0].upper() # if c=='ROWBACKGROUNDS': # command[3]=[str(c) for c in command[3]] # elif c in ['BOX','INNERGRID'] or c.startswith('LINE'): # command[4]=str(command[4]) # Handle alignment constants elif key == 'alignment': style[key] = dict( TA_LEFT=0, LEFT=0, TA_CENTER=1, CENTER=1, TA_CENTRE=1, CENTRE=1, TA_RIGHT=2, RIGHT=2, TA_JUSTIFY=4, JUSTIFY=4, DECIMAL=8, )[style[key].upper()] elif key == 'language': if not style[key] in self.languages: self.languages.append(style[key]) # Make keys str instead of unicode (required by reportlab) sdict[str(key)] = style[key] sdict['name'] = skey # If the style already exists, update it if skey in self.stylesheet: self.stylesheet[skey].update(sdict) else: # New style self.stylesheet[skey] = sdict self.styles.append(sdict) # If the stylesheet has a style name docutils won't reach # make a copy with a sanitized name. # This may make name collisions possible but that should be # rare (who would have custom_name and custom-name in the # same stylesheet? ;-) # Issue 339 styles2 = [] for s in self.styles: if not re.match("^[a-z](-?[a-z0-9]+)*$", s['name']): s2 = copy.copy(s) s2['name'] = docutils.nodes.make_id(s['name']) log.warning(('%s is an invalid docutils class name, adding ' + 'alias %s') % (s['name'], s2['name'])) styles2.append(s2) self.styles.extend(styles2) # And create reportlabs stylesheet self.StyleSheet = StyleSheet1() # Patch to make the code compatible with reportlab from SVN 2.4+ and # 2.4 if not hasattr(self.StyleSheet, 'has_key'): self.StyleSheet.__class__.has_key = lambda s, k: k in s for s in self.styles: if 'parent' in s: if s['parent'] is None: if s['name'] != 'base': s['parent'] = self.StyleSheet['base'] else: del (s['parent']) else: s['parent'] = self.StyleSheet[s['parent']] else: if s['name'] != 'base': s['parent'] = self.StyleSheet['base'] # If the style has no bulletFontName but it has a fontName, set it if ('bulletFontName' not in s) and ('fontName' in s): s['bulletFontName'] = s['fontName'] hasFS = True # Adjust fontsize units if 'fontSize' not in s: s['fontSize'] = s['parent'].fontSize s['trueFontSize'] = None hasFS = False elif 'parent' in s: # This means you can set the fontSize to # "2cm" or to "150%" which will be calculated # relative to the parent style s['fontSize'] = self.adjustUnits(s['fontSize'], s['parent'].fontSize) s['trueFontSize'] = s['fontSize'] else: # If s has no parent, it's base, which has # an explicit point size by default and % # makes no sense, but guess it as % of 10pt s['fontSize'] = self.adjustUnits(s['fontSize'], 10) # If the leading is not set, but the size is, set it if 'leading' not in s and hasFS: s['leading'] = 1.2 * s['fontSize'] # If the bullet font size is not set, set it as fontSize if ('bulletFontSize' not in s) and ('fontSize' in s): s['bulletFontSize'] = s['fontSize'] # If the borderPadding is a list and wordaxe <=0.3.2, # convert it to an integer. Workaround for Issue if ('borderPadding' in s and HAS_WORDAXE and wordaxe_version <= 'wordaxe 0.3.2' and isinstance(s['borderPadding'], list)): log.warning(('Using a borderPadding list in style %s with ' + 'wordaxe <= 0.3.2. That is not supported, so ' + 'it will probably look wrong') % s['name']) s['borderPadding'] = s['borderPadding'][0] self.StyleSheet.add(ParagraphStyle(**s)) self.emsize = self['base'].fontSize # Make stdFont the basefont, for Issue 65 reportlab.rl_config.canvas_basefontname = self['base'].fontName # Make stdFont the default font for table cell styles (Issue 65) reportlab.platypus.tables.CellStyle.fontname = self['base'].fontName
def get_backend(self, uri, client): """ Given the filename of an image, return (fname, backend). fname is the filename to be used (could be the same as filename, or something different if the image had to be converted or is missing), and backend is an Image class that can handle fname. If uri ends with '.*' then the returned filename will be the best quality supported at the moment. That means: PDF > SVG > anything else """ backend = defaultimage # Extract all the information from the URI filename, extension, options = self.split_uri(uri) if '*' in filename: preferred = ['gif', 'jpg', 'png', 'svg', 'pdf'] # Find out what images are available available = glob.glob(filename) cfn = available[0] cv = -10 for fn in available: ext = fn.split('.')[-1] if ext in preferred: v = preferred.index(ext) else: v = -1 if v > cv: cv = v cfn = fn # cfn should have our favourite type of # those available filename = cfn extension = cfn.split('.')[-1] uri = filename # If the image doesn't exist, we use a 'missing' image if not os.path.exists(filename): log.error("Missing image file: %s", filename) filename = missing if extension in ['svg', 'svgz']: log.info('Backend for %s is SVGIMage' % filename) backend = SVGImage elif extension in ['pdf']: if VectorPdf is not None and filename is not missing: backend = VectorPdf filename = uri # PDF images are implemented by converting via PythonMagick # w,h are in pixels. I need to set the density # of the image to the right dpi so this # looks decent elif LazyImports.PMImage or LazyImports.gfx: filename = self.raster(filename, client) else: log.warning('Minimal PDF image support requires ' + 'PythonMagick or the vectorpdf extension [%s]', filename) filename = missing elif extension != 'jpg' and not LazyImports.PILImage: if LazyImports.PMImage: # Need to convert to JPG via PythonMagick filename = self.raster(filename, client) else: # No way to make this work log.error('To use a %s image you need PIL installed [%s]', extension, filename) filename = missing return filename, backend
def get_backend(self, uri, client): """ Given the filename of an image, return (fname, backend). fname is the filename to be used (could be the same as filename, or something different if the image had to be converted or is missing), and backend is an Image class that can handle fname. If uri ends with '.*' then the returned filename will be the best quality supported at the moment. That means: PDF > SVG > anything else """ backend = defaultimage # Extract all the information from the URI filename, extension, options = self.split_uri(uri) if '*' in filename: preferred = ['gif', 'jpg', 'png', 'svg', 'pdf'] # Find out what images are available available = glob.glob(filename) cfn = available[0] cv = -10 for fn in available: ext = fn.split('.')[-1] if ext in preferred: v = preferred.index(ext) else: v = -1 if v > cv: cv = v cfn = fn # cfn should have our favourite type of # those available filename = cfn extension = cfn.split('.')[-1] uri = filename # If the image doesn't exist, we use a 'missing' image if not os.path.exists(filename): log.error("Missing image file: %s", filename) filename = missing if extension in ['svg', 'svgz']: log.info('Backend for %s is SVGIMage' % filename) backend = SVGImage elif extension in ['pdf']: if VectorPdf is not None and filename is not missing: backend = VectorPdf filename = uri # PDF images are implemented by converting via PythonMagick # w,h are in pixels. I need to set the density # of the image to the right dpi so this # looks decent elif LazyImports.PMImage or LazyImports.gfx: filename = self.raster(filename, client) else: log.warning( 'Minimal PDF image support requires ' + 'PythonMagick or the vectorpdf extension [%s]', filename) filename = missing elif extension != 'jpg' and not LazyImports.PILImage: if LazyImports.PMImage: # Need to convert to JPG via PythonMagick filename = self.raster(filename, client) else: # No way to make this work log.error('To use a %s image you need PIL installed [%s]', extension, filename) filename = missing return filename, backend
def size_for_node(self, node, client): """ Given a docutils image node, return the size the image should have in the PDF document, and what 'kind' of size that is. That involves lots of guesswork. """ uri = str(node.get("uri")) if uri.split("://")[0].lower() not in ('http', 'ftp', 'https'): uri = os.path.join(client.basedir, uri) else: uri, _ = urllib.request.urlretrieve(uri) client.to_unlink.append(uri) srcinfo = client, uri # Extract all the information from the URI imgname, extension, options = self.split_uri(uri) if not os.path.isfile(imgname): imgname = missing scale = float(node.get('scale', 100)) / 100 # Figuring out the size to display of an image is ... annoying. # If the user provides a size with a unit, it's simple, adjustUnits # will return it in points and we're done. # However, often the unit wil be "%" (specially if it's meant for # HTML originally. In which case, we will use a percentage of # the containing frame. # Find the image size in pixels: kind = 'direct' xdpi, ydpi = client.styles.def_dpi, client.styles.def_dpi extension = imgname.split('.')[-1].lower() if extension in ['svg', 'svgz']: iw, ih = SVGImage(imgname, srcinfo=srcinfo).wrap(0, 0) # These are in pt, so convert to px iw = iw * xdpi / 72 ih = ih * ydpi / 72 elif extension == 'pdf': if VectorPdf is not None: xobj = VectorPdf.load_xobj(srcinfo) iw, ih = xobj.w, xobj.h else: pdf = LazyImports.pdfinfo if pdf is None: log.warning( 'PDF images are not supported without pyPdf or pdfrw [%s]', nodeid(node)) return 0, 0, 'direct' reader = pdf.PdfFileReader(open(imgname, 'rb')) box = [float(x) for x in reader.getPage(0)['/MediaBox']] iw, ih = x2 - x1, y2 - y1 # These are in pt, so convert to px iw = iw * xdpi / 72.0 ih = ih * ydpi / 72.0 else: keeptrying = True if LazyImports.PILImage: try: img = LazyImports.PILImage.open(imgname) img.load() iw, ih = img.size xdpi, ydpi = img.info.get('dpi', (xdpi, ydpi)) keeptrying = False except IOError: # PIL throws this when it's a broken/unknown image pass if keeptrying and LazyImports.PMImage: img = LazyImports.PMImage(imgname) iw = img.size().width() ih = img.size().height() density = img.density() # The density is in pixelspercentimeter (!?) xdpi = density.width() * 2.54 ydpi = density.height() * 2.54 keeptrying = False if keeptrying: if extension not in ['jpg', 'jpeg']: log.error( "The image (%s, %s) is broken or in an unknown format", imgname, nodeid(node)) raise ValueError else: # Can be handled by reportlab log.warning( "Can't figure out size of the image (%s, %s). Install PIL for better results.", imgname, nodeid(node)) iw = 1000 ih = 1000 # Try to get the print resolution from the image itself via PIL. # If it fails, assume a DPI of 300, which is pretty much made up, # and then a 100% size would be iw*inch/300, so we pass # that as the second parameter to adjustUnits # # Some say the default DPI should be 72. That would mean # the largest printable image in A4 paper would be something # like 480x640. That would be awful. # w = node.get('width') h = node.get('height') if h is None and w is None: # Nothing specified # Guess from iw, ih log.debug( "Using image %s without specifying size." "Calculating based on image size at %ddpi [%s]", imgname, xdpi, nodeid(node)) w = iw * inch / xdpi h = ih * inch / ydpi elif w is not None: # Node specifies only w # In this particular case, we want the default unit # to be pixels so we work like rst2html if w[-1] == '%': kind = 'percentage_of_container' w = int(w[:-1]) else: # This uses default DPI setting because we # are not using the image's "natural size" # this is what LaTeX does, according to the # docutils mailing list discussion w = client.styles.adjustUnits(w, client.styles.tw, default_unit='px') if h is None: # h is set from w with right aspect ratio h = w * ih / iw else: h = client.styles.adjustUnits(h, ih * inch / ydpi, default_unit='px') elif h is not None and w is None: if h[-1] != '%': h = client.styles.adjustUnits(h, ih * inch / ydpi, default_unit='px') # w is set from h with right aspect ratio w = h * iw / ih else: log.error( 'Setting height as a percentage does **not** work. ' + 'ignoring height parameter [%s]', nodeid(node)) # Set both from image data w = iw * inch / xdpi h = ih * inch / ydpi # Apply scale factor w = w * scale h = h * scale # And now we have this probably completely bogus size! log.info("Image %s size calculated: %fcm by %fcm [%s]", imgname, w / cm, h / cm, nodeid(node)) return w, h, kind
def support_warning(cls): if cls.warned or LazyImports.PILImage: return cls.warned = True log.warning("Support for images other than JPG," " is now limited. Please install PIL.")
def __init__(self, flist, font_path=None, style_path=None, def_dpi=300): log.info('Using stylesheets: %s' % ','.join(flist)) # find base path if hasattr(sys, 'frozen'): self.PATH = abspath(dirname(sys.executable)) else: self.PATH = abspath(dirname(__file__)) # flist is a list of stylesheet filenames. # They will be loaded and merged in order. # but the two default stylesheets will always # be loaded first flist = [join(self.PATH, 'styles', 'styles.style'), join(self.PATH, 'styles', 'default.style')] + flist self.def_dpi = def_dpi if font_path is None: font_path = [] font_path += ['.', os.path.join(self.PATH, 'fonts')] self.FontSearchPath = [os.path.expanduser(p) for p in font_path] if style_path is None: style_path = [] style_path += ['.', os.path.join(self.PATH, 'styles'), '~/.rst2pdf/styles'] self.StyleSearchPath = [os.path.expanduser(p) for p in style_path] self.FontSearchPath = list(set(self.FontSearchPath)) self.StyleSearchPath = list(set(self.StyleSearchPath)) log.info('FontPath:%s' % self.FontSearchPath) log.info('StylePath:%s' % self.StyleSearchPath) findfonts.flist = self.FontSearchPath # Page width, height self.pw = 0 self.ph = 0 # Page size [w,h] self.ps = None # Margins (top,bottom,left,right,gutter) self.tm = 0 self.bm = 0 self.lm = 0 self.rm = 0 self.gm = 0 # text width self.tw = 0 # Default emsize, later it will be the fontSize of the base style self.emsize = 10 self.languages = [] ssdata = self.readSheets(flist) # Get pageSetup data from all stylessheets in order: self.ps = pagesizes.A4 self.page = {} for data, ssname in ssdata: page = data.get('pageSetup', {}) if page: self.page.update(page) pgs = page.get('size', None) if pgs: # A standard size pgs = pgs.upper() if pgs in pagesizes.__dict__: self.ps = list(pagesizes.__dict__[pgs]) self.psname = pgs if 'width' in self.page: del(self.page['width']) if 'height' in self.page: del(self.page['height']) elif pgs.endswith('-LANDSCAPE'): self.psname = pgs.split('-')[0] self.ps = list(pagesizes.landscape(pagesizes.__dict__[self.psname])) if 'width' in self.page: del(self.page['width']) if 'height' in self.page: del(self.page['height']) else: log.critical('Unknown page size %s in stylesheet %s' % (page['size'], ssname)) continue else: # A custom size if 'size'in self.page: del(self.page['size']) # The sizes are expressed in some unit. # For example, 2cm is 2 centimeters, and we need # to do 2*cm (cm comes from reportlab.lib.units) if 'width' in page: self.ps[0] = self.adjustUnits(page['width']) if 'height' in page: self.ps[1] = self.adjustUnits(page['height']) self.pw, self.ph = self.ps if 'margin-left' in page: self.lm = self.adjustUnits(page['margin-left']) if 'margin-right' in page: self.rm = self.adjustUnits(page['margin-right']) if 'margin-top' in page: self.tm = self.adjustUnits(page['margin-top']) if 'margin-bottom' in page: self.bm = self.adjustUnits(page['margin-bottom']) if 'margin-gutter' in page: self.gm = self.adjustUnits(page['margin-gutter']) if 'spacing-header' in page: self.ts = self.adjustUnits(page['spacing-header']) if 'spacing-footer' in page: self.bs = self.adjustUnits(page['spacing-footer']) if 'firstTemplate' in page: self.firstTemplate = page['firstTemplate'] # tw is the text width. # We need it to calculate header-footer height # and compress literal blocks. self.tw = self.pw - self.lm - self.rm - self.gm # Get page templates from all stylesheets self.pageTemplates = {} for data, ssname in ssdata: templates = data.get('pageTemplates', {}) # templates is a dictionary of pageTemplates for key in templates: template = templates[key] # template is a dict. # template[´frames'] is a list of frames if key in self.pageTemplates: self.pageTemplates[key].update(template) else: self.pageTemplates[key] = template # Get font aliases from all stylesheets in order self.fontsAlias = {} for data, ssname in ssdata: self.fontsAlias.update(data.get('fontsAlias', {})) embedded_fontnames = [] self.embedded = [] # Embed all fonts indicated in all stylesheets for data, ssname in ssdata: embedded = data.get('embeddedFonts', []) for font in embedded: try: # Just a font name, try to embed it if isinstance(font, str): # See if we can find the font fname, pos = findfonts.guessFont(font) if font in embedded_fontnames: pass else: fontList = findfonts.autoEmbed(font) if fontList: embedded_fontnames.append(font) if not fontList: if (fname, pos) in embedded_fontnames: fontList = None else: fontList = findfonts.autoEmbed(fname) if fontList is not None: self.embedded += fontList # Maybe the font we got is not called # the same as the one we gave # so check that out suff = ["", "-Oblique", "-Bold", "-BoldOblique"] if not fontList[0].startswith(font): # We need to create font aliases, and use them for fname, aliasname in zip( fontList, [font + suffix for suffix in suff]): self.fontsAlias[aliasname] = fname continue # Each "font" is a list of four files, which will be # used for regular / bold / italic / bold+italic # versions of the font. # If your font doesn't have one of them, just repeat # the regular font. # Example, using the Tuffy font from # http://tulrich.com/fonts/ # "embeddedFonts" : [ # ["Tuffy.ttf", # "Tuffy_Bold.ttf", # "Tuffy_Italic.ttf", # "Tuffy_Bold_Italic.ttf"] # ], # The fonts will be registered with the file name, # minus the extension. if font[0].lower().endswith('.ttf'): # A True Type font for variant in font: location = self.findFont(variant) pdfmetrics.registerFont( TTFont(str(variant.split('.')[0]), location)) log.info('Registering font: %s from %s' % (str(variant.split('.')[0]), location)) self.embedded.append(str(variant.split('.')[0])) # And map them all together regular, bold, italic, bolditalic = [ variant.split('.')[0] for variant in font] addMapping(regular, 0, 0, regular) addMapping(regular, 0, 1, italic) addMapping(regular, 1, 0, bold) addMapping(regular, 1, 1, bolditalic) else: # A Type 1 font # For type 1 fonts we require # [FontName,regular,italic,bold,bolditalic] # where each variant is a (pfbfile,afmfile) pair. # For example, for the URW palladio from TeX: # ["Palatino",("uplr8a.pfb","uplr8a.afm"), # ("uplri8a.pfb","uplri8a.afm"), # ("uplb8a.pfb","uplb8a.afm"), # ("uplbi8a.pfb","uplbi8a.afm")] regular = pdfmetrics.EmbeddedType1Face(*font[1]) italic = pdfmetrics.EmbeddedType1Face(*font[2]) bold = pdfmetrics.EmbeddedType1Face(*font[3]) bolditalic = pdfmetrics.EmbeddedType1Face(*font[4]) except Exception as e: try: if isinstance(font, list): fname = font[0] else: fname = font log.error("Error processing font %s: %s", os.path.splitext(fname)[0], str(e)) log.error("Registering %s as Helvetica alias", fname) self.fontsAlias[fname] = 'Helvetica' except Exception as e: log.critical("Error processing font %s: %s", fname, str(e)) continue # Go though all styles in all stylesheets and find all fontNames. # Then decide what to do with them for data, ssname in ssdata: for [skey, style] in self.stylepairs(data): for key in style: if key == 'fontName' or key.endswith('FontName'): # It's an alias, replace it if style[key] in self.fontsAlias: style[key] = self.fontsAlias[style[key]] # Embedded already, nothing to do if style[key] in self.embedded: continue # Standard font, nothing to do if style[key] in ( "Courier", "Courier-Bold", "Courier-BoldOblique", "Courier-Oblique", "Helvetica", "Helvetica-Bold", "Helvetica-BoldOblique", "Helvetica-Oblique", "Symbol", "Times-Bold", "Times-BoldItalic", "Times-Italic", "Times-Roman", "ZapfDingbats" ): continue # Now we need to do something # See if we can find the font fname, pos = findfonts.guessFont(style[key]) if style[key] in embedded_fontnames: pass else: fontList = findfonts.autoEmbed(style[key]) if fontList: embedded_fontnames.append(style[key]) if not fontList: if (fname, pos) in embedded_fontnames: fontList = None else: fontList = findfonts.autoEmbed(fname) if fontList: embedded_fontnames.append((fname, pos)) if fontList: self.embedded += fontList # Maybe the font we got is not called # the same as the one we gave so check that out suff = ["", "-Bold", "-Oblique", "-BoldOblique"] if not fontList[0].startswith(style[key]): # We need to create font aliases, and use them basefname = style[key].split('-')[0] for fname, aliasname in zip( fontList, [basefname + suffix for suffix in suff] ): self.fontsAlias[aliasname] = fname style[key] = self.fontsAlias[basefname + suff[pos]] else: log.error('Unknown font: "%s",' "replacing with Helvetica", style[key]) style[key] = "Helvetica" # log.info('FontList: %s'%self.embedded) # log.info('FontAlias: %s'%self.fontsAlias) # Get styles from all stylesheets in order self.stylesheet = {} self.styles = [] self.linkColor = 'navy' # FIXME: linkColor should probably not be a global # style, and tocColor should probably not # be a special case, but for now I'm going # with the flow... self.tocColor = None for data, ssname in ssdata: self.linkColor = data.get('linkColor') or self.linkColor self.tocColor = data.get('tocColor') or self.tocColor for [skey, style] in self.stylepairs(data): sdict = {} # FIXME: this is done completely backwards for key in style: # Handle color references by name if key == 'color' or key.endswith('Color') and style[key]: style[key] = formatColor(style[key]) # Yet another workaround for the unicode bug in # reportlab's toColor elif key == 'commands': style[key] = validateCommands(style[key]) # for command in style[key]: # c=command[0].upper() # if c=='ROWBACKGROUNDS': # command[3]=[str(c) for c in command[3]] # elif c in ['BOX','INNERGRID'] or c.startswith('LINE'): # command[4]=str(command[4]) # Handle alignment constants elif key == 'alignment': style[key] = dict(TA_LEFT=0, LEFT=0, TA_CENTER=1, CENTER=1, TA_CENTRE=1, CENTRE=1, TA_RIGHT=2, RIGHT=2, TA_JUSTIFY=4, JUSTIFY=4, DECIMAL=8,)[style[key].upper()] elif key == 'language': if not style[key] in self.languages: self.languages.append(style[key]) # Make keys str instead of unicode (required by reportlab) sdict[str(key)] = style[key] sdict['name'] = skey # If the style already exists, update it if skey in self.stylesheet: self.stylesheet[skey].update(sdict) else: # New style self.stylesheet[skey] = sdict self.styles.append(sdict) # If the stylesheet has a style name docutils won't reach # make a copy with a sanitized name. # This may make name collisions possible but that should be # rare (who would have custom_name and custom-name in the # same stylesheet? ;-) # Issue 339 styles2 = [] for s in self.styles: if not re.match("^[a-z](-?[a-z0-9]+)*$", s['name']): s2 = copy.copy(s) s2['name'] = docutils.nodes.make_id(s['name']) log.warning(('%s is an invalid docutils class name, adding ' + 'alias %s') % (s['name'], s2['name'])) styles2.append(s2) self.styles.extend(styles2) # And create reportlabs stylesheet self.StyleSheet = StyleSheet1() # Patch to make the code compatible with reportlab from SVN 2.4+ and # 2.4 if not hasattr(self.StyleSheet, 'has_key'): self.StyleSheet.__class__.has_key = lambda s, k: k in s for s in self.styles: if 'parent' in s: if s['parent'] is None: if s['name'] != 'base': s['parent'] = self.StyleSheet['base'] else: del(s['parent']) else: s['parent'] = self.StyleSheet[s['parent']] else: if s['name'] != 'base': s['parent'] = self.StyleSheet['base'] # If the style has no bulletFontName but it has a fontName, set it if ('bulletFontName' not in s) and ('fontName' in s): s['bulletFontName'] = s['fontName'] hasFS = True # Adjust fontsize units if 'fontSize' not in s: s['fontSize'] = s['parent'].fontSize s['trueFontSize'] = None hasFS = False elif 'parent' in s: # This means you can set the fontSize to # "2cm" or to "150%" which will be calculated # relative to the parent style s['fontSize'] = self.adjustUnits(s['fontSize'], s['parent'].fontSize) s['trueFontSize'] = s['fontSize'] else: # If s has no parent, it's base, which has # an explicit point size by default and % # makes no sense, but guess it as % of 10pt s['fontSize'] = self.adjustUnits(s['fontSize'], 10) # If the leading is not set, but the size is, set it if 'leading' not in s and hasFS: s['leading'] = 1.2 * s['fontSize'] # If the bullet font size is not set, set it as fontSize if ('bulletFontSize' not in s) and ('fontSize' in s): s['bulletFontSize'] = s['fontSize'] # If the borderPadding is a list and wordaxe <=0.3.2, # convert it to an integer. Workaround for Issue if ( 'borderPadding' in s and HAS_WORDAXE and wordaxe_version <= 'wordaxe 0.3.2' and isinstance(s['borderPadding'], list) ): log.warning(('Using a borderPadding list in style %s with ' + 'wordaxe <= 0.3.2. That is not supported, so ' + 'it will probably look wrong') % s['name']) s['borderPadding'] = s['borderPadding'][0] self.StyleSheet.add(ParagraphStyle(**s)) self.emsize = self['base'].fontSize # Make stdFont the basefont, for Issue 65 reportlab.rl_config.canvas_basefontname = self['base'].fontName # Make stdFont the default font for table cell styles (Issue 65) reportlab.platypus.tables.CellStyle.fontname = self['base'].fontName
def loadFonts(): """ Search the system and build lists of available fonts. """ if not any([afmList, pfbList, ttfList]): # FIXME: When we drop support por py2, use recursive globs for folder in flist: for root, _, files in os.walk(folder): for f in files: if fnmatch(f, "*.ttf") or fnmatch(f, "*.ttc"): ttfList.append(os.path.join(root, f)) elif fnmatch(f, "*.afm"): afmList.append(os.path.join(root, f)) elif fnmatch(f, "*.pfb"): pfbList[f[:-4]] = os.path.join(root, f) for ttf in ttfList: try: font = TTFontFile(ttf) except TTFError: log.warning("Error processing %s", ttf) continue family = make_string(font.familyName.lower()) fontName = make_string(font.name).lower() baseName = os.path.basename(ttf)[:-4] fullName = make_string(font.fullName).lower() for k in (fontName, fullName, fullName.replace("italic", "oblique")): fonts[k] = (ttf, ttf, family) bold = FF_FORCEBOLD == FF_FORCEBOLD & font.flags italic = FF_ITALIC == FF_ITALIC & font.flags # And we can try to build/fill the family mapping if family not in families: families[family] = [fontName, fontName, fontName, fontName] if bold and italic: families[family][3] = fontName elif bold: families[family][1] = fontName elif italic: families[family][2] = fontName # FIXME: what happens if there are Demi and Medium # weights? We get a random one. else: families[family][0] = fontName # Now we have full afm and pbf lists, process the # afm list to figure out family name, weight and if # it's italic or not, as well as where the # matching pfb file is for afm in afmList: family = None fontName = None italic = False bold = False for line in open(afm, "r"): line = line.strip() if line.startswith("StartCharMetrics"): break elif line.startswith("FamilyName"): family = line.split(" ", 1)[1].lower() elif line.startswith("FontName"): fontName = line.split(" ")[1] elif line.startswith("FullName"): fullName = line.split(" ", 1)[1] elif line.startswith("Weight"): bold = line.split(" ")[1] == "Bold" elif line.startswith("ItalicAngle"): italic = line.split(" ")[1] != "0.0" baseName = os.path.basename(afm)[:-4] if family in Ignored or family in Alias: continue if baseName not in pfbList: log.info("afm file without matching pfb file: %s" % baseName) continue # So now we have a font we know we can embed. for n in ( fontName.lower(), fullName.lower(), fullName.lower().replace("italic", "oblique"), ): fonts[n] = (afm, pfbList[baseName], family) # And we can try to build/fill the family mapping if family not in families: families[family] = [fontName, fontName, fontName, fontName] if bold and italic: families[family][3] = fontName elif bold: families[family][1] = fontName elif italic: families[family][2] = fontName # FIXME: what happens if there are Demi and Medium # weights? We get a random one. else: families[family][0] = fontName
def size_for_node(self, node, client): """ Given a docutils image node, return the size the image should have in the PDF document, and what 'kind' of size that is. That involves lots of guesswork. """ uri = str(node.get("uri")) if uri.split("://")[0].lower() not in ('http', 'ftp', 'https'): uri = os.path.join(client.basedir, uri) else: uri, _ = urllib.request.urlretrieve(uri) client.to_unlink.append(uri) srcinfo = client, uri # Extract all the information from the URI imgname, extension, options = self.split_uri(uri) if not os.path.isfile(imgname): imgname = missing scale = float(node.get('scale', 100)) / 100 # Figuring out the size to display of an image is ... annoying. # If the user provides a size with a unit, it's simple, adjustUnits # will return it in points and we're done. # However, often the unit wil be "%" (specially if it's meant for # HTML originally. In which case, we will use a percentage of # the containing frame. # Find the image size in pixels: kind = 'direct' xdpi, ydpi = client.styles.def_dpi, client.styles.def_dpi extension = imgname.split('.')[-1].lower() if extension in ['svg', 'svgz']: iw, ih = SVGImage(imgname, srcinfo=srcinfo).wrap(0, 0) # These are in pt, so convert to px iw = iw * xdpi / 72 ih = ih * ydpi / 72 elif extension == 'pdf': if VectorPdf is not None: xobj = VectorPdf.load_xobj(srcinfo) iw, ih = xobj.w, xobj.h else: pdf = LazyImports.pdfinfo if pdf is None: log.warning('PDF images are not supported without pyPdf or pdfrw [%s]', nodeid(node)) return 0, 0, 'direct' reader = pdf.PdfFileReader(open(imgname, 'rb')) box = [float(x) for x in reader.getPage(0)['/MediaBox']] iw, ih = x2 - x1, y2 - y1 # These are in pt, so convert to px iw = iw * xdpi / 72.0 ih = ih * ydpi / 72.0 else: keeptrying = True if LazyImports.PILImage: try: img = LazyImports.PILImage.open(imgname) img.load() iw, ih = img.size xdpi, ydpi = img.info.get('dpi', (xdpi, ydpi)) keeptrying = False except IOError: # PIL throws this when it's a broken/unknown image pass if keeptrying and LazyImports.PMImage: img = LazyImports.PMImage(imgname) iw = img.size().width() ih = img.size().height() density = img.density() # The density is in pixelspercentimeter (!?) xdpi = density.width() * 2.54 ydpi = density.height() * 2.54 keeptrying = False if keeptrying: if extension not in ['jpg', 'jpeg']: log.error("The image (%s, %s) is broken or in an unknown format" , imgname, nodeid(node)) raise ValueError else: # Can be handled by reportlab log.warning("Can't figure out size of the image (%s, %s). Install PIL for better results." , imgname, nodeid(node)) iw = 1000 ih = 1000 # Try to get the print resolution from the image itself via PIL. # If it fails, assume a DPI of 300, which is pretty much made up, # and then a 100% size would be iw*inch/300, so we pass # that as the second parameter to adjustUnits # # Some say the default DPI should be 72. That would mean # the largest printable image in A4 paper would be something # like 480x640. That would be awful. # w = node.get('width') h = node.get('height') if h is None and w is None: # Nothing specified # Guess from iw, ih log.debug("Using image %s without specifying size." "Calculating based on image size at %ddpi [%s]", imgname, xdpi, nodeid(node)) w = iw * inch / xdpi h = ih * inch / ydpi elif w is not None: # Node specifies only w # In this particular case, we want the default unit # to be pixels so we work like rst2html if w[-1] == '%': kind = 'percentage_of_container' w = int(w[:-1]) else: # This uses default DPI setting because we # are not using the image's "natural size" # this is what LaTeX does, according to the # docutils mailing list discussion w = client.styles.adjustUnits(w, client.styles.tw, default_unit='px') if h is None: # h is set from w with right aspect ratio h = w * ih / iw else: h = client.styles.adjustUnits(h, ih * inch / ydpi, default_unit='px') elif h is not None and w is None: if h[-1] != '%': h = client.styles.adjustUnits(h, ih * inch / ydpi, default_unit='px') # w is set from h with right aspect ratio w = h * iw / ih else: log.error('Setting height as a percentage does **not** work. ' + 'ignoring height parameter [%s]', nodeid(node)) # Set both from image data w = iw * inch / xdpi h = ih * inch / ydpi # Apply scale factor w = w * scale h = h * scale # And now we have this probably completely bogus size! log.info("Image %s size calculated: %fcm by %fcm [%s]", imgname, w / cm, h / cm, nodeid(node)) return w, h, kind