├── license.txt ├── make.xml ├── pom.xml ├── readme.txt └── src ├── main └── java │ └── com │ └── mortennobel │ └── imagescaling │ ├── AdvancedResizeOp.java │ ├── BSplineFilter.java │ ├── BellFilter.java │ ├── BiCubicFilter.java │ ├── BiCubicHighFreqResponse.java │ ├── BoxFilter.java │ ├── DimensionConstrain.java │ ├── HermiteFilter.java │ ├── ImageUtils.java │ ├── Lanczos3Filter.java │ ├── MitchellFilter.java │ ├── MultiStepRescaleOp.java │ ├── ProgressListener.java │ ├── ResampleFilter.java │ ├── ResampleFilters.java │ ├── ResampleOp.java │ ├── ThumbnailRescaleOp.java │ ├── TriangleFilter.java │ └── experimental │ ├── ImprovedMultistepRescaleOp.java │ └── ResampleOpSingleThread.java └── test └── java └── com └── mortennobel └── imagescaling ├── CorrectnessTest.java ├── CreateSharpenMaskTest.java ├── DimensionConstrainTest.java ├── Issue1.java ├── Issue10.java ├── Issue13.java ├── Issue18.java ├── Issue2.java ├── Issue20.java ├── Issue3.java ├── Issue5.java ├── Issue6.java ├── Issue7.java ├── Issue8.java ├── Issue9.java ├── MultipleResizeTest.java ├── MultistepRescaleOpTest.java ├── RescaleWithDimensionConstrainTest.java ├── SpeedDualThreadTest.java ├── SpeedSingleThreadTest.java ├── TestThumbnailRescaleOp.java ├── copyright.txt ├── flower.jpg ├── issue13.png ├── issue18.png ├── issue20.jpg ├── issue22.jpg ├── its-a-trap.png ├── largeimage-thump.jpg ├── largeimage.jpg ├── test.jpg └── test_issue10.jpg /license.txt: -------------------------------------------------------------------------------- 1 | Java Image Scaling 2 | 3 | Copyright (c) 2013, Morten Nobel-Joergensen 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 7 | following conditions are met: 8 | 9 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following 10 | disclaimer. 11 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided with the distribution. 13 | Neither the name of the nor the names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 16 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 20 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 21 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /make.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.mortennobel 5 | java-image-scaling 6 | java-image-scaling 7 | 0.8.7-SNAPSHOT 8 | 9 | The purpose of the library is to provide better image scaling options than the Java runtime provides. 10 | 11 | http://code.google.com/p/java-image-scaling 12 | 13 | 14 | The BSD 3-Clause License 15 | http://opensource.org/licenses/BSD-3-Clause 16 | repo 17 | 18 | 19 | 20 | Google Issues 21 | http://code.google.com/p/java-image-scaling/issues/list 22 | 23 | 24 | 25 | morten@nobel-joergensen 26 | Morten Nobel-Joergensen 27 | morten@nobel-joergensen 28 | +1 29 | 30 | 31 | 32 | 33 | Jan Moxter 34 | jan@moxter.net 35 | moxter.net S.A.C, Peru 36 | http://www.moxter.net/pe/ 37 | -6 38 | 39 | 40 | 41 | 42 | 43 | UTF-8 44 | 45 | 46 | 47 | 48 | http://java-image-scaling.googlecode.com/svn/ 49 | scm:svn:http://java-image-scaling.googlecode.com/svn/trunk 50 | scm:svn:https://java-image-scaling.googlecode.com/svn/trunk 51 | 52 | 53 | 54 | 55 | 56 | false 57 | sonatype-nexus-staging 58 | Nexus Release Repository 59 | http://oss.sonatype.org/service/local/staging/deploy/maven2/ 60 | 61 | 62 | sonatype-nexus-snapshots 63 | Sonatype Nexus Snapshots 64 | http://oss.sonatype.org/content/repositories/snapshots 65 | 66 | 67 | 68 | 71 | 72 | 75 | 76 | release-profile 77 | 78 | 79 | performRelease 80 | true 81 | 82 | 83 | 84 | 85 | 86 | 87 | org.apache.maven.plugins 88 | maven-surefire-plugin 89 | 90 | true 91 | 92 | 93 | 94 | 95 | org.apache.maven.plugins 96 | maven-source-plugin 97 | 98 | 99 | attach-sources 100 | 101 | jar 102 | 103 | 104 | 105 | 106 | 107 | 108 | true 109 | org.apache.maven.plugins 110 | maven-javadoc-plugin 111 | 112 | 113 | attach-javadocs 114 | 115 | jar 116 | 117 | 118 | 119 | 120 | 121 | 122 | true 123 | org.apache.maven.plugins 124 | maven-deploy-plugin 125 | 126 | true 127 | 128 | 129 | 130 | 131 | org.apache.maven.plugins 132 | maven-gpg-plugin 133 | 134 | 135 | sign-artifacts 136 | verify 137 | 138 | sign 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 149 | 150 | snapshot-profile 151 | 152 | 153 | performRelease 154 | true 155 | 156 | 157 | 158 | 159 | 160 | 161 | org.apache.maven.plugins 162 | maven-surefire-plugin 163 | 164 | true 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 175 | 176 | 177 | 178 | 179 | src/main/java 180 | 181 | **/*.java 182 | 183 | 184 | 185 | 186 | 187 | 188 | src/test/java 189 | 190 | **/*.java 191 | 192 | 193 | **/*.jpg 194 | **/*.png 195 | 196 | 197 | 198 | 199 | 200 | 203 | 204 | org.apache.maven.plugins 205 | maven-compiler-plugin 206 | 207 | 1.5 208 | 1.5 209 | UTF-8 210 | 211 | 212 | 215 | 216 | org.apache.maven.plugins 217 | maven-surefire-plugin 218 | 219 | 220 | **/CreateSharpenMaskTest.java 221 | **/SpeedSingleThreadTest.java 222 | **/SpeedDualThreadTest.java 223 | **/MultipleResizeTest.java 224 | **/CorrectnessTest.java 225 | **/TestThumpnailRescaleOp.java 226 | 227 | 228 | 229 | 230 | maven-release-plugin 231 | 232 | forked-path 233 | 234 | 235 | 236 | 237 | 238 | org.jvnet.wagon-svn 239 | wagon-svn 240 | 1.9 241 | 242 | 243 | 244 | 245 | 246 | 247 | com.jhlabs 248 | filters 249 | 2.0.235 250 | 251 | 252 | junit 253 | junit 254 | 4.7 255 | test 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | org.apache.maven.plugins 264 | maven-project-info-reports-plugin 265 | 266 | 267 | 268 | index 269 | summary 270 | dependencies 271 | project-team 272 | 273 | 274 | issue-tracking 275 | license 276 | scm 277 | 278 | 279 | 280 | 281 | 282 | org.apache.maven.plugins 283 | maven-javadoc-plugin 284 | 285 | 286 | org.apache.maven.plugins 287 | maven-jxr-plugin 288 | 289 | false 290 | apidocs 291 | 292 | 293 | 294 | org.apache.maven.plugins 295 | maven-surefire-report-plugin 296 | 297 | 298 | org.apache.maven.plugins 299 | maven-pmd-plugin 300 | 301 | true 302 | utf-8 303 | 1.5 304 | 305 | 306 | 307 | org.codehaus.mojo 308 | findbugs-maven-plugin 309 | 310 | 311 | 312 | 313 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | Image Scaling Library for Java 2 | -------------------------------- 3 | 4 | The purpose of the library is to provide better image scaling options 5 | than the Java runtime provides. 6 | 7 | Copyright 2013 Morten Nobel-Joergensen 8 | 9 | The library is distributed under the BSD 3-Clause License ( http://opensource.org/licenses/BSD-3-Clause ) 10 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/AdvancedResizeOp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | import com.jhlabs.image.UnsharpFilter; 10 | 11 | import java.awt.*; 12 | import java.awt.geom.Point2D; 13 | import java.awt.geom.Rectangle2D; 14 | import java.awt.image.BufferedImage; 15 | import java.awt.image.BufferedImageOp; 16 | import java.awt.image.ColorModel; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | /** 21 | * @author Morten Nobel-Joergensen 22 | */ 23 | public abstract class AdvancedResizeOp implements BufferedImageOp { 24 | public static enum UnsharpenMask{ 25 | None(0), 26 | Soft(0.15f), 27 | Normal(0.3f), 28 | VerySharp(0.45f), 29 | Oversharpened(0.60f); 30 | private final float factor; 31 | 32 | UnsharpenMask(float factor) { 33 | this.factor = factor; 34 | } 35 | } 36 | private List listeners = new ArrayList(); 37 | 38 | private final DimensionConstrain dimensionConstrain; 39 | private UnsharpenMask unsharpenMask = UnsharpenMask.None; 40 | 41 | public AdvancedResizeOp(DimensionConstrain dimensionConstrain) { 42 | this.dimensionConstrain = dimensionConstrain; 43 | } 44 | 45 | public UnsharpenMask getUnsharpenMask() { 46 | return unsharpenMask; 47 | } 48 | 49 | public void setUnsharpenMask(UnsharpenMask unsharpenMask) { 50 | this.unsharpenMask = unsharpenMask; 51 | } 52 | 53 | protected void fireProgressChanged(float fraction){ 54 | for (ProgressListener progressListener:listeners){ 55 | progressListener.notifyProgress(fraction); 56 | } 57 | } 58 | 59 | public final void addProgressListener(ProgressListener progressListener) { 60 | listeners.add(progressListener); 61 | } 62 | 63 | public final boolean removeProgressListener(ProgressListener progressListener) { 64 | return listeners.remove(progressListener); 65 | } 66 | 67 | public final BufferedImage filter(BufferedImage src, BufferedImage dest){ 68 | Dimension dstDimension = dimensionConstrain.getDimension(new Dimension(src.getWidth(),src.getHeight())); 69 | int dstWidth = dstDimension.width; 70 | int dstHeight = dstDimension.height; 71 | BufferedImage bufferedImage = doFilter(src, dest, dstWidth, dstHeight); 72 | 73 | if (unsharpenMask!= UnsharpenMask.None){ 74 | UnsharpFilter unsharpFilter= new UnsharpFilter(); 75 | unsharpFilter.setRadius(2f); 76 | unsharpFilter.setAmount(unsharpenMask.factor); 77 | unsharpFilter.setThreshold(10); 78 | return unsharpFilter.filter(bufferedImage, null); 79 | } 80 | 81 | return bufferedImage; 82 | } 83 | 84 | protected abstract BufferedImage doFilter(BufferedImage src, BufferedImage dest, int dstWidth, int dstHeight); 85 | 86 | /** 87 | * {@inheritDoc} 88 | */ 89 | public final Rectangle2D getBounds2D(BufferedImage src) { 90 | return new Rectangle(0, 0, src.getWidth(), src.getHeight()); 91 | } 92 | 93 | /** 94 | * {@inheritDoc} 95 | */ 96 | public final BufferedImage createCompatibleDestImage(BufferedImage src, 97 | ColorModel destCM) { 98 | if (destCM == null) { 99 | destCM = src.getColorModel(); 100 | } 101 | return new BufferedImage(destCM, 102 | destCM.createCompatibleWritableRaster( 103 | src.getWidth(), src.getHeight()), 104 | destCM.isAlphaPremultiplied(), null); 105 | } 106 | 107 | /** 108 | * {@inheritDoc} 109 | */ 110 | public final Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { 111 | return (Point2D) srcPt.clone(); 112 | } 113 | 114 | /** 115 | * {@inheritDoc} 116 | */ 117 | public final RenderingHints getRenderingHints() { 118 | return null; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/BSplineFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | 10 | /** 11 | * A B-spline resample filter. 12 | */ 13 | final class BSplineFilter implements ResampleFilter 14 | { 15 | public float getSamplingRadius() { 16 | return 2.0f; 17 | } 18 | 19 | public final float apply(float value) 20 | { 21 | if (value < 0.0f) 22 | { 23 | value = - value; 24 | } 25 | if (value < 1.0f) 26 | { 27 | float tt = value * value; 28 | return 0.5f * tt * value - tt + (2.0f / 3.0f); 29 | } 30 | else 31 | if (value < 2.0f) 32 | { 33 | value = 2.0f - value; 34 | return (1.0f / 6.0f) * value * value * value; 35 | } 36 | else 37 | { 38 | return 0.0f; 39 | } 40 | } 41 | 42 | public String getName() { 43 | return "BSpline"; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/BellFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | /** 10 | * A Bell resample filter. 11 | */ 12 | final class BellFilter implements ResampleFilter 13 | { 14 | public float getSamplingRadius() { 15 | return 1.5f; 16 | } 17 | 18 | public final float apply(float value) 19 | { 20 | if (value < 0.0f) 21 | { 22 | value = - value; 23 | } 24 | if (value < 0.5f) 25 | { 26 | return 0.75f - (value * value); 27 | } 28 | else 29 | if (value < 1.5f) 30 | { 31 | value = value - 1.5f; 32 | return 0.5f * (value * value); 33 | } 34 | else 35 | { 36 | return 0.0f; 37 | } 38 | } 39 | 40 | public String getName() { 41 | return "Bell"; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/BiCubicFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | 10 | /** 11 | * @author Heinz Doerr 12 | */ 13 | class BiCubicFilter implements ResampleFilter { 14 | 15 | final protected float a; 16 | 17 | public BiCubicFilter() { 18 | a= -0.5f; 19 | } 20 | 21 | protected BiCubicFilter(float a) { 22 | this.a= a; 23 | } 24 | 25 | public final float apply(float value) { 26 | if (value == 0) 27 | return 1.0f; 28 | if (value < 0.0f) 29 | value = -value; 30 | float vv= value * value; 31 | if (value < 1.0f) { 32 | return (a + 2f) * vv * value - (a + 3f) * vv + 1f; 33 | } 34 | if (value < 2.0f) { 35 | return a * vv * value - 5 * a * vv + 8 * a * value - 4 * a; 36 | } 37 | return 0.0f; 38 | } 39 | 40 | public float getSamplingRadius() { 41 | return 2.0f; 42 | } 43 | 44 | public String getName() 45 | { 46 | return "BiCubic"; // also called cardinal cubic spline 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/BiCubicHighFreqResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | /** 10 | * @author Heinz Doerr 11 | */ 12 | final class BiCubicHighFreqResponse extends BiCubicFilter { 13 | 14 | public BiCubicHighFreqResponse() { 15 | super(-1.f); 16 | } 17 | 18 | @Override 19 | public String getName() { 20 | return "BiCubicHighFreqResponse"; 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/BoxFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | /** 10 | * A box filter (also known as nearest neighbor). 11 | */ 12 | final class BoxFilter implements ResampleFilter 13 | { 14 | public float getSamplingRadius() { 15 | return 0.5f; 16 | } 17 | 18 | public final float apply(float value) 19 | { 20 | if (value > -0.5f && value <= 0.5f) 21 | { 22 | return 1.0f; 23 | } 24 | else 25 | { 26 | return 0.0f; 27 | } 28 | } 29 | 30 | public String getName() { 31 | return "Box"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/DimensionConstrain.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | import java.awt.*; 10 | 11 | /** 12 | * This class let you create dimension constrains based on a actual image. 13 | * 14 | * Class may be subclassed to create user defined behavior. To do this you need to overwrite 15 | * the method getDimension(Dimension). 16 | */ 17 | public class DimensionConstrain { 18 | protected DimensionConstrain () 19 | { 20 | } 21 | 22 | /** 23 | * Will always return a dimension with positive width and height; 24 | * @param dimension of the unscaled image 25 | * @return the dimension of the scaled image 26 | */ 27 | public Dimension getDimension(Dimension dimension){ 28 | return dimension; 29 | } 30 | 31 | /** 32 | * Used when the destination size is fixed. This may not keep the image aspect radio 33 | * @param width destination dimension width 34 | * @param height destination dimension height 35 | * @return destination dimension (width x height) 36 | */ 37 | public static DimensionConstrain createAbsolutionDimension(final int width, final int height){ 38 | assert width>0 && height>0:"Dimension must be a positive integer"; 39 | return new DimensionConstrain(){ 40 | public Dimension getDimension(Dimension dimension) { 41 | return new Dimension(width, height); 42 | } 43 | }; 44 | } 45 | 46 | /** 47 | * Used when the destination size is relative to the source. This keeps the image aspect radio 48 | * @param fraction resize fraction (must be a positive number) 49 | * @return the new dimension (the input dimension times the fraction) 50 | */ 51 | public static DimensionConstrain createRelativeDimension(final float fraction){ 52 | return createRelativeDimension(fraction,fraction); 53 | } 54 | 55 | /** 56 | * Used when the destination size is relative to the source. This keeps the image aspect radio if fractionWidth 57 | * equals fractionHeight 58 | * @param 59 | * @return 60 | */ 61 | public static DimensionConstrain createRelativeDimension(final float fractionWidth, final float fractionHeight){ 62 | assert fractionHeight>0 && fractionWidth>0:"Fractions must be larger than 0.0"; 63 | return new DimensionConstrain(){ 64 | public Dimension getDimension(Dimension dimension) { 65 | int width = Math.max(1,Math.round(fractionWidth*dimension.width)); 66 | int height = Math.max(1,Math.round(fractionHeight*dimension.height)); 67 | return new Dimension(width, height); 68 | } 69 | }; 70 | } 71 | 72 | /** 73 | * Forces the image to keep radio and be keeped within the width and height 74 | * @param width 75 | * @param height 76 | * @return 77 | */ 78 | public static DimensionConstrain createMaxDimension(int width, int height){ 79 | return createMaxDimension(width, height,false); 80 | } 81 | 82 | /** 83 | * Forces the image to keep radio and be keeped within the width and height. 84 | * @param width 85 | * @param height 86 | * @param neverEnlargeImage if true only a downscale will occour 87 | * @return 88 | */ 89 | public static DimensionConstrain createMaxDimension(final int width, final int height, final boolean neverEnlargeImage){ 90 | assert width >0 && height > 0 : "Dimension must be larger that 0"; 91 | final double scaleFactor = width/(double)height; 92 | return new DimensionConstrain(){ 93 | public Dimension getDimension(Dimension dimension) { 94 | double srcScaleFactor = dimension.width/(double)dimension.height; 95 | double scale; 96 | if (srcScaleFactor>scaleFactor){ 97 | scale = width/(double)dimension.width; 98 | } 99 | else{ 100 | scale = height/(double)dimension.height; 101 | } 102 | if (neverEnlargeImage){ 103 | scale = Math.min(scale,1); 104 | } 105 | int dstWidth = (int)Math.round (dimension.width*scale); 106 | int dstHeight = (int) Math.round(dimension.height*scale); 107 | return new Dimension(dstWidth, dstHeight); 108 | } 109 | }; 110 | } 111 | 112 | /** 113 | * Forces the image to keep radio and be keeped within the width and height. Width and height are defined 114 | * (length1 x length2) or (length2 x length1). 115 | * 116 | * This is usefull is the scaling allow a certain format (such as 16x9") but allow the dimension to be rotated 90 117 | * degrees (so also 9x16" is allowed). 118 | * 119 | * @param length1 120 | * @param length2 121 | * @return 122 | */ 123 | public static DimensionConstrain createMaxDimensionNoOrientation(int length1, int length2){ 124 | return createMaxDimensionNoOrientation(length1, length2,false); 125 | } 126 | 127 | /** 128 | * Forces the image to keep radio and be keeped within the width and height. Width and height are defined 129 | * (length1 x length2) or (length2 x length1). 130 | * 131 | * This is usefull is the scaling allow a certain format (such as 16x9") but allow the dimension to be rotated 90 132 | * degrees (so also 9x16" is allowed). 133 | * 134 | * @param length1 135 | * @param length2 136 | * @param neverEnlargeImage if true only a downscale will occour 137 | * @return 138 | */ 139 | public static DimensionConstrain createMaxDimensionNoOrientation(final int length1, final int length2, final boolean neverEnlargeImage){ 140 | assert length1 >0 && length2 > 0 : "Dimension must be larger that 0"; 141 | final double scaleFactor = length1/(double)length2; 142 | return new DimensionConstrain(){ 143 | public Dimension getDimension(Dimension dimension) { 144 | double srcScaleFactor = dimension.width/(double)dimension.height; 145 | int width; 146 | int height; 147 | // swap length1 and length2 148 | if (srcScaleFactor>scaleFactor){ 149 | width = length1; 150 | height = length2; 151 | } 152 | else{ 153 | width = length2; 154 | height = length1; 155 | } 156 | 157 | 158 | final double scaleFactor = width/(double)height; 159 | double scale; 160 | if (srcScaleFactor>scaleFactor){ 161 | scale = width/(double)dimension.width; 162 | } 163 | else{ 164 | scale = height/(double)dimension.height; 165 | } 166 | if (neverEnlargeImage){ 167 | scale = Math.min(scale,1); 168 | } 169 | int dstWidth = (int)Math.round (dimension.width*scale); 170 | int dstHeight = (int) Math.round(dimension.height*scale); 171 | return new Dimension(dstWidth, dstHeight); 172 | } 173 | }; 174 | } 175 | 176 | 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/HermiteFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | /** 10 | * A Hermite resampling filter. 11 | */ 12 | class HermiteFilter implements ResampleFilter 13 | { 14 | public float getSamplingRadius() { 15 | return 1.0f; 16 | } 17 | 18 | public float apply(float value) 19 | { 20 | if (value < 0.0f) 21 | { 22 | value = - value; 23 | } 24 | if (value < 1.0f) 25 | { 26 | return (2.0f * value - 3.0f) * value * value + 1.0f; 27 | } 28 | else 29 | { 30 | return 0.0f; 31 | } 32 | } 33 | 34 | public String getName() { 35 | return "BSpline"; 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/ImageUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | import javax.imageio.IIOImage; 10 | import javax.imageio.ImageIO; 11 | import javax.imageio.ImageReader; 12 | import javax.imageio.ImageWriter; 13 | import javax.imageio.metadata.IIOMetadata; 14 | import javax.imageio.stream.ImageInputStream; 15 | import javax.imageio.stream.ImageOutputStream; 16 | import javax.imageio.stream.MemoryCacheImageInputStream; 17 | import javax.imageio.stream.MemoryCacheImageOutputStream; 18 | import java.awt.Graphics2D; 19 | import java.awt.image.BufferedImage; 20 | import java.awt.image.Raster; 21 | import java.awt.image.WritableRaster; 22 | import java.io.*; 23 | import java.util.Iterator; 24 | 25 | /** 26 | * @author Heinz Doerr 27 | * @author Morten Nobel-Joergensen 28 | */ 29 | public class ImageUtils { 30 | 31 | 32 | static public String imageTypeName(BufferedImage img) { 33 | switch (img.getType()) { 34 | case BufferedImage.TYPE_3BYTE_BGR: return "TYPE_3BYTE_BGR"; 35 | case BufferedImage.TYPE_4BYTE_ABGR: return "TYPE_4BYTE_ABGR"; 36 | case BufferedImage.TYPE_4BYTE_ABGR_PRE: return "TYPE_4BYTE_ABGR_PRE"; 37 | case BufferedImage.TYPE_BYTE_BINARY: return "TYPE_BYTE_BINARY"; 38 | case BufferedImage.TYPE_BYTE_GRAY: return "TYPE_BYTE_GRAY"; 39 | case BufferedImage.TYPE_BYTE_INDEXED: return "TYPE_BYTE_INDEXED"; 40 | case BufferedImage.TYPE_CUSTOM: return "TYPE_CUSTOM"; 41 | case BufferedImage.TYPE_INT_ARGB: return "TYPE_INT_ARGB"; 42 | case BufferedImage.TYPE_INT_ARGB_PRE: return "TYPE_INT_ARGB_PRE"; 43 | case BufferedImage.TYPE_INT_BGR: return "TYPE_INT_BGR"; 44 | case BufferedImage.TYPE_INT_RGB: return "TYPE_INT_RGB"; 45 | case BufferedImage.TYPE_USHORT_555_RGB: return "TYPE_USHORT_555_RGB"; 46 | case BufferedImage.TYPE_USHORT_565_RGB: return "TYPE_USHORT_565_RGB"; 47 | case BufferedImage.TYPE_USHORT_GRAY: return "TYPE_USHORT_GRAY"; 48 | } 49 | return "unknown image type #" + img.getType(); 50 | } 51 | 52 | static public int nrChannels(BufferedImage img) { 53 | switch (img.getType()) { 54 | case BufferedImage.TYPE_3BYTE_BGR: return 3; 55 | case BufferedImage.TYPE_4BYTE_ABGR: return 4; 56 | case BufferedImage.TYPE_BYTE_GRAY: return 1; 57 | case BufferedImage.TYPE_INT_BGR: return 3; 58 | case BufferedImage.TYPE_INT_ARGB: return 4; 59 | case BufferedImage.TYPE_INT_RGB: return 3; 60 | case BufferedImage.TYPE_CUSTOM: return 4; 61 | case BufferedImage.TYPE_4BYTE_ABGR_PRE: return 4; 62 | case BufferedImage.TYPE_INT_ARGB_PRE: return 4; 63 | case BufferedImage.TYPE_USHORT_555_RGB: return 3; 64 | case BufferedImage.TYPE_USHORT_565_RGB: return 3; 65 | case BufferedImage.TYPE_USHORT_GRAY: return 1; 66 | } 67 | return 0; 68 | } 69 | 70 | 71 | 72 | /** 73 | * 74 | * returns one row (height == 1) of byte packed image data in BGR or AGBR form 75 | * 76 | * @param img 77 | * @param y 78 | * @param w 79 | * @param array 80 | * @param temp must be either null or a array with length of w*h 81 | * @return 82 | */ 83 | public static byte[] getPixelsBGR(BufferedImage img, int y, int w, byte[] array, int[] temp) { 84 | final int x= 0; 85 | final int h= 1; 86 | 87 | assert array.length == temp.length * nrChannels(img); 88 | assert (temp.length == w); 89 | 90 | int imageType= img.getType(); 91 | Raster raster; 92 | switch (imageType) { 93 | case BufferedImage.TYPE_3BYTE_BGR: 94 | case BufferedImage.TYPE_4BYTE_ABGR: 95 | case BufferedImage.TYPE_4BYTE_ABGR_PRE: 96 | case BufferedImage.TYPE_BYTE_GRAY: 97 | raster= img.getRaster(); 98 | //int ttype= raster.getTransferType(); 99 | raster.getDataElements(x, y, w, h, array); 100 | break; 101 | case BufferedImage.TYPE_INT_BGR: 102 | raster= img.getRaster(); 103 | raster.getDataElements(x, y, w, h, temp); 104 | ints2bytes(temp, array, 0, 1, 2); // bgr --> bgr 105 | break; 106 | case BufferedImage.TYPE_INT_RGB: 107 | raster= img.getRaster(); 108 | raster.getDataElements(x, y, w, h, temp); 109 | ints2bytes(temp, array, 2, 1, 0); // rgb --> bgr 110 | break; 111 | case BufferedImage.TYPE_INT_ARGB: 112 | case BufferedImage.TYPE_INT_ARGB_PRE: 113 | raster= img.getRaster(); 114 | raster.getDataElements(x, y, w, h, temp); 115 | ints2bytes(temp, array, 2, 1, 0, 3); // argb --> abgr 116 | break; 117 | case BufferedImage.TYPE_CUSTOM: // TODO: works for my icon image loader, but else ??? 118 | img.getRGB(x, y, w, h, temp, 0, w); 119 | ints2bytes(temp, array, 2, 1, 0, 3); // argb --> abgr 120 | break; 121 | default: 122 | img.getRGB(x, y, w, h, temp, 0, w); 123 | ints2bytes(temp, array, 2, 1, 0); // rgb --> bgr 124 | break; 125 | } 126 | 127 | return array; 128 | } 129 | 130 | /** 131 | * converts and copies byte packed BGR or ABGR into the img buffer, 132 | * the img type may vary (e.g. RGB or BGR, int or byte packed) 133 | * but the number of components (w/o alpha, w alpha, gray) must match 134 | * 135 | * does not unmange the image for all (A)RGN and (A)BGR and gray imaged 136 | * 137 | */ 138 | public static void setBGRPixels(byte[] bgrPixels, BufferedImage img, int x, int y, int w, int h) { 139 | int imageType= img.getType(); 140 | WritableRaster raster= img.getRaster(); 141 | //int ttype= raster.getTransferType(); 142 | if (imageType == BufferedImage.TYPE_3BYTE_BGR || 143 | imageType == BufferedImage.TYPE_4BYTE_ABGR || 144 | imageType == BufferedImage.TYPE_4BYTE_ABGR_PRE || 145 | imageType == BufferedImage.TYPE_BYTE_GRAY) { 146 | raster.setDataElements(x, y, w, h, bgrPixels); 147 | } else { 148 | int[] pixels; 149 | if (imageType == BufferedImage.TYPE_INT_BGR) { 150 | pixels= bytes2int(bgrPixels, 2, 1, 0); // bgr --> bgr 151 | } else if (imageType == BufferedImage.TYPE_INT_ARGB || 152 | imageType == BufferedImage.TYPE_INT_ARGB_PRE) { 153 | pixels= bytes2int(bgrPixels, 3, 0, 1, 2); // abgr --> argb 154 | } else { 155 | pixels= bytes2int(bgrPixels, 0, 1, 2); // bgr --> rgb 156 | } 157 | if (w == 0 || h == 0) { 158 | return; 159 | } else if (pixels.length < w * h) { 160 | throw new IllegalArgumentException("pixels array must have a length" + " >= w*h"); 161 | } 162 | if (imageType == BufferedImage.TYPE_INT_ARGB || 163 | imageType == BufferedImage.TYPE_INT_RGB || 164 | imageType == BufferedImage.TYPE_INT_ARGB_PRE || 165 | imageType == BufferedImage.TYPE_INT_BGR) { 166 | raster.setDataElements(x, y, w, h, pixels); 167 | } else { 168 | // Unmanages the image 169 | img.setRGB(x, y, w, h, pixels, 0, w); 170 | } 171 | } 172 | } 173 | 174 | 175 | public static void ints2bytes(int[] in, byte[] out, int index1, int index2, int index3) { 176 | for (int i= 0; i < in.length; i++) { 177 | int index= i * 3; 178 | int value= in[i]; 179 | out[index + index1]= (byte)value; 180 | value= value >> 8; 181 | out[index + index2]= (byte)value; 182 | value= value >> 8; 183 | out[index + index3]= (byte)value; 184 | } 185 | } 186 | 187 | public static void ints2bytes(int[] in, byte[] out, int index1, int index2, int index3, int index4) { 188 | for (int i= 0; i < in.length; i++) { 189 | int index= i * 4; 190 | int value= in[i]; 191 | out[index + index1]= (byte)value; 192 | value= value >> 8; 193 | out[index + index2]= (byte)value; 194 | value= value >> 8; 195 | out[index + index3]= (byte)value; 196 | value= value >> 8; 197 | out[index + index4]= (byte)value; 198 | } 199 | } 200 | 201 | public static int[] bytes2int(byte[] in, int index1, int index2, int index3) { 202 | int[] out= new int[in.length / 3]; 203 | for (int i= 0; i < out.length; i++) { 204 | int index= i * 3; 205 | int b1= (in[index +index1] & 0xff) << 16; 206 | int b2= (in[index + index2] & 0xff) << 8; 207 | int b3= in[index + index3] & 0xff; 208 | out[i]= b1 | b2 | b3; 209 | } 210 | return out; 211 | } 212 | 213 | public static int[] bytes2int(byte[] in, int index1, int index2, int index3, int index4) { 214 | int[] out= new int[in.length / 4]; 215 | for (int i= 0; i < out.length; i++) { 216 | int index= i * 4; 217 | int b1= (in[index +index1] & 0xff) << 24; 218 | int b2= (in[index +index2] & 0xff) << 16; 219 | int b3= (in[index + index3] & 0xff) << 8; 220 | int b4= in[index + index4] & 0xff; 221 | out[i]= b1 | b2 | b3 | b4; 222 | } 223 | return out; 224 | } 225 | 226 | public static BufferedImage convert(BufferedImage src, int bufImgType) { 227 | BufferedImage img= new BufferedImage(src.getWidth(), src.getHeight(), bufImgType); 228 | Graphics2D g2d= img.createGraphics(); 229 | g2d.drawImage(src, 0, 0, null); 230 | g2d.dispose(); 231 | return img; 232 | } 233 | 234 | /** 235 | * Copy jpeg meta data (exif) from source to dest and save it to out. 236 | * 237 | * @param source 238 | * @param dest 239 | * @return result 240 | * @throws IOException 241 | */ 242 | public static byte[] copyJpegMetaData(byte[] source, byte[] dest) throws IOException { 243 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 244 | ImageOutputStream out = new MemoryCacheImageOutputStream(baos); 245 | copyJpegMetaData(new ByteArrayInputStream(source),new ByteArrayInputStream(dest), out); 246 | return baos.toByteArray(); 247 | } 248 | 249 | /** 250 | * Copy jpeg meta data (exif) from source to dest and save it to out 251 | * 252 | * @param source 253 | * @param dest 254 | * @param out 255 | * @throws IOException 256 | */ 257 | public static void copyJpegMetaData(InputStream source, InputStream dest, ImageOutputStream out) throws IOException { 258 | // Read meta data from src image 259 | Iterator iter = ImageIO.getImageReadersByFormatName("jpeg"); 260 | ImageReader reader=(ImageReader) iter.next(); 261 | ImageInputStream iis = new MemoryCacheImageInputStream(source); 262 | reader.setInput(iis); 263 | IIOMetadata metadata = reader.getImageMetadata(0); 264 | iis.close(); 265 | // Read dest image 266 | ImageInputStream outIis = new MemoryCacheImageInputStream(dest); 267 | reader.setInput(outIis); 268 | IIOImage image = reader.readAll(0,null); 269 | image.setMetadata(metadata); 270 | outIis.close(); 271 | // write dest image 272 | iter = ImageIO.getImageWritersByFormatName("jpeg"); 273 | ImageWriter writer=(ImageWriter) iter.next(); 274 | writer.setOutput(out); 275 | writer.write(image); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/Lanczos3Filter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | final class Lanczos3Filter implements ResampleFilter { 10 | private final static float PI_FLOAT = (float) Math.PI; 11 | 12 | private float sincModified(float value) 13 | { 14 | return ((float)Math.sin(value)) / value; 15 | } 16 | 17 | public final float apply(float value) 18 | { 19 | if (value==0){ 20 | return 1.0f; 21 | } 22 | if (value < 0.0f) 23 | { 24 | value = -value; 25 | } 26 | 27 | if (value < 3.0f) 28 | { 29 | value *= PI_FLOAT; 30 | return sincModified(value) * sincModified(value / 3.0f); 31 | } 32 | else 33 | { 34 | return 0.0f; 35 | } 36 | } 37 | 38 | public float getSamplingRadius() { 39 | return 3.0f; 40 | } 41 | 42 | public String getName() 43 | { 44 | return "Lanczos3"; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/MitchellFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | /** 10 | * The Mitchell resample filter. 11 | */ 12 | final class MitchellFilter implements ResampleFilter 13 | { 14 | private static final float B = 1.0f / 3.0f; 15 | private static final float C = 1.0f / 3.0f; 16 | 17 | public float getSamplingRadius() { 18 | return 2.0f; 19 | } 20 | 21 | public final float apply(float value) 22 | { 23 | if (value < 0.0f) 24 | { 25 | value = -value; 26 | } 27 | float tt = value * value; 28 | if (value < 1.0f) 29 | { 30 | value = (((12.0f - 9.0f * B - 6.0f * C) * (value * tt)) 31 | + ((-18.0f + 12.0f * B + 6.0f * C) * tt) 32 | + (6.0f - 2f * B)); 33 | return value / 6.0f; 34 | } 35 | else 36 | if (value < 2.0f) 37 | { 38 | value = (((-1.0f * B - 6.0f * C) * (value * tt)) 39 | + ((6.0f * B + 30.0f * C) * tt) 40 | + ((-12.0f * B - 48.0f * C) * value) 41 | + (8.0f * B + 24 * C)); 42 | return value / 6.0f; 43 | } 44 | else 45 | { 46 | return 0.0f; 47 | } 48 | } 49 | 50 | public String getName() { 51 | return "BSpline"; 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/MultiStepRescaleOp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | import java.awt.*; 10 | import java.awt.image.BufferedImage; 11 | 12 | /** 13 | * This code is very inspired on Chris Campbells article "The Perils of Image.getScaledInstance()" 14 | * 15 | * The article can be found here: 16 | * http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html 17 | * 18 | * Note that the filter method is threadsafe 19 | */ 20 | public class MultiStepRescaleOp extends AdvancedResizeOp { 21 | private final Object renderingHintInterpolation; 22 | 23 | public MultiStepRescaleOp(int dstWidth, int dstHeight) { 24 | this (dstWidth, dstHeight, RenderingHints.VALUE_INTERPOLATION_BILINEAR); 25 | } 26 | 27 | public MultiStepRescaleOp(int dstWidth, int dstHeight, Object renderingHintInterpolation) { 28 | this(DimensionConstrain.createAbsolutionDimension(dstWidth, dstHeight), renderingHintInterpolation); 29 | } 30 | 31 | public MultiStepRescaleOp(DimensionConstrain dimensionConstain) { 32 | this (dimensionConstain, RenderingHints.VALUE_INTERPOLATION_BILINEAR); 33 | } 34 | 35 | public MultiStepRescaleOp(DimensionConstrain dimensionConstain, Object renderingHintInterpolation) { 36 | super(dimensionConstain); 37 | this.renderingHintInterpolation = renderingHintInterpolation; 38 | assert RenderingHints.KEY_INTERPOLATION.isCompatibleValue(renderingHintInterpolation) : 39 | "Rendering hint "+renderingHintInterpolation+" is not compatible with interpolation"; 40 | } 41 | 42 | 43 | public BufferedImage doFilter(BufferedImage img, BufferedImage dest, int dstWidth, int dstHeight) { 44 | int type = (img.getTransparency() == Transparency.OPAQUE) ? 45 | BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; 46 | BufferedImage ret = img; 47 | int w, h; 48 | 49 | // Use multi-step technique: start with original size, then 50 | // scale down in multiple passes with drawImage() 51 | // until the target size is reached 52 | w = img.getWidth(); 53 | h = img.getHeight(); 54 | 55 | do { 56 | if (w > dstWidth) { 57 | w /= 2; 58 | if (w < dstWidth) { 59 | w = dstWidth; 60 | } 61 | } else { 62 | w = dstWidth; 63 | } 64 | 65 | if (h > dstHeight) { 66 | h /= 2; 67 | if (h < dstHeight) { 68 | h = dstHeight; 69 | } 70 | } else { 71 | h = dstHeight; 72 | } 73 | 74 | BufferedImage tmp; 75 | if (dest!=null && dest.getWidth()== w && dest.getHeight()== h && w==dstWidth && h==dstHeight){ 76 | tmp = dest; 77 | } else { 78 | tmp = new BufferedImage(w,h,type); 79 | } 80 | Graphics2D g2 = tmp.createGraphics(); 81 | g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, renderingHintInterpolation); 82 | g2.drawImage(ret, 0, 0, w, h, null); 83 | g2.dispose(); 84 | 85 | ret = tmp; 86 | } while (w != dstWidth || h != dstHeight); 87 | 88 | return ret; 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/ProgressListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | public interface ProgressListener { 10 | public void notifyProgress(float fraction); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/ResampleFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | public interface ResampleFilter { 10 | public float getSamplingRadius(); 11 | 12 | float apply(float v); 13 | 14 | public abstract String getName(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/ResampleFilters.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | public class ResampleFilters { 10 | private static BellFilter bellFilter = new BellFilter(); 11 | private static BiCubicFilter biCubicFilter = new BiCubicFilter(); 12 | private static BiCubicHighFreqResponse biCubicHighFreqResponse = new BiCubicHighFreqResponse(); 13 | private static BoxFilter boxFilter = new BoxFilter(); 14 | private static BSplineFilter bSplineFilter = new BSplineFilter(); 15 | private static HermiteFilter hermiteFilter = new HermiteFilter(); 16 | private static Lanczos3Filter lanczos3Filter = new Lanczos3Filter(); 17 | private static MitchellFilter mitchellFilter = new MitchellFilter(); 18 | private static TriangleFilter triangleFilter = new TriangleFilter(); 19 | 20 | public static ResampleFilter getBellFilter(){ 21 | return bellFilter; 22 | } 23 | 24 | public static ResampleFilter getBiCubicFilter(){ 25 | return biCubicFilter; 26 | } 27 | 28 | public static ResampleFilter getBiCubicHighFreqResponse(){ 29 | return biCubicHighFreqResponse; 30 | } 31 | 32 | public static ResampleFilter getBoxFilter(){ 33 | return boxFilter; 34 | } 35 | 36 | public static ResampleFilter getBSplineFilter(){ 37 | return bSplineFilter; 38 | } 39 | 40 | public static ResampleFilter getHermiteFilter(){ 41 | return hermiteFilter; 42 | } 43 | 44 | public static ResampleFilter getLanczos3Filter(){ 45 | return lanczos3Filter; 46 | } 47 | 48 | public static ResampleFilter getMitchellFilter(){ 49 | return mitchellFilter; 50 | } 51 | 52 | public static ResampleFilter getTriangleFilter(){ 53 | return triangleFilter; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/ResampleOp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | import java.awt.image.BufferedImage; 10 | import java.awt.image.DataBuffer; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | 13 | /** 14 | * Based on work from Java Image Util ( http://schmidt.devlib.org/jiu/ ) 15 | * 16 | * Note that the filter method is not thread safe 17 | * 18 | * @author Morten Nobel-Joergensen 19 | * @author Heinz Doerr 20 | */ 21 | public class ResampleOp extends AdvancedResizeOp 22 | { 23 | private final int MAX_CHANNEL_VALUE= 255; 24 | 25 | private int nrChannels; 26 | private int srcWidth; 27 | private int srcHeight; 28 | private int dstWidth; 29 | private int dstHeight; 30 | 31 | static class SubSamplingData{ 32 | private final int[] arrN; // individual - per row or per column - nr of contributions 33 | private final int[] arrPixel; // 2Dim: [wid or hei][contrib] 34 | private final float[] arrWeight; // 2Dim: [wid or hei][contrib] 35 | private final int numContributors; // the primary index length for the 2Dim arrays : arrPixel and arrWeight 36 | 37 | private SubSamplingData(int[] arrN, int[] arrPixel, float[] arrWeight, int numContributors) { 38 | this.arrN = arrN; 39 | this.arrPixel = arrPixel; 40 | this.arrWeight = arrWeight; 41 | this.numContributors = numContributors; 42 | } 43 | 44 | 45 | public int getNumContributors() { 46 | return numContributors; 47 | } 48 | 49 | public int[] getArrN() { 50 | return arrN; 51 | } 52 | 53 | public int[] getArrPixel() { 54 | return arrPixel; 55 | } 56 | 57 | public float[] getArrWeight() { 58 | return arrWeight; 59 | } 60 | } 61 | 62 | private SubSamplingData horizontalSubsamplingData; 63 | private SubSamplingData verticalSubsamplingData; 64 | 65 | private int processedItems; 66 | private float totalItems; 67 | 68 | private int numberOfThreads = Runtime.getRuntime().availableProcessors(); 69 | 70 | private AtomicInteger multipleInvocationLock = new AtomicInteger(); 71 | 72 | private ResampleFilter filter = ResampleFilters.getLanczos3Filter(); 73 | 74 | 75 | public ResampleOp(int destWidth, int destHeight) { 76 | this(DimensionConstrain.createAbsolutionDimension(destWidth, destHeight)); 77 | } 78 | 79 | public ResampleOp(DimensionConstrain dimensionConstrain) { 80 | super(dimensionConstrain); 81 | } 82 | 83 | public ResampleFilter getFilter() { 84 | return filter; 85 | } 86 | 87 | public void setFilter(ResampleFilter filter) { 88 | this.filter = filter; 89 | } 90 | 91 | public int getNumberOfThreads() { 92 | return numberOfThreads; 93 | } 94 | 95 | public void setNumberOfThreads(int numberOfThreads) { 96 | this.numberOfThreads = numberOfThreads; 97 | } 98 | 99 | public BufferedImage doFilter(BufferedImage srcImg, BufferedImage dest, int dstWidth, int dstHeight) { 100 | this.dstWidth = dstWidth; 101 | this.dstHeight = dstHeight; 102 | 103 | if (dstWidth<3 || dstHeight<3){ 104 | throw new RuntimeException("Error doing rescale. Target size was "+dstWidth+"x"+dstHeight+" but must be at least 3x3."); 105 | } 106 | 107 | assert multipleInvocationLock.incrementAndGet()==1:"Multiple concurrent invocations detected"; 108 | 109 | if (srcImg.getType() == BufferedImage.TYPE_BYTE_BINARY || 110 | srcImg.getType() == BufferedImage.TYPE_BYTE_INDEXED || 111 | srcImg.getType() == BufferedImage.TYPE_CUSTOM) 112 | srcImg = ImageUtils.convert(srcImg, srcImg.getColorModel().hasAlpha() ? 113 | BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR); 114 | 115 | this.nrChannels= ImageUtils.nrChannels(srcImg); 116 | assert nrChannels > 0; 117 | this.srcWidth = srcImg.getWidth(); 118 | this.srcHeight = srcImg.getHeight(); 119 | 120 | byte[][] workPixels = new byte[srcHeight][dstWidth*nrChannels]; 121 | 122 | this.processedItems = 0; 123 | this.totalItems = srcHeight + dstWidth; 124 | 125 | // Pre-calculate sub-sampling 126 | horizontalSubsamplingData = createSubSampling(filter, srcWidth, dstWidth); 127 | verticalSubsamplingData = createSubSampling(filter,srcHeight, dstHeight); 128 | 129 | 130 | final BufferedImage scrImgCopy = srcImg; 131 | final byte[][] workPixelsCopy = workPixels; 132 | Thread[] threads = new Thread[numberOfThreads-1]; 133 | for (int i=1;i= srcSize) { 229 | n= srcSize - j + srcSize - 1; 230 | } else { 231 | n= j; 232 | } 233 | int k= arrN[i]; 234 | //assert k == j-left:String.format("%s = %s %s", k,j,left); 235 | arrN[i]++; 236 | if (n < 0 || n >= srcSize) { 237 | weight= 0.0f;// Flag that cell should not be used 238 | } 239 | arrPixel[subindex +k]= n; 240 | arrWeight[subindex + k]= weight; 241 | } 242 | // normalize the filter's weight's so the sum equals to 1.0, very important for avoiding box type of artifacts 243 | final int max= arrN[i]; 244 | float tot= 0; 245 | for (int k= 0; k < max; k++) 246 | tot+= arrWeight[subindex + k]; 247 | if (tot != 0f) { // 0 should never happen except bug in filter 248 | for (int k= 0; k < max; k++) 249 | arrWeight[subindex + k]/= tot; 250 | } 251 | } 252 | } else 253 | // super-sampling 254 | // Scales from smaller to bigger height 255 | { 256 | numContributors= (int)(fwidth * 2.0f + 1); 257 | arrWeight= new float[dstSize * numContributors]; 258 | arrPixel= new int[dstSize * numContributors]; 259 | // 260 | for (int i= 0; i < dstSize; i++) { 261 | final int subindex= i * numContributors; 262 | float center= i / scale + centerOffset; 263 | int left= (int)Math.floor(center - fwidth); 264 | int right= (int)Math.ceil(center + fwidth); 265 | for (int j= left; j <= right; j++) { 266 | float weight= filter.apply(center - j); 267 | if (weight == 0.0f) { 268 | continue; 269 | } 270 | int n; 271 | if (j < 0) { 272 | n= -j; 273 | } else if (j >= srcSize) { 274 | n= srcSize - j + srcSize - 1; 275 | } else { 276 | n= j; 277 | } 278 | int k= arrN[i]; 279 | arrN[i]++; 280 | if (n < 0 || n >= srcSize) { 281 | weight= 0.0f;// Flag that cell should not be used 282 | } 283 | arrPixel[subindex +k]= n; 284 | arrWeight[subindex + k]= weight; 285 | } 286 | // normalize the filter's weight's so the sum equals to 1.0, very important for avoiding box type of artifacts 287 | final int max= arrN[i]; 288 | float tot= 0; 289 | for (int k= 0; k < max; k++) 290 | tot+= arrWeight[subindex + k]; 291 | assert tot!=0:"should never happen except bug in filter"; 292 | if (tot != 0f) { 293 | for (int k= 0; k < max; k++) 294 | arrWeight[subindex + k]/= tot; 295 | } 296 | } 297 | } 298 | return new SubSamplingData(arrN, arrPixel, arrWeight, numContributors); 299 | } 300 | 301 | private void verticalFromWorkToDst(byte[][] workPixels, byte[] outPixels, int start, int delta) { 302 | if (nrChannels==1){ 303 | verticalFromWorkToDstGray(workPixels, outPixels, start,numberOfThreads); 304 | return; 305 | } 306 | boolean useChannel3 = nrChannels>3; 307 | for (int x = start; x < dstWidth; x+=delta) 308 | { 309 | final int xLocation = x*nrChannels; 310 | for (int y = dstHeight-1; y >=0 ; y--) 311 | { 312 | final int yTimesNumContributors = y * verticalSubsamplingData.numContributors; 313 | final int max= verticalSubsamplingData.arrN[y]; 314 | final int sampleLocation = (y*dstWidth+x)*nrChannels; 315 | 316 | 317 | float sample0 = 0.0f; 318 | float sample1 = 0.0f; 319 | float sample2 = 0.0f; 320 | float sample3 = 0.0f; 321 | int index= yTimesNumContributors; 322 | for (int j= max-1; j >=0 ; j--) { 323 | int valueLocation = verticalSubsamplingData.arrPixel[index]; 324 | float arrWeight = verticalSubsamplingData.arrWeight[index]; 325 | sample0+= (workPixels[valueLocation][xLocation]&0xff) *arrWeight ; 326 | sample1+= (workPixels[valueLocation][xLocation+1]&0xff) * arrWeight; 327 | sample2+= (workPixels[valueLocation][xLocation+2]&0xff) * arrWeight; 328 | if (useChannel3){ 329 | sample3+= (workPixels[valueLocation][xLocation+3]&0xff) * arrWeight; 330 | } 331 | 332 | index++; 333 | } 334 | 335 | outPixels[sampleLocation] = toByte(sample0); 336 | outPixels[sampleLocation +1] = toByte(sample1); 337 | outPixels[sampleLocation +2] = toByte(sample2); 338 | if (useChannel3){ 339 | outPixels[sampleLocation +3] = toByte(sample3); 340 | } 341 | 342 | } 343 | processedItems++; 344 | if (start==0){ // only update progress listener from main thread 345 | setProgress(); 346 | } 347 | } 348 | } 349 | 350 | private void verticalFromWorkToDstGray(byte[][] workPixels, byte[] outPixels, int start, int delta) { 351 | for (int x = start; x < dstWidth; x+=delta) 352 | { 353 | final int xLocation = x; 354 | for (int y = dstHeight-1; y >=0 ; y--) 355 | { 356 | final int yTimesNumContributors = y * verticalSubsamplingData.numContributors; 357 | final int max= verticalSubsamplingData.arrN[y]; 358 | final int sampleLocation = (y*dstWidth+x); 359 | 360 | 361 | float sample0 = 0.0f; 362 | int index= yTimesNumContributors; 363 | for (int j= max-1; j >=0 ; j--) { 364 | int valueLocation = verticalSubsamplingData.arrPixel[index]; 365 | float arrWeight = verticalSubsamplingData.arrWeight[index]; 366 | sample0+= (workPixels[valueLocation][xLocation]&0xff) *arrWeight ; 367 | 368 | index++; 369 | } 370 | 371 | outPixels[sampleLocation] = toByte(sample0); 372 | } 373 | processedItems++; 374 | if (start==0){ // only update progress listener from main thread 375 | setProgress(); 376 | } 377 | } 378 | } 379 | 380 | /** 381 | * Apply filter to sample horizontally from Src to Work 382 | * @param srcImg 383 | * @param workPixels 384 | */ 385 | private void horizontallyFromSrcToWork(BufferedImage srcImg, byte[][] workPixels, int start, int delta) { 386 | if (nrChannels==1){ 387 | horizontallyFromSrcToWorkGray(srcImg, workPixels, start, delta); 388 | return; 389 | } 390 | final int[] tempPixels = new int[srcWidth]; // Used if we work on int based bitmaps, later used to keep channel values 391 | final byte[] srcPixels = new byte[srcWidth*nrChannels]; // create reusable row to minimize memory overhead 392 | final boolean useChannel3 = nrChannels>3; 393 | 394 | 395 | for (int k = start; k < srcHeight; k=k+delta) 396 | { 397 | ImageUtils.getPixelsBGR(srcImg, k, srcWidth, srcPixels, tempPixels); 398 | 399 | for (int i = dstWidth-1;i>=0 ; i--) 400 | { 401 | int sampleLocation = i*nrChannels; 402 | final int max = horizontalSubsamplingData.arrN[i]; 403 | 404 | float sample0 = 0.0f; 405 | float sample1 = 0.0f; 406 | float sample2 = 0.0f; 407 | float sample3 = 0.0f; 408 | int index= i * horizontalSubsamplingData.numContributors; 409 | for (int j= max-1; j >= 0; j--) { 410 | float arrWeight = horizontalSubsamplingData.arrWeight[index]; 411 | int pixelIndex = horizontalSubsamplingData.arrPixel[index]*nrChannels; 412 | 413 | sample0 += (srcPixels[pixelIndex]&0xff) * arrWeight; 414 | sample1 += (srcPixels[pixelIndex+1]&0xff) * arrWeight; 415 | sample2 += (srcPixels[pixelIndex+2]&0xff) * arrWeight; 416 | if (useChannel3){ 417 | sample3 += (srcPixels[pixelIndex+3]&0xff) * arrWeight; 418 | } 419 | index++; 420 | } 421 | 422 | workPixels[k][sampleLocation] = toByte(sample0); 423 | workPixels[k][sampleLocation +1] = toByte(sample1); 424 | workPixels[k][sampleLocation +2] = toByte(sample2); 425 | if (useChannel3){ 426 | workPixels[k][sampleLocation +3] = toByte(sample3); 427 | } 428 | } 429 | processedItems++; 430 | if (start==0){ // only update progress listener from main thread 431 | setProgress(); 432 | } 433 | } 434 | } 435 | 436 | /** 437 | * Apply filter to sample horizontally from Src to Work 438 | * @param srcImg 439 | * @param workPixels 440 | */ 441 | private void horizontallyFromSrcToWorkGray(BufferedImage srcImg, byte[][] workPixels, int start, int delta) { 442 | final int[] tempPixels = new int[srcWidth]; // Used if we work on int based bitmaps, later used to keep channel values 443 | final byte[] srcPixels = new byte[srcWidth]; // create reusable row to minimize memory overhead 444 | 445 | for (int k = start; k < srcHeight; k=k+delta) 446 | { 447 | ImageUtils.getPixelsBGR(srcImg, k, srcWidth, srcPixels, tempPixels); 448 | 449 | for (int i = dstWidth-1;i>=0 ; i--) 450 | { 451 | int sampleLocation = i; 452 | final int max = horizontalSubsamplingData.arrN[i]; 453 | 454 | float sample0 = 0.0f; 455 | int index= i * horizontalSubsamplingData.numContributors; 456 | for (int j= max-1; j >= 0; j--) { 457 | float arrWeight = horizontalSubsamplingData.arrWeight[index]; 458 | int pixelIndex = horizontalSubsamplingData.arrPixel[index]; 459 | 460 | sample0 += (srcPixels[pixelIndex]&0xff) * arrWeight; 461 | index++; 462 | } 463 | 464 | workPixels[k][sampleLocation] = toByte(sample0); 465 | } 466 | processedItems++; 467 | if (start==0){ // only update progress listener from main thread 468 | setProgress(); 469 | } 470 | } 471 | } 472 | 473 | private byte toByte(float f){ 474 | if (f<0){ 475 | return 0; 476 | } 477 | if (f>MAX_CHANNEL_VALUE){ 478 | return (byte) MAX_CHANNEL_VALUE; 479 | } 480 | return (byte)(f+0.5f); // add 0.5 same as Math.round 481 | } 482 | 483 | private void setProgress(){ 484 | fireProgressChanged(processedItems/totalItems); 485 | } 486 | 487 | protected int getResultBufferedImageType(BufferedImage srcImg) { 488 | return nrChannels == 3 ? BufferedImage.TYPE_3BYTE_BGR : 489 | (nrChannels == 4 ? BufferedImage.TYPE_4BYTE_ABGR : 490 | (srcImg.getSampleModel().getDataType() == DataBuffer.TYPE_USHORT ? 491 | BufferedImage.TYPE_USHORT_GRAY : BufferedImage.TYPE_BYTE_GRAY)); 492 | } 493 | } 494 | 495 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/ThumbnailRescaleOp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | 10 | import java.awt.image.BufferedImage; 11 | 12 | /** 13 | * The idea of this class is to provide fast (and inaccurate) rescaling method 14 | * suitable for creating thumbnails. 15 | * 16 | * Note that the algorithm assumes that the source image is significant larger 17 | * than the destination image 18 | */ 19 | public class ThumbnailRescaleOp extends AdvancedResizeOp { 20 | public static enum Sampling { 21 | S_1SAMPLE(new float[][]{{0.5f,0.5f}}), 22 | S_2X2_RGSS(new float[][]{ 23 | {0.6f,0.2f}, 24 | {0.2f,0.4f}, 25 | {0.8f,0.6f}, 26 | {0.4f,0.8f}, 27 | }), 28 | S_8ROCKS(new float[][]{ 29 | {0/6f,2/6f}, 30 | {2/6f,1/6f}, 31 | {4/6f,0/6f}, 32 | {5/6f,2/6f}, 33 | {6/6f,4/6f}, 34 | {4/6f,5/6f}, 35 | {2/6f,6/6f}, 36 | {1/6f,4/6f}, 37 | }) 38 | ; 39 | final float[][] points; 40 | final int rightshift; 41 | 42 | Sampling(float[][] points) { 43 | this.points = points; 44 | rightshift = Integer.numberOfTrailingZeros(points.length); 45 | } 46 | 47 | 48 | } 49 | 50 | private Sampling sampling = Sampling.S_8ROCKS; 51 | 52 | public ThumbnailRescaleOp(int destWidth, int destHeight) { 53 | this(DimensionConstrain.createAbsolutionDimension(destWidth, destHeight)); 54 | } 55 | 56 | public ThumbnailRescaleOp(DimensionConstrain dimensionConstrain) { 57 | super(dimensionConstrain); 58 | } 59 | 60 | protected BufferedImage doFilter(BufferedImage src, BufferedImage dest, int dstWidth, int dstHeight) { 61 | int numberOfChannels = ImageUtils.nrChannels(src); 62 | BufferedImage out; 63 | if (dest!=null && dstWidth==dest.getWidth() && dstHeight==dest.getHeight()){ 64 | out = dest; 65 | }else{ 66 | 67 | out = new BufferedImage(dstWidth, dstHeight, numberOfChannels==4?BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB); 68 | } 69 | 70 | float scaleX = src.getWidth()/(float)dstWidth; 71 | float scaleY = src.getHeight()/(float)dstHeight; 72 | 73 | float[][] scaledSampling = new float[sampling.points.length][2]; 74 | for (int i=0;i>>8; 96 | g += rgb&0xff; 97 | rgb = rgb>>>8; 98 | r += rgb&0xff; 99 | rgb = rgb>>>8; 100 | a += rgb&0xff; 101 | } 102 | r = r>>sampling.rightshift; 103 | g = g>>sampling.rightshift; 104 | b = b>>sampling.rightshift; 105 | a = a>>sampling.rightshift; 106 | 107 | int rgb = (a<<24)+(r<<16)+(g<<8)+b; 108 | out.setRGB(dstX, dstY, rgb); 109 | } 110 | } 111 | return out; 112 | } 113 | 114 | public void setSampling(Sampling sampling) { 115 | this.sampling = sampling; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/TriangleFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | /** 10 | * A triangle filter (also known as linear or bilinear filter). 11 | */ 12 | final class TriangleFilter implements ResampleFilter 13 | { 14 | public float getSamplingRadius() { 15 | return 1.0f; 16 | } 17 | 18 | public final float apply(float value) 19 | { 20 | if (value < 0.0f) 21 | { 22 | value = -value; 23 | } 24 | if (value < 1.0f) 25 | { 26 | return 1.0f - value; 27 | } 28 | else 29 | { 30 | return 0.0f; 31 | } 32 | } 33 | 34 | public String getName() { 35 | return "Triangle"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/experimental/ImprovedMultistepRescaleOp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling.experimental; 8 | 9 | import com.mortennobel.imagescaling.DimensionConstrain; 10 | import com.mortennobel.imagescaling.AdvancedResizeOp; 11 | 12 | import java.awt.*; 13 | import java.awt.image.BufferedImage; 14 | 15 | /** 16 | * The idea of this class is to test if the Sun's implementation multistep image scaling (using either 17 | * RenderingHints.VALUE_INTERPOLATION_BICUBIC or RenderingHints.VALUE_INTERPOLATION_BILINEAR) 18 | */ 19 | public class ImprovedMultistepRescaleOp extends AdvancedResizeOp { 20 | private final Object renderingHintInterpolation; 21 | 22 | public ImprovedMultistepRescaleOp(int dstWidth, int dstHeight) { 23 | this (dstWidth, dstHeight, RenderingHints.VALUE_INTERPOLATION_BILINEAR); 24 | } 25 | 26 | public ImprovedMultistepRescaleOp(int dstWidth, int dstHeight, Object renderingHintInterpolation) { 27 | this(DimensionConstrain.createAbsolutionDimension(dstWidth, dstHeight), renderingHintInterpolation); 28 | } 29 | 30 | public ImprovedMultistepRescaleOp(DimensionConstrain dimensionConstain) { 31 | this (dimensionConstain, RenderingHints.VALUE_INTERPOLATION_BILINEAR); 32 | } 33 | 34 | public ImprovedMultistepRescaleOp(DimensionConstrain dimensionConstain, Object renderingHintInterpolation) { 35 | super(dimensionConstain); 36 | this.renderingHintInterpolation = renderingHintInterpolation; 37 | assert RenderingHints.KEY_INTERPOLATION.isCompatibleValue(renderingHintInterpolation) : 38 | "Rendering hint "+renderingHintInterpolation+" is not compatible with interpolation"; 39 | } 40 | 41 | 42 | public BufferedImage doFilter(BufferedImage img, BufferedImage dest, int dstWidth, int dstHeight) { 43 | int type = (img.getTransparency() == Transparency.OPAQUE) ? 44 | BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; 45 | BufferedImage ret = img; 46 | int w, h; 47 | 48 | // Use multi-step technique: start with original size, then 49 | // scale down in multiple passes with drawImage() 50 | // until the target size is reached 51 | w = img.getWidth(); 52 | h = img.getHeight(); 53 | 54 | do { 55 | if (w > dstWidth) { 56 | w /= 2; 57 | if (w < dstWidth) { 58 | w = dstWidth; 59 | } 60 | } else { 61 | w = dstWidth; 62 | } 63 | 64 | if (h > dstHeight) { 65 | h /= 2; 66 | if (h < dstHeight) { 67 | h = dstHeight; 68 | } 69 | } else { 70 | h = dstHeight; 71 | } 72 | 73 | BufferedImage tmp; 74 | if (dest!=null && dest.getWidth()== w && dest.getHeight()== h && w==dstWidth && h==dstHeight){ 75 | tmp = dest; 76 | } else { 77 | tmp = new BufferedImage(w,h,type); 78 | } 79 | Graphics2D g2 = tmp.createGraphics(); 80 | g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, renderingHintInterpolation); 81 | g2.drawImage(ret, 0, 0, w, h, null); 82 | g2.dispose(); 83 | 84 | ret = tmp; 85 | } while (w != dstWidth || h != dstHeight); 86 | 87 | return ret; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/mortennobel/imagescaling/experimental/ResampleOpSingleThread.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling.experimental; 8 | 9 | import com.mortennobel.imagescaling.*; 10 | 11 | import java.awt.image.BufferedImage; 12 | import java.awt.image.DataBuffer; 13 | import java.util.BitSet; 14 | 15 | /** 16 | * Based on work from Java Image Util ( http://schmidt.devlib.org/jiu/ ) 17 | * 18 | * Note that the filter method is not thread safe 19 | * 20 | * @author Morten Nobel-Joergensen 21 | * @author Heinz Doerr 22 | */ 23 | public class ResampleOpSingleThread extends AdvancedResizeOp 24 | { 25 | private final int MAX_CHANNEL_VALUE = 255; 26 | 27 | private int nrChannels; 28 | private int srcWidth; 29 | private int srcHeight; 30 | private int dstWidth; 31 | private int dstHeight; 32 | 33 | private class SubSamplingData{ 34 | private final int[] arrN; // individual - per row or per column - nr of contributions 35 | private final int[] arrPixel; // 2Dim: [wid or hei][contrib] 36 | private final float[] arrWeight; // 2Dim: [wid or hei][contrib] 37 | private final int numContributors; // the primary index length for the 2Dim arrays : arrPixel and arrWeight 38 | private final float width; 39 | 40 | private SubSamplingData(int[] arrN, int[] arrPixel, float[] arrWeight, int numContributors, float width) { 41 | this.arrN = arrN; 42 | this.arrPixel = arrPixel; 43 | this.arrWeight = arrWeight; 44 | this.numContributors = numContributors; 45 | this.width = width; 46 | } 47 | } 48 | 49 | private SubSamplingData horizontalSubsamplingData; 50 | private SubSamplingData verticalSubsamplingData; 51 | 52 | private int processedItems; 53 | private int totalItems; 54 | 55 | private ResampleFilter filter = ResampleFilters.getLanczos3Filter(); 56 | 57 | public ResampleOpSingleThread(int destWidth, int destHeight) { 58 | super(DimensionConstrain.createAbsolutionDimension(destWidth, destHeight)); 59 | } 60 | 61 | public ResampleOpSingleThread(DimensionConstrain dimensionConstrain) { 62 | super(dimensionConstrain); 63 | } 64 | 65 | 66 | public ResampleFilter getFilter() { 67 | return filter; 68 | } 69 | 70 | public void setFilter(ResampleFilter filter) { 71 | this.filter = filter; 72 | } 73 | 74 | public BufferedImage doFilter(BufferedImage srcImg, BufferedImage dest, int dstWidth, int dstHeight) { 75 | this.dstWidth = dstWidth; 76 | this.dstHeight = dstHeight; 77 | if (srcImg.getType() == BufferedImage.TYPE_BYTE_BINARY || 78 | srcImg.getType() == BufferedImage.TYPE_BYTE_INDEXED || 79 | srcImg.getType() == BufferedImage.TYPE_CUSTOM) 80 | srcImg = ImageUtils.convert(srcImg, srcImg.getColorModel().hasAlpha() ? 81 | BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR); 82 | 83 | this.nrChannels= ImageUtils.nrChannels(srcImg); 84 | assert nrChannels > 0; 85 | this.srcWidth = srcImg.getWidth(); 86 | this.srcHeight = srcImg.getHeight(); 87 | 88 | this.processedItems = 0; 89 | this.totalItems = srcHeight; 90 | 91 | // Pre-calculate sub-sampling 92 | horizontalSubsamplingData = createSubSampling(srcWidth, dstWidth); 93 | verticalSubsamplingData = createSubSampling(srcHeight, dstHeight); 94 | 95 | //final byte[] outPixels = new byte[dstWidth*dstHeight*nrChannels]; 96 | 97 | // Idea: Since only a small part of the buffer is used, scaling the image, we reuse the rows to save memory 98 | final int bufferHeight = (int)Math.ceil(verticalSubsamplingData.width)*2; 99 | byte[][] workPixels = new byte[srcHeight][]; 100 | for (int i=0;i= srcSize) { 159 | n= srcSize - j + srcSize - 1; 160 | } else { 161 | n= j; 162 | } 163 | int k= arrN[i]; 164 | arrN[i]++; 165 | if (n < 0 || n >= srcSize) { 166 | weight= 0.0f;// Flag that cell should not be used 167 | } 168 | arrPixel[subindex +k]= n; 169 | arrWeight[subindex + k]= weight; 170 | } 171 | // normalize the filter's weight's so the sum equals to 1.0, very important for avoiding box type of artifacts 172 | final int max= arrN[i]; 173 | float tot= 0; 174 | for (int k= 0; k < max; k++) 175 | tot+= arrWeight[subindex + k]; 176 | if (tot != 0f) { // 0 should never happen except bug in filter 177 | for (int k= 0; k < max; k++) 178 | arrWeight[subindex + k]/= tot; 179 | } 180 | } 181 | } else 182 | // super-sampling 183 | // Scales from smaller to bigger height 184 | { 185 | width = fwidth; 186 | numContributors= (int)(fwidth * 2.0f + 1); 187 | arrWeight= new float[dstSize * numContributors]; 188 | arrPixel= new int[dstSize * numContributors]; 189 | // 190 | for (int i= 0; i < dstSize; i++) { 191 | final int subindex= i * numContributors; 192 | float center= i / scale; 193 | int left= (int)Math.floor(center - fwidth); 194 | int right= (int)Math.ceil(center + fwidth); 195 | 196 | for (int j= left; j <= right; j++) { 197 | float weight= filter.apply(center - j); 198 | if (weight == 0.0f) { 199 | continue; 200 | } 201 | int n; 202 | if (j < 0) { 203 | n= -j; 204 | } else if (j >= srcSize) { 205 | n= srcSize - j + srcSize - 1; 206 | } else { 207 | n= j; 208 | } 209 | int k= arrN[i]; 210 | arrN[i]++; 211 | if (n < 0 || n >= srcSize) { 212 | weight= 0.0f;// Flag that cell should not be used 213 | } 214 | arrPixel[subindex +k]= n; 215 | arrWeight[subindex + k]= weight; 216 | } 217 | // normalize the filter's weight's so the sum equals to 1.0, very important for avoiding box type of artifacts 218 | final int max= arrN[i]; 219 | float tot= 0; 220 | for (int k= 0; k < max; k++) 221 | tot+= arrWeight[subindex + k]; 222 | assert tot!=0:"should never happen except bug in filter"; 223 | if (tot != 0f) { 224 | for (int k= 0; k < max; k++) 225 | arrWeight[subindex + k]/= tot; 226 | } 227 | } 228 | } 229 | return new SubSamplingData(arrN, arrPixel, arrWeight, numContributors, width); 230 | } 231 | 232 | /** 233 | * Pseudocode: 234 | * for each for in destination image 235 | * check that all dependent rows in source image is scaled horizontally and stored in work pixels 236 | * if not then scale missing rows horizontal and store them in work pixels 237 | * Scale the destination row vertically and store the result in out pixels 238 | * 239 | * It may seem counter intuitive that the vertical scale is done for each row. The simple scaling algorithm would 240 | * first scale the image horizontal (a row at a time) into the temp image, and then scale the temp image vertically 241 | * (a column at a time) into the final image. The disadvantage of the simple approach is that you need a large 242 | * temporary image. 243 | * 244 | * I have avoided this by doing the vertically scale a row at a time. Scaling a single row vertically, needs the 245 | * horizontally scaled rows that the scaling depends on. These dependencies will be lazy initialized. 246 | * Since we know the height of the 'window' we work on (the maximum number of source rows needed to calculate a dest 247 | * row), the work pixels only needs to have the same height. 248 | * 249 | * Instead of creating a temporary image with a height different from the source image's height, I created a double 250 | * array where inner array is repeated (so if the window height is 3 the first and the forth row is the same instance). 251 | * This keeps algorithm a bit simpler (the alternative would be to maintain a mapping between) 252 | * 253 | * @param srcImg source image 254 | * @param workPixels temporary image 255 | * @param outImage result image 256 | */ 257 | private void scale(BufferedImage srcImg, byte[][] workPixels, BufferedImage outImage) { 258 | final int[] tempPixels = new int[srcWidth]; // Used if we work on int based bitmaps, later used to keep channel values 259 | final byte[] srcPixels = new byte[srcWidth*nrChannels]; // create reusable row to minimize memory overhead 260 | 261 | final byte[] dstPixels = new byte[dstWidth*nrChannels]; 262 | 263 | final BitSet isRowInitialized = new BitSet(srcHeight); 264 | 265 | for (int dstY = dstHeight-1; dstY >= 0; dstY--) 266 | { 267 | // vertical scaling 268 | final int yTimesNumContributors = dstY * verticalSubsamplingData.numContributors; 269 | final int max= verticalSubsamplingData.arrN[dstY]; 270 | 271 | // check that the horizontal rows are scaled horizontally 272 | { 273 | int index= yTimesNumContributors; 274 | for (int j= max-1; j >=0 ; j--) { 275 | int valueLocation = verticalSubsamplingData.arrPixel[index]; 276 | index++; 277 | if (!isRowInitialized.get(valueLocation)){ 278 | // do horizontal scaling 279 | isRowInitialized.set(valueLocation); 280 | 281 | // scale row horizontally 282 | ImageUtils.getPixelsBGR(srcImg, valueLocation, srcWidth, srcPixels, tempPixels); 283 | 284 | for (int channel = nrChannels-1;channel>=0 ; channel--) 285 | { 286 | // reuse tempPixels as sample value 287 | getSamplesHorizontal(srcPixels,channel,tempPixels); 288 | 289 | for (int i = dstWidth-1;i>=0 ; i--) 290 | { 291 | int sampleLocation = i*nrChannels; 292 | final int horizontalMax = horizontalSubsamplingData.arrN[i]; 293 | 294 | float sample= 0.0f; 295 | int horizontalIndex= i * horizontalSubsamplingData.numContributors; 296 | for (int jj= horizontalMax-1; jj >= 0; jj--) { 297 | 298 | sample += tempPixels[horizontalSubsamplingData.arrPixel[horizontalIndex]] * horizontalSubsamplingData.arrWeight[horizontalIndex]; 299 | horizontalIndex++; 300 | } 301 | 302 | putSample(workPixels[valueLocation], channel, (int)sample, sampleLocation); 303 | } 304 | } 305 | } 306 | } 307 | } 308 | 309 | for (int x = 0; x < dstWidth; x++) 310 | { 311 | final int xLocation = x*nrChannels; 312 | final int sampleLocation = x*nrChannels; 313 | 314 | for (int channel = nrChannels-1; channel >=0 ; channel--) 315 | { 316 | float sample = 0.0f; 317 | int index= yTimesNumContributors; 318 | for (int j= max-1; j >=0 ; j--) { 319 | int valueLocation = verticalSubsamplingData.arrPixel[index]; 320 | sample+= (workPixels[valueLocation][xLocation+channel]&0xff) * verticalSubsamplingData.arrWeight[index]; 321 | 322 | index++; 323 | } 324 | 325 | putSample(dstPixels, channel, sample, sampleLocation); 326 | } 327 | } 328 | 329 | 330 | ImageUtils.setBGRPixels(dstPixels, outImage, 0, dstY, dstWidth, 1); 331 | 332 | setProgress(processedItems++, totalItems); 333 | } 334 | } 335 | 336 | 337 | private void putSample(byte[] image, int channel, float sample, int location) { 338 | int result = (int) sample; 339 | if (sample<0){ 340 | result = 0; 341 | } 342 | else if (result > MAX_CHANNEL_VALUE) { 343 | result= MAX_CHANNEL_VALUE; 344 | } 345 | image[location+channel] = (byte)result; 346 | } 347 | 348 | private void getSamplesHorizontal(byte[] src, int channel, int[] dest){ 349 | for (int xDest=0, x=channel;x bufferedImage.getWidth()){ 48 | if(bufferedImage.getHeight() > 1600){ 49 | calcWidth = fixlength * bufferedImage.getWidth() / bufferedImage.getHeight(); 50 | calcHeight = 1600; 51 | bFlag = true; 52 | } 53 | else{ 54 | calcWidth = bufferedImage.getWidth(); 55 | calcHeight = bufferedImage.getHeight(); 56 | } 57 | } 58 | else{ 59 | if( bufferedImage.getWidth() > 1600){ 60 | calcHeight = fixlength * bufferedImage.getHeight() / bufferedImage.getWidth(); 61 | calcWidth = 1600; 62 | bFlag = true; 63 | } 64 | else{ 65 | calcWidth = bufferedImage.getWidth(); 66 | calcHeight = bufferedImage.getHeight(); 67 | } 68 | } 69 | 70 | System.out.println(calcHeight); 71 | BufferedImage rescaledImage = createResizedCopy(bufferedImage, calcWidth, calcHeight); 72 | // ImageIO.write(rescaledImage, "jpg", new File("/Users/morten/Programmering/java/ImageScaling/test/com/mortennobel/imagescaling/issues/rescaled.jpg")); 73 | int res = JOptionPane.showConfirmDialog(null, "Do you see a scaled image?", "", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, new ImageIcon(rescaledImage)); 74 | assertEquals(JOptionPane.YES_OPTION ,res); 75 | 76 | } catch (Exception e) { 77 | e.printStackTrace(); 78 | } 79 | } 80 | 81 | 82 | /** 83 | * Workaround for sRGB color space bug on JRE 1.5 84 | * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4705399 85 | * 86 | * @param source (inputstream or File) 87 | * @return fixed buffered image 88 | * @throws IOException 89 | */ 90 | public static BufferedImage readImage(Object source) throws IOException { 91 | ImageInputStream stream = ImageIO.createImageInputStream(source); 92 | ImageReader reader = ImageIO.getImageReaders(stream).next(); 93 | reader.setInput(stream); 94 | ImageReadParam param =reader.getDefaultReadParam(); 95 | 96 | ImageTypeSpecifier typeToUse = null; 97 | for (Iterator i = reader.getImageTypes(0);i.hasNext(); ) { 98 | ImageTypeSpecifier type = (ImageTypeSpecifier) i.next(); 99 | if (type.getColorModel().getColorSpace().isCS_sRGB()) 100 | typeToUse = type; 101 | } 102 | if (typeToUse!=null) param.setDestinationType(typeToUse); 103 | 104 | BufferedImage b = reader.read(0, param); 105 | 106 | reader.dispose(); 107 | stream.close(); 108 | return b; 109 | } 110 | 111 | 112 | 113 | public static BufferedImage createResizedCopy(BufferedImage originalImage, int scaledWidth, int scaledHeight) { 114 | ResampleOp resampleOp = new ResampleOp (scaledWidth,scaledHeight); 115 | resampleOp.setUnsharpenMask(AdvancedResizeOp.UnsharpenMask.VerySharp); 116 | BufferedImage rescaledTomato = resampleOp.filter(originalImage, null); 117 | return rescaledTomato; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/test/java/com/mortennobel/imagescaling/Issue6.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | import junit.framework.TestCase; 10 | 11 | import java.util.Arrays; 12 | 13 | /** 14 | * The idea of this test, is to ensure that the subsampling method 15 | * works correct 16 | */ 17 | public class Issue6 extends TestCase { 18 | public void testSimpleCase() throws Exception { 19 | int srcSize = 100; 20 | int dstSize = 50; 21 | ResampleOp.SubSamplingData subSamplingData = ResampleOp.createSubSampling(ResampleFilters.getBoxFilter(), srcSize,dstSize); 22 | int[] readFreq = new int[srcSize]; 23 | 24 | for (int i=0;i=0 ); 29 | assertTrue(String.format("%f", fraction), fraction<=1 ); 30 | min = Math.min(min, fraction); 31 | max = Math.max(max, fraction); 32 | assertTrue(String.format("%f<=%f", lastProgress, fraction), lastProgress<=fraction); 33 | lastProgress = fraction; 34 | } 35 | }); 36 | BufferedImage thumb = resizeOp.filter(img, null); 37 | assertTrue(min<0.01f); 38 | assertTrue(max>0.99f); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/mortennobel/imagescaling/Issue9.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | import junit.framework.TestCase; 10 | 11 | import javax.imageio.ImageIO; 12 | import java.awt.image.BufferedImage; 13 | import java.io.File; 14 | 15 | /** 16 | * However, when trying the library, I get messages saying that the resize is 17 | * still working and that it is over 100%. This seems a bit odd :-) If I don't 18 | * terminate the program myself, it keeps on running. My code is attached. 19 | * 20 | * I'm using Linux Ubuntu (although this should not matter) and Suns JDK 21 | * 1.6.0.16. Version of java-image-scaling is 0.8.1. 22 | * 23 | * Getting messages like: 24 | * Still working - 450,250580 percent 25 | * Still working - 450,278381 percent 26 | * Still working - 450,306213 percent 27 | * Still working - 450,334076 percent 28 | * Still working - 450,361908 percent 29 | * 30 | */ 31 | public class Issue9 extends TestCase { 32 | boolean inProgress; 33 | public void testForBug(){ 34 | try { 35 | inProgress = true; 36 | BufferedImage image2D = ImageIO.read(getClass().getResourceAsStream("flower.jpg")); 37 | 38 | DimensionConstrain dc = DimensionConstrain.createMaxDimensionNoOrientation(1000,1000); 39 | 40 | ResampleOp resampleOp = new ResampleOp (dc); 41 | resampleOp.setUnsharpenMask(AdvancedResizeOp.UnsharpenMask.Normal); 42 | resampleOp.addProgressListener(new ProgressListener() { 43 | public void notifyProgress(float fraction) { 44 | assertTrue(inProgress); 45 | System.out.printf("Still working - %f percent %n",fraction*100); 46 | } 47 | }); 48 | BufferedImage rescaledTomato = resampleOp.filter(image2D, null); 49 | inProgress = false; 50 | System.out.println("Done scaling"); 51 | //ImageIO.write(rescaledTomato, "jpg", new File("/home/frederik/Afbeeldingen/temp4.JPG")); 52 | try{ 53 | System.out.println("Waiting"); 54 | Thread.sleep(10000); 55 | } 56 | catch (Exception e){ 57 | // e 58 | } 59 | } catch (Exception e) { 60 | 61 | } 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/mortennobel/imagescaling/MultipleResizeTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | import javax.swing.*; 10 | import javax.imageio.ImageIO; 11 | import java.awt.*; 12 | import java.awt.image.BufferedImage; 13 | import java.io.IOException; 14 | 15 | 16 | /** 17 | * This test should make sure that can be rescaled in multiple dimensions (without errors) and still look great. 18 | */ 19 | public class MultipleResizeTest extends JFrame { 20 | private BufferedImage sourceImage; 21 | private BufferedImage scaledImage; 22 | 23 | 24 | public MultipleResizeTest(final BufferedImage sourceImage) { 25 | setSize(700,700); 26 | setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 27 | setVisible(true); 28 | 29 | this.sourceImage = this.scaledImage = sourceImage; 30 | 31 | Thread t = new Thread(){ 32 | public void run() { 33 | while (true){ 34 | int seconds = (int) (System.currentTimeMillis()/1000); 35 | final int MAX_WIDTH = 800; 36 | final int MAX_HEIGHT = 800; 37 | int width = (int) ((Math.sin(seconds/60.0)+1)*MAX_WIDTH); 38 | int height = (int) ((Math.sin(seconds/40.0)+1)*MAX_HEIGHT); 39 | width = Math.max(width, 3); 40 | height = Math.max(height,3); 41 | System.out.println("width = " + width); 42 | System.out.println("height = " + height); 43 | rescaleImage(width,height); 44 | } 45 | } 46 | }; 47 | t.setDaemon(true); 48 | t.start(); 49 | } 50 | 51 | private void rescaleImage(int width, int height){ 52 | ResampleOp resampleOp = new ResampleOp(width,height); 53 | scaledImage = resampleOp.filter(sourceImage,null); 54 | repaint(); 55 | } 56 | 57 | public void paint(Graphics g) { 58 | g.setColor(Color.white); 59 | g.fillRect(0,0,getWidth(),getHeight()); 60 | g.drawImage(scaledImage, 0,0,null); 61 | } 62 | 63 | public static void main(String[] args){ 64 | try { 65 | BufferedImage image = ImageIO.read(MultipleResizeTest.class.getResource("/com/mortennobel/imagescaling/largeimage.jpg")); 66 | 67 | new MultipleResizeTest(image); 68 | } catch (IOException e) { 69 | e.printStackTrace(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/com/mortennobel/imagescaling/MultistepRescaleOpTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.mortennobel.imagescaling; 8 | 9 | import java.awt.RenderingHints; 10 | import java.awt.image.BufferedImage; 11 | 12 | import javax.imageio.ImageIO; 13 | 14 | import junit.framework.TestCase; 15 | 16 | import com.mortennobel.imagescaling.experimental.ImprovedMultistepRescaleOp; 17 | 18 | public class MultistepRescaleOpTest extends TestCase { 19 | 20 | protected BufferedImage image; 21 | 22 | /** 23 | * Sets up the fixture, for example, open a network connection. 24 | * This method is called before a test is executed. 25 | */ 26 | @Override 27 | protected void setUp() throws Exception { 28 | this.image = ImageIO.read(getClass().getResource("largeimage-thump.jpg")); 29 | } 30 | 31 | public void testCorrect(){ 32 | final BufferedImage reference = new MultiStepRescaleOp(this.image.getWidth()/4, this.image.getHeight()/4,RenderingHints.VALUE_INTERPOLATION_BILINEAR).filter(this.image, null); 33 | final BufferedImage result = new ImprovedMultistepRescaleOp(this.image.getWidth()/4, this.image.getHeight()/4,RenderingHints.VALUE_INTERPOLATION_BILINEAR).filter(this.image, null); 34 | assertEquals(result, reference); 35 | } 36 | 37 | public void assertEquals(final BufferedImage image1, final BufferedImage image2){ 38 | assertSame(image1.getWidth(), image2.getWidth()); 39 | assertSame(image1.getHeight(), image2.getHeight()); 40 | 41 | final int pixels = image1.getWidth()*image2.getHeight(); 42 | int errors = 0; 43 | for (int x = 0;x=2); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/mortennobel/imagescaling/SpeedSingleThreadTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package com.mortennobel.imagescaling; 9 | 10 | import java.awt.RenderingHints; 11 | import java.awt.image.BufferedImage; 12 | 13 | import javax.imageio.ImageIO; 14 | 15 | import junit.framework.TestCase; 16 | 17 | /** 18 | * May have to adjust heap using the Xmx vm option 19 | */ 20 | public class SpeedSingleThreadTest extends TestCase { 21 | 22 | protected BufferedImage image; 23 | 24 | public int getNumberOfThreads(){ 25 | return 1; 26 | } 27 | 28 | @Override 29 | protected void setUp() throws Exception { 30 | super.setUp(); 31 | this.image = ImageIO.read(getClass().getResource("largeimage.jpg")); 32 | } 33 | 34 | protected void doRescale(final ResampleFilter filter) throws Exception{ 35 | final ResampleOp resampleOp = new ResampleOp(200,200); 36 | resampleOp.setNumberOfThreads(getNumberOfThreads()); 37 | resampleOp.setFilter(filter); 38 | resampleOp.filter(this.image, null); 39 | } 40 | 41 | public void testBellFilter() throws Exception { 42 | doRescale(new BellFilter()); 43 | } 44 | 45 | public void testBiCubicFilter() throws Exception { 46 | doRescale(new BiCubicFilter()); 47 | } 48 | 49 | public void testBiCubicHighFreqResponse() throws Exception { 50 | doRescale(new BiCubicHighFreqResponse()); 51 | } 52 | 53 | public void testBoxFilter() throws Exception { 54 | doRescale(new BoxFilter()); 55 | } 56 | 57 | public void testBSplineFilter() throws Exception { 58 | doRescale(new BSplineFilter()); 59 | } 60 | 61 | public void testHermiteFilter() throws Exception { 62 | doRescale(new HermiteFilter()); 63 | } 64 | 65 | public void testLanczos3Filter() throws Exception { 66 | doRescale(new Lanczos3Filter()); 67 | } 68 | 69 | public void testMitchellFilter() throws Exception { 70 | doRescale(new MitchellFilter()); 71 | } 72 | 73 | public void testTriangleFilter() throws Exception { 74 | doRescale(new TriangleFilter()); 75 | } 76 | 77 | public void testMultiStepRescaleOpLinear(){ 78 | final MultiStepRescaleOp mro = new MultiStepRescaleOp(200,200, RenderingHints.VALUE_INTERPOLATION_BILINEAR); 79 | mro.filter(this.image, null); 80 | } 81 | 82 | public void testMultiStepRescaleOpBicubic(){ 83 | final MultiStepRescaleOp mro = new MultiStepRescaleOp(200,200, RenderingHints.VALUE_INTERPOLATION_BICUBIC); 84 | mro.filter(this.image, null); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/com/mortennobel/imagescaling/TestThumbnailRescaleOp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013, Morten Nobel-Joergensen 3 | * 4 | * License: The BSD 3-Clause License 5 | * http://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package com.mortennobel.imagescaling; 9 | 10 | import java.awt.image.BufferedImage; 11 | import java.awt.*; 12 | import java.io.IOException; 13 | 14 | import static org.junit.Assert.*; 15 | import org.junit.Test; 16 | import org.junit.Before; 17 | 18 | import javax.imageio.ImageIO; 19 | 20 | public class TestThumbnailRescaleOp { 21 | @Test 22 | public void test1Sample(){ 23 | BufferedImage bi = new BufferedImage(3,3,BufferedImage.TYPE_INT_ARGB); 24 | Graphics2D g2d = bi.createGraphics(); 25 | g2d.setColor(Color.black); 26 | g2d.fillRect(0,0,3,3); 27 | g2d.setColor(Color.red); 28 | g2d.fillRect(1,1,1,1); 29 | g2d.dispose(); 30 | 31 | ThumbnailRescaleOp rescaleOp = new ThumbnailRescaleOp(1,1); 32 | rescaleOp.setSampling(ThumbnailRescaleOp.Sampling.S_1SAMPLE); 33 | BufferedImage scaledImage = rescaleOp.filter(bi, null); 34 | assertNotNull(scaledImage); 35 | assertEquals(scaledImage.getRGB(0,0), Color.red.getRGB()); 36 | } 37 | 38 | @Test 39 | public void test1SampleLarge(){ 40 | BufferedImage bi = new BufferedImage(7,7,BufferedImage.TYPE_INT_ARGB); 41 | Graphics2D g2d = bi.createGraphics(); 42 | g2d.setColor(Color.black); 43 | g2d.fillRect(0,0,bi.getWidth(),bi.getHeight()); 44 | g2d.setColor(Color.red); 45 | g2d.fillRect(1,1,1,1); 46 | g2d.dispose(); 47 | 48 | ThumbnailRescaleOp rescaleOp = new ThumbnailRescaleOp(2,2); 49 | rescaleOp.setSampling(ThumbnailRescaleOp.Sampling.S_1SAMPLE); 50 | BufferedImage scaledImage = rescaleOp.filter(bi, null); 51 | assertNotNull(scaledImage); 52 | for (int x=0;x