/******************************************************************************* * Copyright (c) 2016 Weasis Team and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Nicolas Roduit - initial API and implementation *******************************************************************************/ package org.weasis.core.api.media.data; import java.awt.Component; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.event.KeyListener; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelListener; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import java.io.File; import java.io.IOException; import java.lang.ref.SoftReference; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import javax.imageio.ImageIO; import javax.media.jai.JAI; import javax.media.jai.PlanarImage; import javax.media.jai.operator.SubsampleAverageDescriptor; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.SwingConstants; import javax.swing.SwingWorker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.weasis.core.api.Messages; import org.weasis.core.api.gui.util.AppProperties; import org.weasis.core.api.image.OpManager; import org.weasis.core.api.image.util.ImageFiler; import org.weasis.core.api.media.MimeInspector; import org.weasis.core.api.util.FileUtil; import org.weasis.core.api.util.FontTools; import org.weasis.core.api.util.ThreadUtil; @SuppressWarnings("serial") public class Thumbnail extends JLabel { private static final Logger LOGGER = LoggerFactory.getLogger(Thumbnail.class); public static final File THUMBNAIL_CACHE_DIR = AppProperties.buildAccessibleTempDirectory(AppProperties.FILE_CACHE_DIR.getName(), "thumb"); //$NON-NLS-1$ public static final ExecutorService THUMB_LOADER = ThreadUtil.buildNewSingleThreadExecutor("Thumbnail Loader"); //$NON-NLS-1$ public static final RenderingHints DownScaleQualityHints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); static { DownScaleQualityHints.add(new RenderingHints(JAI.KEY_TILE_CACHE, null)); } public static final int MIN_SIZE = 48; public static final int DEFAULT_SIZE = 112; public static final int MAX_SIZE = 256; private SoftReference<BufferedImage> imageSoftRef; protected volatile boolean readable = true; protected volatile AtomicBoolean loading = new AtomicBoolean(false); protected File thumbnailPath = null; protected int thumbnailSize; public Thumbnail(int thumbnailSize) { super(null, null, SwingConstants.CENTER); this.thumbnailSize = thumbnailSize; } public Thumbnail(final MediaElement media, int thumbnailSize, boolean keepMediaCache, OpManager opManager) { super(null, null, SwingConstants.CENTER); if (media == null) { throw new IllegalArgumentException("image cannot be null"); //$NON-NLS-1$ } this.thumbnailSize = thumbnailSize; init(media, keepMediaCache, opManager); } /** * @param media * @param keepMediaCache * if true will remove the media from cache after building the thumbnail. Only when media is an image. */ protected void init(MediaElement media, boolean keepMediaCache, OpManager opManager) { this.setFont(FontTools.getFont10()); buildThumbnail(media, keepMediaCache, opManager); } public void registerListeners() { removeMouseAndKeyListener(); } public static RenderedImage createThumbnail(RenderedImage source) { if (source == null) { return null; } final double scale = Math.min(Thumbnail.MAX_SIZE / (double) source.getHeight(), Thumbnail.MAX_SIZE / (double) source.getWidth()); return scale < 1.0 ? SubsampleAverageDescriptor.create(source, scale, scale, Thumbnail.DownScaleQualityHints).getRendering() : source; } protected synchronized void buildThumbnail(MediaElement media, boolean keepMediaCache, OpManager opManager) { imageSoftRef = null; Icon icon = MimeInspector.unknownIcon; String type = Messages.getString("Thumbnail.unknown"); //$NON-NLS-1$ if (media != null) { String mime = media.getMimeType(); if (mime != null) { if (mime.startsWith("image")) { //$NON-NLS-1$ type = Messages.getString("Thumbnail.img"); //$NON-NLS-1$ icon = MimeInspector.imageIcon; } else if (mime.startsWith("video")) { //$NON-NLS-1$ type = Messages.getString("Thumbnail.video"); //$NON-NLS-1$ icon = MimeInspector.videoIcon; } else if (mime.startsWith("audio")) { //$NON-NLS-1$ type = Messages.getString("Thumbnail.audio"); //$NON-NLS-1$ icon = MimeInspector.audioIcon; } else if (mime.equals("sr/dicom")) { //$NON-NLS-1$ type = Messages.getString("Thumbnail.dicom_sr"); //$NON-NLS-1$ icon = MimeInspector.textIcon; } else if (mime.startsWith("txt")) { //$NON-NLS-1$ type = Messages.getString("Thumbnail.text"); //$NON-NLS-1$ icon = MimeInspector.textIcon; } else if (mime.endsWith("html")) { //$NON-NLS-1$ type = Messages.getString("Thumbnail.html"); //$NON-NLS-1$ icon = MimeInspector.htmlIcon; } else if (mime.equals("application/pdf")) { //$NON-NLS-1$ type = Messages.getString("Thumbnail.pdf"); //$NON-NLS-1$ icon = MimeInspector.pdfIcon; } else { type = mime; } } } setIcon(media, icon, type, keepMediaCache, opManager); } private void setIcon(final MediaElement media, final Icon mime, final String type, final boolean keepMediaCache, OpManager opManager) { this.setSize(thumbnailSize, thumbnailSize); ImageIcon icon = new ImageIcon() { @Override public void paintIcon(Component c, Graphics g, int x, int y) { Graphics2D g2d = (Graphics2D) g; int width = thumbnailSize; int height = thumbnailSize; final BufferedImage thumbnail = Thumbnail.this.getImage(media, keepMediaCache, opManager); if (thumbnail == null) { FontMetrics fontMetrics = g2d.getFontMetrics(); int fheight = y + (thumbnailSize - fontMetrics.getAscent() + 5 - mime.getIconHeight()) / 2; mime.paintIcon(c, g2d, x + (thumbnailSize - mime.getIconWidth()) / 2, fheight); int startx = x + (thumbnailSize - fontMetrics.stringWidth(type)) / 2; g2d.drawString(type, startx, fheight + mime.getIconHeight() + fontMetrics.getAscent() + 5); } else { width = thumbnail.getWidth(); height = thumbnail.getHeight(); x += (thumbnailSize - width) / 2; y += (thumbnailSize - height) / 2; g2d.drawImage(thumbnail, AffineTransform.getTranslateInstance(x, y), null); } drawOverIcon(g2d, x, y, width, height); } @Override public int getIconWidth() { return thumbnailSize; } @Override public int getIconHeight() { return thumbnailSize; } }; setIcon(icon); } protected void drawOverIcon(Graphics2D g2d, int x, int y, int width, int height) { } public File getThumbnailPath() { return thumbnailPath; } public synchronized BufferedImage getImage(final MediaElement media, final boolean keepMediaCache, final OpManager opManager) { if ((imageSoftRef == null && readable) || (imageSoftRef != null && imageSoftRef.get() == null)) { if (loading.compareAndSet(false, true)) { try { SwingWorker<Boolean, String> thumbnailReader = new SwingWorker<Boolean, String>() { @Override protected void done() { repaint(); } @Override protected Boolean doInBackground() throws Exception { loadThumbnail(media, keepMediaCache, opManager); return Boolean.TRUE; } }; THUMB_LOADER.execute(thumbnailReader); } catch (Exception e) { LOGGER.error("Cannot build thumbnail!", e);//$NON-NLS-1$ loading.set(false); } } } if (imageSoftRef == null) { return null; } return imageSoftRef.get(); } private void loadThumbnail(final MediaElement media, final boolean keepMediaCache, final OpManager opManager) throws Exception { try { File file = thumbnailPath; boolean noPath = file == null || !file.canRead(); if (noPath && media != null) { String path = (String) media.getTagValue(TagW.ThumbnailPath); if (path != null) { file = new File(path); if (file.canRead()) { noPath = false; thumbnailPath = file; } } } if (noPath) { if (media instanceof ImageElement) { final ImageElement image = (ImageElement) media; PlanarImage imgPl = image.getImage(opManager); if (imgPl != null) { RenderedImage img = image.getRenderedImage(imgPl); final RenderedImage thumb = createThumbnail(img); try { file = File.createTempFile("tumb_", ".jpg", Thumbnail.THUMBNAIL_CACHE_DIR); //$NON-NLS-1$ //$NON-NLS-2$ } catch (IOException e) { LOGGER.error("Cannot create file for thumbnail!", e);//$NON-NLS-1$ } try { BufferedImage thumbnail = null; if (file != null) { if (ImageFiler.writeJPG(file, thumb, 0.75f)) { /* * Write the thumbnail in temp folder, better than getting the thumbnail directly * from t.getAsBufferedImage() (it is true if the image is big and cannot handle all * the tiles in memory) */ image.setTag(TagW.ThumbnailPath, file.getPath()); thumbnailPath = file; return; } else { // out of memory } } else if (thumb instanceof PlanarImage) { thumbnail = ((PlanarImage) thumb).getAsBufferedImage(); } if (thumbnail == null) { readable = false; } else { imageSoftRef = new SoftReference<>(thumbnail); } } finally { if (!keepMediaCache) { // Prevent to many files open on Linux (Ubuntu => 1024) and close image stream image.removeImageFromCache(); } } } else { readable = false; } } } else { Load ref = new Load(file); // loading images sequentially, only one thread pool Future<BufferedImage> future = ImageElement.IMAGE_LOADER.submit(ref); BufferedImage img = null; BufferedImage thumb = null; try { img = future.get(); if (img == null) { thumb = null; } else { int width = img.getWidth(); int height = img.getHeight(); if (width > thumbnailSize || height > thumbnailSize) { final double scale = Math.min(thumbnailSize / (double) height, thumbnailSize / (double) width); PlanarImage t = scale < 1.0 ? SubsampleAverageDescriptor.create(img, scale, scale, DownScaleQualityHints) : PlanarImage.wrapRenderedImage(img); thumb = t.getAsBufferedImage(); t.dispose(); } else { thumb = img; } } } catch (InterruptedException e) { // Re-assert the thread's interrupted status Thread.currentThread().interrupt(); // We don't need the result, so cancel the task too future.cancel(true); } catch (ExecutionException e) { LOGGER.error("Cannot read thumbnail pixel data!: {}", file, e);//$NON-NLS-1$ } if (thumb == null && media != null) { readable = false; } else { imageSoftRef = new SoftReference<>(thumb); } } } finally { loading.set(false); } } public void dispose() { // Unload image from memory if (imageSoftRef != null) { BufferedImage temp = imageSoftRef.get(); if (temp != null) { temp.flush(); } if (thumbnailPath != null && thumbnailPath.getPath().startsWith(AppProperties.FILE_CACHE_DIR.getPath())) { FileUtil.delete(thumbnailPath); } } removeMouseAndKeyListener(); } public void removeMouseAndKeyListener() { MouseListener[] listener = this.getMouseListeners(); MouseMotionListener[] motionListeners = this.getMouseMotionListeners(); KeyListener[] keyListeners = this.getKeyListeners(); MouseWheelListener[] wheelListeners = this.getMouseWheelListeners(); for (int i = 0; i < listener.length; i++) { this.removeMouseListener(listener[i]); } for (int i = 0; i < motionListeners.length; i++) { this.removeMouseMotionListener(motionListeners[i]); } for (int i = 0; i < keyListeners.length; i++) { this.removeKeyListener(keyListeners[i]); } for (int i = 0; i < wheelListeners.length; i++) { this.removeMouseWheelListener(wheelListeners[i]); } } class Load implements Callable<BufferedImage> { private final File path; public Load(File path) { this.path = path; } @Override public BufferedImage call() throws Exception { return ImageIO.read(path); } } }