diff --git a/build.third_step1-jdk11.gradle b/build.third_step1-jdk11.gradle index 912577a40..d2754f480 100644 --- a/build.third_step1-jdk11.gradle +++ b/build.third_step1-jdk11.gradle @@ -48,6 +48,7 @@ sourceSets{ "${srcDir}/fine-freehep/src/main/java", "${srcDir}/fine-guava/src", "${srcDir}/fine-hsqldb/src/main/java", + "${srcDir}/fine-iconloader/src/main/java", "${srcDir}/fine-icu4j/src", "${srcDir}/fine-imageJ/src/main/java", "${srcDir}/fine-j2v8/src", @@ -111,6 +112,7 @@ def resourceDirs = [ "${srcDir}/fine-freehep/src/main/java", "${srcDir}/fine-guava/src", "${srcDir}/fine-hsqldb/src/main/java", + "${srcDir}/fine-iconloader/src/main/java", "${srcDir}/fine-icu4j/src", "${srcDir}/fine-imageJ/src/main/java", "${srcDir}/fine-j2v8/src", @@ -177,6 +179,7 @@ dependencies{ compile fileTree(dir:"${srcDir}/fine-cssparser/lib",include:'**/*.jar') compile fileTree(dir:"${srcDir}/fine-freehep/lib",include:'**/*.jar') compile fileTree(dir:"${srcDir}/fine-hsqldb/lib",include:'**/*.jar') + compile fileTree(dir:"${srcDir}/fine-iconloader/lib",include:'**/*.jar') compile fileTree(dir:"${srcDir}/fine-jgit/lib",include:'**/*.jar') compile fileTree(dir:"${srcDir}/fine-org-dom4j/lib",include:'**/*.jar') compile fileTree(dir:"${srcDir}/fine-sense4/lib",include:'**/*.jar') diff --git a/build.third_step1.gradle b/build.third_step1.gradle index 396f7fec9..cb52b4b26 100644 --- a/build.third_step1.gradle +++ b/build.third_step1.gradle @@ -49,6 +49,7 @@ sourceSets{ "${srcDir}/fine-freehep/src/main/java", "${srcDir}/fine-guava/src", "${srcDir}/fine-hsqldb/src/main/java", + "${srcDir}/fine-iconloader/src/main/java", "${srcDir}/fine-icu4j/src", "${srcDir}/fine-imageJ/src/main/java", "${srcDir}/fine-j2v8/src", @@ -108,6 +109,7 @@ dependencies{ compile fileTree(dir:"${srcDir}/fine-cssparser/lib",include:'**/*.jar') compile fileTree(dir:"${srcDir}/fine-freehep/lib",include:'**/*.jar') compile fileTree(dir:"${srcDir}/fine-hsqldb/lib",include:'**/*.jar') + compile fileTree(dir:"${srcDir}/fine-iconloader/lib",include:'**/*.jar') compile fileTree(dir:"${srcDir}/fine-jgit/lib",include:'**/*.jar') compile fileTree(dir:"${srcDir}/fine-org-dom4j/lib",include:'**/*.jar') compile fileTree(dir:"${srcDir}/fine-sense4/lib",include:'**/*.jar') diff --git a/fine-iconloader/README.md b/fine-iconloader/README.md new file mode 100644 index 000000000..94978010e --- /dev/null +++ b/fine-iconloader/README.md @@ -0,0 +1,2 @@ +源码地址:https://github.com/bulenkov/iconloader
+版本:1.0 \ No newline at end of file diff --git a/fine-iconloader/lib/annotations.jar b/fine-iconloader/lib/annotations.jar new file mode 100644 index 000000000..91a44ecf9 Binary files /dev/null and b/fine-iconloader/lib/annotations.jar differ diff --git a/fine-iconloader/lib/eawtstub.jar b/fine-iconloader/lib/eawtstub.jar new file mode 100644 index 000000000..1f60d7f64 Binary files /dev/null and b/fine-iconloader/lib/eawtstub.jar differ diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/AppleHiDPIScaledImage.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/AppleHiDPIScaledImage.java new file mode 100644 index 000000000..7e0d0dbf8 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/AppleHiDPIScaledImage.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader; + +import apple.awt.CImage; + +import java.awt.*; +import java.awt.image.BufferedImage; + +/** + * @author Konstantin Bulenkov + */ +public class AppleHiDPIScaledImage { + public static BufferedImage create(int width, int height, int imageType) { + return new CImage.HiDPIScaledImage(width, height, imageType) { + @Override + protected void drawIntoImage(BufferedImage image, float scale) { + } + }; + } + + public static boolean is(Image image) { + return image instanceof CImage.HiDPIScaledImage; + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/HiDPIScaledGraphics.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/HiDPIScaledGraphics.java new file mode 100644 index 000000000..70f63d886 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/HiDPIScaledGraphics.java @@ -0,0 +1,484 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader; + +import com.bulenkov.iconloader.util.GraphicsUtil; + +import java.awt.*; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ImageObserver; +import java.awt.image.RenderedImage; +import java.awt.image.renderable.RenderableImage; +import java.text.AttributedCharacterIterator; +import java.util.Map; + +/** + * @author Konstantin Bulenkov + */ +class HiDPIScaledGraphics extends Graphics2D { + protected final Graphics2D myPeer; + private BufferedImage myImage; + + public HiDPIScaledGraphics(Graphics g, BufferedImage image) { + myImage = image; + myPeer = (Graphics2D) g; + scale(2, 2); + GraphicsUtil.setupAAPainting(myPeer); + } + + @Override + public void draw3DRect(int x, int y, int width, int height, boolean raised) { + myPeer.draw3DRect(x, y, width, height, raised); + } + + @Override + public void fill3DRect(int x, int y, int width, int height, boolean raised) { + myPeer.fill3DRect(x, y, width, height, raised); + } + + @Override + public void draw(Shape s) { + myPeer.draw(s); + } + + @Override + public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) { + return myPeer.drawImage(img, xform, obs); + } + + @Override + public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { + myPeer.drawImage(img, op, x, y); + } + + @Override + public void drawRenderedImage(RenderedImage img, AffineTransform xform) { + myPeer.drawRenderedImage(img, xform); + } + + @Override + public void drawRenderableImage(RenderableImage img, AffineTransform xform) { + myPeer.drawRenderableImage(img, xform); + } + + @Override + public void drawString(String str, int x, int y) { + myPeer.drawString(str, x, y); + } + + @Override + public void drawString(String str, float x, float y) { + myPeer.drawString(str, x, y); + } + + @Override + public void drawString(AttributedCharacterIterator iterator, int x, int y) { + myPeer.drawString(iterator, x, y); + } + + @Override + public void drawString(AttributedCharacterIterator iterator, float x, float y) { + myPeer.drawString(iterator, x, y); + } + + @Override + public void drawGlyphVector(GlyphVector g, float x, float y) { + myPeer.drawGlyphVector(g, x, y); + } + + @Override + public void fill(Shape s) { + myPeer.fill(s); + } + + @Override + public boolean hit(Rectangle rect, Shape s, boolean onStroke) { + return myPeer.hit(rect, s, onStroke); + } + + @Override + public GraphicsConfiguration getDeviceConfiguration() { + return myPeer.getDeviceConfiguration(); + } + + @Override + public void setComposite(Composite comp) { + myPeer.setComposite(comp); + } + + @Override + public void setPaint(Paint paint) { + myPeer.setPaint(paint); + } + + @Override + public void setStroke(Stroke s) { + myPeer.setStroke(s); + } + + @Override + public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) { + myPeer.setRenderingHint(hintKey, hintValue); + } + + @Override + public Object getRenderingHint(RenderingHints.Key hintKey) { + return myPeer.getRenderingHint(hintKey); + } + + @Override + public void setRenderingHints(Map hints) { + myPeer.setRenderingHints(hints); + } + + @Override + public void addRenderingHints(Map hints) { + myPeer.addRenderingHints(hints); + } + + @Override + public RenderingHints getRenderingHints() { + return myPeer.getRenderingHints(); + } + + @Override + public void translate(int x, int y) { + myPeer.translate(x, y); + } + + @Override + public void translate(double tx, double ty) { + myPeer.translate(tx, ty); + } + + @Override + public void rotate(double theta) { + myPeer.rotate(theta); + } + + @Override + public void rotate(double theta, double x, double y) { + myPeer.rotate(theta, x, y); + } + + @Override + public void scale(double sx, double sy) { + myPeer.scale(sx, sy); + } + + @Override + public void shear(double shx, double shy) { + myPeer.shear(shx, shy); + } + + @Override + public void transform(AffineTransform Tx) { + myPeer.transform(Tx); + } + + @Override + public void setTransform(AffineTransform Tx) { + myPeer.setTransform(Tx); + } + + @Override + public AffineTransform getTransform() { + return myPeer.getTransform(); + } + + @Override + public Paint getPaint() { + return myPeer.getPaint(); + } + + @Override + public Composite getComposite() { + return myPeer.getComposite(); + } + + @Override + public void setBackground(Color color) { + myPeer.setBackground(color); + } + + @Override + public Color getBackground() { + return myPeer.getBackground(); + } + + @Override + public Stroke getStroke() { + return myPeer.getStroke(); + } + + @Override + public void clip(Shape s) { + myPeer.clip(s); + } + + @Override + public FontRenderContext getFontRenderContext() { + return myPeer.getFontRenderContext(); + } + + @Override + public Graphics create() { + Graphics g = myPeer.create(); + return g; + } + + @Override + public Graphics create(int x, int y, int width, int height) { + return myPeer.create(x, y, width, height); + } + + @Override + public Color getColor() { + return myPeer.getColor(); + } + + @Override + public void setColor(Color c) { + myPeer.setColor(c); + } + + @Override + public void setPaintMode() { + myPeer.setPaintMode(); + } + + @Override + public void setXORMode(Color c1) { + myPeer.setXORMode(c1); + } + + @Override + public Font getFont() { + return myPeer.getFont(); + } + + @Override + public void setFont(Font font) { + myPeer.setFont(font); + } + + @Override + public FontMetrics getFontMetrics() { + return myPeer.getFontMetrics(); + } + + @Override + public FontMetrics getFontMetrics(Font f) { + return myPeer.getFontMetrics(f); + } + + @Override + public Rectangle getClipBounds() { + return myPeer.getClipBounds(); + } + + @Override + public void clipRect(int x, int y, int width, int height) { + myPeer.clipRect(x, y, width, height); + } + + @Override + public void setClip(int x, int y, int width, int height) { + myPeer.setClip(x, y, width, height); + } + + @Override + public Shape getClip() { + return myPeer.getClip(); + } + + @Override + public void setClip(Shape clip) { + myPeer.setClip(clip); + } + + @Override + public void copyArea(int x, int y, int width, int height, int dx, int dy) { + myPeer.copyArea(x, y, width, height, dx, dy); + } + + @Override + public void drawLine(int x1, int y1, int x2, int y2) { + myPeer.drawLine(x1, y1, x2, y2); + } + + @Override + public void fillRect(int x, int y, int width, int height) { + myPeer.fillRect(x, y, width, height); + } + + @Override + public void drawRect(int x, int y, int width, int height) { + myPeer.drawRect(x, y, width, height); + } + + @Override + public void clearRect(int x, int y, int width, int height) { + myPeer.clearRect(x, y, width, height); + } + + @Override + public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { + myPeer.drawRoundRect(x, y, width, height, arcWidth, arcHeight); + } + + @Override + public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { + myPeer.fillRoundRect(x, y, width, height, arcWidth, arcHeight); + } + + @Override + public void drawOval(int x, int y, int width, int height) { + myPeer.drawOval(x, y, width, height); + } + + @Override + public void fillOval(int x, int y, int width, int height) { + myPeer.fillOval(x, y, width, height); + } + + @Override + public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + myPeer.drawArc(x, y, width, height, startAngle, arcAngle); + } + + @Override + public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + myPeer.fillArc(x, y, width, height, startAngle, arcAngle); + } + + @Override + public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) { + + myPeer.drawPolyline(xPoints, yPoints, nPoints); + } + + @Override + public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { + + myPeer.drawPolygon(xPoints, yPoints, nPoints); + } + + @Override + public void drawPolygon(Polygon p) { + + myPeer.drawPolygon(p); + } + + @Override + public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { + + myPeer.fillPolygon(xPoints, yPoints, nPoints); + } + + @Override + public void fillPolygon(Polygon p) { + + myPeer.fillPolygon(p); + } + + @Override + public void drawChars(char[] data, int offset, int length, int x, int y) { + myPeer.drawChars(data, offset, length, x, y); + } + + @Override + public void drawBytes(byte[] data, int offset, int length, int x, int y) { + myPeer.drawBytes(data, offset, length, x, y); + } + + @Override + public boolean drawImage(Image img, int x, int y, ImageObserver observer) { + return myPeer.drawImage(img, x, y, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) { + return myPeer.drawImage(img, x, y, width, height, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) { + return myPeer.drawImage(img, x, y, bgcolor, observer); + } + + @Override + public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer) { + return myPeer.drawImage(img, x, y, width, height, bgcolor, observer); + } + + @Override + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer) { + return myPeer.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer); + } + + @Override + public boolean drawImage(Image img, + int dx1, + int dy1, + int dx2, + int dy2, + int sx1, + int sy1, + int sx2, + int sy2, + Color bgcolor, + ImageObserver observer) { + return myPeer.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer); + } + + @Override + public void dispose() { + myPeer.dispose(); + } + + @Override + public void finalize() { + myPeer.finalize(); + } + + @Override + public String toString() { + return myPeer.toString(); + } + + @Override + @Deprecated + public Rectangle getClipRect() { + return myPeer.getClipRect(); + } + + @Override + public boolean hitClip(int x, int y, int width, int height) { + return myPeer.hitClip(x, y, width, height); + } + + @Override + public Rectangle getClipBounds(Rectangle r) { + return myPeer.getClipBounds(r); + } + +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/IconLoader.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/IconLoader.java new file mode 100644 index 000000000..41771c73a --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/IconLoader.java @@ -0,0 +1,557 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader; + +import com.bulenkov.iconloader.util.*; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.ImageFilter; +import java.lang.ref.Reference; +import java.lang.reflect.Field; +import java.net.URL; +import java.util.*; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author Konstantin Bulenkov + */ +@SuppressWarnings("UnusedDeclaration") +public final class IconLoader { + public static boolean STRICT = false; + private static boolean USE_DARK_ICONS = UIUtil.isUnderDarcula(); + + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private static final ConcurrentMap ourIconsCache = new ConcurrentHashMap(100, 0.9f, 2); + + /** + * This cache contains mapping between icons and disabled icons. + */ + private static final Map ourIcon2DisabledIcon = new WeakHashMap(200); + + private static float SCALE = JBUI.scale(1f); + private static ImageFilter IMAGE_FILTER; + + private static final ImageIcon EMPTY_ICON = new ImageIcon(UIUtil.createImage(1, 1, BufferedImage.TYPE_3BYTE_BGR)) { + @NonNls + public String toString() { + return "Empty icon " + super.toString(); + } + }; + + private static AtomicBoolean ourIsActivated = new AtomicBoolean(true); + private static AtomicBoolean ourIsSaveRealIconPath = new AtomicBoolean(false); + public static final Component ourComponent = new Component() {}; + + private IconLoader() { } + + @Deprecated + public static Icon getIcon(@NotNull final Image image) { + return new JBImageIcon(image); + } + + public static void setUseDarkIcons(boolean useDarkIcons) { + USE_DARK_ICONS = useDarkIcons; + clearCache(); + } + + public static void setScale(float scale) { + if (scale != SCALE) { + SCALE = scale; + clearCache(); + } + } + + public static void setFilter(ImageFilter filter) { + if (!Registry.is("color.blindness.icon.filter")) { + filter = null; + } + if (IMAGE_FILTER != filter) { + IMAGE_FILTER = filter; + clearCache(); + } + } + + private static void clearCache() { + ourIconsCache.clear(); + ourIcon2DisabledIcon.clear(); + } + + //TODO[kb] support iconsets + //public static Icon getIcon(@NotNull final String path, @NotNull final String darkVariantPath) { + // return new InvariantIcon(getIcon(path), getIcon(darkVariantPath)); + //} + + @NotNull + public static Icon getIcon(@NonNls @NotNull final String path) { + Class callerClass = ReflectionUtil.getGrandCallerClass(); + + assert callerClass != null : path; + return getIcon(path, callerClass); + } + + @Nullable + private static Icon getReflectiveIcon(@NotNull String path, ClassLoader classLoader) { + try { + @NonNls String pckg = path.startsWith("AllIcons.") ? "com.intellij.icons." : "icons."; + Class cur = Class.forName(pckg + path.substring(0, path.lastIndexOf('.')).replace('.', '$'), true, classLoader); + Field field = cur.getField(path.substring(path.lastIndexOf('.') + 1)); + + return (Icon)field.get(null); + } + catch (Exception e) { + return null; + } + } + + /** + * Might return null if icon was not found. + * Use only if you expected null return value, otherwise see {@link IconLoader#getIcon(String)} + */ + @Nullable + public static Icon findIcon(@NonNls @NotNull String path) { + Class callerClass = ReflectionUtil.getGrandCallerClass(); + if (callerClass == null) return null; + return findIcon(path, callerClass); + } + + @NotNull + public static Icon getIcon(@NotNull String path, @NotNull final Class aClass) { + final Icon icon = findIcon(path, aClass); + if (icon == null) { + System.err.println("Icon cannot be found in '" + path + "', aClass='" + aClass + "'"); + } + return icon; + } + + public static void activate() { + ourIsActivated.set(true); + } + + public static void disable() { + ourIsActivated.set(false); + } + + public static boolean isLoaderDisabled() { + return !ourIsActivated.get(); + } + + /** + * This method is for test purposes only + */ + static void enableSaveRealIconPath() { + ourIsSaveRealIconPath.set(true); + } + + /** + * Might return null if icon was not found. + * Use only if you expected null return value, otherwise see {@link IconLoader#getIcon(String, Class)} + */ + @Nullable + public static Icon findIcon(@NotNull final String path, @NotNull final Class aClass) { + return findIcon(path, aClass, false); + } + + @Nullable + public static Icon findIcon(@NotNull String path, @NotNull final Class aClass, boolean computeNow) { + return findIcon(path, aClass, computeNow, STRICT); + } + + @Nullable + public static Icon findIcon(@NotNull String path, @NotNull final Class aClass, boolean computeNow, boolean strict) { + String originalPath = path; + path = patchPath(path); + if (isReflectivePath(path)) return getReflectiveIcon(path, aClass.getClassLoader()); + + URL myURL = aClass.getResource(path); + if (myURL == null) { + if (strict) throw new RuntimeException("Can't find icon in '" + path + "' near " + aClass); + return null; + } + final Icon icon = findIcon(myURL); + if (icon instanceof CachedImageIcon) { + ((CachedImageIcon)icon).myOriginalPath = originalPath; + ((CachedImageIcon)icon).myClassLoader = aClass.getClassLoader(); + } + return icon; + } + + private static String patchPath(@NotNull String path) { +// for (IconPathPatcher patcher : ourPatchers) { +// String newPath = patcher.patchPath(path); +// if (newPath != null) { +// path = newPath; +// } +// } + return path; + } + + private static boolean isReflectivePath(@NotNull String path) { + List paths = StringUtil.split(path, "."); + return paths.size() > 1 && paths.get(0).endsWith("Icons"); + } + + @Nullable + public static Icon findIcon(URL url) { + return findIcon(url, true); + } + + @Nullable + public static Icon findIcon(URL url, boolean useCache) { + if (url == null) { + return null; + } + CachedImageIcon icon = ourIconsCache.get(url); + if (icon == null) { + icon = new CachedImageIcon(url); + if (useCache) { + icon = ConcurrencyUtil.cacheOrGet(ourIconsCache, url, icon); + } + } + return icon; + } + + @Nullable + public static Icon findIcon(@NotNull String path, @NotNull ClassLoader classLoader) { + String originalPath = path; + path = patchPath(path); + if (isReflectivePath(path)) return getReflectiveIcon(path, classLoader); + if (!StringUtil.startsWithChar(path, '/')) return null; + + final URL url = classLoader.getResource(path.substring(1)); + final Icon icon = findIcon(url); + if (icon instanceof CachedImageIcon) { + ((CachedImageIcon)icon).myOriginalPath = originalPath; + ((CachedImageIcon)icon).myClassLoader = classLoader; + } + return icon; + } + + @Nullable + private static ImageIcon checkIcon(final Image image, @NotNull URL url) { + if (image == null || image.getHeight(LabelHolder.ourFakeComponent) < 1) { // image wasn't loaded or broken + return null; + } + + final Icon icon = getIcon(image); + if (icon != null && !isGoodSize(icon)) { + return EMPTY_ICON; + } + + return (ImageIcon)icon; + } + + public static boolean isGoodSize(@NotNull final Icon icon) { + return icon.getIconWidth() > 0 && icon.getIconHeight() > 0; + } + + /** + * Gets (creates if necessary) disabled icon based on the passed one. + * + * @return ImageIcon constructed from disabled image of passed icon. + */ + @Nullable + public static Icon getDisabledIcon(Icon icon) { + if (icon instanceof LazyIcon) icon = ((LazyIcon)icon).getOrComputeIcon(); + if (icon == null) return null; + + Icon disabledIcon = ourIcon2DisabledIcon.get(icon); + if (disabledIcon == null) { + if (!isGoodSize(icon)) { + return EMPTY_ICON; + } + final int scale = UIUtil.isRetina() ? 2 : 1; + @SuppressWarnings("UndesirableClassUsage") + BufferedImage image = new BufferedImage(scale*icon.getIconWidth(), scale*icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); + final Graphics2D graphics = image.createGraphics(); + + graphics.setColor(UIUtil.TRANSPARENT_COLOR); + graphics.fillRect(0, 0, icon.getIconWidth(), icon.getIconHeight()); + graphics.scale(scale, scale); + icon.paintIcon(LabelHolder.ourFakeComponent, graphics, 0, 0); + + graphics.dispose(); + + Image img = ImageUtil.filter(image, UIUtil.getGrayFilter()); + if (UIUtil.isRetina()) img = RetinaImage.createFrom(img); + + disabledIcon = new JBImageIcon(img); + ourIcon2DisabledIcon.put(icon, disabledIcon); + } + return disabledIcon; + } + + public static Icon getTransparentIcon(@NotNull final Icon icon) { + return getTransparentIcon(icon, 0.5f); + } + + public static Icon getTransparentIcon(@NotNull final Icon icon, final float alpha) { + return new RetrievableIcon() { + @Nullable + @Override + public Icon retrieveIcon() { + return icon; + } + + @Override + public int getIconHeight() { + return icon.getIconHeight(); + } + + @Override + public int getIconWidth() { + return icon.getIconWidth(); + } + + @Override + public void paintIcon(final Component c, final Graphics g, final int x, final int y) { + final Graphics2D g2 = (Graphics2D)g; + final Composite saveComposite = g2.getComposite(); + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)); + icon.paintIcon(c, g2, x, y); + g2.setComposite(saveComposite); + } + }; + } + + /** + * Gets a snapshot of the icon, immune to changes made by these calls: + * {@link IconLoader#setScale(float)}, {@link IconLoader#setFilter(ImageFilter)}, {@link IconLoader#setUseDarkIcons(boolean)} + * + * @param icon the source icon + * @return the icon snapshot + */ + @NotNull + public static Icon getIconSnapshot(@NotNull Icon icon) { + if (icon instanceof CachedImageIcon) { + return ((CachedImageIcon)icon).getRealIcon(); + } + return icon; + } + + public static final class CachedImageIcon implements ScalableIcon { + private volatile Object myRealIcon; + public String myOriginalPath; + private ClassLoader myClassLoader; + @NotNull + private URL myUrl; + private volatile boolean dark; + private volatile float scale; + private volatile int numberOfPatchers = 0; + + private volatile ImageFilter filter; + private final MyScaledIconsCache myScaledIconsCache = new MyScaledIconsCache(); + + public CachedImageIcon(@NotNull URL url) { + myUrl = url; + dark = USE_DARK_ICONS; + scale = SCALE; + filter = IMAGE_FILTER; + } + + @NotNull + private synchronized ImageIcon getRealIcon() { + if (isLoaderDisabled() && (myRealIcon == null || dark != USE_DARK_ICONS || scale != SCALE || filter != IMAGE_FILTER)) return EMPTY_ICON; + + if (!isValid()) { + myRealIcon = null; + dark = USE_DARK_ICONS; + scale = SCALE; + filter = IMAGE_FILTER; + myScaledIconsCache.clear(); + } + Object realIcon = myRealIcon; + if (realIcon instanceof Icon) return (ImageIcon)realIcon; + + ImageIcon icon; + if (realIcon instanceof Reference) { + icon = ((Reference)realIcon).get(); + if (icon != null) return (ImageIcon)icon; + } + + Image image = ImageLoader.loadFromUrl(myUrl, true, filter); + icon = checkIcon(image, myUrl); + + if (icon != null) { + if (icon.getIconWidth() < 50 && icon.getIconHeight() < 50) { + realIcon = icon; + } + else { + realIcon = new SoftReference(icon); + } + myRealIcon = realIcon; + } + + return icon == null ? EMPTY_ICON : icon; + } + + private boolean isValid() { + return dark == USE_DARK_ICONS && scale == SCALE && filter == IMAGE_FILTER; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + getRealIcon().paintIcon(c, g, x, y); + } + + @Override + public int getIconWidth() { + return getRealIcon().getIconWidth(); + } + + @Override + public int getIconHeight() { + return getRealIcon().getIconHeight(); + } + + @Override + public String toString() { + return myUrl.toString(); + } + + @Override + public Icon scale(float scaleFactor) { + if (scaleFactor == 1f) { + return this; + } + + if (!isValid()) getRealIcon(); // force state update & cache reset + + Icon icon = myScaledIconsCache.getScaledIcon(scaleFactor); + if (icon != null) { + return icon; + } + return this; + } + + private class MyScaledIconsCache { + // Map {false -> image}, {true -> image@2x} + private Map> origImagesCache = Collections.synchronizedMap(new HashMap>(2)); + + private static final int SCALED_ICONS_CACHE_LIMIT = 5; + + // Map {effective scale -> icon} + private Map> scaledIconsCache = Collections.synchronizedMap(new LinkedHashMap>(SCALED_ICONS_CACHE_LIMIT) { + @Override + public boolean removeEldestEntry(Map.Entry> entry) { + return size() > SCALED_ICONS_CACHE_LIMIT; + } + }); + + public Image getOrigImage(boolean retina) { + Image img = SoftReference.dereference(origImagesCache.get(retina)); + + if (img == null) { + img = ImageLoader.loadFromUrl(myUrl, UIUtil.isUnderDarcula(), retina, filter); + origImagesCache.put(retina, new SoftReference(img)); + } + return img; + } + + public Icon getScaledIcon(float scale) { + float effectiveScale = scale * JBUI.scale(1f); + Icon icon = SoftReference.dereference(scaledIconsCache.get(effectiveScale)); + + if (icon == null) { + boolean needRetinaImage = (effectiveScale >= 1.5f || UIUtil.isRetina()); + Image image = getOrigImage(needRetinaImage); + + if (image != null) { + Image iconImage = getRealIcon().getImage(); + int width = (int)(ImageUtil.getRealWidth(iconImage) * scale); + int height = (int)(ImageUtil.getRealHeight(iconImage) * scale); + + Image resizedImage = Scalr.resize(ImageUtil.toBufferedImage(image), Scalr.Method.ULTRA_QUALITY, width, height); + if (UIUtil.isRetina()) resizedImage = RetinaImage.createFrom(resizedImage); + + icon = getIcon(resizedImage); + scaledIconsCache.put(effectiveScale, new SoftReference(icon)); + } + } + return icon; + } + + public void clear() { + scaledIconsCache.clear(); + origImagesCache.clear(); + } + } + } + + public abstract static class LazyIcon implements Icon { + private boolean myWasComputed; + private Icon myIcon; + private boolean isDarkVariant = USE_DARK_ICONS; + private float scale = SCALE; +// private int numberOfPatchers = 0; + private ImageFilter filter = IMAGE_FILTER; + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + final Icon icon = getOrComputeIcon(); + if (icon != null) { + icon.paintIcon(c, g, x, y); + } + } + + @Override + public int getIconWidth() { + final Icon icon = getOrComputeIcon(); + return icon != null ? icon.getIconWidth() : 0; + } + + @Override + public int getIconHeight() { + final Icon icon = getOrComputeIcon(); + return icon != null ? icon.getIconHeight() : 0; + } + + protected final synchronized Icon getOrComputeIcon() { + if (!myWasComputed || isDarkVariant != USE_DARK_ICONS || scale != SCALE || filter != IMAGE_FILTER /*|| numberOfPatchers != ourPatchers.size()*/) { + isDarkVariant = USE_DARK_ICONS; + scale = SCALE; + filter = IMAGE_FILTER; + myWasComputed = true; +// numberOfPatchers = ourPatchers.size(); + myIcon = compute(); + } + + return myIcon; + } + + public final void load() { + getIconWidth(); + } + + protected abstract Icon compute(); + } + + private static class LabelHolder { + /** + * To get disabled icon with paint it into the image. Some icons require + * not null component to paint. + */ + private static final JComponent ourFakeComponent = new JLabel(); + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/IsRetina.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/IsRetina.java new file mode 100644 index 000000000..c77ccb212 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/IsRetina.java @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader; + +import apple.awt.CImage; +import com.bulenkov.iconloader.util.Ref; + +import java.awt.image.BufferedImage; + +/** + * @author Konstantin Bulenkov + */ +public class IsRetina { + public static boolean isRetina() { + try { + final Ref isRetina = Ref.create(false); + + new CImage.HiDPIScaledImage(1, 1, BufferedImage.TYPE_INT_ARGB) { + @Override + public void drawIntoImage(BufferedImage image, float v) { + isRetina.set(v > 1); + } + }; + + return isRetina.get(); + } catch (Throwable ignore) { + return false; + } + } +} + diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/JBHiDPIScaledImage.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/JBHiDPIScaledImage.java new file mode 100644 index 000000000..7681e4222 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/JBHiDPIScaledImage.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader; + +import java.awt.*; +import java.awt.image.BufferedImage; + +/** + * @author Konstantin Bulenkov + */ +public class JBHiDPIScaledImage extends BufferedImage { + private final Image myImage; + + public JBHiDPIScaledImage(int width, int height, int type) { + this(null, 2 * width, 2 * height, type); + } + + public JBHiDPIScaledImage(Image image, int width, int height, int type) { + super(width, height, type); + myImage = image; + } + + public Image getDelegate() { + return myImage; + } + + @Override + public Graphics2D createGraphics() { + final Graphics2D g = super.createGraphics(); + + if (myImage == null) { + return new HiDPIScaledGraphics(g, this); + } + + return g; + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/RetinaImage.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/RetinaImage.java new file mode 100644 index 000000000..f3402cf08 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/RetinaImage.java @@ -0,0 +1,88 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader; + +import com.bulenkov.iconloader.util.SystemInfo; +import com.bulenkov.iconloader.util.UIUtil; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.ImageObserver; + +/** + * @author Konstantin Bulenkov + */ +public class RetinaImage { + + /** + * Creates a Retina-aware wrapper over a raw image. + * The raw image should be provided in scale of the Retina default scale factor (2x). + * The wrapper will represent the raw image in the user coordinate space. + * + * @param image the raw image + * @return the Retina-aware wrapper + */ + public static Image createFrom(Image image) { + return createFrom(image, 2, IconLoader.ourComponent); + } + + /** + * Creates a Retina-aware wrapper over a raw image. + * The raw image should be provided in the specified scale. + * The wrapper will represent the raw image in the user coordinate space. + * + * @param image the raw image + * @param scale the raw image scale + * @param observer the raw image observer + * @return the Retina-aware wrapper + */ + public static Image createFrom(Image image, final int scale, ImageObserver observer) { + int w = image.getWidth(observer); + int h = image.getHeight(observer); + + Image hidpi = create(image, w / scale, h / scale, BufferedImage.TYPE_INT_ARGB); + if (SystemInfo.isAppleJvm) { + Graphics2D g = (Graphics2D)hidpi.getGraphics(); + g.scale(1f / scale, 1f / scale); + g.drawImage(image, 0, 0, null); + g.dispose(); + } + + return hidpi; + } + + public static BufferedImage create(final int width, int height, int type) { + return create(null, width, height, type); + } + + + private static BufferedImage create(Image image, final int width, int height, int type) { + if (SystemInfo.isAppleJvm) { + return AppleHiDPIScaledImage.create(width, height, type); + } else { + if (image == null) { + return new JBHiDPIScaledImage(width, height, type); + } else { + return new JBHiDPIScaledImage(image, width, height, type); + } + } + } + + public static boolean isAppleHiDPIScaledImage(Image image) { + return UIUtil.isAppleRetina() && AppleHiDPIScaledImage.is(image); + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ArrayUtilRt.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ArrayUtilRt.java new file mode 100644 index 000000000..ca40542b3 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ArrayUtilRt.java @@ -0,0 +1,77 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import java.util.Collection; + +/** + * @author Konstantin Bulenkov + */ +@SuppressWarnings({"UtilityClassWithoutPrivateConstructor", "SSBasedInspection"}) +public class ArrayUtilRt { + private static final int ARRAY_COPY_THRESHOLD = 20; + + public static final String[] EMPTY_STRING_ARRAY = new String[0]; + + + public static String[] toStringArray(Collection collection) { + return collection == null || collection.isEmpty() + ? EMPTY_STRING_ARRAY : toArray(collection, new String[collection.size()]); + } + + /** + * This is a replacement for {@link Collection#toArray(Object[])}. For small collections it is faster to stay at java level and refrain + * from calling JNI {@link System#arraycopy(Object, int, Object, int, int)} + */ + + public static T[] toArray(Collection c, T[] sample) { + final int size = c.size(); + if (size == sample.length && size < ARRAY_COPY_THRESHOLD) { + int i = 0; + for (T t : c) { + sample[i++] = t; + } + return sample; + } + + return c.toArray(sample); + } + + /** + * @param src source array. + * @param obj object to be found. + * @return index of obj in the src array. + * Returns -1 if passed object isn't found. This method uses + * equals of arrays elements to compare obj with + * these elements. + */ + public static int find(final T[] src, final T obj) { + for (int i = 0; i < src.length; i++) { + final T o = src[i]; + if (o == null) { + if (obj == null) { + return i; + } + } else { + if (o.equals(obj)) { + return i; + } + } + } + return -1; + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/AsyncByteArrayOutputStream.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/AsyncByteArrayOutputStream.java new file mode 100644 index 000000000..643c30498 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/AsyncByteArrayOutputStream.java @@ -0,0 +1,88 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; + +/** + * @author Konstantin Bulenkov + */ +public class AsyncByteArrayOutputStream extends OutputStream { + protected byte[] myBuffer; + protected int myCount; + + public AsyncByteArrayOutputStream() { + this(32); + } + + public AsyncByteArrayOutputStream(int size) { + myBuffer = new byte[size]; + } + + @Override + public void write(int b) { + int count = myCount + 1; + + if (count > myBuffer.length) { + myBuffer = Arrays.copyOf(myBuffer, Math.max(myBuffer.length << 1, count)); + } + + myBuffer[myCount] = (byte) b; + myCount = count; + } + + @Override + public void write(byte b[], int off, int len) { + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + + int count = myCount + len; + + if (count > myBuffer.length) { + myBuffer = Arrays.copyOf(myBuffer, Math.max(myBuffer.length << 1, count)); + } + + System.arraycopy(b, off, myBuffer, myCount, len); + myCount = count; + } + + public void writeTo(OutputStream out) throws IOException { + out.write(myBuffer, 0, myCount); + } + + public void reset() { + myCount = 0; + } + + public byte[] toByteArray() { + return Arrays.copyOf(myBuffer, myCount); + } + + public int size() { + return myCount; + } + + public String toString() { + return new String(myBuffer, 0, myCount); + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Base64Converter.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Base64Converter.java new file mode 100644 index 000000000..8ab712a0a --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Base64Converter.java @@ -0,0 +1,180 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +/** + * @author Konstantin Bulenkov + */ +public class Base64Converter { + private static final char[] alphabet = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0 to 7 + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 8 to 15 + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 16 to 23 + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 24 to 31 + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 32 to 39 + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 40 to 47 + 'w', 'x', 'y', 'z', '0', '1', '2', '3', // 48 to 55 + '4', '5', '6', '7', '8', '9', '+', '/'}; // 56 to 63 + + private static final byte[] decodeTable = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, + -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, + }; + + public static String encode(String s) { + return encode(s.getBytes()); + } + + public static String encode(byte[] octetString) { + int bits24; + int bits6; + + char[] out = new char[((octetString.length - 1) / 3 + 1) * 4]; + + int outIndex = 0; + int i = 0; + + while ((i + 3) <= octetString.length) { + // store the octets + bits24 = (octetString[i++] & 0xFF) << 16; + bits24 |= (octetString[i++] & 0xFF) << 8; + bits24 |= (octetString[i++] & 0xFF); + + bits6 = (bits24 & 0x00FC0000) >> 18; + out[outIndex++] = alphabet[bits6]; + bits6 = (bits24 & 0x0003F000) >> 12; + out[outIndex++] = alphabet[bits6]; + bits6 = (bits24 & 0x00000FC0) >> 6; + out[outIndex++] = alphabet[bits6]; + bits6 = (bits24 & 0x0000003F); + out[outIndex++] = alphabet[bits6]; + } + + if (octetString.length - i == 2) { + // store the octets + bits24 = (octetString[i] & 0xFF) << 16; + bits24 |= (octetString[i + 1] & 0xFF) << 8; + + bits6 = (bits24 & 0x00FC0000) >> 18; + out[outIndex++] = alphabet[bits6]; + bits6 = (bits24 & 0x0003F000) >> 12; + out[outIndex++] = alphabet[bits6]; + bits6 = (bits24 & 0x00000FC0) >> 6; + out[outIndex++] = alphabet[bits6]; + + // padding + out[outIndex] = '='; + } else if (octetString.length - i == 1) { + // store the octets + bits24 = (octetString[i] & 0xFF) << 16; + + bits6 = (bits24 & 0x00FC0000) >> 18; + out[outIndex++] = alphabet[bits6]; + bits6 = (bits24 & 0x0003F000) >> 12; + out[outIndex++] = alphabet[bits6]; + + // padding + out[outIndex++] = '='; + out[outIndex] = '='; + } + + return StringFactory.createShared(out); + } + + public static String decode(String s) { + return new String(decode(s.getBytes())); + } + + public static byte[] decode(byte[] bytes) { + int paddingCount = 0; + int realLength = 0; + + for (int i = bytes.length - 1; i >= 0; i--) { + if (bytes[i] > ' ') { + realLength++; + } + + if (bytes[i] == '=') { + paddingCount++; + } + } + + if (realLength % 4 != 0) { + throw new IllegalArgumentException("Incorrect length " + realLength + ". Must be a multiple of 4"); + } + + final byte[] out = new byte[realLength / 4 * 3 - paddingCount]; + final byte[] t = new byte[4]; + int outIndex = 0; + int index = 0; + t[0] = t[1] = t[2] = t[3] = '='; + + for (byte c : bytes) { + if (c > ' ') { + t[index++] = c; + } + + if (index == 4) { + outIndex += decode(out, outIndex, t[0], t[1], t[2], t[3]); + index = 0; + t[0] = t[1] = t[2] = t[3] = '='; + } + } + + if (index > 0) { + decode(out, outIndex, t[0], t[1], t[2], t[3]); + } + return out; + } + + private static int decode(byte[] output, int outIndex, byte a, byte b, byte c, byte d) { + byte da = decodeTable[a]; + byte db = decodeTable[b]; + byte dc = decodeTable[c]; + byte dd = decodeTable[d]; + + if ((da == -1) || (db == -1) || ((dc == -1) && (c != '=')) || ((dd == -1) && (d != '='))) { + throw new IllegalArgumentException( + "Invalid character [" + (a & 0xFF) + ", " + (b & 0xFF) + ", " + (c & 0xFF) + ", " + (d & 0xFF) + "]"); + } + output[outIndex++] = (byte) ((da << 2) | db >>> 4); + + if (c == '=') { + return 1; + } + output[outIndex++] = (byte) ((db << 4) | dc >>> 2); + + if (d == '=') { + return 2; + } + output[outIndex] = (byte) ((dc << 6) | dd); + return 3; + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/BufferExposingByteArrayOutputStream.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/BufferExposingByteArrayOutputStream.java new file mode 100644 index 000000000..be9ad8983 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/BufferExposingByteArrayOutputStream.java @@ -0,0 +1,40 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +/** + * @author Konstantin Bulenkov + */ +public class BufferExposingByteArrayOutputStream extends AsyncByteArrayOutputStream { + public BufferExposingByteArrayOutputStream() { + } + + public BufferExposingByteArrayOutputStream(int size) { + super(size); + } + + public byte[] getInternalBuffer() { + return myBuffer; + } + + public int backOff(int size) { + assert size >= 0 : size; + myCount -= size; + assert myCount >= 0 : myCount; + return myCount; + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/CenteredIcon.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/CenteredIcon.java new file mode 100644 index 000000000..2ba45fe0b --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/CenteredIcon.java @@ -0,0 +1,74 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import javax.swing.*; +import java.awt.*; + +/** + * @author Konstantin Bulenkov + */ +public class CenteredIcon implements Icon { + private final Icon myIcon; + + private final int myWidth; + private final int myHight; + + private final boolean myCenteredInComponent; + + public CenteredIcon(Icon icon) { + this(icon, icon.getIconWidth(), icon.getIconHeight(), true); + } + + public CenteredIcon(Icon icon, int width, int height) { + this(icon, width, height, true); + } + + public CenteredIcon(Icon icon, int width, int height, boolean centeredInComponent) { + myIcon = icon; + myWidth = width; + myHight = height; + myCenteredInComponent = centeredInComponent; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + int offsetX; + int offsetY; + + if (myCenteredInComponent) { + final Dimension size = c.getSize(); + offsetX = size.width / 2 - myIcon.getIconWidth() / 2; + offsetY = size.height / 2 - myIcon.getIconHeight() / 2; + } else { + offsetX = (myWidth - myIcon.getIconWidth()) / 2; + offsetY = (myHight - myIcon.getIconHeight()) / 2; + } + + myIcon.paintIcon(c, g, x + offsetX, y + offsetY); + } + + @Override + public int getIconWidth() { + return myWidth; + } + + @Override + public int getIconHeight() { + return myHight; + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ColorUtil.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ColorUtil.java new file mode 100644 index 000000000..68c6fab63 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ColorUtil.java @@ -0,0 +1,84 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import java.awt.*; + +/** + * @author Konstantin Bulenkov + */ +public class ColorUtil { + private static int shift(int colorComponent, double d) { + final int n = (int) (colorComponent * d); + return n > 255 ? 255 : n < 0 ? 0 : n; + } + + public static Color shift(Color c, double d) { + return new Color(shift(c.getRed(), d), shift(c.getGreen(), d), shift(c.getBlue(), d), c.getAlpha()); + } + + public static Color toAlpha(Color color, int a) { + Color c = color != null ? color : Color.black; + return new Color(c.getRed(), c.getGreen(), c.getBlue(), a); + } + + /** + * Return Color object from string. The following formats are allowed: + * #abc123, + * ABC123, + * ab5, + * #FFF. + * + * @param str hex string + * @return Color object + */ + public static Color fromHex(String str) { + if (str.startsWith("#")) { + str = str.substring(1); + } + if (str.length() == 3) { + return new Color( + 17 * Integer.valueOf(String.valueOf(str.charAt(0)), 16), + 17 * Integer.valueOf(String.valueOf(str.charAt(1)), 16), + 17 * Integer.valueOf(String.valueOf(str.charAt(2)), 16)); + } else if (str.length() == 6) { + return Color.decode("0x" + str); + } else { + throw new IllegalArgumentException("Should be String of 3 or 6 chars length."); + } + } + + public static Color fromHex(String str, Color defaultValue) { + try { + return fromHex(str); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Checks whether color is dark or not based on perceptional luminosity + * http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color + * + * @param c color to check + * @return dark or not + */ + public static boolean isDark(final Color c) { + // based on perceptional luminosity, see + return (1 - (0.299 * c.getRed() + 0.587 * c.getGreen() + 0.114 * c.getBlue()) / 255) >= 0.5; + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ComparingUtils.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ComparingUtils.java new file mode 100644 index 000000000..274201ab0 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ComparingUtils.java @@ -0,0 +1,184 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import java.util.*; + +/** + * @author Konstantin Bulenkov + */ +public class ComparingUtils { + private ComparingUtils() { + } + + public static boolean equal(T arg1, T arg2) { + if (arg1 == null || arg2 == null) { + return arg1 == arg2; + } + if (arg1 instanceof Object[] && arg2 instanceof Object[]) { + Object[] arr1 = (Object[]) arg1; + Object[] arr2 = (Object[]) arg2; + return Arrays.equals(arr1, arr2); + } + if (arg1 instanceof CharSequence && arg2 instanceof CharSequence) { + return equal((CharSequence) arg1, (CharSequence) arg2, true); + } + return arg1.equals(arg2); + } + + public static boolean equal(T[] arr1, T[] arr2) { + if (arr1 == null || arr2 == null) { + return arr1 == arr2; + } + return Arrays.equals(arr1, arr2); + } + + public static boolean equal(CharSequence s1, CharSequence s2) { + return equal(s1, s2, true); + } + + public static boolean equal(String arg1, String arg2) { + return arg1 == null ? arg2 == null : arg1.equals(arg2); + } + + public static boolean equal(CharSequence s1, CharSequence s2, boolean caseSensitive) { + if (s1 == s2) return true; + if (s1 == null || s2 == null) return false; + + // Algorithm from String.regionMatches() + + if (s1.length() != s2.length()) return false; + int to = 0; + int po = 0; + int len = s1.length(); + + while (len-- > 0) { + char c1 = s1.charAt(to++); + char c2 = s2.charAt(po++); + if (c1 == c2) { + continue; + } + if (!caseSensitive && StringUtil.charsEqualIgnoreCase(c1, c2)) continue; + return false; + } + + return true; + } + + public static boolean equal(String arg1, String arg2, boolean caseSensitive) { + if (arg1 == null || arg2 == null) { + return arg1 == null && arg2 == null; + } else { + return caseSensitive ? arg1.equals(arg2) : arg1.equalsIgnoreCase(arg2); + } + } + + public static boolean strEqual(String arg1, String arg2) { + return strEqual(arg1, arg2, true); + } + + public static boolean strEqual(String arg1, String arg2, boolean caseSensitive) { + return equal(arg1 == null ? "" : arg1, arg2 == null ? "" : arg2, caseSensitive); + } + + public static boolean haveEqualElements(Collection a, Collection b) { + if (a.size() != b.size()) { + return false; + } + + Set aSet = new HashSet(a); + for (T t : b) { + if (!aSet.contains(t)) { + return false; + } + } + return true; + } + + public static boolean haveEqualElements(T[] a, T[] b) { + if (a == null || b == null) { + return a == b; + } + + if (a.length != b.length) { + return false; + } + + Set aSet = new HashSet(Arrays.asList(a)); + for (T t : b) { + if (!aSet.contains(t)) { + return false; + } + } + return true; + } + + public static int hashcode(Object obj) { + return obj == null ? 0 : obj.hashCode(); + } + + public static int hashcode(Object obj1, Object obj2) { + return hashcode(obj1) ^ hashcode(obj2); + } + + public static int compare(byte o1, byte o2) { + return o1 < o2 ? -1 : o1 == o2 ? 0 : 1; + } + + public static int compare(boolean o1, boolean o2) { + return o1 == o2 ? 0 : o1 ? 1 : -1; + } + + public static int compare(int o1, int o2) { + return o1 < o2 ? -1 : o1 == o2 ? 0 : 1; + } + + public static int compare(long o1, long o2) { + return o1 < o2 ? -1 : o1 == o2 ? 0 : 1; + } + + public static int compare(double o1, double o2) { + return o1 < o2 ? -1 : o1 == o2 ? 0 : 1; + } + + public static int compare(byte[] o1, byte[] o2) { + if (o1 == o2) return 0; + if (o1 == null) return 1; + if (o2 == null) return -1; + + if (o1.length > o2.length) return 1; + if (o1.length < o2.length) return -1; + + for (int i = 0; i < o1.length; i++) { + if (o1[i] > o2[i]) return 1; + else if (o1[i] < o2[i]) return -1; + } + return 0; + } + + public static > int compare(final T o1, final T o2) { + if (o1 == null) return o2 == null ? 0 : -1; + if (o2 == null) return 1; + return o1.compareTo(o2); + } + + public static int compare(final T o1, final T o2, final Comparator notNullComparator) { + if (o1 == null) return o2 == null ? 0 : -1; + if (o2 == null) return 1; + return notNullComparator.compare(o1, o2); + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ConcurrencyUtil.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ConcurrencyUtil.java new file mode 100644 index 000000000..5a9ed0b3d --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ConcurrencyUtil.java @@ -0,0 +1,133 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.*; + +/** + * @author Konstantin Bulenkov + */ +public class ConcurrencyUtil { + /** + * Invokes and waits all tasks using threadPool, avoiding thread starvation on the way + * (see "A Thread Pool Puzzler"). + */ + public static List> invokeAll(Collection> tasks, ExecutorService executorService) throws Throwable { + if (executorService == null) { + for (Callable task : tasks) { + task.call(); + } + return null; + } + + List> futures = new ArrayList>(tasks.size()); + boolean done = false; + try { + for (Callable t : tasks) { + Future future = executorService.submit(t); + futures.add(future); + } + // force not started futures to execute using the current thread + for (Future f : futures) { + ((Runnable) f).run(); + } + for (Future f : futures) { + try { + f.get(); + } catch (CancellationException ignore) { + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause != null) { + throw cause; + } + } + } + done = true; + } finally { + if (!done) { + for (Future f : futures) { + f.cancel(false); + } + } + } + return futures; + } + + /** + * @return defaultValue if there is no entry in the map (in that case defaultValue is placed into the map), + * or corresponding value if entry already exists. + */ + + public static V cacheOrGet(ConcurrentMap map, final K key, final V defaultValue) { + V v = map.get(key); + if (v != null) return v; + V prev = map.putIfAbsent(key, defaultValue); + return prev == null ? defaultValue : prev; + } + + + public static ThreadPoolExecutor newSingleThreadExecutor(final String threadFactoryName) { + return newSingleThreadExecutor(threadFactoryName, Thread.NORM_PRIORITY); + } + + + public static ThreadPoolExecutor newSingleThreadExecutor(final String threadFactoryName, final int threadPriority) { + return new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), newNamedThreadFactory(threadFactoryName, true, threadPriority)); + } + + + public static ScheduledThreadPoolExecutor newSingleScheduledThreadExecutor(final String threadFactoryName) { + return newSingleScheduledThreadExecutor(threadFactoryName, Thread.NORM_PRIORITY); + } + + + public static ScheduledThreadPoolExecutor newSingleScheduledThreadExecutor(final String threadFactoryName, final int threadPriority) { + ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, newNamedThreadFactory(threadFactoryName, true, threadPriority)); + executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); + executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + return executor; + } + + + public static ThreadFactory newNamedThreadFactory(final String threadName, final boolean isDaemon, final int threadPriority) { + return new ThreadFactory() { + + @Override + public Thread newThread(final Runnable r) { + final Thread thread = new Thread(r, threadName); + thread.setDaemon(isDaemon); + thread.setPriority(threadPriority); + return thread; + } + }; + } + + public static ThreadFactory newNamedThreadFactory(final String threadName) { + return new ThreadFactory() { + + @Override + public Thread newThread(final Runnable r) { + return new Thread(r, threadName); + } + }; + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ConcurrentRefValueHashMap.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ConcurrentRefValueHashMap.java new file mode 100644 index 000000000..9fd77f9a2 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ConcurrentRefValueHashMap.java @@ -0,0 +1,243 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.TestOnly; + +import java.lang.ref.ReferenceQueue; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Base class for concurrent strong key:K -> (soft/weak) value:V map + * Null keys are NOT allowed + * Null values are NOT allowed + */ +abstract class ConcurrentRefValueHashMap implements ConcurrentMap { + private final ConcurrentMap> myMap; + protected final ReferenceQueue myQueue = new ReferenceQueue(); + + public ConcurrentRefValueHashMap(@NotNull Map map) { + this(); + putAll(map); + } + + public ConcurrentRefValueHashMap() { + myMap = new ConcurrentHashMap>(); + } + + public ConcurrentRefValueHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { + myMap = new ConcurrentHashMap>(initialCapacity, loadFactor, concurrencyLevel); + } + +// public ConcurrentRefValueHashMap(int initialCapacity, +// float loadFactor, +// int concurrencyLevel, +// @NotNull TObjectHashingStrategy hashingStrategy) { +// myMap = new ConcurrentHashMap>(initialCapacity, loadFactor, concurrencyLevel, hashingStrategy); +// } + + protected interface ValueReference { + @NotNull + K getKey(); + + V get(); + } + + // returns true if some refs were tossed + boolean processQueue() { + boolean processed = false; + + while (true) { + @SuppressWarnings("unchecked") + ValueReference ref = (ValueReference)myQueue.poll(); + if (ref == null) break; + myMap.remove(ref.getKey(), ref); + processed = true; + } + return processed; + } + + @Override + public V get(@NotNull Object key) { + ValueReference ref = myMap.get(key); + if (ref == null) return null; + return ref.get(); + } + + @Override + public V put(@NotNull K key, @NotNull V value) { + processQueue(); + ValueReference oldRef = myMap.put(key, createValueReference(key, value)); + return oldRef != null ? oldRef.get() : null; + } + + @NotNull + protected abstract ValueReference createValueReference(@NotNull K key, @NotNull V value); + + @Override + public V putIfAbsent(@NotNull K key, @NotNull V value) { + ValueReference newRef = createValueReference(key, value); + while (true) { + processQueue(); + ValueReference oldRef = myMap.putIfAbsent(key, newRef); + if (oldRef == null) return null; + final V oldVal = oldRef.get(); + if (oldVal == null) { + if (myMap.replace(key, oldRef, newRef)) return null; + } + else { + return oldVal; + } + } + } + + @Override + public boolean remove(@NotNull final Object key, @NotNull Object value) { + processQueue(); + //noinspection unchecked + return myMap.remove(key, createValueReference((K)key, (V)value)); + } + + @Override + public boolean replace(@NotNull final K key, @NotNull final V oldValue, @NotNull final V newValue) { + processQueue(); + return myMap.replace(key, createValueReference(key, oldValue), createValueReference(key, newValue)); + } + + @Override + public V replace(@NotNull final K key, @NotNull final V value) { + processQueue(); + ValueReference ref = myMap.replace(key, createValueReference(key, value)); + return ref == null ? null : ref.get(); + } + + @Override + public V remove(@NotNull Object key) { + processQueue(); + ValueReference ref = myMap.remove(key); + return ref == null ? null : ref.get(); + } + + @Override + public void putAll(@NotNull Map t) { + processQueue(); + for (Entry entry : t.entrySet()) { + V v = entry.getValue(); + if (v != null) { + K key = entry.getKey(); + put(key, v); + } + } + } + + @Override + public void clear() { + myMap.clear(); + processQueue(); + } + + @Override + public int size() { + processQueue(); + return myMap.size(); + } + + @Override + public boolean isEmpty() { + processQueue(); + return myMap.isEmpty(); + } + + @Override + public boolean containsKey(@NotNull Object key) { + return get(key) != null; + } + + @Override + public boolean containsValue(@NotNull Object value) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Set keySet() { + return myMap.keySet(); + } + + @NotNull + @Override + public Collection values() { + Collection result = new ArrayList(); + final Collection> refs = myMap.values(); + for (ValueReference ref : refs) { + final V value = ref.get(); + if (value != null) { + result.add(value); + } + } + return result; + } + + @NotNull + @Override + public Set> entrySet() { + final Set keys = keySet(); + Set> entries = new HashSet>(); + + for (final K key : keys) { + final V value = get(key); + if (value != null) { + entries.add(new Entry() { + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(@NotNull V value) { + throw new UnsupportedOperationException("setValue is not implemented"); + } + + @Override + public String toString() { + return "(" + getKey() + " : " + getValue() + ")"; + } + }); + } + } + + return entries; + } + + @Override + public String toString() { + return "map size:" + size() + " [" + StringUtil.join(entrySet(), ",") + "]"; + } + + @TestOnly + int underlyingMapSize() { + return myMap.size(); + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ConcurrentSoftValueHashMap.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ConcurrentSoftValueHashMap.java new file mode 100644 index 000000000..d0b811406 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ConcurrentSoftValueHashMap.java @@ -0,0 +1,79 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import org.jetbrains.annotations.NotNull; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.util.Map; + +/** + * Concurrent strong key:K -> soft value:V map + * Null keys are NOT allowed + * Null values are NOT allowed + */ +public final class ConcurrentSoftValueHashMap extends ConcurrentRefValueHashMap { + public ConcurrentSoftValueHashMap(@NotNull Map map) { + super(map); + } + + public ConcurrentSoftValueHashMap() { + } + + public ConcurrentSoftValueHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { + super(initialCapacity, loadFactor, concurrencyLevel); + } + +// public ConcurrentSoftValueHashMap(int initialCapacity, float loadFactor, int concurrencyLevel, @NotNull TObjectHashingStrategy hashingStrategy) { +// super(initialCapacity, loadFactor, concurrencyLevel, hashingStrategy); +// } + + private static class MySoftReference extends SoftReference implements ValueReference { + private final K key; + private MySoftReference(@NotNull K key, @NotNull V referent, @NotNull ReferenceQueue q) { + super(referent, q); + this.key = key; + } + + @NotNull + @Override + public K getKey() { + return key; + } + + // When referent is collected, equality should be identity-based (for the processQueues() remove this very same SoftValue) + // otherwise it's just canonical equals on referents for replace(K,V,V) to work + public final boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + @SuppressWarnings("unchecked") + ValueReference that = (ValueReference)o; + + V v = get(); + V thatV = that.get(); + return key.equals(that.getKey()) && v != null && thatV != null && v.equals(thatV); + } + } + + @NotNull + @Override + protected ValueReference createValueReference(@NotNull K key, @NotNull V value) { + return new MySoftReference(key, value, myQueue); + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/DoubleColor.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/DoubleColor.java new file mode 100644 index 000000000..4fb8f1b3c --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/DoubleColor.java @@ -0,0 +1,204 @@ +/* + * Copyright 2000-2013 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bulenkov.iconloader.util; + + +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.ColorModel; + +/** + * @author Konstantin Bulenkov + */ +public class DoubleColor extends Color { + + private static volatile boolean DARK = UIUtil.isUnderDarcula(); + + private final Color darkColor; + + public DoubleColor(int rgb, int darkRGB) { + this(new Color(rgb), new Color(darkRGB)); + } + + public DoubleColor(Color regular, Color dark) { + super(regular.getRGB(), regular.getAlpha() != 255); + darkColor = dark; + //noinspection AssignmentToStaticFieldFromInstanceMethod + DARK = UIUtil.isUnderDarcula(); //Double check. Sometimes DARK != isDarcula() after dialogs appear on splash screen + } + + public static void setDark(boolean dark) { + DARK = dark; + } + + Color getDarkVariant() { + return darkColor; + } + + @Override + public int getRed() { + return DARK ? getDarkVariant().getRed() : super.getRed(); + } + + @Override + public int getGreen() { + return DARK ? getDarkVariant().getGreen() : super.getGreen(); + } + + @Override + public int getBlue() { + return DARK ? getDarkVariant().getBlue() : super.getBlue(); + } + + @Override + public int getAlpha() { + return DARK ? getDarkVariant().getAlpha() : super.getAlpha(); + } + + @Override + public int getRGB() { + return DARK ? getDarkVariant().getRGB() : super.getRGB(); + } + + @Override + public Color brighter() { + return new DoubleColor(super.brighter(), getDarkVariant().brighter()); + } + + @Override + public Color darker() { + return new DoubleColor(super.darker(), getDarkVariant().darker()); + } + + @Override + public int hashCode() { + return DARK ? getDarkVariant().hashCode() : super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return DARK ? getDarkVariant().equals(obj) : super.equals(obj); + } + + @Override + public String toString() { + return DARK ? getDarkVariant().toString() : super.toString(); + } + + @Override + public float[] getRGBComponents(float[] compArray) { + return DARK ? getDarkVariant().getRGBComponents(compArray) : super.getRGBComponents(compArray); + } + + @Override + public float[] getRGBColorComponents(float[] compArray) { + return DARK ? getDarkVariant().getRGBColorComponents(compArray) : super.getRGBComponents(compArray); + } + + @Override + public float[] getComponents(float[] compArray) { + return DARK ? getDarkVariant().getComponents(compArray) : super.getComponents(compArray); + } + + @Override + public float[] getColorComponents(float[] compArray) { + return DARK ? getDarkVariant().getColorComponents(compArray) : super.getColorComponents(compArray); + } + + @Override + public float[] getComponents(ColorSpace cspace, float[] compArray) { + return DARK ? getDarkVariant().getComponents(cspace, compArray) : super.getComponents(cspace, compArray); + } + + @Override + public float[] getColorComponents(ColorSpace cspace, float[] compArray) { + return DARK ? getDarkVariant().getColorComponents(cspace, compArray) : super.getColorComponents(cspace, compArray); + } + + @Override + public ColorSpace getColorSpace() { + return DARK ? getDarkVariant().getColorSpace() : super.getColorSpace(); + } + + @Override + public synchronized PaintContext createContext(ColorModel cm, Rectangle r, Rectangle2D r2d, AffineTransform xform, RenderingHints hints) { + return DARK ? getDarkVariant().createContext(cm, r, r2d, xform, hints) : super.createContext(cm, r, r2d, xform, hints); + } + + @Override + public int getTransparency() { + return DARK ? getDarkVariant().getTransparency() : super.getTransparency(); + } + + public static final DoubleColor red = new DoubleColor(Color.red, new Color(255, 100, 100)); + public static final DoubleColor RED = red; + + public static final DoubleColor blue = new DoubleColor(Color.blue, new Color(0x589df6)); + public static final DoubleColor BLUE = blue; + + public static final DoubleColor white = new DoubleColor(Color.white, UIUtil.getListBackground()) { + @Override + Color getDarkVariant() { + return UIUtil.getListBackground(); + } + }; + public static final DoubleColor WHITE = white; + + public static final DoubleColor black = new DoubleColor(Color.black, UIUtil.getListForeground()) { + @Override + Color getDarkVariant() { + return UIUtil.getListForeground(); + } + }; + public static final DoubleColor BLACK = black; + + public static final DoubleColor gray = new DoubleColor(Gray._128, Gray._128); + public static final DoubleColor GRAY = gray; + + public static final DoubleColor lightGray = new DoubleColor(Gray._192, Gray._64); + public static final DoubleColor LIGHT_GRAY = lightGray; + + public static final DoubleColor darkGray = new DoubleColor(Gray._64, Gray._192); + public static final DoubleColor DARK_GRAY = darkGray; + + public static final DoubleColor pink = new DoubleColor(Color.pink, Color.pink); + public static final DoubleColor PINK = pink; + + public static final DoubleColor orange = new DoubleColor(Color.orange, new Color(159, 107, 0)); + public static final DoubleColor ORANGE = orange; + + public static final DoubleColor yellow = new DoubleColor(Color.yellow, new Color(138, 138, 0)); + public static final DoubleColor YELLOW = yellow; + + public static final DoubleColor green = new DoubleColor(Color.green, new Color(98, 150, 85)); + public static final DoubleColor GREEN = green; + + public static final Color magenta = new DoubleColor(Color.magenta, new Color(151, 118, 169)); + public static final Color MAGENTA = magenta; + + public static final Color cyan = new DoubleColor(Color.cyan, new Color(0, 137, 137)); + public static final Color CYAN = cyan; + + public static Color foreground() { + return UIUtil.getLabelForeground(); + } + + public static Color background() { + return UIUtil.getListBackground(); + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/EmptyIcon.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/EmptyIcon.java new file mode 100644 index 000000000..ce02e9462 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/EmptyIcon.java @@ -0,0 +1,92 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import javax.swing.*; +import java.awt.*; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Konstantin Bulenkov + */ +public class EmptyIcon implements Icon { + private static final Map cache = new HashMap(); + private final int width; + private final int height; + + public static Icon create(int size) { + Icon icon = cache.get(size); + if (icon == null && size < 129) { + cache.put(size, icon = new EmptyIcon(size, size)); + } + return icon == null ? new EmptyIcon(size, size) : icon; + } + + public static Icon create(int width, int height) { + return width == height ? create(width) : new EmptyIcon(width, height); + } + + public static Icon create(Icon base) { + return create(base.getIconWidth(), base.getIconHeight()); + } + + /** + * @deprecated use {@linkplain #create(int)} for caching. + */ + public EmptyIcon(int size) { + this(size, size); + } + + public EmptyIcon(int width, int height) { + this.width = width; + this.height = height; + } + + /** + * @deprecated use {@linkplain #create(javax.swing.Icon)} for caching. + */ + public EmptyIcon(Icon base) { + this(base.getIconWidth(), base.getIconHeight()); + } + + public int getIconWidth() { + return width; + } + + public int getIconHeight() { + return height; + } + + public void paintIcon(Component component, Graphics g, int i, int j) { + } + + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof EmptyIcon)) return false; + + final EmptyIcon icon = (EmptyIcon) o; + + return height == icon.height && width == icon.width; + + } + + public int hashCode() { + int sum = width + height; + return sum * (sum + 1) / 2 + width; + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Getter.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Getter.java new file mode 100644 index 000000000..7b0189567 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Getter.java @@ -0,0 +1,21 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +public interface Getter { + A get(); +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/GraphicsConfig.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/GraphicsConfig.java new file mode 100644 index 000000000..4d5f6b827 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/GraphicsConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import java.awt.*; +import java.util.Map; + +/** + * @author Konstantin Bulenkov + */ +public class GraphicsConfig { + + private final Graphics2D myG; + private final Map myHints; + + public GraphicsConfig(Graphics g) { + myG = (Graphics2D) g; + myHints = (Map) myG.getRenderingHints().clone(); + } + + public GraphicsConfig setAntialiasing(boolean on) { + myG.setRenderingHint(RenderingHints.KEY_ANTIALIASING, on ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); + return this; + } + + public Graphics2D getG() { + return myG; + } + + public void restore() { + myG.setRenderingHints(myHints); + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/GraphicsUtil.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/GraphicsUtil.java new file mode 100644 index 000000000..5a6619635 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/GraphicsUtil.java @@ -0,0 +1,62 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import java.awt.*; +import java.util.Map; + +/** + * @author Konstantin Bulenkov + */ +public class GraphicsUtil { + public static void setupAntialiasing(Graphics g2) { + setupAntialiasing(g2, true, false); + } + + public static void setupAntialiasing(Graphics g2, boolean enableAA, boolean ignoreSystemSettings) { + if (g2 instanceof Graphics2D) { + Graphics2D g = (Graphics2D) g2; + Toolkit tk = Toolkit.getDefaultToolkit(); + //noinspection HardCodedStringLiteral + Map map = (Map) tk.getDesktopProperty("awt.font.desktophints"); + + if (map != null && !ignoreSystemSettings) { + g.addRenderingHints(map); + } else { + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + enableAA ? RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); + } + } + } + + public static GraphicsConfig setupAAPainting(Graphics g) { + final GraphicsConfig config = new GraphicsConfig(g); + final Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); + return config; + } + + public static GraphicsConfig paintWithAlpha(Graphics g, float alpha) { + assert 0.0f <= alpha && alpha <= 1.0f : "alpha should be in range 0.0f .. 1.0f"; + final GraphicsConfig config = new GraphicsConfig(g); + final Graphics2D g2 = (Graphics2D) g; + + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); + return config; + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Gray.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Gray.java new file mode 100644 index 000000000..3f0dd6a2f --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Gray.java @@ -0,0 +1,605 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import java.awt.*; + +/** + * @author Konstantin Bulenkov + */ +@SuppressWarnings({"InspectionUsingGrayColors", "UnusedDeclaration"}) +public class Gray extends Color { + private Gray(int num) { + super(num, num, num); + } + + private Gray(int num, int alpha) { + super(num, num, num, alpha); + } + + public Color withAlpha(int alpha) { + assert 0 <= alpha && alpha <= 255 : "Alpha " + alpha + "is incorrect. Alpha should be in range 0..255"; + return new Gray(getRed(), alpha); + } + + public static Gray get(int gray) { + assert 0 <= gray && gray <= 255 : "Gray == " + gray + "Gray should be in range 0..255"; + return cache[gray]; + } + + public static Color get(int gray, int alpha) { + return get(gray).withAlpha(alpha); + } + + public static final Gray _0 = new Gray(0); + public static final Gray _1 = new Gray(1); + public static final Gray _2 = new Gray(2); + public static final Gray _3 = new Gray(3); + public static final Gray _4 = new Gray(4); + public static final Gray _5 = new Gray(5); + public static final Gray _6 = new Gray(6); + public static final Gray _7 = new Gray(7); + public static final Gray _8 = new Gray(8); + public static final Gray _9 = new Gray(9); + public static final Gray _10 = new Gray(10); + public static final Gray _11 = new Gray(11); + public static final Gray _12 = new Gray(12); + public static final Gray _13 = new Gray(13); + public static final Gray _14 = new Gray(14); + public static final Gray _15 = new Gray(15); + public static final Gray _16 = new Gray(16); + public static final Gray _17 = new Gray(17); + public static final Gray _18 = new Gray(18); + public static final Gray _19 = new Gray(19); + public static final Gray _20 = new Gray(20); + public static final Gray _21 = new Gray(21); + public static final Gray _22 = new Gray(22); + public static final Gray _23 = new Gray(23); + public static final Gray _24 = new Gray(24); + public static final Gray _25 = new Gray(25); + public static final Gray _26 = new Gray(26); + public static final Gray _27 = new Gray(27); + public static final Gray _28 = new Gray(28); + public static final Gray _29 = new Gray(29); + public static final Gray _30 = new Gray(30); + public static final Gray _31 = new Gray(31); + public static final Gray _32 = new Gray(32); + public static final Gray _33 = new Gray(33); + public static final Gray _34 = new Gray(34); + public static final Gray _36 = new Gray(36); + public static final Gray _35 = new Gray(35); + public static final Gray _37 = new Gray(37); + public static final Gray _38 = new Gray(38); + public static final Gray _39 = new Gray(39); + public static final Gray _40 = new Gray(40); + public static final Gray _41 = new Gray(41); + public static final Gray _42 = new Gray(42); + public static final Gray _43 = new Gray(43); + public static final Gray _44 = new Gray(44); + public static final Gray _45 = new Gray(45); + public static final Gray _46 = new Gray(46); + public static final Gray _47 = new Gray(47); + public static final Gray _48 = new Gray(48); + public static final Gray _49 = new Gray(49); + public static final Gray _50 = new Gray(50); + public static final Gray _51 = new Gray(51); + public static final Gray _52 = new Gray(52); + public static final Gray _53 = new Gray(53); + public static final Gray _54 = new Gray(54); + public static final Gray _55 = new Gray(55); + public static final Gray _56 = new Gray(56); + public static final Gray _57 = new Gray(57); + public static final Gray _58 = new Gray(58); + public static final Gray _59 = new Gray(59); + public static final Gray _60 = new Gray(60); + public static final Gray _61 = new Gray(61); + public static final Gray _62 = new Gray(62); + public static final Gray _63 = new Gray(63); + public static final Gray _64 = new Gray(64); + public static final Gray _65 = new Gray(65); + public static final Gray _66 = new Gray(66); + public static final Gray _67 = new Gray(67); + public static final Gray _68 = new Gray(68); + public static final Gray _69 = new Gray(69); + public static final Gray _70 = new Gray(70); + public static final Gray _71 = new Gray(71); + public static final Gray _72 = new Gray(72); + public static final Gray _73 = new Gray(73); + public static final Gray _74 = new Gray(74); + public static final Gray _75 = new Gray(75); + public static final Gray _76 = new Gray(76); + public static final Gray _77 = new Gray(77); + public static final Gray _78 = new Gray(78); + public static final Gray _79 = new Gray(79); + public static final Gray _80 = new Gray(80); + public static final Gray _81 = new Gray(81); + public static final Gray _82 = new Gray(82); + public static final Gray _83 = new Gray(83); + public static final Gray _84 = new Gray(84); + public static final Gray _85 = new Gray(85); + public static final Gray _86 = new Gray(86); + public static final Gray _87 = new Gray(87); + public static final Gray _88 = new Gray(88); + public static final Gray _89 = new Gray(89); + public static final Gray _90 = new Gray(90); + public static final Gray _91 = new Gray(91); + public static final Gray _92 = new Gray(92); + public static final Gray _93 = new Gray(93); + public static final Gray _94 = new Gray(94); + public static final Gray _95 = new Gray(95); + public static final Gray _96 = new Gray(96); + public static final Gray _97 = new Gray(97); + public static final Gray _98 = new Gray(98); + public static final Gray _99 = new Gray(99); + public static final Gray _100 = new Gray(100); + public static final Gray _101 = new Gray(101); + public static final Gray _102 = new Gray(102); + public static final Gray _103 = new Gray(103); + public static final Gray _104 = new Gray(104); + public static final Gray _105 = new Gray(105); + public static final Gray _106 = new Gray(106); + public static final Gray _107 = new Gray(107); + public static final Gray _108 = new Gray(108); + public static final Gray _109 = new Gray(109); + public static final Gray _110 = new Gray(110); + public static final Gray _111 = new Gray(111); + public static final Gray _112 = new Gray(112); + public static final Gray _113 = new Gray(113); + public static final Gray _114 = new Gray(114); + public static final Gray _115 = new Gray(115); + public static final Gray _116 = new Gray(116); + public static final Gray _117 = new Gray(117); + public static final Gray _118 = new Gray(118); + public static final Gray _119 = new Gray(119); + public static final Gray _120 = new Gray(120); + public static final Gray _121 = new Gray(121); + public static final Gray _122 = new Gray(122); + public static final Gray _123 = new Gray(123); + public static final Gray _124 = new Gray(124); + public static final Gray _125 = new Gray(125); + public static final Gray _126 = new Gray(126); + public static final Gray _127 = new Gray(127); + public static final Gray _128 = new Gray(128); + public static final Gray _129 = new Gray(129); + public static final Gray _130 = new Gray(130); + public static final Gray _131 = new Gray(131); + public static final Gray _132 = new Gray(132); + public static final Gray _133 = new Gray(133); + public static final Gray _134 = new Gray(134); + public static final Gray _135 = new Gray(135); + public static final Gray _136 = new Gray(136); + public static final Gray _137 = new Gray(137); + public static final Gray _138 = new Gray(138); + public static final Gray _139 = new Gray(139); + public static final Gray _140 = new Gray(140); + public static final Gray _141 = new Gray(141); + public static final Gray _142 = new Gray(142); + public static final Gray _143 = new Gray(143); + public static final Gray _144 = new Gray(144); + public static final Gray _145 = new Gray(145); + public static final Gray _146 = new Gray(146); + public static final Gray _147 = new Gray(147); + public static final Gray _148 = new Gray(148); + public static final Gray _149 = new Gray(149); + public static final Gray _150 = new Gray(150); + public static final Gray _151 = new Gray(151); + public static final Gray _152 = new Gray(152); + public static final Gray _153 = new Gray(153); + public static final Gray _154 = new Gray(154); + public static final Gray _155 = new Gray(155); + public static final Gray _156 = new Gray(156); + public static final Gray _157 = new Gray(157); + public static final Gray _158 = new Gray(158); + public static final Gray _159 = new Gray(159); + public static final Gray _160 = new Gray(160); + public static final Gray _161 = new Gray(161); + public static final Gray _162 = new Gray(162); + public static final Gray _163 = new Gray(163); + public static final Gray _164 = new Gray(164); + public static final Gray _165 = new Gray(165); + public static final Gray _166 = new Gray(166); + public static final Gray _167 = new Gray(167); + public static final Gray _168 = new Gray(168); + public static final Gray _169 = new Gray(169); + public static final Gray _170 = new Gray(170); + public static final Gray _171 = new Gray(171); + public static final Gray _172 = new Gray(172); + public static final Gray _173 = new Gray(173); + public static final Gray _174 = new Gray(174); + public static final Gray _175 = new Gray(175); + public static final Gray _176 = new Gray(176); + public static final Gray _177 = new Gray(177); + public static final Gray _178 = new Gray(178); + public static final Gray _179 = new Gray(179); + public static final Gray _180 = new Gray(180); + public static final Gray _181 = new Gray(181); + public static final Gray _182 = new Gray(182); + public static final Gray _183 = new Gray(183); + public static final Gray _184 = new Gray(184); + public static final Gray _185 = new Gray(185); + public static final Gray _186 = new Gray(186); + public static final Gray _187 = new Gray(187); + public static final Gray _188 = new Gray(188); + public static final Gray _189 = new Gray(189); + public static final Gray _190 = new Gray(190); + public static final Gray _191 = new Gray(191); + public static final Gray _192 = new Gray(192); + public static final Gray _193 = new Gray(193); + public static final Gray _194 = new Gray(194); + public static final Gray _195 = new Gray(195); + public static final Gray _196 = new Gray(196); + public static final Gray _197 = new Gray(197); + public static final Gray _198 = new Gray(198); + public static final Gray _199 = new Gray(199); + public static final Gray _200 = new Gray(200); + public static final Gray _201 = new Gray(201); + public static final Gray _202 = new Gray(202); + public static final Gray _203 = new Gray(203); + public static final Gray _204 = new Gray(204); + public static final Gray _205 = new Gray(205); + public static final Gray _206 = new Gray(206); + public static final Gray _207 = new Gray(207); + public static final Gray _208 = new Gray(208); + public static final Gray _209 = new Gray(209); + public static final Gray _210 = new Gray(210); + public static final Gray _211 = new Gray(211); + public static final Gray _212 = new Gray(212); + public static final Gray _213 = new Gray(213); + public static final Gray _214 = new Gray(214); + public static final Gray _215 = new Gray(215); + public static final Gray _216 = new Gray(216); + public static final Gray _217 = new Gray(217); + public static final Gray _218 = new Gray(218); + public static final Gray _219 = new Gray(219); + public static final Gray _220 = new Gray(220); + public static final Gray _221 = new Gray(221); + public static final Gray _222 = new Gray(222); + public static final Gray _223 = new Gray(223); + public static final Gray _224 = new Gray(224); + public static final Gray _225 = new Gray(225); + public static final Gray _226 = new Gray(226); + public static final Gray _227 = new Gray(227); + public static final Gray _228 = new Gray(228); + public static final Gray _229 = new Gray(229); + public static final Gray _230 = new Gray(230); + public static final Gray _231 = new Gray(231); + public static final Gray _232 = new Gray(232); + public static final Gray _233 = new Gray(233); + public static final Gray _234 = new Gray(234); + public static final Gray _235 = new Gray(235); + public static final Gray _236 = new Gray(236); + public static final Gray _237 = new Gray(237); + public static final Gray _238 = new Gray(238); + public static final Gray _239 = new Gray(239); + public static final Gray _240 = new Gray(240); + public static final Gray _241 = new Gray(241); + public static final Gray _242 = new Gray(242); + public static final Gray _243 = new Gray(243); + public static final Gray _244 = new Gray(244); + public static final Gray _245 = new Gray(245); + public static final Gray _246 = new Gray(246); + public static final Gray _247 = new Gray(247); + public static final Gray _248 = new Gray(248); + public static final Gray _249 = new Gray(249); + public static final Gray _250 = new Gray(250); + public static final Gray _251 = new Gray(251); + public static final Gray _252 = new Gray(252); + public static final Gray _253 = new Gray(253); + public static final Gray _254 = new Gray(254); + public static final Gray _255 = new Gray(255); + + public static final Gray x00 = _0; + public static final Gray x01 = _1; + public static final Gray x02 = _2; + public static final Gray x03 = _3; + public static final Gray x04 = _4; + public static final Gray x05 = _5; + public static final Gray x06 = _6; + public static final Gray x07 = _7; + public static final Gray x08 = _8; + public static final Gray x09 = _9; + public static final Gray x0A = _10; + public static final Gray x0B = _11; + public static final Gray x0C = _12; + public static final Gray x0D = _13; + public static final Gray x0E = _14; + public static final Gray x0F = _15; + public static final Gray x10 = _16; + public static final Gray x11 = _17; + public static final Gray x12 = _18; + public static final Gray x13 = _19; + public static final Gray x14 = _20; + public static final Gray x15 = _21; + public static final Gray x16 = _22; + public static final Gray x17 = _23; + public static final Gray x18 = _24; + public static final Gray x19 = _25; + public static final Gray x1A = _26; + public static final Gray x1B = _27; + public static final Gray x1C = _28; + public static final Gray x1D = _29; + public static final Gray x1E = _30; + public static final Gray x1F = _31; + public static final Gray x20 = _32; + public static final Gray x21 = _33; + public static final Gray x22 = _34; + public static final Gray x23 = _35; + public static final Gray x24 = _36; + public static final Gray x25 = _37; + public static final Gray x26 = _38; + public static final Gray x27 = _39; + public static final Gray x28 = _40; + public static final Gray x29 = _41; + public static final Gray x2A = _42; + public static final Gray x2B = _43; + public static final Gray x2C = _44; + public static final Gray x2D = _45; + public static final Gray x2E = _46; + public static final Gray x2F = _47; + public static final Gray x30 = _48; + public static final Gray x31 = _49; + public static final Gray x32 = _50; + public static final Gray x33 = _51; + public static final Gray x34 = _52; + public static final Gray x35 = _53; + public static final Gray x36 = _54; + public static final Gray x37 = _55; + public static final Gray x38 = _56; + public static final Gray x39 = _57; + public static final Gray x3A = _58; + public static final Gray x3B = _59; + public static final Gray x3C = _60; + public static final Gray x3D = _61; + public static final Gray x3E = _62; + public static final Gray x3F = _63; + public static final Gray x40 = _64; + public static final Gray x41 = _65; + public static final Gray x42 = _66; + public static final Gray x43 = _67; + public static final Gray x44 = _68; + public static final Gray x45 = _69; + public static final Gray x46 = _70; + public static final Gray x47 = _71; + public static final Gray x48 = _72; + public static final Gray x49 = _73; + public static final Gray x4A = _74; + public static final Gray x4B = _75; + public static final Gray x4C = _76; + public static final Gray x4D = _77; + public static final Gray x4E = _78; + public static final Gray x4F = _79; + public static final Gray x50 = _80; + public static final Gray x51 = _81; + public static final Gray x52 = _82; + public static final Gray x53 = _83; + public static final Gray x54 = _84; + public static final Gray x55 = _85; + public static final Gray x56 = _86; + public static final Gray x57 = _87; + public static final Gray x58 = _88; + public static final Gray x59 = _89; + public static final Gray x5A = _90; + public static final Gray x5B = _91; + public static final Gray x5C = _92; + public static final Gray x5D = _93; + public static final Gray x5E = _94; + public static final Gray x5F = _95; + public static final Gray x60 = _96; + public static final Gray x61 = _97; + public static final Gray x62 = _98; + public static final Gray x63 = _99; + public static final Gray x64 = _100; + public static final Gray x65 = _101; + public static final Gray x66 = _102; + public static final Gray x67 = _103; + public static final Gray x68 = _104; + public static final Gray x69 = _105; + public static final Gray x6A = _106; + public static final Gray x6B = _107; + public static final Gray x6C = _108; + public static final Gray x6D = _109; + public static final Gray x6E = _110; + public static final Gray x6F = _111; + public static final Gray x70 = _112; + public static final Gray x71 = _113; + public static final Gray x72 = _114; + public static final Gray x73 = _115; + public static final Gray x74 = _116; + public static final Gray x75 = _117; + public static final Gray x76 = _118; + public static final Gray x77 = _119; + public static final Gray x78 = _120; + public static final Gray x79 = _121; + public static final Gray x7A = _122; + public static final Gray x7B = _123; + public static final Gray x7C = _124; + public static final Gray x7D = _125; + public static final Gray x7E = _126; + public static final Gray x7F = _127; + public static final Gray x80 = _128; + public static final Gray x81 = _129; + public static final Gray x82 = _130; + public static final Gray x83 = _131; + public static final Gray x84 = _132; + public static final Gray x85 = _133; + public static final Gray x86 = _134; + public static final Gray x87 = _135; + public static final Gray x88 = _136; + public static final Gray x89 = _137; + public static final Gray x8A = _138; + public static final Gray x8B = _139; + public static final Gray x8C = _140; + public static final Gray x8D = _141; + public static final Gray x8E = _142; + public static final Gray x8F = _143; + public static final Gray x90 = _144; + public static final Gray x91 = _145; + public static final Gray x92 = _146; + public static final Gray x93 = _147; + public static final Gray x94 = _148; + public static final Gray x95 = _149; + public static final Gray x96 = _150; + public static final Gray x97 = _151; + public static final Gray x98 = _152; + public static final Gray x99 = _153; + public static final Gray x9A = _154; + public static final Gray x9B = _155; + public static final Gray x9C = _156; + public static final Gray x9D = _157; + public static final Gray x9E = _158; + public static final Gray x9F = _159; + public static final Gray xA0 = _160; + public static final Gray xA1 = _161; + public static final Gray xA2 = _162; + public static final Gray xA3 = _163; + public static final Gray xA4 = _164; + public static final Gray xA5 = _165; + public static final Gray xA6 = _166; + public static final Gray xA7 = _167; + public static final Gray xA8 = _168; + public static final Gray xA9 = _169; + public static final Gray xAA = _170; + public static final Gray xAB = _171; + public static final Gray xAC = _172; + public static final Gray xAD = _173; + public static final Gray xAE = _174; + public static final Gray xAF = _175; + public static final Gray xB0 = _176; + public static final Gray xB1 = _177; + public static final Gray xB2 = _178; + public static final Gray xB3 = _179; + public static final Gray xB4 = _180; + public static final Gray xB5 = _181; + public static final Gray xB6 = _182; + public static final Gray xB7 = _183; + public static final Gray xB8 = _184; + public static final Gray xB9 = _185; + public static final Gray xBA = _186; + public static final Gray xBB = _187; + public static final Gray xBC = _188; + public static final Gray xBD = _189; + public static final Gray xBE = _190; + public static final Gray xBF = _191; + public static final Gray xC0 = _192; + public static final Gray xC1 = _193; + public static final Gray xC2 = _194; + public static final Gray xC3 = _195; + public static final Gray xC4 = _196; + public static final Gray xC5 = _197; + public static final Gray xC6 = _198; + public static final Gray xC7 = _199; + public static final Gray xC8 = _200; + public static final Gray xC9 = _201; + public static final Gray xCA = _202; + public static final Gray xCB = _203; + public static final Gray xCC = _204; + public static final Gray xCD = _205; + public static final Gray xCE = _206; + public static final Gray xCF = _207; + public static final Gray xD0 = _208; + public static final Gray xD1 = _209; + public static final Gray xD2 = _210; + public static final Gray xD3 = _211; + public static final Gray xD4 = _212; + public static final Gray xD5 = _213; + public static final Gray xD6 = _214; + public static final Gray xD7 = _215; + public static final Gray xD8 = _216; + public static final Gray xD9 = _217; + public static final Gray xDA = _218; + public static final Gray xDB = _219; + public static final Gray xDC = _220; + public static final Gray xDD = _221; + public static final Gray xDE = _222; + public static final Gray xDF = _223; + public static final Gray xE0 = _224; + public static final Gray xE1 = _225; + public static final Gray xE2 = _226; + public static final Gray xE3 = _227; + public static final Gray xE4 = _228; + public static final Gray xE5 = _229; + public static final Gray xE6 = _230; + public static final Gray xE7 = _231; + public static final Gray xE8 = _232; + public static final Gray xE9 = _233; + public static final Gray xEA = _234; + public static final Gray xEB = _235; + public static final Gray xEC = _236; + public static final Gray xED = _237; + public static final Gray xEE = _238; + public static final Gray xEF = _239; + public static final Gray xF0 = _240; + public static final Gray xF1 = _241; + public static final Gray xF2 = _242; + public static final Gray xF3 = _243; + public static final Gray xF4 = _244; + public static final Gray xF5 = _245; + public static final Gray xF6 = _246; + public static final Gray xF7 = _247; + public static final Gray xF8 = _248; + public static final Gray xF9 = _249; + public static final Gray xFA = _250; + public static final Gray xFB = _251; + public static final Gray xFC = _252; + public static final Gray xFD = _253; + public static final Gray xFE = _254; + public static final Gray xFF = _255; + + private static final Gray[] cache = { + _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, + _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, + _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, + _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, + _64, _65, _66, _67, _68, _69, _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, + _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, + _96, _97, _98, _99, _100, _101, _102, _103, _104, _105, _106, _107, _108, _109, _110, _111, + _112, _113, _114, _115, _116, _117, _118, _119, _120, _121, _122, _123, _124, _125, _126, _127, + _128, _129, _130, _131, _132, _133, _134, _135, _136, _137, _138, _139, _140, _141, _142, _143, + _144, _145, _146, _147, _148, _149, _150, _151, _152, _153, _154, _155, _156, _157, _158, _159, + _160, _161, _162, _163, _164, _165, _166, _167, _168, _169, _170, _171, _172, _173, _174, _175, + _176, _177, _178, _179, _180, _181, _182, _183, _184, _185, _186, _187, _188, _189, _190, _191, + _192, _193, _194, _195, _196, _197, _198, _199, _200, _201, _202, _203, _204, _205, _206, _207, + _208, _209, _210, _211, _212, _213, _214, _215, _216, _217, _218, _219, _220, _221, _222, _223, + _224, _225, _226, _227, _228, _229, _230, _231, _232, _233, _234, _235, _236, _237, _238, _239, + _240, _241, _242, _243, _244, _245, _246, _247, _248, _249, _250, _251, _252, _253, _254, _255}; + + //public static void main(String[] args) { + // for (int i = 0; i < 256; i++) { + // System.out.println("public static final Gray _" + i + " = new Gray("+ i + ");"); + // } + // for (int i = 0; i < 256; i++) { + // System.out.println("public static final Gray x" + (i < 16 ? "0" : "") + Integer.toHexString(i).toUpperCase() + " = _" + i + ";"); + // } + // + // System.out.println(); + // System.out.println("private static final Gray[] cache = {"); + // System.out.print(" "); + // for (int i = 0; i < 256; i++) { + // System.out.print(String.format("%4s", "_" + String.valueOf(i))); + // if (i == 255) { + // System.out.println("};"); + // } else { + // if (i % 16 == 15) { + // System.out.println(","); + // System.out.print(" "); + // } else { + // System.out.print(", "); + // } + // } + // } + //} +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ImageLoader.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ImageLoader.java new file mode 100644 index 000000000..224790189 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ImageLoader.java @@ -0,0 +1,404 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import com.bulenkov.iconloader.IconLoader; +import com.bulenkov.iconloader.RetinaImage; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.ImageFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.concurrent.ConcurrentMap; + +/** + * @author Konstantin Bulenkov + */ +public class ImageLoader implements Serializable { +// private static final Log LOG = Logger.getLogger("#com.intellij.util.ImageLoader"); + + private static final ConcurrentMap ourCache = new ConcurrentSoftValueHashMap(); + + private static class ImageDesc { + public enum Type { + PNG, + +// SVG { +// @Override +// public Image load(URL url, InputStream is, float scale) throws IOException { +// return SVGLoader.load(url, is, scale); +// } +// }, + + UNDEFINED; + + public Image load(URL url, InputStream stream, float scale) throws IOException { + return ImageLoader.load(stream, (int)scale); + } + } + + public final String path; + public final @Nullable Class cls; // resource class if present + public final float scale; // initial scale factor + public final Type type; + public final boolean original; // path is not altered + + public ImageDesc(String path, Class cls, float scale, Type type) { + this(path, cls, scale, type, false); + } + + public ImageDesc(String path, Class cls, float scale, Type type, boolean original) { + this.path = path; + this.cls = cls; + this.scale = scale; + this.type = type; + this.original = original; + } + + @Nullable + public Image load() throws IOException { + String cacheKey = null; + InputStream stream = null; + URL url = null; + if (cls != null) { + //noinspection IOResourceOpenedButNotSafelyClosed + stream = cls.getResourceAsStream(path); + if (stream == null) return null; + } + if (stream == null) { + url = new URL(path); + URLConnection connection = url.openConnection(); + if (connection instanceof HttpURLConnection) { + if (!original) return null; + connection.addRequestProperty("User-Agent", "IntelliJ"); + + cacheKey = path; + Image image = ourCache.get(cacheKey); + if (image != null) return image; + } + stream = connection.getInputStream(); + } + Image image = type.load(url, stream, scale); + if (image != null && cacheKey != null) { + ourCache.put(cacheKey, image); + } + return image; + } + + @Override + public String toString() { + return path + ", scale: " + scale + ", type: " + type; + } + } + + private static class ImageDescList extends ArrayList { + private ImageDescList() {} + + @Nullable + public Image load() { + return load(ImageConverterChain.create()); + } + + @Nullable + public Image load(@NotNull ImageConverterChain converters) { + for (ImageDesc desc : this) { + try { + Image image = desc.load(); + if (image == null) continue; +// LOG.debug("Loaded image: " + desc); + return converters.convert(image, desc); + } + catch (IOException ignore) { + } + } + return null; + } + + public static ImageDescList create(@NotNull String file, + @Nullable Class cls, + boolean dark, + boolean retina, + boolean allowFloatScaling) + { + ImageDescList vars = new ImageDescList(); + if (retina || dark) { + final String name = getNameWithoutExtension(file); + final String ext = getExtension(file); + + float scale = calcScaleFactor(allowFloatScaling); + + // TODO: allow SVG images to freely scale on Retina + +// if (Registry.is("ide.svg.icon") && dark) { +// vars.add(new ImageDesc(name + "_dark.svg", cls, UIUtil.isRetina() ? 2f : scale, ImageDesc.Type.SVG)); +// } +// +// if (Registry.is("ide.svg.icon")) { +// vars.add(new ImageDesc(name + ".svg", cls, UIUtil.isRetina() ? 2f : scale, ImageDesc.Type.SVG)); +// } + + if (dark && retina) { + vars.add(new ImageDesc(name + "@2x_dark." + ext, cls, 2f, ImageDesc.Type.PNG)); + } + + if (dark) { + vars.add(new ImageDesc(name + "_dark." + ext, cls, 1f, ImageDesc.Type.PNG)); + } + + if (retina) { + vars.add(new ImageDesc(name + "@2x." + ext, cls, 2f, ImageDesc.Type.PNG)); + } + } + vars.add(new ImageDesc(file, cls, 1f, ImageDesc.Type.PNG, true)); + return vars; + } + } + + private interface ImageConverter { + Image convert(@Nullable Image source, ImageDesc desc); + } + + private static class ImageConverterChain extends ArrayList { + private ImageConverterChain() {} + + public static ImageConverterChain create() { + return new ImageConverterChain(); + } + + public ImageConverterChain withFilter(final ImageFilter filter) { + return with(new ImageConverter() { + @Override + public Image convert(Image source, ImageDesc desc) { + return ImageUtil.filter(source, filter); + } + }); + } + + public ImageConverterChain withRetina() { + return with(new ImageConverter() { + @Override + public Image convert(Image source, ImageDesc desc) { + if (source != null && UIUtil.isRetina() && desc.scale > 1) { + return RetinaImage.createFrom(source, (int)desc.scale, ourComponent); + } + return source; + } + }); + } + + public ImageConverterChain with(ImageConverter f) { + add(f); + return this; + } + + public Image convert(Image image, ImageDesc desc) { + for (ImageConverter f : this) { + image = f.convert(image, desc); + } + return image; + } + } + + public static final Component ourComponent = new Component() { + }; + + private static boolean waitForImage(Image image) { + if (image == null) return false; + if (image.getWidth(null) > 0) return true; + MediaTracker mediatracker = new MediaTracker(ourComponent); + mediatracker.addImage(image, 1); + try { + mediatracker.waitForID(1, 5000); + } + catch (InterruptedException ex) { + ex.printStackTrace(); + } + return !mediatracker.isErrorID(1); + } + + @Nullable + public static Image loadFromUrl(@NotNull URL url) { + return loadFromUrl(url, true); + } + + @Nullable + public static Image loadFromUrl(@NotNull URL url, boolean allowFloatScaling) { + return loadFromUrl(url, allowFloatScaling, null); + } + + @Nullable + public static Image loadFromUrl(@NotNull URL url, boolean allowFloatScaling, ImageFilter filter) { + final float scaleFactor = calcScaleFactor(allowFloatScaling); + + // We can't check all 3rd party plugins and convince the authors to add @2x icons. + // (scaleFactor > 1.0) != isRetina() => we should scale images manually. + // Note we never scale images on Retina displays because scaling is handled by the system. + final boolean scaleImages = (scaleFactor > 1.0f) && !UIUtil.isRetina(); + + // For any scale factor > 1.0, always prefer retina images, because downscaling + // retina images provides a better result than upscaling non-retina images. + final boolean loadRetinaImages = UIUtil.isRetina() || scaleImages; + + return ImageDescList.create(url.toString(), null, UIUtil.isUnderDarcula(), loadRetinaImages, allowFloatScaling).load( + ImageConverterChain.create(). + withFilter(filter). + withRetina(). + with(new ImageConverter() { + public Image convert(Image source, ImageDesc desc) { + if (source != null && scaleImages /*&& desc.type != ImageDesc.Type.SVG*/) { + if (desc.path.contains("@2x")) + return scaleImage(source, scaleFactor / 2.0f); // divide by 2.0 as Retina images are 2x the resolution. + else + return scaleImage(source, scaleFactor); + } + return source; + } + })); + } + + private static float calcScaleFactor(boolean allowFloatScaling) { + float scaleFactor = allowFloatScaling ? JBUI.scale(1f) : JBUI.scale(1f) > 1.5f ? 2f : 1f; + assert scaleFactor >= 1.0f : "By design, only scale factors >= 1.0 are supported"; + return scaleFactor; + } + + @NotNull + private static Image scaleImage(Image image, float scale) { + int w = image.getWidth(null); + int h = image.getHeight(null); + if (w <= 0 || h <= 0) { + return image; + } + int width = (int)(scale * w); + int height = (int)(scale * h); + // Using "QUALITY" instead of "ULTRA_QUALITY" results in images that are less blurry + // because ultra quality performs a few more passes when scaling, which introduces blurriness + // when the scaling factor is relatively small (i.e. <= 3.0f) -- which is the case here. + return Scalr.resize(ImageUtil.toBufferedImage(image), Scalr.Method.QUALITY, width, height); + } + + @Nullable + public static Image loadFromUrl(URL url, boolean dark, boolean retina) { + return loadFromUrl(url, dark, retina, null); + } + + @Nullable + public static Image loadFromUrl(URL url, boolean dark, boolean retina, ImageFilter filter) { + return ImageDescList.create(url.toString(), null, dark, retina, true). + load(ImageConverterChain.create().withFilter(filter).withRetina()); + } + + @Nullable + public static Image loadFromResource(@NonNls @NotNull String s) { + Class callerClass = ReflectionUtil.getGrandCallerClass(); + if (callerClass == null) return null; + return loadFromResource(s, callerClass); + } + + @Nullable + public static Image loadFromResource(@NonNls @NotNull String path, @NotNull Class aClass) { + return ImageDescList.create(path, aClass, UIUtil.isUnderDarcula(), UIUtil.isRetina() || JBUI.scale(1.0f) >= 1.5f, true). + load(ImageConverterChain.create().withRetina()); + } + + public static Image loadFromStream(@NotNull final InputStream inputStream) { + return loadFromStream(inputStream, 1); + } + + public static Image loadFromStream(@NotNull final InputStream inputStream, final int scale) { + return loadFromStream(inputStream, scale, null); + } + + public static Image loadFromStream(@NotNull final InputStream inputStream, final int scale, ImageFilter filter) { + Image image = load(inputStream, scale); + ImageDesc desc = new ImageDesc("", null, scale, ImageDesc.Type.UNDEFINED); + return ImageConverterChain.create().withFilter(filter).withRetina().convert(image, desc); + } + + private static Image load(@NotNull final InputStream inputStream, final int scale) { + if (scale <= 0) throw new IllegalArgumentException("Scale must be 1 or greater"); + try { + BufferExposingByteArrayOutputStream outputStream = new BufferExposingByteArrayOutputStream(); + try { + byte[] buffer = new byte[1024]; + while (true) { + final int n = inputStream.read(buffer); + if (n < 0) break; + outputStream.write(buffer, 0, n); + } + } + finally { + inputStream.close(); + } + + Image image = Toolkit.getDefaultToolkit().createImage(outputStream.getInternalBuffer(), 0, outputStream.size()); + + waitForImage(image); + + return image; + } + catch (Exception ex) { + ex.printStackTrace(); + } + + return null; + } + + public static boolean isGoodSize(final Icon icon) { + return IconLoader.isGoodSize(icon); + } + + /** + * @deprecated use {@link ImageDescList} + */ + public static java.util.List> getFileNames(@NotNull String file) { + return getFileNames(file, false, false); + } + + /** + * @deprecated use {@link ImageDescList} + */ + public static java.util.List> getFileNames(@NotNull String file, boolean dark, boolean retina) { + new UnsupportedOperationException("unsupported method").printStackTrace(); + return new ArrayList>(); + } + + @NotNull + public static String getNameWithoutExtension(@NotNull String name) { + int i = name.lastIndexOf('.'); + if (i != -1) { + name = name.substring(0, i); + } + return name; + } + + @NotNull + public static String getExtension(@NotNull String fileName) { + int index = fileName.lastIndexOf('.'); + if (index < 0) return ""; + return fileName.substring(index + 1); + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ImageUtil.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ImageUtil.java new file mode 100644 index 000000000..32d123111 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ImageUtil.java @@ -0,0 +1,71 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import com.bulenkov.iconloader.JBHiDPIScaledImage; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.FilteredImageSource; +import java.awt.image.ImageFilter; + +/** + * @author Konstantin Bulenkov + */ +public class ImageUtil { + public static BufferedImage toBufferedImage(@NotNull Image image) { + if (image instanceof JBHiDPIScaledImage) { + Image img = ((JBHiDPIScaledImage)image).getDelegate(); + if (img != null) { + image = img; + } + } + if (image instanceof BufferedImage) { + return (BufferedImage)image; + } + + @SuppressWarnings("UndesirableClassUsage") + BufferedImage bufferedImage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB); + Graphics2D g = bufferedImage.createGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + return bufferedImage; + } + + public static int getRealWidth(@NotNull Image image) { + if (image instanceof JBHiDPIScaledImage) { + Image img = ((JBHiDPIScaledImage)image).getDelegate(); + if (img != null) image = img; + } + return image.getWidth(null); + } + + public static int getRealHeight(@NotNull Image image) { + if (image instanceof JBHiDPIScaledImage) { + Image img = ((JBHiDPIScaledImage)image).getDelegate(); + if (img != null) image = img; + } + return image.getHeight(null); + } + + public static Image filter(Image image, ImageFilter filter) { + if (image == null || filter == null) return image; + return Toolkit.getDefaultToolkit().createImage( + new FilteredImageSource(toBufferedImage(image).getSource(), filter)); + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBDimension.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBDimension.java new file mode 100644 index 000000000..1ccd9eb20 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBDimension.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import javax.swing.plaf.UIResource; +import java.awt.*; + +/** + * @author Konstantin Bulenkov + */ +public class JBDimension extends Dimension { + public final float originalScale = JBUI.scale(1f); + + public JBDimension(int width, int height) { + super(scale(width), scale(height)); + } + + private static int scale(int size) { + return size == -1 ? -1 : JBUI.scale(size); + } + + public static JBDimension create(Dimension from) { + if (from instanceof JBDimension) { + return ((JBDimension)from); + } + return new JBDimension(from.width, from.height); + } + + public JBDimensionUIResource asUIResource() { + return new JBDimensionUIResource(this); + } + + public static class JBDimensionUIResource extends JBDimension implements UIResource { + public JBDimensionUIResource(JBDimension size) { + super(0, 0); + width = size.width; + height = size.height; + } + } + + public JBDimension withWidth(int width) { + JBDimension size = new JBDimension(0, 0); + size.width = scale(width); + size.height = height; + return size; + } + + public JBDimension withHeight(int height) { + JBDimension size = new JBDimension(0, 0); + size.width = width; + size.height = scale(height); + return size; + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBEmptyBorder.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBEmptyBorder.java new file mode 100644 index 000000000..e5bc560fc --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBEmptyBorder.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bulenkov.iconloader.util; + +import javax.swing.border.EmptyBorder; +import javax.swing.plaf.UIResource; +import java.awt.*; + +/** + * @author Konstantin Bulenkov + */ +public class JBEmptyBorder extends EmptyBorder { + public JBEmptyBorder(int top, int left, int bottom, int right) { + super(JBUI.insets(top, left, bottom, right)); + } + + public JBEmptyBorder(Insets insets) { + super(JBUI.insets(insets)); + } + + public JBEmptyBorder(int offset) { + this(offset, offset, offset, offset); + } + + public JBEmptyBorderUIResource asUIResource() { + return new JBEmptyBorderUIResource(this); + } + + public static class JBEmptyBorderUIResource extends JBEmptyBorder implements UIResource { + public JBEmptyBorderUIResource(JBEmptyBorder border) { + super(0,0,0,0); + top = border.top; + left = border.left; + bottom = border.bottom; + right = border.right; + } + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBFont.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBFont.java new file mode 100644 index 000000000..f2bc2cca9 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBFont.java @@ -0,0 +1,84 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bulenkov.iconloader.util; + +import javax.swing.plaf.UIResource; +import java.awt.*; + +/** + * @author Konstantin Bulenkov + */ +public class JBFont extends Font { + private JBFont(Font font) { + super(font); + } + + public static JBFont create(Font font) { + return create(font, true); + } + + public static JBFont create(Font font, boolean tryToScale) { + if (font instanceof JBFont) { + return ((JBFont)font); + } + Font scaled = font; + if (tryToScale) { + scaled = font.deriveFont(font.getSize() * JBUI.scale(1f)); + } + + if (font instanceof UIResource) { + return new JBFontUIResource(scaled); + } + + return new JBFont(scaled); + } + + public JBFont asBold() { + return deriveFont(BOLD, getSize()); + } + + public JBFont asItalic() { + return deriveFont(ITALIC, getSize()); + } + + public JBFont asPlain() { + return deriveFont(PLAIN, getSize()); + } + + @Override + public JBFont deriveFont(int style, float size) { + return create(super.deriveFont(style, size), false); + } + + @Override + public JBFont deriveFont(float size) { + return create(super.deriveFont(size), false); + } + + public JBFont biggerOn(float size) { + return deriveFont(getSize() + JBUI.scale(size)); + } + + public JBFont lessOn(float size) { + return deriveFont(getSize() - JBUI.scale(size)); + } + + private static class JBFontUIResource extends JBFont implements UIResource { + private JBFontUIResource(Font font) { + super(font); + } + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBImageIcon.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBImageIcon.java new file mode 100644 index 000000000..c3f9131f1 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBImageIcon.java @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.ImageObserver; + +/** + * HiDPI-aware image icon + * + * @author Konstantin Bulenkov + */ +public class JBImageIcon extends ImageIcon { + public JBImageIcon(@NotNull Image image) { + super(image); + } + + @Override + public synchronized void paintIcon(final Component c, final Graphics g, final int x, final int y) { + final ImageObserver observer = getImageObserver(); + + UIUtil.drawImage(g, getImage(), x, y, observer == null ? c : observer); + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBInsets.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBInsets.java new file mode 100644 index 000000000..61f3d511a --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBInsets.java @@ -0,0 +1,121 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bulenkov.iconloader.util; + +import javax.swing.plaf.UIResource; +import java.awt.*; + +import static com.bulenkov.iconloader.util.JBUI.scale; + +/** + * @author Konstantin Bulenkov + */ +public class JBInsets extends Insets { + /** + * Creates and initializes a new Insets object with the + * specified top, left, bottom, and right insets. + * + * @param top the inset from the top. + * @param left the inset from the left. + * @param bottom the inset from the bottom. + * @param right the inset from the right. + */ + public JBInsets(int top, int left, int bottom, int right) { + super(scale(top), scale(left), scale(bottom), scale(right)); + } + + public int width() { + return left + right; + } + + public int height() { + return top + bottom; + } + + public static JBInsets create(Insets insets) { + if (insets instanceof JBInsets) { + JBInsets copy = new JBInsets(0, 0, 0, 0); + copy.top = insets.top; + copy.left = insets.left; + copy.bottom = insets.bottom; + copy.right = insets.right; + return copy; + } + return new JBInsets(insets.top, insets.left, insets.bottom, insets.right); + } + + public JBInsetsUIResource asUIResource() { + return new JBInsetsUIResource(this); + } + + public static class JBInsetsUIResource extends JBInsets implements UIResource { + public JBInsetsUIResource(JBInsets insets) { + super(0, 0, 0, 0); + top = insets.top; + left = insets.left; + bottom = insets.bottom; + right = insets.right; + } + } + + /** + * @param dimension the size to increase + * @param insets the insets to add + */ + public static void addTo(Dimension dimension, Insets insets) { + if (insets != null) { + dimension.width += insets.left + insets.right; + dimension.height += insets.top + insets.bottom; + } + } + + /** + * @param dimension the size to decrease + * @param insets the insets to remove + */ + public static void removeFrom(Dimension dimension, Insets insets) { + if (insets != null) { + dimension.width -= insets.left + insets.right; + dimension.height -= insets.top + insets.bottom; + } + } + + /** + * @param rectangle the size to increase and the location to move + * @param insets the insets to add + */ + public static void addTo(Rectangle rectangle, Insets insets) { + if (insets != null) { + rectangle.x -= insets.left; + rectangle.y -= insets.top; + rectangle.width += insets.left + insets.right; + rectangle.height += insets.top + insets.bottom; + } + } + + /** + * @param rectangle the size to decrease and the location to move + * @param insets the insets to remove + */ + public static void removeFrom(Rectangle rectangle, Insets insets) { + if (insets != null) { + rectangle.x += insets.left; + rectangle.y += insets.top; + rectangle.width -= insets.left + insets.right; + rectangle.height -= insets.top + insets.bottom; + } + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBUI.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBUI.java new file mode 100644 index 000000000..6050e3aae --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/JBUI.java @@ -0,0 +1,221 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import com.bulenkov.iconloader.IconLoader; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.plaf.UIResource; +import java.awt.*; + +/** + * @author Konstantin Bulenkov + */ +public class JBUI { + private static float scaleFactor = 1.0f; + + static { + calculateScaleFactor(); + } + + private static void calculateScaleFactor() { + if (SystemInfo.isMac) { + scaleFactor = 1.0f; + return; + } + + if (System.getProperty("hidpi") != null && !"true".equalsIgnoreCase(System.getProperty("hidpi"))) { + scaleFactor = 1.0f; + return; + } + + UIUtil.initSystemFontData(); + Pair fdata = UIUtil.getSystemFontData(); + + int size; + if (fdata != null) { + size = fdata.getSecond(); + } else { + size = Fonts.label().getSize(); + } + setScaleFactor(size / UIUtil.DEF_SYSTEM_FONT_SIZE); + } + + public static void setScaleFactor(float scale) { + final String value = System.getProperty("hidpi"); + if (value != null && "false".equalsIgnoreCase(value)) { + return; + } + + if (scale < 1.25f) scale = 1.0f; + else if (scale < 1.5f) scale = 1.25f; + else if (scale < 1.75f) scale = 1.5f; + else if (scale < 2f) scale = 1.75f; + else scale = 2.0f; + + if (SystemInfo.isLinux && scale == 1.25f) { + //Default UI font size for Unity and Gnome is 15. Scaling factor 1.25f works badly on Linux + scale = 1f; + } + if (scaleFactor == scale) { + return; + } + + scaleFactor = scale; + IconLoader.setScale(scale); + } + + public static int scale(int i) { + return Math.round(scaleFactor * i); + } + + public static int scaleFontSize(int fontSize) { + if (scaleFactor == 1.25f) return (int)(fontSize * 1.34f); + if (scaleFactor == 1.75f) return (int)(fontSize * 1.67f); + return scale(fontSize); + } + + public static JBDimension size(int width, int height) { + return new JBDimension(width, height); + } + + public static JBDimension size(int widthAndHeight) { + return new JBDimension(widthAndHeight, widthAndHeight); + } + + public static JBDimension size(Dimension size) { + if (size instanceof JBDimension) { + final JBDimension jbSize = (JBDimension)size; + if (jbSize.originalScale == scale(1f)) { + return jbSize; + } + final JBDimension newSize = new JBDimension((int)(jbSize.width / jbSize.originalScale), (int)(jbSize.height / jbSize.originalScale)); + return size instanceof UIResource ? newSize.asUIResource() : newSize; + } + return new JBDimension(size.width, size.height); + } + + public static JBInsets insets(int top, int left, int bottom, int right) { + return new JBInsets(top, left, bottom, right); + } + + public static JBInsets insets(int all) { + return insets(all, all, all, all); + } + + public static JBInsets insets(int topBottom, int leftRight) { + return insets(topBottom, leftRight, topBottom, leftRight); + } + + public static JBInsets emptyInsets() { + return new JBInsets(0, 0, 0, 0); + } + + public static JBInsets insetsTop(int t) { + return insets(t, 0, 0, 0); + } + + public static JBInsets insetsLeft(int l) { + return insets(0, l, 0, 0); + } + + public static JBInsets insetsBottom(int b) { + return insets(0, 0, b, 0); + } + + public static JBInsets insetsRight(int r) { + return insets(0, 0, 0, r); + } + + public static EmptyIcon emptyIcon(int i) { + return (EmptyIcon)EmptyIcon.create(scale(i)); + } + + public static JBDimension emptySize() { + return new JBDimension(0, 0); + } + + public static float scale(float f) { + return f * scaleFactor; + } + + public static JBInsets insets(Insets insets) { + return JBInsets.create(insets); + } + + public static boolean isHiDPI() { + return scaleFactor > 1.0f; + } + + public static class Fonts { + public static JBFont label() { + return JBFont.create(UIManager.getFont("Label.font"), false); + } + + public static JBFont label(float size) { + return label().deriveFont(scale(size)); + } + + public static JBFont smallFont() { + return label().deriveFont(UIUtil.getFontSize(UIUtil.FontSize.SMALL)); + } + + public static JBFont miniFont() { + return label().deriveFont(UIUtil.getFontSize(UIUtil.FontSize.MINI)); + } + + public static JBFont create(String fontFamily, int size) { + return JBFont.create(new Font(fontFamily, Font.PLAIN, size)); + } + } + + public static class Borders { + public static JBEmptyBorder empty(int top, int left, int bottom, int right) { + return new JBEmptyBorder(top, left, bottom, right); + } + + public static JBEmptyBorder empty(int topAndBottom, int leftAndRight) { + return empty(topAndBottom, leftAndRight, topAndBottom, leftAndRight); + } + + public static JBEmptyBorder emptyTop(int offset) { + return empty(offset, 0, 0, 0); + } + + public static JBEmptyBorder emptyLeft(int offset) { + return empty(0, offset, 0, 0); + } + + public static JBEmptyBorder emptyBottom(int offset) { + return empty(0, 0, offset, 0); + } + + public static JBEmptyBorder emptyRight(int offset) { + return empty(0, 0, 0, offset); + } + + public static JBEmptyBorder empty() { + return empty(0, 0, 0, 0); + } + + public static Border empty(int offsets) { + return empty(offsets, offsets, offsets, offsets); + } + + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Pair.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Pair.java new file mode 100644 index 000000000..b38a3aacd --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Pair.java @@ -0,0 +1,74 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +/** + * @author Konstantin Bulenkov + */ +public class Pair { + public final FIRST first; + public final SECOND second; + + @SuppressWarnings("unchecked") + private static final Pair EMPTY = create(null, null); + + @SuppressWarnings("unchecked") + public static Pair empty() { + return EMPTY; + } + + public Pair(FIRST first, SECOND second) { + this.first = first; + this.second = second; + } + + public final FIRST getFirst() { + return first; + } + + public final SECOND getSecond() { + return second; + } + + public final boolean equals(Object o) { + return o instanceof Pair + && ComparingUtils.equal(first, ((Pair) o).first) + && ComparingUtils.equal(second, ((Pair) o).second); + } + + public int hashCode() { + int result = first != null ? first.hashCode() : 0; + result = 31 * result + (second != null ? second.hashCode() : 0); + return result; + } + + public String toString() { + return "<" + first + ", " + second + ">"; + } + + public static Pair create(F first, S second) { + return new Pair(first, second); + } + + public static T getFirst(Pair pair) { + return pair != null ? pair.first : null; + } + + public static T getSecond(Pair pair) { + return pair != null ? pair.second : null; + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Ref.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Ref.java new file mode 100644 index 000000000..f3748ed82 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Ref.java @@ -0,0 +1,64 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +/** + * @author Konstantin Bulenkov + */ +public class Ref { + private T myValue; + + public Ref() { + } + + public Ref(T value) { + myValue = value; + } + + public boolean isNull() { + return myValue == null; + } + + public T get() { + return myValue; + } + + public void set(T value) { + myValue = value; + } + + public boolean setIfNull(T value) { + if (myValue == null) { + myValue = value; + return true; + } + return false; + } + + public static Ref create() { + return new Ref(); + } + + public static Ref create(V value) { + return new Ref(value); + } + + @Override + public String toString() { + return String.valueOf(myValue); + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ReflectionUtil.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ReflectionUtil.java new file mode 100644 index 000000000..19a8b8680 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ReflectionUtil.java @@ -0,0 +1,65 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import org.jetbrains.annotations.Nullable; + +public class ReflectionUtil { + @Nullable + public static Class getGrandCallerClass() { + int stackFrameCount = 3; + Class callerClass = findCallerClass(stackFrameCount); + while (callerClass != null && callerClass.getClassLoader() == null) { // looks like a system class + callerClass = findCallerClass(++stackFrameCount); + } + if (callerClass == null) { + callerClass = findCallerClass(2); + } + return callerClass; + } + + /** + * Returns the class this method was called 'framesToSkip' frames up the caller hierarchy. + * + * NOTE: + * Extremely expensive! + * Please consider not using it. + * These aren't the droids you're looking for! + */ + @Nullable + public static Class findCallerClass(int framesToSkip) { + try { + Class[] stack = MySecurityManager.INSTANCE.getStack(); + int indexFromTop = 1 + framesToSkip; + return stack.length > indexFromTop ? stack[indexFromTop] : null; + } + catch (Exception e) { +// LOG.warn(e); + return null; + } + } + + private static class MySecurityManager extends SecurityManager { + private static final MySecurityManager INSTANCE = new MySecurityManager(); + public Class[] getStack() { + return getClassContext(); + } + + } + + private ReflectionUtil() { } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Registry.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Registry.java new file mode 100644 index 000000000..979a63af8 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Registry.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +/** + * @author Konstantin Bulenkov + */ +public class Registry { + public static boolean is(String key) { + final String value = System.getProperty(key); + return "true".equalsIgnoreCase(value); + } + + public static Float getFloat(String key) { + try { + return Float.parseFloat(System.getProperty(key)); + } catch (Exception e) { + return null; + } + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/RetrievableIcon.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/RetrievableIcon.java new file mode 100644 index 000000000..6d37addec --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/RetrievableIcon.java @@ -0,0 +1,30 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +/** + * This class is mostly for testing purposes: in case an icon is hidden behind a private or a restricted interface, + * marking it as RetrievableIcon will help get the actual icon and perform checks. + */ +public interface RetrievableIcon extends Icon { + @Nullable + Icon retrieveIcon(); +} \ No newline at end of file diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ScalableIcon.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ScalableIcon.java new file mode 100644 index 000000000..d3e46df95 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/ScalableIcon.java @@ -0,0 +1,30 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import javax.swing.*; + +/** + * @author Konstantin Bulenkov + */ +public interface ScalableIcon extends Icon { + /** + * @param scaleFactor scale + * @return scaled icon with width getIconWidth() * scaleFactor and height getIconHeight() * scaleFactor + */ + Icon scale(float scaleFactor); +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Scalr.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Scalr.java new file mode 100644 index 000000000..94c3f56e1 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/Scalr.java @@ -0,0 +1,2303 @@ +/** + * Copyright 2011 The Buzz Media, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.*; + +/** + * Class used to implement performant, high-quality and intelligent image + * scaling and manipulation algorithms in native Java 2D. + *

+ * This class utilizes the Java2D "best practices" for image manipulation, + * ensuring that all operations (even most user-provided {@link BufferedImageOp} + * s) are hardware accelerated if provided by the platform and host-VM. + *

+ *

Image Quality

+ * This class implements a few different methods for scaling an image, providing + * either the best-looking result, the fastest result or a balanced result + * between the two depending on the scaling hint provided (see {@link Method}). + *

+ * This class also implements an optimized version of the incremental scaling + * algorithm presented by Chris Campbell in his Perils of + * Image.getScaledInstance() article in order to give the best-looking image + * resize results (e.g. generating thumbnails that aren't blurry or jagged). + *

+ * The results generated by imgscalr using this method, as compared to a single + * {@link RenderingHints#VALUE_INTERPOLATION_BICUBIC} scale operation look much + * better, especially when using the {@link Method#ULTRA_QUALITY} method. + *

+ * Only when scaling using the {@link Method#AUTOMATIC} method will this class + * look at the size of the image before selecting an approach to scaling the + * image. If {@link Method#QUALITY} is specified, the best-looking algorithm + * possible is always used. + *

+ * Minor modifications are made to Campbell's original implementation in the + * form of: + *

    + *
  1. Instead of accepting a user-supplied interpolation method, + * {@link RenderingHints#VALUE_INTERPOLATION_BICUBIC} interpolation is always + * used. This was done after A/B comparison testing with large images + * down-scaled to thumbnail sizes showed noticeable "blurring" when BILINEAR + * interpolation was used. Given that Campbell's algorithm is only used in + * QUALITY mode when down-scaling, it was determined that the user's expectation + * of a much less blurry picture would require that BICUBIC be the default + * interpolation in order to meet the QUALITY expectation.
  2. + *
  3. After each iteration of the do-while loop that incrementally scales the + * source image down, an explicit effort is made to call + * {@link BufferedImage#flush()} on the interim temporary {@link BufferedImage} + * instances created by the algorithm in an attempt to ensure a more complete GC + * cycle by the VM when cleaning up the temporary instances (this is in addition + * to disposing of the temporary {@link Graphics2D} references as well).
  4. + *
  5. Extensive comments have been added to increase readability of the code.
  6. + *
  7. Variable names have been expanded to increase readability of the code.
  8. + *
+ *

+ * NOTE: This class does not call {@link BufferedImage#flush()} + * on any of the source images passed in by calling code; it is up to + * the original caller to dispose of their source images when they are no longer + * needed so the VM can most efficiently GC them. + *

Image Proportions

+ * All scaling operations implemented by this class maintain the proportions of + * the original image unless a mode of {@link Mode#FIT_EXACT} is specified; in + * which case the orientation and proportion of the source image is ignored and + * the image is stretched (if necessary) to fit the exact dimensions given. + *

+ * When not using {@link Mode#FIT_EXACT}, in order to maintain the + * proportionality of the original images, this class implements the following + * behavior: + *

    + *
  1. If the image is LANDSCAPE-oriented or SQUARE, treat the + * targetWidth as the primary dimension and re-calculate the + * targetHeight regardless of what is passed in.
  2. + *
  3. If image is PORTRAIT-oriented, treat the targetHeight as the + * primary dimension and re-calculate the targetWidth regardless of + * what is passed in.
  4. + *
  5. If a {@link Mode} value of {@link Mode#FIT_TO_WIDTH} or + * {@link Mode#FIT_TO_HEIGHT} is passed in to the resize method, + * the image's orientation is ignored and the scaled image is fit to the + * preferred dimension by using the value passed in by the user for that + * dimension and recalculating the other (regardless of image orientation). This + * is useful, for example, when working with PORTRAIT oriented images that you + * need to all be the same width or visa-versa (e.g. showing user profile + * pictures in a directory listing).
  6. + *
+ *

Optimized Image Handling

+ * Java2D provides support for a number of different image types defined as + * BufferedImage.TYPE_* variables, unfortunately not all image + * types are supported equally in the Java2D rendering pipeline. + *

+ * Some more obscure image types either have poor or no support, leading to + * severely degraded quality and processing performance when an attempt is made + * by imgscalr to create a scaled instance of the same type as the + * source image. In many cases, especially when applying {@link BufferedImageOp} + * s, using poorly supported image types can even lead to exceptions or total + * corruption of the image (e.g. solid black image). + *

+ * imgscalr specifically accounts for and automatically hands + * ALL of these pain points for you internally by shuffling all + * images into one of two types: + *

    + *
  1. {@link BufferedImage#TYPE_INT_RGB}
  2. + *
  3. {@link BufferedImage#TYPE_INT_ARGB}
  4. + *
+ * depending on if the source image utilizes transparency or not. This is a + * recommended approach by the Java2D team for dealing with poorly (or non) + * supported image types. More can be read about this issue here. + *

+ * This is also the reason we recommend using + * {@link #apply(BufferedImage, BufferedImageOp...)} to apply your own ops to + * images even if you aren't using imgscalr for anything else. + *

GIF Transparency

+ * Unfortunately in Java 6 and earlier, support for GIF's + * {@link IndexColorModel} is sub-par, both in accurate color-selection and in + * maintaining transparency when moving to an image of type + * {@link BufferedImage#TYPE_INT_ARGB}; because of this issue when a GIF image + * is processed by imgscalr and the result saved as a GIF file (instead of PNG), + * it is possible to lose the alpha channel of a transparent image or in the + * case of applying an optional {@link BufferedImageOp}, lose the entire picture + * all together in the result (long standing JDK bugs are filed for all of these + * issues). + *

+ * imgscalr currently does nothing to work around this manually because it is a + * defect in the native platform code itself. Fortunately it looks like the + * issues are half-fixed in Java 7 and any manual workarounds we could attempt + * internally are relatively expensive, in the form of hand-creating and setting + * RGB values pixel-by-pixel with a custom {@link ColorModel} in the scaled + * image. This would lead to a very measurable negative impact on performance + * without the caller understanding why. + *

+ * Workaround: A workaround to this issue with all version of + * Java is to simply save a GIF as a PNG; no change to your code needs to be + * made except when the image is saved out, e.g. using {@link ImageIO}. + *

+ * When a file type of "PNG" is used, both the transparency and high color + * quality will be maintained as the PNG code path in Java2D is superior to the + * GIF implementation. + *

+ * If the issue with optional {@link BufferedImageOp}s destroying GIF image + * content is ever fixed in the platform, saving out resulting images as GIFs + * should suddenly start working. + *

+ * More can be read about the issue here and here. + *

Thread Safety

+ * The {@link Scalr} class is thread-safe (as all the methods + * are static); this class maintains no internal state while + * performing any of the provided operations and is safe to call simultaneously + * from multiple threads. + *

Logging

+ * This class implements all its debug logging via the + * {@link #log(int, String, Object...)} method. At this time logging is done + * directly to System.out via the printf method. This + * allows the logging to be light weight and easy to capture (every imgscalr log + * message is prefixed with the {@link #LOG_PREFIX} string) while adding no + * dependencies to the library. + *

+ * Implementation of logging in this class is as efficient as possible; avoiding + * any calls to the logger method or passing of arguments if logging is not + * enabled to avoid the (hidden) cost of constructing the Object[] argument for + * the varargs-based method call. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 1.1 + */ +public class Scalr { + /** + * System property name used to define the debug boolean flag. + *

+ * Value is "imgscalr.debug". + */ + public static final String DEBUG_PROPERTY_NAME = "imgscalr.debug"; + + /** + * System property name used to define a custom log prefix. + *

+ * Value is "imgscalr.logPrefix". + */ + public static final String LOG_PREFIX_PROPERTY_NAME = "imgscalr.logPrefix"; + + /** + * Flag used to indicate if debugging output has been enabled by setting the + * "imgscalr.debug" system property to true. This + * value will be false if the "imgscalr.debug" + * system property is undefined or set to false. + *

+ * This property can be set on startup with:
+ * + * -Dimgscalr.debug=true + * or by calling {@link System#setProperty(String, String)} to set a + * new property value for {@link #DEBUG_PROPERTY_NAME} before this class is + * loaded. + *

+ * Default value is false. + */ + public static final boolean DEBUG = Boolean.getBoolean(DEBUG_PROPERTY_NAME); + + /** + * Prefix to every log message this library logs. Using a well-defined + * prefix helps make it easier both visually and programmatically to scan + * log files for messages produced by this library. + *

+ * This property can be set on startup with:
+ * + * -Dimgscalr.logPrefix=<YOUR PREFIX HERE> + * or by calling {@link System#setProperty(String, String)} to set a + * new property value for {@link #LOG_PREFIX_PROPERTY_NAME} before this + * class is loaded. + *

+ * Default value is "[imgscalr] " (including the space). + */ + public static final String LOG_PREFIX = System.getProperty( + LOG_PREFIX_PROPERTY_NAME, "[imgscalr] "); + + /** + * A {@link ConvolveOp} using a very light "blur" kernel that acts like an + * anti-aliasing filter (softens the image a bit) when applied to an image. + *

+ * A common request by users of the library was that they wished to "soften" + * resulting images when scaling them down drastically. After quite a bit of + * A/B testing, the kernel used by this Op was selected as the closest match + * for the target which was the softer results from the deprecated + * {@link AreaAveragingScaleFilter} (which is used internally by the + * deprecated {@link Image#getScaledInstance(int, int, int)} method in the + * JDK that imgscalr is meant to replace). + *

+ * This ConvolveOp uses a 3x3 kernel with the values: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
.0f.08f.0f
.08f.68f.08f
.0f.08f.0f
+ *

+ * For those that have worked with ConvolveOps before, this Op uses the + * {@link ConvolveOp#EDGE_NO_OP} instruction to not process the pixels along + * the very edge of the image (otherwise EDGE_ZERO_FILL would create a + * black-border around the image). If you have not worked with a ConvolveOp + * before, it just means this default OP will "do the right thing" and not + * give you garbage results. + *

+ * This ConvolveOp uses no {@link RenderingHints} values as internally the + * {@link ConvolveOp} class only uses hints when doing a color conversion + * between the source and destination {@link BufferedImage} targets. + * imgscalr allows the {@link ConvolveOp} to create its own destination + * image every time, so no color conversion is ever needed and thus no + * hints. + *

Performance

+ * Use of this (and other) {@link ConvolveOp}s are hardware accelerated when + * possible. For more information on if your image op is hardware + * accelerated or not, check the source code of the underlying JDK class + * that actually executes the Op code, sun.awt.image.ImagingLib. + *

Known Issues

+ * In all versions of Java (tested up to Java 7 preview Build 131), running + * this op against a GIF with transparency and attempting to save the + * resulting image as a GIF results in a corrupted/empty file. The file must + * be saved out as a PNG to maintain the transparency. + * + * @since 3.0 + */ + public static final ConvolveOp OP_ANTIALIAS = new ConvolveOp( + new Kernel(3, 3, new float[] { .0f, .08f, .0f, .08f, .68f, .08f, + .0f, .08f, .0f }), ConvolveOp.EDGE_NO_OP, null); + + /** + * A {@link RescaleOp} used to make any input image 10% darker. + *

+ * This operation can be applied multiple times in a row if greater than 10% + * changes in brightness are desired. + * + * @since 4.0 + */ + public static final RescaleOp OP_DARKER = new RescaleOp(0.9f, 0, null); + + /** + * A {@link RescaleOp} used to make any input image 10% brighter. + *

+ * This operation can be applied multiple times in a row if greater than 10% + * changes in brightness are desired. + * + * @since 4.0 + */ + public static final RescaleOp OP_BRIGHTER = new RescaleOp(1.1f, 0, null); + + /** + * A {@link ColorConvertOp} used to convert any image to a grayscale color + * palette. + *

+ * Applying this op multiple times to the same image has no compounding + * effects. + * + * @since 4.0 + */ + public static final ColorConvertOp OP_GRAYSCALE = new ColorConvertOp( + ColorSpace.getInstance(ColorSpace.CS_GRAY), null); + + /** + * Static initializer used to prepare some of the variables used by this + * class. + */ + static { + log(0, "Debug output ENABLED"); + } + + /** + * Used to define the different scaling hints that the algorithm can use. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 1.1 + */ + public static enum Method { + /** + * Used to indicate that the scaling implementation should decide which + * method to use in order to get the best looking scaled image in the + * least amount of time. + *

+ * The scaling algorithm will use the + * {@link Scalr#THRESHOLD_QUALITY_BALANCED} or + * {@link Scalr#THRESHOLD_BALANCED_SPEED} thresholds as cut-offs to + * decide between selecting the QUALITY, + * BALANCED or SPEED scaling algorithms. + *

+ * By default the thresholds chosen will give nearly the best looking + * result in the fastest amount of time. We intend this method to work + * for 80% of people looking to scale an image quickly and get a good + * looking result. + */ + AUTOMATIC, + /** + * Used to indicate that the scaling implementation should scale as fast + * as possible and return a result. For smaller images (800px in size) + * this can result in noticeable aliasing but it can be a few magnitudes + * times faster than using the QUALITY method. + */ + SPEED, + /** + * Used to indicate that the scaling implementation should use a scaling + * operation balanced between SPEED and QUALITY. Sometimes SPEED looks + * too low quality to be useful (e.g. text can become unreadable when + * scaled using SPEED) but using QUALITY mode will increase the + * processing time too much. This mode provides a "better than SPEED" + * quality in a "less than QUALITY" amount of time. + */ + BALANCED, + /** + * Used to indicate that the scaling implementation should do everything + * it can to create as nice of a result as possible. This approach is + * most important for smaller pictures (800px or smaller) and less + * important for larger pictures as the difference between this method + * and the SPEED method become less and less noticeable as the + * source-image size increases. Using the AUTOMATIC method will + * automatically prefer the QUALITY method when scaling an image down + * below 800px in size. + */ + QUALITY, + /** + * Used to indicate that the scaling implementation should go above and + * beyond the work done by {@link Method#QUALITY} to make the image look + * exceptionally good at the cost of more processing time. This is + * especially evident when generating thumbnails of images that look + * jagged with some of the other {@link Method}s (even + * {@link Method#QUALITY}). + */ + ULTRA_QUALITY; + } + + /** + * Used to define the different modes of resizing that the algorithm can + * use. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 3.1 + */ + public static enum Mode { + /** + * Used to indicate that the scaling implementation should calculate + * dimensions for the resultant image by looking at the image's + * orientation and generating proportional dimensions that best fit into + * the target width and height given + * + * See "Image Proportions" in the {@link Scalr} class description for + * more detail. + */ + AUTOMATIC, + /** + * Used to fit the image to the exact dimensions given regardless of the + * image's proportions. If the dimensions are not proportionally + * correct, this will introduce vertical or horizontal stretching to the + * image. + *

+ * It is recommended that you use one of the other FIT_TO + * modes or {@link Mode#AUTOMATIC} if you want the image to look + * correct, but if dimension-fitting is the #1 priority regardless of + * how it makes the image look, that is what this mode is for. + */ + FIT_EXACT, + /** + * Used to indicate that the scaling implementation should calculate + * dimensions for the resultant image that best-fit within the given + * width, regardless of the orientation of the image. + */ + FIT_TO_WIDTH, + /** + * Used to indicate that the scaling implementation should calculate + * dimensions for the resultant image that best-fit within the given + * height, regardless of the orientation of the image. + */ + FIT_TO_HEIGHT; + } + + /** + * Used to define the different types of rotations that can be applied to an + * image during a resize operation. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 3.2 + */ + public static enum Rotation { + /** + * 90-degree, clockwise rotation (to the right). This is equivalent to a + * quarter-turn of the image to the right; moving the picture on to its + * right side. + */ + CW_90, + /** + * 180-degree, clockwise rotation (to the right). This is equivalent to + * 1 half-turn of the image to the right; rotating the picture around + * until it is upside down from the original position. + */ + CW_180, + /** + * 270-degree, clockwise rotation (to the right). This is equivalent to + * a quarter-turn of the image to the left; moving the picture on to its + * left side. + */ + CW_270, + /** + * Flip the image horizontally by reflecting it around the y axis. + *

+ * This is not a standard rotation around a center point, but instead + * creates the mirrored reflection of the image horizontally. + *

+ * More specifically, the vertical orientation of the image stays the + * same (the top stays on top, and the bottom on bottom), but the right + * and left sides flip. This is different than a standard rotation where + * the top and bottom would also have been flipped. + */ + FLIP_HORZ, + /** + * Flip the image vertically by reflecting it around the x axis. + *

+ * This is not a standard rotation around a center point, but instead + * creates the mirrored reflection of the image vertically. + *

+ * More specifically, the horizontal orientation of the image stays the + * same (the left stays on the left and the right stays on the right), + * but the top and bottom sides flip. This is different than a standard + * rotation where the left and right would also have been flipped. + */ + FLIP_VERT; + } + + /** + * Threshold (in pixels) at which point the scaling operation using the + * {@link Method#AUTOMATIC} method will decide if a {@link Method#BALANCED} + * method will be used (if smaller than or equal to threshold) or a + * {@link Method#SPEED} method will be used (if larger than threshold). + *

+ * The bigger the image is being scaled to, the less noticeable degradations + * in the image becomes and the faster algorithms can be selected. + *

+ * The value of this threshold (1600) was chosen after visual, by-hand, A/B + * testing between different types of images scaled with this library; both + * photographs and screenshots. It was determined that images below this + * size need to use a {@link Method#BALANCED} scale method to look decent in + * most all cases while using the faster {@link Method#SPEED} method for + * images bigger than this threshold showed no noticeable degradation over a + * BALANCED scale. + */ + public static final int THRESHOLD_BALANCED_SPEED = 1600; + + /** + * Threshold (in pixels) at which point the scaling operation using the + * {@link Method#AUTOMATIC} method will decide if a {@link Method#QUALITY} + * method will be used (if smaller than or equal to threshold) or a + * {@link Method#BALANCED} method will be used (if larger than threshold). + *

+ * The bigger the image is being scaled to, the less noticeable degradations + * in the image becomes and the faster algorithms can be selected. + *

+ * The value of this threshold (800) was chosen after visual, by-hand, A/B + * testing between different types of images scaled with this library; both + * photographs and screenshots. It was determined that images below this + * size need to use a {@link Method#QUALITY} scale method to look decent in + * most all cases while using the faster {@link Method#BALANCED} method for + * images bigger than this threshold showed no noticeable degradation over a + * QUALITY scale. + */ + public static final int THRESHOLD_QUALITY_BALANCED = 800; + + /** + * Used to apply, in the order given, 1 or more {@link BufferedImageOp}s to + * a given {@link BufferedImage} and return the result. + *

+ * Feature: This implementation works around a + * decade-old JDK bug that can cause a {@link RasterFormatException} + * when applying a perfectly valid {@link BufferedImageOp}s to images. + *

+ * Feature: This implementation also works around + * {@link BufferedImageOp}s failing to apply and throwing + * {@link ImagingOpException}s when run against a src image + * type that is poorly supported. Unfortunately using {@link ImageIO} and + * standard Java methods to load images provides no consistency in getting + * images in well-supported formats. This method automatically accounts and + * corrects for all those problems (if necessary). + *

+ * It is recommended you always use this method to apply any + * {@link BufferedImageOp}s instead of relying on directly using the + * {@link BufferedImageOp#filter(BufferedImage, BufferedImage)} method. + *

+ * Performance: Not all {@link BufferedImageOp}s are + * hardware accelerated operations, but many of the most popular (like + * {@link ConvolveOp}) are. For more information on if your image op is + * hardware accelerated or not, check the source code of the underlying JDK + * class that actually executes the Op code, sun.awt.image.ImagingLib. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will have the ops applied to it. + * @param ops + * 1 or more ops to apply to the image. + * + * @return a new {@link BufferedImage} that represents the src + * with all the given operations applied to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if ops is null or empty. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage apply(BufferedImage src, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + long t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (ops == null || ops.length == 0) + throw new IllegalArgumentException("ops cannot be null or empty"); + + int type = src.getType(); + + /* + * Ensure the src image is in the best supported image type before we + * continue, otherwise it is possible our calls below to getBounds2D and + * certainly filter(...) may fail if not. + * + * Java2D makes an attempt at applying most BufferedImageOps using + * hardware acceleration via the ImagingLib internal library. + * + * Unfortunately may of the BufferedImageOp are written to simply fail + * with an ImagingOpException if the operation cannot be applied with no + * additional information about what went wrong or attempts at + * re-applying it in different ways. + * + * This is assuming the failing BufferedImageOp even returns a null + * image after failing to apply; some simply return a corrupted/black + * image that result in no exception and it is up to the user to + * discover this. + * + * In internal testing, EVERY failure I've ever seen was the result of + * the source image being in a poorly-supported BufferedImage Type like + * BGR or ABGR (even though it was loaded with ImageIO). + * + * To avoid this nasty/stupid surprise with BufferedImageOps, we always + * ensure that the src image starts in an optimally supported format + * before we try and apply the filter. + */ + if (!(type == BufferedImage.TYPE_INT_RGB || type == BufferedImage.TYPE_INT_ARGB)) + src = copyToOptimalImage(src); + + if (DEBUG) + log(0, "Applying %d BufferedImageOps...", ops.length); + + boolean hasReassignedSrc = false; + + for (int i = 0; i < ops.length; i++) { + long subT = System.currentTimeMillis(); + BufferedImageOp op = ops[i]; + + // Skip null ops instead of throwing an exception. + if (op == null) + continue; + + if (DEBUG) + log(1, "Applying BufferedImageOp [class=%s, toString=%s]...", + op.getClass(), op.toString()); + + /* + * Must use op.getBounds instead of src.getWidth and src.getHeight + * because we are trying to create an image big enough to hold the + * result of this operation (which may be to scale the image + * smaller), in that case the bounds reported by this op and the + * bounds reported by the source image will be different. + */ + Rectangle2D resultBounds = op.getBounds2D(src); + + // Watch out for flaky/misbehaving ops that fail to work right. + if (resultBounds == null) + throw new ImagingOpException( + "BufferedImageOp [" + + op.toString() + + "] getBounds2D(src) returned null bounds for the target image; this should not happen and indicates a problem with application of this type of op."); + + /* + * We must manually create the target image; we cannot rely on the + * null-destination filter() method to create a valid destination + * for us thanks to this JDK bug that has been filed for almost a + * decade: + * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4965606 + */ + BufferedImage dest = createOptimalImage(src, + (int) Math.round(resultBounds.getWidth()), + (int) Math.round(resultBounds.getHeight())); + + // Perform the operation, update our result to return. + BufferedImage result = op.filter(src, dest); + + /* + * Flush the 'src' image ONLY IF it is one of our interim temporary + * images being used when applying 2 or more operations back to + * back. We never want to flush the original image passed in. + */ + if (hasReassignedSrc) + src.flush(); + + /* + * Incase there are more operations to perform, update what we + * consider the 'src' reference to our last result so on the next + * iteration the next op is applied to this result and not back + * against the original src passed in. + */ + src = result; + + /* + * Keep track of when we re-assign 'src' to an interim temporary + * image, so we know when we can explicitly flush it and clean up + * references on future iterations. + */ + hasReassignedSrc = true; + + if (DEBUG) + log(1, + "Applied BufferedImageOp in %d ms, result [width=%d, height=%d]", + System.currentTimeMillis() - subT, result.getWidth(), + result.getHeight()); + } + + if (DEBUG) + log(0, "All %d BufferedImageOps applied in %d ms", ops.length, + System.currentTimeMillis() - t); + + return src; + } + + /** + * Used to crop the given src image from the top-left corner + * and applying any optional {@link BufferedImageOp}s to the result before + * returning it. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image to crop. + * @param width + * The width of the bounding cropping box. + * @param height + * The height of the bounding cropping box. + * @param ops + * 0 or more ops to apply to the image. If + * null or empty then src is return + * unmodified. + * + * @return a new {@link BufferedImage} representing the cropped region of + * the src image with any optional operations applied + * to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if any coordinates of the bounding crop box is invalid within + * the bounds of the src image (e.g. negative or + * too big). + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage crop(BufferedImage src, int width, int height, + BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + return crop(src, 0, 0, width, height, ops); + } + + /** + * Used to crop the given src image and apply any optional + * {@link BufferedImageOp}s to it before returning the result. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image to crop. + * @param x + * The x-coordinate of the top-left corner of the bounding box + * used for cropping. + * @param y + * The y-coordinate of the top-left corner of the bounding box + * used for cropping. + * @param width + * The width of the bounding cropping box. + * @param height + * The height of the bounding cropping box. + * @param ops + * 0 or more ops to apply to the image. If + * null or empty then src is return + * unmodified. + * + * @return a new {@link BufferedImage} representing the cropped region of + * the src image with any optional operations applied + * to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if any coordinates of the bounding crop box is invalid within + * the bounds of the src image (e.g. negative or + * too big). + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage crop(BufferedImage src, int x, int y, + int width, int height, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + long t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (x < 0 || y < 0 || width < 0 || height < 0) + throw new IllegalArgumentException("Invalid crop bounds: x [" + x + + "], y [" + y + "], width [" + width + "] and height [" + + height + "] must all be >= 0"); + + int srcWidth = src.getWidth(); + int srcHeight = src.getHeight(); + + if ((x + width) > srcWidth) + throw new IllegalArgumentException( + "Invalid crop bounds: x + width [" + (x + width) + + "] must be <= src.getWidth() [" + srcWidth + "]"); + if ((y + height) > srcHeight) + throw new IllegalArgumentException( + "Invalid crop bounds: y + height [" + (y + height) + + "] must be <= src.getHeight() [" + srcHeight + + "]"); + + if (DEBUG) + log(0, + "Cropping Image [width=%d, height=%d] to [x=%d, y=%d, width=%d, height=%d]...", + srcWidth, srcHeight, x, y, width, height); + + // Create a target image of an optimal type to render into. + BufferedImage result = createOptimalImage(src, width, height); + Graphics g = result.getGraphics(); + + /* + * Render the region specified by our crop bounds from the src image + * directly into our result image (which is the exact size of the crop + * region). + */ + g.drawImage(src, 0, 0, width, height, x, y, (x + width), (y + height), + null); + g.dispose(); + + if (DEBUG) + log(0, "Cropped Image in %d ms", System.currentTimeMillis() - t); + + // Apply any optional operations (if specified). + if (ops != null && ops.length > 0) + result = apply(result, ops); + + return result; + } + + /** + * Used to apply padding around the edges of an image using + * {@link Color#BLACK} to fill the extra padded space and then return the + * result. + *

+ * The amount of padding specified is applied to all sides; + * more specifically, a padding of 2 would add 2 + * extra pixels of space (filled by the given color) on the + * top, bottom, left and right sides of the resulting image causing the + * result to be 4 pixels wider and 4 pixels taller than the src + * image. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image the padding will be added to. + * @param padding + * The number of pixels of padding to add to each side in the + * resulting image. If this value is 0 then + * src is returned unmodified. + * @param ops + * 0 or more ops to apply to the image. If + * null or empty then src is return + * unmodified. + * + * @return a new {@link BufferedImage} representing src with + * the given padding applied to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if padding is < 1. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage pad(BufferedImage src, int padding, + BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + return pad(src, padding, Color.BLACK); + } + + /** + * Used to apply padding around the edges of an image using the given color + * to fill the extra padded space and then return the result. {@link Color}s + * using an alpha channel (i.e. transparency) are supported. + *

+ * The amount of padding specified is applied to all sides; + * more specifically, a padding of 2 would add 2 + * extra pixels of space (filled by the given color) on the + * top, bottom, left and right sides of the resulting image causing the + * result to be 4 pixels wider and 4 pixels taller than the src + * image. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image the padding will be added to. + * @param padding + * The number of pixels of padding to add to each side in the + * resulting image. If this value is 0 then + * src is returned unmodified. + * @param color + * The color to fill the padded space with. {@link Color}s using + * an alpha channel (i.e. transparency) are supported. + * @param ops + * 0 or more ops to apply to the image. If + * null or empty then src is return + * unmodified. + * + * @return a new {@link BufferedImage} representing src with + * the given padding applied to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if padding is < 1. + * @throws IllegalArgumentException + * if color is null. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage pad(BufferedImage src, int padding, + Color color, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + long t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (padding < 1) + throw new IllegalArgumentException("padding [" + padding + + "] must be > 0"); + if (color == null) + throw new IllegalArgumentException("color cannot be null"); + + int srcWidth = src.getWidth(); + int srcHeight = src.getHeight(); + + /* + * Double the padding to account for all sides of the image. More + * specifically, if padding is "1" we add 2 pixels to width and 2 to + * height, so we have 1 new pixel of padding all the way around our + * image. + */ + int sizeDiff = (padding * 2); + int newWidth = srcWidth + sizeDiff; + int newHeight = srcHeight + sizeDiff; + + if (DEBUG) + log(0, + "Padding Image from [originalWidth=%d, originalHeight=%d, padding=%d] to [newWidth=%d, newHeight=%d]...", + srcWidth, srcHeight, padding, newWidth, newHeight); + + boolean colorHasAlpha = (color.getAlpha() != 255); + boolean imageHasAlpha = (src.getTransparency() != BufferedImage.OPAQUE); + + BufferedImage result; + + /* + * We need to make sure our resulting image that we render into contains + * alpha if either our original image OR the padding color we are using + * contain it. + */ + if (colorHasAlpha || imageHasAlpha) { + if (DEBUG) + log(1, + "Transparency FOUND in source image or color, using ARGB image type..."); + + result = new BufferedImage(newWidth, newHeight, + BufferedImage.TYPE_INT_ARGB); + } else { + if (DEBUG) + log(1, + "Transparency NOT FOUND in source image or color, using RGB image type..."); + + result = new BufferedImage(newWidth, newHeight, + BufferedImage.TYPE_INT_RGB); + } + + Graphics g = result.getGraphics(); + + // "Clear" the background of the new image with our padding color first. + g.setColor(color); + g.fillRect(0, 0, newWidth, newHeight); + + // Draw the image into the center of the new padded image. + g.drawImage(src, padding, padding, null); + g.dispose(); + + if (DEBUG) + log(0, "Padding Applied in %d ms", System.currentTimeMillis() - t); + + // Apply any optional operations (if specified). + if (ops != null && ops.length > 0) + result = apply(result, ops); + + return result; + } + + /** + * Resize a given image (maintaining its original proportion) to a width and + * height no bigger than targetSize and apply the given + * {@link BufferedImageOp}s (if any) to the result before returning it. + *

+ * A scaling method of {@link Method#AUTOMATIC} and mode of + * {@link Mode#AUTOMATIC} are used. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param targetSize + * The target width and height (square) that you wish the image + * to fit within. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if targetSize is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage resize(BufferedImage src, int targetSize, + BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + return resize(src, Method.AUTOMATIC, Mode.AUTOMATIC, targetSize, + targetSize, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to a width and + * height no bigger than targetSize using the given scaling + * method and apply the given {@link BufferedImageOp}s (if any) to the + * result before returning it. + *

+ * A mode of {@link Mode#AUTOMATIC} is used. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param scalingMethod + * The method used for scaling the image; preferring speed to + * quality or a balance of both. + * @param targetSize + * The target width and height (square) that you wish the image + * to fit within. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if scalingMethod is null. + * @throws IllegalArgumentException + * if targetSize is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Method + */ + public static BufferedImage resize(BufferedImage src, Method scalingMethod, + int targetSize, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return resize(src, scalingMethod, Mode.AUTOMATIC, targetSize, + targetSize, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to a width and + * height no bigger than targetSize (or fitting the image to + * the given WIDTH or HEIGHT explicitly, depending on the {@link Mode} + * specified) and apply the given {@link BufferedImageOp}s (if any) to the + * result before returning it. + *

+ * A scaling method of {@link Method#AUTOMATIC} is used. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param resizeMode + * Used to indicate how imgscalr should calculate the final + * target size for the image, either fitting the image to the + * given width ({@link Mode#FIT_TO_WIDTH}) or fitting the image + * to the given height ({@link Mode#FIT_TO_HEIGHT}). If + * {@link Mode#AUTOMATIC} is passed in, imgscalr will calculate + * proportional dimensions for the scaled image based on its + * orientation (landscape, square or portrait). Unless you have + * very specific size requirements, most of the time you just + * want to use {@link Mode#AUTOMATIC} to "do the right thing". + * @param targetSize + * The target width and height (square) that you wish the image + * to fit within. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if resizeMode is null. + * @throws IllegalArgumentException + * if targetSize is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Mode + */ + public static BufferedImage resize(BufferedImage src, Mode resizeMode, + int targetSize, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return resize(src, Method.AUTOMATIC, resizeMode, targetSize, + targetSize, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to a width and + * height no bigger than targetSize (or fitting the image to + * the given WIDTH or HEIGHT explicitly, depending on the {@link Mode} + * specified) using the given scaling method and apply the given + * {@link BufferedImageOp}s (if any) to the result before returning it. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param scalingMethod + * The method used for scaling the image; preferring speed to + * quality or a balance of both. + * @param resizeMode + * Used to indicate how imgscalr should calculate the final + * target size for the image, either fitting the image to the + * given width ({@link Mode#FIT_TO_WIDTH}) or fitting the image + * to the given height ({@link Mode#FIT_TO_HEIGHT}). If + * {@link Mode#AUTOMATIC} is passed in, imgscalr will calculate + * proportional dimensions for the scaled image based on its + * orientation (landscape, square or portrait). Unless you have + * very specific size requirements, most of the time you just + * want to use {@link Mode#AUTOMATIC} to "do the right thing". + * @param targetSize + * The target width and height (square) that you wish the image + * to fit within. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if scalingMethod is null. + * @throws IllegalArgumentException + * if resizeMode is null. + * @throws IllegalArgumentException + * if targetSize is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Method + * @see Mode + */ + public static BufferedImage resize(BufferedImage src, Method scalingMethod, + Mode resizeMode, int targetSize, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return resize(src, scalingMethod, resizeMode, targetSize, targetSize, + ops); + } + + /** + * Resize a given image (maintaining its original proportion) to the target + * width and height and apply the given {@link BufferedImageOp}s (if any) to + * the result before returning it. + *

+ * A scaling method of {@link Method#AUTOMATIC} and mode of + * {@link Mode#AUTOMATIC} are used. + *

+ * TIP: See the class description to understand how this + * class handles recalculation of the targetWidth or + * targetHeight depending on the image's orientation in order + * to maintain the original proportion. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param targetWidth + * The target width that you wish the image to have. + * @param targetHeight + * The target height that you wish the image to have. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if targetWidth is < 0 or if + * targetHeight is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage resize(BufferedImage src, int targetWidth, + int targetHeight, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return resize(src, Method.AUTOMATIC, Mode.AUTOMATIC, targetWidth, + targetHeight, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to the target + * width and height using the given scaling method and apply the given + * {@link BufferedImageOp}s (if any) to the result before returning it. + *

+ * A mode of {@link Mode#AUTOMATIC} is used. + *

+ * TIP: See the class description to understand how this + * class handles recalculation of the targetWidth or + * targetHeight depending on the image's orientation in order + * to maintain the original proportion. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param scalingMethod + * The method used for scaling the image; preferring speed to + * quality or a balance of both. + * @param targetWidth + * The target width that you wish the image to have. + * @param targetHeight + * The target height that you wish the image to have. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if scalingMethod is null. + * @throws IllegalArgumentException + * if targetWidth is < 0 or if + * targetHeight is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Method + */ + public static BufferedImage resize(BufferedImage src, Method scalingMethod, + int targetWidth, int targetHeight, BufferedImageOp... ops) { + return resize(src, scalingMethod, Mode.AUTOMATIC, targetWidth, + targetHeight, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to the target + * width and height (or fitting the image to the given WIDTH or HEIGHT + * explicitly, depending on the {@link Mode} specified) and apply the given + * {@link BufferedImageOp}s (if any) to the result before returning it. + *

+ * A scaling method of {@link Method#AUTOMATIC} is used. + *

+ * TIP: See the class description to understand how this + * class handles recalculation of the targetWidth or + * targetHeight depending on the image's orientation in order + * to maintain the original proportion. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param resizeMode + * Used to indicate how imgscalr should calculate the final + * target size for the image, either fitting the image to the + * given width ({@link Mode#FIT_TO_WIDTH}) or fitting the image + * to the given height ({@link Mode#FIT_TO_HEIGHT}). If + * {@link Mode#AUTOMATIC} is passed in, imgscalr will calculate + * proportional dimensions for the scaled image based on its + * orientation (landscape, square or portrait). Unless you have + * very specific size requirements, most of the time you just + * want to use {@link Mode#AUTOMATIC} to "do the right thing". + * @param targetWidth + * The target width that you wish the image to have. + * @param targetHeight + * The target height that you wish the image to have. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if resizeMode is null. + * @throws IllegalArgumentException + * if targetWidth is < 0 or if + * targetHeight is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Mode + */ + public static BufferedImage resize(BufferedImage src, Mode resizeMode, + int targetWidth, int targetHeight, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return resize(src, Method.AUTOMATIC, resizeMode, targetWidth, + targetHeight, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to the target + * width and height (or fitting the image to the given WIDTH or HEIGHT + * explicitly, depending on the {@link Mode} specified) using the given + * scaling method and apply the given {@link BufferedImageOp}s (if any) to + * the result before returning it. + *

+ * TIP: See the class description to understand how this + * class handles recalculation of the targetWidth or + * targetHeight depending on the image's orientation in order + * to maintain the original proportion. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param scalingMethod + * The method used for scaling the image; preferring speed to + * quality or a balance of both. + * @param resizeMode + * Used to indicate how imgscalr should calculate the final + * target size for the image, either fitting the image to the + * given width ({@link Mode#FIT_TO_WIDTH}) or fitting the image + * to the given height ({@link Mode#FIT_TO_HEIGHT}). If + * {@link Mode#AUTOMATIC} is passed in, imgscalr will calculate + * proportional dimensions for the scaled image based on its + * orientation (landscape, square or portrait). Unless you have + * very specific size requirements, most of the time you just + * want to use {@link Mode#AUTOMATIC} to "do the right thing". + * @param targetWidth + * The target width that you wish the image to have. + * @param targetHeight + * The target height that you wish the image to have. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if scalingMethod is null. + * @throws IllegalArgumentException + * if resizeMode is null. + * @throws IllegalArgumentException + * if targetWidth is < 0 or if + * targetHeight is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Method + * @see Mode + */ + public static BufferedImage resize(BufferedImage src, Method scalingMethod, + Mode resizeMode, int targetWidth, int targetHeight, + BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + long t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (targetWidth < 0) + throw new IllegalArgumentException("targetWidth must be >= 0"); + if (targetHeight < 0) + throw new IllegalArgumentException("targetHeight must be >= 0"); + if (scalingMethod == null) + throw new IllegalArgumentException( + "scalingMethod cannot be null. A good default value is Method.AUTOMATIC."); + if (resizeMode == null) + throw new IllegalArgumentException( + "resizeMode cannot be null. A good default value is Mode.AUTOMATIC."); + + BufferedImage result = null; + + int currentWidth = src.getWidth(); + int currentHeight = src.getHeight(); + + // <= 1 is a square or landscape-oriented image, > 1 is a portrait. + float ratio = ((float) currentHeight / (float) currentWidth); + + if (DEBUG) + log(0, + "Resizing Image [size=%dx%d, resizeMode=%s, orientation=%s, ratio(H/W)=%f] to [targetSize=%dx%d]", + currentWidth, currentHeight, resizeMode, + (ratio <= 1 ? "Landscape/Square" : "Portrait"), ratio, + targetWidth, targetHeight); + + /* + * First determine if ANY size calculation needs to be done, in the case + * of FIT_EXACT, ignore image proportions and orientation and just use + * what the user sent in, otherwise the proportion of the picture must + * be honored. + * + * The way that is done is to figure out if the image is in a + * LANDSCAPE/SQUARE or PORTRAIT orientation and depending on its + * orientation, use the primary dimension (width for LANDSCAPE/SQUARE + * and height for PORTRAIT) to recalculate the alternative (height and + * width respectively) value that adheres to the existing ratio. + * + * This helps make life easier for the caller as they don't need to + * pre-compute proportional dimensions before calling the API, they can + * just specify the dimensions they would like the image to roughly fit + * within and it will do the right thing without mangling the result. + */ + if (resizeMode != Mode.FIT_EXACT) { + if ((ratio <= 1 && resizeMode == Mode.AUTOMATIC) + || (resizeMode == Mode.FIT_TO_WIDTH)) { + // First make sure we need to do any work in the first place + if (targetWidth == src.getWidth()) + return src; + + // Save for detailed logging (this is cheap). + int originalTargetHeight = targetHeight; + + /* + * Landscape or Square Orientation: Ignore the given height and + * re-calculate a proportionally correct value based on the + * targetWidth. + */ + targetHeight = Math.round((float) targetWidth * ratio); + + if (DEBUG && originalTargetHeight != targetHeight) + log(1, + "Auto-Corrected targetHeight [from=%d to=%d] to honor image proportions.", + originalTargetHeight, targetHeight); + } else { + // First make sure we need to do any work in the first place + if (targetHeight == src.getHeight()) + return src; + + // Save for detailed logging (this is cheap). + int originalTargetWidth = targetWidth; + + /* + * Portrait Orientation: Ignore the given width and re-calculate + * a proportionally correct value based on the targetHeight. + */ + targetWidth = Math.round((float) targetHeight / ratio); + + if (DEBUG && originalTargetWidth != targetWidth) + log(1, + "Auto-Corrected targetWidth [from=%d to=%d] to honor image proportions.", + originalTargetWidth, targetWidth); + } + } else { + if (DEBUG) + log(1, + "Resize Mode FIT_EXACT used, no width/height checking or re-calculation will be done."); + } + + // If AUTOMATIC was specified, determine the real scaling method. + if (scalingMethod == Scalr.Method.AUTOMATIC) + scalingMethod = determineScalingMethod(targetWidth, targetHeight, + ratio); + + if (DEBUG) + log(1, "Using Scaling Method: %s", scalingMethod); + + // Now we scale the image + if (scalingMethod == Scalr.Method.SPEED) { + result = scaleImage(src, targetWidth, targetHeight, + RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + } else if (scalingMethod == Scalr.Method.BALANCED) { + result = scaleImage(src, targetWidth, targetHeight, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + } else if (scalingMethod == Scalr.Method.QUALITY + || scalingMethod == Scalr.Method.ULTRA_QUALITY) { + /* + * If we are scaling up (in either width or height - since we know + * the image will stay proportional we just check if either are + * being scaled up), directly using a single BICUBIC will give us + * better results then using Chris Campbell's incremental scaling + * operation (and take a lot less time). + * + * If we are scaling down, we must use the incremental scaling + * algorithm for the best result. + */ + if (targetWidth > currentWidth || targetHeight > currentHeight) { + if (DEBUG) + log(1, + "QUALITY scale-up, a single BICUBIC scale operation will be used..."); + + /* + * BILINEAR and BICUBIC look similar the smaller the scale jump + * upwards is, if the scale is larger BICUBIC looks sharper and + * less fuzzy. But most importantly we have to use BICUBIC to + * match the contract of the QUALITY rendering scalingMethod. + * This note is just here for anyone reading the code and + * wondering how they can speed their own calls up. + */ + result = scaleImage(src, targetWidth, targetHeight, + RenderingHints.VALUE_INTERPOLATION_BICUBIC); + } else { + if (DEBUG) + log(1, + "QUALITY scale-down, incremental scaling will be used..."); + + /* + * Originally we wanted to use BILINEAR interpolation here + * because it takes 1/3rd the time that the BICUBIC + * interpolation does, however, when scaling large images down + * to most sizes bigger than a thumbnail we witnessed noticeable + * "softening" in the resultant image with BILINEAR that would + * be unexpectedly annoying to a user expecting a "QUALITY" + * scale of their original image. Instead BICUBIC was chosen to + * honor the contract of a QUALITY scale of the original image. + */ + result = scaleImageIncrementally(src, targetWidth, + targetHeight, scalingMethod, + RenderingHints.VALUE_INTERPOLATION_BICUBIC); + } + } + + if (DEBUG) + log(0, "Resized Image in %d ms", System.currentTimeMillis() - t); + + // Apply any optional operations (if specified). + if (ops != null && ops.length > 0) + result = apply(result, ops); + + return result; + } + + /** + * Used to apply a {@link Rotation} and then 0 or more + * {@link BufferedImageOp}s to a given image and return the result. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will have the rotation applied to it. + * @param rotation + * The rotation that will be applied to the image. + * @param ops + * Zero or more optional image operations (e.g. sharpen, blur, + * etc.) that can be applied to the final result before returning + * the image. + * + * @return a new {@link BufferedImage} representing src rotated + * by the given amount and any optional ops applied to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if rotation is null. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Rotation + */ + public static BufferedImage rotate(BufferedImage src, Rotation rotation, + BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + long t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (rotation == null) + throw new IllegalArgumentException("rotation cannot be null"); + + if (DEBUG) + log(0, "Rotating Image [%s]...", rotation); + + /* + * Setup the default width/height values from our image. + * + * In the case of a 90 or 270 (-90) degree rotation, these two values + * flip-flop and we will correct those cases down below in the switch + * statement. + */ + int newWidth = src.getWidth(); + int newHeight = src.getHeight(); + + /* + * We create a transform per operation request as (oddly enough) it ends + * up being faster for the VM to create, use and destroy these instances + * than it is to re-use a single AffineTransform per-thread via the + * AffineTransform.setTo(...) methods which was my first choice (less + * object creation); after benchmarking this explicit case and looking + * at just how much code gets run inside of setTo() I opted for a new AT + * for every rotation. + * + * Besides the performance win, trying to safely reuse AffineTransforms + * via setTo(...) would have required ThreadLocal instances to avoid + * race conditions where two or more resize threads are manipulating the + * same transform before applying it. + * + * Misusing ThreadLocals are one of the #1 reasons for memory leaks in + * server applications and since we have no nice way to hook into the + * init/destroy Servlet cycle or any other initialization cycle for this + * library to automatically call ThreadLocal.remove() to avoid the + * memory leak, it would have made using this library *safely* on the + * server side much harder. + * + * So we opt for creating individual transforms per rotation op and let + * the VM clean them up in a GC. I only clarify all this reasoning here + * for anyone else reading this code and being tempted to reuse the AT + * instances of performance gains; there aren't any AND you get a lot of + * pain along with it. + */ + AffineTransform tx = new AffineTransform(); + + switch (rotation) { + case CW_90: + /* + * A 90 or -90 degree rotation will cause the height and width to + * flip-flop from the original image to the rotated one. + */ + newWidth = src.getHeight(); + newHeight = src.getWidth(); + + // Reminder: newWidth == result.getHeight() at this point + tx.translate(newWidth, 0); + tx.rotate(Math.toRadians(90)); + + break; + + case CW_270: + /* + * A 90 or -90 degree rotation will cause the height and width to + * flip-flop from the original image to the rotated one. + */ + newWidth = src.getHeight(); + newHeight = src.getWidth(); + + // Reminder: newHeight == result.getWidth() at this point + tx.translate(0, newHeight); + tx.rotate(Math.toRadians(-90)); + break; + + case CW_180: + tx.translate(newWidth, newHeight); + tx.rotate(Math.toRadians(180)); + break; + + case FLIP_HORZ: + tx.translate(newWidth, 0); + tx.scale(-1.0, 1.0); + break; + + case FLIP_VERT: + tx.translate(0, newHeight); + tx.scale(1.0, -1.0); + break; + } + + // Create our target image we will render the rotated result to. + BufferedImage result = createOptimalImage(src, newWidth, newHeight); + Graphics2D g2d = (Graphics2D) result.createGraphics(); + + /* + * Render the resultant image to our new rotatedImage buffer, applying + * the AffineTransform that we calculated above during rendering so the + * pixels from the old position are transposed to the new positions in + * the resulting image correctly. + */ + g2d.drawImage(src, tx, null); + g2d.dispose(); + + if (DEBUG) + log(0, "Rotation Applied in %d ms, result [width=%d, height=%d]", + System.currentTimeMillis() - t, result.getWidth(), + result.getHeight()); + + // Apply any optional operations (if specified). + if (ops != null && ops.length > 0) + result = apply(result, ops); + + return result; + } + + /** + * Used to write out a useful and well-formatted log message by any piece of + * code inside of the imgscalr library. + *

+ * If a message cannot be logged (logging is disabled) then this method + * returns immediately. + *

+ * NOTE: Because Java will auto-box primitive arguments + * into Objects when building out the params array, care should + * be taken not to call this method with primitive values unless + * {@link Scalr#DEBUG} is true; otherwise the VM will be + * spending time performing unnecessary auto-boxing calculations. + * + * @param depth + * The indentation level of the log message. + * @param message + * The log message in format string syntax that will be logged. + * @param params + * The parameters that will be swapped into all the place holders + * in the original messages before being logged. + * + * @see Scalr#LOG_PREFIX + * @see Scalr#LOG_PREFIX_PROPERTY_NAME + */ + protected static void log(int depth, String message, Object... params) { + if (Scalr.DEBUG) { + System.out.print(Scalr.LOG_PREFIX); + + for (int i = 0; i < depth; i++) + System.out.print("\t"); + + System.out.printf(message, params); + System.out.println(); + } + } + + /** + * Used to create a {@link BufferedImage} with the most optimal RGB TYPE ( + * {@link BufferedImage#TYPE_INT_RGB} or {@link BufferedImage#TYPE_INT_ARGB} + * ) capable of being rendered into from the given src. The + * width and height of both images will be identical. + *

+ * This does not perform a copy of the image data from src into + * the result image; see {@link #copyToOptimalImage(BufferedImage)} for + * that. + *

+ * We force all rendering results into one of these two types, avoiding the + * case where a source image is of an unsupported (or poorly supported) + * format by Java2D causing the rendering result to end up looking terrible + * (common with GIFs) or be totally corrupt (e.g. solid black image). + *

+ * Originally reported by Magnus Kvalheim from Movellas when scaling certain + * GIF and PNG images. + * + * @param src + * The source image that will be analyzed to determine the most + * optimal image type it can be rendered into. + * + * @return a new {@link BufferedImage} representing the most optimal target + * image type that src can be rendered into. + * + * @see How + * Java2D handles poorly supported image types + * @see Thanks + * to Morten Nobel for implementation hint + */ + protected static BufferedImage createOptimalImage(BufferedImage src) { + return createOptimalImage(src, src.getWidth(), src.getHeight()); + } + + /** + * Used to create a {@link BufferedImage} with the given dimensions and the + * most optimal RGB TYPE ( {@link BufferedImage#TYPE_INT_RGB} or + * {@link BufferedImage#TYPE_INT_ARGB} ) capable of being rendered into from + * the given src. + *

+ * This does not perform a copy of the image data from src into + * the result image; see {@link #copyToOptimalImage(BufferedImage)} for + * that. + *

+ * We force all rendering results into one of these two types, avoiding the + * case where a source image is of an unsupported (or poorly supported) + * format by Java2D causing the rendering result to end up looking terrible + * (common with GIFs) or be totally corrupt (e.g. solid black image). + *

+ * Originally reported by Magnus Kvalheim from Movellas when scaling certain + * GIF and PNG images. + * + * @param src + * The source image that will be analyzed to determine the most + * optimal image type it can be rendered into. + * @param width + * The width of the newly created resulting image. + * @param height + * The height of the newly created resulting image. + * + * @return a new {@link BufferedImage} representing the most optimal target + * image type that src can be rendered into. + * + * @throws IllegalArgumentException + * if width or height are < 0. + * + * @see How + * Java2D handles poorly supported image types + * @see Thanks + * to Morten Nobel for implementation hint + */ + protected static BufferedImage createOptimalImage(BufferedImage src, + int width, int height) throws IllegalArgumentException { + if (width < 0 || height < 0) + throw new IllegalArgumentException("width [" + width + + "] and height [" + height + "] must be >= 0"); + + return new BufferedImage( + width, + height, + (src.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB + : BufferedImage.TYPE_INT_ARGB)); + } + + /** + * Used to copy a {@link BufferedImage} from a non-optimal type into a new + * {@link BufferedImage} instance of an optimal type (RGB or ARGB). If + * src is already of an optimal type, then it is returned + * unmodified. + *

+ * This method is meant to be used by any calling code (imgscalr's or + * otherwise) to convert any inbound image from a poorly supported image + * type into the 2 most well-supported image types in Java2D ( + * {@link BufferedImage#TYPE_INT_RGB} or {@link BufferedImage#TYPE_INT_ARGB} + * ) in order to ensure all subsequent graphics operations are performed as + * efficiently and correctly as possible. + *

+ * When using Java2D to work with image types that are not well supported, + * the results can be anything from exceptions bubbling up from the depths + * of Java2D to images being completely corrupted and just returned as solid + * black. + * + * @param src + * The image to copy (if necessary) into an optimally typed + * {@link BufferedImage}. + * + * @return a representation of the src image in an optimally + * typed {@link BufferedImage}, otherwise src if it was + * already of an optimal type. + * + * @throws IllegalArgumentException + * if src is null. + */ + protected static BufferedImage copyToOptimalImage(BufferedImage src) + throws IllegalArgumentException { + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + + // Calculate the type depending on the presence of alpha. + int type = (src.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB + : BufferedImage.TYPE_INT_ARGB); + BufferedImage result = new BufferedImage(src.getWidth(), + src.getHeight(), type); + + // Render the src image into our new optimal source. + Graphics g = result.getGraphics(); + g.drawImage(src, 0, 0, null); + g.dispose(); + + return result; + } + + /** + * Used to determine the scaling {@link Method} that is best suited for + * scaling the image to the targeted dimensions. + *

+ * This method is intended to be used to select a specific scaling + * {@link Method} when a {@link Method#AUTOMATIC} method is specified. This + * method utilizes the {@link Scalr#THRESHOLD_QUALITY_BALANCED} and + * {@link Scalr#THRESHOLD_BALANCED_SPEED} thresholds when selecting which + * method should be used by comparing the primary dimension (width or + * height) against the threshold and seeing where the image falls. The + * primary dimension is determined by looking at the orientation of the + * image: landscape or square images use their width and portrait-oriented + * images use their height. + * + * @param targetWidth + * The target width for the scaled image. + * @param targetHeight + * The target height for the scaled image. + * @param ratio + * A height/width ratio used to determine the orientation of the + * image so the primary dimension (width or height) can be + * selected to test if it is greater than or less than a + * particular threshold. + * + * @return the fastest {@link Method} suited for scaling the image to the + * specified dimensions while maintaining a good-looking result. + */ + protected static Method determineScalingMethod(int targetWidth, + int targetHeight, float ratio) { + // Get the primary dimension based on the orientation of the image + int length = (ratio <= 1 ? targetWidth : targetHeight); + + // Default to speed + Method result = Method.SPEED; + + // Figure out which scalingMethod should be used + if (length <= Scalr.THRESHOLD_QUALITY_BALANCED) + result = Method.QUALITY; + else if (length <= Scalr.THRESHOLD_BALANCED_SPEED) + result = Method.BALANCED; + + if (DEBUG) + log(2, "AUTOMATIC scaling method selected: %s", result.name()); + + return result; + } + + /** + * Used to implement a straight-forward image-scaling operation using Java + * 2D. + *

+ * This method uses the Oracle-encouraged method of + * Graphics2D.drawImage(...) to scale the given image with the + * given interpolation hint. + * + * @param src + * The image that will be scaled. + * @param targetWidth + * The target width for the scaled image. + * @param targetHeight + * The target height for the scaled image. + * @param interpolationHintValue + * The {@link RenderingHints} interpolation value used to + * indicate the method that {@link Graphics2D} should use when + * scaling the image. + * + * @return the result of scaling the original src to the given + * dimensions using the given interpolation method. + */ + protected static BufferedImage scaleImage(BufferedImage src, + int targetWidth, int targetHeight, Object interpolationHintValue) { + // Setup the rendering resources to match the source image's + BufferedImage result = createOptimalImage(src, targetWidth, + targetHeight); + Graphics2D resultGraphics = result.createGraphics(); + + // Scale the image to the new buffer using the specified rendering hint. + resultGraphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + interpolationHintValue); + resultGraphics.drawImage(src, 0, 0, targetWidth, targetHeight, null); + + // Just to be clean, explicitly dispose our temporary graphics object + resultGraphics.dispose(); + + // Return the scaled image to the caller. + return result; + } + + /** + * Used to implement Chris Campbell's incremental-scaling algorithm: http://today.java.net/pub/a/today/2007/04/03/perils + * -of-image-getscaledinstance.html. + *

+ * Modifications to the original algorithm are variable names and comments + * added for clarity and the hard-coding of using BICUBIC interpolation as + * well as the explicit "flush()" operation on the interim BufferedImage + * instances to avoid resource leaking. + * + * @param src + * The image that will be scaled. + * @param targetWidth + * The target width for the scaled image. + * @param targetHeight + * The target height for the scaled image. + * @param scalingMethod + * The scaling method specified by the user (or calculated by + * imgscalr) to use for this incremental scaling operation. + * @param interpolationHintValue + * The {@link RenderingHints} interpolation value used to + * indicate the method that {@link Graphics2D} should use when + * scaling the image. + * + * @return an image scaled to the given dimensions using the given rendering + * hint. + */ + protected static BufferedImage scaleImageIncrementally(BufferedImage src, + int targetWidth, int targetHeight, Method scalingMethod, + Object interpolationHintValue) { + boolean hasReassignedSrc = false; + int incrementCount = 0; + int currentWidth = src.getWidth(); + int currentHeight = src.getHeight(); + + /* + * The original QUALITY mode, representing Chris Campbell's algorithm, + * is to step down by 1/2s every time when scaling the image + * incrementally. Users pointed out that using this method to scale + * images with noticeable straight lines left them really jagged in + * smaller thumbnail format. + * + * After investigation it was discovered that scaling incrementally by + * smaller increments was the ONLY way to make the thumbnail sized + * images look less jagged and more accurate; almost matching the + * accuracy of Mac's built in thumbnail generation which is the highest + * quality resize I've come across (better than GIMP Lanczos3 and + * Windows 7). + * + * A divisor of 7 was chose as using 5 still left some jaggedness in the + * image while a divisor of 8 or higher made the resulting thumbnail too + * soft; like our OP_ANTIALIAS convolve op had been forcibly applied to + * the result even if the user didn't want it that soft. + * + * Using a divisor of 7 for the ULTRA_QUALITY seemed to be the sweet + * spot. + * + * NOTE: Below when the actual fraction is used to calculate the small + * portion to subtract from the current dimension, this is a + * progressively smaller and smaller chunk. When the code was changed to + * do a linear reduction of the image of equal steps for each + * incremental resize (e.g. say 50px each time) the result was + * significantly worse than the progressive approach used below; even + * when a very high number of incremental steps (13) was tested. + */ + int fraction = (scalingMethod == Method.ULTRA_QUALITY ? 7 : 2); + + do { + int prevCurrentWidth = currentWidth; + int prevCurrentHeight = currentHeight; + + /* + * If the current width is bigger than our target, cut it in half + * and sample again. + */ + if (currentWidth > targetWidth) { + currentWidth -= (currentWidth / fraction); + + /* + * If we cut the width too far it means we are on our last + * iteration. Just set it to the target width and finish up. + */ + if (currentWidth < targetWidth) + currentWidth = targetWidth; + } + + /* + * If the current height is bigger than our target, cut it in half + * and sample again. + */ + + if (currentHeight > targetHeight) { + currentHeight -= (currentHeight / fraction); + + /* + * If we cut the height too far it means we are on our last + * iteration. Just set it to the target height and finish up. + */ + + if (currentHeight < targetHeight) + currentHeight = targetHeight; + } + + /* + * Stop when we cannot incrementally step down anymore. + * + * This used to use a || condition, but that would cause problems + * when using FIT_EXACT such that sometimes the width OR height + * would not change between iterations, but the other dimension + * would (e.g. resizing 500x500 to 500x250). + * + * Now changing this to an && condition requires that both + * dimensions do not change between a resize iteration before we + * consider ourselves done. + */ + if (prevCurrentWidth == currentWidth + && prevCurrentHeight == currentHeight) + break; + + if (DEBUG) + log(2, "Scaling from [%d x %d] to [%d x %d]", prevCurrentWidth, + prevCurrentHeight, currentWidth, currentHeight); + + // Render the incremental scaled image. + BufferedImage incrementalImage = scaleImage(src, currentWidth, + currentHeight, interpolationHintValue); + + /* + * Before re-assigning our interim (partially scaled) + * incrementalImage to be the new src image before we iterate around + * again to process it down further, we want to flush() the previous + * src image IF (and only IF) it was one of our own temporary + * BufferedImages created during this incremental down-sampling + * cycle. If it wasn't one of ours, then it was the original + * caller-supplied BufferedImage in which case we don't want to + * flush() it and just leave it alone. + */ + if (hasReassignedSrc) + src.flush(); + + /* + * Now treat our incremental partially scaled image as the src image + * and cycle through our loop again to do another incremental + * scaling of it (if necessary). + */ + src = incrementalImage; + + /* + * Keep track of us re-assigning the original caller-supplied source + * image with one of our interim BufferedImages so we know when to + * explicitly flush the interim "src" on the next cycle through. + */ + hasReassignedSrc = true; + + // Track how many times we go through this cycle to scale the image. + incrementCount++; + } while (currentWidth != targetWidth || currentHeight != targetHeight); + + if (DEBUG) + log(2, "Incrementally Scaled Image in %d steps.", incrementCount); + + /* + * Once the loop has exited, the src image argument is now our scaled + * result image that we want to return. + */ + return src; + } +} \ No newline at end of file diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/SoftReference.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/SoftReference.java new file mode 100644 index 000000000..97185ecf6 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/SoftReference.java @@ -0,0 +1,58 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import org.jetbrains.annotations.Nullable; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; + +/** + * The class is necessary to debug memory allocations via soft references. All IDEA classes should use this SoftReference + * instead of original from java.lang.ref. Whenever we suspect soft memory allocation overhead this easily becomes a hard + * reference so we can see allocations and memory consumption in memory profiler. + * + * @author max + */ +@SuppressWarnings("ClassNameSameAsAncestorName") +public class SoftReference extends java.lang.ref.SoftReference implements Getter { + //private final T myReferent; + + public SoftReference(final T referent) { + super(referent); + //myReferent = referent; + } + + public SoftReference(final T referent, final ReferenceQueue q) { + super(referent, q); + //myReferent = referent; + } + + //@Override + //public T get() { + // return myReferent; + //} + + @Nullable + public static T dereference(@Nullable Reference ref) { + return ref == null ? null : ref.get(); + } + @Nullable + public static T deref(@Nullable Getter ref) { + return ref == null ? null : ref.get(); + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/StringFactory.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/StringFactory.java new file mode 100644 index 000000000..4dec7079d --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/StringFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +//import sun.reflect.ConstructorAccessor; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/** + * @author Konstantin Bulenkov + */ +public class StringFactory { + // String(char[], boolean). Works since JDK1.7, earlier JDKs have too slow reflection anyway +// private static final ConstructorAccessor ourConstructorAccessor; + +// static { +// ConstructorAccessor constructorAccessor = null; +// try { +// Constructor newC = String.class.getDeclaredConstructor(char[].class, boolean.class); +// newC.setAccessible(true); + // it is faster to invoke constructor via sun.reflect.ConstructorAccessor; it avoids AccessibleObject.checkAccess() +// Method accessor = Constructor.class.getDeclaredMethod("acquireConstructorAccessor"); +// accessor.setAccessible(true); +// constructorAccessor = (ConstructorAccessor) accessor.invoke(newC); +// } catch (Exception ignored) { +// } +// ourConstructorAccessor = constructorAccessor; +// } + + + /** + * @return new instance of String which backed by 'chars' array. + *

+ * CAUTION. EXTREMELY DANGEROUS. + * DO NOT USE THIS METHOD UNLESS YOU ARE TOO DESPERATE + */ + public static String createShared(char[] chars) { +// if (ourConstructorAccessor != null) { +// try { +// return (String) ourConstructorAccessor.newInstance(new Object[]{chars, Boolean.TRUE}); +// } catch (Exception e) { +// throw new RuntimeException(e); +// } +// } + return new String(chars); + } +} + diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/StringUtil.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/StringUtil.java new file mode 100644 index 000000000..2435cafa0 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/StringUtil.java @@ -0,0 +1,304 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @author Konstantin Bulenkov + */ +public class StringUtil { + public static List split(String s, String separator) { + return split(s, separator, true); + } + + public static List split(String s, String separator, + boolean excludeSeparator) { + return split(s, separator, excludeSeparator, true); + } + + public static List split(String s, String separator, + boolean excludeSeparator, boolean excludeEmptyStrings) { + if (separator.isEmpty()) { + return Collections.singletonList(s); + } + List result = new ArrayList(); + int pos = 0; + while (true) { + int index = s.indexOf(separator, pos); + if (index == -1) break; + final int nextPos = index + separator.length(); + String token = s.substring(pos, excludeSeparator ? index : nextPos); + if (!token.isEmpty() || !excludeEmptyStrings) { + result.add(token); + } + pos = nextPos; + } + if (pos < s.length() || (!excludeEmptyStrings && pos == s.length())) { + result.add(s.substring(pos, s.length())); + } + return result; + } + + public static int indexOfIgnoreCase(String where, String what, int fromIndex) { + int targetCount = what.length(); + int sourceCount = where.length(); + + if (fromIndex >= sourceCount) { + return targetCount == 0 ? sourceCount : -1; + } + + if (fromIndex < 0) { + fromIndex = 0; + } + + if (targetCount == 0) { + return fromIndex; + } + + char first = what.charAt(0); + int max = sourceCount - targetCount; + + for (int i = fromIndex; i <= max; i++) { + /* Look for first character. */ + if (!charsEqualIgnoreCase(where.charAt(i), first)) { + while (++i <= max && !charsEqualIgnoreCase(where.charAt(i), first)) ; + } + + /* Found first character, now look at the rest of v2 */ + if (i <= max) { + int j = i + 1; + int end = j + targetCount - 1; + for (int k = 1; j < end && charsEqualIgnoreCase(where.charAt(j), what.charAt(k)); j++, k++) ; + + if (j == end) { + /* Found whole string. */ + return i; + } + } + } + + return -1; + } + + + public static int indexOfIgnoreCase(String where, char what, int fromIndex) { + int sourceCount = where.length(); + + if (fromIndex >= sourceCount) { + return -1; + } + + if (fromIndex < 0) { + fromIndex = 0; + } + + for (int i = fromIndex; i < sourceCount; i++) { + if (charsEqualIgnoreCase(where.charAt(i), what)) { + return i; + } + } + + return -1; + } + + public static boolean containsIgnoreCase(String where, String what) { + return indexOfIgnoreCase(where, what, 0) >= 0; + } + + public static boolean charsEqualIgnoreCase(char a, char b) { + return a == b || toUpperCase(a) == toUpperCase(b) || toLowerCase(a) == toLowerCase(b); + } + + public static char toUpperCase(char a) { + if (a < 'a') { + return a; + } + if (a <= 'z') { + return (char) (a + ('A' - 'a')); + } + return Character.toUpperCase(a); + } + + public static char toLowerCase(char a) { + if (a < 'A' || a >= 'a' && a <= 'z') { + return a; + } + + if (a <= 'Z') { + return (char) (a + ('a' - 'A')); + } + + return Character.toLowerCase(a); + } + + public static int compareVersionNumbers(String v1, String v2) { + if (v1 == null && v2 == null) { + return 0; + } + if (v1 == null) { + return -1; + } + if (v2 == null) { + return 1; + } + + String[] part1 = v1.split("[\\.\\_\\-]"); + String[] part2 = v2.split("[\\.\\_\\-]"); + + int idx = 0; + for (; idx < part1.length && idx < part2.length; idx++) { + String p1 = part1[idx]; + String p2 = part2[idx]; + + int cmp; + if (p1.matches("\\d+") && p2.matches("\\d+")) { + cmp = new Integer(p1).compareTo(new Integer(p2)); + } else { + cmp = part1[idx].compareTo(part2[idx]); + } + if (cmp != 0) return cmp; + } + + if (part1.length == part2.length) { + return 0; + } else { + boolean left = part1.length > idx; + String[] parts = left ? part1 : part2; + + for (; idx < parts.length; idx++) { + String p = parts[idx]; + int cmp; + if (p.matches("\\d+")) { + cmp = new Integer(p).compareTo(0); + } else { + cmp = 1; + } + if (cmp != 0) return left ? cmp : -cmp; + } + return 0; + } + } + + public static boolean startsWithChar(CharSequence s, char prefix) { + return s != null && s.length() != 0 && s.charAt(0) == prefix; + } + + public static boolean endsWithChar(CharSequence s, char suffix) { + return s != null && s.length() != 0 && s.charAt(s.length() - 1) == suffix; + } + + + public static String stripQuotesAroundValue(String text) { + if (startsWithChar(text, '\"') || startsWithChar(text, '\'')) text = text.substring(1); + if (endsWithChar(text, '\"') || endsWithChar(text, '\'')) text = text.substring(0, text.length() - 1); + return text; + } + + /** + * Equivalent to string.startsWith(prefixes[0] + prefixes[1] + ...) but avoids creating an object for concatenation. + */ + public static boolean startsWithConcatenation(String string, String... prefixes) { + int offset = 0; + for (String prefix : prefixes) { + int prefixLen = prefix.length(); + if (!string.regionMatches(offset, prefix, 0, prefixLen)) { + return false; + } + offset += prefixLen; + } + return true; + } + + public static String getFileExtension(String fileName) { + int index = fileName.lastIndexOf('.'); + if (index < 0) return ""; + return fileName.substring(index + 1); + } + + public static String getFileNameWithoutExtension(String name) { + int i = name.lastIndexOf('.'); + if (i != -1) { + name = name.substring(0, i); + } + return name; + + } + + @NotNull + @Contract(pure = true) + public static String join(@NotNull Collection strings, @NotNull String separator) { + if (strings.size() <= 1) { + return notNullize(getFirstItem(strings)); + } + StringBuilder result = new StringBuilder(); + join(strings, separator, result); + return result.toString(); + } + + @Contract(pure = true) + public static String join(@NotNull Iterable items, @NotNull @NonNls String separator) { + StringBuilder result = new StringBuilder(); + for (Object item : items) { + result.append(item).append(separator); + } + if (result.length() > 0) { + result.setLength(result.length() - separator.length()); + } + return result.toString(); + } + + + @NotNull + public static String notNullize(@Nullable final String s) { + return notNullize(s, ""); + } + + @NotNull + public static String notNullize(@Nullable final String s, @NotNull String defaultValue) { + return s == null ? defaultValue : s; + } + + public static void join(@NotNull Collection strings, @NotNull String separator, @NotNull StringBuilder result) { + boolean isFirst = true; + for (String string : strings) { + if (string != null) { + if (isFirst) { + isFirst = false; + } + else { + result.append(separator); + } + result.append(string); + } + } + } + + @Nullable + public static T getFirstItem(@Nullable Collection items) { + return items == null || items.isEmpty() ? null : items.iterator().next(); + } + +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/SystemInfo.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/SystemInfo.java new file mode 100644 index 000000000..d49982d4e --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/SystemInfo.java @@ -0,0 +1,137 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +/** + * @author Konstantin Bulenkov + */ +@SuppressWarnings({"HardCodedStringLiteral", "UtilityClassWithoutPrivateConstructor", "UnusedDeclaration"}) +public class SystemInfo { + public static final String OS_NAME = System.getProperty("os.name"); + public static final String OS_VERSION = System.getProperty("os.version").toLowerCase(); + public static final String JAVA_VERSION = System.getProperty("java.version"); + public static final String JAVA_RUNTIME_VERSION = System.getProperty("java.runtime.version"); + + protected static final String _OS_NAME = OS_NAME.toLowerCase(); + public static final boolean isWindows = _OS_NAME.startsWith("windows"); + public static final boolean isOS2 = _OS_NAME.startsWith("os/2") || _OS_NAME.startsWith("os2"); + public static final boolean isMac = _OS_NAME.startsWith("mac"); + public static final boolean isLinux = _OS_NAME.startsWith("linux"); + public static final boolean isUnix = !isWindows && !isOS2; + + public static final boolean isFileSystemCaseSensitive = isUnix && !isMac; + public static final boolean isMacOSLion = isLion(); + + public static final boolean isAppleJvm = isAppleJvm(); + public static final boolean isOracleJvm = isOracleJvm(); + public static final boolean isSunJvm = isSunJvm(); + public static final boolean isJetbrainsJvm = isJetbrainsJvm(); + + public static boolean isOsVersionAtLeast(String version) { + return compareVersionNumbers(OS_VERSION, version) >= 0; + } + + + private static boolean isTiger() { + return isMac && + !OS_VERSION.startsWith("10.0") && + !OS_VERSION.startsWith("10.1") && + !OS_VERSION.startsWith("10.2") && + !OS_VERSION.startsWith("10.3"); + } + + private static boolean isLeopard() { + return isMac && isTiger() && !OS_VERSION.startsWith("10.4"); + } + + private static boolean isSnowLeopard() { + return isMac && isLeopard() && !OS_VERSION.startsWith("10.5"); + } + + private static boolean isLion() { + return isMac && isSnowLeopard() && !OS_VERSION.startsWith("10.6"); + } + + private static boolean isMountainLion() { + return isMac && isLion() && !OS_VERSION.startsWith("10.7"); + } + + public static int compareVersionNumbers(String v1, String v2) { + if (v1 == null && v2 == null) { + return 0; + } + if (v1 == null) { + return -1; + } + if (v2 == null) { + return 1; + } + + String[] part1 = v1.split("[\\.\\_\\-]"); + String[] part2 = v2.split("[\\.\\_\\-]"); + + int idx = 0; + for (; idx < part1.length && idx < part2.length; idx++) { + String p1 = part1[idx]; + String p2 = part2[idx]; + + int cmp; + if (p1.matches("\\d+") && p2.matches("\\d+")) { + cmp = new Integer(p1).compareTo(new Integer(p2)); + } else { + cmp = part1[idx].compareTo(part2[idx]); + } + if (cmp != 0) return cmp; + } + + if (part1.length == part2.length) { + return 0; + } else if (part1.length > idx) { + return 1; + } else { + return -1; + } + } + + public static boolean isJavaVersionAtLeast(String v) { + return StringUtil.compareVersionNumbers(JAVA_RUNTIME_VERSION, v) >= 0; + } + + private static boolean isOracleJvm() { + final String vendor = getJavaVmVendor(); + return vendor != null && StringUtil.containsIgnoreCase(vendor, "Oracle"); + } + + private static boolean isSunJvm() { + final String vendor = getJavaVmVendor(); + return vendor != null && StringUtil.containsIgnoreCase(vendor, "Sun") && StringUtil.containsIgnoreCase(vendor, "Microsystems"); + } + + private static boolean isAppleJvm() { + final String vendor = getJavaVmVendor(); + return vendor != null && StringUtil.containsIgnoreCase(vendor, "Apple"); + } + + public static String getJavaVmVendor() { + return System.getProperty("java.vm.vendor"); + } + + private static boolean isJetbrainsJvm() { + final String vendor = System.getProperty("java.vendor"); + return vendor != null && StringUtil.containsIgnoreCase(vendor, "jetbrains"); + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/UIUtil.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/UIUtil.java new file mode 100644 index 000000000..9267b6e1c --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/UIUtil.java @@ -0,0 +1,272 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import com.bulenkov.iconloader.IsRetina; +import com.bulenkov.iconloader.JBHiDPIScaledImage; +import com.bulenkov.iconloader.RetinaImage; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.ImageObserver; +import java.lang.reflect.Field; + +/** + * @author Konstantin Bulenkov + */ +public class UIUtil { + public static final Color TRANSPARENT_COLOR = new Color(0, 0, 0, 0); + private static volatile Pair ourSystemFontData; + public static final float DEF_SYSTEM_FONT_SIZE = 12f; // TODO: consider 12 * 1.33 to compensate JDK's 72dpi font scale + + public static T findComponentOfType(JComponent parent, Class cls) { + if (parent == null || cls.isAssignableFrom(parent.getClass())) { + @SuppressWarnings({"unchecked"}) final T t = (T) parent; + return t; + } + for (Component component : parent.getComponents()) { + if (component instanceof JComponent) { + T comp = findComponentOfType((JComponent) component, cls); + if (comp != null) return comp; + } + } + return null; + } + + @Nullable + public static Pair getSystemFontData() { + return ourSystemFontData; + } + + public static T getParentOfType(Class cls, Component c) { + Component eachParent = c; + while (eachParent != null) { + if (cls.isAssignableFrom(eachParent.getClass())) { + @SuppressWarnings({"unchecked"}) final T t = (T) eachParent; + return t; + } + + eachParent = eachParent.getParent(); + } + + return null; + } + + public static boolean isAppleRetina() { + return isRetina() && SystemInfo.isAppleJvm; + } + + public static Color getControlColor() { + return UIManager.getColor("control"); + } + + public static Color getPanelBackground() { + return UIManager.getColor("Panel.background"); + } + + public static boolean isUnderDarcula() { + return UIManager.getLookAndFeel().getName().equals("Darcula"); + } + + public static Color getListBackground() { + return UIManager.getColor("List.background"); + } + + public static Color getListForeground() { + return UIManager.getColor("List.foreground"); + } + + public static Color getLabelForeground() { + return UIManager.getColor("Label.foreground"); + } + + public static Color getTextFieldBackground() { + return UIManager.getColor("TextField.background"); + } + + public static Color getTreeSelectionForeground() { + return UIManager.getColor("Tree.selectionForeground"); + } + + public static Color getTreeForeground() { + return UIManager.getColor("Tree.foreground"); + } + + private static final Color DECORATED_ROW_BG_COLOR = new DoubleColor(new Color(242, 245, 249), new Color(65, 69, 71)); + + public static Color getDecoratedRowColor() { + return DECORATED_ROW_BG_COLOR; + } + + public static Color getTreeSelectionBackground(boolean focused) { + return focused ? getTreeSelectionBackground() : getTreeUnfocusedSelectionBackground(); + } + + private static Color getTreeSelectionBackground() { + return UIManager.getColor("Tree.selectionBackground"); + } + + public static Color getTreeUnfocusedSelectionBackground() { + Color background = getTreeTextBackground(); + return ColorUtil.isDark(background) ? new DoubleColor(Gray._30, new Color(13, 41, 62)) : Gray._212; + } + + public static Color getTreeTextBackground() { + return UIManager.getColor("Tree.textBackground"); + } + + public static void drawImage(Graphics g, Image image, int x, int y, ImageObserver observer) { + if (image instanceof JBHiDPIScaledImage) { + final Graphics2D newG = (Graphics2D) g.create(x, y, image.getWidth(observer), image.getHeight(observer)); + newG.scale(0.5, 0.5); + Image img = ((JBHiDPIScaledImage) image).getDelegate(); + if (img == null) { + img = image; + } + newG.drawImage(img, 0, 0, observer); + newG.scale(1, 1); + newG.dispose(); + } else { + g.drawImage(image, x, y, observer); + } + } + + + private static final Ref ourRetina = Ref.create(SystemInfo.isMac ? null : false); + + public static boolean isRetina() { + synchronized (ourRetina) { + if (ourRetina.isNull()) { + ourRetina.set(false); // in case HiDPIScaledImage.drawIntoImage is not called for some reason + + if (SystemInfo.isJavaVersionAtLeast("1.6.0_33") && SystemInfo.isAppleJvm) { + if (!"false".equals(System.getProperty("ide.mac.retina"))) { + ourRetina.set(IsRetina.isRetina()); + return ourRetina.get(); + } + } else if (SystemInfo.isJavaVersionAtLeast("1.7.0_40") && (SystemInfo.isOracleJvm || SystemInfo.isJetbrainsJvm)) { + GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); + final GraphicsDevice device = env.getDefaultScreenDevice(); + try { + Field field = device.getClass().getDeclaredField("scale"); + if (field != null) { + field.setAccessible(true); + Object scale = field.get(device); + if (scale instanceof Integer && (Integer) scale == 2) { + ourRetina.set(true); + return true; + } + } + } catch (Exception ignore) { + } + } + ourRetina.set(false); + } + + return ourRetina.get(); + } + } + + public static BufferedImage createImage(int width, int height, int type) { + if (isRetina()) { + return RetinaImage.create(width, height, type); + } + //noinspection UndesirableClassUsage + return new BufferedImage(width, height, type); + } + + + private static final GrayFilter DEFAULT_GRAY_FILTER = new GrayFilter(true, 65); + private static final GrayFilter DARCULA_GRAY_FILTER = new GrayFilter(true, 30); + + public static GrayFilter getGrayFilter() { + return isUnderDarcula() ? DARCULA_GRAY_FILTER : DEFAULT_GRAY_FILTER; + } + + public static Font getLabelFont() { + return UIManager.getFont("Label.font"); + } + + public static float getFontSize(FontSize size) { + int defSize = getLabelFont().getSize(); + switch (size) { + case SMALL: + return Math.max(defSize - JBUI.scale(2f), JBUI.scale(11f)); + case MINI: + return Math.max(defSize - JBUI.scale(4f), JBUI.scale(9f)); + default: + return defSize; + } + } + + public enum FontSize {NORMAL, SMALL, MINI} + + public static void initSystemFontData() { + if (ourSystemFontData != null) return; + + // With JB Linux JDK the label font comes properly scaled based on Xft.dpi settings. + Font font = getLabelFont(); + + Float forcedScale = null; + if (Registry.is("ide.ui.scale.override")) { + forcedScale = Registry.getFloat("ide.ui.scale"); + } + else if (SystemInfo.isLinux && !SystemInfo.isJetbrainsJvm) { + // With Oracle JDK: derive scale from X server DPI + float scale = getScreenScale(); + if (scale > 1f) { + forcedScale = scale; + } + // Or otherwise leave the detected font. It's undetermined if it's scaled or not. + // If it is (likely with GTK DE), then the UI scale will be derived from it, + // if it's not, then IDEA will start unscaled. This lets the users of GTK DEs + // not to bother about X server DPI settings. Users of other DEs (like KDE) + // will have to set X server DPI to meet their display. + } + else if (SystemInfo.isWindows) { + //noinspection HardCodedStringLiteral + Font winFont = (Font)Toolkit.getDefaultToolkit().getDesktopProperty("win.messagebox.font"); + if (winFont != null) { + font = winFont; // comes scaled + } + } + if (forcedScale != null) { + // With forced scale, we derive font from a hard-coded value as we cannot be sure + // the system font comes unscaled. + font = font.deriveFont(DEF_SYSTEM_FONT_SIZE * forcedScale.floatValue()); + } + ourSystemFontData = Pair.create(font.getName(), font.getSize()); + } + + private static float getScreenScale() { + int dpi = 96; + try { + dpi = Toolkit.getDefaultToolkit().getScreenResolution(); + } catch (HeadlessException e) { + } + float scale = 1f; + if (dpi < 120) scale = 1f; + else if (dpi < 144) scale = 1.25f; + else if (dpi < 168) scale = 1.5f; + else if (dpi < 192) scale = 1.75f; + else scale = 2f; + + return scale; + } +} diff --git a/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/URLUtil.java b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/URLUtil.java new file mode 100644 index 000000000..c6e70ff98 --- /dev/null +++ b/fine-iconloader/src/main/java/com/bulenkov/iconloader/util/URLUtil.java @@ -0,0 +1,199 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bulenkov.iconloader.util; + +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * @author Konstantin Bulenkov + */ +public class URLUtil { + public static final String SCHEME_SEPARATOR = "://"; + public static final String FILE_PROTOCOL = "file"; + public static final String HTTP_PROTOCOL = "http"; + public static final String JAR_PROTOCOL = "jar"; + public static final String JAR_SEPARATOR = "!/"; + + public static final Pattern DATA_URI_PATTERN = Pattern.compile("data:([^,;]+/[^,;]+)(;charset=[^,;]+)?(;base64)?,(.+)"); + + private URLUtil() { + } + + /** + * Opens a url stream. The semantics is the sames as {@link java.net.URL#openStream()}. The + * separate method is needed, since jar URLs open jars via JarFactory and thus keep them + * mapped into memory. + */ + + public static InputStream openStream(URL url) throws IOException { + String protocol = url.getProtocol(); + boolean isFile = protocol.equals(JAR_PROTOCOL) && !url.getFile().startsWith(HTTP_PROTOCOL); + return isFile ? openJarStream(url) : url.openStream(); + } + + + public static InputStream openResourceStream(final URL url) throws IOException { + try { + return openStream(url); + } catch (FileNotFoundException ex) { + final String protocol = url.getProtocol(); + String file = null; + if (protocol.equals(FILE_PROTOCOL)) { + file = url.getFile(); + } else if (protocol.equals(JAR_PROTOCOL)) { + int pos = url.getFile().indexOf("!"); + if (pos >= 0) { + file = url.getFile().substring(pos + 1); + } + } + if (file != null && file.startsWith("/")) { + InputStream resourceStream = URLUtil.class.getResourceAsStream(file); + if (resourceStream != null) return resourceStream; + } + throw ex; + } + } + + + private static InputStream openJarStream(URL url) throws IOException { + Pair paths = splitJarUrl(url.getFile()); + if (paths == null) { + throw new MalformedURLException(url.getFile()); + } + + @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") final ZipFile zipFile = new ZipFile(unquote(paths.first)); + ZipEntry zipEntry = zipFile.getEntry(paths.second); + if (zipEntry == null) { + throw new FileNotFoundException("Entry " + paths.second + " not found in " + paths.first); + } + + return new FilterInputStream(zipFile.getInputStream(zipEntry)) { + @Override + public void close() throws IOException { + super.close(); + zipFile.close(); + } + }; + } + + public static String unquote(String urlString) { + urlString = urlString.replace('/', File.separatorChar); + return unescapePercentSequences(urlString); + } + + + public static Pair splitJarUrl(String fullPath) { + int delimiter = fullPath.indexOf(JAR_SEPARATOR); + if (delimiter >= 0) { + String resourcePath = fullPath.substring(delimiter + 2); + String jarPath = fullPath.substring(0, delimiter); + if (StringUtil.startsWithConcatenation(jarPath, FILE_PROTOCOL, ":")) { + jarPath = jarPath.substring(FILE_PROTOCOL.length() + 1); + return Pair.create(jarPath, resourcePath); + } + } + return null; + } + + + public static String unescapePercentSequences(String s) { + if (s.indexOf('%') == -1) { + return s; + } + + StringBuilder decoded = new StringBuilder(); + final int len = s.length(); + int i = 0; + while (i < len) { + char c = s.charAt(i); + if (c == '%') { + ArrayList bytes = new ArrayList(); + while (i + 2 < len && s.charAt(i) == '%') { + final int d1 = decode(s.charAt(i + 1)); + final int d2 = decode(s.charAt(i + 2)); + if (d1 != -1 && d2 != -1) { + bytes.add(((d1 & 0xf) << 4 | d2 & 0xf)); + i += 3; + } else { + break; + } + } + if (!bytes.isEmpty()) { + final byte[] bytesArray = new byte[bytes.size()]; + for (int j = 0; j < bytes.size(); j++) { + bytesArray[j] = bytes.get(j).byteValue(); + } + decoded.append(new String(bytesArray, Charset.forName("UTF-8"))); + continue; + } + } + + decoded.append(c); + i++; + } + return decoded.toString(); + } + + private static int decode(char c) { + if ((c >= '0') && (c <= '9')) + return c - '0'; + if ((c >= 'a') && (c <= 'f')) + return c - 'a' + 10; + if ((c >= 'A') && (c <= 'F')) + return c - 'A' + 10; + return -1; + } + + public static boolean containsScheme(String url) { + return url.contains(SCHEME_SEPARATOR); + } + + public static boolean isDataUri(String value) { + return !value.isEmpty() && value.startsWith("data:", value.charAt(0) == '"' || value.charAt(0) == '\'' ? 1 : 0); + } + + /** + * Extracts byte array from given data:URL string. + * data:URL will be decoded from base64 if it contains the marker of base64 encoding. + * + * @param dataUrl data:URL-like string (may be quoted) + * @return extracted byte array or {@code null} if it cannot be extracted. + */ + + public static byte[] getBytesFromDataUri(String dataUrl) { + Matcher matcher = DATA_URI_PATTERN.matcher(StringUtil.stripQuotesAroundValue(dataUrl)); + if (matcher.matches()) { + try { + String content = matcher.group(4); + final Charset charset = Charset.forName("UTF-8"); + final byte[] bytes = content.getBytes(charset); + return ";base64".equalsIgnoreCase(matcher.group(3)) ? Base64Converter.decode(bytes) : bytes; + } catch (IllegalArgumentException e) { + return null; + } + } + return null; + } +}