Fix image in pdf using the EXIF image header
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -287,19 +290,6 @@ public class PdfUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user