Same thing as #471, which was closed as expected behaviour. I don't think it is, and I can't reopen that one, so I'm filing it separately with the extra detail I dug up. The README describes the right rule, the code just doesn't implement it.
We hit this while evaluating an upgrade of a floor plan report generator from 1.5.1. The geometry came out fine, but every label was clearly too small against the drawing it belongs to, and I ended up in the same place @blayzen-w did.
The reason this is easy to get wrong (I got it wrong first time round too): the Tf operator really does say 12 for font-size="16", exactly like the README says. But it sits inside a cm matrix that already carries the 0.75, so what actually gets rendered is 9pt.
Repro, only needs svglib + reportlab:
import io, os, re, tempfile
from svglib.svglib import svg2rlg
from reportlab.graphics import renderPDF
from reportlab.pdfgen import canvas
svg = """<svg width="96" height="96" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="48" height="8" fill="black"/>
<text x="10" y="50" font-size="16" font-family="Helvetica">HHHH</text>
</svg>"""
f = tempfile.NamedTemporaryFile("w", suffix=".svg", delete=False, encoding="utf-8")
f.write(svg)
f.close()
d = svg2rlg(f.name)
os.unlink(f.name)
buf = io.BytesIO()
c = canvas.Canvas(buf, pagesize=(d.width, d.height))
c.setPageCompression(0) # so the content stream stays readable
renderPDF.draw(d, c, 0, 0)
c.save()
print("Drawing:", d.width, "x", d.height)
stream = max(re.findall(rb"stream\r?\n(.*?)endstream", buf.getvalue(), re.S), key=len)
for line in stream.decode("latin-1").splitlines():
if line.strip():
print(line.strip())
The bar is 48 units wide and the font-size is 16, so in any renderer the text is a third of the bar.
On 2.1.0 (trimmed to the interesting lines):
Drawing: 72.0 x 72.0
...
.75 0 0 -0.75 0 72 cm
n 0 0 48 8 re f
1 0 0 -1 0 0 cm
BT /F1 12 Tf 14.4 TL ET
BT 1 0 0 1 10 -50 Tm (HHHH) Tj T* ET
The .75 matrix applies to everything below it. The rect comes out 36pt wide, which is right. The text comes out at 12 * 0.75 = 9pt, which isn't. That's a quarter of the bar instead of a third.
Same script on 1.5.1:
Drawing: 96.0 x 96.0
...
1 0 0 -1 0 96 cm
n 0 0 48 8 re f*
1 0 0 -1 0 0 cm
BT /F1 16 Tf 19.2 TL ET
BT 1 0 0 1 10 -50 Tm (HHHH) Tj T* ET
48pt bar, 16pt text.
I rendered the same file in Chrome as well, mostly to check I wasn't just preferring the old behaviour out of habit. Measuring cap height against bar width: Chrome 0.2396, svglib 1.5.1 0.2396, svglib 2.1.0 0.1806. 1.5.1 is pixel identical to the browser, 2.1.0 is off by the 0.75.
Versions I tried:
|
|
| 1.5.1 |
ok |
| 1.6.0 |
ok |
| 2.0.2 |
wrong |
| 2.1.0 |
wrong |
So it arrived with the unit rework in 2.0b1, which matches what @blayzen-w suspected.
In svglib.py at v2.1.0:
# L1837-1838, the viewport matrix already has PX_TO_PT in it
x_scale = (width * PX_TO_PT) / view_box.width if view_box.width else 1
y_scale = (height * PX_TO_PT) / view_box.height if view_box.height else 1
# L2192-2193, and font-size gets it again
fs = cast(float, attrConv.convertLength(fs_attr)) # user units
fs_pt = fs * PX_TO_PT
# L2196, while x/y stay in user units
x, y = self.convert_length_attrs(node, "x", "y", em_base=fs)
fs_pt goes into String.fontSize, and that String sits inside the group carrying the viewport matrix, so it picks up PX_TO_PT a second time. The x/y three lines below stay in user units, which is where the mismatch comes from.
One other thing worth mentioning: the README suggests scaling the returned Drawing by 4/3 to get pre-2.0 sizing back. That doesn't help here, because it scales text and geometry alike and the ratio stays wrong. The only workaround I found is walking the Drawing and scaling String.fontSize on its own.
I did look at fixing it, but I'm not sure which way you'd want it done. Dropping the * PX_TO_PT at L2193 looks like the obvious move, except fs_pt also feeds the stringWidth() calls that build frag_lengths, and those end up in x positions that are in user units, so multi-tspan text would start shifting instead. The other option would be keeping fs_pt for the metrics and dividing it back out where the String is created. Happy to have a go at a PR if you have a preference.
Tested on Python 3.13 / Windows, with reportlab 4.4.6 and 5.0.0.
Same thing as #471, which was closed as expected behaviour. I don't think it is, and I can't reopen that one, so I'm filing it separately with the extra detail I dug up. The README describes the right rule, the code just doesn't implement it.
We hit this while evaluating an upgrade of a floor plan report generator from 1.5.1. The geometry came out fine, but every label was clearly too small against the drawing it belongs to, and I ended up in the same place @blayzen-w did.
The reason this is easy to get wrong (I got it wrong first time round too): the
Tfoperator really does say 12 forfont-size="16", exactly like the README says. But it sits inside acmmatrix that already carries the 0.75, so what actually gets rendered is 9pt.Repro, only needs svglib + reportlab:
The bar is 48 units wide and the font-size is 16, so in any renderer the text is a third of the bar.
On 2.1.0 (trimmed to the interesting lines):
The
.75matrix applies to everything below it. The rect comes out 36pt wide, which is right. The text comes out at 12 * 0.75 = 9pt, which isn't. That's a quarter of the bar instead of a third.Same script on 1.5.1:
48pt bar, 16pt text.
I rendered the same file in Chrome as well, mostly to check I wasn't just preferring the old behaviour out of habit. Measuring cap height against bar width: Chrome 0.2396, svglib 1.5.1 0.2396, svglib 2.1.0 0.1806. 1.5.1 is pixel identical to the browser, 2.1.0 is off by the 0.75.
Versions I tried:
So it arrived with the unit rework in 2.0b1, which matches what @blayzen-w suspected.
In svglib.py at v2.1.0:
fs_ptgoes intoString.fontSize, and that String sits inside the group carrying the viewport matrix, so it picks up PX_TO_PT a second time. Thex/ythree lines below stay in user units, which is where the mismatch comes from.One other thing worth mentioning: the README suggests scaling the returned Drawing by 4/3 to get pre-2.0 sizing back. That doesn't help here, because it scales text and geometry alike and the ratio stays wrong. The only workaround I found is walking the Drawing and scaling
String.fontSizeon its own.I did look at fixing it, but I'm not sure which way you'd want it done. Dropping the
* PX_TO_PTat L2193 looks like the obvious move, exceptfs_ptalso feeds thestringWidth()calls that buildfrag_lengths, and those end up inxpositions that are in user units, so multi-tspan text would start shifting instead. The other option would be keepingfs_ptfor the metrics and dividing it back out where the String is created. Happy to have a go at a PR if you have a preference.Tested on Python 3.13 / Windows, with reportlab 4.4.6 and 5.0.0.