Skip to content

Commit a127b52

Browse files
committed
Reference HTML report images by a report-relative path
The HTML backend of the text reports (e.g. Detailed Ancestor/Descendant Report) wrote each embedded image's <img src> as the absolute filesystem path of the report's data directory (datadirfull()). The generated .html therefore only rendered on the machine that produced it — copy or share it and every image broke. HtmlDoc.add_media now references images by the report-relative data-directory path (datadir(), the data subdirectory basename) while the on-disk copy still targets the absolute datadirfull() location. The backend already copies images into that report-relative subdirectory, so the written src now matches the on-disk layout and the document stays valid wherever it is opened. Only the reference changes; the copy destination is unchanged. - gramps/plugins/docgen/htmldoc.py: reference images by the report-relative datadir() path instead of the absolute datadirfull() path - gramps/plugins/test/htmldoc_relmedia_test.py: new test asserting the emitted <img src> is relative and the copy destination stays absolute - po/POTFILES.skip: register test file Fixes #6824 Signed-off-by: Eduard Ralph <eduard@ralphovi.net>
1 parent 0d9e148 commit a127b52

3 files changed

Lines changed: 125 additions & 4 deletions

File tree

gramps/plugins/docgen/htmldoc.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,10 @@ def add_media(self, name, pos, w_cm, h_cm, alt="", style_name=None, crop=None):
608608
refname = "is%s" % os.path.basename(name)
609609

610610
imdir = self._backend.datadirfull()
611+
# The on-disk copy goes to the absolute datadirfull() path, but the
612+
# <img src> must be relative to the report (the data subdirectory
613+
# basename), so the generated .html stays valid when moved/shared.
614+
imref = self._backend.datadir()
611615

612616
try:
613617
resize_to_jpeg(name, imdir + os.sep + refname, size, size, crop=crop)
@@ -621,24 +625,24 @@ def add_media(self, name, pos, w_cm, h_cm, alt="", style_name=None, crop=None):
621625
if pos not in ["right", "left"]:
622626
if len(alt):
623627
self.htmllist[-1] += Html("div") + (
624-
Html("img", src=imdir + os.sep + refname, border="0", alt=alt),
628+
Html("img", src=imref + "/" + refname, border="0", alt=alt),
625629
Html("p", class_="DDR-Caption") + alt,
626630
)
627631
else:
628632
self.htmllist[-1] += Html(
629-
"img", src=imdir + os.sep + refname, border="0", alt=alt
633+
"img", src=imref + "/" + refname, border="0", alt=alt
630634
)
631635
else:
632636
if len(alt):
633637
self.htmllist[-1] += Html(
634638
"div", style_="float: %s; padding: 5px; margin: 0;" % pos
635639
) + (
636-
Html("img", src=imdir + os.sep + refname, border="0", alt=alt),
640+
Html("img", src=imref + "/" + refname, border="0", alt=alt),
637641
Html("p", class_="DDR-Caption") + alt,
638642
)
639643
else:
640644
self.htmllist[-1] += Html(
641-
"img", src=imdir + os.sep + refname, border="0", alt=alt, align=pos
645+
"img", src=imref + "/" + refname, border="0", alt=alt, align=pos
642646
)
643647

644648
def page_break(self):
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#
2+
# Gramps - a GTK+/GNOME based genealogy program
3+
#
4+
# Copyright (C) 2026 Gramps Development Team
5+
#
6+
# This program is free software; you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation; either version 2 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License along
17+
# with this program; if not, see <https://www.gnu.org/licenses/>.
18+
#
19+
20+
"""
21+
Regression test for bug 6824.
22+
23+
The HTML text-report backend used to write each embedded image's ``<img src>``
24+
as the *absolute* filesystem path of the report's data directory
25+
(``datadirfull()``), so the generated ``.html`` only rendered on the machine
26+
that produced it: copy/share it and every image broke.
27+
28+
This test drives the production ``HtmlDoc.add_media`` path and asserts the
29+
emitted ``src`` is report-relative (the data-subdirectory basename + filename),
30+
carrying no absolute directory prefix, while the on-disk copy still lands in the
31+
absolute ``datadirfull()`` location.
32+
"""
33+
34+
import os
35+
import re
36+
import shutil
37+
import tempfile
38+
import unittest
39+
from unittest import mock
40+
41+
from gramps.gen.plug.docgen import StyleSheet
42+
from gramps.plugins.docgen import htmldoc
43+
44+
# Extract the src of the (first) <img> from a rendered fragment of HTML.
45+
_IMG_SRC = re.compile(r"<img\b[^>]*\bsrc=\"([^\"]*)\"")
46+
47+
48+
class HtmlDocRelativeMediaTest(unittest.TestCase):
49+
"""Exercise HtmlDoc.add_media and inspect the emitted <img src>."""
50+
51+
def setUp(self):
52+
self.tmpdir = tempfile.mkdtemp(prefix="gramps_htmldoc_")
53+
self.addCleanup(shutil.rmtree, self.tmpdir, ignore_errors=True)
54+
55+
def _emit_img_src(self, pos, alt):
56+
"""Open an HtmlDoc, add one media item, and return (src, resize_dest).
57+
58+
Only the image-resize step is stubbed (so no real image encoder is
59+
needed); everything else runs the real production code.
60+
"""
61+
report = os.path.join(self.tmpdir, "myreport.html")
62+
doc = htmldoc.HtmlDoc(StyleSheet(), None)
63+
doc.open(report) # creates the "myreport" data subdirectory on disk
64+
65+
recorded = {}
66+
67+
def fake_resize(source, destination, width, height, crop=None):
68+
recorded["dest"] = destination
69+
# simulate the encoder writing the resized image to disk
70+
with open(destination, "w", encoding="utf-8") as handle:
71+
handle.write("stub-jpeg")
72+
73+
with mock.patch.object(htmldoc, "resize_to_jpeg", fake_resize):
74+
doc.add_media(
75+
os.path.join(self.tmpdir, "photo.jpg"), pos, 4.0, 3.0, alt=alt
76+
)
77+
78+
rendered = str(doc.htmllist[-1])
79+
srcs = _IMG_SRC.findall(rendered)
80+
self.assertTrue(srcs, "add_media emitted no <img> tag: %r" % rendered)
81+
return srcs[0], recorded, doc
82+
83+
def test_add_media_src_is_report_relative(self):
84+
"""The <img src> is report-relative, not the absolute datadirfull()."""
85+
for pos, alt in (("single", ""), ("right", ""), ("left", ["a caption"])):
86+
with self.subTest(pos=pos, alt=alt):
87+
src, recorded, doc = self._emit_img_src(pos, alt)
88+
datadirfull = doc._backend.datadirfull()
89+
90+
# The absolute host path must NOT leak into the reference, and
91+
# the reference must be relative to the document.
92+
self.assertNotIn(
93+
datadirfull,
94+
src,
95+
"img src carries the absolute datadirfull() path: %r" % src,
96+
)
97+
self.assertFalse(
98+
os.path.isabs(src),
99+
"img src should be report-relative, got absolute: %r" % src,
100+
)
101+
# It is exactly the data-subdirectory basename + refname.
102+
self.assertEqual(src, "myreport/isphoto.jpg")
103+
104+
def test_copy_destination_stays_absolute(self):
105+
"""The on-disk copy still targets the absolute datadirfull() path."""
106+
_src, recorded, doc = self._emit_img_src("single", "")
107+
expected_dest = os.path.join(doc._backend.datadirfull(), "isphoto.jpg")
108+
self.assertEqual(recorded.get("dest"), expected_dest)
109+
self.assertTrue(
110+
os.path.exists(expected_dest),
111+
"resized image did not land in the datadir: %r" % expected_dest,
112+
)
113+
114+
115+
if __name__ == "__main__":
116+
unittest.main()

po/POTFILES.skip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,7 @@ gramps/plugins/sidebar/expandersidebar.py
674674
#
675675
gramps/plugins/test/db_undo_and_signals_test.py
676676
gramps/plugins/test/exports_test.py
677+
gramps/plugins/test/htmldoc_relmedia_test.py
677678
gramps/plugins/test/imports_test.py
678679
gramps/plugins/test/reports_test.py
679680
gramps/plugins/test/schema_validation_test.py

0 commit comments

Comments
 (0)