All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.osgl.util.Img Maven / Gradle / Ivy

The newest version!
package org.osgl.util;

/*-
 * #%L
 * Java Tool
 * %%
 * Copyright (C) 2014 - 2018 OSGL (Open Source General Library)
 * %%
 * 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.
 * #L%
 */

import static org.osgl.Lang.requireNotNull;
import static org.osgl.util.E.*;
import static org.osgl.util.N.*;
import static org.osgl.util.S.requireNotBlank;

import org.osgl.$;
import org.osgl.Lang;
import org.osgl.exception.NotAppliedException;

import java.awt.*;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.io.*;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;

/**
 * Image utilities
 *
 * Disclaim: some of the logic in this class comes from
 * https://github.com/playframework/play1/blob/master/framework/src/play/libs/Images.java
 */
public enum Img {
    ;

    private static Processor COPIER = new Processor() {
        @Override
        protected BufferedImage run() {
            return source;
        }
    };

    /**
     * The default image MIME type when not specified
     *
     * The value is `image/png`
     */
    public static final String DEF_MIME_TYPE = "image/png";

    public static final String GIF_MIME_TYPE = "image/gif";

    public static final String PNG_MIME_TYPE = "image/png";

    public static final String JPG_MIME_TYPE = "image/jpeg";

    public static final Color COLOR_TRANSPARENT = new Color(0, 0, 0, 0);

    /**
     * Byte array of a tracking pixel image in gif format
     */
    public static final byte[] TRACKING_PIXEL_BYTES = new ProcessorStage(F.TRACKING_PIXEL).toByteArray(GIF_MIME_TYPE);

    /**
     * Base64 string of a tracking pixel image in gif format
     */
    public static final String TRACKING_PIXEL_BASE64 = toBase64(TRACKING_PIXEL_BYTES, GIF_MIME_TYPE);

    /**
     * The direction used to process image
     */
    public enum Direction {
        HORIZONTAL, VERTICAL;

        public boolean isHorizontal() {
            return HORIZONTAL == this;
        }

        public boolean isVertical() {
            return VERTICAL == this;
        }

        public N.WH concatenate(N.Dimension d1, N.Dimension d2) {
            return isHorizontal() ? N.dimension(d1.w() + d2.w(), N.max(d1.h(), d2.h()))
                    : N.dimension(N.max(d1.w(), d2.w()), d1.h() + d2.h());
        }

        public void drawImage(Graphics2D g, BufferedImage source1, BufferedImage source2) {
            g.drawImage(source1, 0, 0, source1.getWidth(), source1.getHeight(), null);
            int x2 = 0, y2 = 0;
            if (isHorizontal()) {
                x2 = source1.getWidth();
            } else {
                y2 = source1.getHeight();
            }
            g.drawImage(source2, x2, y2, source2.getWidth(), source2.getHeight(), null);
        }
    }

    public enum Random {
        ;

        public static Color color() {
            return new Color(randInt(255), randInt(255), randInt(255));
        }

        public static Color lightColor() {
            return randomColor(170, 255, 170, 255, 170, 255);
        }

        public static Color darkColor() {
            return randomColor(0, 85, 0, 85, 0, 85);
        }

        public static Color moderateColor() {
            return randomColor(85,  170, 85, 170, 85, 170);
        }

        public static Color grayColor() {
            int n = randInt(255);
            return new Color(n, n, n);
        }

        public static Color lightGrayColor() {
            return randomGrayColor(170, 255);
        }

        public static Color darkGrayColor() {
            return randomGrayColor(0, 85);
        }

        public static Color moderateGrayColor() {
            return randomGrayColor(85, 170);
        }

        public static Color randomLightRedColor() {
            return randomColor(170, 255, 85, 170, 85, 170);
        }

        public static Color randomDarkRedColor() {
            return randomColor(85, 170, 0, 85, 0, 85);
        }

        public static Color randomModerateRedColor() {
            return randomColor(170, 255, 0, 85, 0, 85);
        }

        public static Color randomLightGreenColor() {
            return randomColor(85, 170, 170, 255, 85, 170);
        }

        public static Color randomDarkGreenColor() {
            return randomColor(0, 85, 85, 170, 0, 85);
        }

        public static Color randomModerateGreenColor() {
            return randomColor(0, 85, 170, 255, 0, 85);
        }

        public static Color randomLightBlueColor() {
            return randomColor(85, 170, 85, 170, 170, 255);
        }

        public static Color randomDarkBlueColor() {
            return randomColor(0, 85, 0, 85, 85, 170);
        }

        public static Color randomModerateBlueColor() {
            return randomColor(0, 85, 0, 85, 170, 255);
        }

        public static Color randomGrayColor(int min, int max) {
            int r = randInt(min, max);
            return new Color(r, r, r);
        }

        public static Color randomColor(int minR, int maxR, int minG, int maxG, int minB, int maxB) {
            return new Color(randInt(minR, maxR), randInt(minG, maxG), randInt(minB, maxB));
        }

    }

    /**
     * Base class for image operator function which provides source width, height, ratio parameters
     * on demand
     */
    public abstract static class Processor, STAGE extends ProcessorStage> extends $.Producer {
        /**
         * The source image
         */
        protected BufferedImage source;
        /**
         * The source image width
         */
        protected int sourceWidth;
        /**
         * The source image height
         */
        protected int sourceHeight;
        /**
         * The source image width/height ratio
         */
        protected double sourceRatio;

        /**
         * The target image
         */
        protected BufferedImage target;

        /**
         * The graphics
         */
        protected Graphics2D g;

        private Class stageClass;

        protected Processor() {
            exploreStageClass();
        }

        /**
         * Create a builder for this processor.
         *
         * To provide better fluent coding experience, sub class can overwrite this
         * function to provide specified builder instance instead of a general
         * `ProcessorBuilder` as provided here
         *
         * @return a builder for this processor
         */
        protected final STAGE createStage() {
            return createStage(get());
        }

        /**
         * Create a builder for this processor.
         *
         * To provide better fluent coding experience, sub class can overwrite this
         * function to provide specified builder instance instead of a general
         * `ProcessorBuilder` as provided here
         *
         * @param source the source image
         * @return a builder for this processor
         */
        protected STAGE createStage(BufferedImage source) {
            return null == stageClass ? (STAGE) new ProcessorStage<>(source, (PROCESSOR) this) : $.newInstance(stageClass, source, this).source(source);
        }

        @Override
        public BufferedImage produce() {
            try {
                beforeRun();
                return run();
            } finally {
                if (null != g) {
                    g.dispose();
                }
            }
        }

        protected void beforeRun() {}

        /*
         * Sub class shall implement the image process logic in
         * this method
         *
         * @return the processed image from {@link #source source image}
         */
        protected abstract BufferedImage run();

        /**
         * Set source image. This method will calculate and cache the following
         * parameters about the source image:
         *
         * * {@link #sourceWidth}
         * * {@link #sourceHeight}
         * * {@link #sourceRatio}
         *
         * @param source the source image
         * @return this Processor instance
         */
        public Processor source(BufferedImage source) {
            this.source = requireNotNull(source);
            this.sourceWidth = source.getWidth();
            this.sourceHeight = source.getHeight();
            this.sourceRatio = (double) this.sourceWidth / this.sourceHeight;
            return this;
        }

        public STAGE process(InputStream is) {
            return createStage(read(is));
        }

        public STAGE process(BufferedImage source) {
            return createStage(source);
        }

        /**
         * Get {@link Graphics2D} instance. If it is not created yet
         * then call {@link #createGraphics2D()} to create the instance
         *
         * @return the g instance
         */
        protected Graphics2D g() {
            if (null == g) {
                g = createGraphics2D();
            }
            return g;
        }

        protected Graphics2D cloneSource() {
            g().drawImage(source, 0, 0, null);
            return g;
        }

        /**
         * Create the {@link Graphics2D}. This method will trigger
         * {@link #createTarget()} method if target has not been
         * created yet
         *
         * @return an new Graphics2D
         */
        protected Graphics2D createGraphics2D() {
            if (null == target) {
                createTarget();
            }
            return target.createGraphics();
        }

        /**
         * Create {@link #target} image using source width/height. It will
         * use source color model to check if alpha channel should be be
         * added or not
         */
        protected void createTarget() {
            setTargetSpec(sourceWidth, sourceHeight, source.getColorModel().hasAlpha());
        }

        /**
         * Create {@link #target} image using specified width and height.
         *
         * This method will use source code model it check if alpha channel should be
         * added or not
         *
         * @param w the width of target image
         * @param h the height of target image
         */
        protected void setTargetSpec(int w, int h) {
            setTargetSpec(w, h, source.getColorModel().hasAlpha());
        }

        /**
         * Create {@link #target} image using specified width, height and alpha channel flag
         *
         * @param w                the width of target image
         * @param h                the height of target image
         * @param withAlphaChannel whether it shall be created with alpha channel
         */
        protected void setTargetSpec(int w, int h, boolean withAlphaChannel) {
            target = new BufferedImage(w, h, withAlphaChannel ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB);
        }

        private void exploreStageClass() {
            try {
                List types = Generics.typeParamImplementations(getClass(), Processor.class);
                if (types.size() > 1) {
                    Type stageType = types.get(1);
                    stageClass = Generics.classOf(stageType);
                }
            } catch (RuntimeException e) {
                stageClass = null;
            }
        }
    }

    public static abstract class Filter, STAGE extends ProcessorStage> extends Processor {

        @Override
        protected void beforeRun() {
            createTarget();
        }

        @Override
        protected void createTarget() {
            target = source;
        }


    }

    /**
     * The base class that process two image sources and produce result image
     */
    public abstract static class BinarySourceProcessor, STAGE extends ProcessorStage> extends Processor {

        /**
         * How to handle two image sources scale mismatch
         */
        public enum ScaleFix {
            /**
             * Scale smaller image to larger one
             */
            SCALE_TO_MAX() {
                @Override
                public int targetScale(int scale1, int scale2) {
                    return N.max(scale1, scale2);
                }
            },

            /**
             * Shrink larger image to smaller one
             */
            SHRINK_TO_MIN() {
                @Override
                public int targetScale(int scale1, int scale2) {
                    return N.min(scale1, scale2);
                }

            },

            /**
             * Do not fix the scale mismatch
             */
            NO_FIX() {
                @Override
                public boolean shouldFix() {
                    return false;
                }
            };

            public int targetScale(int scale1, int scale2) {
                throw unsupport();
            }

            public boolean shouldFix() {
                return true;
            }
        }

        /**
         * The second source image
         */
        protected BufferedImage source2;
        /**
         * The second source image width
         */
        protected int source2Width;
        /**
         * The second source image height
         */
        protected int source2Height;
        /**
         * The second source image width/height ratio
         */
        protected double source2Ratio;

        /**
         * Set second source image. This method will calculate and cache the following
         * parameters about the source image:
         *
         * * {@link #source2Width}
         * * {@link #source2Height}
         * * {@link #source2Ratio}
         *
         * @param source the second source image
         * @return this Processor instance
         */
        public Processor secondSource(BufferedImage source) {
            this.source2 = requireNotNull(source);
            this.source2Width = source.getWidth();
            this.source2Height = source.getHeight();
            this.source2Ratio = (double) this.sourceWidth / this.sourceHeight;
            return this;
        }

    }


    public static class ProcessorStage, PROCESSOR extends Processor> extends _Load {
        /**
         * The image target as a result of processing. Note if {@link #processor} is not provided
         * then it will use {@link #source} directly as the target
         */
        protected volatile BufferedImage target;
        /**
         * The processor that apply a certain logic on source
         * and generates the target
         */
        protected PROCESSOR processor;
        /**
         * Define the compression quality of the target image
         */
        protected float compressionQuality = Float.NaN;

        private ProcessorStage($.Func0 source) {
            this(source.apply(), (PROCESSOR) COPIER);
        }

        private ProcessorStage(BufferedImage source) {
            this(source, (PROCESSOR) COPIER);
        }

        /**
         * Construct a ProcessorBuilder with an image source provider
         *
         * @param source the function that provides the image source
         */
        public ProcessorStage($.Func0 source, PROCESSOR processor) {
            this(source.apply(), processor);
        }

        /**
         * Construct a ProcessorBuilder with image source specified
         *
         * @param source the image source to be processed
         */
        public ProcessorStage(BufferedImage source, PROCESSOR processor) {
            super(source);
            this.processor = requireNotNull(processor);
        }

        /**
         * Returns the the target image as a result of processing or source image if no {@link #processor} is provided
         *
         * @return the target image
         */
        @Override
        public BufferedImage get() {
            return target();
        }

        public STAGE source(InputStream is) {
            this.source = read(is);
            return me();
        }

        public STAGE source(BufferedImage source) {
            this.source = requireNotNull(source);
            return me();
        }

        public _Load pipeline() {
            return new _Load(target());
        }

        private BufferedImage target() {
            if (null == target) {
                doJob();
            }
            return target;
        }

        private synchronized void doJob() {
            preTransform();
            target = null == processor ? source : processor.source(source).produce();
        }

        protected void preTransform() {
        }

    }

    public static class _Load extends $.Provider {

        protected BufferedImage source;
        /**
         * Define the compression quality of the target image
         */
        protected float compressionQuality = Float.NaN;

        private _Load(InputStream is) {
            this.source = read(is);
        }

        private _Load(BufferedImage source) {
            this.source = requireNotNull(source);
        }

        public T compressionQuality(float compressionQuality) {
            this.compressionQuality = N.requireAlpha(compressionQuality);
            return me();
        }

        public void writeTo(String fileName) {
            writeTo(new File(fileName));
        }

        public void writeTo(File file, String mimeType) {
            writeTo(IO.outputStream(file), mimeType);
        }

        public void writeTo(File file) {
            writeTo(IO.outputStream(file), mimeType(file));
        }

        public void writeTo(OutputStream os, String mimeType) {
            ImageWriter writer = ImageIO.getImageWritersByMIMEType(mimeType(mimeType)).next();
            dropAlphaChannelIfJPEG(writer);
            ImageWriteParam params = writer.getDefaultWriteParam();

            if (!Float.isNaN(compressionQuality) && params.canWriteCompressed()) {
                params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                params.setCompressionType(params.getCompressionTypes()[0]);
                params.setCompressionQuality(compressionQuality);
            }

            ImageOutputStream ios = os(os);
            writer.setOutput(ios);
            IIOImage image = new IIOImage(get(), null, null);
            try {
                writer.write(null, image, params);
            } catch (IOException e) {
                throw ioException(e);
            }
            IO.flush(ios);
            writer.dispose();
        }

        public byte[] toByteArray() {
            return toByteArray(DEF_MIME_TYPE);
        }

        public byte[] toByteArray(String mimeType) {
            ByteArrayOutputStream baos = IO.baos();
            writeTo(baos, mimeType(mimeType));
            return baos.toByteArray();
        }

        public String toBase64() {
            return toBase64(DEF_MIME_TYPE);
        }

        public String toBase64(String mimeType) {
            return Img.toBase64(toByteArray(mimeType), mimeType);
        }

        public void dropAlphaChannelIfJPEG(ImageWriter writer) {
            if (writer.getClass().getSimpleName().toUpperCase().contains("JPEG")) {
                BufferedImage src = source;
                BufferedImage convertedImg = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_INT_RGB);
                convertedImg.getGraphics().drawImage(src, 0, 0, null);
                source = convertedImg;
            }
        }

        @Override
        public BufferedImage get() {
            return source;
        }

        /**
         * Pipeline the target image as an input (source) image to to another processor
         *
         * @param processor the next processor
         * @param        the processor builder type
         * @param 

the processor type * @return a {@link ProcessBuilder} for the processor specified */ public , P extends Processor> B pipeline(P processor) { return processor.createStage(get()); } public ProcessorStage pipeline(Processor p, Processor... others) { ProcessorStage stage = pipeline(p); for (Processor other : others) { stage = stage.pipeline(other); } return stage; } public ProcessorStage pipeline(List processors) { org.osgl.util.E.illegalArgumentIf(processors.isEmpty()); int sz = processors.size(); Processor first = processors.get(0); ProcessorStage stage = pipeline(first); for (int i = 1; i < sz; ++i) { stage = stage.pipeline(processors.get(i)); } return stage; } public , P extends Processor> B pipeline(Class processorClass) { return $.newInstance(processorClass).createStage(get()); } public Resizer.Stage resize() { return new Resizer.Stage(get()); } public Resizer.Stage resize(float scale) { return new Resizer.Stage(get()).scale(scale); } public Resizer.Stage resize(int w, int h) { return new Resizer.Stage(get()).dimension(w, h); } public Resizer.Stage resize($.Tuple dimension) { return resize(dimension.left(), dimension.right()); } public Resizer.Stage resize(Dimension dimension) { return resize(dimension.width, dimension.height); } public Cropper.Stage crop() { return new Cropper.Stage(get()); } public Cropper.Stage crop(int x1, int y1, int x2, int y2) { return new Cropper.Stage(get()).from(x1, y1).to(x2, y2); } public Cropper.Stage crop($.Tuple leftTop, $.Tuple rightBottom) { return crop(leftTop._1, leftTop._2, rightBottom._1, rightBottom._2); } public TextWriter.Stage text(String text) { return new TextWriter.Stage(get()).text(text); } public TextWriter.Stage watermark() { return new TextWriter.Stage(get()); } public TextWriter.Stage watermark(String text) { return new TextWriter.Stage(get()).color(Color.LIGHT_GRAY).alpha(0.8f).rotate(-Math.PI / 7).offset(-130, 80).text(text); } public Blur.Stage blur() { return new Blur.Stage(get()); } public Blur.Stage blur(int level) { return new Blur.Stage(get()).level(level); } public NoiseMaker.Stage makeNoise() { return new NoiseMaker.Stage(get()); } public Flip.Stage flip() { return flip(Direction.HORIZONTAL); } public Flip.Stage flipVertial() { return flip(Direction.VERTICAL); } public Flip.Stage flip(Direction dir) { return new Flip.Stage(get()).dir(dir); } public ProcessorStage compress(float compressionQuality) { return new ProcessorStage(get()).compressionQuality(compressionQuality).pipeline(COPIER); } public ProcessorStage copy() { return new ProcessorStage(get()).pipeline(COPIER); } public ProcessorStage processor(Processor processor) { return processor.createStage(); } public Concatenater.Stage appendWith($.Func0 secondImange) { return new Concatenater.Stage(get()).with(secondImange); } public Concatenater.Stage appendTo($.Func0 firstImage) { return appendWith(firstImage).reverse(); } public Concatenater.Stage appendWith(BufferedImage secondImange) { return new Concatenater.Stage(get()).with(F.source(secondImange)); } public Concatenater.Stage appendTo(BufferedImage firstImage) { return appendWith(firstImage).reverse(); } protected T me() { return $.cast(this); } } public static _Load source(InputStream is) { return new _Load(is); } public static _Load source(URL url) { return new _Load(IO.inputStream(url)); } public static _Load source(File file) { return new _Load(IO.inputStream(file)); } public static _Load source($.Func0 imageProducer) { return new ProcessorStage<>(imageProducer).pipeline(); } public static _Load source(BufferedImage image) { return new ProcessorStage<>(image).pipeline(); } public static Resizer.Stage resize($.Func0 imageProvider) { return source(imageProvider).resize(); } public static Cropper.Stage crop($.Func0 imageProvider) { return source(imageProvider).crop(); } public static Flip.Stage flip($.Func0 imageProvider) { return source(imageProvider).flip(); } public static Blur.Stage blur($.Func0 imageProvider) { return source(imageProvider).blur(); } public static TextWriter.Stage watermark($.Func0 imageProvider) { return source(imageProvider).watermark(); } public static Concatenater.Stage concat($.Func0 image1) { return new Concatenater.Stage(image1.apply()); } public static Concatenater.Stage concat($.Func0 image1, $.Func0 image2) { return source(image1).appendWith(image2); } /** * Encode an image to base64 using a data: URI * * @param image The image file * @return The base64 encoded value */ public static String toBase64(File image) { return toBase64(IO.inputStream(image), mimeType(image)); } /** * Encode an image to base64 using a data: URI * * @param inputStream The image input stream * @param mimeType The mime type, if not specified then default to {@link #DEF_MIME_TYPE} * @return The base64 encoded value */ public static String toBase64(InputStream inputStream, String mimeType) { return toBase64(IO.readContent(inputStream), mimeType); } /** * Encode an image to base64 using a data: URI * * @param bytes The image byte array * @param mimeType the mime type, if not specified then default to {@link #DEF_MIME_TYPE} * @return The base64 encoded value */ public static String toBase64(byte[] bytes, String mimeType) { return "data:" + mimeType(mimeType) + ";base64," + Codec.encodeBase64(bytes); } private static String mimeType(File target) { return mimeType(target.getName()); } private static String mimeType(String hint) { String mimeType = DEF_MIME_TYPE; if (S.blank(hint)) { return mimeType; } if (1 == S.count(hint, "/", false) && !hint.contains(".")) { // this is a mime type string return hint; } if (hint.endsWith("jpeg") || hint.endsWith("jpg")) { mimeType = JPG_MIME_TYPE; } if (hint.endsWith("gif")) { mimeType = GIF_MIME_TYPE; } return mimeType; } public static ImageOutputStream os(File file) { try { return new FileImageOutputStream(file); } catch (IOException e) { throw ioException(e); } } public static ImageOutputStream os(OutputStream os) { return new MemoryCacheImageOutputStream(os); } public static BufferedImage read(InputStream is) { try { return ImageIO.read(is); } catch (Exception e) { throw unexpected(e); } } public static BufferedImage read(File file) { try { return ImageIO.read(file); } catch (Exception e) { throw unexpected(e); } } public static BufferedImage read(URL url) { try { return ImageIO.read(url); } catch (Exception e) { throw unexpected(e); } } // -- Image operators /** * Resize an image */ public static class Resizer extends Processor { public static class Stage extends ProcessorStage { Stage(BufferedImage source, Resizer processor) { super(source, processor); } Stage(BufferedImage source) { super(source, new Resizer()); } public Stage dimension(int w, int h) { processor.w = requireNonNegative(w); processor.h = requireNonNegative(h); return this; } public Stage dimension($.Tuple dimension) { return to(dimension); } public Stage dimension(Dimension dimension) { return dimension(dimension.width, dimension.height); } public Stage to(int w, int h) { return dimension(w, h); } public Stage to($.Tuple dimension) { return to(dimension.left(), dimension.right()); } public Stage to(Dimension dimension) { return to(dimension.width, dimension.height); } public Stage to(float scale) { return scale(scale); } public Stage scale(float scale) { processor.scale = requirePositive(scale); return this; } public Stage keepRatio() { processor.keepRatio = true; return this; } } int w; int h; float scale = Float.NaN; boolean keepRatio; Resizer() { } Resizer(int w, int h, boolean keepRatio) { this.w = requireNonNegative(w); this.h = requireNonNegative(h); this.keepRatio = keepRatio; } Resizer(float scale) { this.scale = requireNotNaN(scale); this.keepRatio = true; } @Override protected Stage createStage(BufferedImage source) { return new Stage(source, this); } @Override protected BufferedImage run() { int w = this.w; int h = this.h; final int maxWidth = w; final int maxHeight = h; if (Float.isNaN(scale)) { if (w < 0 && h < 0) { w = sourceWidth; h = sourceHeight; } final double ratio = this.sourceRatio; if (w < 0 && h > 0) { w = (int) (h * ratio); } if (w > 0 && h < 0) { h = (int) (w / ratio); } if (keepRatio) { h = (int) (w / ratio); if (h > maxHeight) { h = maxHeight; w = (int) (h * ratio); } if (w > maxWidth) { w = maxWidth; h = (int) (w / ratio); } } } else { w = (int) (sourceWidth * scale); h = (int) (sourceHeight * scale); } // out setTargetSpec(w, h); Graphics g = g(); if (!source.getColorModel().hasAlpha()) { // Create a white background if not transparency define g = target.getGraphics(); g.setColor(Color.WHITE); g.fillRect(0, 0, w, h); } Image srcResized = source.getScaledInstance(w, h, Image.SCALE_SMOOTH); g.drawImage(srcResized, 0, 0, null); return target; } } public static class Cropper extends Processor { public static class Stage extends ProcessorStage { Stage(BufferedImage source, Cropper processor) { super(source, processor); } Stage(BufferedImage source) { super(source, new Cropper()); } public Stage from(int x, int y) { processor.x1 = x; processor.y1 = y; return this; } public Stage to(int x, int y) { processor.x2 = x; processor.y2 = y; return this; } } private int x1; private int y1; private int x2; private int y2; Cropper() { } @Override protected Stage createStage(BufferedImage source) { return new Stage(source, this); } @Override protected BufferedImage run() { int x2 = this.x2; x2 = x2 < 0 ? sourceWidth + x2 : x2; int y2 = this.y2; y2 = y2 < 0 ? sourceHeight + y2 : y2; int w = x2 - x1; int h = y2 - y1; if (w < 0) { x1 = x2; w = -w; } if (h < 0) { y1 = y2; h = -h; } // out setTargetSpec(w, h); Image croppedImage = source.getSubimage(x1, y1, w, h); Graphics g = g(); g.setColor(Color.WHITE); g.fillRect(0, 0, w, h); g.drawImage(croppedImage, 0, 0, null); return target; } } public static class Flip extends Processor { public static class Stage extends ProcessorStage { Stage(BufferedImage source, Flip processor) { super(source, processor); } Stage(BufferedImage source) { super(source, new Flip()); } public Flip.Stage vertically() { processor.dir = Direction.VERTICAL; return this; } public Flip.Stage horizontally() { processor.dir = Direction.HORIZONTAL; return this; } public Flip.Stage dir(Direction dir) { processor.dir = requireNotNull(dir); return this; } } Direction dir = Direction.HORIZONTAL; Flip() { } @Override protected Stage createStage(BufferedImage source) { return new Stage(source, this); } @Override protected BufferedImage run() { Graphics2D g = g(); if (dir.isHorizontal()) { g.drawImage(source, sourceWidth, 0, -sourceWidth, sourceHeight, null); } else { g.drawImage(source, 0, sourceHeight, sourceWidth, -sourceHeight, null); } return target; } } public static class Blur extends Filter { public static class Stage extends ProcessorStage { public Stage(BufferedImage source) { super(source, new Blur()); } public Stage(BufferedImage source, Blur processor) { super(source, processor); } public Stage level(int level) { processor.setLevel(level); return this; } } static final int DEFAULT_LEVEL = 3; float[] matrix; int level; Blur() { setLevel(DEFAULT_LEVEL); } @Override protected Stage createStage(BufferedImage source) { return new Stage(source, this); } void setLevel(int level) { this.level = requirePositive(level); int max = level * level; matrix = new float[requirePositive(max)]; for (int i = 0; i < max; ++i) { matrix[i] = (float) 1 / (float) max; } } @Override protected BufferedImage run() { BufferedImageOp op = new ConvolveOp(new Kernel(level, level, matrix), ConvolveOp.EDGE_NO_OP, null); target = op.filter(target, null); return target; } } public static class NoiseMaker extends Filter { public static class Stage extends ProcessorStage { public Stage(BufferedImage source) { super(source, new NoiseMaker()); } public Stage(BufferedImage source, NoiseMaker processor) { super(source, processor); } public Stage setMinArcs(int n) { processor.minArcs = N.requireNonNegative(n); return this; } public Stage setMaxArcs(int n) { processor.maxArcs = N.requirePositive(n); return this; } public Stage setMaxArcSize(int size) { processor.maxArcSize = size; return this; } public Stage setMaxLines(int n) { processor.maxLines = N.requirePositive(n); return this; } public Stage setMaxLineWidth(int n) { processor.maxLineWidth = N.requirePositive(n); return this; } } private int minArcs = 100; private int maxArcs = 200; private int maxArcSize = 5; private int minLines = 1; private int maxLines = 5; private int maxLineWidth = 2; @Override protected NoiseMaker.Stage createStage(BufferedImage source) { return new NoiseMaker.Stage(source, this); } @Override protected BufferedImage run() { int w = sourceWidth; int h = sourceHeight; java.util.Random r = ThreadLocalRandom.current(); Graphics2D g = g(); // draw random arcs if (maxArcs < minArcs) { int tmp = maxArcs; maxArcs = minArcs; minArcs = tmp; } int dots = minArcs + r.nextInt(maxArcs); for (int i = 0; i < dots; i++) { // set a random color g.setColor(new Color(randomColorValue())); // pick up a random position int xInt = r.nextInt(w - 1); int yInt = r.nextInt(h - 1); // random angle int sAngleInt = r.nextInt(360); int eAngleInt = r.nextInt(360); // size of the arc int wInt = 1 + r.nextInt(maxArcSize); int hInt = 1 + r.nextInt(maxArcSize); g.fillArc(xInt, yInt, wInt, hInt, sAngleInt, eAngleInt); } // draw random lines; int lines = minLines + r.nextInt(maxLines - minLines); for (int i = 0; i < lines; ++i) { int xInt = r.nextInt(w - 1); int yInt = r.nextInt(h - 1); int xInt2 = r.nextInt(w - 1); int yInt2 = r.nextInt(h - 1); g.setColor(Random.color()); if (1 < maxLineWidth) { int width = N.randInt(1, maxLineWidth + 1); Stroke stroke = new BasicStroke((float) width); g.setStroke(stroke); } g.drawLine(xInt, yInt, xInt2, yInt2); } return target; } } public static class TextWriter extends Filter { public static class Stage extends ProcessorStage { public Stage(BufferedImage source) { super(source, new TextWriter()); } public Stage(BufferedImage source, TextWriter processor) { super(source, processor); } public TextWriter.Stage text(String text) { processor.text = requireNotBlank(text); return this; } public TextWriter.Stage color(Color color) { processor.color = requireNotNull(color); return this; } public TextWriter.Stage font(Font font) { processor.font = requireNotNull(font); return this; } public TextWriter.Stage alpha(float alpha) { processor.alpha = requireAlpha(alpha); return this; } public TextWriter.Stage offset(int offsetX, int offsetY) { processor.offsetX = offsetX; processor.offsetY = offsetY; return this; } public TextWriter.Stage offsetY(int offsetY) { this.processor.offsetY = offsetY; return this; } public TextWriter.Stage offsetX(int offsetX) { this.processor.offsetX = offsetX; return this; } public TextWriter.Stage rotate(double theta) { this.processor.theta = theta; return this; } } Color color = Color.DARK_GRAY; Font font = new Font("Arial", Font.BOLD, 32); float alpha = 1.0f; String text; int offsetX; int offsetY; Double theta; TextWriter() { } TextWriter(String text) { this.text = text; } TextWriter(String text, int offsetX, int offsetY, Color color, Font font, float alpha) { this.text = text; this.offsetX = offsetX; this.offsetY = offsetY; this.color = color; this.font = font; this.alpha = alpha; } @Override protected Stage createStage(BufferedImage source) { return new Stage(source, this); } @Override protected BufferedImage run() { int w = sourceWidth; int h = sourceHeight; Graphics2D g = g(); g.setColor(color); g.setFont(font); if (null != theta) { g.rotate(theta); } g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)); FontMetrics metrics = g.getFontMetrics(); int centerX = (w - metrics.stringWidth(text)) / 2; int centerY = ((h - metrics.getHeight()) / 2) + metrics.getAscent(); g.drawString(text, centerX, centerY); return target; } } public static class Concatenater extends BinarySourceProcessor { public static class Stage extends ProcessorStage { protected Stage(BufferedImage source) { super(source); this.processor = new Concatenater(); } public Stage dir(Direction dir) { this.processor.dir = requireNotNull(dir); return this; } public Stage horizontally() { this.processor.dir = Direction.HORIZONTAL; return this; } public Stage vertically() { this.processor.dir = Direction.VERTICAL; return this; } public Stage shinkToSmall() { this.processor.scaleFix = ScaleFix.SHRINK_TO_MIN; return this; } public Stage scaleToMax() { this.processor.scaleFix = ScaleFix.SCALE_TO_MAX; return this; } public Stage noScaleFix() { this.processor.scaleFix = ScaleFix.NO_FIX; return this; } public Stage scaleFix(ScaleFix scaleFix) { this.processor.scaleFix = requireNotNull(scaleFix); return this; } public Stage background(Color backgroundColor) { this.processor.background = requireNotNull(backgroundColor); return this; } public Stage reverse() { this.processor.reversed = !this.processor.reversed; return this; } public Stage with($.Func0 secondImage) { this.processor.secondSource(secondImage.apply()); return this; } public Stage appendWith($.Func0 anotherOne) { return Img.concat(this, anotherOne); } public Stage appendTo($.Func0 anotherOne) { return Img.concat(anotherOne, this); } } /** * Define the direction to concatenate two image sources * * Default value is {@link Direction#VERTICAL} */ Direction dir = Direction.HORIZONTAL; /** * Define the stategy to handle scale mismatch of two image sources * * Default value is {@link ScaleFix#SCALE_TO_MAX} */ ScaleFix scaleFix = ScaleFix.SCALE_TO_MAX; /** * The background color */ Color background = COLOR_TRANSPARENT; boolean reversed = false; private Concatenater() {} Concatenater(BufferedImage secondImage) { this.secondSource(secondImage); } Concatenater(BufferedImage secondImage, Direction dir, ScaleFix scaleFix, Color background) { this.secondSource(secondImage); this.dir = requireNotNull(dir); this.scaleFix = requireNotNull(scaleFix); this.background = requireNotNull(background); } @Override protected BufferedImage run() { if (dir.isHorizontal()) { fixScale(sourceHeight, source2Height); } else { fixScale(sourceWidth, source2Width); } N.Dimension d = dir.concatenate(N.wh(sourceWidth, sourceHeight), N.wh(source2Width, source2Height)); int w = d.w(), h = d.h(); setTargetSpec(w, h); Graphics2D g = g(); g.setColor(background); g.fillRect(0, 0, w, h); if (!reversed) { dir.drawImage(g, source, source2); } else { dir.drawImage(g, source2, source); } return target; } private void fixScale(int scale1, int scale2) { if (scale1 != scale2 && scaleFix.shouldFix()) { int targetScale = scaleFix.targetScale(scale1, scale2); float r1 = (float) targetScale / (float) scale1; float r2 = (float) targetScale / (float) scale2; if (N.neq(r1, 1.0f)) { source(new Resizer(r1).source(source).run()); } if (N.neq(r2, 1.0f)) { secondSource(new Resizer(r2).source(source2).run()); } } } } private static int randomColorValue() { return randomColorValue(true); } private static int randomColorValue(boolean withAlpha) { int a = randInt(256); int r = randInt(256); int g = randInt(256); return withAlpha ? a << 24 | r << 16 | g << 8 : a << 24 | r << 16 | g << 8; } /** * The namespace for functions */ public enum F { ; public static $.Producer RANDOM_COLOR = new $.Producer() { @Override public Color produce() { java.util.Random r = ThreadLocalRandom.current(); return new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255), r.nextInt(255)); } }; /** * A function that generates a transparent tracking pixel */ public static $.Producer TRACKING_PIXEL = new $.Producer() { @Override public BufferedImage produce() { BufferedImage trackPixel = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR); trackPixel.setRGB(0, 0, COLOR_TRANSPARENT.getRGB()); return trackPixel; } }; /** * A function that generates a transparent background in rectangular area * * @param w the width * @param h the height * @return a function as described above */ public static $.Producer background(final int w, final int h) { return background(w, h, $.val(COLOR_TRANSPARENT)); } /** * A function that generates a image with random picked pixels with random color. The * background is transparent. There are about 6 percent of pixels in the image selected * to be rendered with random color. * * @param w the width * @param h the height * @return a function as described above */ public static $.Producer randomPixels(final int w, final int h) { return randomPixels(w, h, 6); } /** * A function that generates a image with random picked pixels with random color. * The image background color is specified as `background`. There are about 6 percent * of pixels in the image selected to be rendered with random color. * * * @param w the width * @param h the height * @return a function as described above */ public static $.Producer randomPixels(final int w, final int h, Color background) { return randomPixels(w, h, 7, background); } /** * A function that generates a image with random picked pixels with random color. The * background is transparent. * * @param w the width * @param h the height * @param percent the percent of pixels selected from all pixels in the image * @return a function as described above */ public static $.Producer randomPixels(final int w, final int h, final int percent) { return randomPixels(w, h, percent, COLOR_TRANSPARENT); } /** * A function that generates a image with random picked pixels with random color. The image * use `background` color as the background * * @param w the width * @param h the height * @param percent the percent of pixels selected from all pixels in the image * @param background the background color * @return a function as described above */ public static $.Producer randomPixels(final int w, final int h, final int percent, final Color background) { return background(w, h, $.val(background)).andThen(new $.Function() { @Override public BufferedImage apply(BufferedImage img) throws NotAppliedException, Lang.Break { java.util.Random r = ThreadLocalRandom.current(); for (int i = 0; i < w; ++i) { for (int j = 0; j < h; ++j) { if (r.nextInt(100) < percent) { int v = Img.randomColorValue(false); img.setRGB(i, j, v); } } } return img; } }); } /** * A function that generates a background in rectangular area with color specified * * @param w the width * @param h the height * @param color the background color * @return a function as described above */ public static $.Producer background(final int w, final int h, final Color color) { return background(w, h, $.val(color)); } /** * A function that generates a background in rectangular area with color specified * * @param w the width * @param h the height * @param colorValueProvider the background color provider * @return a function as described above */ public static $.Producer background(final int w, final int h, final $.Func0 colorValueProvider) { $.NPE(colorValueProvider); requirePositive(w); requirePositive(h); return new $.Producer() { @Override public BufferedImage produce() { BufferedImage b = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); Graphics2D g = b.createGraphics(); g.setPaint(colorValueProvider.apply()); g.fillRect(0, 0, w, h); return b; } }; } public static $.Provider source(final InputStream is) { return new $.Provider() { @Override public BufferedImage get() { return read(is); } }; } public static $.Provider source(final File file) { return new $.Provider() { @Override public BufferedImage get() { return read(file); } }; } public static $.Val source(final BufferedImage image) { return $.F.provides(image); } } }