package cn.iocoder.yudao.module.qcreport.service.aiimport.document; import cn.iocoder.yudao.module.qcreport.config.QcReportAiImportProperties; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.text.PDFTextStripper; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_FILE_EMPTY; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.AI_IMPORT_TOO_MANY_PAGES; /** * PDF 抽取器,双通道。 *
* 通道判定依据是「这份 PDF 有没有文本层」,不是「它是不是 PDF」。 * 这个区别很实际:电子版 PDF 抽出文本只要几十 KB、读得也准;扫描版 PDF 抽出来是空的, * 只能把页面渲染成图交给多模态模型——成本高一个数量级,但这是唯一能读出字的办法。 * 判据是平均每页字符数超过阈值。 *
* 刻意不做混合型 PDF 的逐页通路(有的页有文本层、有的没有):那需要按页决定通道,
* 调用次数翻倍而收益只在少数文件上。整份文件按一个通道走,行为可预期。
*/
@Slf4j
@Component
@Order(2)
public class PdfImportAdapter implements QcReportImportAdapter {
@Resource
private QcReportAiImportProperties properties;
@Override
public boolean supports(String extension, String contentType) {
return "pdf".equals(extension) || "application/pdf".equalsIgnoreCase(contentType);
}
@Override
public QcReportDocumentExtract extract(byte[] content, String originalFilename) {
if (content == null || content.length == 0) {
throw exception(AI_IMPORT_FILE_EMPTY, originalFilename, 0);
}
try (PDDocument document = Loader.loadPDF(content)) {
int pageCount = document.getNumberOfPages();
if (pageCount <= 0) {
throw exception(AI_IMPORT_FILE_EMPTY, originalFilename, 0);
}
// 超限直接报错而不是截断:静默截断会让用户以为整份文件都识别过了,
// 拿到的模板少了几页却没有任何提示,比报错危险得多
if (pageCount > properties.getMaxPagesPerFile()) {
throw exception(AI_IMPORT_TOO_MANY_PAGES, originalFilename, pageCount,
properties.getMaxPagesPerFile());
}
List
* 渲染失败不吞:回落去试文本通道,两条路都不通才报「未解析出任何可用内容」。
* 之所以要回落,是因为「字符数低于阈值」可能只是文件用的字体没带 ToUnicode 表
* (抽出来是乱码或空),但渲染本身还是好的——不过那种情况下回落取到的文本也是空的,
* 所以真正会落到 {@link #extractPageTexts} 的场景是渲染器对这个文件不工作。
*/
private QcReportDocumentExtract renderPages(PDDocument document, int pageCount,
String originalFilename, List