diff --git a/app/allowed-licenses.json b/app/allowed-licenses.json index 830ae037a..315c6bb18 100644 --- a/app/allowed-licenses.json +++ b/app/allowed-licenses.json @@ -96,6 +96,22 @@ "moduleName": ".*", "moduleLicense": "MPL 2.0" }, + { + "moduleName": ".*", + "moduleLicense": "Mozilla Public License, Version 2.0" + }, + { + "moduleName": ".*", + "moduleLicense": "Mozilla Public License 2.0 (MPL-2.0)" + }, + { + "moduleName": ".*", + "moduleLicense": "CDDL+GPL License" + }, + { + "moduleName": ".*", + "moduleLicense": "BSD" + }, { "moduleName": ".*", "moduleLicense": "UnboundID SCIM2 SDK Free Use License" diff --git a/app/core/build.gradle b/app/core/build.gradle index d31125a26..e3ad9ca97 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -66,6 +66,13 @@ dependencies { implementation "org.apache.pdfbox:preflight:$pdfboxVersion" implementation "org.apache.pdfbox:xmpbox:$pdfboxVersion" + implementation 'org.verapdf:validation-model:1.28.2' + + // veraPDF still uses javax.xml.bind, not the new jakarta namespace + implementation 'javax.xml.bind:jaxb-api:2.3.1' + implementation 'com.sun.xml.bind:jaxb-impl:2.3.9' + implementation 'com.sun.xml.bind:jaxb-core:2.3.0.1' + // https://mvnrepository.com/artifact/technology.tabula/tabula implementation ('technology.tabula:tabula:1.0.5') { exclude group: 'org.slf4j', module: 'slf4j-simple' diff --git a/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index 0178c2597..d5106bb48 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -273,6 +273,7 @@ public class EndpointConfiguration { addEndpointToGroup("Security", "auto-redact"); addEndpointToGroup("Security", "redact"); addEndpointToGroup("Security", "validate-signature"); + addEndpointToGroup("Security", "verify-pdf"); addEndpointToGroup("Security", "stamp"); addEndpointToGroup("Security", "sign"); @@ -395,6 +396,8 @@ public class EndpointConfiguration { addEndpointToGroup("Java", "add-attachments"); addEndpointToGroup("Java", "compress-pdf"); addEndpointToGroup("rar", "pdf-to-cbr"); + addEndpointToGroup("Java", "pdf-to-video"); + addEndpointToGroup("Java", "verify-pdf"); // Javascript addEndpointToGroup("Javascript", "pdf-organizer"); @@ -444,6 +447,9 @@ public class EndpointConfiguration { addEndpointToGroup("Weasyprint", "markdown-to-pdf"); addEndpointToGroup("Weasyprint", "eml-to-pdf"); + // veraPDF dependent endpoints + addEndpointToGroup("veraPDF", "verify-pdf"); + // Pdftohtml dependent endpoints addEndpointToGroup("Pdftohtml", "pdf-to-html"); addEndpointToGroup("Pdftohtml", "pdf-to-markdown"); @@ -493,7 +499,9 @@ public class EndpointConfiguration { || "Javascript".equals(group) || "Weasyprint".equals(group) || "Pdftohtml".equals(group) - || "rar".equals(group); + || "rar".equals(group) + || "FFmpeg".equals(group) + || "veraPDF".equals(group); } private boolean isEndpointEnabledDirectly(String endpoint) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/VerifyPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/VerifyPDFController.java new file mode 100644 index 000000000..2cd111516 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/VerifyPDFController.java @@ -0,0 +1,102 @@ +package stirling.software.SPDF.controller.api.security; + +import java.io.IOException; +import java.util.List; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.verapdf.core.EncryptedPdfException; +import org.verapdf.core.ModelParsingException; +import org.verapdf.core.ValidationException; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.SPDF.model.api.security.PDFVerificationRequest; +import stirling.software.SPDF.model.api.security.PDFVerificationResult; +import stirling.software.SPDF.service.VeraPDFService; +import stirling.software.common.util.ExceptionUtils; + +@RestController +@RequestMapping("/api/v1/security") +@Tag(name = "Security", description = "Security APIs") +@RequiredArgsConstructor +@Slf4j +public class VerifyPDFController { + + private final VeraPDFService veraPDFService; + + @Operation( + summary = "Verify PDF Standards Compliance", + description = + "Validates PDF files against PDF/A, PDF/UA-1, PDF/UA-2, and WTPDF standards" + + " using veraPDF. Can auto-detect declared standards or verify against" + + " a specific standard. Input:PDF Output:JSON Type:SISO") + @PostMapping(value = "/verify-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> verifyPDF( + @ModelAttribute PDFVerificationRequest request) { + + MultipartFile file = request.getFileInput(); + String standard = request.getStandard(); + + if (file == null || file.isEmpty()) { + throw ExceptionUtils.createRuntimeException( + "error.pdfRequired", "PDF file is required", null); + } + + try { + List results; + + if (standard != null && !standard.trim().isEmpty()) { + log.info( + "Verifying PDF '{}' against standard: {}", + file.getOriginalFilename(), + standard); + PDFVerificationResult result = + veraPDFService.validatePDF(file.getInputStream(), standard.trim()); + results = List.of(result); + } else { + log.info("Auto-detecting standards in PDF '{}'", file.getOriginalFilename()); + results = veraPDFService.validateAllDeclaredStandards(file.getInputStream()); + } + + log.info( + "Verification complete for '{}': {} standard(s) checked", + file.getOriginalFilename(), + results.size()); + + return ResponseEntity.ok(results); + + } catch (ValidationException e) { + log.error("Validation exception for file: {}", file.getOriginalFilename(), e); + throw ExceptionUtils.createRuntimeException( + "error.validationFailed", "PDF validation failed: {0}", e, e.getMessage()); + } catch (ModelParsingException e) { + log.error("Model parsing exception for file: {}", file.getOriginalFilename(), e); + throw ExceptionUtils.createRuntimeException( + "error.modelParsingFailed", "PDF model parsing failed: {0}", e, e.getMessage()); + } catch (EncryptedPdfException e) { + log.error("Encrypted PDF exception for file: {}", file.getOriginalFilename(), e); + throw ExceptionUtils.createRuntimeException( + "error.encryptedPdf", + "Cannot verify encrypted PDF. Please remove password first: {0}", + e, + e.getMessage()); + } catch (IOException e) { + log.error("IO exception for file: {}", file.getOriginalFilename(), e); + throw ExceptionUtils.createRuntimeException( + "error.ioException", + "IO error during PDF verification: {0}", + e, + e.getMessage()); + } + } +} diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/security/PDFVerificationRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/security/PDFVerificationRequest.java new file mode 100644 index 000000000..4f2133ae3 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/security/PDFVerificationRequest.java @@ -0,0 +1,22 @@ +package stirling.software.SPDF.model.api.security; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import stirling.software.common.model.api.PDFFile; + +@Data +@EqualsAndHashCode(callSuper = true) +public class PDFVerificationRequest extends PDFFile { + + @Schema( + description = + "Specific PDF standard to verify against (e.g., '1b', '2a', '3u', 'ua1', 'ua2'," + + " 'wtpdf-1.0'). Leave empty to auto-detect and verify all declared" + + " standards. The response will include both errors (compliance failures)" + + " and warnings (non-critical issues) separately.", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String standard; +} diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/security/PDFVerificationResult.java b/app/core/src/main/java/stirling/software/SPDF/model/api/security/PDFVerificationResult.java new file mode 100644 index 000000000..9845681b2 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/security/PDFVerificationResult.java @@ -0,0 +1,48 @@ +package stirling.software.SPDF.model.api.security; + +import java.util.ArrayList; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PDFVerificationResult { + + private String standard; + private String standardName; + private String validationProfile; + private String validationProfileName; + private String complianceSummary; + private boolean declaredPdfa; + private boolean compliant; + private int totalFailures; + private int totalWarnings; + private List failures = new ArrayList<>(); + private List warnings = new ArrayList<>(); + + public void addFailure(ValidationIssue failure) { + this.failures.add(failure); + this.totalFailures = this.failures.size(); + } + + public void addWarning(ValidationIssue warning) { + this.warnings.add(warning); + this.totalWarnings = this.warnings.size(); + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ValidationIssue { + private String ruleId; + private String message; + private String location; + private String specification; + private String clause; + private String testNumber; + } +} diff --git a/app/core/src/main/java/stirling/software/SPDF/service/VeraPDFService.java b/app/core/src/main/java/stirling/software/SPDF/service/VeraPDFService.java new file mode 100644 index 000000000..002a91543 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/service/VeraPDFService.java @@ -0,0 +1,455 @@ +package stirling.software.SPDF.service; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.common.PDMetadata; +import org.apache.xmpbox.XMPMetadata; +import org.apache.xmpbox.schema.PDFAIdentificationSchema; +import org.apache.xmpbox.xml.DomXmpParser; +import org.apache.xmpbox.xml.XmpParsingException; +import org.springframework.stereotype.Service; +import org.verapdf.core.EncryptedPdfException; +import org.verapdf.core.ModelParsingException; +import org.verapdf.core.ValidationException; +import org.verapdf.gf.foundry.VeraGreenfieldFoundryProvider; +import org.verapdf.pdfa.Foundries; +import org.verapdf.pdfa.PDFAParser; +import org.verapdf.pdfa.PDFAValidator; +import org.verapdf.pdfa.flavours.PDFAFlavour; +import org.verapdf.pdfa.flavours.PDFFlavours; +import org.verapdf.pdfa.results.TestAssertion; +import org.verapdf.pdfa.results.ValidationResult; + +import jakarta.annotation.PostConstruct; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.SPDF.model.api.security.PDFVerificationResult; + +@Service +@Slf4j +public class VeraPDFService { + + private static final Set WARNING_RULES = + Set.of( + "6.1.13-1", // Recommended metadata properties + "6.7.3-1", // Optional XMP metadata + "6.2.2-2" // Non-critical font issues + ); + + private static final Set CRITICAL_CLAUSE_PREFIXES = Set.of("6.1", "6.2", "6.3", "6.4"); + private static final String NOT_PDFA_STANDARD_ID = "not-pdfa"; + private static final String NOT_PDFA_STANDARD_NAME = + "Not PDF/A (no PDF/A identification metadata)"; + + @PostConstruct + public void initialize() { + try { + VeraGreenfieldFoundryProvider.initialise(); + log.info("veraPDF Greenfield initialized successfully"); + } catch (Exception e) { + log.error("Failed to initialize veraPDF", e); + } + } + + private static boolean isWarningByMessage(String message) { + // Check for null or blank message - explicit null check is necessary to avoid NPE + if (message == null || message.isBlank()) { + return false; + } + + String normalized = message.toLowerCase(Locale.ROOT); + + return normalized.contains("recommended") + || normalized.contains("should") + || normalized.contains("optional") + || normalized.contains("missing recommended"); + } + + private static boolean isWarningByClause(String clause) { + // Check for null or blank clause - explicit null check is necessary to avoid NPE + if (clause == null || clause.isBlank()) { + return false; + } + + if (clause.startsWith("6.7")) { + return true; + } + + for (String criticalPrefix : CRITICAL_CLAUSE_PREFIXES) { + if (clause.startsWith(criticalPrefix)) { + return false; + } + } + + return true; + } + + private static PDFVerificationResult convertToVerificationResult( + ValidationResult result, PDFAFlavour declaredFlavour, PDFAFlavour validationFlavour) { + PDFVerificationResult verificationResult = new PDFVerificationResult(); + + PDFAFlavour validationProfile = + validationFlavour != null ? validationFlavour : result.getPDFAFlavour(); + boolean validationIsPdfa = isPdfaFlavour(validationProfile); + + if (validationProfile != null) { + verificationResult.setValidationProfile(validationProfile.getId()); + verificationResult.setValidationProfileName(getStandardName(validationProfile)); + } + + if (declaredFlavour != null) { + verificationResult.setStandard(declaredFlavour.getId()); + verificationResult.setDeclaredPdfa(isPdfaFlavour(declaredFlavour)); + } else if (validationIsPdfa) { + verificationResult.setStandard(NOT_PDFA_STANDARD_ID); + verificationResult.setDeclaredPdfa(false); + } else if (validationProfile != null) { + verificationResult.setStandard(validationProfile.getId()); + verificationResult.setDeclaredPdfa(false); + } else { + verificationResult.setStandard(NOT_PDFA_STANDARD_ID); + verificationResult.setDeclaredPdfa(false); + } + + List assertions = result.getTestAssertions(); + + for (TestAssertion assertion : assertions) { + TestAssertion.Status status = assertion.getStatus(); + + if (status == TestAssertion.Status.FAILED) { + classifyAssertion(assertion, verificationResult); + } + } + + verificationResult.setCompliant(verificationResult.getTotalFailures() == 0); + + String baseName; + if (declaredFlavour != null) { + baseName = getStandardName(declaredFlavour); + } else if (validationIsPdfa) { + baseName = NOT_PDFA_STANDARD_NAME; + } else if (validationProfile != null) { + baseName = getStandardName(validationProfile); + } else { + baseName = "Unknown standard"; + } + + String standardDisplay = + formatStandardDisplay( + baseName, + verificationResult.getTotalFailures(), + verificationResult.getTotalWarnings(), + isPdfaFlavour(declaredFlavour), + validationIsPdfa && declaredFlavour == null); + verificationResult.setStandardName(standardDisplay); + verificationResult.setComplianceSummary(standardDisplay); + + log.debug( + "Validation complete for profile {} (declared: {}): {} errors, {} warnings", + validationProfile != null ? validationProfile.getId() : "unknown", + declaredFlavour != null ? declaredFlavour.getId() : NOT_PDFA_STANDARD_ID, + verificationResult.getTotalFailures(), + verificationResult.getTotalWarnings()); + + return verificationResult; + } + + private static void classifyAssertion( + TestAssertion assertion, PDFVerificationResult verificationResult) { + + PDFVerificationResult.ValidationIssue issue = createValidationIssue(assertion); + String ruleId = assertion.getRuleId() != null ? assertion.getRuleId().toString() : ""; + String message = assertion.getMessage() != null ? assertion.getMessage() : ""; + String clause = assertion.getRuleId() != null ? assertion.getRuleId().getClause() : ""; + + if (isWarningRule(ruleId)) { + verificationResult.addWarning(issue); + return; + } + + if (isWarningByMessage(message)) { + verificationResult.addWarning(issue); + return; + } + + if (isWarningByClause(clause)) { + verificationResult.addWarning(issue); + return; + } + + verificationResult.addFailure(issue); + } + + private static PDFVerificationResult.ValidationIssue createValidationIssue( + TestAssertion assertion) { + PDFVerificationResult.ValidationIssue issue = new PDFVerificationResult.ValidationIssue(); + issue.setRuleId(assertion.getRuleId() != null ? assertion.getRuleId().toString() : ""); + issue.setMessage(assertion.getMessage()); + issue.setLocation( + assertion.getLocation() != null ? assertion.getLocation().toString() : "Unknown"); + issue.setSpecification( + assertion.getRuleId() != null && assertion.getRuleId().getSpecification() != null + ? assertion.getRuleId().getSpecification().toString() + : ""); + issue.setClause(assertion.getRuleId() != null ? assertion.getRuleId().getClause() : ""); + int testNumber = assertion.getRuleId() != null ? assertion.getRuleId().getTestNumber() : 0; + issue.setTestNumber(testNumber > 0 ? String.valueOf(testNumber) : ""); + return issue; + } + + private static boolean isWarningRule(String ruleId) { + return ruleId != null && WARNING_RULES.contains(ruleId); + } + + public PDFVerificationResult validatePDF(InputStream pdfStream, String standardString) + throws IOException, ValidationException, ModelParsingException, EncryptedPdfException { + + byte[] pdfBytes = pdfStream.readAllBytes(); + PDFAFlavour validationFlavour = PDFAFlavour.fromString(standardString); + Optional declaredPdfaFlavour = extractDeclaredPdfaFlavour(pdfBytes); + + try (PDFAParser parser = + Foundries.defaultInstance() + .createParser(new ByteArrayInputStream(pdfBytes), validationFlavour)) { + PDFAValidator validator = + Foundries.defaultInstance().createValidator(validationFlavour, false); + ValidationResult result = validator.validate(parser); + + return convertToVerificationResult( + result, declaredPdfaFlavour.orElse(null), validationFlavour); + } + } + + public List validateAllDeclaredStandards(InputStream pdfStream) + throws IOException, ValidationException, ModelParsingException, EncryptedPdfException { + + byte[] pdfBytes = pdfStream.readAllBytes(); + Optional declaredPdfaFlavour = extractDeclaredPdfaFlavour(pdfBytes); + List results = new ArrayList<>(); + + List detectedFlavours; + try (PDFAParser detectionParser = + Foundries.defaultInstance().createParser(new ByteArrayInputStream(pdfBytes))) { + detectedFlavours = detectionParser.getFlavours(); + } + + List flavoursToValidate = new ArrayList<>(); + + declaredPdfaFlavour.ifPresent(flavoursToValidate::add); + + for (PDFAFlavour flavour : detectedFlavours) { + if (PDFFlavours.isFlavourFamily(flavour, PDFAFlavour.SpecificationFamily.PDF_A)) { + if (declaredPdfaFlavour.isPresent() && !declaredPdfaFlavour.get().equals(flavour)) { + flavoursToValidate.add(flavour); + } else if (declaredPdfaFlavour.isEmpty()) { + log.debug( + "Ignoring detected PDF/A flavour {} because no PDF/A declaration exists in XMP", + flavour.getId()); + } + } else if (PDFFlavours.isFlavourFamily(flavour, PDFAFlavour.SpecificationFamily.PDF_UA) + || PDFFlavours.isFlavourFamily( + flavour, PDFAFlavour.SpecificationFamily.WTPDF)) { + flavoursToValidate.add(flavour); + } + } + + if (declaredPdfaFlavour.isEmpty()) { + results.add(createNoPdfaDeclarationResult()); + } + + if (flavoursToValidate.isEmpty()) { + log.info("No verifiable PDF/A, PDF/UA, or WTPDF standards declared via XMP metadata"); + return results; + } + + for (PDFAFlavour flavour : flavoursToValidate) { + try (PDFAParser parser = + Foundries.defaultInstance() + .createParser(new ByteArrayInputStream(pdfBytes), flavour)) { + PDFAValidator validator = + Foundries.defaultInstance().createValidator(flavour, false); + ValidationResult result = validator.validate(parser); + PDFAFlavour declaredForResult = + PDFFlavours.isFlavourFamily(flavour, PDFAFlavour.SpecificationFamily.PDF_A) + ? declaredPdfaFlavour.orElse(null) + : flavour; + results.add(convertToVerificationResult(result, declaredForResult, flavour)); + } catch (Exception e) { + log.error("Error validating standard {}: {}", flavour.getId(), e.getMessage()); + results.add( + buildErrorResult( + declaredPdfaFlavour, + flavour, + "Validation error: " + e.getMessage())); + } + } + + return results; + } + + private static PDFVerificationResult createNoPdfaDeclarationResult() { + PDFVerificationResult result = new PDFVerificationResult(); + result.setStandard(NOT_PDFA_STANDARD_ID); + result.setStandardName(NOT_PDFA_STANDARD_NAME); + result.setComplianceSummary(NOT_PDFA_STANDARD_NAME); + result.setCompliant(false); + result.setDeclaredPdfa(false); + PDFVerificationResult.ValidationIssue issue = new PDFVerificationResult.ValidationIssue(); + issue.setMessage("Document does not declare PDF/A compliance in its XMP metadata."); + issue.setSpecification("XMP pdfaid"); + result.addFailure(issue); + return result; + } + + private static Optional extractDeclaredPdfaFlavour(byte[] pdfBytes) { + try (PDDocument document = Loader.loadPDF(pdfBytes)) { + PDMetadata metadata = document.getDocumentCatalog().getMetadata(); + if (metadata == null) { + return Optional.empty(); + } + + try (InputStream xmpStream = metadata.createInputStream()) { + if (xmpStream == null) { + return Optional.empty(); + } + DomXmpParser parser = new DomXmpParser(); + XMPMetadata xmpMetadata = parser.parse(xmpStream); + PDFAIdentificationSchema pdfaid = xmpMetadata.getPDFAIdentificationSchema(); + if (pdfaid == null) { + return Optional.empty(); + } + + Integer part = pdfaid.getPart(); + String conformance = pdfaid.getConformance(); + + if (part == null || conformance == null || conformance.isBlank()) { + return Optional.empty(); + } + + String flavourId = part + conformance.trim().toLowerCase(Locale.ROOT); + return Optional.ofNullable(PDFAFlavour.fromString(flavourId)); + } + } catch (XmpParsingException e) { + log.warn( + "Invalid XMP metadata encountered while checking PDF/A declaration: {}", + e.getMessage()); + log.debug("XMP parsing error", e); + return Optional.empty(); + } catch (Exception e) { + log.warn("Unable to extract PDF/A declaration from XMP: {}", e.getMessage()); + log.debug("XMP extraction error", e); + return Optional.empty(); + } + } + + private static PDFVerificationResult buildErrorResult( + Optional declaredPdfaFlavour, + PDFAFlavour validationFlavour, + String errorMessage) { + + PDFVerificationResult errorResult = new PDFVerificationResult(); + PDFAFlavour declaredForResult = + validationFlavour != null && isPdfaFlavour(validationFlavour) + ? declaredPdfaFlavour.orElse(null) + : validationFlavour; + + if (declaredForResult != null) { + errorResult.setStandard(declaredForResult.getId()); + errorResult.setStandardName(getStandardName(declaredForResult) + " with errors"); + errorResult.setDeclaredPdfa(isPdfaFlavour(declaredForResult)); + } else if (validationFlavour != null && isPdfaFlavour(validationFlavour)) { + errorResult.setStandard(NOT_PDFA_STANDARD_ID); + errorResult.setStandardName(NOT_PDFA_STANDARD_NAME); + errorResult.setDeclaredPdfa(false); + } else { + errorResult.setStandard( + validationFlavour != null ? validationFlavour.getId() : NOT_PDFA_STANDARD_ID); + errorResult.setStandardName( + (validationFlavour != null + ? getStandardName(validationFlavour) + : "Unknown standard") + + " with errors"); + errorResult.setDeclaredPdfa(false); + } + + errorResult.setValidationProfile( + validationFlavour != null ? validationFlavour.getId() : NOT_PDFA_STANDARD_ID); + errorResult.setValidationProfileName( + validationFlavour != null + ? getStandardName(validationFlavour) + : "Unknown standard"); + errorResult.setComplianceSummary(errorResult.getStandardName()); + errorResult.setCompliant(false); + + PDFVerificationResult.ValidationIssue failure = new PDFVerificationResult.ValidationIssue(); + failure.setMessage(errorMessage); + errorResult.addFailure(failure); + + return errorResult; + } + + private static boolean isPdfaFlavour(PDFAFlavour flavour) { + return PDFFlavours.isFlavourFamily(flavour, PDFAFlavour.SpecificationFamily.PDF_A); + } + + private static String formatStandardDisplay( + String baseName, + int errorCount, + int warningCount, + boolean declaredPdfa, + boolean inferredPdfaWithoutDeclaration) { + + if (inferredPdfaWithoutDeclaration) { + return NOT_PDFA_STANDARD_NAME; + } + + if (!declaredPdfa && NOT_PDFA_STANDARD_NAME.equals(baseName)) { + return NOT_PDFA_STANDARD_NAME; + } + + if (errorCount > 0) { + return baseName + " with errors"; + } + + if (warningCount > 0) { + return baseName + " with warnings"; + } + + return baseName + " compliant"; + } + + private static String getStandardName(PDFAFlavour flavour) { + String id = flavour.getId(); + String part = flavour.getPart().toString(); + String level = flavour.getLevel().toString(); + + // PDF/A standards - Fixed: proper length check and parentheses + if (!id.isEmpty() + && (id.charAt(0) == '1' + || id.charAt(0) == '2' + || id.charAt(0) == '3' + || id.charAt(0) == '4')) { + return "PDF/A-" + part + (level.isEmpty() ? "" : level); + } + // PDF/UA standards + else if (id.contains("ua")) { + return "PDF/UA-" + part; + } + // WTPDF standards + else if (id.contains("wtpdf")) { + return "WTPDF " + part; + } + + return flavour.toString(); + } +}