Skip to content

Commit 75e688a

Browse files
committed
fix(odf): scale drawn text font size with report scale factor
When a graphical report applies a "scale tree to fit" factor, ODT box text now renders at the same scaled font size as PDF/cairo output instead of staying at the original unscaled size and overflowing the shrunk boxes. The ODF backend moved text-style resolution from init time (when the sheet was unscaled) to draw time: drawn text now emits a style override when the current font size differs from the pre-written named style, enabling descent-tree and similar reports to scale their box text in ODT parity with the PDF path. Unscaled output stays byte-identical. Two edge cases (empty paragraph-style name, style-name collision) are guarded by regression tests. Fixes #5733 Signed-off-by: Eduard Ralph <eduard@ralphovi.net>
1 parent 0d9e148 commit 75e688a

3 files changed

Lines changed: 363 additions & 33 deletions

File tree

gramps/plugins/docgen/odfdoc.py

Lines changed: 128 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,15 @@ def __init__(self, styles, ftype, uistate=None):
464464
self.first_page = 1
465465
self.stylelist_notes = [] # styles to create for styled notes.
466466
self.stylelist_photos = [] # styles to create for clipped images.
467+
# bug #5733: init() writes the named "F<name>" draw-text styles up
468+
# front from the (then unscaled) style sheet. A graphical report
469+
# ("scale tree to fit") scales the style sheet's fonts AFTER init(),
470+
# so a drawn text span must be able to override to the current
471+
# (scaled) size at draw time — see _scaled_text_style().
472+
self._draw_text_props = {} # para style name -> init "F<name>" props
473+
self._draw_text_style_names = set() # every "F<name>" init() wrote
474+
self._scaled_text_styles = {} # props -> registered override name
475+
self._scaled_text_defs = [] # (name, props) overrides to serialize
467476

468477
def open(self, filename):
469478
"""
@@ -648,40 +657,18 @@ def init(self):
648657
+ "</style:style>\n"
649658
)
650659

660+
# bug #5733: remember the properties this "F<name>" text style is
661+
# written with (from the CURRENT, still-unscaled font) so a later
662+
# scaled draw can tell whether the font changed and, if so, emit an
663+
# override instead of the stale named style (see _scaled_text_style).
664+
self._draw_text_props[style_name] = self._draw_text_properties(style)
665+
self._draw_text_style_names.add("F%s" % style_name)
651666
wrt(
652667
'<style:style style:name="F%s" ' % style_name
653668
+ 'style:family="text">\n'
654669
+ "<style:text-properties "
655-
)
656-
657-
align = style.get_alignment()
658-
if align == PARA_ALIGN_LEFT:
659-
wrt('fo:text-align="start" ')
660-
elif align == PARA_ALIGN_RIGHT:
661-
wrt('fo:text-align="end" ')
662-
elif align == PARA_ALIGN_CENTER:
663-
wrt('fo:text-align="center" ' 'style:justify-single-word="false" ')
664-
665-
font = style.get_font()
666-
wrt(
667-
'style:font-name="%s" '
668-
% (
669-
"Arial"
670-
if font.get_type_face() == FONT_SANS_SERIF
671-
else "Times New Roman"
672-
)
673-
)
674-
675-
color = font.get_color()
676-
wrt('fo:color="#%02x%02x%02x" ' % color)
677-
if font.get_bold():
678-
wrt('fo:font-weight="bold" ')
679-
if font.get_italic():
680-
wrt('fo:font-style="italic" ')
681-
682-
wrt(
683-
'fo:font-size="%.2fpt" ' % font.get_size()
684-
+ 'style:font-size-asian="%.2fpt"/> ' % font.get_size()
670+
+ self._draw_text_props[style_name]
671+
+ "/> "
685672
+ "</style:style>\n"
686673
)
687674

@@ -782,6 +769,7 @@ def finish_cntnt_creation(self):
782769
self.add_styled_notes_fonts()
783770
self.add_styled_notes_styles()
784771
self.add_styled_photo_styles()
772+
self.add_scaled_text_styles() # bug #5733: scaled draw-text overrides
785773
self.cntntx.write(self.cntnt1.getvalue())
786774
self.cntntx.write(self.cntnt2.getvalue())
787775
self.cntntx.write(self.cntnt.getvalue())
@@ -1858,6 +1846,110 @@ def draw_line(self, style, x1, y1, x2, y2):
18581846
+ "</draw:line>\n"
18591847
)
18601848

1849+
def _draw_text_properties(self, style):
1850+
"""
1851+
Return the body of the ``<style:text-properties …>`` element for a
1852+
paragraph style's draw-text ("F<name>") style, computed from the
1853+
style's CURRENT font.
1854+
1855+
Shared by :meth:`init` — which writes the named "F<name>" styles once,
1856+
up front — and :meth:`_scaled_text_style`, which runs later, after a
1857+
graphical report may have scaled the style sheet's fonts (bug #5733).
1858+
Because both paths format the font identically, an unscaled draw
1859+
reproduces the named style byte-for-byte, so :meth:`_scaled_text_style`
1860+
can reuse "F<name>" when nothing changed and register an override only
1861+
when the (scaled) font differs.
1862+
"""
1863+
props = ""
1864+
align = style.get_alignment()
1865+
if align == PARA_ALIGN_LEFT:
1866+
props += 'fo:text-align="start" '
1867+
elif align == PARA_ALIGN_RIGHT:
1868+
props += 'fo:text-align="end" '
1869+
elif align == PARA_ALIGN_CENTER:
1870+
props += 'fo:text-align="center" ' 'style:justify-single-word="false" '
1871+
font = style.get_font()
1872+
props += 'style:font-name="%s" ' % (
1873+
"Arial" if font.get_type_face() == FONT_SANS_SERIF else "Times New Roman"
1874+
)
1875+
props += 'fo:color="#%02x%02x%02x" ' % font.get_color()
1876+
if font.get_bold():
1877+
props += 'fo:font-weight="bold" '
1878+
if font.get_italic():
1879+
props += 'fo:font-style="italic" '
1880+
props += 'fo:font-size="%.2fpt" ' % font.get_size()
1881+
props += 'style:font-size-asian="%.2fpt"' % font.get_size()
1882+
return props
1883+
1884+
def _scaled_text_style(self, para_name):
1885+
"""
1886+
Return the name of the text style a drawn text span must reference so
1887+
that its font size reflects any per-report scaling applied AFTER
1888+
:meth:`init` wrote the fixed named "F<name>" styles (bug #5733).
1889+
1890+
"F<para_name>" is written once, up front, from the unscaled style
1891+
sheet. A graphical report ("scale tree to fit") then scales the style
1892+
sheet's fonts, but that pre-written style keeps the original size, so
1893+
ODT box text would render unscaled while the cairo (PDF) backend —
1894+
which reads the font at draw time — scales it. Here we read the
1895+
CURRENT font (post-scale): if it matches what "F<name>" already holds,
1896+
we reuse "F<name>" (unscaled output stays byte-identical); otherwise we
1897+
register an automatic text style carrying the current size and return
1898+
its name (flushed by :meth:`add_scaled_text_styles`).
1899+
"""
1900+
if para_name not in self._draw_text_props:
1901+
# No named "F<name>" text style was written for this paragraph
1902+
# style — e.g. a GraphicsStyle with an unset paragraph style, whose
1903+
# name defaults to "". Preserve the original, pre-#5733 behaviour
1904+
# (emit the "F<name>" reference verbatim) rather than resolving the
1905+
# style sheet and risking a KeyError.
1906+
return "F%s" % para_name
1907+
style = self.get_style_sheet().get_paragraph_style(para_name)
1908+
props = self._draw_text_properties(style)
1909+
if props == self._draw_text_props[para_name]:
1910+
return "F%s" % para_name
1911+
name = self._scaled_text_styles.get(props)
1912+
if name is None:
1913+
name = self._new_scaled_style_name()
1914+
self._scaled_text_styles[props] = name
1915+
self._scaled_text_defs.append((name, props))
1916+
return name
1917+
1918+
def _new_scaled_style_name(self):
1919+
"""
1920+
Allocate a text-style name for a scaled draw-text override that cannot
1921+
collide with any "F<name>" style :meth:`init` wrote nor any override
1922+
already registered (bug #5733). A fixed prefix alone is unsafe: a user
1923+
paragraph style literally named "Scaled1" yields an init style
1924+
"FScaled1", so we skip any candidate already reserved.
1925+
"""
1926+
n = len(self._scaled_text_defs) + 1
1927+
while True:
1928+
name = "FScaled%d" % n
1929+
if name not in self._draw_text_style_names:
1930+
self._draw_text_style_names.add(name)
1931+
return name
1932+
n += 1
1933+
1934+
def add_scaled_text_styles(self):
1935+
"""
1936+
Write the automatic text styles registered by
1937+
:meth:`_scaled_text_style` into the automatic-styles section (bug
1938+
#5733). Mirrors :meth:`add_styled_notes_styles`: collected while the
1939+
body is generated and flushed, via ``cntnt2``, ahead of the body
1940+
content at :meth:`finish_cntnt_creation` time.
1941+
"""
1942+
wrt2 = self.cntnt2.write
1943+
for name, props in self._scaled_text_defs:
1944+
wrt2(
1945+
'<style:style style:name="%s" ' % name
1946+
+ 'style:family="text">\n'
1947+
+ "<style:text-properties "
1948+
+ props
1949+
+ "/> "
1950+
+ "</style:style>\n"
1951+
)
1952+
18611953
def draw_text(self, style, text, x, y, mark=None):
18621954
"""
18631955
Draw a text
@@ -1869,6 +1961,7 @@ def draw_text(self, style, text, x, y, mark=None):
18691961
pstyle = style_sheet.get_paragraph_style(para_name)
18701962
font = pstyle.get_font()
18711963
sw = utils.pt2cm(string_width(font, text)) * 1.3
1964+
text_style = self._scaled_text_style(para_name) # bug #5733
18721965

18731966
self._write_mark(mark, text)
18741967

@@ -1882,7 +1975,7 @@ def draw_text(self, style, text, x, y, mark=None):
18821975
+ 'svg:y="%.2fcm">' % float(y)
18831976
+ "<draw:text-box> "
18841977
+ '<text:p text:style-name="F%s">' % para_name
1885-
+ '<text:span text:style-name="F%s">' % para_name
1978+
+ '<text:span text:style-name="%s">' % text_style
18861979
+
18871980
#' fo:max-height="%.2f">' % font.get_size() +
18881981
escape(text, ESC_MAP)
@@ -1928,9 +2021,10 @@ def draw_box(self, style, text, x, y, w, h, mark=None):
19282021
+ 'svg:y="%.2fcm">\n' % float(y)
19292022
)
19302023
if text:
2024+
text_style = self._scaled_text_style(para_name) # bug #5733
19312025
self.cntnt.write(
19322026
'<text:p text:style-name="%s">' % para_name
1933-
+ '<text:span text:style-name="F%s">' % para_name
2027+
+ '<text:span text:style-name="%s">' % text_style
19342028
+ escape(text, ESC_MAP)
19352029
+ "</text:span>"
19362030
"</text:p>\n"
@@ -1963,10 +2057,11 @@ def center_text(self, style, text, x, y, mark=None):
19632057
)
19642058

19652059
if text:
2060+
text_style = self._scaled_text_style(para_name) # bug #5733
19662061
self.cntnt.write(
19672062
"<draw:text-box>"
19682063
+ '<text:p text:style-name="X%s">' % para_name
1969-
+ '<text:span text:style-name="F%s">' % para_name
2064+
+ '<text:span text:style-name="%s">' % text_style
19702065
+ escape(text, ESC_MAP)
19712066
+ "</text:span>\n"
19722067
+ "</text:p>\n"

0 commit comments

Comments
 (0)