diff --git a/backend/src/main/java/com/crystalpdf/backend/service/ExtractImagesService.java b/backend/src/main/java/com/crystalpdf/backend/service/ExtractImagesService.java index 93be443..426ac8f 100644 --- a/backend/src/main/java/com/crystalpdf/backend/service/ExtractImagesService.java +++ b/backend/src/main/java/com/crystalpdf/backend/service/ExtractImagesService.java @@ -14,6 +14,7 @@ import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.Locale; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -24,7 +25,12 @@ public class ExtractImagesService { * Extract all images from a PDF and return them as a ZIP file. */ public byte[] extractImages(byte[] pdfBytes, String format) throws IOException { - String imgFormat = (format != null && !format.isBlank()) ? format : "png"; + String imgFormat = (format != null && !format.isBlank()) + ? format.toLowerCase(Locale.ROOT) + : "png"; + if (!imgFormat.equals("png") && !imgFormat.equals("jpg") && !imgFormat.equals("jpeg")) { + throw new IllegalArgumentException("Unsupported image output format: " + imgFormat); + } try (PDDocument doc = Loader.loadPDF(new RandomAccessReadBuffer(pdfBytes)); ByteArrayOutputStream zipOut = new ByteArrayOutputStream(); @@ -35,18 +41,20 @@ public byte[] extractImages(byte[] pdfBytes, String format) throws IOException { for (int pageIdx = 0; pageIdx < doc.getNumberOfPages(); pageIdx++) { PDPage page = doc.getPage(pageIdx); PDResources resources = page.getResources(); - if (resources == null) continue; + if (resources == null) { + continue; + } for (COSName name : resources.getXObjectNames()) { PDXObject xobj = resources.getXObject(name); if (xobj instanceof PDImageXObject image) { BufferedImage bimg = image.getImage(); - ByteArrayOutputStream imgBytes = new ByteArrayOutputStream(); - ImageIO.write(bimg, imgFormat, imgBytes); String entryName = String.format("page%d_img%d.%s", pageIdx + 1, imgIndex++, imgFormat); zos.putNextEntry(new ZipEntry(entryName)); - zos.write(imgBytes.toByteArray()); + if (!ImageIO.write(bimg, imgFormat, zos)) { + throw new IllegalArgumentException("Unsupported image output format: " + imgFormat); + } zos.closeEntry(); } } diff --git a/backend/src/main/java/com/crystalpdf/backend/service/PdfToImageService.java b/backend/src/main/java/com/crystalpdf/backend/service/PdfToImageService.java index 44503e6..7bf2069 100644 --- a/backend/src/main/java/com/crystalpdf/backend/service/PdfToImageService.java +++ b/backend/src/main/java/com/crystalpdf/backend/service/PdfToImageService.java @@ -31,12 +31,11 @@ public byte[] convert(byte[] pdfBytes, String format, int dpi) throws IOExceptio for (int i = 0; i < doc.getNumberOfPages(); i++) { BufferedImage image = renderer.renderImageWithDPI(i, dpi, ImageType.RGB); - ByteArrayOutputStream imgOut = new ByteArrayOutputStream(); - ImageIO.write(image, imageioFormat, imgOut); - ZipEntry entry = new ZipEntry(String.format("page_%03d.%s", i + 1, fmt)); zos.putNextEntry(entry); - zos.write(imgOut.toByteArray()); + if (!ImageIO.write(image, imageioFormat, zos)) { + throw new IOException("Unsupported image output format: " + imageioFormat); + } zos.closeEntry(); } diff --git a/backend/src/test/java/com/crystalpdf/backend/helper/PdfTestHelper.java b/backend/src/test/java/com/crystalpdf/backend/helper/PdfTestHelper.java index 4ccfcf4..bb428e4 100644 --- a/backend/src/test/java/com/crystalpdf/backend/helper/PdfTestHelper.java +++ b/backend/src/test/java/com/crystalpdf/backend/helper/PdfTestHelper.java @@ -2,8 +2,14 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -34,4 +40,34 @@ public static byte[] createPdf(int pageCount) throws IOException { public static byte[] createPdf() throws IOException { return createPdf(1); } + + /** + * Returns a single-page PDF with one embedded image XObject. + */ + public static byte[] createPdfWithImage() throws IOException { + try (PDDocument doc = new PDDocument(); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + PDPage page = new PDPage(PDRectangle.A4); + doc.addPage(page); + + BufferedImage image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = image.createGraphics(); + try { + graphics.setColor(Color.CYAN); + graphics.fillRect(0, 0, 32, 32); + graphics.setColor(Color.BLUE); + graphics.fillOval(8, 8, 16, 16); + } finally { + graphics.dispose(); + } + + PDImageXObject ximage = LosslessFactory.createFromImage(doc, image); + try (PDPageContentStream content = new PDPageContentStream(doc, page)) { + content.drawImage(ximage, 72, 720, 32, 32); + } + + doc.save(out); + return out.toByteArray(); + } + } } diff --git a/backend/src/test/java/com/crystalpdf/backend/service/ExtractImagesServiceTest.java b/backend/src/test/java/com/crystalpdf/backend/service/ExtractImagesServiceTest.java new file mode 100644 index 0000000..c5314f8 --- /dev/null +++ b/backend/src/test/java/com/crystalpdf/backend/service/ExtractImagesServiceTest.java @@ -0,0 +1,49 @@ +package com.crystalpdf.backend.service; + +import com.crystalpdf.backend.helper.PdfTestHelper; +import org.junit.jupiter.api.Test; + +import javax.imageio.ImageIO; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ExtractImagesServiceTest { + + private final ExtractImagesService extractImagesService = new ExtractImagesService(); + + @Test + void extractImages_returnsZipWithDecodableImages() throws Exception { + byte[] pdf = PdfTestHelper.createPdfWithImage(); + byte[] zipBytes = extractImagesService.extractImages(pdf, "png"); + + try (ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { + ZipEntry entry = zip.getNextEntry(); + assertThat(entry).isNotNull(); + assertThat(entry.getName()).isEqualTo("page1_img1.png"); + assertThat(ImageIO.read(new ByteArrayInputStream(readEntry(zip)))).isNotNull(); + zip.closeEntry(); + + assertThat(zip.getNextEntry()).isNull(); + } + } + + @Test + void extractImages_throwsForUnsupportedFormat() throws Exception { + byte[] pdf = PdfTestHelper.createPdfWithImage(); + + assertThatThrownBy(() -> extractImagesService.extractImages(pdf, "tiff")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unsupported image output format: tiff"); + } + + private static byte[] readEntry(ZipInputStream zip) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + zip.transferTo(out); + return out.toByteArray(); + } +} diff --git a/backend/src/test/java/com/crystalpdf/backend/service/PdfToImageServiceTest.java b/backend/src/test/java/com/crystalpdf/backend/service/PdfToImageServiceTest.java new file mode 100644 index 0000000..006a885 --- /dev/null +++ b/backend/src/test/java/com/crystalpdf/backend/service/PdfToImageServiceTest.java @@ -0,0 +1,45 @@ +package com.crystalpdf.backend.service; + +import com.crystalpdf.backend.helper.PdfTestHelper; +import org.junit.jupiter.api.Test; + +import javax.imageio.ImageIO; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static org.assertj.core.api.Assertions.assertThat; + +class PdfToImageServiceTest { + + private final PdfToImageService pdfToImageService = new PdfToImageService(); + + @Test + void convert_returnsZipWithDecodablePageImages() throws Exception { + byte[] pdf = PdfTestHelper.createPdf(2); + byte[] zipBytes = pdfToImageService.convert(pdf, "png", 72); + + try (ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { + ZipEntry first = zip.getNextEntry(); + assertThat(first).isNotNull(); + assertThat(first.getName()).isEqualTo("page_001.png"); + assertThat(ImageIO.read(new ByteArrayInputStream(readEntry(zip)))).isNotNull(); + zip.closeEntry(); + + ZipEntry second = zip.getNextEntry(); + assertThat(second).isNotNull(); + assertThat(second.getName()).isEqualTo("page_002.png"); + assertThat(ImageIO.read(new ByteArrayInputStream(readEntry(zip)))).isNotNull(); + zip.closeEntry(); + + assertThat(zip.getNextEntry()).isNull(); + } + } + + private static byte[] readEntry(ZipInputStream zip) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + zip.transferTo(out); + return out.toByteArray(); + } +}