header_footer.py 7.7 KB

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