Advertisement
gwilliams

PDF.py

Oct 2nd, 2017
145
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.76 KB | None | 0 0
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3.  
  4. __author__ = 'Gary Williams, Grin Technologies'
  5.  
  6. '''
  7. The PDF class below adds convenience functions and enumerations to Reportlab's Canvas class.
  8. '''
  9.  
  10.  
  11. import colorsys
  12. import math
  13.  
  14. from enum import Enum # https://pypi.python.org/pypi/enum34
  15.  
  16. from reportlab.graphics.charts.textlabels import _text2Path
  17. from reportlab.pdfgen.canvas import Canvas
  18. from reportlab.pdfbase import pdfmetrics
  19. from reportlab.pdfbase.ttfonts import TTFont
  20.  
  21. import reportlab.lib.colors
  22. import reportlab.lib.pagesizes
  23. import reportlab.lib.units
  24.  
  25. colors = reportlab.lib.colors
  26. page_sizes = reportlab.lib.pagesizes
  27. units = reportlab.lib.units
  28.  
  29. class HSVColor(colors.Color):
  30.     def __init__(self, h, s, v, a = 1):
  31.         r, g, b = colorsys.hsv_to_rgb(h, s, v)
  32.         colors.Color.__init__(self, r, g, b, a)
  33. #       self.luminance = 0.299 * r + 0.587 * g + 0.114 * b
  34.  
  35.         self.luminance = math.sqrt(0.299 * r**2 + 0.587 * g**2 + 0.114 * b**2)
  36.  
  37. class PageOrientation(Enum):
  38.     Portrait = 0
  39.     Landscape = 1
  40.  
  41. class Direction(Enum):
  42.     Right = 0
  43.     Up = 90
  44.     Left = 180
  45.     Down = 270
  46.  
  47. PageOrientation.Portrait.orient = page_sizes.portrait
  48. PageOrientation.Landscape.orient = page_sizes.landscape
  49.  
  50.  
  51. class PDF(Canvas):
  52.     # shared class-level object
  53.     font_metrics = {}
  54.  
  55.     x = 0
  56.     y = 0
  57.  
  58.     def __init__(self, filename, page_orientation = PageOrientation.Portrait, page_size = page_sizes.letter):
  59.         Canvas.__init__(self, filename = filename, pagesize = page_orientation.orient(page_size))
  60.  
  61.     def register_truetype_font(self, friendlyname, filename):
  62.         pdfmetrics.registerFont(TTFont(friendlyname, filename))
  63.  
  64.     def get_page_width(self):
  65.         return self._pagesize[0]
  66.  
  67.     def get_page_height(self):
  68.         return self._pagesize[1]
  69.  
  70.     def get_font_size(self):
  71.         return self._fontsize
  72.  
  73.     def set_font_size(self, value):
  74.         self.setFont(self.get_font_name(), value)
  75.  
  76.     def get_font_name(self):
  77.         return self._fontname
  78.  
  79.     def set_font_name(self, value):
  80.         self.setFont(value, self.get_font_size())
  81.  
  82.  
  83.     def get_text_bounds(self, text):
  84.         # this function is based on a calculation found in source code of unknown provenance found at
  85.         # http://reportlab-users.reportlab.narkive.com/20CtH5lY/font-size-baseline_height-and-typography-stuff
  86.         return _text2Path(text, fontName = self.get_font_name(), fontSize = self.get_font_size()).getBounds()
  87.  
  88.     '''
  89.         key = (self.get_font_name(), VAlign.ascender)
  90.         if key not in self.font_metrics:
  91.             self.font_metrics[key] =
  92.         return self.get_font_size() * self.font_metrics[key]
  93.     '''
  94.     def get_font_ascender_height(self):
  95.         key = (self.get_font_name(), VAlign.ascender)
  96.         if key not in self.font_metrics:
  97.             self.font_metrics[key] = pdfmetrics.getAscent(self.get_font_name()) / 1000.0
  98.         return self.get_font_size() * self.font_metrics[key]
  99.  
  100.     def get_font_arrow_center_height(self):
  101.         key = (self.get_font_name(), VAlign.arrow_center)
  102.         if key not in self.font_metrics:
  103.             x0, y0, x1, y1 = self.get_text_bounds('→')
  104.             self.font_metrics[key] = 0.5 * (y0 + y1) / self.get_font_size()
  105.         return self.get_font_size() * self.font_metrics[key]
  106.  
  107.     def get_font_x_height(self):
  108.         key = (self.get_font_name(), VAlign.x_height)
  109.         if key not in self.font_metrics:
  110.             x0, y0, x1, y1 = self.get_text_bounds('x')
  111.             self.font_metrics[key] = float(y1) / self.get_font_size()
  112.         return self.get_font_size() * self.font_metrics[key]
  113.  
  114.     def get_font_cap_height(self):
  115.         key = (self.get_font_name(), VAlign.cap)
  116.         if key not in self.font_metrics:
  117.             x0, y0, x1, y1 = self.get_text_bounds('H')
  118.             self.font_metrics[key] = float(y1) / self.get_font_size()
  119.         return self.get_font_size() * self.font_metrics[key]
  120.  
  121.     def get_font_half_cap_height(self):
  122.         return 0.5 * self.get_font_cap_height()
  123.  
  124.     def get_font_baseline_height(self):
  125.         return 0
  126.  
  127.     def get_font_descender_height(self):
  128.         key = (self.get_font_name(), VAlign.descender)
  129.         if key not in self.font_metrics:
  130.             self.font_metrics[key] = pdfmetrics.getDescent(self.get_font_name()) / 1000.0
  131.         return self.get_font_size() * self.font_metrics[key]
  132.  
  133.     def get_font_leading(self):
  134.         return self._leading
  135.  
  136.  
  137.     # draws an optionally-filled rectangle while preserving existing line width, stroke color, and fill color
  138.     # TODO rename this to something more intuitive
  139.     def erase(self, x, y, width, height, fill_color = None, border_color = None, border_width = None):
  140.         self.saveState()
  141.         try:
  142.             if border_width is not None:
  143.                 self.setLineWidth(border_width)
  144.  
  145.             if border_color is not None:
  146.                 self.setStrokeColor(border_color)
  147.  
  148.             if fill_color is not None:
  149.                 self.setFillColor(fill_color)
  150.  
  151.             self.rect(x, y, width, height, fill = fill_color is not None, stroke = border_color is not None)
  152.         finally:
  153.             self.restoreState()
  154.  
  155.  
  156.     def draw_crosshairs(self, x, y):
  157.         # plot crosshairs to check text positioning
  158.         size = 5 # points
  159.  
  160.         self.saveState()
  161.         try:
  162.             self.translate(x, y)
  163.             self.setLineWidth(0)
  164.             self.line(-size, 0, size, 0)
  165.             self.line(0, -size, 0, size)
  166.         finally:
  167.             self.restoreState()
  168.  
  169.  
  170.     def get_text_width(self, text):
  171.         assert isinstance(text, basestring)
  172.         return self.stringWidth(text, self.get_font_name(), self.get_font_size())
  173.  
  174.  
  175.     def draw_dot(self, x, y):
  176.         self.circle(x, y, 0.1)
  177.  
  178.  
  179.     def draw_text(self, x, y, text, halign, valign, angle = 0, outline = False, border = False):
  180.         assert isinstance(text, basestring)
  181.  
  182.         x0, y0, x1, y1 = self.get_text_bounds(text)
  183.  
  184.         width = x1
  185.  
  186.         dx = -halign.dx * width
  187.         dy = -valign.dy(self)
  188.  
  189.         if angle == 0 and not(outline):
  190.             self.drawString(x + dx, y + dy, text)
  191.             if border:
  192.                 self.rect(x + dx + x0, y + dy + y0, x1 - x0, y1 - y0)
  193.         else:
  194.             self.saveState()
  195.             try:
  196.                 if outline:
  197.                     t = self.beginText()
  198.                     t.setTextRenderMode(2)
  199.                     self._code.append(t.getCode())
  200.                 self.translate(x, y)
  201.                 self.rotate(angle)
  202.                 self.drawString(dx, dy, text)
  203.                 if border:
  204.                     self.rect(dx + x0, dy + y0, x1 - x0, y1 - y0)
  205.             finally:
  206.                 self.restoreState()
  207.  
  208.         #self.draw_crosshairs(x, y)
  209.  
  210.  
  211.     # TODO add optional max_width and wordwrap parameters
  212.     def draw_text_block(self, x, y, lines, halign, valign2, fill_color = None, border_color = None, border_width = None):
  213.         assert isinstance(lines, list)
  214.         for line in lines:
  215.             assert isinstance(line, basestring)
  216.  
  217.         width = max([self.get_text_width(line) for line in lines])
  218.  
  219.         dx = -halign.dx * width
  220.  
  221.         height = len(lines) * self._leading
  222.  
  223.         y += valign2.dy * height # y is now the top of the rectangle
  224.  
  225.         if (fill_color is not None) or (border_color is not None):
  226.             self.erase(x + dx, y - height, width, height, fill_color, border_color, border_width)
  227.  
  228.         for line in lines:
  229.             self.draw_text(x, y, line, halign, VAlign.ascender)
  230.             y -= self._leading
  231.  
  232.  
  233.     def draw_polyline(self, xlist, ylist):
  234.         assert len(xlist) == len(ylist)
  235.         path = self.beginPath()
  236.         path.moveTo(xlist[0], ylist[0])
  237.         for i in range(1, len(xlist)):
  238.             path.lineTo(xlist[i], ylist[i])
  239.         self.drawPath(path)
  240.  
  241.  
  242.     def draw_arc(self, xc, yc, r, start_angle, extent):
  243.         self.arc(xc - r, yc - r, xc + r, yc + r, start_angle, extent)
  244.  
  245.  
  246.     # Turtle-like functions
  247.  
  248.     def set_xy(self, x, y):
  249.         self.x = x
  250.         self.y = y
  251.  
  252.  
  253.     def set_dir(self, dir):
  254.         self.dir = dir
  255.  
  256.  
  257.     def forward(self, dist, mark = True):
  258.         x2 = self.x + dist * math.cos(math.radians(self.dir))
  259.         y2 = self.y + dist * math.sin(math.radians(self.dir))
  260.         if mark:
  261.             self.line(self.x, self.y, x2, y2)
  262.         self.set_xy(x2, y2)
  263.  
  264.  
  265.     def turn_left(self, angle = 90):
  266.         self.set_dir(self.dir + angle)
  267.  
  268.  
  269.     def turn_right(self, angle = 90):
  270.         self.set_dir(self.dir - angle)
  271.  
  272.  
  273.     def curve_left(self, radius, angle = 90):
  274.         curve_right(radius, -angle)
  275.         return
  276.  
  277.  
  278.     def curve_right(self, radius, angle = 90):
  279.         cx = self.x + radius * math.cos(math.radians(self.dir - 90))
  280.         cy = self.y + radius * math.sin(math.radians(self.dir - 90))
  281.         #self.draw_crosshairs(cx, cy)
  282.         self.draw_arc(cx, cy, radius, self.dir + 90, -angle)
  283.         end_angle = math.radians(self.dir - angle)
  284.         self.set_xy(cx - radius * math.sin(end_angle),
  285.                     cy + radius * math.cos(end_angle))
  286.         self.set_dir(self.dir - angle)
  287.  
  288.  
  289. class HAlign(Enum):
  290.     left = 0
  291.     center = 1
  292.     right = 2
  293.  
  294. HAlign.left.dx = 0
  295. HAlign.center.dx = 0.5
  296. HAlign.right.dx = 1
  297.  
  298. class VAlign(Enum):
  299.     ascender = 0
  300.     cap = 1
  301.     half_cap = 2
  302.     x_height = 3
  303.     baseline = 4
  304.     descender = 5
  305.     arrow_center = 6
  306.  
  307. VAlign.ascender.dy     = PDF.get_font_ascender_height
  308. VAlign.cap.dy          = PDF.get_font_cap_height
  309. VAlign.half_cap.dy     = PDF.get_font_half_cap_height
  310. VAlign.x_height.dy     = PDF.get_font_x_height
  311. VAlign.baseline.dy     = PDF.get_font_baseline_height
  312. VAlign.descender.dy    = PDF.get_font_descender_height
  313. VAlign.arrow_center.dy = PDF.get_font_arrow_center_height
  314.  
  315. class VAlign2(Enum):
  316.     top = 0
  317.     center = 1
  318.     bottom = 2
  319.  
  320. VAlign2.top.dy = 0
  321. VAlign2.center.dy = 0.5
  322. VAlign2.bottom.dy = 1
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement