package net.pms.encoders; import java.awt.Dimension; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.JComponent; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.drew.lang.ByteArrayReader; import net.coobird.thumbnailator.Thumbnails; import net.pms.PMS; import net.pms.configuration.PmsConfiguration; import net.pms.dlna.DLNAMediaInfo; import net.pms.dlna.DLNAResource; import net.pms.formats.Format; import net.pms.image.ExifInfo; import net.pms.image.ExifOrientation; import net.pms.image.ImageInfo; import net.pms.image.ImagesUtil; import net.pms.image.thumbnailator.ExifFilterUtils; import net.pms.io.InternalJavaProcessImpl; import net.pms.io.OutputParams; import net.pms.io.ProcessWrapper; import net.pms.io.ProcessWrapperImpl; public class DCRaw extends ImagePlayer { public final static String ID = "dcraw"; private static final Logger LOGGER = LoggerFactory.getLogger(DCRaw.class); protected String[] getDefaultArgs() { return new String[]{ "-e", "-c" }; } @Override public String[] args() { return getDefaultArgs(); } @Override public JComponent config() { return null; } @Override public String executable() { return configuration.getDCRawPath(); } @Override public String id() { return ID; } @Override public ProcessWrapper launchTranscode( DLNAResource dlna, DLNAMediaInfo media, OutputParams params ) throws IOException { if (media == null || dlna == null) { return null; } final String filename = dlna.getSystemName(); byte[] image = getImage(params, filename, media.getImageInfo()); if (image == null) { return null; } ProcessWrapper pw = new InternalJavaProcessImpl(new ByteArrayInputStream(image)); return pw; } @Override public String mimeType() { return "image/jpeg"; } @Override public String name() { return "DCRaw"; } @Override public int purpose() { return MISC_PLAYER; } /** * Converts {@code fileName} into PPM format. * * @param params the {@link OutputParams} to use. Can be {@code null}. * @param fileName the path of the image file to process. * @param imageInfo the {@link ImageInfo} for the image file. Can be {@code null}. * @return A byte array containing the converted image or {@code null}. * @throws IOException if an IO error occurs. */ public byte[] getImage(OutputParams params, String fileName, ImageInfo imageInfo) { if (LOGGER.isTraceEnabled()) { LOGGER.trace("Decoding image \"{}\" with DCRaw", fileName); } if (params == null) { params = new OutputParams(PMS.getConfiguration()); } // Use device-specific pms conf PmsConfiguration configuration = PMS.getConfiguration(params); params.log = false; // Setting the buffer to the size of the source file or 5 MB. The // output won't be the same size as the input, but it will hopefully // give us a somewhat relevant buffer size. Every time the buffer has // to grow, the whole buffer must be copied in memory. params.outputByteArrayStreamBufferSize = imageInfo != null && imageInfo.getSize() != ImageInfo.SIZE_UNKNOWN ? (int) imageInfo.getSize() : 5000000; // First try to get the embedded thumbnail String cmdArray[] = new String[5]; cmdArray[0] = configuration.getDCRawPath(); cmdArray[1] = "-c"; cmdArray[2] = "-M"; cmdArray[3] = "-w"; cmdArray[4] = fileName; ProcessWrapperImpl pw = new ProcessWrapperImpl(cmdArray, true, params, false, true); pw.runInSameThread(); byte[] bytes = pw.getOutputByteArray().toByteArray(); List<String> results = pw.getResults(); if (bytes == null || bytes.length == 0) { if (!results.isEmpty() && results.get(0).startsWith("Cannot decode file")) { LOGGER.warn("DCRaw could not decode image \"{}\"", fileName); } else if (!results.isEmpty()) { LOGGER.debug("DCRaw failed to decode image \"{}\": {}", fileName, StringUtils.join(results, "\n")); } return null; } return bytes; } /** * Extracts or generates a thumbnail for {@code fileName}. * * @param params the {@link OutputParams} to use. Can be {@code null}. * @param fileName the path of the image file to process. * @param imageInfo the {@link ImageInfo} for the image file. * @return A byte array containing the thumbnail or {@code null}. * @throws IOException if an IO error occurs. */ public byte[] getThumbnail(OutputParams params, String fileName, ImageInfo imageInfo) { boolean trace = LOGGER.isTraceEnabled(); if (trace) { LOGGER.trace("Extracting thumbnail from \"{}\" with DCRaw", fileName); } if (params == null) { params = new OutputParams(PMS.getConfiguration()); } // Use device-specific pms conf PmsConfiguration configuration = PMS.getConfiguration(params); params.log = false; // This is a wild guess at a decent buffer size for an embedded thumbnail. // Every time the buffer has to grow, the whole buffer must be copied in memory. params.outputByteArrayStreamBufferSize = 150000; // First try to get the embedded thumbnail String cmdArray[] = new String[6]; cmdArray[0] = configuration.getDCRawPath(); cmdArray[1] = "-e"; cmdArray[2] = "-c"; cmdArray[3] = "-M"; cmdArray[4] = "-w"; cmdArray[5] = fileName; ProcessWrapperImpl pw = new ProcessWrapperImpl(cmdArray, true, params, false, true); pw.runInSameThread(); byte[] bytes = pw.getOutputByteArray().toByteArray(); List<String> results = pw.getResults(); if (bytes.length > 0) { // DCRaw doesn't seem to apply Exif Orientation to embedded thumbnails, handle it boolean isJPEG = (bytes[0] & 0xFF) == 0xFF && (bytes[1] & 0xFF) == 0xD8; ExifOrientation thumbnailOrientation = null; Dimension jpegResolution = null; int exifOrientationOffset = -1; if (isJPEG) { try { ByteArrayReader reader = new ByteArrayReader(bytes); exifOrientationOffset = ImagesUtil.getJPEGExifIFDTagOffset(0x112, reader); jpegResolution = ImagesUtil.getJPEGResolution(reader); } catch (IOException e) { exifOrientationOffset = -1; LOGGER.debug( "Unexpected error while trying to find Exif orientation offset in embedded thumbnail for \"{}\": {}", fileName, e.getMessage()); LOGGER.trace("", e); } if (exifOrientationOffset > 0) { thumbnailOrientation = ExifOrientation.typeOf(bytes[exifOrientationOffset]); } else { LOGGER.debug("Couldn't find Exif orientation in the thumbnail extracted from \"{}\"", fileName); } } ExifOrientation imageOrientation = imageInfo instanceof ExifInfo ? ((ExifInfo) imageInfo).getOriginalExifOrientation() : null; // There might be required to impose specific rules depending on the (RAW) format here if (imageOrientation != null && imageOrientation != thumbnailOrientation) { if (thumbnailOrientation != null) { if ( imageInfo.getWidth() > 0 && imageInfo.getHeight() > 0 && jpegResolution != null && jpegResolution.getWidth() > 0 && jpegResolution.getHeight() > 0 ) { // Try to determine which orientation to trust double imageAspect, thumbnailAspect; if (ImagesUtil.isExifAxesSwapNeeded(imageOrientation)) { imageAspect = (double) imageInfo.getHeight() / imageInfo.getWidth(); } else { imageAspect = (double) imageInfo.getWidth() / imageInfo.getHeight(); } if (ImagesUtil.isExifAxesSwapNeeded(thumbnailOrientation)) { thumbnailAspect = (double) jpegResolution.getHeight() / jpegResolution.getWidth(); } else { thumbnailAspect = (double) jpegResolution.getWidth() / jpegResolution.getHeight(); } if (Math.abs(imageAspect - thumbnailAspect) > 0.001d) { // The image and the thumbnail seems to have different aspect ratios, use that of the image bytes[exifOrientationOffset] = (byte) imageOrientation.getValue(); } } } else if (imageOrientation != ExifOrientation.TOP_LEFT) { // Apply the orientation to the thumbnail try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); Thumbnails.of(new ByteArrayInputStream(bytes)) .scale(1.0d) .addFilter(ExifFilterUtils.getFilterForOrientation(imageOrientation.getThumbnailatorOrientation())) .outputFormat("PNG") // PNG here to avoid further degradation from rotation .outputQuality(1.0f) .toOutputStream(outputStream); bytes = outputStream.toByteArray(); } catch (IOException e) { LOGGER.error( "Unexpected error when trying to rotate thumbnail for \"{}\" - cancelling rotation: {}", fileName, e.getMessage() ); LOGGER.trace("", e); } } } } if (bytes.length == 0 || !results.isEmpty() && results.get(0).contains("has no thumbnail")) { // No embedded thumbnail retrieved, generate thumbnail from the actual file if (trace) { LOGGER.trace( "No embedded thumbnail found in \"{}\", " + "trying to generate thumbnail from the image itself", fileName ); } params.outputByteArrayStreamBufferSize = imageInfo != null && imageInfo.getSize() != ImageInfo.SIZE_UNKNOWN ? (int) imageInfo.getSize() / 4 : 500000; cmdArray[1] = "-h"; pw = new ProcessWrapperImpl(cmdArray, true, params); pw.runInSameThread(); bytes = pw.getOutputByteArray().toByteArray(); } if (trace && (bytes == null || bytes.length == 0)) { LOGGER.trace("Failed to generate thumbnail with DCRaw for image \"{}\"", fileName); } return bytes != null && bytes.length > 0 ? bytes : null; } /** * Parses {@code file} and stores the result in {@code media}. * * @param media the {@link DLNAMediaInfo} instance to store the parse * results in. * @param file the {@link File} to parse. */ public void parse(DLNAMediaInfo media, File file) { if (media == null) { throw new NullPointerException("media cannot be null"); } if (file == null) { throw new NullPointerException("file cannot be null"); } OutputParams params = new OutputParams(configuration); params.log = true; String cmdArray[] = new String[4]; cmdArray[0] = configuration.getDCRawPath(); cmdArray[1] = "-i"; cmdArray[2] = "-v"; cmdArray[3] = file.getAbsolutePath(); ProcessWrapperImpl pw = new ProcessWrapperImpl(cmdArray, params, true, false); pw.runInSameThread(); List<String> list = pw.getOtherResults(); Pattern pattern = Pattern.compile("^Output size:\\s*(\\d+)\\s*x\\s*(\\d+)"); Matcher matcher; for (String s : list) { matcher = pattern.matcher(s); if (matcher.find()) { media.setWidth(Integer.parseInt(matcher.group(1))); media.setHeight(Integer.parseInt(matcher.group(2))); if (LOGGER.isTraceEnabled()) { LOGGER.trace( "Parsed resolution {} x {} for image \"{}\" from DCRaw output", Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)), file.getPath() ); } break; } } } /** * {@inheritDoc} */ @Override public boolean isCompatible(DLNAResource resource) { return resource != null && resource.getFormat() != null && resource.getFormat().getIdentifier() == Format.Identifier.RAW; } }