Fix embedded images in epub import
This commit is contained in:
@@ -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("<img")) {
|
||||
// Step 1: resolve images embedded in the EPUB archive (relative src paths)
|
||||
String currentContent = story.getContentHtml();
|
||||
if (currentContent != null && currentContent.contains("<img")) {
|
||||
try {
|
||||
log.info("Processing embedded images for story: {}", savedStory.getId());
|
||||
ImageService.ContentImageProcessingResult imageResult =
|
||||
imageService.processContentImages(originalContent, savedStory.getId());
|
||||
log.info("Resolving EPUB-embedded images for story: {}", savedStory.getId());
|
||||
String resolvedContent = processEpubImages(currentContent, book, savedStory.getId());
|
||||
if (!resolvedContent.equals(currentContent)) {
|
||||
log.info("Updating story content with resolved EPUB images");
|
||||
savedStory.setContentHtml(resolvedContent);
|
||||
savedStory = storyService.update(savedStory.getId(), savedStory);
|
||||
currentContent = resolvedContent;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("EPUB Import - Failed to resolve embedded images for story {}: {}",
|
||||
savedStory.getId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// Update story content with processed images if changed
|
||||
if (!imageResult.getProcessedContent().equals(originalContent)) {
|
||||
log.info("Updating story content with processed images");
|
||||
// Step 2: download any remaining external (http/https) images
|
||||
if (currentContent != null && currentContent.contains("<img")) {
|
||||
try {
|
||||
log.info("Processing external images for story: {}", savedStory.getId());
|
||||
ImageService.ContentImageProcessingResult imageResult =
|
||||
imageService.processContentImages(currentContent, savedStory.getId());
|
||||
|
||||
if (!imageResult.getProcessedContent().equals(currentContent)) {
|
||||
log.info("Updating story content with downloaded external images");
|
||||
savedStory.setContentHtml(imageResult.getProcessedContent());
|
||||
savedStory = storyService.update(savedStory.getId(), savedStory);
|
||||
|
||||
// Log the image processing results
|
||||
log.info("EPUB Import - Image processing completed for story {}. Downloaded {} images.",
|
||||
log.info("EPUB Import - External image processing completed for story {}. Downloaded {} images.",
|
||||
savedStory.getId(), imageResult.getDownloadedImages().size());
|
||||
|
||||
if (imageResult.hasWarnings()) {
|
||||
@@ -105,8 +125,7 @@ public class EPUBImportService {
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Log error but don't fail the import
|
||||
log.error("EPUB Import - Failed to process embedded images for story {}: {}",
|
||||
log.error("EPUB Import - Failed to process external images for story {}: {}",
|
||||
savedStory.getId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
@@ -452,6 +471,113 @@ public class EPUBImportService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves EPUB-internal image references (relative paths) by extracting the image
|
||||
* bytes from the EPUB resource map, saving them via ImageService, and replacing
|
||||
* the src attribute with the resulting local API URL.
|
||||
*/
|
||||
private String processEpubImages(String htmlContent, Book book, java.util.UUID storyId) {
|
||||
if (htmlContent == null || !htmlContent.contains("<img")) {
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
// Index all image resources by href and by bare filename for flexible lookup
|
||||
Map<String, Resource> byHref = new HashMap<>();
|
||||
Map<String, Resource> 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<String, Resource> byHref, Map<String, Resource> 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();
|
||||
|
||||
@@ -53,6 +53,9 @@ class EPUBImportServiceTest {
|
||||
@Mock
|
||||
private ImageService imageService;
|
||||
|
||||
@Mock
|
||||
private LibraryService libraryService;
|
||||
|
||||
@InjectMocks
|
||||
private EPUBImportService epubImportService;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user