package org.rr.jeborker.converter; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.logging.Level; import nl.siegmann.epublib.domain.Book; import nl.siegmann.epublib.domain.Resource; import nl.siegmann.epublib.domain.Spine; import nl.siegmann.epublib.epub.EpubWriter; import org.apache.commons.io.Charsets; import org.apache.commons.io.IOUtils; import org.rr.commons.log.LoggerFactory; import org.rr.commons.mufs.IResourceHandler; import org.rr.commons.mufs.MimeUtils; import org.rr.commons.mufs.ResourceHandlerFactory; import org.rr.jeborker.app.preferences.APreferenceStore; import org.rr.jeborker.app.preferences.PreferenceStoreFactory; import org.rr.jeborker.gui.ConverterPreferenceController; import org.rr.jeborker.gui.MainController; import org.rr.pm.image.IImageProvider; import org.rr.pm.image.ImageProviderFactory; import org.rr.pm.image.ImageUtils; /** * A converter for image archives to epub */ abstract class ACompressedImageToEpubConverter implements IEBookConverter { private static String IMAGE_QUALITY_LABEL = Bundle.getString("MultipleConverter.imageQuality.label"); private static String IMAGE_QUALITY_KEY = ACompressedImageToEpubConverter.class.getName() + "." + IMAGE_QUALITY_LABEL; private APreferenceStore preferenceStore = PreferenceStoreFactory.getPreferenceStore(PreferenceStoreFactory.DB_STORE); protected IResourceHandler comicBookResource; private ConverterPreferenceController converterPreferenceController; private ArrayList<IResourceHandler> tempFiles = new ArrayList<>(); public ACompressedImageToEpubConverter(IResourceHandler comicBookResource) { this.comicBookResource = comicBookResource; } @Override public IResourceHandler convert() throws IOException { final ConverterPreferenceController converterPreferenceController = getConverterPreferenceController(); if(!converterPreferenceController.isConfirmed()) { return null; } final List<String> compressedImageEntries = listEntries(this.comicBookResource); if(compressedImageEntries == null || compressedImageEntries.isEmpty()) { LoggerFactory.getLogger(this).log(Level.WARNING, "The Comic book archive " + comicBookResource.getName() + " is empty."); return null; } final Book epub = this.createEpub(compressedImageEntries); final EpubWriter writer = new EpubWriter(); IResourceHandler targetEpubResource = ResourceHandlerFactory.getUniqueResourceHandler(this.comicBookResource, "epub"); OutputStream contentOutputStream = targetEpubResource.getContentOutputStream(false); try { writer.write(epub, contentOutputStream); } finally { contentOutputStream.flush(); contentOutputStream.close(); } deleteTemporaryFiles(); ConverterUtils.transferMetadata(this.comicBookResource, targetEpubResource); preferenceStore.addGenericEntryAsNumber(IMAGE_QUALITY_KEY, getImageQuality()); return targetEpubResource; } private Book createEpub(List<String> cbzEntries) throws IOException { final Book epub = new Book(); final Spine spine = new Spine(); List<Resource> resources = new ArrayList<>(cbzEntries.size()); for(int i = 0; i < cbzEntries.size(); i++) { final String cbzEntry = cbzEntries.get(i); if(ConverterUtils.isImageFileName(cbzEntry)) { final InputStream imageInputStream = getCompressionEntryStream(this.comicBookResource, cbzEntry); final List<InputStream> convertedImageInputStream = getConvertedImageInputStream(imageInputStream, cbzEntry); for(int j = 0; j < convertedImageInputStream.size(); j++) { final InputStream imageIn = convertedImageInputStream.get(j); final String cbzHrefEntry = createHrefEntry(cbzEntry, epub, j); final Resource imageResource = new Resource(imageIn, cbzHrefEntry); resources.add(imageResource); epub.addResource(imageResource); this.attachSpineEntry(epub, spine, imageResource); //the first image from the cbz is the cover image. if(i == 0) { epub.setCoverImage(imageResource); } } } } epub.setSpine(spine); return epub; } /** * Creates a xhtml doc for the given image and add it as spine to the given epub. * @throws IOException */ private void attachSpineEntry(final Book epub, final Spine spine, final Resource imageResource) throws IOException { String imageName = imageResource.getHref(); if(ConverterUtils.isImageFileName(imageName)) { if(imageName.lastIndexOf('.') != -1) { imageName = imageName.substring(0, imageName.lastIndexOf('.')); } Resource spineResource = new Resource(imageName + ".xhtml"); InputStream spineTemplateIn = ACompressedImageToEpubConverter.class.getResourceAsStream("CbzToEpubSpineImageTemplate"); String spineTemplate = IOUtils.toString(spineTemplateIn); String spineDoc = MessageFormat.format(spineTemplate, new Object[] {imageResource.getHref()}); spineResource.setData(spineDoc.getBytes(Charsets.UTF_8)); spine.addResource(spineResource); epub.addResource(spineResource); } } /** * Takes the given InputStream with image data and does the desired image conversion if necessary. * @return The converted image(s). If no conversion is necessary, the original input stream is returned. */ private List<InputStream> getConvertedImageInputStream(InputStream imageIn, String imageName) throws IOException { if(isImageConversion()) { ArrayList<InputStream> result = new ArrayList<>(); IImageProvider imageProvider = ImageProviderFactory.getImageProvider(ResourceHandlerFactory.getResourceHandler(imageIn)); List<BufferedImage> processImageModifications = ConverterUtils.processImageModifications(imageProvider.getImage(), getImageQuality(), getConverterPreferenceController()); for(BufferedImage image : processImageModifications) { String mime = MimeUtils.getImageMimeFromFileName(imageName, MimeUtils.MIME_JPEG); byte[] imageBytes = ImageUtils.getImageBytes(image, mime); //copy the converted data to HD because we possibly have not enough memory for the whole boo. IResourceHandler temporaryResource = ResourceHandlerFactory.getTemporaryResource(mime.substring(mime.indexOf('/') + 1)); tempFiles.add(temporaryResource); temporaryResource.setContent(imageBytes); result.add(temporaryResource.getContentInputStream()); } return result; } else { return Collections.singletonList(imageIn); } } /** * Tells if some image conversion is needed. */ private boolean isImageConversion() { ConverterPreferenceController preferenceController = getConverterPreferenceController(); return preferenceController.isLandscapePageRotate() || preferenceController.isLandscapePageSplit() || preferenceController.getCommonValueAsInt(IMAGE_QUALITY_LABEL) < 99; } /** * Creates a href entry for the given archive entry. * @param cbzEntry The archive entry name * @param epub The {@link Book} instance to be created. * @param index The index of the href with the same name * @return The desired href entry. */ private String createHrefEntry(final String cbzEntry, final Book epub, final int index) { try { StringBuilder result = new StringBuilder(); for(int i = 0; i < cbzEntry.length(); i++) { char c = cbzEntry.charAt(i); if(Character.isWhitespace(c)) { result.append('_'); } else if(Character.isDigit(c)) { result.append(c); } else if(c >= 'A' || c <= 'z') { result.append(c); } else if(c == '/') { result.setLength(0); //remove path segement } } String href = result.toString(); while(epub.getResources().getByHref(href) != null) { if(href.lastIndexOf('.') != -1) { String ext = href.substring(href.lastIndexOf('.')); href = href.substring(0, href.lastIndexOf('.')) + "_" + index + ext; } else { href = href + index + "_"; } } return href; } catch(Exception e) { return cbzEntry; } } private void deleteTemporaryFiles() { for(IResourceHandler tempFile : tempFiles) { try { tempFile.delete(); } catch (IOException e) { LoggerFactory.getLogger().log(Level.WARNING, "Could not delete temporary file " + tempFile.toString(), e); } } } protected abstract InputStream getCompressionEntryStream(IResourceHandler resourceHandler, String entry); protected abstract List<String> listEntries(IResourceHandler cbzResource); /** * Gets the {@link ConverterPreferenceController} for this instance. Creates a new * {@link ConverterPreferenceController} if no one is created previously. * @see #createConverterPreferenceController() */ private ConverterPreferenceController getConverterPreferenceController() { if(this.converterPreferenceController == null) { this.converterPreferenceController = this.createConverterPreferenceController(); } if(!this.converterPreferenceController.hasShown()) { this.converterPreferenceController.showPreferenceDialog(); } return this.converterPreferenceController; } private int getImageQuality() { return getConverterPreferenceController().getCommonValueAsInt(IMAGE_QUALITY_LABEL); } /** * Create a new {@link ConverterPreferenceController} instance. */ public ConverterPreferenceController createConverterPreferenceController() { ConverterPreferenceController preferenceController = MainController.getController().getConverterPreferenceController(); preferenceController.addCommonSlider(IMAGE_QUALITY_LABEL, preferenceStore.getGenericEntryAsNumber(IMAGE_QUALITY_KEY, 100).intValue()); preferenceController.setShowLandscapePageEntries(true); return preferenceController; } public void setConverterPreferenceController(ConverterPreferenceController controller) { this.converterPreferenceController = controller; } }