From c24e4cb6016ca3ce03c9659f7fa621a2d7890b48 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Fri, 21 Aug 2026 10:30:10 +0200 Subject: [PATCH] fix(html): name a linked ooxml image relative to the document The odf engines return an image href as it is written in the file, which is relative; the ooxml ones resolve the relationship into an absolute container path. The html layer emitted either verbatim, so an absolute one resolved against the server root rather than against the document - every image in a docx or xlsx 404ed for a host serving the service under a mount point, and `bring_offline` threw `not a relative path` outright. Make the location relative where the resource is built, so it holds for every engine. The reference output embeds its images and never rendered the linked form, hence the new test. Closes opendocument-app/OpenDocument.droid#551 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HuzekTz46XJdLDrLgt4hnJ --- CHANGELOG.md | 4 ++ src/odr/internal/html/document_element.cpp | 9 ++-- test/src/html_test.cpp | 48 ++++++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74c13370..9f3deeb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ The release run heads these entries with the version and opens a fresh ## Unreleased +- A linked image in a docx or xlsx (`embed_images = false`) is named relative + to the document, like odf, so a host serving the html under a mount point + resolves it. + ## v6.10.0 - 2026-08-20 - Html views take a zoom from their host: `odr.getZoom()`, `setZoom(value, diff --git a/src/odr/internal/html/document_element.cpp b/src/odr/internal/html/document_element.cpp index a17ad30a..52452f11 100644 --- a/src/odr/internal/html/document_element.cpp +++ b/src/odr/internal/html/document_element.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -498,9 +499,11 @@ void html::translate_image(const Element &element, const WritingState &state) { odr::HtmlResource resource; HtmlResourceLocation resource_location; if (image.is_internal()) { - resource = - HtmlResource::create(HtmlResourceType::image, "image/jpg", image.href(), - image.href(), image.file(), false, false, true); + // the location is resolved against the document, so an engine naming the + // image by its absolute path in the container has to lose the root + const std::string path = Path(image.href()).make_relative().string(); + resource = HtmlResource::create(HtmlResourceType::image, "image/jpg", path, + path, image.file(), false, false, true); resource_location = state.config().resource_locator(resource, state.config()); } else { diff --git a/test/src/html_test.cpp b/test/src/html_test.cpp index 43ec1d72..dbf0121d 100644 --- a/test/src/html_test.cpp +++ b/test/src/html_test.cpp @@ -7,6 +7,8 @@ #include +#include + #include #include @@ -64,6 +66,52 @@ TEST(html, linked_resources_are_served) { "document.html"); } +// Same for a document resource, which unlike a shipped one is named by the +// engine: the reference output embeds every image, so nothing else renders the +// linked form (opendocument-app/OpenDocument.droid#551). +TEST(html, linked_images_are_served) { + const auto logger = Logger::create_stdio("odr-test", LogLevel::verbose); + + const std::string cache_path = + (std::filesystem::current_path() / "images").string(); + + const auto check = [&](const std::string &path) { + const DecodedFile file(TestData::test_file_path(path), logger); + + HtmlConfig config; + config.embed_images = false; + + const HtmlService service = html::translate(file, cache_path, config); + + std::ostringstream out; + const HtmlResources resources = service.list_views().at(0).write_html(out); + + std::size_t linked = 0; + for (const auto &[resource, location] : resources) { + if (resource.type() != HtmlResourceType::image || + !resource.is_accessible()) { + continue; + } + ASSERT_TRUE(location.has_value()) << resource.name(); + ++linked; + + // an absolute location would resolve against the server root rather than + // against the document + EXPECT_TRUE(Path(*location).relative()) << *location; + EXPECT_TRUE(service.exists(*location)) << *location; + + std::ostringstream served; + service.write(*location, served); + EXPECT_FALSE(served.str().empty()) << *location; + } + EXPECT_GT(linked, 0) << path; + }; + + check("odr-public/odt/image-text-wrap.odt"); + check("odr-public/docx/file-sample_100kB.docx"); + check("odr-public/xlsx/sample.xlsx"); +} + // The one archive the reference-output suite renders has no directory in it. // An archive may hold a file named like the stylesheet. Forcing the collision // through the locator saves needing such an archive in the test data.