Chuyển đổi Hình ảnh sang PDF và ngược lại bằng Java

A. Cấu hình Maven Để thực hiện chức năng chuyển đổi giữa hình ảnh và PDF, chúng ta cần tích hợp các thư viện cần thiết vào dự án Java. Dưới đây là danh sách các dependency cần thêm vào tệp pom.xml:

<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>fontbox</artifactId>
    <version>2.0.27</version>
</dependency>
<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>2.0.27</version>
</dependency>
<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox-tools</artifactId>
    <version>2.0.27</version>
</dependency>
<dependency>
    <groupId>com.lowagie</groupId>
    <artifactId>itext</artifactId>
    <version>2.1.7</version>
</dependency>

B. Triển khai dịch vụ chuyển đổi

Chúng ta sẽ xây dựng một lớp dịch vụ (ImgPdfServiceImpl) để xử lý các yêu cầu chuyển đổi. Lớp này sẽ cung cấp các phương thức để chuyển đổi nhiều tệp hình ảnh thành một tệp PDF duy nhất, chuyển đổi danh sách URL hình ảnh thành PDF và chuyển đổi tệp PDF thành hình ảnh.

  1. Chuyển đổi tệp hình ảnh tải lên thành PDF (uploadConvertPdf): Phương thức này nhận vào một mảng các tệp MultipartFile, một tỷ lệ nén (ratio) và kích thước giấy (paperSize). Nó sẽ tạo ra một tệp PDF và ghi các hình ảnh đầu vào vào đó. Các định dạng hình ảnh được hỗ trợ là JPG và PNG. Nếu kích thước giấy không được chỉ định, mặc định sẽ sử dụng khổ A4.
import cn.hutool.http.HttpUtil;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Image;
import com.lowagie.text.pdf.PdfWriter;
import com.zz.business.service.ImgPdfService;
// Các import khác nếu cần
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@Service
public class ImgPdfServiceImpl implements ImgPdfService {

    // Định nghĩa các enum hoặc hằng số cho ContentType, CharacterEncoding, ResponseHeader, FileFormat, PaperSize, StatusCode
    // Ví dụ:
    private enum ContentTypeEnum { DOWNLOAD("application/octet-stream");
        private String value;
        ContentTypeEnum(String value) { this.value = value; }
        public String getValue() { return value; }
    }
    private enum CharacterEncodingEnum { UTF_8("UTF-8");
        private String code;
        CharacterEncodingEnum(String code) { this.code = code; }
        public String getCode() { return code; }
    }
    private enum ResponseHeaderEnum { CONTENT_DISPOSITION("Content-Disposition", "attachment;filename=");
        private String code;
        private String value;
        ResponseHeaderEnum(String code, String value) { this.code = code; this.value = value; }
        public String getCode() { return code; }
        public String getValue() { return value; }
    }
    private enum FileFormatEnum { DOT("."), JPG("jpg"), PNG("png"), PDF("pdf");
        private String code;
        FileFormatEnum(String code) { this.code = code; }
        public String getCode() { return code; }
    }
     private enum PaperSizeEnum {
        A4("A4", 595.28f, 841.89f); // Ví dụ kích thước A4 theo points
        private String size;
        private float width;
        private float height;
        PaperSizeEnum(String size, float width, float height) {
            this.size = size;
            this.width = width;
            this.height = height;
        }
        public String getSize() { return size; }
        public float getWidth() { return width; }
        public float getHeight() { return height; }
    }
    private enum StatusCode {
        UPLOAD_FORMAT_IS_EMPTY(1001, "Tệp tải lên trống"),
        UPLOAD_FORMAT_NO_NAME(1002, "Tên tệp tải lên không hợp lệ"),
        INCORRECT_UPLOAD_FORMAT(1003, "Định dạng tệp tải lên không chính xác"),
        PDF_DOC_HANDLE_EXCEPTION(1004, "Lỗi xử lý tài liệu PDF"),
        FILE_HANDLE_EXCEPTION(1005, "Lỗi xử lý tệp");
        private int code;
        private String message;
        StatusCode(int code, String message) { this.code = code; this.message = message; }
        public int getCode() { return code; }
        public String getMessage() { return message; }
    }
    // Lớp BusinessException để xử lý ngoại lệ kinh doanh
    public static class BusinessException extends RuntimeException {
        private StatusCode status;
        public BusinessException(StatusCode status) {
            super(status.getMessage());
            this.status = status;
        }
        public StatusCode getStatus() { return status; }
    }
    // Lớp DocumentUtil để hỗ trợ thêm ảnh vào tài liệu
    public static class DocumentUtil {
        public static void addImageToDocument(Document document, byte[] imageData, int imageRatio, float pageWidth, float pageHeight) throws DocumentException, IOException {
            Image image = Image.getInstance(imageData);
            // Điều chỉnh kích thước ảnh theo tỷ lệ và kích thước trang
            float scaleX = pageWidth / image.getWidth() * (imageRatio / 100.0f);
            float scaleY = pageHeight / image.getHeight() * (imageRatio / 100.0f);
            float scale = Math.min(scaleX, scaleY);
            image.scalePercent(scale * 100);
            document.add(image);
        }
    }


    @Override
    public boolean uploadConvertPdf(HttpServletResponse response, MultipartFile[] files, int ratio, String paperSize) {
        response.reset();
        response.setContentType(ContentTypeEnum.DOWNLOAD.getValue());
        response.setCharacterEncoding(CharacterEncodingEnum.UTF_8.getCode());
        response.setHeader(ResponseHeaderEnum.CONTENT_DISPOSITION.getCode(),
                ResponseHeaderEnum.CONTENT_DISPOSITION.getValue() + System.currentTimeMillis() + ".pdf");

        try (OutputStream ops = response.getOutputStream()) {
            Document doc = new Document();
            PdfWriter.getInstance(doc, ops);
            doc.open();

            float targetWidth = PaperSizeEnum.A4.getWidth();
            float targetHeight = PaperSizeEnum.A4.getHeight();
            if (paperSize != null && !paperSize.isEmpty()) {
                // Tìm kích thước giấy tương ứng hoặc sử dụng mặc định
                for (PaperSizeEnum size : PaperSizeEnum.values()) {
                    if (size.getSize().equalsIgnoreCase(paperSize)) {
                        targetWidth = size.getWidth();
                        targetHeight = size.getHeight();
                        break;
                    }
                }
            }


            for (MultipartFile file : files) {
                if (file.isEmpty()) {
                    throw new BusinessException(StatusCode.UPLOAD_FORMAT_IS_EMPTY);
                }
                String originalFilename = file.getOriginalFilename();
                if (originalFilename == null || originalFilename.isEmpty()) {
                    throw new BusinessException(StatusCode.UPLOAD_FORMAT_NO_NAME);
                }

                String suffix = getFileSuffix(originalFilename);
                String fileFormat = suffix.substring(1).toLowerCase();

                if (!suffix.startsWith(FileFormatEnum.DOT.getCode()) ||
                    !(FileFormatEnum.JPG.getCode().equals(fileFormat) || FileFormatEnum.PNG.getCode().equals(fileFormat))) {
                    throw new BusinessException(StatusCode.INCORRECT_UPLOAD_FORMAT);
                }

                DocumentUtil.addImageToDocument(doc, file.getBytes(), ratio, targetWidth, targetHeight);
            }
            doc.close();
            ops.flush();
        } catch (DocumentException | IOException e) {
            log.error("ImgPdfServiceImpl#uploadConvertPdf: Failed to generate PDF document", e);
            throw new BusinessException(StatusCode.PDF_DOC_HANDLE_EXCEPTION);
        }
        return true;
    }

    // ... các phương thức khác ...
}
  1. Chuyển đổi URL hình ảnh thành PDF (webImgConvertPdf): Phương thức này nhận vào một danh sách các URL hình ảnh, một tỷ lệ nén và kích thước giấy. Nó sẽ tải hình ảnh từ các URL, sau đó thêm chúng vào một tệp PDF.
// Trong lớp ImgPdfServiceImpl
@Override
public boolean webImgConvertPdf(HttpServletResponse response, List<String> imgUrlList, int ratio, String paperSize) {
    response.reset();
    response.setContentType(ContentTypeEnum.DOWNLOAD.getValue());
    response.setCharacterEncoding(CharacterEncodingEnum.UTF_8.getCode());
    response.setHeader(ResponseHeaderEnum.CONTENT_DISPOSITION.getCode(),
            ResponseHeaderEnum.CONTENT_DISPOSITION.getValue() + System.currentTimeMillis() + ".pdf");

    try (OutputStream ops = response.getOutputStream()) {
        Document doc = new Document();
        PdfWriter.getInstance(doc, ops);
        doc.open();

        float targetWidth = PaperSizeEnum.A4.getWidth();
        float targetHeight = PaperSizeEnum.A4.getHeight();
         if (paperSize != null && !paperSize.isEmpty()) {
            for (PaperSizeEnum size : PaperSizeEnum.values()) {
                if (size.getSize().equalsIgnoreCase(paperSize)) {
                    targetWidth = size.getWidth();
                    targetHeight = size.getHeight();
                    break;
                }
            }
        }

        for (String webPath : imgUrlList) {
            byte[] imgBytes = HttpUtil.downloadBytes(webPath);
            if (imgBytes != null && imgBytes.length > 0) {
                DocumentUtil.addImageToDocument(doc, imgBytes, ratio, targetWidth, targetHeight);
            } else {
                 log.warn("Failed to download image from URL: {}", webPath);
                 // Có thể throw exception hoặc bỏ qua tùy yêu cầu
            }
        }
        doc.close();
        ops.flush();
    } catch (DocumentException | IOException e) {
        log.error("ImgPdfServiceImpl#webImgConvertPdf: Failed to generate PDF document", e);
        throw new BusinessException(StatusCode.PDF_DOC_HANDLE_EXCEPTION);
    }
    return true;
}
  1. Chuyển đổi PDF thành Hình ảnh (uploadConvertImg): Phương thức này nhận vào một tệp PDF MultipartFile, định dạng hình ảnh đầu ra mong muốn (imgType) và một cờ báo hiệu liệu có chuyển thành ảnh dài hay không (longFlag). Nó sử dụng thư viện PDFBox để render từng trang của PDF thành hình ảnh. Kết quả có thể là một ảnh dài duy nhất hoặc nhiều ảnh được nén vào tệp ZIP.
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.rendering.PDFRenderer;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

// Trong lớp ImgPdfServiceImpl
@Override
public boolean uploadConvertImg(HttpServletResponse response, MultipartFile file, String imgType, boolean longFlag) {
    String suffix = getFileSuffix(file.getOriginalFilename());
    String fileFormat = suffix.substring(1).toLowerCase();
    String fileNameWithoutExt = getFileNameWithoutExtension(file.getOriginalFilename());

    if (!suffix.startsWith(FileFormatEnum.DOT.getCode()) || !FileFormatEnum.PDF.getCode().equals(fileFormat)) {
        throw new BusinessException(StatusCode.INCORRECT_UPLOAD_FORMAT);
    }

    try (InputStream stream = new ByteArrayInputStream(file.getBytes())) {
        if (file.isEmpty()) {
            throw new BusinessException(StatusCode.UPLOAD_FORMAT_IS_EMPTY);
        }

        PDDocument doc = PDDocument.load(stream);
        PDFRenderer pdfRenderer = new PDFRenderer(doc);
        PDPageTree pages = doc.getPages();
        int pageCount = pages.getCount();

        response.reset();
        response.setContentType(ContentTypeEnum.DOWNLOAD.getValue());
        response.setCharacterEncoding(CharacterEncodingEnum.UTF_8.getCode());

        if (pageCount == 1 || longFlag) {
            renderAndFlushLongImage(response, imgType, pageCount, pdfRenderer, fileNameWithoutExt);
        } else {
            renderAndFlushZipArchive(response, imgType, pageCount, pdfRenderer, fileNameWithoutExt);
            log.info("File compression complete.");
        }
        doc.close();
    } catch (IOException e) {
        log.error("ImgPdfServiceImpl#uploadConvertImg: IO error during conversion", e);
        throw new BusinessException(StatusCode.FILE_HANDLE_EXCEPTION);
    }
    return true;
}

private void renderAndFlushLongImage(HttpServletResponse response, String imgType, int pageCount, PDFRenderer pdfRenderer, String fileName) {
    String outputFormat = FileFormatEnum.PNG.getCode().equalsIgnoreCase(imgType) ? FileFormatEnum.PNG.getCode() : FileFormatEnum.JPG.getCode();
    response.setHeader(ResponseHeaderEnum.CONTENT_DISPOSITION.getCode(), ResponseHeaderEnum.CONTENT_DISPOSITION.getValue() + fileName + "." + outputFormat);

    List<BufferedImage> images = new ArrayList<>(pageCount);
    int totalHeight = 0;
    int maxWidth = 0;

    try {
        for (int i = 0; i < pageCount; i++) {
            BufferedImage image = pdfRenderer.renderImageWithDPI(i, 200); // DPI có thể điều chỉnh
            images.add(image);
            maxWidth = Math.max(maxWidth, image.getWidth());
            totalHeight += image.getHeight();
        }

        if (totalHeight == 0) {
            log.info("ImgPdfServiceImpl#renderAndFlushLongImage: Total image height is zero.");
            return;
        }

        BufferedImage combinedImage = new BufferedImage(maxWidth, totalHeight, BufferedImage.TYPE_INT_RGB);
        int currentHeight = 0;
        for (BufferedImage img : images) {
            combinedImage.getGraphics().drawImage(img, 0, currentHeight, null);
            currentHeight += img.getHeight();
        }

        ByteArrayOutputStream os = new ByteArrayOutputStream();
        ImageIO.write(combinedImage, outputFormat, os);

        byte[] imageBytes = os.toByteArray();
        try (InputStream input = new ByteArrayInputStream(imageBytes);
             BufferedInputStream bis = new BufferedInputStream(input);
             OutputStream ops = response.getOutputStream()) {

            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = bis.read(buffer)) != -1) {
                ops.write(buffer, 0, bytesRead);
            }
            ops.flush();
        }
    } catch (IOException e) {
        log.error("ImgPdfServiceImpl#renderAndFlushLongImage: IO error while rendering image", e);
        throw new BusinessException(StatusCode.FILE_HANDLE_EXCEPTION);
    }
}

private void renderAndFlushZipArchive(HttpServletResponse response, String imgType, int pageCount, PDFRenderer pdfRenderer, String fileName) {
    String outputFormat = FileFormatEnum.PNG.getCode().equalsIgnoreCase(imgType) ? FileFormatEnum.PNG.getCode() : FileFormatEnum.JPG.getCode();
    response.setHeader(ResponseHeaderEnum.CONTENT_DISPOSITION.getCode(), ResponseHeaderEnum.CONTENT_DISPOSITION.getValue() + fileName + ".zip");

    try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
         ZipOutputStream out = new ZipOutputStream(bos)) {

        byte[] buffer = new byte[1024];
        for (int i = 0; i < pageCount; i++) {
            BufferedImage img = pdfRenderer.renderImageWithDPI(i, 200);
            ByteArrayOutputStream bs = new ByteArrayOutputStream();
            ImageOutputStream imOut = ImageIO.createImageOutputStream(bs);
            ImageIO.write(img, outputFormat, imOut);
            imOut.close();

            ByteArrayInputStream inputStream = new ByteArrayInputStream(bs.toByteArray());
            out.putNextEntry(new ZipEntry(String.format("%s_%d.%s", fileName, i, outputFormat)));

            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            inputStream.close();
            out.closeEntry();
        }
    } catch (IOException e) {
        log.error("ImgPdfServiceImpl#renderAndFlushZipArchive: IO error while creating zip archive", e);
        throw new BusinessException(StatusCode.FILE_HANDLE_EXCEPTION);
    }
}

private String getFileSuffix(String filename) {
    if (filename == null) return "";
    int dotIndex = filename.lastIndexOf('.');
    if (dotIndex == -1) return "";
    return filename.substring(dotIndex);
}

private String getFileNameWithoutExtension(String filename) {
    if (filename == null) return "output";
    int dotIndex = filename.lastIndexOf('.');
    if (dotIndex == -1) return filename;
    return filename.substring(0, dotIndex);
}

C. Các lớp tiện ích và Enum Để mã nguồn rõ ràng và dễ bảo trì, các giá trị cố định như Content Type, Encoding, Header Response, định dạng file và kích thước giấy nên được định nghĩa dưới dạng Enum. Tương tự, các lớp tiện ích như DocumentUtil và lớp xử lý ngoại lệ BusinessException cũng giúp cấu trúc code tốt hơn.

  • Enums: ContentTypeEnum, CharacterEncodingEnum, ResponseHeaderEnum, FileFormatEnum, PaperSizeEnum, StatusCode.
  • Utility Class: DocumentUtil (để thêm ảnh vào tài liệu iText).
  • Exception Class: BusinessException (để xử lý các lỗi nghiệp vụ).

Thẻ: Java PDF Image conversion PDFBox

Đăng vào ngày 3 tháng 8 lúc 06:41