styles.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  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/lib/styles.py
  4. __version__='3.3.0'
  5. __doc__='''Classes for ParagraphStyle and similar things.
  6. A style is a collection of attributes, but with some extra features
  7. to allow 'inheritance' from a parent, and to ensure nobody makes
  8. changes after construction.
  9. ParagraphStyle shows all the attributes available for formatting
  10. paragraphs.
  11. getSampleStyleSheet() returns a stylesheet you can use for initial
  12. development, with a few basic heading and text styles.
  13. '''
  14. __all__=(
  15. 'PropertySet',
  16. 'ParagraphStyle',
  17. 'LineStyle',
  18. 'ListStyle',
  19. 'StyleSheet1',
  20. 'getSampleStyleSheet',
  21. )
  22. from reportlab.lib.colors import white, black
  23. from reportlab.lib.enums import TA_LEFT, TA_CENTER
  24. from reportlab.lib.fonts import tt2ps
  25. from reportlab.rl_config import canvas_basefontname as _baseFontName, \
  26. underlineWidth as _baseUnderlineWidth, \
  27. underlineOffset as _baseUnderlineOffset, \
  28. underlineGap as _baseUnderlineGap, \
  29. strikeWidth as _baseStrikeWidth, \
  30. strikeOffset as _baseStrikeOffset, \
  31. strikeGap as _baseStrikeGap, \
  32. spaceShrinkage as _spaceShrinkage, \
  33. platypus_link_underline as _platypus_link_underline, \
  34. hyphenationLang as _hyphenationLang, \
  35. hyphenationMinWordLength as _hyphenationMinWordLength, \
  36. uriWasteReduce as _uriWasteReduce, \
  37. embeddedHyphenation as _embeddedHyphenation
  38. _baseFontNameB = tt2ps(_baseFontName,1,0)
  39. _baseFontNameI = tt2ps(_baseFontName,0,1)
  40. _baseFontNameBI = tt2ps(_baseFontName,1,1)
  41. ###########################################################
  42. # This class provides an 'instance inheritance'
  43. # mechanism for its descendants, simpler than acquisition
  44. # but not as far-reaching
  45. ###########################################################
  46. class PropertySet:
  47. defaults = {}
  48. def __init__(self, name, parent=None, **kw):
  49. """When initialized, it copies the class defaults;
  50. then takes a copy of the attributes of the parent
  51. if any. All the work is done in init - styles
  52. should cost little to use at runtime."""
  53. # step one - validate the hell out of it
  54. assert 'name' not in self.defaults, "Class Defaults may not contain a 'name' attribute"
  55. assert 'parent' not in self.defaults, "Class Defaults may not contain a 'parent' attribute"
  56. if parent:
  57. assert parent.__class__ == self.__class__, "Parent style %s must have same class as new style %s" % (parent.__class__.__name__,self.__class__.__name__)
  58. #step two
  59. self.name = name
  60. self.parent = parent
  61. self.__dict__.update(self.defaults)
  62. #step two - copy from parent if any. Try to be
  63. # very strict that only keys in class defaults are
  64. # allowed, so they cannot inherit
  65. self.refresh()
  66. self._setKwds(**kw)
  67. def _setKwds(self,**kw):
  68. #step three - copy keywords if any
  69. for key, value in kw.items():
  70. self.__dict__[key] = value
  71. def __repr__(self):
  72. return "<%s '%s'>" % (self.__class__.__name__, self.name)
  73. def refresh(self):
  74. """re-fetches attributes from the parent on demand;
  75. use if you have been hacking the styles. This is
  76. used by __init__"""
  77. if self.parent:
  78. for key, value in self.parent.__dict__.items():
  79. if (key not in ['name','parent']):
  80. self.__dict__[key] = value
  81. def listAttrs(self, indent=''):
  82. print(indent + 'name =', self.name)
  83. print(indent + 'parent =', self.parent)
  84. keylist = list(self.__dict__.keys())
  85. keylist.sort()
  86. keylist.remove('name')
  87. keylist.remove('parent')
  88. for key in keylist:
  89. value = self.__dict__.get(key, None)
  90. print(indent + '%s = %s' % (key, value))
  91. def clone(self, name, parent=None, **kwds):
  92. r = self.__class__(name,parent)
  93. r.__dict__ = self.__dict__.copy()
  94. r.name = name
  95. r.parent = parent is None and self or parent
  96. r._setKwds(**kwds)
  97. return r
  98. class ParagraphStyle(PropertySet):
  99. defaults = {
  100. 'fontName':_baseFontName,
  101. 'fontSize':10,
  102. 'leading':12,
  103. 'leftIndent':0,
  104. 'rightIndent':0,
  105. 'firstLineIndent':0,
  106. 'alignment':TA_LEFT,
  107. 'spaceBefore':0,
  108. 'spaceAfter':0,
  109. 'bulletFontName':_baseFontName,
  110. 'bulletFontSize':10,
  111. 'bulletIndent':0,
  112. #'bulletColor':black,
  113. 'textColor': black,
  114. 'backColor':None,
  115. 'wordWrap':None, #None means do nothing special
  116. #CJK use Chinese Line breaking
  117. #LTR RTL use left to right / right to left
  118. #with support from pyfribi2 if available
  119. 'borderWidth': 0,
  120. 'borderPadding': 0,
  121. 'borderColor': None,
  122. 'borderRadius': None,
  123. 'allowWidows': 1,
  124. 'allowOrphans': 0,
  125. 'textTransform':None, #uppercase lowercase (captitalize not yet) or None or absent
  126. 'endDots':None, #dots on the last line of left/right justified paras
  127. #string or object with text and optional fontName, fontSize, textColor & backColor
  128. #dy
  129. 'splitLongWords':1, #make best efforts to split long words
  130. 'underlineWidth': _baseUnderlineWidth, #underline width default
  131. 'bulletAnchor': 'start', #where the bullet is anchored ie start, middle, end or numeric
  132. 'justifyLastLine': 0, #n allow justification on the last line for more than n words 0 means don't bother
  133. 'justifyBreaks': 0, #justify lines broken with <br/>
  134. 'spaceShrinkage': _spaceShrinkage, #allow shrinkage of percentage of space to fit on line
  135. 'strikeWidth': _baseStrikeWidth, #stroke width default
  136. 'underlineOffset': _baseUnderlineOffset, #fraction of fontsize to offset underlines
  137. 'underlineGap': _baseUnderlineGap, #gap for double/triple underline
  138. 'strikeOffset': _baseStrikeOffset, #fraction of fontsize to offset strikethrough
  139. 'strikeGap': _baseStrikeGap, #gap for double/triple strike
  140. 'linkUnderline': _platypus_link_underline,
  141. 'underlineColor': None,
  142. 'strikeColor': None,
  143. 'hyphenationLang': _hyphenationLang,
  144. #'hyphenationMinWordLength': _hyphenationMinWordLength,
  145. 'embeddedHyphenation': _embeddedHyphenation,
  146. 'uriWasteReduce': _uriWasteReduce,
  147. }
  148. class LineStyle(PropertySet):
  149. defaults = {
  150. 'width':1,
  151. 'color': black
  152. }
  153. def prepareCanvas(self, canvas):
  154. """You can ask a LineStyle to set up the canvas for drawing
  155. the lines."""
  156. canvas.setLineWidth(1)
  157. #etc. etc.
  158. class ListStyle(PropertySet):
  159. defaults = dict(
  160. leftIndent=18,
  161. rightIndent=0,
  162. bulletAlign='left',
  163. bulletType='1',
  164. bulletColor=black,
  165. bulletFontName='Helvetica',
  166. bulletFontSize=12,
  167. bulletOffsetY=0,
  168. bulletDedent='auto',
  169. bulletDir='ltr',
  170. bulletFormat=None,
  171. start=None, #starting value for a list; if a list then the start sequence
  172. )
  173. _stylesheet1_undefined = object()
  174. class StyleSheet1:
  175. """
  176. This may or may not be used. The idea is to:
  177. 1. slightly simplify construction of stylesheets;
  178. 2. enforce rules to validate styles when added
  179. (e.g. we may choose to disallow having both
  180. 'heading1' and 'Heading1' - actual rules are
  181. open to discussion);
  182. 3. allow aliases and alternate style lookup
  183. mechanisms
  184. 4. Have a place to hang style-manipulation
  185. methods (save, load, maybe support a GUI
  186. editor)
  187. Access is via getitem, so they can be
  188. compatible with plain old dictionaries.
  189. """
  190. def __init__(self):
  191. self.byName = {}
  192. self.byAlias = {}
  193. def __getitem__(self, key):
  194. try:
  195. return self.byAlias[key]
  196. except KeyError:
  197. try:
  198. return self.byName[key]
  199. except KeyError:
  200. raise KeyError("Style '%s' not found in stylesheet" % key)
  201. def get(self,key,default=_stylesheet1_undefined):
  202. try:
  203. return self[key]
  204. except KeyError:
  205. if default!=_stylesheet1_undefined: return default
  206. raise
  207. def __contains__(self, key):
  208. return key in self.byAlias or key in self.byName
  209. def has_key(self,key):
  210. return key in self
  211. def add(self, style, alias=None):
  212. key = style.name
  213. if key in self.byName:
  214. raise KeyError("Style '%s' already defined in stylesheet" % key)
  215. if key in self.byAlias:
  216. raise KeyError("Style name '%s' is already an alias in stylesheet" % key)
  217. if alias:
  218. if alias in self.byName:
  219. raise KeyError("Style '%s' already defined in stylesheet" % alias)
  220. if alias in self.byAlias:
  221. raise KeyError("Alias name '%s' is already an alias in stylesheet" % alias)
  222. #passed all tests? OK, add it
  223. self.byName[key] = style
  224. if alias:
  225. self.byAlias[alias] = style
  226. def list(self):
  227. styles = list(self.byName.items())
  228. styles.sort()
  229. alii = {}
  230. for (alias, style) in list(self.byAlias.items()):
  231. alii[style] = alias
  232. for (name, style) in styles:
  233. alias = alii.get(style, None)
  234. print(name, alias)
  235. style.listAttrs(' ')
  236. print()
  237. def testStyles():
  238. pNormal = ParagraphStyle('Normal',None)
  239. pNormal.fontName = _baseFontName
  240. pNormal.fontSize = 12
  241. pNormal.leading = 14.4
  242. pNormal.listAttrs()
  243. print()
  244. pPre = ParagraphStyle('Literal', pNormal)
  245. pPre.fontName = 'Courier'
  246. pPre.listAttrs()
  247. return pNormal, pPre
  248. def getSampleStyleSheet():
  249. """Returns a stylesheet object"""
  250. stylesheet = StyleSheet1()
  251. stylesheet.add(ParagraphStyle(name='Normal',
  252. fontName=_baseFontName,
  253. fontSize=10,
  254. leading=12)
  255. )
  256. stylesheet.add(ParagraphStyle(name='BodyText',
  257. parent=stylesheet['Normal'],
  258. spaceBefore=6)
  259. )
  260. stylesheet.add(ParagraphStyle(name='Italic',
  261. parent=stylesheet['BodyText'],
  262. fontName = _baseFontNameI)
  263. )
  264. stylesheet.add(ParagraphStyle(name='Heading1',
  265. parent=stylesheet['Normal'],
  266. fontName = _baseFontNameB,
  267. fontSize=18,
  268. leading=22,
  269. spaceAfter=6),
  270. alias='h1')
  271. stylesheet.add(ParagraphStyle(name='Title',
  272. parent=stylesheet['Normal'],
  273. fontName = _baseFontNameB,
  274. fontSize=18,
  275. leading=22,
  276. alignment=TA_CENTER,
  277. spaceAfter=6),
  278. alias='title')
  279. stylesheet.add(ParagraphStyle(name='Heading2',
  280. parent=stylesheet['Normal'],
  281. fontName = _baseFontNameB,
  282. fontSize=14,
  283. leading=18,
  284. spaceBefore=12,
  285. spaceAfter=6),
  286. alias='h2')
  287. stylesheet.add(ParagraphStyle(name='Heading3',
  288. parent=stylesheet['Normal'],
  289. fontName = _baseFontNameBI,
  290. fontSize=12,
  291. leading=14,
  292. spaceBefore=12,
  293. spaceAfter=6),
  294. alias='h3')
  295. stylesheet.add(ParagraphStyle(name='Heading4',
  296. parent=stylesheet['Normal'],
  297. fontName = _baseFontNameBI,
  298. fontSize=10,
  299. leading=12,
  300. spaceBefore=10,
  301. spaceAfter=4),
  302. alias='h4')
  303. stylesheet.add(ParagraphStyle(name='Heading5',
  304. parent=stylesheet['Normal'],
  305. fontName = _baseFontNameB,
  306. fontSize=9,
  307. leading=10.8,
  308. spaceBefore=8,
  309. spaceAfter=4),
  310. alias='h5')
  311. stylesheet.add(ParagraphStyle(name='Heading6',
  312. parent=stylesheet['Normal'],
  313. fontName = _baseFontNameB,
  314. fontSize=7,
  315. leading=8.4,
  316. spaceBefore=6,
  317. spaceAfter=2),
  318. alias='h6')
  319. stylesheet.add(ParagraphStyle(name='Bullet',
  320. parent=stylesheet['Normal'],
  321. firstLineIndent=0,
  322. spaceBefore=3),
  323. alias='bu')
  324. stylesheet.add(ParagraphStyle(name='Definition',
  325. parent=stylesheet['Normal'],
  326. firstLineIndent=0,
  327. leftIndent=36,
  328. bulletIndent=0,
  329. spaceBefore=6,
  330. bulletFontName=_baseFontNameBI),
  331. alias='df')
  332. stylesheet.add(ParagraphStyle(name='Code',
  333. parent=stylesheet['Normal'],
  334. fontName='Courier',
  335. fontSize=8,
  336. leading=8.8,
  337. firstLineIndent=0,
  338. leftIndent=36,
  339. hyphenationLang=''))
  340. stylesheet.add(ListStyle(name='UnorderedList',
  341. parent=None,
  342. leftIndent=18,
  343. rightIndent=0,
  344. bulletAlign='left',
  345. bulletType='1',
  346. bulletColor=black,
  347. bulletFontName='Helvetica',
  348. bulletFontSize=12,
  349. bulletOffsetY=0,
  350. bulletDedent='auto',
  351. bulletDir='ltr',
  352. bulletFormat=None,
  353. #start='circle square blackstar sparkle disc diamond'.split(),
  354. start=None,
  355. ),
  356. alias='ul')
  357. stylesheet.add(ListStyle(name='OrderedList',
  358. parent=None,
  359. leftIndent=18,
  360. rightIndent=0,
  361. bulletAlign='left',
  362. bulletType='1',
  363. bulletColor=black,
  364. bulletFontName='Helvetica',
  365. bulletFontSize=12,
  366. bulletOffsetY=0,
  367. bulletDedent='auto',
  368. bulletDir='ltr',
  369. bulletFormat=None,
  370. #start='1 a A i I'.split(),
  371. start=None,
  372. ),
  373. alias='ol')
  374. return stylesheet