Fix image in pdf using the EXIF image header

This commit is contained in:
verboomp
2026-02-06 12:11:04 +01:00
parent 5e941753e2
commit 7819b963f2
3 changed files with 130 additions and 102 deletions

View File

@@ -0,0 +1,123 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import javax.imageio.ImageIO;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.ExifIFD0Directory;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 6 Feb 2026
*/
public interface ImageHandler {
static final Log LOG = LogFactory.getLog(ImageHandler.class);
/**
* Reads image bytes and returns a BufferedImage with correct EXIF orientation applied.
*/
default BufferedImage readImageWithCorrectOrientation(byte[] imageBytes) throws IOException {
int orientation = getExifOrientation(imageBytes);
BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes));
if (image == null) {
throw new IOException("Failed to read image from byte array");
}
return applyOrientation(image, orientation);
}
default int getExifOrientation(byte[] imageBytes) {
try {
Metadata metadata = ImageMetadataReader.readMetadata(new ByteArrayInputStream(imageBytes));
ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
if (directory != null && directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
return directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
}
} catch (ImageProcessingException | IOException | MetadataException e) {
LOG.debug("Could not read EXIF orientation: " + e.getMessage());
}
return 1;
}
default BufferedImage applyOrientation(BufferedImage image, int orientation) {
int width = image.getWidth();
int height = image.getHeight();
AffineTransform transform = new AffineTransform();
int newWidth = width;
int newHeight = height;
switch (orientation) {
case 1 -> {
return image;
}
case 2 -> {
transform.scale(-1.0, 1.0);
transform.translate(-width, 0);
}
case 3 -> {
transform.translate(width, height);
transform.rotate(Math.PI);
}
case 4 -> {
transform.scale(1.0, -1.0);
transform.translate(0, -height);
}
case 5 -> {
newWidth = height;
newHeight = width;
transform.rotate(-Math.PI / 2);
transform.scale(-1.0, 1.0);
}
case 6 -> {
newWidth = height;
newHeight = width;
transform.translate(height, 0);
transform.rotate(Math.PI / 2);
}
case 7 -> {
newWidth = height;
newHeight = width;
transform.scale(-1.0, 1.0);
transform.translate(-height, 0);
transform.translate(0, width);
transform.rotate(3 * Math.PI / 2);
}
case 8 -> {
newWidth = height;
newHeight = width;
transform.translate(0, width);
transform.rotate(-Math.PI / 2);
}
default -> {
return image;
}
}
BufferedImage rotated = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = rotated.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.drawImage(image, transform, null);
g2d.dispose();
return rotated;
}
}

View File

@@ -2,7 +2,6 @@ package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@@ -18,12 +17,6 @@ import javax.imageio.stream.ImageOutputStream;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.ExifIFD0Directory;
/** /**
* *
* <p>Copyright: Copyright (c) 2024</p> * <p>Copyright: Copyright (c) 2024</p>
@@ -34,7 +27,7 @@ import com.drew.metadata.exif.ExifIFD0Directory;
* created: 2 Feb 2026 * created: 2 Feb 2026
*/ */
public class ImageUtil { public class ImageUtil implements ImageHandler {
private static final Log LOG = LogFactory.getLog(ImageUtil.class); private static final Log LOG = LogFactory.getLog(ImageUtil.class);
private static final int NORMAL_MAX_WIDTH = 1200; private static final int NORMAL_MAX_WIDTH = 1200;
@@ -130,84 +123,6 @@ public class ImageUtil {
} }
} }
private int getExifOrientation(byte[] imageBytes) {
try {
Metadata metadata = ImageMetadataReader.readMetadata(new ByteArrayInputStream(imageBytes));
ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
if (directory != null && directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
return directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
}
} catch (ImageProcessingException | IOException | MetadataException e) {
LOG.debug("Could not read EXIF orientation: " + e.getMessage());
}
return 1;
}
private BufferedImage applyOrientation(BufferedImage image, int orientation) {
int width = image.getWidth();
int height = image.getHeight();
AffineTransform transform = new AffineTransform();
int newWidth = width;
int newHeight = height;
switch (orientation) {
case 1 -> {
return image;
}
case 2 -> {
transform.scale(-1.0, 1.0);
transform.translate(-width, 0);
}
case 3 -> {
transform.translate(width, height);
transform.rotate(Math.PI);
}
case 4 -> {
transform.scale(1.0, -1.0);
transform.translate(0, -height);
}
case 5 -> {
newWidth = height;
newHeight = width;
transform.rotate(-Math.PI / 2);
transform.scale(-1.0, 1.0);
}
case 6 -> {
newWidth = height;
newHeight = width;
transform.translate(height, 0);
transform.rotate(Math.PI / 2);
}
case 7 -> {
newWidth = height;
newHeight = width;
transform.scale(-1.0, 1.0);
transform.translate(-height, 0);
transform.translate(0, width);
transform.rotate(3 * Math.PI / 2);
}
case 8 -> {
newWidth = height;
newHeight = width;
transform.translate(0, width);
transform.rotate(-Math.PI / 2);
}
default -> {
return image;
}
}
BufferedImage rotated = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = rotated.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.drawImage(image, transform, null);
g2d.dispose();
return rotated;
}
private byte[] writeJpeg(BufferedImage image, float quality) throws IOException { private byte[] writeJpeg(BufferedImage image, float quality) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream(); ByteArrayOutputStream output = new ByteArrayOutputStream();
ImageWriter writer = ImageIO.getImageWritersByFormatName("jpg").next(); ImageWriter writer = ImageIO.getImageWritersByFormatName("jpg").next();

View File

@@ -1,6 +1,7 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils; package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.awt.Color; import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -19,6 +20,7 @@ import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer; import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
@@ -34,7 +36,7 @@ import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
* created: 2 Feb 2026 * created: 2 Feb 2026
*/ */
@SuppressWarnings({ "java:S818", "squid:S818", "squid:S109" }) @SuppressWarnings({ "java:S818", "squid:S818", "squid:S109" })
public class PdfUtils { public class PdfUtils implements ImageHandler {
private static final Log LOG = LogFactory.getLog(PdfUtils.class); private static final Log LOG = LogFactory.getLog(PdfUtils.class);
private static final String FONT_PANTON_REGULAR = "fonts/Panton-Regular.ttf"; private static final String FONT_PANTON_REGULAR = "fonts/Panton-Regular.ttf";
@@ -97,7 +99,8 @@ public class PdfUtils {
if (picture.getImage() != null) { if (picture.getImage() != null) {
try { try {
byte[] imageBytes = Base64.getDecoder().decode(picture.getImage()); byte[] imageBytes = Base64.getDecoder().decode(picture.getImage());
PDImageXObject pdImage = PDImageXObject.createFromByteArray(document, imageBytes, "picture"); BufferedImage correctedImage = readImageWithCorrectOrientation(imageBytes);
PDImageXObject pdImage = LosslessFactory.createFromImage(document, correctedImage);
float scale = Math.min(imageMaxWidth / pdImage.getWidth(), imageMaxHeight / pdImage.getHeight()); float scale = Math.min(imageMaxWidth / pdImage.getWidth(), imageMaxHeight / pdImage.getHeight());
float drawWidth = pdImage.getWidth() * scale; float drawWidth = pdImage.getWidth() * scale;
@@ -286,20 +289,7 @@ public class PdfUtils {
cs.fill(); cs.fill();
} }
} }
private void drawRoundedRect(PDPageContentStream cs, float x, float y, float w, float h, float r) throws IOException {
cs.moveTo(x + r, y);
cs.lineTo(x + w - r, y);
cs.curveTo(x + w, y, x + w, y, x + w, y + r);
cs.lineTo(x + w, y + h - r);
cs.curveTo(x + w, y + h, x + w, y + h, x + w - r, y + h);
cs.lineTo(x + r, y + h);
cs.curveTo(x, y + h, x, y + h, x, y + h - r);
cs.lineTo(x, y + r);
cs.curveTo(x, y, x, y, x + r, y);
cs.closePath();
}
private void drawCircle(PDPageContentStream cs, float cx, float cy, float r) throws IOException { private void drawCircle(PDPageContentStream cs, float cx, float cy, float r) throws IOException {
float k = 0.5523f; // Bezier approximation for circle float k = 0.5523f; // Bezier approximation for circle
cs.moveTo(cx - r, cy); cs.moveTo(cx - r, cy);