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.