mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-23 16:05:09 +00:00
Merge branch 'main' into proprietary_module
This commit is contained in:
commit
85e92c65bc
1
.gitignore
vendored
1
.gitignore
vendored
@ -13,6 +13,7 @@ local.properties
|
||||
.recommenders
|
||||
.classpath
|
||||
.project
|
||||
*.local.json
|
||||
version.properties
|
||||
|
||||
#### Stirling-PDF Files ###
|
||||
|
@ -128,13 +128,13 @@ Stirling-PDF currently supports 40 languages!
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
|
@ -131,7 +131,7 @@ public class SplitPdfByChaptersController {
|
||||
Integer bookmarkLevel =
|
||||
request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
|
||||
if (bookmarkLevel < 0) {
|
||||
return ResponseEntity.badRequest().body("Invalid bookmark level".getBytes());
|
||||
throw new IllegalArgumentException("Invalid bookmark level");
|
||||
}
|
||||
sourceDocument = pdfDocumentFactory.load(file);
|
||||
|
||||
@ -139,7 +139,7 @@ public class SplitPdfByChaptersController {
|
||||
|
||||
if (outline == null) {
|
||||
log.warn("No outline found for {}", file.getOriginalFilename());
|
||||
return ResponseEntity.badRequest().body("No outline found".getBytes());
|
||||
throw new IllegalArgumentException("No outline found");
|
||||
}
|
||||
List<Bookmark> bookmarks = new ArrayList<>();
|
||||
try {
|
||||
|
@ -91,6 +91,59 @@ public class GetInfoOnPDF {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates structured summary data about the PDF highlighting its unique characteristics such
|
||||
* as encryption status, permission restrictions, and standards compliance.
|
||||
*
|
||||
* @param document The PDF document to analyze
|
||||
* @return An ObjectNode containing structured summary data
|
||||
*/
|
||||
private ObjectNode generatePDFSummaryData(PDDocument document) {
|
||||
ObjectNode summaryData = objectMapper.createObjectNode();
|
||||
|
||||
// Check if encrypted
|
||||
if (document.isEncrypted()) {
|
||||
summaryData.put("encrypted", true);
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
AccessPermission ap = document.getCurrentAccessPermission();
|
||||
ArrayNode restrictedPermissions = objectMapper.createArrayNode();
|
||||
|
||||
if (!ap.canAssembleDocument()) restrictedPermissions.add("document assembly");
|
||||
if (!ap.canExtractContent()) restrictedPermissions.add("content extraction");
|
||||
if (!ap.canExtractForAccessibility()) restrictedPermissions.add("accessibility extraction");
|
||||
if (!ap.canFillInForm()) restrictedPermissions.add("form filling");
|
||||
if (!ap.canModify()) restrictedPermissions.add("modification");
|
||||
if (!ap.canModifyAnnotations()) restrictedPermissions.add("annotation modification");
|
||||
if (!ap.canPrint()) restrictedPermissions.add("printing");
|
||||
|
||||
if (restrictedPermissions.size() > 0) {
|
||||
summaryData.set("restrictedPermissions", restrictedPermissions);
|
||||
summaryData.put("restrictedPermissionsCount", restrictedPermissions.size());
|
||||
}
|
||||
|
||||
// Check standard compliance
|
||||
if (checkForStandard(document, "PDF/A")) {
|
||||
summaryData.put("standardCompliance", "PDF/A");
|
||||
summaryData.put("standardPurpose", "long-term archiving");
|
||||
} else if (checkForStandard(document, "PDF/X")) {
|
||||
summaryData.put("standardCompliance", "PDF/X");
|
||||
summaryData.put("standardPurpose", "graphic exchange");
|
||||
} else if (checkForStandard(document, "PDF/UA")) {
|
||||
summaryData.put("standardCompliance", "PDF/UA");
|
||||
summaryData.put("standardPurpose", "universal accessibility");
|
||||
} else if (checkForStandard(document, "PDF/E")) {
|
||||
summaryData.put("standardCompliance", "PDF/E");
|
||||
summaryData.put("standardPurpose", "engineering workflows");
|
||||
} else if (checkForStandard(document, "PDF/VT")) {
|
||||
summaryData.put("standardCompliance", "PDF/VT");
|
||||
summaryData.put("standardPurpose", "variable and transactional printing");
|
||||
}
|
||||
|
||||
return summaryData;
|
||||
}
|
||||
|
||||
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
|
||||
// Check XMP Metadata
|
||||
try {
|
||||
@ -191,6 +244,12 @@ public class GetInfoOnPDF {
|
||||
}
|
||||
jsonOutput.set("FormFields", formFieldsNode);
|
||||
|
||||
// Generate structured summary data about PDF characteristics
|
||||
ObjectNode summaryData = generatePDFSummaryData(pdfBoxDoc);
|
||||
if (summaryData != null && summaryData.size() > 0) {
|
||||
jsonOutput.set("SummaryData", summaryData);
|
||||
}
|
||||
|
||||
// embeed files TODO size
|
||||
if (catalog.getNames() != null) {
|
||||
PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles();
|
||||
|
@ -6,133 +6,133 @@ language.direction=ltr
|
||||
|
||||
# Language names for reuse throughout the application
|
||||
lang.afr=Afrikaans
|
||||
lang.amh=Amharic
|
||||
lang.ara=Arabic
|
||||
lang.asm=Assamese
|
||||
lang.aze=Azerbaijani
|
||||
lang.aze_cyrl=Azerbaijani (Cyrillic)
|
||||
lang.bel=Belarusian
|
||||
lang.ben=Bengali
|
||||
lang.bod=Tibetan
|
||||
lang.bos=Bosnian
|
||||
lang.bre=Breton
|
||||
lang.bul=Bulgarian
|
||||
lang.cat=Catalan
|
||||
lang.amh=Amharisch
|
||||
lang.ara=Arabisch
|
||||
lang.asm=Assamesisch
|
||||
lang.aze=Aserbaidschanisch
|
||||
lang.aze_cyrl=Aserbaidschanisch (Kyrillisch)
|
||||
lang.bel=Weißrussisch
|
||||
lang.ben=Bengalisch
|
||||
lang.bod=Tibetisch
|
||||
lang.bos=Bosnisch
|
||||
lang.bre=Bretonisch
|
||||
lang.bul=Bulgarisch
|
||||
lang.cat=Katalanisch
|
||||
lang.ceb=Cebuano
|
||||
lang.ces=Czech
|
||||
lang.chi_sim=Chinese (Simplified)
|
||||
lang.chi_sim_vert=Chinese (Simplified, Vertical)
|
||||
lang.chi_tra=Chinese (Traditional)
|
||||
lang.chi_tra_vert=Chinese (Traditional, Vertical)
|
||||
lang.ces=Tschechisch
|
||||
lang.chi_sim=Chinesisch (vereinfacht)
|
||||
lang.chi_sim_vert=Chinesisch (vereinfacht, vertikal)
|
||||
lang.chi_tra=Chinesisch (traditionell)
|
||||
lang.chi_tra_vert=Chinesisch (traditionell, vertikal)
|
||||
lang.chr=Cherokee
|
||||
lang.cos=Corsican
|
||||
lang.cym=Welsh
|
||||
lang.dan=Danish
|
||||
lang.dan_frak=Danish (Fraktur)
|
||||
lang.deu=German
|
||||
lang.deu_frak=German (Fraktur)
|
||||
lang.cos=Korsisch
|
||||
lang.cym=Walisisch
|
||||
lang.dan=Dänisch
|
||||
lang.dan_frak=Dänisch (Fraktur)
|
||||
lang.deu=Deutsch
|
||||
lang.deu_frak=Deutsch (Fraktur)
|
||||
lang.div=Divehi
|
||||
lang.dzo=Dzongkha
|
||||
lang.ell=Greek
|
||||
lang.eng=English
|
||||
lang.enm=English, Middle (1100-1500)
|
||||
lang.ell=Griechisch
|
||||
lang.eng=Englisch
|
||||
lang.enm=Englisch, Mittelenglisch (1100-1500)
|
||||
lang.epo=Esperanto
|
||||
lang.equ=Math / equation detection module
|
||||
lang.est=Estonian
|
||||
lang.eus=Basque
|
||||
lang.fao=Faroese
|
||||
lang.fas=Persian
|
||||
lang.fil=Filipino
|
||||
lang.fin=Finnish
|
||||
lang.fra=French
|
||||
lang.frk=Frankish
|
||||
lang.frm=French, Middle (ca.1400-1600)
|
||||
lang.fry=Western Frisian
|
||||
lang.gla=Scottish Gaelic
|
||||
lang.gle=Irish
|
||||
lang.glg=Galician
|
||||
lang.grc=Ancient Greek
|
||||
lang.equ=Mathe-/Gleichungserkennungsmodul
|
||||
lang.est=Estnisch
|
||||
lang.eus=Baskisch
|
||||
lang.fao=Färöisch
|
||||
lang.fas=Persisch
|
||||
lang.fil=Philippinisch
|
||||
lang.fin=Finnisch
|
||||
lang.fra=Französisch
|
||||
lang.frk=Fränkisch
|
||||
lang.frm=Französisch, Mittelfranzösisch (ca. 1400-1600)
|
||||
lang.fry=Westfriesisch
|
||||
lang.gla=Schottisch-Gälisch
|
||||
lang.gle=Irisch
|
||||
lang.glg=Galizisch
|
||||
lang.grc=Altgriechisch
|
||||
lang.guj=Gujarati
|
||||
lang.hat=Haitian, Haitian Creole
|
||||
lang.heb=Hebrew
|
||||
lang.hat=Haitianisch, haitianisches Kreol
|
||||
lang.heb=Hebräisch
|
||||
lang.hin=Hindi
|
||||
lang.hrv=Croatian
|
||||
lang.hun=Hungarian
|
||||
lang.hye=Armenian
|
||||
lang.hrv=Kroatisch
|
||||
lang.hun=Ungarisch
|
||||
lang.hye=Armenisch
|
||||
lang.iku=Inuktitut
|
||||
lang.ind=Indonesian
|
||||
lang.isl=Icelandic
|
||||
lang.ita=Italian
|
||||
lang.ita_old=Italian (Old)
|
||||
lang.jav=Javanese
|
||||
lang.jpn=Japanese
|
||||
lang.jpn_vert=Japanese (Vertical)
|
||||
lang.ind=Indonesisch
|
||||
lang.isl=Isländisch
|
||||
lang.ita=Italienisch
|
||||
lang.ita_old=Italienisch (Alt)
|
||||
lang.jav=Javanesisch
|
||||
lang.jpn=Japanisch
|
||||
lang.jpn_vert=Japanisch (vertikal)
|
||||
lang.kan=Kannada
|
||||
lang.kat=Georgian
|
||||
lang.kat_old=Georgian (Old)
|
||||
lang.kaz=Kazakh
|
||||
lang.khm=Central Khmer
|
||||
lang.kir=Kirghiz, Kyrgyz
|
||||
lang.kmr=Northern Kurdish
|
||||
lang.kor=Korean
|
||||
lang.kor_vert=Korean (Vertical)
|
||||
lang.lao=Lao
|
||||
lang.lat=Latin
|
||||
lang.lav=Latvian
|
||||
lang.lit=Lithuanian
|
||||
lang.ltz=Luxembourgish
|
||||
lang.kat=Georgisch
|
||||
lang.kat_old=Georgisch (Alt)
|
||||
lang.kaz=Kasachisch
|
||||
lang.khm=Zentral Khmer
|
||||
lang.kir=Kirgisisch
|
||||
lang.kmr=Nordkurdisch
|
||||
lang.kor=Koreanisch
|
||||
lang.kor_vert=Koreanisch (vertikal)
|
||||
lang.lao=Laotisch
|
||||
lang.lat=Latein
|
||||
lang.lav=Lettisch
|
||||
lang.lit=Litauisch
|
||||
lang.ltz=Luxemburgisch
|
||||
lang.mal=Malayalam
|
||||
lang.mar=Marathi
|
||||
lang.mkd=Macedonian
|
||||
lang.mlt=Maltese
|
||||
lang.mon=Mongolian
|
||||
lang.mkd=Mazedonisch
|
||||
lang.mlt=Maltesisch
|
||||
lang.mon=Mongolisch
|
||||
lang.mri=Maori
|
||||
lang.msa=Malay
|
||||
lang.mya=Burmese
|
||||
lang.msa=Malaiisch
|
||||
lang.mya=Burmesisch
|
||||
lang.nep=Nepali
|
||||
lang.nld=Dutch; Flemish
|
||||
lang.nor=Norwegian
|
||||
lang.oci=Occitan (post 1500)
|
||||
lang.nld=Niederländisch; Flämisch
|
||||
lang.nor=Norwegisch
|
||||
lang.oci=Okzitanisch (nach 1500)
|
||||
lang.ori=Oriya
|
||||
lang.osd=Orientation and script detection module
|
||||
lang.osd=Orientierungs- und Skripterkennungsmodul
|
||||
lang.pan=Panjabi, Punjabi
|
||||
lang.pol=Polish
|
||||
lang.por=Portuguese
|
||||
lang.pus=Pushto, Pashto
|
||||
lang.pol=Polnisch
|
||||
lang.por=Portugiesisch
|
||||
lang.pus=Puschtu, Paschtu
|
||||
lang.que=Quechua
|
||||
lang.ron=Romanian, Moldavian, Moldovan
|
||||
lang.rus=Russian
|
||||
lang.ron=Rumänisch, Moldauisch, Moldauisch
|
||||
lang.rus=Russisch
|
||||
lang.san=Sanskrit
|
||||
lang.sin=Sinhala, Sinhalese
|
||||
lang.slk=Slovak
|
||||
lang.slk_frak=Slovak (Fraktur)
|
||||
lang.slv=Slovenian
|
||||
lang.sin=Singhalesisch
|
||||
lang.slk=Slowakisch
|
||||
lang.slk_frak=Slowakisch (Fraktur)
|
||||
lang.slv=Slowenisch
|
||||
lang.snd=Sindhi
|
||||
lang.spa=Spanish
|
||||
lang.spa_old=Spanish (Old)
|
||||
lang.sqi=Albanian
|
||||
lang.srp=Serbian
|
||||
lang.srp_latn=Serbian (Latin)
|
||||
lang.sun=Sundanese
|
||||
lang.swa=Swahili
|
||||
lang.swe=Swedish
|
||||
lang.syr=Syriac
|
||||
lang.spa=Spanisch
|
||||
lang.spa_old=Spanisch (Alt)
|
||||
lang.sqi=Albanisch
|
||||
lang.srp=Serbisch
|
||||
lang.srp_latn=Serbisch (Lateinisch)
|
||||
lang.sun=Sundanesisch
|
||||
lang.swa=Suaheli
|
||||
lang.swe=Schwedisch
|
||||
lang.syr=Syrisch
|
||||
lang.tam=Tamil
|
||||
lang.tat=Tatar
|
||||
lang.tat=Tatarisch
|
||||
lang.tel=Telugu
|
||||
lang.tgk=Tajik
|
||||
lang.tgk=Tadschikisch
|
||||
lang.tgl=Tagalog
|
||||
lang.tha=Thai
|
||||
lang.tha=Thailändisch
|
||||
lang.tir=Tigrinya
|
||||
lang.ton=Tonga (Tonga Islands)
|
||||
lang.tur=Turkish
|
||||
lang.uig=Uighur, Uyghur
|
||||
lang.ukr=Ukrainian
|
||||
lang.ton=Tonga (Tonga-Inseln)
|
||||
lang.tur=Türkisch
|
||||
lang.uig=Uigurisch
|
||||
lang.ukr=Ukrainisch
|
||||
lang.urd=Urdu
|
||||
lang.uzb=Uzbek
|
||||
lang.uzb_cyrl=Uzbek (Cyrillic)
|
||||
lang.vie=Vietnamese
|
||||
lang.yid=Yiddish
|
||||
lang.uzb=Usbekisch
|
||||
lang.uzb_cyrl=Usbekisch (Kyrillisch)
|
||||
lang.vie=Vietnamesisch
|
||||
lang.yid=Jiddisch
|
||||
lang.yor=Yoruba
|
||||
|
||||
addPageNumbers.fontSize=Schriftgröße
|
||||
@ -1570,37 +1570,37 @@ cookieBanner.preferencesModal.analytics.title=Analyse
|
||||
cookieBanner.preferencesModal.analytics.description=Diese Cookies helfen uns zu verstehen, wie unsere Tools genutzt werden, damit wir uns darauf konzentrieren können, die Funktionen zu entwickeln, die unserer Community am meisten am Herzen liegen. Seien Sie beruhigt – Stirling PDF kann und wird niemals den Inhalt der Dokumente verfolgen, mit denen Sie arbeiten.
|
||||
|
||||
#fakeScan
|
||||
fakeScan.title=Fake Scan
|
||||
fakeScan.header=Fake Scan
|
||||
fakeScan.description=Create a PDF that looks like it was scanned
|
||||
fakeScan.selectPDF=Select PDF:
|
||||
fakeScan.quality=Scan Quality
|
||||
fakeScan.quality.low=Low
|
||||
fakeScan.title=Fake-Scan-PDF
|
||||
fakeScan.header=Fake-Scan-PDF
|
||||
fakeScan.description=Erstellen Sie ein PDF, das so aussieht, als wäre es gescannt worden
|
||||
fakeScan.selectPDF=Wählen Sie PDF:
|
||||
fakeScan.quality=Scan-Qualität
|
||||
fakeScan.quality.low=Niedrig
|
||||
fakeScan.quality.medium=Medium
|
||||
fakeScan.quality.high=High
|
||||
fakeScan.rotation=Rotation Angle
|
||||
fakeScan.rotation.none=None
|
||||
fakeScan.rotation.slight=Slight
|
||||
fakeScan.rotation.moderate=Moderate
|
||||
fakeScan.rotation.severe=Severe
|
||||
fakeScan.submit=Create Fake Scan
|
||||
fakeScan.quality.high=Hoch
|
||||
fakeScan.rotation=Rotationswinkel
|
||||
fakeScan.rotation.none=Keiner
|
||||
fakeScan.rotation.slight=Leicht
|
||||
fakeScan.rotation.moderate=Mäßig
|
||||
fakeScan.rotation.severe=Schwer
|
||||
fakeScan.submit=Erstellen Sie einen Fake-Scan
|
||||
|
||||
#home.fakeScan
|
||||
home.fakeScan.title=Fake Scan
|
||||
home.fakeScan.desc=Create a PDF that looks like it was scanned
|
||||
fakeScan.tags=scan,simulate,realistic,convert
|
||||
home.fakeScan.title=Fake-Scan-PDF
|
||||
home.fakeScan.desc=Erstellen Sie ein PDF, das so aussieht, als wäre es gescannt worden
|
||||
fakeScan.tags=scannen,simulieren,realistisch,konvertieren,fake,scan,pdf
|
||||
|
||||
# FakeScan advanced settings (frontend)
|
||||
fakeScan.advancedSettings=Enable Advanced Scan Settings
|
||||
fakeScan.colorspace=Colorspace
|
||||
fakeScan.colorspace.grayscale=Grayscale
|
||||
fakeScan.colorspace.color=Color
|
||||
fakeScan.border=Border (px)
|
||||
fakeScan.rotate=Base Rotation (degrees)
|
||||
fakeScan.rotateVariance=Rotation Variance (degrees)
|
||||
fakeScan.brightness=Brightness
|
||||
fakeScan.contrast=Contrast
|
||||
fakeScan.blur=Blur
|
||||
fakeScan.noise=Noise
|
||||
fakeScan.yellowish=Yellowish (simulate old paper)
|
||||
fakeScan.resolution=Resolution (DPI)
|
||||
fakeScan.advancedSettings=Aktivieren Sie erweiterte Scaneinstellungen
|
||||
fakeScan.colorspace=Farbraum
|
||||
fakeScan.colorspace.grayscale=Graustufen
|
||||
fakeScan.colorspace.color=Farbe
|
||||
fakeScan.border=Grenze (PX)
|
||||
fakeScan.rotate=Grundrotation (Grad)
|
||||
fakeScan.rotateVariance=Rotationsvarianz (Grad)
|
||||
fakeScan.brightness=Helligkeit
|
||||
fakeScan.contrast=Kontrast
|
||||
fakeScan.blur=Verwischen
|
||||
fakeScan.noise=Rauschen
|
||||
fakeScan.yellowish=Gelblich (simulieren Sie altes Papier)
|
||||
fakeScan.resolution=Auflösung (DPI)
|
||||
|
@ -807,6 +807,28 @@ getPdfInfo.title=Get Info on PDF
|
||||
getPdfInfo.header=Get Info on PDF
|
||||
getPdfInfo.submit=Get Info
|
||||
getPdfInfo.downloadJson=Download JSON
|
||||
getPdfInfo.summary=PDF Summary
|
||||
getPdfInfo.summary.encrypted=This PDF is encrypted so may face issues with some applications
|
||||
getPdfInfo.summary.permissions=This PDF has {0} restricted permissions which may limit what you can do with it
|
||||
getPdfInfo.summary.compliance=This PDF complies with the {0} standard
|
||||
getPdfInfo.summary.basicInfo=Basic Information
|
||||
getPdfInfo.summary.docInfo=Document Information
|
||||
getPdfInfo.summary.encrypted.alert=Encrypted PDF - This document is password protected
|
||||
getPdfInfo.summary.not.encrypted.alert=Unencrypted PDF - No password protection
|
||||
getPdfInfo.summary.permissions.alert=Restricted Permissions - {0} actions are not allowed
|
||||
getPdfInfo.summary.all.permissions.alert=All Permissions Allowed
|
||||
getPdfInfo.summary.compliance.alert={0} Compliant
|
||||
getPdfInfo.summary.no.compliance.alert=No Compliance Standards
|
||||
getPdfInfo.summary.security.section=Security Status
|
||||
getPdfInfo.section.BasicInfo=Basic Information about the PDF document including file size, page count, and language
|
||||
getPdfInfo.section.Metadata=Document metadata including title, author, creation date and other document properties
|
||||
getPdfInfo.section.DocumentInfo=Technical details about the PDF document structure and version
|
||||
getPdfInfo.section.Compliancy=PDF standards compliance information (PDF/A, PDF/X, etc.)
|
||||
getPdfInfo.section.Encryption=Security and encryption details of the document
|
||||
getPdfInfo.section.Permissions=Document permission settings that control what actions can be performed
|
||||
getPdfInfo.section.Other=Additional document components like bookmarks, layers, and embedded files
|
||||
getPdfInfo.section.FormFields=Interactive form fields present in the document
|
||||
getPdfInfo.section.PerPageInfo=Detailed information about each page in the document
|
||||
|
||||
|
||||
#markdown-to-pdf
|
||||
|
@ -11,7 +11,7 @@
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="col-md-7 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon other">info</span>
|
||||
<span class="tool-header-text" th:text="#{getPdfInfo.header}"></span>
|
||||
@ -22,6 +22,82 @@
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{getPdfInfo.submit}"></button>
|
||||
</form>
|
||||
<div class="container mt-0">
|
||||
<!-- PDF Summary section -->
|
||||
<div id="pdf-summary" class="card mt-3 mb-3" style="display: none;">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0" id="summary-heading">PDF Summary</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Quick overview of key details -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6 id="summary-basic-info-heading">Basic Information</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><strong>Pages:</strong> <span id="summary-pages">-</span></li>
|
||||
<li><strong>File Size:</strong> <span id="summary-size">-</span></li>
|
||||
<li><strong>PDF Version:</strong> <span id="summary-version">-</span></li>
|
||||
<li><strong>Language:</strong> <span id="summary-language">-</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 id="summary-doc-info-heading">Document Information</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><strong>Title:</strong> <span id="summary-title">-</span></li>
|
||||
<li><strong>Author:</strong> <span id="summary-author">-</span></li>
|
||||
<li><strong>Created:</strong> <span id="summary-created">-</span></li>
|
||||
<li><strong>Modified:</strong> <span id="summary-modified">-</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security section -->
|
||||
<div class="mt-4 mb-3">
|
||||
<h6 id="summary-security-heading">Security Status</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div id="encryption-status" class="card mb-2 h-100">
|
||||
<div class="card-body p-2 d-flex align-items-center">
|
||||
<span id="encryption-icon" class="me-2"><i class="bi bi-lock"></i></span>
|
||||
<span id="encryption-text" class="small">Encryption: Unknown</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div id="permissions-status" class="card mb-2 h-100">
|
||||
<div class="card-body p-2 d-flex align-items-center">
|
||||
<span id="permissions-icon" class="me-2"><i class="bi bi-shield"></i></span>
|
||||
<span id="permissions-text" class="small">Permissions: Unknown</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div id="compliance-status" class="card mb-2 h-100">
|
||||
<div class="card-body p-2 d-flex align-items-center">
|
||||
<span id="compliance-icon" class="me-2"><i class="bi bi-check-circle"></i></span>
|
||||
<span id="compliance-text" class="small">Compliance: Unknown</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detailed alerts -->
|
||||
<div id="summary-alerts" class="mt-3">
|
||||
<!-- Will be populated with detailed alerts for encryption, permissions, etc. -->
|
||||
</div>
|
||||
|
||||
<!-- Descriptive note about PDF characteristics -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">PDF Overview</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p id="summary-text" class="mb-0"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Iterate over each main section in the JSON -->
|
||||
<div id="json-content">
|
||||
<!-- JavaScript will populate this section -->
|
||||
@ -31,10 +107,48 @@
|
||||
<a href="#" id="downloadJson" class="btn btn-primary mt-3" style="display: none;" th:text="#{getPdfInfo.downloadJson}">Download JSON</a>
|
||||
</div>
|
||||
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
||||
<script>
|
||||
<script th:inline="javascript">
|
||||
// Pre-load message translations
|
||||
const getPdfInfoSummary = /*[[#{getPdfInfo.summary}]]*/ "PDF Summary";
|
||||
const getPdfInfoSummaryEncrypted = /*[[#{getPdfInfo.summary.encrypted}]]*/ "This PDF is encrypted so may face issues with some applications";
|
||||
const getPdfInfoSummaryPermissions = /*[[#{getPdfInfo.summary.permissions}]]*/ "This PDF has {0} restricted permissions which may limit what you can do with it";
|
||||
const getPdfInfoSummaryCompliance = /*[[#{getPdfInfo.summary.compliance}]]*/ "This PDF complies with the {0} standard, meaning it is suitable for {1}";
|
||||
const getPdfInfoSummaryBasicInfo = /*[[#{getPdfInfo.summary.basicInfo}]]*/ "Basic Information";
|
||||
const getPdfInfoSummaryDocInfo = /*[[#{getPdfInfo.summary.docInfo}]]*/ "Document Information";
|
||||
const getPdfInfoSummarySecuritySection = /*[[#{getPdfInfo.summary.security.section}]]*/ "Security Status";
|
||||
const getPdfInfoSummaryEncryptedAlert = /*[[#{getPdfInfo.summary.encrypted.alert}]]*/ "Encrypted PDF - This document is password protected";
|
||||
const getPdfInfoSummaryNotEncryptedAlert = /*[[#{getPdfInfo.summary.not.encrypted.alert}]]*/ "Unencrypted PDF - No password protection";
|
||||
const getPdfInfoSummaryPermissionsAlert = /*[[#{getPdfInfo.summary.permissions.alert}]]*/ "Restricted Permissions - {0} actions are not allowed";
|
||||
const getPdfInfoSummaryAllPermissionsAlert = /*[[#{getPdfInfo.summary.all.permissions.alert}]]*/ "All Permissions Allowed";
|
||||
const getPdfInfoSummaryComplianceAlert = /*[[#{getPdfInfo.summary.compliance.alert}]]*/ "{0} Compliant";
|
||||
const getPdfInfoSummaryNoComplianceAlert = /*[[#{getPdfInfo.summary.no.compliance.alert}]]*/ "No Compliance Standards";
|
||||
|
||||
// Update the summary headings
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('summary-heading').textContent = getPdfInfoSummary;
|
||||
document.getElementById('summary-basic-info-heading').textContent = getPdfInfoSummaryBasicInfo;
|
||||
document.getElementById('summary-doc-info-heading').textContent = getPdfInfoSummaryDocInfo;
|
||||
document.getElementById('summary-security-heading').textContent = getPdfInfoSummarySecuritySection;
|
||||
});
|
||||
|
||||
// Pre-load section descriptions
|
||||
const getPdfInfoSectionBasicInfo = /*[[#{getPdfInfo.section.BasicInfo}]]*/ "Basic Information about the PDF document including file size, page count, and language";
|
||||
const getPdfInfoSectionMetadata = /*[[#{getPdfInfo.section.Metadata}]]*/ "Document metadata including title, author, creation date and other document properties";
|
||||
const getPdfInfoSectionDocumentInfo = /*[[#{getPdfInfo.section.DocumentInfo}]]*/ "Technical details about the PDF document structure and version";
|
||||
const getPdfInfoSectionCompliancy = /*[[#{getPdfInfo.section.Compliancy}]]*/ "PDF standards compliance information (PDF/A, PDF/X, etc.)";
|
||||
const getPdfInfoSectionEncryption = /*[[#{getPdfInfo.section.Encryption}]]*/ "Security and encryption details of the document";
|
||||
const getPdfInfoSectionPermissions = /*[[#{getPdfInfo.section.Permissions}]]*/ "Document permission settings that control what actions can be performed";
|
||||
const getPdfInfoSectionOther = /*[[#{getPdfInfo.section.Other}]]*/ "Additional document components like bookmarks, layers, and embedded files";
|
||||
const getPdfInfoSectionFormFields = /*[[#{getPdfInfo.section.FormFields}]]*/ "Interactive form fields present in the document";
|
||||
const getPdfInfoSectionPerPageInfo = /*[[#{getPdfInfo.section.PerPageInfo}]]*/ "Detailed information about each page in the document";
|
||||
|
||||
document.getElementById("pdfInfoForm").addEventListener("submit", function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Clear previous results when submitting a new form
|
||||
document.getElementById('json-content').innerHTML = '';
|
||||
document.getElementById('pdf-summary').style.display = 'none';
|
||||
document.getElementById('downloadJson').style.display = 'none';
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
|
||||
@ -42,10 +156,330 @@
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => response.json()).then(data => {
|
||||
// Populate and display the enhanced PDF summary
|
||||
populateSummarySection(data);
|
||||
|
||||
displayJsonData(data);
|
||||
setDownloadLink(data);
|
||||
document.getElementById("downloadJson").style.display = "block";
|
||||
}).catch(error => console.error('Error:', error));
|
||||
|
||||
// Function to reset all summary elements to default state
|
||||
function resetSummaryElements() {
|
||||
// Reset basic information fields
|
||||
document.getElementById('summary-pages').textContent = '-';
|
||||
document.getElementById('summary-size').textContent = '-';
|
||||
document.getElementById('summary-version').textContent = '-';
|
||||
document.getElementById('summary-language').textContent = '-';
|
||||
|
||||
// Reset document information fields
|
||||
document.getElementById('summary-title').textContent = '-';
|
||||
document.getElementById('summary-author').textContent = '-';
|
||||
document.getElementById('summary-created').textContent = '-';
|
||||
document.getElementById('summary-modified').textContent = '-';
|
||||
|
||||
// Reset security status cards
|
||||
const cards = ['encryption-status', 'permissions-status', 'compliance-status'];
|
||||
cards.forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
// Remove all classes except the base ones
|
||||
card.className = 'card mb-2 h-100';
|
||||
});
|
||||
|
||||
// Reset status text and icons
|
||||
document.getElementById('encryption-icon').innerHTML = '<i class="bi bi-lock"></i>';
|
||||
document.getElementById('encryption-text').textContent = 'Encryption: Unknown';
|
||||
|
||||
document.getElementById('permissions-icon').innerHTML = '<i class="bi bi-shield"></i>';
|
||||
document.getElementById('permissions-text').textContent = 'Permissions: Unknown';
|
||||
|
||||
document.getElementById('compliance-icon').innerHTML = '<i class="bi bi-check-circle"></i>';
|
||||
document.getElementById('compliance-text').textContent = 'Compliance: Unknown';
|
||||
|
||||
// Clear alerts container
|
||||
document.getElementById('summary-alerts').innerHTML = '';
|
||||
|
||||
// Reset summary text
|
||||
document.getElementById('summary-text').innerHTML = '';
|
||||
}
|
||||
|
||||
// Function to populate the enhanced summary section
|
||||
function populateSummarySection(data) {
|
||||
// Reset all elements first
|
||||
resetSummaryElements();
|
||||
|
||||
// Get basic information
|
||||
if (data.BasicInfo) {
|
||||
document.getElementById('summary-pages').textContent = data.BasicInfo["Number of pages"] || "-";
|
||||
|
||||
// Format file size nicely
|
||||
let fileSize = data.BasicInfo["FileSizeInBytes"];
|
||||
if (fileSize) {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(fileSize) / Math.log(1024));
|
||||
fileSize = (fileSize / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
|
||||
document.getElementById('summary-size').textContent = fileSize;
|
||||
}
|
||||
|
||||
document.getElementById('summary-language').textContent = data.BasicInfo["Language"] || "-";
|
||||
}
|
||||
|
||||
// Get document information
|
||||
if (data.DocumentInfo) {
|
||||
document.getElementById('summary-version').textContent = data.DocumentInfo["PDF version"] || "-";
|
||||
}
|
||||
|
||||
// Get metadata
|
||||
if (data.Metadata) {
|
||||
document.getElementById('summary-title').textContent = data.Metadata["Title"] || "-";
|
||||
document.getElementById('summary-author').textContent = data.Metadata["Author"] || "-";
|
||||
document.getElementById('summary-created').textContent = data.Metadata["CreationDate"] || "-";
|
||||
document.getElementById('summary-modified').textContent = data.Metadata["ModificationDate"] || "-";
|
||||
}
|
||||
|
||||
// Update security status cards
|
||||
|
||||
// Encryption status
|
||||
const encryptionStatusCard = document.getElementById('encryption-status');
|
||||
const encryptionIcon = document.getElementById('encryption-icon');
|
||||
const encryptionText = document.getElementById('encryption-text');
|
||||
|
||||
if (data.Encryption && data.Encryption.IsEncrypted) {
|
||||
encryptionIcon.innerHTML = '<i class="bi bi-lock-fill"></i>';
|
||||
encryptionText.textContent = getPdfInfoSummaryEncryptedAlert;
|
||||
} else {
|
||||
encryptionIcon.innerHTML = '<i class="bi bi-unlock-fill"></i>';
|
||||
encryptionText.textContent = getPdfInfoSummaryNotEncryptedAlert;
|
||||
}
|
||||
|
||||
// Permissions status
|
||||
const permissionsStatusCard = document.getElementById('permissions-status');
|
||||
const permissionsIcon = document.getElementById('permissions-icon');
|
||||
const permissionsText = document.getElementById('permissions-text');
|
||||
|
||||
let restrictedPermissions = [];
|
||||
if (data.Permissions) {
|
||||
for (const [permission, state] of Object.entries(data.Permissions)) {
|
||||
if (state === "Not Allowed") {
|
||||
restrictedPermissions.push(permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (restrictedPermissions.length > 0) {
|
||||
permissionsIcon.innerHTML = '<i class="bi bi-shield-lock-fill"></i>';
|
||||
const formattedAlert = getPdfInfoSummaryPermissionsAlert.replace('{0}', restrictedPermissions.length);
|
||||
permissionsText.textContent = formattedAlert;
|
||||
} else {
|
||||
permissionsIcon.innerHTML = '<i class="bi bi-shield-check"></i>';
|
||||
permissionsText.textContent = getPdfInfoSummaryAllPermissionsAlert;
|
||||
}
|
||||
|
||||
// Compliance status
|
||||
const complianceStatusCard = document.getElementById('compliance-status');
|
||||
const complianceIcon = document.getElementById('compliance-icon');
|
||||
const complianceText = document.getElementById('compliance-text');
|
||||
|
||||
let hasCompliance = false;
|
||||
let compliantStandards = [];
|
||||
|
||||
if (data.Compliancy) {
|
||||
for (const [standard, compliant] of Object.entries(data.Compliancy)) {
|
||||
if (compliant === true) {
|
||||
hasCompliance = true;
|
||||
const standardName = standard.replace("Is", "").replace("Compliant", "");
|
||||
compliantStandards.push(standardName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCompliance) {
|
||||
complianceIcon.innerHTML = '<i class="bi bi-check-circle-fill"></i>';
|
||||
const formattedAlert = getPdfInfoSummaryComplianceAlert.replace('{0}', compliantStandards.join(', '));
|
||||
complianceText.textContent = formattedAlert;
|
||||
} else {
|
||||
complianceIcon.innerHTML = '<i class="bi bi-dash-circle"></i>';
|
||||
complianceText.textContent = getPdfInfoSummaryNoComplianceAlert;
|
||||
}
|
||||
|
||||
// Create detailed characteristic alerts
|
||||
const alertsContainer = document.getElementById('summary-alerts');
|
||||
|
||||
// Clear previous alerts
|
||||
alertsContainer.innerHTML = '';
|
||||
|
||||
// Create a single comprehensive security details section
|
||||
let hasSummaryInfo = false;
|
||||
|
||||
// Create a consolidated security details card if there are security details worth highlighting
|
||||
if ((data.Encryption && data.Encryption.IsEncrypted) ||
|
||||
restrictedPermissions.length > 0 ||
|
||||
hasCompliance) {
|
||||
|
||||
const securityDetailsCard = document.createElement('div');
|
||||
securityDetailsCard.className = 'card mt-3 mb-3';
|
||||
|
||||
const cardHeader = document.createElement('div');
|
||||
cardHeader.className = 'card-header';
|
||||
cardHeader.innerHTML = '<h6 class="mb-0">Detailed Security Information</h6>';
|
||||
securityDetailsCard.appendChild(cardHeader);
|
||||
|
||||
const cardBody = document.createElement('div');
|
||||
cardBody.className = 'card-body';
|
||||
|
||||
// Add detailed encryption info
|
||||
if (data.Encryption && data.Encryption.IsEncrypted) {
|
||||
const encryptionDiv = document.createElement('div');
|
||||
encryptionDiv.className = 'mb-3';
|
||||
encryptionDiv.innerHTML = '<h6>Encryption Details:</h6>';
|
||||
|
||||
const encryptionList = document.createElement('ul');
|
||||
encryptionList.className = 'list-unstyled';
|
||||
|
||||
if (data.Encryption.EncryptionAlgorithm) {
|
||||
encryptionList.innerHTML += `<li><strong>Algorithm:</strong> ${data.Encryption.EncryptionAlgorithm}</li>`;
|
||||
}
|
||||
if (data.Encryption.KeyLength) {
|
||||
encryptionList.innerHTML += `<li><strong>Key Length:</strong> ${data.Encryption.KeyLength} bits</li>`;
|
||||
}
|
||||
|
||||
encryptionDiv.appendChild(encryptionList);
|
||||
cardBody.appendChild(encryptionDiv);
|
||||
hasSummaryInfo = true;
|
||||
}
|
||||
|
||||
// Add detailed permissions info
|
||||
if (restrictedPermissions.length > 0) {
|
||||
const permissionsDiv = document.createElement('div');
|
||||
permissionsDiv.className = 'mb-3';
|
||||
permissionsDiv.innerHTML = '<h6>Restricted Permissions:</h6>';
|
||||
|
||||
const permissionsList = document.createElement('ul');
|
||||
restrictedPermissions.forEach(perm => {
|
||||
permissionsList.innerHTML += `<li>${perm}</li>`;
|
||||
});
|
||||
|
||||
permissionsDiv.appendChild(permissionsList);
|
||||
cardBody.appendChild(permissionsDiv);
|
||||
hasSummaryInfo = true;
|
||||
}
|
||||
|
||||
// Add detailed compliance info
|
||||
if (hasCompliance) {
|
||||
const complianceDiv = document.createElement('div');
|
||||
complianceDiv.className = 'mb-3';
|
||||
complianceDiv.innerHTML = '<h6>Standards Compliance:</h6>';
|
||||
|
||||
const complianceList = document.createElement('ul');
|
||||
complianceList.className = 'list-unstyled';
|
||||
|
||||
compliantStandards.forEach(standard => {
|
||||
let standardDescription = '';
|
||||
|
||||
// Add brief descriptions for standards
|
||||
if (standard === "PDF/A") {
|
||||
standardDescription = 'ISO standard for long-term document archiving';
|
||||
} else if (standard === "PDF/X") {
|
||||
standardDescription = 'ISO standard for printing and graphic arts exchange';
|
||||
} else if (standard === "PDF/UA") {
|
||||
standardDescription = 'ISO standard for universal accessibility';
|
||||
} else if (standard === "PDF/E") {
|
||||
standardDescription = 'ISO standard for engineering documents';
|
||||
} else if (standard === "PDF/VT") {
|
||||
standardDescription = 'ISO standard for variable and transactional printing';
|
||||
}
|
||||
|
||||
complianceList.innerHTML += `<li><strong>${standard}:</strong> ${standardDescription}</li>`;
|
||||
});
|
||||
|
||||
complianceDiv.appendChild(complianceList);
|
||||
cardBody.appendChild(complianceDiv);
|
||||
hasSummaryInfo = true;
|
||||
}
|
||||
|
||||
securityDetailsCard.appendChild(cardBody);
|
||||
|
||||
if (hasSummaryInfo) {
|
||||
alertsContainer.appendChild(securityDetailsCard);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a general document summary
|
||||
const summaryTextElement = document.getElementById('summary-text');
|
||||
|
||||
// Create a general summary for the document
|
||||
let generalSummary = `This is a ${data.BasicInfo["Number of pages"] || "multi"}-page PDF`;
|
||||
|
||||
if (data.Metadata && data.Metadata["Title"]) {
|
||||
generalSummary += ` titled "${data.Metadata["Title"]}"`;
|
||||
}
|
||||
|
||||
if (data.Metadata && data.Metadata["Author"]) {
|
||||
generalSummary += ` created by ${data.Metadata["Author"]}`;
|
||||
}
|
||||
|
||||
if (data.DocumentInfo && data.DocumentInfo["PDF version"]) {
|
||||
generalSummary += ` (PDF version ${data.DocumentInfo["PDF version"]})`;
|
||||
}
|
||||
|
||||
// Add security information to the general summary if relevant
|
||||
if (data.Encryption && data.Encryption.IsEncrypted) {
|
||||
generalSummary += '. The document is password protected';
|
||||
|
||||
if (data.Encryption.EncryptionAlgorithm) {
|
||||
generalSummary += ` using ${data.Encryption.EncryptionAlgorithm}`;
|
||||
|
||||
if (data.Encryption.KeyLength) {
|
||||
generalSummary += ` (${data.Encryption.KeyLength} bit)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (restrictedPermissions.length > 0) {
|
||||
generalSummary += `. It has ${restrictedPermissions.length} restricted permissions`;
|
||||
}
|
||||
|
||||
// Add compliance standards if available
|
||||
if (hasCompliance && compliantStandards.length > 0) {
|
||||
generalSummary += `. This document complies with the ${compliantStandards.join(', ')} PDF standard${compliantStandards.length > 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
generalSummary += '.';
|
||||
|
||||
// Remove SummaryData from JSON to avoid duplication
|
||||
if (data.SummaryData) {
|
||||
delete data.SummaryData;
|
||||
}
|
||||
|
||||
summaryTextElement.innerHTML = generalSummary;
|
||||
|
||||
// Display the summary section
|
||||
document.getElementById('pdf-summary').style.display = 'block';
|
||||
}
|
||||
|
||||
function generateSummaryFromData(summaryData) {
|
||||
let summary = [];
|
||||
|
||||
// Handle encryption information
|
||||
if (summaryData.encrypted) {
|
||||
summary.push(getPdfInfoSummaryEncrypted);
|
||||
}
|
||||
|
||||
// Handle permissions information
|
||||
if (summaryData.restrictedPermissions && summaryData.restrictedPermissions.length > 0) {
|
||||
const formattedPermissionsText = getPdfInfoSummaryPermissions.replace('{0}', summaryData.restrictedPermissionsCount);
|
||||
summary.push(formattedPermissionsText);
|
||||
}
|
||||
|
||||
// Handle standard compliance information
|
||||
if (summaryData.standardCompliance) {
|
||||
const formattedComplianceText = getPdfInfoSummaryCompliance
|
||||
.replace('{0}', summaryData.standardCompliance);
|
||||
summary.push(formattedComplianceText);
|
||||
}
|
||||
|
||||
return summary.join(' ');
|
||||
}
|
||||
});
|
||||
|
||||
function displayJsonData(jsonData) {
|
||||
@ -77,8 +511,9 @@
|
||||
header.className = 'card-header';
|
||||
header.id = `${safeKey}-heading-${depth}`;
|
||||
const h5Elem = document.createElement('h5');
|
||||
h5Elem.className = 'mb-0';
|
||||
h5Elem.className = 'mb-0 d-flex align-items-center';
|
||||
|
||||
// Create the main content (button or text)
|
||||
if (key === 'XMPMetadata' && typeof value === "string") {
|
||||
const buttonElem = createButtonElement(key, safeKey, depth);
|
||||
h5Elem.appendChild(buttonElem);
|
||||
@ -94,6 +529,8 @@
|
||||
} else {
|
||||
h5Elem.textContent = `${key}: ${String(value)}`;
|
||||
}
|
||||
|
||||
// Info buttons removed as requested
|
||||
|
||||
header.appendChild(h5Elem);
|
||||
card.appendChild(header);
|
||||
|
Loading…
x
Reference in New Issue
Block a user