/* * Universal Media Server, for streaming any media to DLNA * compatible renderers based on the http://www.ps3mediaserver.org. * Copyright (C) 2012 UMS developers. * * This program is a free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License only. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package net.pms.util; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; import javax.imageio.ImageIO; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.pms.PMS; import net.pms.dlna.DLNAMediaInfo; import net.pms.dlna.DLNAResource; import net.pms.dlna.DLNAThumbnail; import net.pms.dlna.DLNAThumbnailInputStream; import net.pms.formats.Format; import net.pms.image.ImageFormat; import net.pms.image.ImageIOTools; import net.pms.image.ImagesUtil.ScaleType; /** * This is an singleton class for providing and caching generic file extension * icons. Thread-safe. */ public enum GenericIcons { INSTANCE; private final BufferedImage genericAudioIcon = readBufferedImage("formats/audio.png"); private final BufferedImage genericImageIcon = readBufferedImage("formats/image.png"); private final BufferedImage genericVideoIcon = readBufferedImage("formats/video.png"); private final BufferedImage genericUnknownIcon = readBufferedImage("formats/unknown.png"); private final DLNAThumbnail genericFolderThumbnail; private final ReentrantLock cacheLock = new ReentrantLock(); /** * All access to {@link #cache} must be protected with {@link #cacheLock}. */ private final Map<ImageFormat, Map<IconType, Map<String, DLNAThumbnail>>> cache = new HashMap<>(); private static final Logger LOGGER = LoggerFactory.getLogger(GenericIcons.class); private GenericIcons() { DLNAThumbnail thumbnail; try { thumbnail = DLNAThumbnail.toThumbnail(getResourceAsStream("thumbnail-folder-256.png")); } catch (IOException e) { thumbnail = null; } genericFolderThumbnail = thumbnail; } public DLNAThumbnailInputStream getGenericIcon(DLNAResource resource) { ImageFormat imageFormat = ImageFormat.JPEG; if (resource == null) { ImageIO.setUseCache(false); ByteArrayOutputStream out = new ByteArrayOutputStream(); try { ImageIOTools.imageIOWrite(genericUnknownIcon, imageFormat.toString(), out); return DLNAThumbnailInputStream.toThumbnailInputStream(out.toByteArray()); } catch (IOException e) { LOGGER.warn( "Unexpected error while generating generic thumbnail for null resource: {}", e.getMessage() ); LOGGER.trace("", e); return null; } } IconType iconType = IconType.UNKNOWN; if (resource.getMedia() != null) { if (resource.getMedia().isAudio()) { iconType = IconType.AUDIO; } else if (resource.getMedia().isImage()) { iconType = IconType.IMAGE; } else if (resource.getMedia().isVideo()) { // FFmpeg parses images as video, try to rectify if (resource.getFormat() != null && resource.getFormat().isImage()) { iconType = IconType.IMAGE; } else { iconType = IconType.VIDEO; } } } else if (resource.getFormat() != null) { if (resource.getFormat().isAudio()) { iconType = IconType.AUDIO; } else if (resource.getFormat().isImage()) { iconType = IconType.IMAGE; } else if (resource.getFormat().isVideo()) { iconType = IconType.VIDEO; } } DLNAThumbnail image = null; cacheLock.lock(); try { if (!cache.containsKey(imageFormat)) { cache.put(imageFormat, new HashMap<IconType, Map<String,DLNAThumbnail>>()); } Map<IconType, Map<String,DLNAThumbnail>> typeCache = cache.get(imageFormat); if (!typeCache.containsKey(iconType)) { typeCache.put(iconType, new HashMap<String, DLNAThumbnail>()); } Map<String, DLNAThumbnail> imageCache = typeCache.get(iconType); String label = getLabelFromImageFormat(resource.getMedia()); if (label == null) { label = getLabelFromFormat(resource.getFormat()); } if (label == null) { label = getLabelFromContainer(resource.getMedia()); } if (label != null && label.length() < 5) { label = label.toUpperCase(Locale.ROOT); } else if (label != null && label.toLowerCase(Locale.ROOT).equals(label)) { label = StringUtils.capitalize(label); } if (imageCache.containsKey(label)) { return DLNAThumbnailInputStream.toThumbnailInputStream(imageCache.get(label)); } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Creating generic {} thumbnail for {} ({})", iconType.toString().toLowerCase(), label.toUpperCase(), imageFormat); } try { image = addFormatLabelToImage(label, imageFormat, iconType); } catch (IOException e) { LOGGER.warn("Unexpected error while generating generic thumbnail for \"{}\": {}", resource.getName(), e.getMessage()); LOGGER.trace("", e); } imageCache.put(label, image); } finally { cacheLock.unlock(); } return DLNAThumbnailInputStream.toThumbnailInputStream(image); } public DLNAThumbnailInputStream getGenericFolderIcon() { return DLNAThumbnailInputStream.toThumbnailInputStream(genericFolderThumbnail); } private String getLabelFromImageFormat(DLNAMediaInfo mediaInfo) { return mediaInfo != null && mediaInfo.isImage() && mediaInfo.getImageInfo() != null && mediaInfo.getImageInfo().getFormat() != null ? mediaInfo.getImageInfo().getFormat().toString() : null; } private String getLabelFromFormat(Format format) { if (format == null || format.getIdentifier() == null) { return null; } // Replace some Identifier names with prettier ones switch (format.getIdentifier()) { case AUDIO_AS_VIDEO: return "Audio as Video"; case MICRODVD: return "MicroDVD"; case SUBRIP: return "SubRip"; case THREEG2A: return "3G2A"; case THREEGA: return "3GA"; case WEBVTT: return "WebVTT"; default: return format.getIdentifier().toString(); } } private String getLabelFromContainer(DLNAMediaInfo mediaInfo) { return mediaInfo != null ? mediaInfo.getContainer() : null; } /** * Add the format(container) name of the media to the generic icon image. * * @param image BufferdImage to be the label added * @param label the media container name to be added as a label * @param renderer the renderer configuration * * @return the generic icon with the container label added and scaled in accordance with renderer setting */ private DLNAThumbnail addFormatLabelToImage(String label, ImageFormat imageFormat, IconType iconType) throws IOException { BufferedImage image; switch (iconType) { case AUDIO: image = genericAudioIcon; break; case IMAGE: image = genericImageIcon; break; case VIDEO: image = genericVideoIcon; break; default: image = genericUnknownIcon; } if (image != null) { // Make a copy ColorModel colorModel = image.getColorModel(); image = new BufferedImage(colorModel, image.copyData(null), colorModel.isAlphaPremultiplied(), null); } ByteArrayOutputStream out = null; if (label != null && image != null) { out = new ByteArrayOutputStream(); Graphics2D g = image.createGraphics(); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); try { int size = 40; Font font = new Font(Font.SANS_SERIF, Font.BOLD, size); FontMetrics metrics = g.getFontMetrics(font); while (size > 7 && metrics.stringWidth(label) > 135) { size--; font = new Font(Font.SANS_SERIF, Font.BOLD, size); metrics = g.getFontMetrics(font); } // Text center point 127x, 49y - calculate centering coordinates int x = 127 - metrics.stringWidth(label) / 2; int y = 46 + metrics.getAscent() / 2; g.drawImage(image, 0, 0, null); g.setColor(Color.WHITE); g.setFont(font); g.drawString(label, x, y); ImageIO.setUseCache(false); ImageIOTools.imageIOWrite(image, imageFormat.toString(), out); } finally { g.dispose(); } } return out != null ? DLNAThumbnail.toThumbnail(out.toByteArray(), 0, 0, ScaleType.MAX, imageFormat, false) : null; } /** * Reads a resource from a given resource path into a * {@link BufferedImage}. {@code /resources/images/} is already * prepended to the path and only the rest should be specified. * * @param resourcePath the path to the resource relative to * {@code /resources/images/}} * @return The {@link BufferedImage} created from the specified resource or * {@code null} if the path is invalid. */ protected BufferedImage readBufferedImage(String resourcePath) { InputStream inputStream = getResourceAsStream(resourcePath); if (inputStream != null) { try { return ImageIO.read(inputStream); } catch (IOException e) { LOGGER.error("Could not read resource \"{}\": {}", resourcePath, e.getMessage()); LOGGER.trace("", e); return null; } } return null; } protected InputStream getResourceAsStream(String resourcePath) { return PMS.class.getResourceAsStream("/resources/images/" + resourcePath); } protected static enum IconType { AUDIO, IMAGE, UNKNOWN, VIDEO } }