header_footer.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. from __future__ import absolute_import
  2. # Copyright (c) 2010-2019 openpyxl
  3. # Simplified implementation of headers and footers: let worksheets have separate items
  4. import re
  5. from warnings import warn
  6. from openpyxl.descriptors import (
  7. Alias,
  8. Bool,
  9. Strict,
  10. String,
  11. Integer,
  12. MatchPattern,
  13. Typed,
  14. )
  15. from openpyxl.descriptors.serialisable import Serialisable
  16. from openpyxl.compat import unicode
  17. from openpyxl.xml.functions import Element
  18. from openpyxl.utils.escape import escape, unescape
  19. FONT_PATTERN = '&"(?P<font>.+)"'
  20. COLOR_PATTERN = "&K(?P<color>[A-F0-9]{6})"
  21. SIZE_REGEX = r"&(?P<size>\d+\s?)"
  22. FORMAT_REGEX = re.compile("{0}|{1}|{2}".format(FONT_PATTERN, COLOR_PATTERN,
  23. SIZE_REGEX)
  24. )
  25. def _split_string(text):
  26. """
  27. Split the combined (decoded) string into left, center and right parts
  28. # See http://stackoverflow.com/questions/27711175/regex-with-multiple-optional-groups for discussion
  29. """
  30. ITEM_REGEX = re.compile("""
  31. (&L(?P<left>.+?))?
  32. (&C(?P<center>.+?))?
  33. (&R(?P<right>.+?))?
  34. $""", re.VERBOSE | re.DOTALL)
  35. m = ITEM_REGEX.match(text)
  36. try:
  37. parts = m.groupdict()
  38. except AttributeError:
  39. warn("""Cannot parse header or footer so it will be ignored""")
  40. parts = {'left':'', 'right':'', 'center':''}
  41. return parts
  42. class _HeaderFooterPart(Strict):
  43. """
  44. Individual left/center/right header/footer part
  45. Do not use directly.
  46. Header & Footer ampersand codes:
  47. * &A Inserts the worksheet name
  48. * &B Toggles bold
  49. * &D or &[Date] Inserts the current date
  50. * &E Toggles double-underline
  51. * &F or &[File] Inserts the workbook name
  52. * &I Toggles italic
  53. * &N or &[Pages] Inserts the total page count
  54. * &S Toggles strikethrough
  55. * &T Inserts the current time
  56. * &[Tab] Inserts the worksheet name
  57. * &U Toggles underline
  58. * &X Toggles superscript
  59. * &Y Toggles subscript
  60. * &P or &[Page] Inserts the current page number
  61. * &P+n Inserts the page number incremented by n
  62. * &P-n Inserts the page number decremented by n
  63. * &[Path] Inserts the workbook path
  64. * && Escapes the ampersand character
  65. * &"fontname" Selects the named font
  66. * &nn Selects the specified 2-digit font point size
  67. Colours are in RGB Hex
  68. """
  69. text = String(allow_none=True)
  70. font = String(allow_none=True)
  71. size = Integer(allow_none=True)
  72. RGB = ("^[A-Fa-f0-9]{6}$")
  73. color = MatchPattern(allow_none=True, pattern=RGB)
  74. def __init__(self, text=None, font=None, size=None, color=None):
  75. self.text = text
  76. self.font = font
  77. self.size = size
  78. self.color = color
  79. def __str__(self):
  80. """
  81. Convert to Excel HeaderFooter miniformat minus position
  82. """
  83. fmt = []
  84. if self.font:
  85. fmt.append(u'&"{0}"'.format(self.font))
  86. if self.size:
  87. fmt.append("&{0} ".format(self.size))
  88. if self.color:
  89. fmt.append("&K{0}".format(self.color))
  90. return u"".join(fmt + [self.text])
  91. def __bool__(self):
  92. return bool(self.text)
  93. __nonzero__ = __bool__
  94. @classmethod
  95. def from_str(cls, text):
  96. """
  97. Convert from miniformat to object
  98. """
  99. keys = ('font', 'color', 'size')
  100. kw = dict((k, v) for match in FORMAT_REGEX.findall(text)
  101. for k, v in zip(keys, match) if v)
  102. kw['text'] = FORMAT_REGEX.sub('', text)
  103. return cls(**kw)
  104. class HeaderFooterItem(Strict):
  105. """
  106. Header or footer item
  107. """
  108. left = Typed(expected_type=_HeaderFooterPart)
  109. center = Typed(expected_type=_HeaderFooterPart)
  110. centre = Alias("center")
  111. right = Typed(expected_type=_HeaderFooterPart)
  112. __keys = ('L', 'C', 'R')
  113. def __init__(self, left=None, right=None, center=None):
  114. if left is None:
  115. left = _HeaderFooterPart()
  116. self.left = left
  117. if center is None:
  118. center = _HeaderFooterPart()
  119. self.center = center
  120. if right is None:
  121. right = _HeaderFooterPart()
  122. self.right = right
  123. def __str__(self):
  124. """
  125. Pack parts into a single string
  126. """
  127. TRANSFORM = {'&[Tab]': '&A', '&[Pages]': '&N', '&[Date]': '&D',
  128. '&[Path]': '&Z', '&[Page]': '&P', '&[Time]': '&T', '&[File]': '&F',
  129. '&[Picture]': '&G'}
  130. # escape keys and create regex
  131. SUBS_REGEX = re.compile("|".join(["({0})".format(re.escape(k))
  132. for k in TRANSFORM]))
  133. def replace(match):
  134. """
  135. Callback for re.sub
  136. Replace expanded control with mini-format equivalent
  137. """
  138. sub = match.group(0)
  139. return TRANSFORM[sub]
  140. txt = []
  141. for key, part in zip(
  142. self.__keys, [self.left, self.center, self.right]):
  143. if part.text is not None:
  144. txt.append(u"&{0}{1}".format(key, unicode(part)))
  145. txt = "".join(txt)
  146. txt = SUBS_REGEX.sub(replace, txt)
  147. return escape(txt)
  148. def __bool__(self):
  149. return any([self.left, self.center, self.right])
  150. __nonzero__ = __bool__
  151. def to_tree(self, tagname):
  152. """
  153. Return as XML node
  154. """
  155. el = Element(tagname)
  156. el.text = unicode(self)
  157. return el
  158. @classmethod
  159. def from_tree(cls, node):
  160. if node.text:
  161. text = unescape(node.text)
  162. parts = _split_string(text)
  163. for k, v in parts.items():
  164. if v is not None:
  165. parts[k] = _HeaderFooterPart.from_str(v)
  166. self = cls(**parts)
  167. return self
  168. class HeaderFooter(Serialisable):
  169. tagname = "headerFooter"
  170. differentOddEven = Bool(allow_none=True)
  171. differentFirst = Bool(allow_none=True)
  172. scaleWithDoc = Bool(allow_none=True)
  173. alignWithMargins = Bool(allow_none=True)
  174. oddHeader = Typed(expected_type=HeaderFooterItem, allow_none=True)
  175. oddFooter = Typed(expected_type=HeaderFooterItem, allow_none=True)
  176. evenHeader = Typed(expected_type=HeaderFooterItem, allow_none=True)
  177. evenFooter = Typed(expected_type=HeaderFooterItem, allow_none=True)
  178. firstHeader = Typed(expected_type=HeaderFooterItem, allow_none=True)
  179. firstFooter = Typed(expected_type=HeaderFooterItem, allow_none=True)
  180. __elements__ = ("oddHeader", "oddFooter", "evenHeader", "evenFooter", "firstHeader", "firstFooter")
  181. def __init__(self,
  182. differentOddEven=None,
  183. differentFirst=None,
  184. scaleWithDoc=None,
  185. alignWithMargins=None,
  186. oddHeader=None,
  187. oddFooter=None,
  188. evenHeader=None,
  189. evenFooter=None,
  190. firstHeader=None,
  191. firstFooter=None,
  192. ):
  193. self.differentOddEven = differentOddEven
  194. self.differentFirst = differentFirst
  195. self.scaleWithDoc = scaleWithDoc
  196. self.alignWithMargins = alignWithMargins
  197. if oddHeader is None:
  198. oddHeader = HeaderFooterItem()
  199. self.oddHeader = oddHeader
  200. if oddFooter is None:
  201. oddFooter = HeaderFooterItem()
  202. self.oddFooter = oddFooter
  203. if evenHeader is None:
  204. evenHeader = HeaderFooterItem()
  205. self.evenHeader = evenHeader
  206. if evenFooter is None:
  207. evenFooter = HeaderFooterItem()
  208. self.evenFooter = evenFooter
  209. if firstHeader is None:
  210. firstHeader = HeaderFooterItem()
  211. self.firstHeader = firstHeader
  212. if firstFooter is None:
  213. firstFooter = HeaderFooterItem()
  214. self.firstFooter = firstFooter
  215. def __bool__(self):
  216. parts = [getattr(self, attr) for attr in self.__attrs__ + self.__elements__]
  217. return any(parts)
  218. __nonzero__ = __bool__