From 4da3c884007cf2820b2a73bc855112df74395a8d Mon Sep 17 00:00:00 2001 From: oneao Date: Sun, 7 Jun 2026 21:12:12 +0800 Subject: [PATCH] 20260607211212 --- {报关单 => bgd}/11.pdf | Bin {报关单 => bgd}/22-1.pdf | Bin {报关单 => bgd}/33-1.pdf | Bin {报关单 => bgd}/44-1.pdf | Bin {报关单 => bgd}/55-1.pdf | Bin {报关单 => bgd}/66-1.pdf | Bin {报关单 => bgd}/77-1.pdf | Bin {报关单 => bgd}/990400报.pdf | Bin .../EGLV072100006229-裕锦出库报关单.pdf | Bin {报关单 => bgd}/HDMUCGPA26415100报.pdf | Bin {报关单 => bgd}/I20210000769540727.pdf | Bin {报关单 => bgd}/I20210000769558253.pdf | Bin {报关单 => bgd}/I20220000786622159.pdf | Bin {报关单 => bgd}/bgd-01.pdf | Bin {报关单 => bgd}/bgd-02.pdf | Bin {报关单 => bgd}/bgd-03.pdf | Bin {报关单 => bgd}/bgd-04.pdf | Bin {报关单 => bgd}/bgd-05.pdf | Bin {报关单 => bgd}/bgd-06.pdf | Bin {报关单 => bgd}/bgd-07.pdf | Bin {报关单 => bgd}/bgd.pdf | Bin {报关单 => bgd}/报关单.pdf | Bin code/ai/test/.gitignore | 39 ++++ code/ai/test/.idea/.gitignore | 10 + code/ai/test/.idea/encodings.xml | 7 + code/ai/test/.idea/misc.xml | 14 ++ code/ai/test/.idea/vcs.xml | 6 + ...-boot-starter-parent-4.0.6.pom.lastUpdated | 4 + code/ai/test/pom.xml | 77 +++++++ .../test/src/main/java/org/example/Main.java | 125 ++++++++++ .../example/Springboot4ApiApplication.java | 12 + .../org/example/api/HealthController.java | 20 ++ .../api/layout/LayoutParsingController.java | 37 +++ .../api/layout/LayoutParsingRequest.java | 68 ++++++ .../api/layout/LayoutParsingResponse.java | 23 ++ .../api/layout/LayoutParsingService.java | 216 ++++++++++++++++++ .../api/layout/LayoutParsingUseCase.java | 6 + .../test/src/main/java/org/example/test.http | 9 + .../test/src/main/resources/application.yml | 12 + .../Springboot4ApiApplicationTests.java | 12 + .../example/api/HealthControllerTests.java | 25 ++ .../layout/LayoutParsingControllerTests.java | 73 ++++++ 42 files changed, 795 insertions(+) rename {报关单 => bgd}/11.pdf (100%) rename {报关单 => bgd}/22-1.pdf (100%) rename {报关单 => bgd}/33-1.pdf (100%) rename {报关单 => bgd}/44-1.pdf (100%) rename {报关单 => bgd}/55-1.pdf (100%) rename {报关单 => bgd}/66-1.pdf (100%) rename {报关单 => bgd}/77-1.pdf (100%) rename {报关单 => bgd}/990400报.pdf (100%) rename {报关单 => bgd}/EGLV072100006229-裕锦出库报关单.pdf (100%) rename {报关单 => bgd}/HDMUCGPA26415100报.pdf (100%) rename {报关单 => bgd}/I20210000769540727.pdf (100%) rename {报关单 => bgd}/I20210000769558253.pdf (100%) rename {报关单 => bgd}/I20220000786622159.pdf (100%) rename {报关单 => bgd}/bgd-01.pdf (100%) rename {报关单 => bgd}/bgd-02.pdf (100%) rename {报关单 => bgd}/bgd-03.pdf (100%) rename {报关单 => bgd}/bgd-04.pdf (100%) rename {报关单 => bgd}/bgd-05.pdf (100%) rename {报关单 => bgd}/bgd-06.pdf (100%) rename {报关单 => bgd}/bgd-07.pdf (100%) rename {报关单 => bgd}/bgd.pdf (100%) rename {报关单 => bgd}/报关单.pdf (100%) create mode 100644 code/ai/test/.gitignore create mode 100644 code/ai/test/.idea/.gitignore create mode 100644 code/ai/test/.idea/encodings.xml create mode 100644 code/ai/test/.idea/misc.xml create mode 100644 code/ai/test/.idea/vcs.xml create mode 100644 code/ai/test/.m2/repository/org/springframework/boot/spring-boot-starter-parent/4.0.6/spring-boot-starter-parent-4.0.6.pom.lastUpdated create mode 100644 code/ai/test/pom.xml create mode 100644 code/ai/test/src/main/java/org/example/Main.java create mode 100644 code/ai/test/src/main/java/org/example/Springboot4ApiApplication.java create mode 100644 code/ai/test/src/main/java/org/example/api/HealthController.java create mode 100644 code/ai/test/src/main/java/org/example/api/layout/LayoutParsingController.java create mode 100644 code/ai/test/src/main/java/org/example/api/layout/LayoutParsingRequest.java create mode 100644 code/ai/test/src/main/java/org/example/api/layout/LayoutParsingResponse.java create mode 100644 code/ai/test/src/main/java/org/example/api/layout/LayoutParsingService.java create mode 100644 code/ai/test/src/main/java/org/example/api/layout/LayoutParsingUseCase.java create mode 100644 code/ai/test/src/main/java/org/example/test.http create mode 100644 code/ai/test/src/main/resources/application.yml create mode 100644 code/ai/test/src/test/java/org/example/Springboot4ApiApplicationTests.java create mode 100644 code/ai/test/src/test/java/org/example/api/HealthControllerTests.java create mode 100644 code/ai/test/src/test/java/org/example/api/layout/LayoutParsingControllerTests.java diff --git a/报关单/11.pdf b/bgd/11.pdf similarity index 100% rename from 报关单/11.pdf rename to bgd/11.pdf diff --git a/报关单/22-1.pdf b/bgd/22-1.pdf similarity index 100% rename from 报关单/22-1.pdf rename to bgd/22-1.pdf diff --git a/报关单/33-1.pdf b/bgd/33-1.pdf similarity index 100% rename from 报关单/33-1.pdf rename to bgd/33-1.pdf diff --git a/报关单/44-1.pdf b/bgd/44-1.pdf similarity index 100% rename from 报关单/44-1.pdf rename to bgd/44-1.pdf diff --git a/报关单/55-1.pdf b/bgd/55-1.pdf similarity index 100% rename from 报关单/55-1.pdf rename to bgd/55-1.pdf diff --git a/报关单/66-1.pdf b/bgd/66-1.pdf similarity index 100% rename from 报关单/66-1.pdf rename to bgd/66-1.pdf diff --git a/报关单/77-1.pdf b/bgd/77-1.pdf similarity index 100% rename from 报关单/77-1.pdf rename to bgd/77-1.pdf diff --git a/报关单/990400报.pdf b/bgd/990400报.pdf similarity index 100% rename from 报关单/990400报.pdf rename to bgd/990400报.pdf diff --git a/报关单/EGLV072100006229-裕锦出库报关单.pdf b/bgd/EGLV072100006229-裕锦出库报关单.pdf similarity index 100% rename from 报关单/EGLV072100006229-裕锦出库报关单.pdf rename to bgd/EGLV072100006229-裕锦出库报关单.pdf diff --git a/报关单/HDMUCGPA26415100报.pdf b/bgd/HDMUCGPA26415100报.pdf similarity index 100% rename from 报关单/HDMUCGPA26415100报.pdf rename to bgd/HDMUCGPA26415100报.pdf diff --git a/报关单/I20210000769540727.pdf b/bgd/I20210000769540727.pdf similarity index 100% rename from 报关单/I20210000769540727.pdf rename to bgd/I20210000769540727.pdf diff --git a/报关单/I20210000769558253.pdf b/bgd/I20210000769558253.pdf similarity index 100% rename from 报关单/I20210000769558253.pdf rename to bgd/I20210000769558253.pdf diff --git a/报关单/I20220000786622159.pdf b/bgd/I20220000786622159.pdf similarity index 100% rename from 报关单/I20220000786622159.pdf rename to bgd/I20220000786622159.pdf diff --git a/报关单/bgd-01.pdf b/bgd/bgd-01.pdf similarity index 100% rename from 报关单/bgd-01.pdf rename to bgd/bgd-01.pdf diff --git a/报关单/bgd-02.pdf b/bgd/bgd-02.pdf similarity index 100% rename from 报关单/bgd-02.pdf rename to bgd/bgd-02.pdf diff --git a/报关单/bgd-03.pdf b/bgd/bgd-03.pdf similarity index 100% rename from 报关单/bgd-03.pdf rename to bgd/bgd-03.pdf diff --git a/报关单/bgd-04.pdf b/bgd/bgd-04.pdf similarity index 100% rename from 报关单/bgd-04.pdf rename to bgd/bgd-04.pdf diff --git a/报关单/bgd-05.pdf b/bgd/bgd-05.pdf similarity index 100% rename from 报关单/bgd-05.pdf rename to bgd/bgd-05.pdf diff --git a/报关单/bgd-06.pdf b/bgd/bgd-06.pdf similarity index 100% rename from 报关单/bgd-06.pdf rename to bgd/bgd-06.pdf diff --git a/报关单/bgd-07.pdf b/bgd/bgd-07.pdf similarity index 100% rename from 报关单/bgd-07.pdf rename to bgd/bgd-07.pdf diff --git a/报关单/bgd.pdf b/bgd/bgd.pdf similarity index 100% rename from 报关单/bgd.pdf rename to bgd/bgd.pdf diff --git a/报关单/报关单.pdf b/bgd/报关单.pdf similarity index 100% rename from 报关单/报关单.pdf rename to bgd/报关单.pdf diff --git a/code/ai/test/.gitignore b/code/ai/test/.gitignore new file mode 100644 index 00000000..480bdf52 --- /dev/null +++ b/code/ai/test/.gitignore @@ -0,0 +1,39 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ +.kotlin + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/code/ai/test/.idea/.gitignore b/code/ai/test/.idea/.gitignore new file mode 100644 index 00000000..30cf57ed --- /dev/null +++ b/code/ai/test/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/code/ai/test/.idea/encodings.xml b/code/ai/test/.idea/encodings.xml new file mode 100644 index 00000000..aa00ffab --- /dev/null +++ b/code/ai/test/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/code/ai/test/.idea/misc.xml b/code/ai/test/.idea/misc.xml new file mode 100644 index 00000000..7ace0970 --- /dev/null +++ b/code/ai/test/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/code/ai/test/.idea/vcs.xml b/code/ai/test/.idea/vcs.xml new file mode 100644 index 00000000..c2365ab1 --- /dev/null +++ b/code/ai/test/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/code/ai/test/.m2/repository/org/springframework/boot/spring-boot-starter-parent/4.0.6/spring-boot-starter-parent-4.0.6.pom.lastUpdated b/code/ai/test/.m2/repository/org/springframework/boot/spring-boot-starter-parent/4.0.6/spring-boot-starter-parent-4.0.6.pom.lastUpdated new file mode 100644 index 00000000..614545f9 --- /dev/null +++ b/code/ai/test/.m2/repository/org/springframework/boot/spring-boot-starter-parent/4.0.6/spring-boot-starter-parent-4.0.6.pom.lastUpdated @@ -0,0 +1,4 @@ +#NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice. +#Sun Jun 07 19:41:40 CST 2026 +@default-aliyunmaven-https\://maven.aliyun.com/repository/central/.lastUpdated=1780832500060 +https\://maven.aliyun.com/repository/central/.error=Could not transfer artifact org.springframework.boot\:spring-boot-starter-parent\:pom\:4.0.6 from/to aliyunmaven (https\://maven.aliyun.com/repository/central)\: Permission denied\: getsockopt diff --git a/code/ai/test/pom.xml b/code/ai/test/pom.xml new file mode 100644 index 00000000..64760a5e --- /dev/null +++ b/code/ai/test/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.0.6 + + + + org.example + springboot4-api + 0.0.1-SNAPSHOT + springboot4-api + Spring Boot 4 REST API project + + + 21 + UTF-8 + + + + + + + org.springframework.boot + spring-boot-starter-webmvc + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.squareup.okhttp3 + okhttp + 5.1.0 + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + -Xshare:off -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/code/ai/test/src/main/java/org/example/Main.java b/code/ai/test/src/main/java/org/example/Main.java new file mode 100644 index 00000000..6a4bd6d1 --- /dev/null +++ b/code/ai/test/src/main/java/org/example/Main.java @@ -0,0 +1,125 @@ +package org.example; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.http.*; +import org.springframework.web.client.RestTemplate; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.file.Files; +import java.util.Base64; + +public class Main { + + public static void main(String[] args) throws Exception { + + String apiUrl = "http://localhost:8866/layout-parsing"; + String pdfPath = "D:/workspace/bgd/bgd-01.pdf"; + + // 读取PDF + File file = new File(pdfPath); + byte[] bytes = Files.readAllBytes(file.toPath()); + + // Base64编码 + String base64 = Base64.getEncoder().encodeToString(bytes); + + // JSON请求体 + ObjectMapper mapper = new ObjectMapper(); + + ObjectNode payload = mapper.createObjectNode(); + payload.put("file", base64); + payload.put("fileType", 0); + + // 打印请求大小 + System.out.println("Base64 Length: " + base64.length()); + + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity request = + new HttpEntity<>(payload.toString(), headers); + + ResponseEntity response = + restTemplate.exchange( + apiUrl, + HttpMethod.POST, + request, + String.class + ); + + System.out.println("HTTP Status: " + + response.getStatusCode()); + + String responseBody = response.getBody(); + + System.out.println("Response:"); + System.out.println(responseBody); + + // 解析返回结果 + JsonNode root = mapper.readTree(responseBody); + + JsonNode result = root.path("result"); + + JsonNode layoutParsingResults = + result.path("layoutParsingResults"); + + for (int i = 0; i < layoutParsingResults.size(); i++) { + + JsonNode item = layoutParsingResults.get(i); + + System.out.println( + "\n========== Page " + i + " ==========" + ); + + JsonNode prunedResult = + item.path("prunedResult"); + + System.out.println( + mapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(prunedResult) + ); + + JsonNode outputImages = + item.path("outputImages"); + + outputImages.fieldNames() + .forEachRemaining(imgName -> { + + try { + + String imgBase64 = + outputImages.get(imgName) + .asText(); + + byte[] imgBytes = + Base64.getDecoder() + .decode(imgBase64); + + String imgPath = + imgName + "_" + i + ".jpg"; + + try (FileOutputStream fos = + new FileOutputStream(imgPath)) { + + fos.write(imgBytes); + } + + System.out.println( + "Saved image: " + imgPath + ); + + } catch (Exception e) { + + System.err.println( + "Save image failed: " + + e.getMessage() + ); + } + }); + } + } +} \ No newline at end of file diff --git a/code/ai/test/src/main/java/org/example/Springboot4ApiApplication.java b/code/ai/test/src/main/java/org/example/Springboot4ApiApplication.java new file mode 100644 index 00000000..a81e0669 --- /dev/null +++ b/code/ai/test/src/main/java/org/example/Springboot4ApiApplication.java @@ -0,0 +1,12 @@ +package org.example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Springboot4ApiApplication { + + public static void main(String[] args) { + SpringApplication.run(Springboot4ApiApplication.class, args); + } +} diff --git a/code/ai/test/src/main/java/org/example/api/HealthController.java b/code/ai/test/src/main/java/org/example/api/HealthController.java new file mode 100644 index 00000000..c3e821e2 --- /dev/null +++ b/code/ai/test/src/main/java/org/example/api/HealthController.java @@ -0,0 +1,20 @@ +package org.example.api; + +import java.time.Instant; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class HealthController { + + @GetMapping("/health") + public HealthResponse health() { + return new HealthResponse("UP", Instant.now()); + } + + public record HealthResponse(String status, Instant timestamp) { + } +} diff --git a/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingController.java b/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingController.java new file mode 100644 index 00000000..35909649 --- /dev/null +++ b/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingController.java @@ -0,0 +1,37 @@ +package org.example.api.layout; + +import jakarta.validation.Valid; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/layout-parsing") +public class LayoutParsingController { + + private final LayoutParsingUseCase layoutParsingUseCase; + private final String defaultFilePath; + + public LayoutParsingController( + LayoutParsingUseCase layoutParsingUseCase, + @Value("${layout.parsing.default-file-path}") String defaultFilePath + ) { + this.layoutParsingUseCase = layoutParsingUseCase; + this.defaultFilePath = defaultFilePath; + } + + @GetMapping + public LayoutParsingResponse parseDefault() { + LayoutParsingRequest request = LayoutParsingRequest.pdf(defaultFilePath); + return layoutParsingUseCase.parse(request); + } + + @PostMapping + public LayoutParsingResponse parse(@Valid @RequestBody LayoutParsingRequest request) { + return layoutParsingUseCase.parse(request); + } +} diff --git a/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingRequest.java b/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingRequest.java new file mode 100644 index 00000000..f3a9354c --- /dev/null +++ b/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingRequest.java @@ -0,0 +1,68 @@ +package org.example.api.layout; + +import java.nio.file.Path; +import java.util.List; + +public record LayoutParsingRequest( + String filePath, + String imagePath, + Integer fileType, + String outputDirectory, + Boolean useDocOrientationClassify, + Boolean useDocUnwarping, + Boolean useTextlineOrientation, + Boolean useSealRecognition, + Boolean useTableRecognition, + Boolean useFormulaRecognition, + Boolean useChartRecognition, + Boolean useRegionDetection, + Boolean formatBlockContent, + Boolean returnMarkdownImages, + Boolean visualize, + List outputFormats +) { + + public static LayoutParsingRequest pdf(String filePath) { + return new LayoutParsingRequest( + filePath, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + true, + false, + false, + null + ); + } + + public String resolvedFilePath() { + if (hasText(filePath)) { + return filePath; + } + if (hasText(imagePath)) { + return imagePath; + } + throw new IllegalArgumentException("filePath is required"); + } + + public int resolvedFileType() { + if (fileType != null) { + return fileType; + } + + String fileName = Path.of(resolvedFilePath()).getFileName().toString().toLowerCase(); + return fileName.endsWith(".pdf") ? 0 : 1; + } + + private boolean hasText(String value) { + return value != null && !value.isBlank(); + } +} diff --git a/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingResponse.java b/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingResponse.java new file mode 100644 index 00000000..9ddb752f --- /dev/null +++ b/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingResponse.java @@ -0,0 +1,23 @@ +package org.example.api.layout; + +import java.util.List; + +import tools.jackson.databind.JsonNode; + +public record LayoutParsingResponse( + String filePath, + int fileType, + List items +) { + + public record Item( + JsonNode prunedResult, + String recognizedText, + String markdownText, + String savedPrunedResult, + String savedMarkdown, + String savedText, + List savedImages + ) { + } +} diff --git a/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingService.java b/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingService.java new file mode 100644 index 00000000..9b65c6c7 --- /dev/null +++ b/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingService.java @@ -0,0 +1,216 @@ +package org.example.api.layout; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestClient; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.ObjectNode; + +@Service +public class LayoutParsingService implements LayoutParsingUseCase { + + private final RestClient restClient; + private final ObjectMapper objectMapper; + private final String apiUrl; + private final Path defaultOutputDirectory; + + public LayoutParsingService( + ObjectMapper objectMapper, + @Value("${layout.parsing.api-url}") String apiUrl, + @Value("${layout.parsing.output-directory:output}") String defaultOutputDirectory + ) { + this.restClient = RestClient.create(); + this.objectMapper = objectMapper; + this.apiUrl = apiUrl; + this.defaultOutputDirectory = Path.of(defaultOutputDirectory); + } + + @Override + public LayoutParsingResponse parse(LayoutParsingRequest request) { + Path filePath = Path.of(request.resolvedFilePath()).toAbsolutePath().normalize(); + if (!Files.isRegularFile(filePath)) { + throw new IllegalArgumentException("File does not exist: " + filePath); + } + + JsonNode root = callLayoutParsingApi(filePath, request); + JsonNode layoutParsingResults = root.path("result").path("layoutParsingResults"); + if (!layoutParsingResults.isArray()) { + throw new IllegalStateException("Invalid layout parsing response: result.layoutParsingResults is missing"); + } + + Path outputDirectory = resolveOutputDirectory(request.outputDirectory()); + List items = new ArrayList<>(); + System.out.println("Layout parsing file: " + filePath); + System.out.println("Layout parsing result count: " + layoutParsingResults.size()); + for (int i = 0; i < layoutParsingResults.size(); i++) { + JsonNode item = layoutParsingResults.get(i); + JsonNode prunedResult = item.path("prunedResult"); + String markdownText = item.path("markdown").path("text").asText(); + String recognizedText = extractRecognizedText(prunedResult); + String savedPrunedResult = savePrunedResult(prunedResult, outputDirectory, i); + String savedMarkdown = saveTextFile(markdownText, outputDirectory, "markdown_" + i + ".md"); + String savedText = saveTextFile(recognizedText, outputDirectory, "recognized_text_" + i + ".txt"); + List savedImages = saveOutputImages(item.path("outputImages"), outputDirectory, i); + System.out.println("Pruned Result [" + i + "]: " + prunedResult); + System.out.println("Recognized Text [" + i + "]:"); + System.out.println(recognizedText); + System.out.println("Markdown Text [" + i + "]:"); + System.out.println(markdownText); + System.out.println("Saved pruned result: " + savedPrunedResult); + System.out.println("Saved markdown: " + savedMarkdown); + System.out.println("Saved text: " + savedText); + savedImages.forEach(savedImage -> System.out.println("Saved image: " + savedImage)); + items.add(new LayoutParsingResponse.Item( + prunedResult, + recognizedText, + markdownText, + savedPrunedResult, + savedMarkdown, + savedText, + savedImages + )); + } + + return new LayoutParsingResponse(filePath.toString(), request.resolvedFileType(), items); + } + + private JsonNode callLayoutParsingApi(Path filePath, LayoutParsingRequest request) { + try { + String base64File = Base64.getEncoder().encodeToString(Files.readAllBytes(filePath)); + + ObjectNode payload = objectMapper.createObjectNode(); + payload.put("file", base64File); + payload.put("fileType", request.resolvedFileType()); + putIfNotNull(payload, "useDocOrientationClassify", request.useDocOrientationClassify()); + putIfNotNull(payload, "useDocUnwarping", request.useDocUnwarping()); + putIfNotNull(payload, "useTextlineOrientation", request.useTextlineOrientation()); + putIfNotNull(payload, "useSealRecognition", request.useSealRecognition()); + putIfNotNull(payload, "useTableRecognition", request.useTableRecognition()); + putIfNotNull(payload, "useFormulaRecognition", request.useFormulaRecognition()); + putIfNotNull(payload, "useChartRecognition", request.useChartRecognition()); + putIfNotNull(payload, "useRegionDetection", request.useRegionDetection()); + putIfNotNull(payload, "formatBlockContent", request.formatBlockContent()); + putIfNotNull(payload, "returnMarkdownImages", request.returnMarkdownImages()); + putIfNotNull(payload, "visualize", request.visualize()); + if (request.outputFormats() != null && !request.outputFormats().isEmpty()) { + payload.set("outputFormats", objectMapper.valueToTree(request.outputFormats())); + } + + return restClient.post() + .uri(apiUrl) + .contentType(MediaType.APPLICATION_JSON) + .body(payload) + .retrieve() + .body(JsonNode.class); + } catch (IOException e) { + throw new IllegalStateException("Failed to read file: " + filePath, e); + } + } + + private void putIfNotNull(ObjectNode payload, String name, Boolean value) { + if (value != null) { + payload.put(name, value); + } + } + + private Path resolveOutputDirectory(String outputDirectory) { + Path directory = StringUtils.hasText(outputDirectory) + ? Path.of(outputDirectory) + : defaultOutputDirectory; + try { + Files.createDirectories(directory); + return directory.toAbsolutePath().normalize(); + } catch (IOException e) { + throw new IllegalStateException("Failed to create output directory: " + directory, e); + } + } + + private List saveOutputImages(JsonNode outputImages, Path outputDirectory, int resultIndex) { + List savedImages = new ArrayList<>(); + if (!outputImages.isObject()) { + return savedImages; + } + + Iterator> fields = outputImages.properties().iterator(); + while (fields.hasNext()) { + Entry field = fields.next(); + String imageName = sanitizeFileName(field.getKey()); + Path outputPath = outputDirectory.resolve(imageName + "_" + resultIndex + ".jpg"); + try { + byte[] imageBytes = Base64.getDecoder().decode(field.getValue().asText()); + Files.write(outputPath, imageBytes); + savedImages.add(outputPath.toString()); + } catch (IllegalArgumentException | IOException e) { + throw new IllegalStateException("Failed to save output image: " + outputPath, e); + } + } + return savedImages; + } + + private String savePrunedResult(JsonNode prunedResult, Path outputDirectory, int resultIndex) { + Path outputPath = outputDirectory.resolve("pruned_result_" + resultIndex + ".json"); + try { + Files.writeString(outputPath, prunedResult.toString()); + return outputPath.toString(); + } catch (IOException e) { + throw new IllegalStateException("Failed to save pruned result: " + outputPath, e); + } + } + + private String saveTextFile(String content, Path outputDirectory, String fileName) { + if (!StringUtils.hasText(content)) { + return null; + } + + Path outputPath = outputDirectory.resolve(fileName); + try { + Files.writeString(outputPath, content); + return outputPath.toString(); + } catch (IOException e) { + throw new IllegalStateException("Failed to save text file: " + outputPath, e); + } + } + + private String extractRecognizedText(JsonNode prunedResult) { + List lines = new ArrayList<>(); + + JsonNode parsingResults = prunedResult.path("parsing_res_list"); + if (parsingResults.isArray()) { + for (JsonNode block : parsingResults) { + String blockContent = block.path("block_content").asText(); + if (StringUtils.hasText(blockContent)) { + lines.add(blockContent); + } + } + } + + JsonNode recognizedTexts = prunedResult.path("overall_ocr_res").path("rec_texts"); + if (lines.isEmpty() && recognizedTexts.isArray()) { + for (JsonNode text : recognizedTexts) { + String value = text.asText(); + if (StringUtils.hasText(value)) { + lines.add(value); + } + } + } + + return String.join(System.lineSeparator(), lines); + } + + private String sanitizeFileName(String value) { + String sanitized = value.replaceAll("[^a-zA-Z0-9._-]", "_"); + return StringUtils.hasText(sanitized) ? sanitized : "image"; + } +} diff --git a/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingUseCase.java b/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingUseCase.java new file mode 100644 index 00000000..d96482b9 --- /dev/null +++ b/code/ai/test/src/main/java/org/example/api/layout/LayoutParsingUseCase.java @@ -0,0 +1,6 @@ +package org.example.api.layout; + +public interface LayoutParsingUseCase { + + LayoutParsingResponse parse(LayoutParsingRequest request); +} diff --git a/code/ai/test/src/main/java/org/example/test.http b/code/ai/test/src/main/java/org/example/test.http new file mode 100644 index 00000000..b3d873ee --- /dev/null +++ b/code/ai/test/src/main/java/org/example/test.http @@ -0,0 +1,9 @@ +### Send POST request with json body +POST http://localhost:8866/layout-parsing +Content-Type: application/json + +{ + "file": "D:\\workspace\\bgd\\bgd-01.pdf", + "fileType": 0 +} + diff --git a/code/ai/test/src/main/resources/application.yml b/code/ai/test/src/main/resources/application.yml new file mode 100644 index 00000000..da73f664 --- /dev/null +++ b/code/ai/test/src/main/resources/application.yml @@ -0,0 +1,12 @@ +spring: + application: + name: springboot4-api + +server: + port: 8080 + +layout: + parsing: + api-url: http://localhost:8866/layout-parsing + default-file-path: D:/workspace/bgd/bgd-01.pdf + output-directory: output diff --git a/code/ai/test/src/test/java/org/example/Springboot4ApiApplicationTests.java b/code/ai/test/src/test/java/org/example/Springboot4ApiApplicationTests.java new file mode 100644 index 00000000..14ea63b8 --- /dev/null +++ b/code/ai/test/src/test/java/org/example/Springboot4ApiApplicationTests.java @@ -0,0 +1,12 @@ +package org.example; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Springboot4ApiApplicationTests { + + @Test + void contextLoads() { + } +} diff --git a/code/ai/test/src/test/java/org/example/api/HealthControllerTests.java b/code/ai/test/src/test/java/org/example/api/HealthControllerTests.java new file mode 100644 index 00000000..75d3d078 --- /dev/null +++ b/code/ai/test/src/test/java/org/example/api/HealthControllerTests.java @@ -0,0 +1,25 @@ +package org.example.api; + +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(HealthController.class) +class HealthControllerTests { + + @Autowired + private MockMvc mockMvc; + + @Test + void healthReturnsUp() throws Exception { + mockMvc.perform(get("/api/health")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status", is("UP"))); + } +} diff --git a/code/ai/test/src/test/java/org/example/api/layout/LayoutParsingControllerTests.java b/code/ai/test/src/test/java/org/example/api/layout/LayoutParsingControllerTests.java new file mode 100644 index 00000000..348e87a7 --- /dev/null +++ b/code/ai/test/src/test/java/org/example/api/layout/LayoutParsingControllerTests.java @@ -0,0 +1,73 @@ +package org.example.api.layout; + +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Bean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import tools.jackson.databind.ObjectMapper; + +@WebMvcTest(LayoutParsingController.class) +class LayoutParsingControllerTests { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Test + void parseAcceptsLocalPdfPath() throws Exception { + String request = """ + { + "filePath": "D:/workspace/bgd/bgd-01.pdf", + "outputDirectory": "output", + "returnMarkdownImages": false, + "visualize": false + } + """; + + mockMvc.perform(post("/api/layout-parsing") + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.filePath", is("D:/workspace/bgd/bgd-01.pdf"))) + .andExpect(jsonPath("$.fileType", is(0))) + .andExpect(jsonPath("$.items[0].recognizedText", is("demo text"))) + .andExpect(jsonPath("$.items[0].markdownText", is("# demo"))) + .andExpect(jsonPath("$.items[0].savedPrunedResult", is("output/pruned_result_0.json"))) + .andExpect(jsonPath("$.items[0].savedMarkdown", is("output/markdown_0.md"))) + .andExpect(jsonPath("$.items[0].savedText", is("output/recognized_text_0.txt"))) + .andExpect(jsonPath("$.items[0].savedImages[0]", is("output/table_0.jpg"))); + } + + @TestConfiguration + static class TestConfig { + + @Bean + LayoutParsingUseCase layoutParsingUseCase(ObjectMapper objectMapper) { + return request -> new LayoutParsingResponse( + request.resolvedFilePath(), + request.resolvedFileType(), + List.of(new LayoutParsingResponse.Item( + objectMapper.createObjectNode().put("text", "demo"), + "demo text", + "# demo", + "output/pruned_result_0.json", + "output/markdown_0.md", + "output/recognized_text_0.txt", + List.of("output/table_0.jpg") + )) + ); + } + } +}