使用Java實(shí)現(xiàn)轉(zhuǎn)換掃描的文檔為可搜索的PDF
在上一篇文章中,我們編寫了一個(gè)JavaFX的演示程序,使用Dynamsoft Service的REST API掃描文檔。該演示程序可以通過(guò)TWAIN、WIA、SANE和ICA等協(xié)議用文檔掃描儀掃描文檔,并使用PDFBox將文檔保存到PDF文件中。
在本文中,我們將擴(kuò)展它的功能,讓它能將掃描的文檔轉(zhuǎn)換為可搜索的PDF。
打開(kāi)可搜索的PDF時(shí),我們可以直接選擇文本和搜索關(guān)鍵字。生成可搜索的PDF對(duì)于文檔索引或管理系統(tǒng)非常有用。
如果PDF是用InDesign和Word等工具生成的,那么它的文本已經(jīng)可以搜索了。但是,如果PDF包含掃描的圖像,我們需要添加額外的文本覆蓋層,使其可搜索。
掃描文檔的OCR
我們首先需要用OCR得到掃描圖像中的文字和其位置信息。有各種OCR引擎或API服務(wù)可以調(diào)用。這里,我們使用OCRSpace的免費(fèi)OCR API。
1.創(chuàng)建相關(guān)定義。
TextLine.class
:
public class TextLine { public double left; public double top; public double width; public double height; public String text; public TextLine(double left, double top, double width, double height, String text) { this.left = left; this.top = top; this.width = width; this.height = height; this.text = text; } }
OCRResult.class
:
public class OCRResult { public ArrayList<TextLine> lines = new ArrayList<TextLine>(); }
2.創(chuàng)建一個(gè)OCRSpace
類,該類提供從base64編碼圖像獲取OCR結(jié)果的靜態(tài)方法。
public class OCRSpace { public static String key = ""; public static String lang = "eng"; /** * Get OCR result from a base64-encoded image in JPEG format * * @param base64 - base64-encoded image * */ public static OCRResult detect(String base64) throws IOException { OCRResult result = new OCRResult(); OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(120, TimeUnit.SECONDS) .build(); RequestBody requestBody=new FormBody.Builder() .add("apikey",key) .add("language",lang) .add("base64Image","data:image/jpeg;base64,"+base64.trim()) .add("isOverlayRequired","true") .build(); Request httpRequest = new Request.Builder() .url("https://api.ocr.space/parse/image") .post(requestBody) .build(); try (Response response = client.newCall(httpRequest).execute()) { try { String json = response.body().string(); parse(json,result); } catch (IOException e) { throw new RuntimeException(e); } } return result; } private static void parse(String json,OCRResult ocrResult) throws JsonProcessingException { ObjectMapper objectMapper = new ObjectMapper(); Map<String,Object> body = objectMapper.readValue(json,new TypeReference<Map<String,Object>>() {}); List<Map<String,Object>> parsedResults = (List<Map<String, Object>>) body.get("ParsedResults"); for (Map<String,Object> parsedResult:parsedResults) { Map<String,Object> textOverlay = (Map<String, Object>) parsedResult.get("TextOverlay"); List<Map<String,Object>> lines = (List<Map<String, Object>>) textOverlay.get("Lines"); for (Map<String,Object> line:lines) { TextLine textLine = parseAsTextLine(line); ocrResult.lines.add(textLine); } } } private static TextLine parseAsTextLine(Map<String,Object> line){ String lineText = (String) line.get("LineText"); List<Map<String,Object>> words = (List<Map<String, Object>>) line.get("Words"); int minX = (int)((double) words.get(0).get("Left")); int minY = (int)((double) words.get(0).get("Top")); int maxX = 0; int maxY = 0; for (Map<String,Object> word:words) { int x = (int)((double) word.get("Left")); int y = (int)((double) word.get("Top")); int width = (int)((double) word.get("Width")); int height = (int)((double) word.get("Height")); minX = Math.min(minX,x); minY = Math.min(minY,y); maxX = Math.max(maxX,x+width); maxY = Math.max(maxY,y+height); } return new TextLine(minX,minY,maxX - minX,maxY-minY,lineText); } }
使用OKHttp用于HTTP請(qǐng)求,Jackson用于處理JSON。
將文本覆蓋層添加到PDF頁(yè)面中
創(chuàng)建SearchablePDFCreator
類以提供相關(guān)方法。
public class SearchablePDFCreator {}
添加addTextOverlay
方法,將文本覆蓋層添加到現(xiàn)有的PDF頁(yè)面。
/** * Add text overlay to an existing PDF page * @param contentStream - PDF content stream * @param result - OCR result * @param pageHeight - Height of the image * @param pdFont - Specify a font for evaluation of the position * @param percent - image's height / page's height */ public static void addTextOverlay(PDPageContentStream contentStream,OCRResult result, double pageHeight, PDFont pdFont,double percent) throws IOException { PDFont font = pdFont; contentStream.setFont(font, 16); contentStream.setRenderingMode(RenderingMode.NEITHER); for (int i = 0; i <result.lines.size() ; i++) { TextLine line = result.lines.get(i); FontInfo fi = calculateFontSize(font,line.text, (float) (line.width * percent), (float) (line.height * percent)); contentStream.beginText(); contentStream.setFont(font, fi.fontSize); contentStream.newLineAtOffset((float) (line.left * percent), (float) ((pageHeight - line.top - line.height) * percent)); contentStream.showText(line.text); contentStream.endText(); } } private static FontInfo calculateFontSize(PDFont font, String text, float bbWidth, float bbHeight) throws IOException { int fontSize = 17; float textWidth = font.getStringWidth(text) / 1000 * fontSize; float textHeight = font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize; if(textWidth > bbWidth){ while(textWidth > bbWidth){ fontSize -= 1; textWidth = font.getStringWidth(text) / 1000 * fontSize; textHeight = font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize; } } else if(textWidth < bbWidth){ while(textWidth < bbWidth){ fontSize += 1; textWidth = font.getStringWidth(text) / 1000 * fontSize; textHeight = font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize; } } FontInfo fi = new FontInfo(); fi.fontSize = fontSize; fi.textHeight = textHeight; fi.textWidth = textWidth; return fi; }
字體大小是根據(jù)指定的字體和文字行寬度自動(dòng)計(jì)算的。
添加addPage
方法,可以將文本覆蓋層與圖像一起添加為文檔的新頁(yè)面。
public static void addPage(byte[] imageBytes,OCRResult result, PDDocument document,int pageIndex,PDFont pdFont) throws IOException { ByteArrayInputStream in = new ByteArrayInputStream(imageBytes); BufferedImage bi = ImageIO.read(in); // Create a new PDF page PDRectangle rect = new PDRectangle((float) bi.getWidth(),(float) bi.getHeight()); PDPage page = new PDPage(rect); document.addPage(page); PDPageContentStream contentStream = new PDPageContentStream(document, page); PDImageXObject image = PDImageXObject.createFromByteArray(document,imageBytes,String.valueOf(pageIndex)); contentStream.drawImage(image, 0, 0); addTextOverlay(contentStream,result,bi.getHeight(),pdFont); contentStream.close(); }
讓我們檢查一下添加結(jié)果。
使用RenderingMode.NEITHER
將使文本圖層不可見(jiàn)。我們可以把這一行注釋掉,以看到添加上去的文字。以下是覆蓋了文本的PDF文件的一個(gè)區(qū)域。我們可以看到文本與圖像基本吻合。
將掃描的文檔圖像保存為可搜索的PDF
接下來(lái),我們可以嘗試使用我們剛剛編寫的類從圖像文件創(chuàng)建一個(gè)可搜索的PDF。
File image = new File("F://WebTWAINImage.jpg"); byte[] byteArray = new byte[(int) image.length()]; try (FileInputStream inputStream = new FileInputStream(image)) { inputStream.read(byteArray); } catch (IOException e) { throw new RuntimeException(e); } String base64 = Base64.getEncoder().encodeToString(byteArray); OCRSpace.key = "your key"; OCRResult result = OCRSpace.detect(base64); PDDocument document = new PDDocument(); SearchablePDFCreator.addPage(byteArray,result,document,0); document.save(new File("F://output.pdf")); document.close();
使用Searchable PDF Creator加強(qiáng)JavaFX演示程序
編輯pom.xml
,將庫(kù)添加為依賴項(xiàng)。
<repositories> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository> </repositories> <dependencies> <dependency> <groupId>com.github.tony-xlh</groupId> <artifactId>searchablePDF4j</artifactId> <version>1.0.0</version> </dependency> </dependencies>
添加復(fù)選框以在UI中啟用可搜索PDF的生成。
如果選中該復(fù)選框,則通過(guò)添加文本覆蓋層來(lái)生成可搜索的PDF。
PDDocument document = new PDDocument(); int index = 0; for (DocumentImage di: documentListView.getItems()) { index = index + 1; ImageView imageView = di.imageView; PDRectangle rect = new PDRectangle((float) imageView.getImage().getWidth(),(float) imageView.getImage().getHeight()); System.out.println(rect); PDPage page = new PDPage(rect); document.addPage(page); PDPageContentStream contentStream = new PDPageContentStream(document, page); PDImageXObject image = PDImageXObject.createFromByteArray(document,di.image,String.valueOf(index)); contentStream.drawImage(image, 0, 0); + if (searchablePDFCheckBox.isSelected()) { + String base64 = Base64.getEncoder().encodeToString(di.image); + OCRSpace.key = "your key"; + OCRResult result = OCRSpace.detect(base64); + SearchablePDFCreator.addTextOverlay(contentStream,result,image.getHeight()); + } contentStream.close(); } document.save(fileToSave.getAbsolutePath()); document.close();
到此這篇關(guān)于使用Java實(shí)現(xiàn)轉(zhuǎn)換掃描的文檔為可搜索的PDF的文章就介紹到這了,更多相關(guān)Java文檔轉(zhuǎn)換為PDF內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot整合Mysql和Redis的詳細(xì)過(guò)程
這篇文章主要介紹了SpringBoot整合Mysql和Redis的示例代碼,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-02-02IDEA 如何控制編輯左側(cè)的功能圖標(biāo)ICON(操作步驟)
很多朋友被idea左側(cè)的圖標(biāo)不見(jiàn)了這一問(wèn)題搞的焦頭爛額,不知道該怎么操作,今天小編就交大家如何控制編輯左側(cè)的功能圖標(biāo) ICON,文字內(nèi)容不多,主要通過(guò)兩張截圖給大家說(shuō)明,感興趣的朋友一起看看吧2021-05-05Spring boot redis cache的key的使用方法
這篇文章主要介紹了Spring boot redis cache的key的使用方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05一個(gè)簡(jiǎn)單的Spring容器初始化流程詳解
這篇文章主要給大家介紹了一個(gè)簡(jiǎn)單的Spring容器初始化流程的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01java實(shí)現(xiàn)圖片上加文字水印(SpringMVC + Jsp)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)在圖片上加文字水印的方法,水印可以是圖片或者文字,操作方便,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-05-05java網(wǎng)絡(luò)通信技術(shù)之簡(jiǎn)單聊天小程序
這篇文章主要為大家詳細(xì)介紹了java網(wǎng)絡(luò)通信技術(shù)之簡(jiǎn)單聊天小程序,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07淺析Java中ConcurrentHashMap的存儲(chǔ)流程
ConcurrentHashMap技術(shù)在互聯(lián)網(wǎng)技術(shù)使用如此廣泛,幾乎所有的后端技術(shù)面試官都要在ConcurrentHashMap技術(shù)的使用和原理方面對(duì)小伙伴們進(jìn)行360°的刁難,本文詳細(xì)給大家介紹一下ConcurrentHashMap的存儲(chǔ)流程,需要的朋友可以參考下2023-05-05