Skip to content

Commit 6be4742

Browse files
✨ add support for image resize & pdf/image compression (#199)
1 parent 7bb1ce4 commit 6be4742

File tree

9 files changed

+765
-35
lines changed

9 files changed

+765
-35
lines changed

.editorconfig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ trim_trailing_whitespace = true
99
[*.{java,json,xml,md,markdown}]
1010
indent_style = space
1111
indent_size = 2
12+
ij_continuation_indent_size = 4
1213

1314
[*.{md,markdown}]
1415
trim_trailing_whitespace = false
16+
17+
[*.java]
18+
ij_java_method_parameters_new_line_after_left_paren = true
19+
ij_java_method_parameters_right_paren_on_new_line = true
20+
ij_java_call_parameters_new_line_after_left_paren = true
21+
ij_java_call_parameters_right_paren_on_new_line = true
22+
ij_java_align_multiline_parameters = false
23+
ij_java_align_multiline_parameters_in_calls = false
24+
ij_java_keep_line_breaks = true

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* :sparkles: add support for (FR) EnergyBillV1
88
* :sparkles: add support for (FR) PayslipV1
99
* :sparkles: add support for NutritionFactsLabelV1
10+
* :sparkles: add support for (US) HealthcareCardV1
1011

1112
### Fixes
1213
* :bug: fixed a bug that prevented longer decimals from appearing in the string representation of some objects
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.mindee.image;
2+
3+
import java.awt.Graphics2D;
4+
import java.awt.Image;
5+
import java.awt.image.BufferedImage;
6+
import java.io.ByteArrayInputStream;
7+
import java.io.ByteArrayOutputStream;
8+
import java.io.IOException;
9+
import java.util.Iterator;
10+
import javax.imageio.IIOImage;
11+
import javax.imageio.ImageIO;
12+
import javax.imageio.ImageWriteParam;
13+
import javax.imageio.ImageWriter;
14+
15+
/**
16+
* Image compression class.
17+
*/
18+
public final class ImageCompressor {
19+
public static BufferedImage resize(
20+
BufferedImage inputImage, Integer newWidth,
21+
Integer newHeight
22+
) {
23+
Image scaledImage = inputImage.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH);
24+
BufferedImage outImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
25+
26+
Graphics2D g2d = outImage.createGraphics();
27+
g2d.drawImage(scaledImage, 0, 0, null);
28+
g2d.dispose();
29+
30+
return outImage;
31+
}
32+
33+
34+
public static byte[] compressImage(
35+
byte[] imageData, Integer quality, Integer maxWidth,
36+
Integer maxHeight
37+
) throws IOException {
38+
39+
ByteArrayInputStream bis = new ByteArrayInputStream(imageData);
40+
BufferedImage original = ImageIO.read(bis);
41+
ImageUtils.Dimensions dimensions =
42+
ImageUtils.calculateNewDimensions(original, maxWidth, maxHeight);
43+
return compressImage(original, quality, dimensions.width, dimensions.height);
44+
}
45+
46+
public static byte[] compressImage(byte[] imageData, Integer quality, Integer finalWidth)
47+
throws IOException {
48+
return compressImage(imageData, quality, finalWidth, null);
49+
}
50+
51+
public static byte[] compressImage(byte[] imageData, Integer quality) throws IOException {
52+
return compressImage(imageData, quality, null, null);
53+
}
54+
55+
public static byte[] compressImage(byte[] imageData) throws IOException {
56+
return compressImage(imageData, null, null, null);
57+
}
58+
59+
public static byte[] compressImage(
60+
BufferedImage original,
61+
Integer quality,
62+
Integer finalWidth,
63+
Integer finalHeight
64+
) throws IOException {
65+
if (quality == null) {
66+
quality = 85;
67+
}
68+
BufferedImage resizedImage = resize(original, finalWidth, finalHeight);
69+
return encodeToJpegByteArray(resizedImage, (float) quality / 100f);
70+
}
71+
72+
public static BufferedImage removeAlphaChannel(BufferedImage original) {
73+
if (original.getType() == BufferedImage.TYPE_INT_RGB) {
74+
return original;
75+
}
76+
BufferedImage newImage = new BufferedImage(
77+
original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_RGB);
78+
Graphics2D g = newImage.createGraphics();
79+
g.drawImage(original, 0, 0, null);
80+
g.dispose();
81+
return newImage;
82+
}
83+
84+
public static byte[] encodeToJpegByteArray(
85+
BufferedImage image, float quality
86+
) throws IOException {
87+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
88+
89+
90+
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
91+
ImageWriter writer = writers.next();
92+
93+
ImageWriteParam params = writer.getDefaultWriteParam();
94+
params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
95+
params.setCompressionQuality(quality);
96+
97+
writer.setOutput(ImageIO.createImageOutputStream(outputStream));
98+
BufferedImage alphaCheckedImage = removeAlphaChannel(image);
99+
writer.write(null, new IIOImage(alphaCheckedImage, null, null), params);
100+
writer.dispose();
101+
102+
return outputStream.toByteArray();
103+
}
104+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.mindee.image;
2+
3+
import java.awt.image.BufferedImage;
4+
5+
/**
6+
* Image utility class.
7+
*/
8+
public class ImageUtils {
9+
10+
/**
11+
* Dimension class for pages/images.
12+
*/
13+
public static class Dimensions {
14+
public final Integer width;
15+
public final Integer height;
16+
17+
public Dimensions(Integer width, Integer height) {
18+
this.width = width;
19+
this.height = height;
20+
}
21+
}
22+
23+
public static Dimensions calculateNewDimensions(
24+
BufferedImage original,
25+
Integer maxWidth,
26+
Integer maxHeight
27+
) {
28+
if (original == null) {
29+
throw new IllegalArgumentException("Generated image could not be processed for resizing.");
30+
}
31+
32+
if (maxWidth == null && maxHeight == null) {
33+
return new Dimensions(original.getWidth(), original.getHeight());
34+
}
35+
36+
double widthRatio =
37+
maxWidth != null ? (double) maxWidth / original.getWidth() : Double.POSITIVE_INFINITY;
38+
double heightRatio =
39+
maxHeight != null ? (double) maxHeight / original.getHeight() : Double.POSITIVE_INFINITY;
40+
41+
double scaleFactor = Math.min(widthRatio, heightRatio);
42+
43+
int newWidth = (int) (original.getWidth() * scaleFactor);
44+
int newHeight = (int) (original.getHeight() * scaleFactor);
45+
46+
return new Dimensions(newWidth, newHeight);
47+
}
48+
}

src/main/java/com/mindee/input/InputSourceUtils.java

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package com.mindee.input;
22

33
import com.mindee.MindeeException;
4+
import java.io.ByteArrayInputStream;
5+
import java.io.IOException;
46
import java.net.URL;
7+
import javax.imageio.ImageIO;
8+
import org.apache.pdfbox.pdmodel.PDDocument;
9+
import org.apache.pdfbox.text.PDFTextStripper;
510

611
/**
712
* Utilities for working with files.
@@ -44,6 +49,7 @@ public static String getFileExtension(String fileName) {
4449

4550
/**
4651
* Split the filename into a name and an extension.
52+
*
4753
* @param filename the filename to split.
4854
* @return first element is name, second is extension.
4955
*/
@@ -57,14 +63,18 @@ public static String[] splitNameStrict(String filename) throws MindeeException {
5763
} else {
5864
throw new MindeeException("File name must include a valid extension.");
5965
}
60-
return new String[]{name, extension};
66+
return new String[] {name, extension};
6167
}
6268

6369
/**
6470
* Returns true if the file is a PDF.
6571
*/
66-
public static boolean isPdf(String fileName) {
67-
return getFileExtension(fileName).equalsIgnoreCase("pdf");
72+
public static boolean isPdf(byte[] fileBytes) {
73+
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(fileBytes))) {
74+
return true;
75+
} catch (IOException e) {
76+
return false;
77+
}
6878
}
6979

7080
/**
@@ -75,4 +85,33 @@ public static void validateUrl(URL inputUrl) {
7585
throw new MindeeException("Only HTTPS source URLs are allowed");
7686
}
7787
}
88+
89+
/**
90+
* Returns true if the source PDF has source text inside. Returns false for images.
91+
*
92+
* @param fileBytes A byte array representing a PDF.
93+
* @return True if at least one character exists in one page.
94+
* @throws MindeeException if the file could not be read.
95+
*/
96+
public static boolean hasSourceText(byte[] fileBytes) {
97+
try {
98+
PDDocument document = PDDocument.load(new ByteArrayInputStream(fileBytes));
99+
PDFTextStripper stripper = new PDFTextStripper();
100+
101+
for (int i = 0; i < document.getNumberOfPages(); i++) {
102+
stripper.setStartPage(i + 1);
103+
stripper.setEndPage(i + 1);
104+
String pageText = stripper.getText(document);
105+
if (!pageText.trim().isEmpty()) {
106+
document.close();
107+
return true;
108+
}
109+
}
110+
document.close();
111+
} catch (IOException e) {
112+
return false;
113+
}
114+
115+
return false;
116+
}
78117
}

src/main/java/com/mindee/input/LocalInputSource.java

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.mindee.input;
22

3+
import com.mindee.image.ImageCompressor;
4+
import com.mindee.pdf.PdfCompressor;
35
import java.io.File;
46
import java.io.IOException;
57
import java.io.InputStream;
@@ -14,7 +16,7 @@
1416
@Getter
1517
public final class LocalInputSource {
1618

17-
private final byte[] file;
19+
private byte[] file;
1820
private final String filename;
1921

2022
public LocalInputSource(InputStream file, String filename) throws IOException {
@@ -44,6 +46,45 @@ public LocalInputSource(String fileAsBase64, String filename) {
4446
}
4547

4648
public boolean isPdf() {
47-
return InputSourceUtils.isPdf(this.filename);
49+
return InputSourceUtils.isPdf(this.file);
50+
}
51+
52+
public boolean hasSourceText() {
53+
return InputSourceUtils.hasSourceText(this.file);
54+
}
55+
56+
public void compress(
57+
Integer quality, Integer maxWidth, Integer maxHeight,
58+
Boolean forceSourceText, Boolean disableSourceText
59+
)
60+
throws IOException {
61+
if (isPdf()) {
62+
this.file = PdfCompressor.compressPdf(this.file, quality, forceSourceText, disableSourceText);
63+
} else {
64+
this.file = ImageCompressor.compressImage(this.file, quality, maxWidth, maxHeight);
65+
}
66+
}
67+
68+
public void compress(
69+
Integer quality, Integer maxWidth, Integer maxHeight,
70+
Boolean forceSourceText
71+
) throws IOException {
72+
this.compress(quality, maxWidth, maxHeight, forceSourceText, true);
73+
}
74+
75+
public void compress(Integer quality, Integer maxWidth, Integer maxHeight) throws IOException {
76+
this.compress(quality, maxWidth, maxHeight, false, true);
77+
}
78+
79+
public void compress(Integer quality, Integer maxWidth) throws IOException {
80+
this.compress(quality, maxWidth, null, false, true);
81+
}
82+
83+
public void compress(Integer quality) throws IOException {
84+
this.compress(quality, null, null, false, true);
85+
}
86+
87+
public void compress() throws IOException {
88+
this.compress(85, null, null, false, true);
4889
}
4990
}

0 commit comments

Comments
 (0)