more controllable under and strike lines; fix for issue 137 contributed by Tom Alexander @ bitbucket; version-->3.4.30
authorrobin <robin@reportlab.com>
Fri, 23 Mar 2018 16:02:08 +0000
changeset 4389 61a7f0840d00
parent 4388 9f93d62b9f6e
child 4390 bf8c59a72ff3
more controllable under and strike lines; fix for issue 137 contributed by Tom Alexander @ bitbucket; version-->3.4.30
src/reportlab/__init__.py
src/reportlab/lib/rl_accel.py
src/reportlab/lib/styles.py
src/reportlab/platypus/paragraph.py
src/reportlab/platypus/paraparser.py
src/reportlab/rl_settings.py
src/rl_addons/rl_accel/_rl_accel.c
tests/test_paragraphs.py
tests/test_platypus_paragraphs.py
tests/test_rl_accel.py
--- a/src/reportlab/__init__.py	Thu Mar 22 10:18:32 2018 +0000
+++ b/src/reportlab/__init__.py	Fri Mar 23 16:02:08 2018 +0000
@@ -1,9 +1,9 @@
 #Copyright ReportLab Europe Ltd. 2000-2017
 #see license.txt for license details
 __doc__="""The Reportlab PDF generation library."""
-Version = "3.4.29"
+Version = "3.4.30"
 __version__=Version
-__date__='20180322'
+__date__='20180323'
 
 import sys, os
 
--- a/src/reportlab/lib/rl_accel.py	Thu Mar 22 10:18:32 2018 +0000
+++ b/src/reportlab/lib/rl_accel.py	Fri Mar 23 16:02:08 2018 +0000
@@ -320,7 +320,7 @@
         'returns 1 if two ParaFrags map out the same'
         if (hasattr(f,'cbDefn') or hasattr(g,'cbDefn')
                 or hasattr(f,'lineBreak') or hasattr(g,'lineBreak')): return 0
-        for a in ('fontName', 'fontSize', 'textColor', 'rise', 'underline', 'strike', 'link', "backColor"):
+        for a in ('fontName', 'fontSize', 'textColor', 'rise', 'us_lines', 'link', "backColor"):
             if getattr(f,a,None)!=getattr(g,a,None): return 0
         return 1
     _py_funcs['sameFrag'] = sameFrag
--- a/src/reportlab/lib/styles.py	Thu Mar 22 10:18:32 2018 +0000
+++ b/src/reportlab/lib/styles.py	Fri Mar 23 16:02:08 2018 +0000
@@ -26,8 +26,13 @@
 from reportlab.lib.enums import TA_LEFT, TA_CENTER
 from reportlab.lib.fonts import tt2ps
 from reportlab.rl_config import canvas_basefontname as _baseFontName, \
-                                baseUnderlineProportion as _baseUnderlineProportion, \
-                                spaceShrinkage
+                                underlineWidth as _baseUnderlineWidth, \
+                                underlineOffset as _baseUnderlineOffset, \
+                                underlineGap as _baseUnderlineGap, \
+                                strikeWidth as _baseStrikeWidth, \
+                                strikeOffset as _baseStrikeOffset, \
+                                strikeGap as _baseStrikeGap, \
+                                spaceShrinkage, platypus_link_underline
 _baseFontNameB = tt2ps(_baseFontName,1,0)
 _baseFontNameI = tt2ps(_baseFontName,0,1)
 _baseFontNameBI = tt2ps(_baseFontName,1,1)
@@ -130,11 +135,19 @@
                                 #string or object with text and optional fontName, fontSize, textColor & backColor
                                 #dy
         'splitLongWords':1,     #make best efforts to split long words
-        'underlineProportion': _baseUnderlineProportion,    #set to non-zero to get proportional
+        'underlineWidth': _baseUnderlineWidth,  #underline width
         'bulletAnchor': 'start',    #where the bullet is anchored ie start, middle, end or numeric
         'justifyLastLine': 0,   #n allow justification on the last line for more than n words 0 means don't bother
         'justifyBreaks': 0,     #justify lines broken with <br/>
         'spaceShrinkage': spaceShrinkage,   #allow shrinkage of percentage of space to fit on line
+        'strikeWidth': _baseStrikeWidth,    #stroke width
+        'underlineOffset': _baseUnderlineOffset,    #fraction of fontsize to offset underlines
+        'underlineGap': _baseUnderlineGap,      #gap for double/triple underline
+        'strikeOffset': _baseStrikeOffset,  #fraction of fontsize to offset strikethrough
+        'strikeGap': _baseStrikeGap,        #gap for double/triple strike
+        'linkUnderline': platypus_link_underline,
+        #'underlineColor':  None,
+        #'strikeColor': None,
         }
 
 class LineStyle(PropertySet):
--- a/src/reportlab/platypus/paragraph.py	Thu Mar 22 10:18:32 2018 +0000
+++ b/src/reportlab/platypus/paragraph.py	Fri Mar 23 16:02:08 2018 +0000
@@ -7,7 +7,7 @@
 from operator import truth
 from unicodedata import category
 from reportlab.pdfbase.pdfmetrics import stringWidth, getFont, getAscentDescent
-from reportlab.platypus.paraparser import ParaParser, _PCT
+from reportlab.platypus.paraparser import ParaParser, _PCT, _num as _parser_num, _re_us_value
 from reportlab.platypus.flowables import Flowable
 from reportlab.lib.colors import Color
 from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
@@ -15,8 +15,9 @@
 from reportlab.lib.textsplit import wordSplit, ALL_CANNOT_START
 from copy import deepcopy
 from reportlab.lib.abag import ABag
-from reportlab.rl_config import platypus_link_underline, decimalSymbol, _FUZZ, paraFontSizeHeightOffset
-from reportlab.lib.utils import _className, isBytes, unicodeT, bytesT, strTypes
+from reportlab.rl_config import platypus_link_underline, decimalSymbol, _FUZZ, paraFontSizeHeightOffset, \
+                                strikeGap, underlineGap
+from reportlab.lib.utils import _className, isBytes, unicodeT, bytesT, isStr
 from reportlab.lib.rl_accel import sameFrag
 from reportlab import xrange
 import re
@@ -60,6 +61,20 @@
 _wsc_re_split=re.compile('[%s]+'% re.escape(_wsc)).split
 _wsc_end_search=re.compile('[%s]+$'% re.escape(_wsc)).search
 
+def _usConv(s, vMap, default=None):
+    '''convert a strike/underline distance to a number'''
+    if isStr(s):
+        s = s.strip()
+        if s:
+            m = _re_us_value.match(s)
+            if m:
+                return float(m.group(1))*vMap[m.group(2)]
+            else:
+                return _parser_num(s,allowRelative=False)
+        elif default:
+            return default
+    return s
+
 def split(text, delim=None):
     if isBytes(text): text = text.decode('utf8')
     if delim is not None and isBytes(delim): delim = delim.decode('utf8')
@@ -176,7 +191,7 @@
 
 def _getDotsInfo(style):
     dots = style.endDots
-    if isinstance(dots,strTypes):
+    if isStr(dots):
         text = dots
         fontName = style.fontName
         fontSize = style.fontSize
@@ -227,6 +242,10 @@
     ws = getattr(tx,'_wordSpace',0)
     nSpaces = 0
     words = line.words
+    AL = []
+    LL = []
+    us_lines = xs.us_lines
+    links = xs.links
     for i, f in enumerate(words):
         if hasattr(f,'cbDefn'):
             cbDefn = f.cbDefn
@@ -264,59 +283,55 @@
         else:
             cur_x_s = cur_x + nSpaces*ws
             end_x = cur_x_s
+            fontSize = f.fontSize
+            textColor = f.textColor
+            rise = f.rise
             if i > 0:
                 end_x = cur_x_s - _trailingSpaceLength(words[i-1].text, tx)
-            if (tx._fontname,tx._fontsize)!=(f.fontName,f.fontSize):
-                tx._setFont(f.fontName, f.fontSize)
-            if xs.textColor!=f.textColor:
-                xs.textColor = f.textColor
-                tx.setFillColor(f.textColor)
-            if xs.rise!=f.rise:
-                xs.rise=f.rise
-                tx.setRise(f.rise)
+            if (tx._fontname,tx._fontsize)!=(f.fontName,fontSize):
+                tx._setFont(f.fontName, fontSize)
+            if xs.textColor!=textColor:
+                xs.textColor = textColor
+                tx.setFillColor(textColor)
+            if xs.rise!=rise:
+                xs.rise=rise
+                tx.setRise(rise)
             text = f.text
             tx._textOut(text,f is words[-1])    # cheap textOut
-            if not xs.underline and f.underline:
-                xs.underline = 1
-                xs.underline_x = cur_x_s
-                xs.underlineColor = f.textColor
-            elif xs.underline:
-                if not f.underline:
-                    xs.underline = 0
-                    xs.underlines.append( (xs.underline_x, end_x, xs.underlineColor) )
-                    xs.underlineColor = None
-                elif xs.textColor!=xs.underlineColor:
-                    xs.underlines.append( (xs.underline_x, end_x, xs.underlineColor) )
-                    xs.underlineColor = xs.textColor
-                    xs.underline_x = cur_x_s
-            if not xs.strike and f.strike:
-                xs.strike = 1
-                xs.strike_x = cur_x_s
-                xs.strikeColor = f.textColor
-            elif xs.strike:
-                if not f.strike:
-                    xs.strike = 0
-                    xs.strikes.append( (xs.strike_x, end_x, xs.strikeColor) )
-                    xs.strikeColor = None
-                elif xs.textColor!=xs.strikeColor:
-                    xs.strikes.append( (xs.strike_x, end_x, xs.strikeColor) )
-                    xs.strikeColor = xs.textColor
-                    xs.strike_x = cur_x_s
-            if f.link and not xs.link:
-                if not xs.link:
-                    xs.link = f.link
-                    xs.link_x = cur_x_s
-                    xs.linkColor = xs.textColor
-            elif xs.link:
-                if not f.link:
-                    xs.links.append( (xs.link_x, end_x, xs.link, xs.linkColor) )
-                    xs.link = None
-                    xs.linkColor = None
-                elif f.link!=xs.link or xs.textColor!=xs.linkColor:
-                    xs.links.append( (xs.link_x, end_x, xs.link, xs.linkColor) )
-                    xs.link = f.link
-                    xs.link_x = cur_x_s
-                    xs.linkColor = xs.textColor
+            if LL != f.us_lines:
+                S = set(LL)
+                NS = set(f.us_lines)
+                nL = NS - S #new lines
+                eL = S - NS #ending lines
+                for l in eL:
+                    us_lines[l] = us_lines[l],end_x
+                for l in nL:
+                    us_lines[l] = (l,fontSize,textColor,cur_x),fontSize
+                LL = f.us_lines
+            if LL:
+                for l in LL:
+                    l0, fsmax = us_lines[l]
+                    if fontSize>fsmax:
+                        us_lines[l] = l0, fontSize
+
+            nlo = rise - 0.2*fontSize
+            nhi = rise + fontSize
+            if AL != f.link:
+                S = set(AL)
+                NS = set(f.link)
+                nL = NS - S #new linkis
+                eL = S - NS #ending links
+                for l in eL:
+                    links[l] = links[l],end_x
+                for l in nL:
+                    links[l] = (l,cur_x),nlo,nhi
+                AL = f.link
+            if AL:
+                for l in AL:
+                    l0, lo, hi = links[l]
+                    if nlo<lo or nhi>hi:
+                        links[l] = l0,min(nlo,lo),max(nhi,hi)
+
             bg = getattr(f,'backColor',None)
             if bg and not xs.backColor:
                 xs.backColor = bg
@@ -332,15 +347,19 @@
             txtlen = tx._canvas.stringWidth(text, tx._fontname, tx._fontsize)
             cur_x += txtlen
             nSpaces += text.count(' ')+_nbspCount(text)
+
     cur_x_s = cur_x+(nSpaces-1)*ws
     if last and pKind!='right' and xs.style.endDots:
         _do_dots_frag(cur_x,cur_x_s,line.maxWidth,xs,tx)
-    if xs.underline:
-        xs.underlines.append( (xs.underline_x, cur_x_s, xs.underlineColor) )
-    if xs.strike:
-        xs.strikes.append( (xs.strike_x, cur_x_s, xs.strikeColor) )
-    if xs.link:
-        xs.links.append( (xs.link_x, cur_x_s, xs.link,xs.linkColor) )
+
+    if LL:
+        for l in LL:
+            us_lines[l] = us_lines[l], cur_x_s
+
+    if AL:
+        for l in AL:
+            links[l] = links[l], cur_x_s
+
     if xs.backColor:
         xs.backColors.append( (xs.backColor_x, cur_x_s, xs.backColor) )
     if tx._x0!=x0:
@@ -708,7 +727,7 @@
     bulletAnchor = style.bulletAnchor
     if rtl or style.bulletAnchor!='start':
         numeric = bulletAnchor=='numeric'
-        if isinstance(bulletText,strTypes):
+        if isStr(bulletText):
             t =  bulletText
             q = numeric and decimalSymbol in t
             if q: t = t[:t.index(decimalSymbol)]
@@ -738,7 +757,7 @@
         tx2 = canvas.beginText(bulletStart, cur_y)
     tx2.setFont(style.bulletFontName, style.bulletFontSize)
     tx2.setFillColor(getattr(style,'bulletColor',style.textColor))
-    if isinstance(bulletText,strTypes):
+    if isStr(bulletText):
         tx2.textOut(bulletText)
     else:
         for f in bulletText:
@@ -758,7 +777,7 @@
     '''work out bullet width and adjust maxWidths[0] if neccessary
     '''
     if bulletText:
-        if isinstance(bulletText,strTypes):
+        if isStr(bulletText):
             bulletWidth = stringWidth( bulletText, style.bulletFontName, style.bulletFontSize)
         else:
             #it's a list of fragments
@@ -832,23 +851,38 @@
             if j==lim:
                 i += 1
 
-def _old_do_line(tx, x1, y1, x2, y2):
-    tx._canvas.line(x1, y1, x2, y2)
-
-def _do_line(tx, x1, y1, x2, y2):
-    olw = tx._canvas._lineWidth
-    nlw = tx._underlineProportion*tx._fontsize
+def _do_line(tx, x1, y1, x2, y2, nlw, nsc):
+    canv = tx._canvas
+    olw = canv._lineWidth
     if nlw!=olw:
-        tx._canvas.setLineWidth(nlw)
-        tx._canvas.line(x1, y1, x2, y2)
-        tx._canvas.setLineWidth(olw)
-    else:
-        tx._canvas.line(x1, y1, x2, y2)
+        canv.setLineWidth(nlw)
+    osc = canv._strokeColorObj
+    if nsc!=osc:
+        canv.setStrokeColor(nsc)
+    canv.line(x1, y1, x2, y2)
 
-def _do_under_line(i, t_off, ws, tx, lm=-0.125):
-    y = tx.XtraState.cur_y - i*tx.XtraState.style.leading + lm*tx.XtraState.f.fontSize
-    textlen = tx._canvas.stringWidth(' '.join(tx.XtraState.lines[i][1]), tx._fontname, tx._fontsize)
-    tx._do_line(t_off, y, t_off+textlen, y)
+def _do_under_line(i, x1, ws, tx, us_lines):
+    xs = tx.XtraState
+    style = xs.style
+    y0 = xs.cur_y - i*style.leading
+    f = xs.f
+    fs = f.fontSize
+    tc = f.textColor
+    values = dict(L=fs,F=fs,f=fs)
+    dw = tx._defaultLineWidth
+    x2 = x1 + tx._canvas.stringWidth(' '.join(tx.XtraState.lines[i][1]), tx._fontname, fs)
+    for n,k,c,w,o,r,m,g in us_lines:
+        underline = k=='underline'
+        lw = _usConv(w,values,default=tx._defaultLineWidth)
+        lg = _usConv(g,values,default=1)
+        dy = lg+lw
+        if not underline: dy = -dy
+        y = y0 + r + _usConv(('-0.125*L' if underline else '0.25*L') if o=='' else o,values)
+        if not c: c = tc
+        while m>0:
+            tx._do_line(x1, y, x2, y, lw, c)
+            y -= dy
+            m -= 1
 
 _scheme_re = re.compile('^[a-zA-Z][-+a-zA-Z0-9]+$')
 def _doLink(tx,link,rect):
@@ -859,6 +893,7 @@
         if kind=='GoToR': link = parts[1]
         tx._canvas.linkURL(link, rect, relative=1, kind=kind)
     else:
+        if not link: return
         if link[0]=='#':
             link = link[1:]
             scheme=''
@@ -870,59 +905,55 @@
     y = xs.cur_y - i*leading - xs.f.fontSize/8.0 # 8.0 factor copied from para.py
     text = ' '.join(xs.lines[i][1])
     textlen = tx._canvas.stringWidth(text, tx._fontname, tx._fontsize)
-    _doLink(tx, xs.link, (t_off, y, t_off+textlen, y+leading))
+    for n, link in xs.link:
+        _doLink(tx, link, (t_off, y, t_off+textlen, y+leading))
 
 def _do_post_text(tx):
     xs = tx.XtraState
-    leading = xs.style.leading
-    autoLeading = xs.autoLeading
     y0 = xs.cur_y
     f = xs.f
-    ff = 0.125*f.fontSize
-    yl = y0 + f.fontSize
+    leading = xs.style.leading
+    autoLeading = xs.autoLeading
+    fontSize = f.fontSize
     if autoLeading=='max':
-        leading = max(leading,1.2*f.fontSize)
+        leading = max(leading,1.2*fontSize)
     elif autoLeading=='min':
-        leading = 1.2*f.fontSize
-    ydesc = yl - leading
+        leading = 1.2*fontSize
+
+    if xs.backColors:
+        yl = y0 + fontSize
+        ydesc = yl - leading
 
-    for x1,x2,c in xs.backColors:
-        tx._canvas.setFillColor(c)
-        tx._canvas.rect(x1,ydesc,x2-x1,leading,stroke=0,fill=1)
-    xs.backColors=[]
-    xs.backColor=None
+        for x1,x2,c in xs.backColors:
+            tx._canvas.setFillColor(c)
+            tx._canvas.rect(x1,ydesc,x2-x1,leading,stroke=0,fill=1)
+        xs.backColors=[]
+        xs.backColor=None
 
-    y = y0 - ff
-    csc = None
-    for x1,x2,c in xs.underlines:
-        if c!=csc:
-            tx._canvas.setStrokeColor(c)
-            csc = c
-        tx._do_line(x1, y, x2, y)
-    xs.underlines = []
-    xs.underline=0
-    xs.underlineColor=None
+    for (((n,link),x1),lo,hi),x2 in sorted(xs.links.values()):
+        _doLink(tx, link, (x1, y0+lo, x2, y0+hi))
+    xs.links = {}
 
-    ys = y0 + 2*ff
-    for x1,x2,c in xs.strikes:
-        if c!=csc:
-            tx._canvas.setStrokeColor(c)
-            csc = c
-        tx._do_line(x1, ys, x2, ys)
-    xs.strikes = []
-    xs.strike=0
-    xs.strikeColor=None
-
-    for x1,x2,link,c in xs.links:
-        if platypus_link_underline:
-            if c!=csc:
-                tx._canvas.setStrokeColor(c)
-                csc = c
-            tx._do_line(x1, y, x2, y)
-        _doLink(tx, link, (x1, ydesc, x2, yl))
-    xs.links = []
-    xs.link=None
-    xs.linkColor=None
+    if xs.us_lines:
+        #print 'lines'
+        dw = tx._defaultLineWidth
+        values = dict(L=fontSize)
+        for (((n,k,c,w,o,r,m,g),fs,tc,x1),fsmax),x2 in sorted(xs.us_lines.values()):
+            underline = k=='underline'
+            values['f'] = fs
+            values['F'] = fsmax
+            lw = _usConv(w,values,default=tx._defaultLineWidth)
+            lg = _usConv(g,values,default=1)
+            dy = lg+lw
+            if not underline: dy = -dy
+            y = y0 + r + _usConv(o if o!='' else ('-0.125*L' if underline else '0.25*L'),values)
+            #print 'n=%s k=%s x1=%s x2=%s r=%s c=%s w=%r o=%r fs=%r tc=%s y=%s lW=%r offs=%r' % (n,k,x1,x2,r,(c.hexval() if c else ''),w,o,fs,tc.hexval(),y,lW,y-y0-r)
+            if not c: c = tc
+            while m>0:
+                tx._do_line(x1, y, x2, y, lw, c)
+                y -= dy
+                m -= 1
+        xs.us_lines = {}
 
     xs.cur_y -= leading
 
@@ -1102,6 +1133,10 @@
 
         The paragraph Text can contain XML-like markup including the tags:
         <b> ... </b> - bold
+        < u [color="red"] [width="pts"] [offset="pts"]> < /u > - underline
+            width and offset can be empty meaning use existing canvas line width
+            or with an f/F suffix regarded as a fraction of the font size
+        < strike > < /strike > - strike through has the same parameters as underline
         <i> ... </i> - italics
         <u> ... </u> - underline
         <strike> ... </strike> - strike through
@@ -1112,19 +1147,21 @@
         <onDraw name=callable label="a label"/>
         <index [name="callablecanvasattribute"] label="a label"/>
         <link>link text</link>
-        attributes of links
-        size/fontSize=num
-        name/face/fontName=name
-        fg/textColor/color=color
-        backcolor/backColor/bgcolor=color
-        dest/destination/target/href/link=target
+            attributes of links
+                size/fontSize/uwidth/uoffset=num
+                name/face/fontName=name
+                fg/textColor/color/ucolor=color
+                backcolor/backColor/bgcolor=color
+                dest/destination/target/href/link=target
+                underline=bool turn on underline
         <a>anchor text</a>
-        attributes of anchors
-        fontSize=num
-        fontName=name
-        fg/textColor/color=color
-        backcolor/backColor/bgcolor=color
-        href=href
+            attributes of anchors
+                size/fontSize/uwidth/uoffset=num
+                fontName=name
+                fg/textColor/color/ucolor=color
+                backcolor/backColor/bgcolor=color
+                href=href
+                underline="yes|no"
         <a name="anchorpoint"/>
         <unichar name="unicode character name"/>
         <unichar value="unicode code point"/>
@@ -1744,12 +1781,6 @@
                 canvas.setFillColor(f.textColor)
 
                 tx = self.beginText(cur_x, cur_y)
-                if style.underlineProportion:
-                    tx._underlineProportion = style.underlineProportion
-                    tx._do_line = _do_line
-                else:
-                    tx._do_line = _old_do_line
-                tx._do_line = MethodType(tx._do_line,tx)
                 if autoLeading=='max':
                     leading = max(leading,blPara.ascent-blPara.descent)
                 elif autoLeading=='min':
@@ -1766,29 +1797,22 @@
                 if lastLine and jllwc and len(words)>jllwc:
                     lastLine=False
                 t_off = dpl( tx, offset, ws, words, lastLine)
-                if f.underline or f.link or f.strike or style.endDots:
-                    xs = tx.XtraState = ABag()
+                if f.us_lines or f.link or style.endDots:
+                    tx._do_line = MethodType(_do_line,tx)
+                    tx.xs = xs = tx.XtraState = ABag()
+                    tx._defaultLineWidth = canvas._lineWidth
                     xs.cur_y = cur_y
                     xs.f = f
                     xs.style = style
                     xs.lines = lines
-                    xs.underlines=[]
-                    xs.underlineColor=None
-                    xs.strikes=[]
-                    xs.strikeColor=None
-                    xs.links=[]
                     xs.link=f.link
                     xs.textColor = f.textColor
                     xs.backColors = []
-                    canvas.setStrokeColor(f.textColor)
                     dx = t_off+leftIndent
                     if dpl!=_justifyDrawParaLine: ws = 0
-                    underline = f.underline or (f.link and platypus_link_underline)
-                    strike = f.strike
-                    link = f.link
-                    if underline: _do_under_line(0, dx, ws, tx)
-                    if strike: _do_under_line(0, dx, ws, tx, lm=0.125)
-                    if link: _do_link_line(0, dx, ws, tx)
+                    if f.us_lines:
+                        _do_under_line(0, t_off, ws, tx, f.us_lines)
+                    if f.link: _do_link_line(0, dx, ws, tx)
                     if lastLine and style.endDots and dpl!=_rightDrawParaLine: _do_dots(0, dx, ws, xs, tx, dpl)
 
                     #now the middle of the paragraph, aligned with the left margin which is our origin.
@@ -1801,9 +1825,9 @@
                         t_off = dpl( tx, _offsets[i], ws, words, lastLine)
                         dx = t_off+leftIndent
                         if dpl!=_justifyDrawParaLine: ws = 0
-                        if underline: _do_under_line(i, dx, ws, tx)
-                        if strike: _do_under_line(i, dx, ws, tx, lm=0.125)
-                        if link: _do_link_line(i, dx, ws, tx)
+                        if f.us_lines:
+                            _do_under_line(i, t_off, ws, tx, f.us_lines)
+                        if f.link: _do_link_line(i, dx, ws, tx)
                         if lastLine and style.endDots and dpl!=_rightDrawParaLine: _do_dots(i, dx, ws, xs, tx, dpl)
                 else:
                     for i in xrange(1, nLines):
@@ -1839,12 +1863,12 @@
 
                 #set up the font etc.
                 tx = self.beginText(cur_x, cur_y)
-                if style.underlineProportion:
-                    tx._underlineProportion = style.underlineProportion
-                    tx._do_line = _do_line
-                else:
-                    tx._do_line = _old_do_line
-                tx._do_line = MethodType(tx._do_line,tx)
+                tx._defaultLineWidth = canvas._lineWidth
+                tx._underlineWidth = getattr(style,'underlineWidth','')
+                tx._underlineOffset = getattr(style,'underlineOffset','') or '-0.125f'
+                tx._strikeWidth = getattr(style,'strikeWidth','')
+                tx._strikeOffset = getattr(style,'strikeOffset','') or '0.25f'
+                tx._do_line = MethodType(_do_line,tx)
                 # set the paragraph direction
                 tx.direction = self.style.wordWrap
 
@@ -1852,15 +1876,10 @@
                 xs.textColor=None
                 xs.backColor=None
                 xs.rise=0
-                xs.underline=0
-                xs.underlines=[]
-                xs.underlineColor=None
-                xs.strike=0
-                xs.strikes=[]
-                xs.strikeColor=None
                 xs.backColors=[]
-                xs.links=[]
-                xs.link=None
+                xs.us_lines = {}
+                xs.links = {}
+                xs.link={}
                 xs.leading = style.leading
                 xs.leftIndent = leftIndent
                 tx._leading = None
--- a/src/reportlab/platypus/paraparser.py	Thu Mar 22 10:18:32 2018 +0000
+++ b/src/reportlab/platypus/paraparser.py	Fri Mar 23 16:02:08 2018 +0000
@@ -20,6 +20,7 @@
 from reportlab.lib.fonts import tt2ps, ps2tt
 from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
 from reportlab.lib.units import inch,mm,cm,pica
+from reportlab.rl_config import platypus_link_underline
 if isPy3:
     from html.parser import HTMLParser
     from html.entities import name2codepoint
@@ -47,29 +48,18 @@
         except ValueError:
             return float(s)*unit
 
-def _num(s, unit=1, allowRelative=True):
+def _num(s, unit=1, allowRelative=True,
+        _unit_map = {'i':inch,'in':inch,'pt':1,'cm':cm,'mm':mm,'pica':pica },
+        _re_unit = re.compile('^\s*(.*)(i|in|cm|mm|pt|pica)\s*$'),
+        ):
     """Convert a string like '10cm' to an int or float (in points).
        The default unit is point, but optionally you can use other
        default units like mm.
     """
-    if s.endswith('cm'):
-        unit=cm
-        s = s[:-2]
-    if s.endswith('in'):
-        unit=inch
-        s = s[:-2]
-    if s.endswith('pt'):
-        unit=1
-        s = s[:-2]
-    if s.endswith('i'):
-        unit=inch
-        s = s[:-1]
-    if s.endswith('mm'):
-        unit=mm
-        s = s[:-2]
-    if s.endswith('pica'):
-        unit=pica
-        s = s[:-4]
+    m = _re_unit.match(s)
+    if m:
+        unit = _unit_map[m.group(2)]
+        s = m.group(1)
     return _convnum(s,unit,allowRelative)
 
 def _int(s):
@@ -112,21 +102,53 @@
     v = _numpct(getattr(frag,attr),allowRelative=True)
     return (v[1]+frag.fontSize) if isinstance(v,tuple) else v.normalizedValue(frag.fontSize) if isinstance(v,_PCT) else v
 
-class _CheckSup:
-    '''class for syntax checking <sup> attributes
-    if the check succeeds then we always return the string for later evaluation
+class _ExValidate:
+    '''class for syntax checking attributes
     '''
-    def __init__(self,kind):
-        self.kind = kind
-        self.fontSize = 10
+    def __init__(self,tag,attr):
+        self.tag = tag
+        self.attr = attr
+
+    def invalid(self,s):
+        raise ValueError('<%s> invalid value %r for attribute %s' % (self.tag,s,self.attr))
+
+    def validate(self, parser,s):
+        raise ValueError('abstract method called')
+        return s
+
+    def __call__(self, parser, s):
+        try:
+            return self.validate(parser, s)
+        except:
+            self.invalid(s)
 
-    def __call__(self,s):
-        setattr(self,self.kind,s)
-        try:
-            fontSizeNormalize(self,self.kind,None)
-            return s
-        except:
-            raise ValueError('<sup> invalid value %r for attribute %s' % (s,self.kind))
+class _CheckSup(_ExValidate):
+    '''class for syntax checking <sup|sub> attributes
+    if the check succeeds then we always return the string for later evaluation'''
+    def validate(self,parser,s):
+        self.fontSize = parser._stack[-1].fontSize
+        fontSizeNormalize(self,self.attr,'')
+        return s
+
+    def __call__(self, parser, s):
+        setattr(self,self.attr,s)
+        return _ExValidate.__call__(self,parser,s)
+
+_lineRepeats = dict(single=1,double=2,triple=3)
+_re_us_value = re.compile(r'^\s*(.*)\s*\*\s*(P|L|f|F)\s*$')
+class _CheckUS(_ExValidate):
+    '''class for syntax checking <u|strike> width/offset attributes'''
+    def validate(self,parser,s):
+        s = s.strip()
+        if s:
+            m = _re_us_value.match(s)
+            if m:
+                v = float(m.group(1))
+                if m.group(2)=='P':
+                    return parser._stack[0].fontSize*v
+            else:
+                _num(s,allowRelative=False)
+        return s
 
 def _valignpc(s):
     s = s.lower()
@@ -211,7 +233,14 @@
                 'borderradius': ('borderRadius',_num),
                 'texttransform':('textTransform',_textTransformConv),
                 'enddots':('endDots',None),
-                'underlineproportion':('underlineProportion',_num),
+                'underlinewidth':('underlineWidth',_CheckUS('para','underlineWidth')),
+                'underlinecolor':('underlineColor',toColor),
+                'underlineoffset':('underlineOffset',_CheckUS('para','underlineOffset')),
+                'underlinegap':('underlineGap',_CheckUS('para','underlineGap')),
+                'strikewidth':('strikeWidth',_CheckUS('para','strikeWidth')),
+                'strikecolor':('strikeColor',toColor),
+                'strikeoffset':('strikeOffset',_CheckUS('para','strikeOffset')),
+                'strikegap':('strikeGap',_CheckUS('para','strikeGap')),
                 'spaceshrinkage':('spaceShrinkage',_num),
                 }
 
@@ -258,15 +287,15 @@
                 'destination': ('link', None),
                 'target': ('link', None),
                 'href': ('link', None),
+                'ucolor': ('underlineColor', toColor),
+                'uoffset': ('underlineOffset', _CheckUS('link','underlineOffset')),
+                'uwidth': ('underlineWidth', _CheckUS('link','underlineWidth')),
+                'ugap': ('underlineGap', _CheckUS('link','underlineGap')),
+                'underline': ('underline',_bool),
+                'ukind': ('underlineKind',None),
                 }
-_anchorAttrMap = {'fontSize': ('fontSize', _num),
-                'fontName': ('fontName', None),
+_anchorAttrMap = {
                 'name': ('name', None),
-                'fg':   ('textColor', toColor),
-                'color':('textColor', toColor),
-                'backcolor':('backColor',toColor),
-                'bgcolor':('backColor',toColor),
-                'href': ('href', None),
                 }
 _imgAttrMap = {
                 'src': ('src', None),
@@ -281,9 +310,23 @@
                 'format': ('format',None),
                 }
 _supAttrMap = {
-                'rise': ('supr', _CheckSup('rise')),
-                'size': ('sups', _CheckSup('size')),
+                'rise': ('supr', _CheckSup('sup|sub','rise')),
+                'size': ('sups', _CheckSup('sup|sub','size')),
                 }
+_uAttrMap = {
+            'color':('underlineColor', toColor),
+            'width':('underlineWidth', _CheckUS('underline','underlineWidth')),
+            'offset':('underlineOffset', _CheckUS('underline','underlineOffset')),
+            'gap':('underlineGap', _CheckUS('underline','underlineGap')),
+            'kind':('underlineKind',None),
+            }
+_strikeAttrMap = {
+            'color':('strikeColor', toColor),
+            'width':('strikeWidth', _CheckUS('strike','strikeWidth')),
+            'offset':('strikeOffset', _CheckUS('strike','strikeOffset')),
+            'gap':('strikeGap', _CheckUS('strike','strikeGap')),
+            'kind':('strikeKind',None),
+            }
 
 def _addAttributeNames(m):
     K = list(m.keys())
@@ -613,8 +656,10 @@
 # tags:
 #       < /b > - bold
 #       < /i > - italics
-#       < u > < /u > - underline
-#       < strike > < /strike > - strike through
+#       < u [color="red"] [width="pts"] [offset="pts"]> < /u > - underline
+#           width and offset can be empty meaning use existing canvas line width
+#           or with an f/F suffix regarded as a fraction of the font size
+#       < strike > < /strike > - strike through has the same parameters as underline
 #       < super [size="pts"] [rise="pts"]> < /super > - superscript
 #       < sup ="pts"] [rise="pts"]> < /sup > - superscript
 #       < sub ="pts"] [rise="pts"]> < /sub > - subscript
@@ -624,14 +669,15 @@
 #       <onDraw name=callable label="a label"/>
 #       <index [name="callablecanvasattribute"] label="a label"/>
 #       <link>link text</link>
-#           attributes of links 
-#               size/fontSize=num
+#           attributes of links
+#               size/fontSize/uwidth/uoffset=num
 #               name/face/fontName=name
-#               fg/textColor/color=color
+#               fg/textColor/color/ucolor=color
 #               backcolor/backColor/bgcolor=color
 #               dest/destination/target/href/link=target
+#               underline=bool turn on underline
 #       <a>anchor text</a>
-#           attributes of anchors 
+#           attributes of anchors
 #               fontSize=num
 #               fontName=name
 #               fg/textColor/color=color
@@ -698,23 +744,53 @@
     def end_em( self ):
         self._pop('em')
 
+    def _new_line(self,k):
+        frag = self._stack[-1]
+        frag.us_lines = frag.us_lines + [(
+                    self.nlines,
+                    k,
+                    getattr(frag,k+'Color',None),
+                    getattr(frag,k+'Width',self._defaultLineWidths[k]),
+                    getattr(frag,k+'Offset',self._defaultLineOffsets[k]),
+                    frag.rise,
+                    _lineRepeats[getattr(frag,k+'Kind','single')],
+                    getattr(frag,k+'Gap',self._defaultLineGaps[k]),
+                    )]
+        self.nlines += 1
+
     #### underline
     def start_u( self, attributes ):
-        self._push('u',underline=1)
+        A = self.getAttributes(attributes,_uAttrMap)
+        self._push('u',**A)
+        self._new_line('underline')
 
     def end_u( self ):
         self._pop('u')
 
     #### strike
     def start_strike( self, attributes ):
-        self._push('strike',strike=1)
+        A = self.getAttributes(attributes,_strikeAttrMap)
+        self._push('strike',strike=1,**A)
+        self._new_line('strike')
 
     def end_strike( self ):
         self._pop('strike')
 
     #### link
-    def start_link(self, attributes):
-        self._push('link',**self.getAttributes(attributes,_linkAttrMap))
+    def _handle_link(self, tag, attributes):
+        A = self.getAttributes(attributes,_linkAttrMap)
+        underline = A.pop('underline',self._defaultLinkUnderline)
+        A['link'] = self._stack[-1].link + [(
+                        self.nlinks,
+                        A.pop('link','').strip(),
+                        )]
+        self.nlinks += 1
+        self._push(tag,**A)
+        if underline:
+            self._new_line('underline')
+
+    def start_link(self,attributes):
+        self._handle_link('link',attributes)
 
     def end_link(self):
         if self._pop('link').link is None:
@@ -722,9 +798,10 @@
 
     #### anchor
     def start_a(self, attributes):
-        A = self.getAttributes(attributes,_anchorAttrMap)
-        name = A.get('name',None)
-        if name is not None:
+        anchor = 'name' in attributes
+        if anchor:
+            A = self.getAttributes(attributes,_anchorAttrMap)
+            name = A.get('name',None)
             name = name.strip()
             if not name:
                 self._syntax_error('<a name="..."/> anchor variant requires non-blank name')
@@ -732,11 +809,9 @@
                 self._syntax_error('<a name="..."/> anchor variant only allows name attribute')
                 A = dict(name=A['name'])
             A['_selfClosingTag'] = 'anchor'
+            self._push('a',**A)
         else:
-            href = A.get('href','').strip()
-            A['link'] = href    #convert to our link form
-            A.pop('href',None)
-        self._push('a',**A)
+            self._handle_link('a',attributes)
 
     def end_a(self):
         frag = self._stack[-1]
@@ -780,25 +855,25 @@
     #### super script
     def start_super( self, attributes ):
         A = self.getAttributes(attributes,_supAttrMap)
-        A['sup']=1
+        #A['sup']=1
         self._push('super',**A)
+        frag = self._stack[-1]
+        frag.rise += fontSizeNormalize(frag,'supr',frag.fontSize*supFraction)
+        frag.fontSize = fontSizeNormalize(frag,'sups',frag.fontSize-min(sizeDelta,0.2*frag.fontSize))
 
     def end_super( self ):
         self._pop('super')
 
-    def start_sup( self, attributes ):
-        A = self.getAttributes(attributes,_supAttrMap)
-        A['sup']=1
-        self._push('sup',**A)
-
-    def end_sup( self ):
-        self._pop('sup')
+    start_sup = start_super
+    end_sup = end_super
 
     #### sub script
     def start_sub( self, attributes ):
         A = self.getAttributes(attributes,_supAttrMap)
-        A['sub']=1
         self._push('sub',**A)
+        frag = self._stack[-1]
+        frag.rise -= fontSizeNormalize(frag,'supr',frag.fontSize*subFraction)
+        frag.fontSize = fontSizeNormalize(frag,'sups',frag.fontSize-min(sizeDelta,0.2*frag.fontSize))
 
     def end_sub( self ):
         self._pop('sub')
@@ -888,7 +963,7 @@
 
     def start_br(self, attr):
         self._push('br',_selfClosingTag='br',lineBreak=True,text='')
-        
+
     def end_br(self):
         #print('\nend_br called, %d frags in list' % len(self.fragList))
         frag = self._stack[-1]
@@ -907,13 +982,9 @@
 
         # initialize semantic values
         frag = ParaFrag()
-        frag.sub = 0
-        frag.sup = 0
         frag.rise = 0
-        frag.underline = 0
-        frag.strike = 0
         frag.greek = 0
-        frag.link = None
+        frag.link = []
         if bullet:
             frag.fontName, frag.bold, frag.italic = ps2tt(style.bulletFontName)
             frag.fontSize = style.bulletFontSize
@@ -922,6 +993,21 @@
             frag.fontName, frag.bold, frag.italic = ps2tt(style.fontName)
             frag.fontSize = style.fontSize
             frag.textColor = style.textColor
+        frag.us_lines = []
+        self.nlinks = self.nlines = 0
+        self._defaultLineWidths = dict(
+                                    underline = getattr(style,'underlineWidth',''),
+                                    strike = getattr(style,'strikeWidth',''),
+                                    )
+        self._defaultLineOffsets = dict(
+                                    underline = getattr(style,'underlineOffset',''),
+                                    strike = getattr(style,'strikeOffset',''),
+                                    )
+        self._defaultLineGaps = dict(
+                                    underline = getattr(style,'underlineGap',''),
+                                    strike = getattr(style,'strikeGap',''),
+                                    )
+        self._defaultLinkUnderline = getattr(style,'linkUnderline',platypus_link_underline)
         return frag
 
     def start_para(self,attr):
@@ -1041,7 +1127,7 @@
         self._push('ondraw',cbDefn=defn)
         self.handle_data('')
         self._pop('ondraw')
-    start_onDraw=start_ondraw 
+    start_onDraw=start_ondraw
     end_onDraw=end_ondraw=end_seq
 
     def start_index(self,attr):
@@ -1096,7 +1182,10 @@
             if k in attrMap:
                 j = attrMap[k]
                 func = j[1]
-                A[j[0]] = v if func is None else func(v)
+                if func is not None:
+                    #it's a function
+                    v = func(self,v) if isinstance(func,_ExValidate) else func(v)
+                A[j[0]] = v
             else:
                 self._syntax_error('invalid attribute name %s attrMap=%r'% (k,list(sorted(attrMap.keys()))))
         return A
@@ -1144,23 +1233,12 @@
             if data!='': self._syntax_error('No content allowed in %s tag' % frag._selfClosingTag)
             return
         else:
-            # if sub and sup are both on they will cancel each other out
-            if frag.sub == 1 and frag.sup == 1:
-                frag.sub = 0
-                frag.sup = 0
-
-            if frag.sub:
-                frag.rise = -fontSizeNormalize(frag,'supr',frag.fontSize*subFraction)
-                frag.fontSize = fontSizeNormalize(frag,'sups',frag.fontSize-min(sizeDelta,0.2*frag.fontSize))
-            elif frag.sup:
-                frag.rise = fontSizeNormalize(frag,'supr',frag.fontSize*supFraction)
-                frag.fontSize = fontSizeNormalize(frag,'sups',frag.fontSize-min(sizeDelta,0.2*frag.fontSize))
-
+            #get the right parameters for the
             if frag.greek:
                 frag.fontName = 'symbol'
                 data = _greekConvert(data)
 
-        # bold, italic, and underline
+        # bold, italic
         frag.fontName = tt2ps(frag.fontName,frag.bold,frag.italic)
 
         #save our data
@@ -1181,6 +1259,8 @@
 
     def _complete_parse(self):
         "Reset after parsing, to be ready for next paragraph"
+        if self._stack:
+            self._syntax_error('parse ended with %d unclosed tags\n %s' % (len(self._stack),'\n '.join((x.__tag__ for x in reversed(self._stack)))))
         del self._seq
         style = self._style
         del self._style
@@ -1267,7 +1347,7 @@
             start = self.start_unknown
         #call it
         start(attrs or {})
-        
+
     def handle_endtag(self, tag):
         "Called by HTMLParser when a tag ends"
         #find the existing end_tagname method
--- a/src/reportlab/rl_settings.py	Thu Mar 22 10:18:32 2018 +0000
+++ b/src/reportlab/rl_settings.py	Fri Mar 23 16:02:08 2018 +0000
@@ -48,12 +48,17 @@
 T1SearchPath
 TTFSearchPath
 CMapSearchPath
-baseUnderlineProportion
 decimalSymbol
 errorOnDuplicatePageLabelPage
 autoGenerateMissingTTFName
 allowTTFSubsetting
-spaceShrinkage'''.split())
+spaceShrinkage
+underlineWidth
+underlineOffset
+underlineGap
+strikeWidth
+strikeOffset
+strikeGap'''.split())
 
 allowTableBoundsErrors =    1 # set to 0 to die on too large elements in tables in debug (recommend 1 for production use)
 shapeChecking =             1
@@ -103,7 +108,21 @@
 rtlSupport=                 0                       #set to 1 to attempt import of RTL assistance eg fribidi etc etc
 listWrapOnFakeWidth=        1                       #set to 0/False to force platypus.flowables._listWrapOn to report correct widths
                                                     #else it reports minimum(required,available) width
-baseUnderlineProportion=    0.0                     #non-zero for doing font size proportional lines in Paragraph.py
+
+underlineWidth=             ''                      #empty to use canvas strokeWidth or a distance or number*<letter>
+                                                    #   num * <letter> make value proportional to a font size
+                                                    #   P paragraph font size
+                                                    #   L line max font size
+                                                    #   f first use font size
+                                                    #   F max fontsize in the tag
+
+underlineOffset=            '-0.125*F'              #fraction of fontSize from baseline to draw underlines at.
+underlineGap=               '1'                     #gap for double/triple underline
+
+strikeWidth=                ''
+strikeOffset=               '0.25*F'                #fraction of fontSize from baseline to draw strike through at.
+strikeGap=                  '1'                     #gap for double/triple strike
+
                                                     #by default typical value 0.05. may be overridden on a parastyle.
 decimalSymbol=              '.'                     #what we use to align floats numerically
 errorOnDuplicatePageLabelPage= 0                    #if True will cause repeated PageLabel page numbers to raise an error.
--- a/src/rl_addons/rl_accel/_rl_accel.c	Thu Mar 22 10:18:32 2018 +0000
+++ b/src/rl_addons/rl_accel/_rl_accel.c	Fri Mar 23 16:02:08 2018 +0000
@@ -29,7 +29,7 @@
 #ifndef min
 #	define min(a,b) ((a)<(b)?(a):(b))
 #endif
-#define VERSION "0.72"
+#define VERSION "0.73"
 #define MODULE "_rl_accel"
 
 struct module_state	{
@@ -444,7 +444,7 @@
 static PyObject *sameFrag(PyObject *module, PyObject* args)
 {
 	PyObject *f, *g;
-	static char *names[] = {"fontName", "fontSize", "textColor", "rise", "underline", "strike", "link", "backColor", NULL};
+	static char *names[] = {"fontName", "fontSize", "textColor", "rise", "us_lines", "link", "backColor", NULL};
 	int	r=0, t;
 	char **p;
 	if (!PyArg_ParseTuple(args, "OO:sameFrag", &f, &g)) return NULL;
--- a/tests/test_paragraphs.py	Thu Mar 22 10:18:32 2018 +0000
+++ b/tests/test_paragraphs.py	Fri Mar 23 16:02:08 2018 +0000
@@ -253,7 +253,7 @@
             SA(Paragraph('A very much' +50*' longer'+' justified complex <font color="green">paragraph</font> with equals', i and styDotsJCJK or styDotsJ))
             SA(Paragraph('A very much' +50*' longer'+' justified complex <font color="green">indented paragraph</font> with equals', i and istyDotsJCJK or istyDotsJ))
 
-        SA(Paragraph('<para>This image has the image data embedded in the src attribute: <img src="DATA:image/gif;base64,R0lGODdhDwAMAIACAAAAAPf/EiwAAAAADwAMAAACHIyPqcvtCl4ENEQzbc3XnjtV2Dd247mBnva0SwEAOw==" />', styNormal))
+        SA(Paragraph('<para>This image has the image data embedded in the src attribute: <img src="DATA:image/gif;base64,R0lGODdhDwAMAIACAAAAAPf/EiwAAAAADwAMAAACHIyPqcvtCl4ENEQzbc3XnjtV2Dd247mBnva0SwEAOw==" /></para>', styNormal))
         template = SimpleDocTemplate(outputfile('test_paragraphs.pdf'),
                                      showBoundary=1)
         template.build(story,
--- a/tests/test_platypus_paragraphs.py	Thu Mar 22 10:18:32 2018 +0000
+++ b/tests/test_platypus_paragraphs.py	Fri Mar 23 16:02:08 2018 +0000
@@ -120,7 +120,7 @@
         sty.wordWrap = 'CJK'
         p0=Paragraph('ABCDEFGHIJKL]N',sty)
         p1=Paragraph('AB<font color="red">C</font>DEFGHIJKL]N',sty)
-        canv = Canvas('test_platypus_paragraph_cjk3.pdf')
+        canv = Canvas(outputfile('test_platypus_paragraph_cjk3.pdf'))
         ix = len(canv._code)
         aW = pdfmetrics.stringWidth('ABCD','Courier',15)
         w,h=p0.wrap(aW,1000000)
@@ -199,14 +199,17 @@
 should be a pound sign &amp;pound;="&pound;" and this an alpha &amp;alpha;="&alpha;". You can have links in the page <link href="http://www.reportlab.com" color="blue">ReportLab</link> &amp; <a href="http://www.reportlab.org" color="green">ReportLab.org</a>.
 Use scheme "pdf:" to indicate an external PDF link, "http:", "https:" to indicate an external link eg something to open in
 your browser. If an internal link begins with something that looks like a scheme, precede with "document:". Empty hrefs should be allowed ie <a href="">&lt;a href=""&gt;test&lt;/a&gt;</a> should be allowed.
-<u>This text should be underlined.</u>
-<strike>This text should have a strike through it.</strike>
-<span backcolor="yellow"><strike>This text should have a strike through it and be highlighted.</strike></span>
-<span backcolor="yellow"><strike><u>This text should have a strike through it and be highlighted and underlined.</u></strike></span>
-This should be a mailto link <a href="mailto:reportlab-users@lists2.reportlab.com"><font color="blue">reportlab-users at lists2.reportlab.com</font></a>.
-This should be an underlined mailto link <a href="mailto:reportlab-users@lists2.reportlab.com"><font color="blue">reportlab-users at lists2.reportlab.com</font></a>.
-This should be a highlighted mailto link <span backcolor="yellow"><a href="mailto:reportlab-users@lists2.reportlab.com"><font color="blue">reportlab-users at lists2.reportlab.com</font></a></span>.
-This should be a highlighted &amp; undelined mailto link <span backcolor="yellow"><a href="mailto:reportlab-users@lists2.reportlab.com"><u><font color="blue">reportlab-users at lists2.reportlab.com</font></u></a></span>.
+<u>This text should be underlined.</u><br/>
+<strike>This text should have a strike through it.</strike><br/>
+<span backcolor="yellow"><strike>This text should have a strike through it and be highlighted.</strike></span><br/>
+<span backcolor="yellow"><strike><u>This text should have a strike through it and be highlighted and underlined.</u></strike></span><br/>
+This should be a mailto link <a href="mailto:reportlab-users@lists2.reportlab.com"><font color="blue">reportlab-users at lists2.reportlab.com</font></a>.<br/>
+This should be an underlined mailto link <a underline="1" href="mailto:reportlab-users@lists2.reportlab.com"><font color="blue">reportlab-users at lists2.reportlab.com</font></a>.<br/>
+This should be a highlighted mailto link <span backcolor="yellow"><a href="mailto:reportlab-users@lists2.reportlab.com"><font color="blue">reportlab-users at lists2.reportlab.com</font></a></span>.<br/>
+This should be a highlighted &amp; underlined mailto link <span backcolor="yellow"><a underline="1" ucolor="red" uwidth="0.01*F" href="mailto:reportlab-users@lists2.reportlab.com"><font color="blue">reportlab-users at lists2.reportlab.com</font></a></span>.<br/>
+<u offset="-.125*F">Underlined <font size="-1">Underlined</font></u><br/>
+This is A<sup><u>underlined</u></sup> as is A<u><sup>this</sup></u>
+<u color="red">This is A<sup><u>underlined</u></sup> as is A<u><sup>this</sup></u></u>
 '''
         from reportlab.platypus.flowables import ImageAndFlowables, Image
         from reportlab.lib.testutils import testsFolder
@@ -459,6 +462,30 @@
                             t,' '.join((n+1)*['A']),t,text0,t,' '.join((n+1)*['A']),t,text1),
                             style=s))
         a(Paragraph("The jump at the beginning should come here &lt;a name=\"theEnd\"/&gt;<a name=\"theEnd\"/>!",style=normal))
+        a(Paragraph('Underlining <span fontSize="11"><u color="red">A<u color="green">B</u><u color="blue">C</u>D<sup><strike width="0.5" color="magenta">2</strike><sup><u color="darkgreen" width="0.2">3</u></sup></sup></u></span>',normal))
+        a(Paragraph('<para autoLeading="max" spaceAfter="10">this is in 12 <font size=30>this is in 30</font> <u offset="-0.5" width="0.5" color="red"><u offset="-1.5" width="0.5" color="blue">and</u></u> <link underline="1" ucolor="blue" href="http://google.com/">the link box<sup><a color="red" ucolor="green" underline="1" href="https://www.reportlab.com">2</a></sup> is right (twice).</link></para>''',normal))
+        a(Paragraph('<para autoLeading="max" spaceAfter="10">this is in 12 <font size=30>this is in 30</font> and <link underline="1" ucolor="blue" href="http://google.com/">the link box is right.</link></para>''',normal))
+        a(Paragraph('Underlining <u><span color="red">underlined in red? <span color="blue"><u>or blue</u></span> or red again?</span></u>',normal))
+        a(Paragraph('Link <a href="#theEnd" color="blue">jump</a> to end.<br/>Underlined link <a href="#theEnd" underline="1" ucolor="red" color="blue">jump</a> to end!',style=normal))
+        a(Paragraph('<para autoleading=""><u>A</u>. Furthermore, a subset of <font size="14">English sentences</font> interesting on quite\nindependent grounds is not quite equivalent to a stipulation to place\nthe constructions into these various categories. <u>A</u>. We will bring evidence in favor of\nThe following thesis: most of the methodological work in modern\nlinguistics can be defined in such a way as to impose problems of\nphonemic and morphological analysis.</para>',normal))
+        a(Paragraph('<para autoleading=""><u>A</u>. Furthermore, a subset of <font size="14">English sentences</font> interesting on quite<br/><u>A</u>.</para>',normal))
+        a(Paragraph(u"<para>This is a <sup rise=5><span color='red'>sup</span></sup>rise=5.</para>",normal))
+        a(Paragraph('<span fontSize="11"><u color="green"><strike color="blue">AAAAAA</strike></u></span>',normal))
+        a(Paragraph("Underlining &amp; width proportional to first use font size ('f' suffix) <u offset='-0.125*f' width='0.05*f'>underlined <span size=14>underlined</span></u>!",style=normal))
+        a(Paragraph("Underlining &amp; width proportional to first use font size ('F' suffix) <u offset='-0.125*F' width='0.05*F'>underlined <span size=14>underlined</span></u>!",style=normal))
+        a(Paragraph('''<para spaceBefore="10">This is underlined &lt;sup&gt;: a<sup><u><span color="red">sup</span></u></sup></para>''',style=normal))
+        a(Paragraph('''<para spaceBefore="10">This is <u>underlined</u></para>''',style=normal))
+        a(Paragraph('''<para spaceBefore="10">This is <u kind="double">underlined double</u></para>''',style=normal))
+        a(Paragraph('''<para spaceBefore="10">This is <strike>striken</strike></para>''',style=normal))
+        a(Paragraph('''<para spaceBefore="10">This is <strike><u>both</u></strike></para>''',style=normal))
+        a(Paragraph('''<para spaceBefore="10">This is <u width="0.5" offset="-1" kind="double">underlined kind="double"</u></para>''',style=normal))
+        a(Paragraph('''<para spaceBefore="10">This is <u width="0.25" offset="-1" kind="double">double underlined with thinner lines</u></para>''',style=normal))
+        a(Paragraph('''<para spaceBefore="10">This is <u width="0.5" offset="-0.5" color="red">underlined in red</u></para>''',style=normal))
+        a(Paragraph('''<para spaceBefore="10">This is <strike width="0.5" color="red">overstruck in red</strike></para>''',style=normal))
+        a(Paragraph('''<para spaceBefore="10">This is <strike width="0.5" color="red" kind="double">doubly overstruck in red</strike></para>''',style=normal))
+        a(Paragraph('''<para spaceBefore="10">This is <strike width="0.5" offset="0.125*F" color="red" kind="triple" gap="0.5">triply overstruck in red</strike></para>''',style=normal))
+        a(Paragraph('''<para autoLeading="max" spaceAfter="10" spaceBefore="30">this is in 12 <font size="30">this is in 30</font> <u offset="-0.5" width="0.5" color="red"><u offset="-1.5" width="0.5" color="blue">and</u></u> <link underline="1" ucolor="blue" href="http://google.com/">the link box<sup><a color="red" ucolor="green" underline="1" href="https://www.reportlab.com">2</a></sup> is right (twice).</link></para>''',style=normal))
+        a(Paragraph("",style=normal))
         doc = MyDocTemplate(outputfile('test_platypus_paragraphs_ul.pdf'))
         doc.build(story)
 
--- a/tests/test_rl_accel.py	Thu Mar 22 10:18:32 2018 +0000
+++ b/tests/test_rl_accel.py	Fri Mar 23 16:02:08 2018 +0000
@@ -157,9 +157,9 @@
 
         for func,kind in getFuncs('sameFrag'):
             if not func: continue
-            a=ABag(fontName='Helvetica',fontSize=12, textColor="red", rise=0, underline=0, strike=0, link="aaaa")
-            b=ABag(fontName='Helvetica',fontSize=12, textColor="red", rise=0, underline=0, strike=0, link="aaaa")
-            for name in ("fontName", "fontSize", "textColor", "rise", "underline", "strike", "link"):
+            a=ABag(fontName='Helvetica',fontSize=12, textColor="red", rise=0, us_lines=0, link="aaaa")
+            b=ABag(fontName='Helvetica',fontSize=12, textColor="red", rise=0, us_lines=0, link="aaaa")
+            for name in ("fontName", "fontSize", "textColor", "rise", "us_lines", "link"):
                 old = getattr(a,name)
                 assert func(a,b)==1, "%s sameFrag(%s,%s)!=1" % (kind,a,b)
                 assert func(b,a)==1, "%s sameFrag(%s,%s)!=1" % (kind,b,a)