Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Rename menu, autoscale image on resize, add dialog corner icon

AuthorDaveJarvis <email>
Date2021-05-12 22:59:07 GMT-0700
Commit535fe60043ac7b8e0f442fbbf92bf3e1256165c4
Parent2d3c499
Delta907 lines added, 4 lines removed, 903-line increase
src/main/resources/com/keenwrite/messages.properties
Action.view.statusbar.text=Status bar
-Action.view.issues.description=Open document issues
-Action.view.issues.accelerator=F12
-Action.view.issues.text=Issues
+Action.view.log.description=Open document issues
+Action.view.log.accelerator=F12
+Action.view.log.text=Log
src/main/java/com/keenwrite/ui/actions/ApplicationBars.java
addAction( "view.statusbar", e -> actions.view‿statusbar() ),
SEPARATOR_ACTION,
- addAction( "view.issues", e -> actions.view‿issues() )
+ addAction( "view.log", e -> actions.view‿log() )
),
createMenu(
src/main/java/com/keenwrite/preview/images/ImageUtils.java
+/*
+ * Copyright 2013, Morten Nobel-Joergensen
+ *
+ * License: The BSD 3-Clause License
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+package com.keenwrite.preview.images;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+
+import static java.awt.image.BufferedImage.*;
+
+/**
+ * @author Heinz Doerr
+ * @author Morten Nobel-Joergensen
+ */
+public final class ImageUtils {
+ @SuppressWarnings( "DuplicateBranchesInSwitch" )
+ static int nrChannels( final BufferedImage img ) {
+ return switch( img.getType() ) {
+ case TYPE_3BYTE_BGR -> 3;
+ case TYPE_4BYTE_ABGR -> 4;
+ case TYPE_BYTE_GRAY -> 1;
+ case TYPE_INT_BGR -> 3;
+ case TYPE_INT_ARGB -> 4;
+ case TYPE_INT_RGB -> 3;
+ case TYPE_CUSTOM -> 4;
+ case TYPE_4BYTE_ABGR_PRE -> 4;
+ case TYPE_INT_ARGB_PRE -> 4;
+ case TYPE_USHORT_555_RGB -> 3;
+ case TYPE_USHORT_565_RGB -> 3;
+ case TYPE_USHORT_GRAY -> 1;
+ default -> 0;
+ };
+ }
+
+ /**
+ * returns one row (height == 1) of byte packed image data in BGR or AGBR form
+ *
+ * @param temp must be either null or a array with length of w*h
+ */
+ static void getPixelsBGR(
+ BufferedImage img, int y, int w, byte[] array, int[] temp ) {
+ final int x = 0;
+ final int h = 1;
+
+ assert array.length == temp.length * nrChannels( img );
+ assert (temp.length == w);
+
+ final Raster raster;
+ switch( img.getType() ) {
+ case TYPE_3BYTE_BGR, TYPE_4BYTE_ABGR,
+ TYPE_4BYTE_ABGR_PRE, TYPE_BYTE_GRAY -> {
+ raster = img.getRaster();
+ //int ttype= raster.getTransferType();
+ raster.getDataElements( x, y, w, h, array );
+ }
+ case TYPE_INT_BGR -> {
+ raster = img.getRaster();
+ raster.getDataElements( x, y, w, h, temp );
+ ints2bytes( temp, array, 0, 1, 2 ); // bgr --> bgr
+ }
+ case TYPE_INT_RGB -> {
+ raster = img.getRaster();
+ raster.getDataElements( x, y, w, h, temp );
+ ints2bytes( temp, array, 2, 1, 0 ); // rgb --> bgr
+ }
+ case TYPE_INT_ARGB, TYPE_INT_ARGB_PRE -> {
+ raster = img.getRaster();
+ raster.getDataElements( x, y, w, h, temp );
+ ints2bytes( temp, array, 2, 1, 0, 3 ); // argb --> abgr
+ }
+ case TYPE_CUSTOM -> {
+ // loader, but else ???
+ img.getRGB( x, y, w, h, temp, 0, w );
+ ints2bytes( temp, array, 2, 1, 0, 3 ); // argb --> abgr
+ }
+ default -> {
+ img.getRGB( x, y, w, h, temp, 0, w );
+ ints2bytes( temp, array, 2, 1, 0 ); // rgb --> bgr
+ }
+ }
+ }
+
+ /**
+ * converts and copies byte packed BGR or ABGR into the img buffer,
+ * the img type may vary (e.g. RGB or BGR, int or byte packed)
+ * but the number of components (w/o alpha, w alpha, gray) must match
+ * <p>
+ * does not unmange the image for all (A)RGN and (A)BGR and gray imaged
+ */
+ public static void setBGRPixels( byte[] bgrPixels, BufferedImage img, int x,
+ int y, int w, int h ) {
+ int imageType = img.getType();
+ WritableRaster raster = img.getRaster();
+
+ if( imageType == TYPE_3BYTE_BGR ||
+ imageType == TYPE_4BYTE_ABGR ||
+ imageType == TYPE_4BYTE_ABGR_PRE ||
+ imageType == TYPE_BYTE_GRAY ) {
+ raster.setDataElements( x, y, w, h, bgrPixels );
+ }
+ else {
+ int[] pixels;
+ if( imageType == TYPE_INT_BGR ) {
+ pixels = bytes2int( bgrPixels, 2, 1, 0 ); // bgr --> bgr
+ }
+ else if( imageType == TYPE_INT_ARGB ||
+ imageType == TYPE_INT_ARGB_PRE ) {
+ pixels = bytes2int( bgrPixels, 3, 0, 1, 2 ); // abgr --> argb
+ }
+ else {
+ pixels = bytes2int( bgrPixels, 0, 1, 2 ); // bgr --> rgb
+ }
+ if( w == 0 || h == 0 ) {
+ return;
+ }
+ else if( pixels.length < w * h ) {
+ throw new IllegalArgumentException( "pixels array must have a length" + " >= w*h" );
+ }
+ if( imageType == TYPE_INT_ARGB ||
+ imageType == TYPE_INT_RGB ||
+ imageType == TYPE_INT_ARGB_PRE ||
+ imageType == TYPE_INT_BGR ) {
+ raster.setDataElements( x, y, w, h, pixels );
+ }
+ else {
+ // Unmanages the image
+ img.setRGB( x, y, w, h, pixels, 0, w );
+ }
+ }
+ }
+
+ public static void ints2bytes( int[] in, byte[] out, int index1, int index2,
+ int index3 ) {
+ for( int i = 0; i < in.length; i++ ) {
+ int index = i * 3;
+ int value = in[ i ];
+ out[ index + index1 ] = (byte) value;
+ value = value >> 8;
+ out[ index + index2 ] = (byte) value;
+ value = value >> 8;
+ out[ index + index3 ] = (byte) value;
+ }
+ }
+
+ public static void ints2bytes( int[] in, byte[] out, int index1, int index2,
+ int index3, int index4 ) {
+ for( int i = 0; i < in.length; i++ ) {
+ int index = i * 4;
+ int value = in[ i ];
+ out[ index + index1 ] = (byte) value;
+ value = value >> 8;
+ out[ index + index2 ] = (byte) value;
+ value = value >> 8;
+ out[ index + index3 ] = (byte) value;
+ value = value >> 8;
+ out[ index + index4 ] = (byte) value;
+ }
+ }
+
+ public static int[] bytes2int( byte[] in, int index1, int index2,
+ int index3 ) {
+ int[] out = new int[ in.length / 3 ];
+ for( int i = 0; i < out.length; i++ ) {
+ int index = i * 3;
+ int b1 = (in[ index + index1 ] & 0xff) << 16;
+ int b2 = (in[ index + index2 ] & 0xff) << 8;
+ int b3 = in[ index + index3 ] & 0xff;
+ out[ i ] = b1 | b2 | b3;
+ }
+ return out;
+ }
+
+ public static int[] bytes2int( byte[] in, int index1, int index2, int index3,
+ int index4 ) {
+ int[] out = new int[ in.length / 4 ];
+ for( int i = 0; i < out.length; i++ ) {
+ int index = i * 4;
+ int b1 = (in[ index + index1 ] & 0xff) << 24;
+ int b2 = (in[ index + index2 ] & 0xff) << 16;
+ int b3 = (in[ index + index3 ] & 0xff) << 8;
+ int b4 = in[ index + index4 ] & 0xff;
+ out[ i ] = b1 | b2 | b3 | b4;
+ }
+ return out;
+ }
+
+ public static BufferedImage convert( BufferedImage src, int bufImgType ) {
+ BufferedImage img = new BufferedImage( src.getWidth(),
+ src.getHeight(),
+ bufImgType );
+ Graphics2D g2d = img.createGraphics();
+ g2d.drawImage( src, 0, 0, null );
+ g2d.dispose();
+ return img;
+ }
+}
src/main/java/com/keenwrite/preview/images/Lanczos3.java
+package com.keenwrite.preview.images;
+
+import java.awt.image.BufferedImage;
+
+/**
+ * Unused. Needs to extract image data from {@link BufferedImage} and create
+ * down-sampled version.
+ */
+public class Lanczos3 {
+ static double sinc( double x ) {
+ x *= Math.PI;
+
+ if( (x < 0.01f) && (x > -0.01f) ) {
+ return 1.0f + x * x * (-1.0f / 6.0f + x * x * 1.0f / 120.0f);
+ }
+
+ return Math.sin( x ) / x;
+ }
+
+ static float clip( double t ) {
+ final float eps = .0000125f;
+
+ if( Math.abs( t ) < eps ) { return 0.0f; }
+
+ return (float) t;
+ }
+
+ static float lancos( float t ) {
+ if( t < 0.0f ) { t = -t; }
+
+ if( t < 3.0f ) { return clip( sinc( t ) * sinc( t / 3.0f ) ); }
+ else { return (0.0f); }
+ }
+
+ static float lancos3_resample_x(
+ int[][] arr, int src_w, int src_h, int y, int x, float xscale ) {
+ float s = 0;
+ float coef_sum = 0.0f;
+ float coef;
+ float pix;
+ int i;
+
+ int l, r;
+ float c;
+ float hw;
+
+ // For the reduction of the situation hw is equivalent to expanding the
+ // number of pixels in the field, if you do not do this, the final
+ // reduction of the image effect is not much different from the recent
+ // field interpolation method, the effect is equivalent to the first
+ // low-pass filtering, and then interpolate
+ if( xscale > 1.0f ) { hw = 3.0f; }
+ else { hw = 3.0f / xscale; }
+
+ c = (float) x / xscale;
+ l = (int) Math.floor( c - hw );
+ r = (int) Math.ceil( c + hw );
+
+ if( y < 0 ) { y = 0; }
+ if( y >= src_h ) { y = src_h - 1; }
+ if( xscale > 1.0f ) { xscale = 1.0f; }
+ for( i = l; i <= r; i++ ) {
+ x = Math.max( i, 0 );
+ if( i >= src_w ) { x = src_w - 1; }
+ pix = arr[ y ][ x ];
+ coef = lancos( (c - i) * xscale );
+ s += pix * coef;
+ coef_sum += coef;
+ }
+ s /= coef_sum;
+ return s;
+ }
+
+ static class uint8_2d {
+ int[][] arr;
+ int rows;
+ int cols;
+
+ public uint8_2d( final int h1, final int w1 ) {
+ arr = new int[ h1 ][ w1 ];
+ rows = h1;
+ cols = w1;
+ }
+ }
+
+ void img_resize_using_lancos3( uint8_2d src, uint8_2d dst ) {
+ if( src == null || dst == null ) { return; }
+
+ int src_rows, src_cols;
+ int dst_rows, dst_cols;
+ int i, j;
+ int[][] src_arr;
+ int[][] dst_arr;
+ float xratio;
+ float yratio;
+ int val;
+ int k;
+ float hw;
+
+ src_arr = src.arr;
+ dst_arr = dst.arr;
+ src_rows = src.rows;
+ src_cols = src.cols;
+ dst_rows = dst.rows;
+ dst_cols = dst.cols;
+
+ xratio = (float) (dst_cols) / (float) src_cols;
+ yratio = (float) (dst_rows) / (float) src_rows;
+
+ float scale;
+
+ if( yratio > 1.0f ) {
+ hw = 3.0f;
+ scale = 1.0f;
+ }
+ else {
+ hw = 3.0f / yratio;
+ scale = yratio;
+ }
+
+ for( i = 0; i < dst_rows; i++ ) {
+ for( j = 0; j < dst_cols; j++ ) {
+ int t, b;
+ float c;
+
+ float s = 0;
+ float coef_sum = 0.0f;
+ float coef;
+ float pix;
+
+ c = (float) i / yratio;
+ t = (int) Math.floor( c - hw );
+ b = (int) Math.ceil( c + hw );
+ // Interpolate in the x direction first, then interpolate in the y
+ // direction.
+ for( k = t; k <= b; k++ ) {
+ pix = lancos3_resample_x( src_arr, src_cols, src_rows, k, j, xratio );
+ coef = lancos( (c - k) * scale );
+ coef_sum += coef;
+ pix *= coef;
+ s += pix;
+ }
+ val = (int) (s / coef_sum);
+ if( val < 0 ) { val = 0; }
+ if( val > 255 ) { val = 255; }
+ dst_arr[ i ][ j ] = val;
+ }
+ }
+ }
+
+ BufferedImage test_lancos3_resize( BufferedImage img, float factor ) {
+ assert img != null;
+
+ uint8_2d r = null;
+ uint8_2d g = null;
+ uint8_2d b = null;
+
+ BufferedImage out = null;
+ // TODO: Split buffered image into RGB components.
+ //split_img_data( img, r, g, b );
+
+ int w, h;
+ int w1, h1;
+ w = img.getWidth();
+ h = img.getHeight();
+
+ // TODO: Maintain aspect ratio.
+ w1 = (int) (factor * w);
+ h1 = (int) (factor * h);
+
+ uint8_2d r1 = new uint8_2d( h1, w1 );
+ uint8_2d g1 = new uint8_2d( h1, w1 );
+ uint8_2d b1 = new uint8_2d( h1, w1 );
+
+ img_resize_using_lancos3( r, r1 );
+ img_resize_using_lancos3( g, g1 );
+ img_resize_using_lancos3( b, b1 );
+
+ // TODO: Combine rescaled image into RGB components.
+ //merge_img_data( r1, g1, b1, out);
+
+ return out;
+ }
+}
src/main/java/com/keenwrite/preview/images/Lanczos3Filter.java
+/*
+ * Copyright 2013, Morten Nobel-Joergensen
+ *
+ * License: The BSD 3-Clause License
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+package com.keenwrite.preview.images;
+
+public final class Lanczos3Filter implements ResampleFilter {
+ private final static float PI_FLOAT = (float) Math.PI;
+
+ private float sincModified( float value ) {
+ return (float) Math.sin( value ) / value;
+ }
+
+ public final float apply( float value ) {
+ if( value == 0 ) {
+ return 1.0f;
+ }
+
+ if( value < 0.0f ) {
+ value = -value;
+ }
+
+ if( value < 3.0f ) {
+ value *= PI_FLOAT;
+ return sincModified( value ) * sincModified( value / 3.0f );
+ }
+
+ return 0.0f;
+ }
+
+ public float getSamplingRadius() {
+ return 3.0f;
+ }
+}
src/main/java/com/keenwrite/preview/images/ResampleFilter.java
+/*
+ * Copyright 2013, Morten Nobel-Joergensen
+ *
+ * License: The BSD 3-Clause License
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+package com.keenwrite.preview.images;
+
+public interface ResampleFilter {
+ float getSamplingRadius();
+
+ float apply(float v);
+}
src/main/java/com/keenwrite/preview/images/ResampleOp.java
+/*
+ * Copyright 2013, Morten Nobel-Joergensen
+ *
+ * License: The BSD 3-Clause License
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+package com.keenwrite.preview.images;
+
+import java.awt.image.BufferedImage;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.keenwrite.preview.images.ConstrainedDimension.createAbsolutionDimension;
+import static java.awt.image.BufferedImage.*;
+import static java.awt.image.DataBuffer.TYPE_USHORT;
+import static java.lang.Runtime.getRuntime;
+import static java.lang.String.format;
+import static java.lang.Thread.currentThread;
+
+/**
+ * Based on <a href="http://schmidt.devlib.org/jiu/">Java Image Util</a>.
+ * <p>
+ * Note that the filter method is not thread-safe.
+ * </p>
+ *
+ * @author Morten Nobel-Joergensen
+ * @author Heinz Doerr
+ */
+public class ResampleOp extends AdvancedResizeOp {
+ private static final int MAX_CHANNEL_VALUE = 255;
+
+ private int nrChannels;
+ private int srcWidth;
+ private int srcHeight;
+ private int dstWidth;
+ private int dstHeight;
+
+ static class SubSamplingData {
+ // individual - per row or per column - nr of contributions
+ private final int[] arrN;
+ // 2Dim: [wid or hei][contrib]
+ private final int[] arrPixel;
+ // 2Dim: [wid or hei][contrib]
+ private final float[] arrWeight;
+ // the primary index length for the 2Dim arrays : arrPixel and arrWeight
+ private final int numContributors;
+
+ private SubSamplingData( int[] arrN, int[] arrPixel, float[] arrWeight,
+ int numContributors ) {
+ this.arrN = arrN;
+ this.arrPixel = arrPixel;
+ this.arrWeight = arrWeight;
+ this.numContributors = numContributors;
+ }
+
+ public int getNumContributors() {
+ return numContributors;
+ }
+
+ public int[] getArrN() {
+ return arrN;
+ }
+
+ public float[] getArrWeight() {
+ return arrWeight;
+ }
+ }
+
+ private SubSamplingData horizontalSubsamplingData;
+ private SubSamplingData verticalSubsamplingData;
+
+ private final int threadCount = getRuntime().availableProcessors();
+ private final AtomicInteger multipleInvocationLock = new AtomicInteger();
+ private final ResampleFilter mFilter;
+
+ public ResampleOp(
+ final ResampleFilter filter, final int destWidth, final int destHeight ) {
+ this( filter,
+ createAbsolutionDimension( destWidth, destHeight ) );
+ }
+
+ public ResampleOp(
+ final ResampleFilter filter, ConstrainedDimension dimensionConstrain ) {
+ super( dimensionConstrain );
+ mFilter = filter;
+ }
+
+ public BufferedImage doFilter(
+ BufferedImage srcImg, BufferedImage dest, int dstWidth, int dstHeight ) {
+ this.dstWidth = dstWidth;
+ this.dstHeight = dstHeight;
+
+ if( dstWidth < 3 || dstHeight < 3 ) {
+ throw new IllegalArgumentException( "Target must be at least 3x3." );
+ }
+
+ assert multipleInvocationLock.incrementAndGet() == 1 :
+ "Multiple concurrent invocations detected";
+
+ final var srcType = srcImg.getType();
+
+ if( srcType == TYPE_BYTE_BINARY ||
+ srcType == TYPE_BYTE_INDEXED ||
+ srcType == TYPE_CUSTOM ) {
+ srcImg = ImageUtils.convert(
+ srcImg,
+ srcImg.getColorModel().hasAlpha() ? TYPE_4BYTE_ABGR : TYPE_3BYTE_BGR );
+ }
+
+ this.nrChannels = ImageUtils.nrChannels( srcImg );
+ assert nrChannels > 0;
+ this.srcWidth = srcImg.getWidth();
+ this.srcHeight = srcImg.getHeight();
+
+ byte[][] workPixels = new byte[ srcHeight ][ dstWidth * nrChannels ];
+
+ // Pre-calculate sub-sampling
+ horizontalSubsamplingData = createSubSampling(
+ mFilter, srcWidth, dstWidth );
+ verticalSubsamplingData = createSubSampling(
+ mFilter, srcHeight, dstHeight );
+
+ final BufferedImage scrImgCopy = srcImg;
+ final byte[][] workPixelsCopy = workPixels;
+ final Thread[] threads = new Thread[ threadCount - 1 ];
+
+ for( int i = 1; i < threadCount; i++ ) {
+ final int finalI = i;
+ threads[ i - 1 ] = new Thread( () -> horizontallyFromSrcToWork(
+ scrImgCopy, workPixelsCopy, finalI, threadCount ) );
+ threads[ i - 1 ].start();
+ }
+
+ horizontallyFromSrcToWork( scrImgCopy, workPixelsCopy, 0, threadCount );
+ waitForAllThreads( threads );
+
+ byte[] outPixels = new byte[ dstWidth * dstHeight * nrChannels ];
+
+ // --------------------------------------------------
+ // Apply filter to sample vertically from Work to Dst
+ // --------------------------------------------------
+ final byte[] outPixelsCopy = outPixels;
+ for( int i = 1; i < threadCount; i++ ) {
+ final int finalI = i;
+ threads[ i - 1 ] = new Thread( () -> verticalFromWorkToDst(
+ workPixelsCopy, outPixelsCopy, finalI, threadCount ) );
+ threads[ i - 1 ].start();
+ }
+ verticalFromWorkToDst( workPixelsCopy, outPixelsCopy, 0, threadCount );
+ waitForAllThreads( threads );
+
+ //noinspection UnusedAssignment
+ workPixels = null; // free memory
+ final BufferedImage out;
+ if( dest != null && dstWidth == dest.getWidth() && dstHeight == dest.getHeight() ) {
+ out = dest;
+ int nrDestChannels = ImageUtils.nrChannels( dest );
+ if( nrDestChannels != nrChannels ) {
+ final var errorMgs = format(
+ "Destination image must be compatible width source image. Source " +
+ "image had %d channels destination image had %d channels",
+ nrChannels, nrDestChannels );
+ throw new RuntimeException( errorMgs );
+ }
+ }
+ else {
+ out = new BufferedImage(
+ dstWidth, dstHeight, getResultBufferedImageType( srcImg ) );
+ }
+
+ ImageUtils.setBGRPixels( outPixels, out, 0, 0, dstWidth, dstHeight );
+
+ assert multipleInvocationLock.decrementAndGet() == 0 : "Multiple " +
+ "concurrent invocations detected";
+
+ return out;
+ }
+
+ private void waitForAllThreads( final Thread[] threads ) {
+ try {
+ for( final Thread thread : threads ) {
+ thread.join( Long.MAX_VALUE );
+ }
+ } catch( final InterruptedException e ) {
+ currentThread().interrupt();
+ throw new RuntimeException( e );
+ }
+ }
+
+ static SubSamplingData createSubSampling(
+ ResampleFilter filter, int srcSize, int dstSize ) {
+ final float scale = (float) dstSize / (float) srcSize;
+ final int[] arrN = new int[ dstSize ];
+ final int numContributors;
+ final float[] arrWeight;
+ final int[] arrPixel;
+
+ final float fwidth = filter.getSamplingRadius();
+
+ float centerOffset = 0.5f / scale;
+
+ if( scale < 1.0f ) {
+ final float width = fwidth / scale;
+ // Add 2 to be safe with the ceiling
+ numContributors = (int) (width * 2.0f + 2);
+ arrWeight = new float[ dstSize * numContributors ];
+ arrPixel = new int[ dstSize * numContributors ];
+
+ final float fNormFac = (float) (1f / (Math.ceil( width ) / fwidth));
+
+ for( int i = 0; i < dstSize; i++ ) {
+ final int subindex = i * numContributors;
+ float center = i / scale + centerOffset;
+ int left = (int) Math.floor( center - width );
+ int right = (int) Math.ceil( center + width );
+ for( int j = left; j <= right; j++ ) {
+ float weight;
+ weight = filter.apply( (center - j) * fNormFac );
+
+ if( weight == 0.0f ) {
+ continue;
+ }
+ int n;
+ if( j < 0 ) {
+ n = -j;
+ }
+ else if( j >= srcSize ) {
+ n = srcSize - j + srcSize - 1;
+ }
+ else {
+ n = j;
+ }
+ int k = arrN[ i ];
+ //assert k == j-left:String.format("%s = %s %s", k,j,left);
+ arrN[ i ]++;
+ if( n < 0 || n >= srcSize ) {
+ weight = 0.0f;// Flag that cell should not be used
+ }
+ arrPixel[ subindex + k ] = n;
+ arrWeight[ subindex + k ] = weight;
+ }
+ // normalize the filter's weight's so the sum equals to 1.0, very
+ // important for avoiding box type of artifacts
+ final int max = arrN[ i ];
+ float tot = 0;
+ for( int k = 0; k < max; k++ ) { tot += arrWeight[ subindex + k ]; }
+ if( tot != 0f ) { // 0 should never happen except bug in filter
+ for( int k = 0; k < max; k++ ) { arrWeight[ subindex + k ] /= tot; }
+ }
+ }
+ }
+ else {
+ // super-sampling
+ // Scales from smaller to bigger height
+ numContributors = (int) (fwidth * 2.0f + 1);
+ arrWeight = new float[ dstSize * numContributors ];
+ arrPixel = new int[ dstSize * numContributors ];
+ //
+ for( int i = 0; i < dstSize; i++ ) {
+ final int subindex = i * numContributors;
+ float center = i / scale + centerOffset;
+ int left = (int) Math.floor( center - fwidth );
+ int right = (int) Math.ceil( center + fwidth );
+ for( int j = left; j <= right; j++ ) {
+ float weight = filter.apply( center - j );
+ if( weight == 0.0f ) {
+ continue;
+ }
+ int n;
+ if( j < 0 ) {
+ n = -j;
+ }
+ else if( j >= srcSize ) {
+ n = srcSize - j + srcSize - 1;
+ }
+ else {
+ n = j;
+ }
+ int k = arrN[ i ];
+ arrN[ i ]++;
+ if( n < 0 || n >= srcSize ) {
+ weight = 0.0f;// Flag that cell should not be used
+ }
+ arrPixel[ subindex + k ] = n;
+ arrWeight[ subindex + k ] = weight;
+ }
+ // normalize the filter's weight's so the sum equals to 1.0, very
+ // important for avoiding box type of artifacts
+ final int max = arrN[ i ];
+ float tot = 0;
+ for( int k = 0; k < max; k++ ) { tot += arrWeight[ subindex + k ]; }
+ assert tot != 0 : "should never happen except bug in filter";
+ if( tot != 0f ) {
+ for( int k = 0; k < max; k++ ) { arrWeight[ subindex + k ] /= tot; }
+ }
+ }
+ }
+ return new SubSamplingData( arrN, arrPixel, arrWeight, numContributors );
+ }
+
+ private void verticalFromWorkToDst( byte[][] workPixels, byte[] outPixels,
+ int start, int delta ) {
+ if( nrChannels == 1 ) {
+ verticalFromWorkToDstGray(
+ workPixels, outPixels, start, threadCount );
+ return;
+ }
+ boolean useChannel3 = nrChannels > 3;
+ for( int x = start; x < dstWidth; x += delta ) {
+ final int xLocation = x * nrChannels;
+ for( int y = dstHeight - 1; y >= 0; y-- ) {
+ final int yTimesNumContributors =
+ y * verticalSubsamplingData.numContributors;
+ final int max = verticalSubsamplingData.arrN[ y ];
+ final int sampleLocation = (y * dstWidth + x) * nrChannels;
+
+ float sample0 = 0.0f;
+ float sample1 = 0.0f;
+ float sample2 = 0.0f;
+ float sample3 = 0.0f;
+ int index = yTimesNumContributors;
+ for( int j = max - 1; j >= 0; j-- ) {
+ int valueLocation = verticalSubsamplingData.arrPixel[ index ];
+ float arrWeight = verticalSubsamplingData.arrWeight[ index ];
+ sample0 += (workPixels[ valueLocation ][ xLocation ] & 0xff) * arrWeight;
+ sample1 += (workPixels[ valueLocation ][ xLocation + 1 ] & 0xff) * arrWeight;
+ sample2 += (workPixels[ valueLocation ][ xLocation + 2 ] & 0xff) * arrWeight;
+ if( useChannel3 ) {
+ sample3 += (workPixels[ valueLocation ][ xLocation + 3 ] & 0xff) * arrWeight;
+ }
+
+ index++;
+ }
+
+ outPixels[ sampleLocation ] = toByte( sample0 );
+ outPixels[ sampleLocation + 1 ] = toByte( sample1 );
+ outPixels[ sampleLocation + 2 ] = toByte( sample2 );
+
+ if( useChannel3 ) {
+ outPixels[ sampleLocation + 3 ] = toByte( sample3 );
+ }
+ }
+ }
+ }
+
+ private void verticalFromWorkToDstGray(
+ byte[][] workPixels, byte[] outPixels, int start, int delta ) {
+ for( int x = start; x < dstWidth; x += delta ) {
+ for( int y = dstHeight - 1; y >= 0; y-- ) {
+ final int yTimesNumContributors =
+ y * verticalSubsamplingData.numContributors;
+ final int max = verticalSubsamplingData.arrN[ y ];
+ final int sampleLocation = y * dstWidth + x;
+ float sample0 = 0.0f;
+ int index = yTimesNumContributors;
+
+ for( int j = max - 1; j >= 0; j-- ) {
+ int valueLocation = verticalSubsamplingData.arrPixel[ index ];
+ float arrWeight = verticalSubsamplingData.arrWeight[ index ];
+ sample0 += (workPixels[ valueLocation ][ x ] & 0xff) * arrWeight;
+
+ index++;
+ }
+
+ outPixels[ sampleLocation ] = toByte( sample0 );
+ }
+ }
+ }
+
+ /**
+ * Apply filter to sample horizontally from Src to Work
+ */
+ private void horizontallyFromSrcToWork(
+ BufferedImage srcImg, byte[][] workPixels, int start, int delta ) {
+ if( nrChannels == 1 ) {
+ horizontallyFromSrcToWorkGray( srcImg, workPixels, start, delta );
+ return;
+ }
+
+ // Used if we work on int based bitmaps, later used to keep channel values
+ final int[] tempPixels = new int[ srcWidth ];
+ // create reusable row to minimize memory overhead
+ final byte[] srcPixels = new byte[ srcWidth * nrChannels ];
+ final boolean useChannel3 = nrChannels > 3;
+
+ for( int k = start; k < srcHeight; k = k + delta ) {
+ ImageUtils.getPixelsBGR( srcImg, k, srcWidth, srcPixels, tempPixels );
+
+ for( int i = dstWidth - 1; i >= 0; i-- ) {
+ int sampleLocation = i * nrChannels;
+ final int max = horizontalSubsamplingData.arrN[ i ];
+
+ float sample0 = 0.0f;
+ float sample1 = 0.0f;
+ float sample2 = 0.0f;
+ float sample3 = 0.0f;
+ int index = i * horizontalSubsamplingData.numContributors;
+ for( int j = max - 1; j >= 0; j-- ) {
+ float arrWeight = horizontalSubsamplingData.arrWeight[ index ];
+ int pixelIndex =
+ horizontalSubsamplingData.arrPixel[ index ] * nrChannels;
+
+ sample0 += (srcPixels[ pixelIndex ] & 0xff) * arrWeight;
+ sample1 += (srcPixels[ pixelIndex + 1 ] & 0xff) * arrWeight;
+ sample2 += (srcPixels[ pixelIndex + 2 ] & 0xff) * arrWeight;
+ if( useChannel3 ) {
+ sample3 += (srcPixels[ pixelIndex + 3 ] & 0xff) * arrWeight;
+ }
+ index++;
+ }
+
+ workPixels[ k ][ sampleLocation ] = toByte( sample0 );
+ workPixels[ k ][ sampleLocation + 1 ] = toByte( sample1 );
+ workPixels[ k ][ sampleLocation + 2 ] = toByte( sample2 );
+ if( useChannel3 ) {
+ workPixels[ k ][ sampleLocation + 3 ] = toByte( sample3 );
+ }
+ }
+ }
+ }
+
+ /**
+ * Apply filter to sample horizontally from Src to Work
+ */
+ private void horizontallyFromSrcToWorkGray(
+ BufferedImage srcImg, byte[][] workPixels, int start, int delta ) {
+ // Used if we work on int based bitmaps, later used to keep channel values
+ final int[] tempPixels = new int[ srcWidth ];
+ // create reusable row to minimize memory overhead
+ final byte[] srcPixels = new byte[ srcWidth ];
+
+ for( int k = start; k < srcHeight; k = k + delta ) {
+ ImageUtils.getPixelsBGR( srcImg, k, srcWidth, srcPixels, tempPixels );
+
+ for( int i = dstWidth - 1; i >= 0; i-- ) {
+ final int max = horizontalSubsamplingData.arrN[ i ];
+
+ float sample0 = 0.0f;
+ int index = i * horizontalSubsamplingData.numContributors;
+ for( int j = max - 1; j >= 0; j-- ) {
+ float arrWeight = horizontalSubsamplingData.arrWeight[ index ];
+ int pixelIndex = horizontalSubsamplingData.arrPixel[ index ];
+
+ sample0 += (srcPixels[ pixelIndex ] & 0xff) * arrWeight;
+ index++;
+ }
+
+ workPixels[ k ][ i ] = toByte( sample0 );
+ }
+ }
+ }
+
+ private static byte toByte( final float f ) {
+ if( f < 0 ) {
+ return 0;
+ }
+
+ return (byte) (f > MAX_CHANNEL_VALUE ? MAX_CHANNEL_VALUE : f + 0.5f);
+ }
+
+ protected int getResultBufferedImageType( BufferedImage srcImg ) {
+ return nrChannels == 3
+ ? TYPE_3BYTE_BGR
+ : nrChannels == 4
+ ? TYPE_4BYTE_ABGR
+ : srcImg.getSampleModel().getDataType() == TYPE_USHORT
+ ? TYPE_USHORT_GRAY
+ : TYPE_BYTE_GRAY;
+ }
+}