//----------------------------------------------------------------------------// // // // P i c t u r e L o a d e r // // // //----------------------------------------------------------------------------// // <editor-fold defaultstate="collapsed" desc="hdr"> // // Copyright (C) Brenton Partridge 2007-2008. // // This software is released under the GNU General Public License. // // Goto http://kenai.com/projects/audiveris to report bugs or suggestions. // //----------------------------------------------------------------------------// // </editor-fold> package omr.sheet.picture; import omr.constant.Constant; import omr.constant.ConstantSet; import omr.sheet.picture.jai.JaiLoader; import omr.util.FileUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; /** * Class {@code PictureLoader} gathers helper functions for {@link * Picture} to handle the loading of one or several images out of an * input file. * * <p>It leverages several software pieces: JAI, ImageIO, and Ghostscript. * * @author Hervé Bitteur * @author Brenton Partridge * @author Maxim Poliakovski */ public class PictureLoader { //~ Static fields/initializers --------------------------------------------- /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(PictureLoader.class); //~ Constructors ----------------------------------------------------------- /** * To disallow instantiation. */ private PictureLoader () { } //~ Methods ---------------------------------------------------------------- // //----------// // loadFile // //----------// /** * Loads a sequence of RenderedImage instances from a file. * * If ImageIO can read the file, it is used preferentially. * If not, or if ImageIO has an error, a PDF loader is used for files * ending with ".pdf" and JAI is used for all other files. * * @param imgFile the image file to load * @param pages if not null or empty, specifies (counted from 1) which * pages are desired. Otherwise all pages are loaded. * @return a sorted map of RenderedImage's (often but not always a * BufferedImage), guaranteed not to be null, id counted from 1. * @throws IllegalArgumentException if file does not exist * @throws RuntimeException if we are unable to load the file */ public static SortedMap<Integer, RenderedImage> loadImages (File imgFile, SortedSet<Integer> pages) { if (!imgFile.exists()) { throw new IllegalArgumentException(imgFile + " does not exist"); } logger.info("Loading {} ...", imgFile); logger.debug("Trying ImageIO"); SortedMap<Integer, RenderedImage> images = loadImageIO(imgFile, pages, 0); if (images == null) { String extension = FileUtil.getExtension(imgFile); if (extension.equalsIgnoreCase(".pdf")) { images = loadPDF(imgFile, pages); } else { logger.debug("Using JAI"); images = JaiLoader.loadJAI(imgFile); } } if (images == null) { logger.warn("Unable to load any image from {}", imgFile); } return images; } //-------------// // loadImageIO // //-------------// /** * Try to load a sequence of images, using ImageIO. * * @param imgFile the input image file * @param pages if not null or empty, specifies (counted from 1) which * precise pages are desired. Otherwise all pages are loaded. * @param offset specify offset on page ids. * @return a map (id -> image), or null if failed to load */ private static SortedMap<Integer, RenderedImage> loadImageIO (File imgFile, SortedSet<Integer> pages, int offset) { logger.debug("loadImageIO {} pages:{} offset:{}", imgFile, pages, offset); // Input stream ImageInputStream stream; try { stream = ImageIO.createImageInputStream(imgFile); } catch (IOException ex) { logger.warn("Unable to make ImageIO stream", ex); return null; } if (stream == null) { logger.debug("No ImageIO input stream provider"); return null; } try { Iterator<ImageReader> readers = ImageIO.getImageReaders(stream); if (!readers.hasNext()) { logger.debug("No ImageIO reader"); return null; } ImageReader reader = readers.next(); try { reader.setInput(stream, false); int imageCount = reader.getNumImages(true); if (imageCount > 1) { logger.info("{} contains {} images", imgFile.getName(), imageCount); } SortedMap<Integer, RenderedImage> images = new TreeMap<>(); for (int i = 1; i <= imageCount; i++) { int id = i + offset; if ((pages == null) || pages.isEmpty() || (pages.contains(id))) { BufferedImage img = reader.read(i - 1); images.put(id, img); logger.info("Loaded image #{} ({} x {})", id, img.getWidth(), img.getHeight()); } } return images; } catch (Exception ex) { logger.warn("ImageIO failed", ex); return null; } finally { reader.dispose(); } } finally { try { stream.close(); } catch (IOException ignored) { } } } //---------// // loadPDF // //---------// /** * Load a sequence of images out of a PDF file. * We spawn a Ghostscript subprocess to convert PDF to TIFF and then * load the temporary TIFF file via loadImageIO(). * * @param imgFile the input PDF file * @param pages if not null or empty, specifies (counted from 1) which * precise images are desired. Otherwise all pages are * loaded. * @return a map of images, or null if failed to load */ private static SortedMap<Integer, RenderedImage> loadPDF (File imgFile, SortedSet<Integer> pages) { logger.debug("loadPDF {} pages:{}", imgFile, pages); // Create a temporary tiff file from the PDF input Path temp = null; try { temp = Files.createTempFile("pic-", ".tif"); } catch (IOException ex) { logger.warn("Cannot create temporary file " + temp, ex); return null; } // Arguments for Ghostscript List<String> gsArgs = new ArrayList<>(); gsArgs.add(Ghostscript.getPath()); gsArgs.add("-dQUIET"); gsArgs.add("-dNOPAUSE"); gsArgs.add("-dBATCH"); gsArgs.add("-dSAFER"); gsArgs.add("-sDEVICE=" + constants.pdfDevice.getValue()); gsArgs.add("-r" + constants.pdfResolution.getValue()); gsArgs.add("-sOutputFile=" + temp); if (pages != null && !pages.isEmpty()) { gsArgs.add("-dFirstPage=" + pages.first()); gsArgs.add("-dLastPage=" + pages.last()); } gsArgs.add(imgFile.toString()); logger.debug("gsArgs:{}", gsArgs); try { // Spawn Ghostscript process and wait for its completion new ProcessBuilder(gsArgs).start().waitFor(); // Now load the temporary tiff file if (pages != null && !pages.isEmpty()) { return loadImageIO(temp.toFile(), pages, pages.first() - 1); } else { return loadImageIO(temp.toFile(), null, 0); } } catch (IOException | InterruptedException ex) { logger.warn("Error running Ghostscript " + gsArgs, ex); return null; } finally { try { Files.delete(temp); } catch (IOException ex) { logger.warn("Error deleting file " + temp, ex); } } } //~ Inner Classes ---------------------------------------------------------- //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- Constant.Integer pdfResolution = new Constant.Integer( "DPI", 300, "DPI resolution for PDF images"); Constant.String pdfDevice = new Constant.String( "tiff24nc", "Ghostscript output device (tiff24nc or tiffscaled8)"); } }