From 6ec7b93589065a7ba693dd78d1afd6fbbfe0b218 Mon Sep 17 00:00:00 2001 From: Stefan Hardegger Date: Sat, 21 Mar 2026 15:59:05 +0100 Subject: [PATCH] Fix embedded images in epub import --- .../storycove/service/EPUBImportService.java | 158 ++++++++++++++++-- .../service/EPUBImportServiceTest.java | 3 + 2 files changed, 145 insertions(+), 16 deletions(-) diff --git a/backend/src/main/java/com/storycove/service/EPUBImportService.java b/backend/src/main/java/com/storycove/service/EPUBImportService.java index e9c34d0..4a0d508 100644 --- a/backend/src/main/java/com/storycove/service/EPUBImportService.java +++ b/backend/src/main/java/com/storycove/service/EPUBImportService.java @@ -26,7 +26,9 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; @Service @@ -41,15 +43,17 @@ public class EPUBImportService { private final ReadingPositionRepository readingPositionRepository; private final HtmlSanitizationService sanitizationService; private final ImageService imageService; - + private final LibraryService libraryService; + @Autowired public EPUBImportService(StoryService storyService, - AuthorService authorService, + AuthorService authorService, SeriesService seriesService, TagService tagService, ReadingPositionRepository readingPositionRepository, HtmlSanitizationService sanitizationService, - ImageService imageService) { + ImageService imageService, + LibraryService libraryService) { this.storyService = storyService; this.authorService = authorService; this.seriesService = seriesService; @@ -57,6 +61,7 @@ public class EPUBImportService { this.readingPositionRepository = readingPositionRepository; this.sanitizationService = sanitizationService; this.imageService = imageService; + this.libraryService = libraryService; } public EPUBImportResponse importEPUB(EPUBImportRequest request) { @@ -81,22 +86,37 @@ public class EPUBImportService { Story savedStory = storyService.create(story); log.info("Story saved successfully with ID: {}", savedStory.getId()); - // Process embedded images if content contains any - String originalContent = story.getContentHtml(); - if (originalContent != null && originalContent.contains(" byHref = new HashMap<>(); + Map byFilename = new HashMap<>(); + for (Resource resource : book.getResources().getAll()) { + if (resource.getMediaType() != null && + resource.getMediaType().toString().startsWith("image/")) { + String href = resource.getHref(); + if (href != null) { + byHref.put(href, resource); + String filename = href.contains("/") ? href.substring(href.lastIndexOf('/') + 1) : href; + byFilename.putIfAbsent(filename, resource); + } + } + } + + if (byHref.isEmpty()) { + log.debug("No image resources found in EPUB for story: {}", storyId); + return htmlContent; + } + + String currentLibraryId = libraryService.getCurrentLibraryId(); + if (currentLibraryId == null || currentLibraryId.trim().isEmpty()) { + currentLibraryId = "default"; + } + + org.jsoup.nodes.Document doc = Jsoup.parse(htmlContent); + for (org.jsoup.nodes.Element img : doc.select("img[src]")) { + String src = img.attr("src"); + + // Skip already-resolved or external URLs + if (src.startsWith("http://") || src.startsWith("https://") || + src.startsWith("data:") || src.startsWith("/api/")) { + continue; + } + + Resource resource = resolveEpubResource(src, byHref, byFilename); + if (resource == null) { + log.warn("Could not find EPUB resource for image src: {}", src); + continue; + } + + try { + byte[] imageData = resource.getData(); + if (imageData == null || imageData.length == 0) { + log.warn("EPUB image resource has no data for src: {}", src); + continue; + } + + String mediaType = resource.getMediaType() != null ? + resource.getMediaType().toString() : "image/jpeg"; + String extension = getExtensionFromMediaType(mediaType); + String filename = "epub-img-" + System.currentTimeMillis() + "-" + + (int) (Math.random() * 100000) + "." + extension; + + MultipartFile imageFile = new EPUBCoverMultipartFile(imageData, filename, mediaType); + String imagePath = imageService.uploadImage(imageFile, ImageService.ImageType.CONTENT); + String imageUrl = "/api/files/images/" + currentLibraryId + "/" + imagePath; + + img.attr("src", imageUrl); + log.debug("Resolved EPUB image: {} -> {}", src, imageUrl); + + } catch (Exception e) { + log.error("Failed to save EPUB image {}: {}", src, e.getMessage(), e); + } + } + + return doc.body().html(); + } + + /** + * Tries to match a relative EPUB src path against the resource maps. + * Resolution order: exact href match → strip leading ../ segments → filename only. + */ + private Resource resolveEpubResource(String src, Map byHref, Map byFilename) { + if (byHref.containsKey(src)) { + return byHref.get(src); + } + + // Strip leading ../ and ./ navigation to get a plain relative path + String normalized = src; + while (normalized.startsWith("../")) { + normalized = normalized.substring(3); + } + if (normalized.startsWith("./")) { + normalized = normalized.substring(2); + } + + if (byHref.containsKey(normalized)) { + return byHref.get(normalized); + } + + // Fall back to filename-only match + String filename = normalized.contains("/") ? + normalized.substring(normalized.lastIndexOf('/') + 1) : normalized; + return byFilename.get(filename); + } + private String extractAndSaveCoverImage(Book book) { try { Resource coverResource = book.getCoverImage(); diff --git a/backend/src/test/java/com/storycove/service/EPUBImportServiceTest.java b/backend/src/test/java/com/storycove/service/EPUBImportServiceTest.java index e60b1d6..2f020e8 100644 --- a/backend/src/test/java/com/storycove/service/EPUBImportServiceTest.java +++ b/backend/src/test/java/com/storycove/service/EPUBImportServiceTest.java @@ -53,6 +53,9 @@ class EPUBImportServiceTest { @Mock private ImageService imageService; + @Mock + private LibraryService libraryService; + @InjectMocks private EPUBImportService epubImportService;