Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various bugfixes for cropping & rotation handling #220

Merged
merged 11 commits into from
Feb 27, 2023
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.