Skip to content

Commit

Permalink
Merge pull request #220 from dbmdz/crop-rotation-bug
Browse files Browse the repository at this point in the history
Various bugfixes for cropping & rotation handling
  • Loading branch information
stefan-it authored Feb 27, 2023
2 parents f9fd8bf + 8fa0895 commit 9a24c2c
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
package de.digitalcollections.turbojpeg.imageio;

import static java.awt.image.BufferedImage.TYPE_3BYTE_BGR;
import static java.awt.image.BufferedImage.TYPE_BYTE_GRAY;

import de.digitalcollections.turbojpeg.Info;
import de.digitalcollections.turbojpeg.TurboJpeg;
import de.digitalcollections.turbojpeg.TurboJpegException;
import de.digitalcollections.turbojpeg.lib.enums.TJCS;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.awt.image.BufferedImage.TYPE_3BYTE_BGR;
import static java.awt.image.BufferedImage.TYPE_BYTE_GRAY;

public class TurboJpegImageReader extends ImageReader {

Expand Down Expand Up @@ -116,6 +120,30 @@ public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) {
.iterator();
}

/**
* Calculate the closest value to a given minimum. This function should be used when defining min sizes
* of region height or width, because Math.min is not sufficient in rare cases, when the returned minimum
* value is smaller than the desired size. Consider this example:
* First value is 44 and second value is 112 with a user-specified min value of 100. Math.min would select 44,
* which is wrong, because it is under the user-specified threshold of 100.
* @param minValue The minimum value
* @param xs Integer values
* @return Integer of the closest value w.r.t. a given min value.
* If all values are under the min value, min value will be returned
*/
int getClosestValue(int minValue, int... xs) {
Integer min = null;
for (int x : xs) {
if (x < minValue) {
continue;
}
if (min == null || x < min) {
min = x;
}
}
return min == null ? minValue : min;
}

/**
* Since TurboJPEG can only crop to values divisible by the MCU size, we may need to expand the
* cropping area to get a suitable rectangle. Thus, cropping becomes a two-stage process: 1. Crop
Expand Down Expand Up @@ -161,6 +189,8 @@ Rectangle adjustRegion(Dimension mcuSize, Rectangle region, int rotation, Dimens
region.height = w;
}

int originalRegionWidth = region.width;
int originalRegionHeight = region.height;
// Calculate how much of the region returned from libjpeg has to be cropped on the JVM-side
Rectangle extraCrop =
new Rectangle(
Expand All @@ -172,33 +202,38 @@ Rectangle adjustRegion(Dimension mcuSize, Rectangle region, int rotation, Dimens
if (region.x % mcuSize.width != 0) {
extraCrop.x = region.x % mcuSize.width;
region.x -= extraCrop.x;
if (region.width > 0) {
region.width = Math.min(region.width + extraCrop.x, originalWidth - region.x);
}
region.width = getClosestValue(
originalRegionWidth,
region.width + extraCrop.x,
originalWidth - region.x
);
}
// Y-Offset + Height
if (region.y % mcuSize.height != 0) {
extraCrop.y = region.y % mcuSize.height;
region.y -= extraCrop.y;
if (region.height > 0) {
region.height = Math.min(region.height + extraCrop.y, originalHeight - region.y);
region.height = getClosestValue(
originalRegionHeight,
region.height + extraCrop.y,
originalHeight - region.y
);
}
}

if ((region.x + region.width) != originalWidth && region.width % mcuSize.width != 0) {
region.width =
Math.min(
(int) (mcuSize.width * (Math.ceil(region.getWidth() / mcuSize.width))),
imageSize.width - region.x);
region.width = getClosestValue(
originalRegionWidth,
imageSize.width - region.x,
(int) (mcuSize.width * (Math.ceil(region.getWidth() / mcuSize.width)))
);
}

if ((region.y + region.height) != originalHeight && region.height % mcuSize.height != 0) {
region.height =
Math.min(
region.height = getClosestValue(
originalRegionHeight,
(int) (mcuSize.height * (Math.ceil(region.getHeight() / mcuSize.height))),
imageSize.height - region.y);
imageSize.height - region.y
);
}

boolean modified =
originalRegion.x != region.x
|| originalRegion.y != region.y
Expand Down Expand Up @@ -280,17 +315,22 @@ public BufferedImage read(int imageIndex, ImageReadParam param) throws IOExcepti
region = null;
}
}

int finalHeight = getHeight(0);
int finalWidth = getWidth(0);

// Rotations 90 and 270 switch image dimensions!
if (rotation == 90 || rotation == 270) {
finalHeight = getWidth(0);
finalWidth = getHeight(0);
}

if (region != null
&& (region.x + region.width > getWidth(0) || region.y + region.height > getHeight(0))) {
&& (region.x + region.width > finalWidth || region.y + region.height > finalHeight)) {
throw new IllegalArgumentException(
String.format(
"Selected region (%dx%d+%d+%d) exceeds the image boundaries (%dx%d).",
region.width,
region.height,
region.x,
region.y,
getWidth(imageIndex),
getHeight(imageIndex)));
region.width, region.height, region.x, region.y, finalWidth, finalHeight));
}
if (region != null || rotation != 0) {
data = lib.transform(data.array(), info, region, rotation);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
package de.digitalcollections.turbojpeg.imageio;

import static de.digitalcollections.turbojpeg.imageio.CustomAssertions.assertThat;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Test;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Rectangle;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.function.Supplier;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Test;

class TurboJpegImageReaderTest {
import static de.digitalcollections.turbojpeg.imageio.CustomAssertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

class TurboJpegImageReaderTest {
@Test
public void testReaderIsRegistered() {
Supplier<List<ImageReader>> getReaderIter =
Expand Down Expand Up @@ -282,4 +281,59 @@ public void testReadGrayscale() throws IOException {
BufferedImage controlImg = ImageIO.read(input);
assertThat(img).isEqualTo(controlImg);
}

@Test
public void testReadRotatedAndCroppedGridSearch() throws IOException {
ImageReader reader = getReader("crop_rotation.jpg");

// Unit-test it hard
int originalHeight = reader.getHeight(0);
int originalWidth = reader.getWidth(0);

// defines distance between new regions
int padding = 20;

int regionHeight = 100;
int regionWidth = 50;

int[] rotationSizes = {90, 180, 270};

for (int rotationSize : rotationSizes) {
for (int y = padding; y < originalHeight; y += regionHeight + padding) {
if (y + regionHeight > originalHeight) {
break;
}

for (int x = padding; x < originalWidth; x += regionWidth + padding) {
if (x + regionWidth > originalWidth) {
break;
}

TurboJpegImageReadParam current_param = (TurboJpegImageReadParam) reader.getDefaultReadParam();
current_param.setSourceRegion(new Rectangle(x, y, regionWidth, regionHeight));
current_param.setRotationDegree(rotationSize);

BufferedImage currentCroppedImage = reader.read(0, current_param);

int referenceRegionHeight = rotationSize == 90 || rotationSize == 270 ? regionWidth : regionHeight;
int referenceRegionWidth = rotationSize == 90 || rotationSize == 270 ? regionHeight : regionWidth;
assertThat(currentCroppedImage.getHeight()).isEqualTo(referenceRegionHeight);
assertThat(currentCroppedImage.getWidth()).isEqualTo(referenceRegionWidth);
}
}
}
}

@Test
public void testReadRotatedAndCroppedSpecial() throws IOException {
ImageReader reader = getReader("crop_rotation.jpg");
TurboJpegImageReadParam param = (TurboJpegImageReadParam) reader.getDefaultReadParam();
param.setSourceRegion(new Rectangle(160, 740, 50, 100));
param.setRotationDegree(90);
BufferedImage rotatedCroppedImage = reader.read(0, param);

assertThat(rotatedCroppedImage.getHeight()).isEqualTo(50);
assertThat(rotatedCroppedImage.getWidth()).isEqualTo(100);
}

}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 9a24c2c

Please sign in to comment.