mirror of
				https://github.com/Frooodle/Stirling-PDF.git
				synced 2025-11-01 01:21:18 +01:00 
			
		
		
		
	Merge branch 'main' into testStuff
This commit is contained in:
		
						commit
						30ee33002d
					
				
							
								
								
									
										2
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							@ -1,5 +1,5 @@
 | 
			
		||||
blank_issues_enabled: true
 | 
			
		||||
contact_links:
 | 
			
		||||
  - name: 💬 Discord Server
 | 
			
		||||
    url: https://discord.gg/Cn8pWhQRxZ
 | 
			
		||||
    url: https://discord.gg/HYmhKj45pU
 | 
			
		||||
    about: You can join our Discord server for real time discussion and support
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
# Main stage
 | 
			
		||||
FROM alpine:3.20.3
 | 
			
		||||
FROM alpine:3.21.0
 | 
			
		||||
 | 
			
		||||
# Copy necessary files
 | 
			
		||||
COPY scripts /scripts
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ RUN DOCKER_ENABLE_SECURITY=true \
 | 
			
		||||
./gradlew clean build
 | 
			
		||||
 | 
			
		||||
# Main stage
 | 
			
		||||
FROM alpine:3.20.3
 | 
			
		||||
FROM alpine:3.21.0
 | 
			
		||||
 | 
			
		||||
# Copy necessary files
 | 
			
		||||
COPY scripts /scripts
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
# use alpine
 | 
			
		||||
FROM alpine:3.20.3
 | 
			
		||||
FROM alpine:3.21.0
 | 
			
		||||
 | 
			
		||||
ARG VERSION_TAG
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										48
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								README.md
									
									
									
									
									
								
							@ -2,7 +2,7 @@
 | 
			
		||||
<h1 align="center">Stirling-PDF</h1>
 | 
			
		||||
 | 
			
		||||
[](https://hub.docker.com/r/frooodle/s-pdf)
 | 
			
		||||
[](https://discord.gg/Cn8pWhQRxZ)
 | 
			
		||||
[](https://discord.gg/HYmhKj45pU)
 | 
			
		||||
[](https://github.com/Stirling-Tools/Stirling-PDF/)
 | 
			
		||||
[](https://github.com/Stirling-Tools/stirling-pdf)
 | 
			
		||||
 | 
			
		||||
@ -191,44 +191,44 @@ Stirling-PDF currently supports 38 languages!
 | 
			
		||||
 | 
			
		||||
| Language                                     | Progress                               |
 | 
			
		||||
| -------------------------------------------- | -------------------------------------- |
 | 
			
		||||
| Arabic (العربية) (ar_AR)                        |    |
 | 
			
		||||
| Arabic (العربية) (ar_AR)                        |    |
 | 
			
		||||
| Azerbaijani (Azərbaycan Dili) (az_AZ)        |    |
 | 
			
		||||
| Basque (Euskara) (eu_ES)                     |    |
 | 
			
		||||
| Basque (Euskara) (eu_ES)                     |    |
 | 
			
		||||
| Bulgarian (Български) (bg_BG)                |    |
 | 
			
		||||
| Catalan (Català) (ca_CA)                     |    |
 | 
			
		||||
| Croatian (Hrvatski) (hr_HR)                  |    |
 | 
			
		||||
| Catalan (Català) (ca_CA)                     |    |
 | 
			
		||||
| Croatian (Hrvatski) (hr_HR)                  |    |
 | 
			
		||||
| Czech (Česky) (cs_CZ)                        |    |
 | 
			
		||||
| Danish (Dansk) (da_DK)                       |    |
 | 
			
		||||
| Dutch (Nederlands) (nl_NL)                   |    |
 | 
			
		||||
| Dutch (Nederlands) (nl_NL)                   |    |
 | 
			
		||||
| English (English) (en_GB)                    |  |
 | 
			
		||||
| English (US) (en_US)                         |  |
 | 
			
		||||
| French (Français) (fr_FR)                    |    |
 | 
			
		||||
| German (Deutsch) (de_DE)                     |    |
 | 
			
		||||
| Greek (Ελληνικά) (el_GR)                     |    |
 | 
			
		||||
| Hindi (हिंदी) (hi_IN)                          |    |
 | 
			
		||||
| Hungarian (Magyar) (hu_HU)                   |    |
 | 
			
		||||
| French (Français) (fr_FR)                    |    |
 | 
			
		||||
| 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)                    |    |
 | 
			
		||||
| Persian (فارسی) (fa_IR)                      |    |
 | 
			
		||||
| Polish (Polski) (pl_PL)                      |    |
 | 
			
		||||
| Korean (한국어) (ko_KR)                      |    |
 | 
			
		||||
| Norwegian (Norsk) (no_NB)                    |    |
 | 
			
		||||
| Persian (فارسی) (fa_IR)                      |    |
 | 
			
		||||
| Polish (Polski) (pl_PL)                      |    |
 | 
			
		||||
| Portuguese (Português) (pt_PT)               |    |
 | 
			
		||||
| Portuguese Brazilian (Português) (pt_BR)     |    |
 | 
			
		||||
| Portuguese Brazilian (Português) (pt_BR)     |    |
 | 
			
		||||
| Romanian (Română) (ro_RO)                    |    |
 | 
			
		||||
| Russian (Русский) (ru_RU)                    |    |
 | 
			
		||||
| Russian (Русский) (ru_RU)                    |    |
 | 
			
		||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |    |
 | 
			
		||||
| Simplified Chinese (简体中文) (zh_CN)         |    |
 | 
			
		||||
| Slovakian (Slovensky) (sk_SK)                |    |
 | 
			
		||||
| Spanish (Español) (es_ES)                    |    |
 | 
			
		||||
| Swedish (Svenska) (sv_SE)                    |    |
 | 
			
		||||
| Thai (ไทย) (th_TH)                           |    |
 | 
			
		||||
| Traditional Chinese (繁體中文) (zh_TW)        |    |
 | 
			
		||||
| Turkish (Türkçe) (tr_TR)                     |    |
 | 
			
		||||
| Spanish (Español) (es_ES)                    |    |
 | 
			
		||||
| Swedish (Svenska) (sv_SE)                    |    |
 | 
			
		||||
| Thai (ไทย) (th_TH)                           |    |
 | 
			
		||||
| Traditional Chinese (繁體中文) (zh_TW)        |    |
 | 
			
		||||
| Turkish (Türkçe) (tr_TR)                     |    |
 | 
			
		||||
| Ukrainian (Українська) (uk_UA)               |    |
 | 
			
		||||
| Vietnamese (Tiếng Việt) (vi_VN)              |    |
 | 
			
		||||
| Vietnamese (Tiếng Việt) (vi_VN)              |    |
 | 
			
		||||
 | 
			
		||||
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -296,7 +296,7 @@ dependencies {
 | 
			
		||||
 | 
			
		||||
    if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
 | 
			
		||||
        implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
 | 
			
		||||
        implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE"
 | 
			
		||||
        implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE"
 | 
			
		||||
        implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
 | 
			
		||||
        implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -77,6 +77,11 @@ ignore = [
 | 
			
		||||
    'language.direction',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[fa_IR]
 | 
			
		||||
ignore = [
 | 
			
		||||
    'language.direction',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[fr_FR]
 | 
			
		||||
ignore = [
 | 
			
		||||
    'AddStampRequest.alphabet',
 | 
			
		||||
 | 
			
		||||
@ -39,10 +39,7 @@ public class PasswordController {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PostMapping(consumes = "multipart/form-data", value = "/remove-password")
 | 
			
		||||
    @Operation(
 | 
			
		||||
            summary = "Remove password from a PDF file",
 | 
			
		||||
            description =
 | 
			
		||||
                    "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO")
 | 
			
		||||
    @Operation(summary = "Remove password from a PDF file", description = "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO")
 | 
			
		||||
    public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request)
 | 
			
		||||
            throws IOException {
 | 
			
		||||
        MultipartFile fileInput = request.getFileInput();
 | 
			
		||||
@ -57,10 +54,7 @@ public class PasswordController {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PostMapping(consumes = "multipart/form-data", value = "/add-password")
 | 
			
		||||
    @Operation(
 | 
			
		||||
            summary = "Add password to a PDF file",
 | 
			
		||||
            description =
 | 
			
		||||
                    "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF")
 | 
			
		||||
    @Operation(summary = "Add password to a PDF file", description = "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF")
 | 
			
		||||
    public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request)
 | 
			
		||||
            throws IOException {
 | 
			
		||||
        MultipartFile fileInput = request.getFileInput();
 | 
			
		||||
 | 
			
		||||
@ -88,15 +88,45 @@ public class GeneralUtils {
 | 
			
		||||
 | 
			
		||||
    public static boolean isURLReachable(String urlStr) {
 | 
			
		||||
        try {
 | 
			
		||||
            // Parse the URL
 | 
			
		||||
            URL url = URI.create(urlStr).toURL();
 | 
			
		||||
 | 
			
		||||
            // Allow only http and https protocols
 | 
			
		||||
            String protocol = url.getProtocol();
 | 
			
		||||
            if (!protocol.equals("http") && !protocol.equals("https")) {
 | 
			
		||||
                return false; // Disallow other protocols
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Check if the host is a local address
 | 
			
		||||
            String host = url.getHost();
 | 
			
		||||
            if (isLocalAddress(host)) {
 | 
			
		||||
                return false; // Exclude local addresses
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Check if the URL is reachable
 | 
			
		||||
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 | 
			
		||||
            connection.setRequestMethod("HEAD");
 | 
			
		||||
            // connection.setConnectTimeout(5000); // Set connection timeout
 | 
			
		||||
            // connection.setReadTimeout(5000);    // Set read timeout
 | 
			
		||||
            int responseCode = connection.getResponseCode();
 | 
			
		||||
            return (200 <= responseCode && responseCode <= 399);
 | 
			
		||||
        } catch (MalformedURLException e) {
 | 
			
		||||
            return false;
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            return false;
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            return false; // Return false in case of any exception
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static boolean isLocalAddress(String host) {
 | 
			
		||||
        try {
 | 
			
		||||
            // Resolve DNS to IP address
 | 
			
		||||
            InetAddress address = InetAddress.getByName(host);
 | 
			
		||||
 | 
			
		||||
            // Check for local addresses
 | 
			
		||||
            return address.isAnyLocalAddress() ||  // Matches 0.0.0.0 or similar
 | 
			
		||||
                   address.isLoopbackAddress() || // Matches 127.0.0.1 or ::1
 | 
			
		||||
                   address.isSiteLocalAddress() || // Matches private IPv4 ranges: 192.168.x.x, 10.x.x.x, 172.16.x.x to 172.31.x.x
 | 
			
		||||
                   address.getHostAddress().startsWith("fe80:"); // Matches link-local IPv6 addresses
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            return false; // Return false for invalid or unresolved addresses
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=الصفحات المحددة
 | 
			
		||||
multiTool.undo=تراجع
 | 
			
		||||
multiTool.redo=إعادة إجراء
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=هذه الميزة متوفرة في <a href="{0}">صفحة الأدوات المتعددة</a> لدينا. اطلع عليها للحصول على واجهة مستخدم محسّنة لكل صفحة وميزات إضافية!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Seçilmiş Səhifə(lər)
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=Bu xüsusiyyət bizim <a href="{0}">multi-alət səhifə</a>mizdə də mövcuddur. Əlavə xüsusiyyətlər və səhifə-səhifə interfeys üçün sınaqdan keçirin!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Ausgewählte Seite(n)
 | 
			
		||||
multiTool.undo=Rückgängig machen
 | 
			
		||||
multiTool.redo=Wiederherstellen
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=Diese Funktion ist auch auf unserer <a href="{0}">PDF-Multitool-Seite</a> verfügbar. Probieren Sie sie aus, denn sie bietet eine verbesserte Benutzeroberfläche und zusätzliche Funktionen!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -122,6 +122,7 @@ enterpriseEdition.warning=این ویژگی فقط برای کاربران حر
 | 
			
		||||
enterpriseEdition.yamlAdvert=Stirling PDF Pro از فایلهای پیکربندی YAML و دیگر ویژگیهای SSO پشتیبانی میکند.
 | 
			
		||||
enterpriseEdition.ssoAdvert=به دنبال ویژگیهای بیشتر برای مدیریت کاربران هستید؟ Stirling PDF Pro را بررسی کنید
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#################
 | 
			
		||||
#  Analytics    #
 | 
			
		||||
#################
 | 
			
		||||
@ -515,6 +516,7 @@ home.validateSignature.title=اعتبارسنجی امضای PDF
 | 
			
		||||
home.validateSignature.desc=تأیید امضاها و گواهیهای دیجیتال در اسناد PDF
 | 
			
		||||
validateSignature.tags=امضا، تأیید، اعتبارسنجی، PDF، گواهینامه، امضای دیجیتال
 | 
			
		||||
 | 
			
		||||
#replace-invert-color
 | 
			
		||||
replace-color.title=جایگزینی/معکوس کردن رنگ
 | 
			
		||||
replace-color.header=جایگزینی/معکوس کردن رنگ PDF
 | 
			
		||||
home.replaceColorPdf.title=جایگزینی و معکوس کردن رنگ
 | 
			
		||||
@ -535,7 +537,6 @@ replace-color.submit=جایگزینی
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
###########################
 | 
			
		||||
#                         #
 | 
			
		||||
#       WEB PAGES         #
 | 
			
		||||
@ -611,6 +612,7 @@ MarkdownToPDF.help=در حال پیشرفت
 | 
			
		||||
MarkdownToPDF.credit=از WeasyPrint استفاده میکند
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#url-to-pdf
 | 
			
		||||
URLToPDF.title=URL به PDF
 | 
			
		||||
URLToPDF.header=URL به PDF
 | 
			
		||||
@ -911,7 +913,7 @@ addImage.upload=افزودن تصویر
 | 
			
		||||
addImage.submit=افزودن تصویر
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Merge
 | 
			
		||||
#merge
 | 
			
		||||
merge.title=ادغام
 | 
			
		||||
merge.header=ادغام چندین PDF (۲+)
 | 
			
		||||
merge.sortByName=مرتبسازی بر اساس نام
 | 
			
		||||
@ -920,7 +922,7 @@ merge.removeCertSign=حذف امضای دیجیتال در فایل ادغام
 | 
			
		||||
merge.submit=ادغام
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# PDF Organizer
 | 
			
		||||
#pdfOrganiser
 | 
			
		||||
pdfOrganiser.title=سازماندهی صفحات
 | 
			
		||||
pdfOrganiser.header=سازماندهی صفحات PDF
 | 
			
		||||
pdfOrganiser.submit=بازآرایی صفحات
 | 
			
		||||
@ -938,7 +940,7 @@ pdfOrganiser.mode.10=ادغام فرد-زوج
 | 
			
		||||
pdfOrganiser.placeholder=(مثال: ۱,۳,۲ یا ۴-۸,۲,۱۰-۱۲ یا 2n-1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Multi Tool
 | 
			
		||||
#multiTool
 | 
			
		||||
multiTool.title=ابزار چندگانه PDF
 | 
			
		||||
multiTool.header=ابزار چندگانه PDF
 | 
			
		||||
multiTool.uploadPrompts=نام فایل
 | 
			
		||||
@ -963,14 +965,24 @@ multiTool.dragDropMessage=صفحه(ها) انتخاب شدهاند
 | 
			
		||||
multiTool.undo=واگرد
 | 
			
		||||
multiTool.redo=بازگرداندن
 | 
			
		||||
 | 
			
		||||
# Multi Tool Advert
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=این ویژگی همچنین در <a href="{0}">صفحه ابزار چندگانه ما</a> موجود است. برای رابط کاربری صفحه به صفحه پیشرفته و ویژگیهای اضافی بررسی کنید!
 | 
			
		||||
 | 
			
		||||
# View PDF
 | 
			
		||||
#view pdf
 | 
			
		||||
viewPdf.title=مشاهده PDF
 | 
			
		||||
viewPdf.header=مشاهده PDF
 | 
			
		||||
 | 
			
		||||
# Page Remover
 | 
			
		||||
#pageRemover
 | 
			
		||||
pageRemover.title=حذف صفحات
 | 
			
		||||
pageRemover.header=حذف صفحات PDF
 | 
			
		||||
pageRemover.pagesToDelete=صفحات برای حذف (یک لیست از اعداد صفحه جدا شده با کاما وارد کنید):
 | 
			
		||||
@ -978,14 +990,14 @@ pageRemover.submit=حذف صفحات
 | 
			
		||||
pageRemover.placeholder=(مثال: ۱,۲,۶ یا ۱-۱۰,۱۵-۳۰)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Rotate
 | 
			
		||||
#rotate
 | 
			
		||||
rotate.title=چرخش PDF
 | 
			
		||||
rotate.header=چرخش PDF
 | 
			
		||||
rotate.selectAngle=زاویه چرخش را انتخاب کنید (به مضربهای ۹۰ درجه):
 | 
			
		||||
rotate.submit=چرخش
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Split PDFs
 | 
			
		||||
#split-pdfs
 | 
			
		||||
split.title=تقسیم PDF
 | 
			
		||||
split.header=تقسیم PDF
 | 
			
		||||
split.desc.1=اعدادی که انتخاب میکنید شماره صفحههایی هستند که میخواهید بر روی آنها تقسیم انجام دهید
 | 
			
		||||
@ -1014,7 +1026,7 @@ imageToPDF.selectText.4=ادغام در یک PDF واحد
 | 
			
		||||
imageToPDF.selectText.5=تبدیل به PDF های جداگانه
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# PDF to Image
 | 
			
		||||
#pdfToImage
 | 
			
		||||
pdfToImage.title=PDF به تصویر
 | 
			
		||||
pdfToImage.header=PDF به تصویر
 | 
			
		||||
pdfToImage.selectText=فرمت تصویر
 | 
			
		||||
@ -1029,7 +1041,7 @@ pdfToImage.submit=تبدیل
 | 
			
		||||
pdfToImage.info=پایتون نصب نشده است. برای تبدیل WebP لازم است.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Add Password
 | 
			
		||||
#addPassword
 | 
			
		||||
addPassword.title=افزودن گذرواژه
 | 
			
		||||
addPassword.header=افزودن گذرواژه (رمزنگاری)
 | 
			
		||||
addPassword.selectText.1=انتخاب PDF برای رمزنگاری
 | 
			
		||||
@ -1051,7 +1063,7 @@ addPassword.selectText.16=محدودیتهای باز شدن خود سند
 | 
			
		||||
addPassword.submit=رمزنگاری
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Watermark
 | 
			
		||||
#watermark
 | 
			
		||||
watermark.title=افزودن واترمارک
 | 
			
		||||
watermark.header=افزودن واترمارک
 | 
			
		||||
watermark.customColor=رنگ متن سفارشی
 | 
			
		||||
@ -1070,7 +1082,7 @@ watermark.type.1=متن
 | 
			
		||||
watermark.type.2=تصویر
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Change Permissions
 | 
			
		||||
#Change permissions
 | 
			
		||||
permissions.title=تغییر مجوزها
 | 
			
		||||
permissions.header=تغییر مجوزها
 | 
			
		||||
permissions.warning=برای اینکه این مجوزها غیرقابل تغییر باشند، توصیه میشود آنها را با گذرواژه از طریق صفحه افزودن گذرواژه تنظیم کنید
 | 
			
		||||
@ -1087,7 +1099,7 @@ permissions.selectText.10=جلوگیری از چاپ فرمتهای مختل
 | 
			
		||||
permissions.submit=تغییر
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove Password
 | 
			
		||||
#remove password
 | 
			
		||||
removePassword.title=حذف گذرواژه
 | 
			
		||||
removePassword.header=حذف گذرواژه (رمزگشایی)
 | 
			
		||||
removePassword.selectText.1=PDFی را برای رمزگشایی انتخاب کنید
 | 
			
		||||
@ -1095,7 +1107,7 @@ removePassword.selectText.2=گذرواژه
 | 
			
		||||
removePassword.submit=حذف
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Change Metadata
 | 
			
		||||
#changeMetadata
 | 
			
		||||
changeMetadata.title=عنوان:
 | 
			
		||||
changeMetadata.header=تغییر متادادهها
 | 
			
		||||
changeMetadata.selectText.1=لطفاً متغیرهایی که مایل به تغییر آنها هستید را ویرایش کنید
 | 
			
		||||
@ -1114,7 +1126,7 @@ changeMetadata.selectText.5=افزودن ورودی متاداده سفارشی
 | 
			
		||||
changeMetadata.submit=تغییر
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# PDF to PDF/A
 | 
			
		||||
#pdfToPDFA
 | 
			
		||||
pdfToPDFA.title=PDF به PDF/A
 | 
			
		||||
pdfToPDFA.header=PDF به PDF/A
 | 
			
		||||
pdfToPDFA.credit=این سرویس از qpdf برای تبدیل PDF/A استفاده میکند
 | 
			
		||||
@ -1124,7 +1136,7 @@ pdfToPDFA.outputFormat=فرمت خروجی
 | 
			
		||||
pdfToPDFA.pdfWithDigitalSignature=PDF حاوی یک امضای دیجیتال است. این در مرحله بعد حذف خواهد شد.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# PDF to Word
 | 
			
		||||
#PDFToWord
 | 
			
		||||
PDFToWord.title=PDF به ورد
 | 
			
		||||
PDFToWord.header=PDF به ورد
 | 
			
		||||
PDFToWord.selectText.1=فرمت فایل خروجی
 | 
			
		||||
@ -1132,7 +1144,7 @@ PDFToWord.credit=این سرویس از LibreOffice برای تبدیل فایل
 | 
			
		||||
PDFToWord.submit=تبدیل
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# PDF to Presentation
 | 
			
		||||
#PDFToPresentation
 | 
			
		||||
PDFToPresentation.title=PDF به ارائه
 | 
			
		||||
PDFToPresentation.header=PDF به ارائه
 | 
			
		||||
PDFToPresentation.selectText.1=فرمت فایل خروجی
 | 
			
		||||
@ -1140,7 +1152,7 @@ PDFToPresentation.credit=این سرویس از LibreOffice برای تبدیل
 | 
			
		||||
PDFToPresentation.submit=تبدیل
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# PDF to Text
 | 
			
		||||
#PDFToText
 | 
			
		||||
PDFToText.title=PDF به RTF (متن)
 | 
			
		||||
PDFToText.header=PDF به RTF (متن)
 | 
			
		||||
PDFToText.selectText.1=فرمت فایل خروجی
 | 
			
		||||
@ -1148,27 +1160,26 @@ PDFToText.credit=این سرویس از LibreOffice برای تبدیل فایل
 | 
			
		||||
PDFToText.submit=تبدیل
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# PDF to HTML
 | 
			
		||||
#PDFToHTML
 | 
			
		||||
PDFToHTML.title=PDF به HTML
 | 
			
		||||
PDFToHTML.header=PDF به HTML
 | 
			
		||||
PDFToHTML.credit=این سرویس از pdftohtml برای تبدیل فایل استفاده میکند.
 | 
			
		||||
PDFToHTML.submit=تبدیل
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# PDF to XML
 | 
			
		||||
#PDFToXML
 | 
			
		||||
PDFToXML.title=PDF به XML
 | 
			
		||||
PDFToXML.header=PDF به XML
 | 
			
		||||
PDFToXML.credit=این سرویس از LibreOffice برای تبدیل فایل استفاده میکند.
 | 
			
		||||
PDFToXML.submit=تبدیل
 | 
			
		||||
 | 
			
		||||
# PDF to CSV
 | 
			
		||||
#PDFToCSV
 | 
			
		||||
PDFToCSV.title=PDF به CSV
 | 
			
		||||
PDFToCSV.header=PDF به CSV
 | 
			
		||||
PDFToCSV.prompt=صفحهای که میخواهید جدول استخراج شود را انتخاب کنید
 | 
			
		||||
PDFToCSV.submit=استخراج
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Split by Size or Count
 | 
			
		||||
#split-by-size-or-count
 | 
			
		||||
split-by-size-or-count.title=تقسیم PDF بر اساس اندازه یا تعداد
 | 
			
		||||
split-by-size-or-count.header=تقسیم PDF بر اساس اندازه یا تعداد
 | 
			
		||||
split-by-size-or-count.type.label=انتخاب نوع تقسیم
 | 
			
		||||
@ -1180,7 +1191,7 @@ split-by-size-or-count.value.placeholder=اندازه را وارد کنید (م
 | 
			
		||||
split-by-size-or-count.submit=ارسال
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Overlay PDFs
 | 
			
		||||
#overlay-pdfs
 | 
			
		||||
overlay-pdfs.header=ترکیب فایلهای PDF
 | 
			
		||||
overlay-pdfs.baseFile.label=انتخاب فایل پایه PDF
 | 
			
		||||
overlay-pdfs.overlayFiles.label=انتخاب فایلهای ترکیبی PDF
 | 
			
		||||
@ -1196,7 +1207,7 @@ overlay-pdfs.position.background=پسزمینه
 | 
			
		||||
overlay-pdfs.submit=ارسال
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Split by Sections
 | 
			
		||||
#split-by-sections
 | 
			
		||||
split-by-sections.title=تقسیم PDF به بخشها
 | 
			
		||||
split-by-sections.header=تقسیم PDF به بخشها
 | 
			
		||||
split-by-sections.horizontal.label=تقسیمات افقی
 | 
			
		||||
@ -1207,7 +1218,7 @@ split-by-sections.submit=تقسیم PDF
 | 
			
		||||
split-by-sections.merge=ادغام به یک PDF
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Print File
 | 
			
		||||
#printFile
 | 
			
		||||
printFile.title=چاپ فایل
 | 
			
		||||
printFile.header=چاپ فایل به چاپگر
 | 
			
		||||
printFile.selectText.1=انتخاب فایل برای چاپ
 | 
			
		||||
@ -1215,7 +1226,7 @@ printFile.selectText.2=نام چاپگر را وارد کنید
 | 
			
		||||
printFile.submit=چاپ
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Licenses
 | 
			
		||||
#licenses
 | 
			
		||||
licenses.nav=مجوزها
 | 
			
		||||
licenses.title=مجوزهای شخص ثالث
 | 
			
		||||
licenses.header=مجوزهای شخص ثالث
 | 
			
		||||
@ -1223,7 +1234,7 @@ licenses.module=ماژول
 | 
			
		||||
licenses.version=نسخه
 | 
			
		||||
licenses.license=مجوز
 | 
			
		||||
 | 
			
		||||
# Survey
 | 
			
		||||
#survey
 | 
			
		||||
survey.nav=نظرسنجی
 | 
			
		||||
survey.title=نظرسنجی Stirling-PDF
 | 
			
		||||
survey.description=Stirling-PDF هیچ ردیابی ندارد، بنابراین ما میخواهیم از کاربران خود بشنویم تا Stirling-PDF را بهبود دهیم!
 | 
			
		||||
@ -1235,7 +1246,7 @@ survey.button=شرکت در نظرسنجی
 | 
			
		||||
survey.dontShowAgain=دیگر نشان نده
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Error
 | 
			
		||||
#error
 | 
			
		||||
error.sorry=متأسفیم برای مشکل موجود!
 | 
			
		||||
error.needHelp=نیاز به کمک / یافتن مشکلی؟
 | 
			
		||||
error.contactTip=اگر هنوز مشکلی دارید، دریغ نکنید که با ما تماس بگیرید. میتوانید یک تیکت در صفحه GitHub ما ارسال کنید یا از طریق Discord با ما تماس بگیرید:
 | 
			
		||||
@ -1249,14 +1260,13 @@ error.githubSubmit=GitHub - ارسال تیکت
 | 
			
		||||
error.discordSubmit=Discord - ارسال پست پشتیبانی
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove Image
 | 
			
		||||
#remove-image
 | 
			
		||||
removeImage.title=حذف تصویر
 | 
			
		||||
removeImage.header=حذف تصویر
 | 
			
		||||
removeImage.removeImage=حذف تصویر
 | 
			
		||||
removeImage.submit=حذف تصویر
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Split by Chapters
 | 
			
		||||
splitByChapters.title=تقسیم PDF بر اساس فصلها
 | 
			
		||||
splitByChapters.header=تقسیم PDF بر اساس فصلها
 | 
			
		||||
splitByChapters.bookmarkLevel=سطح نشانک
 | 
			
		||||
@ -1274,7 +1284,7 @@ fileChooser.or=یا
 | 
			
		||||
fileChooser.dragAndDrop=بکشید و رها کنید
 | 
			
		||||
fileChooser.hoveredDragAndDrop=فایل(های) خود را اینجا بکشید و رها کنید
 | 
			
		||||
 | 
			
		||||
# Release Notes
 | 
			
		||||
#release notes
 | 
			
		||||
releases.footer=نسخهها
 | 
			
		||||
releases.title=یادداشتهای نسخه
 | 
			
		||||
releases.header=یادداشتهای نسخه
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) sélectionnées
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=Cette fonctionnalité est aussi disponible dans la <a href="{0}">page de l'outil multifonction</a>. Allez-y pour une interface page par page améliorée et des fonctionnalités additionnelles !
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Pagina(e) selezionata(e)
 | 
			
		||||
multiTool.undo=Annulla
 | 
			
		||||
multiTool.redo=Rifai
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=Questo file è protetto da password. Inserisci la password:
 | 
			
		||||
decrypt.cancelled=Operazione annullata per il PDF: {0}
 | 
			
		||||
decrypt.noPassword=Nessuna password fornita per il PDF crittografato: {0}
 | 
			
		||||
decrypt.invalidPassword=Riprova con la password corretta.
 | 
			
		||||
decrypt.invalidPasswordHeader=Password errata o crittografia non supportata per il PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=Si è verificato un errore durante l'elaborazione del file. Riprova..
 | 
			
		||||
decrypt.serverError=Errore del server durante la decrittazione: {0}
 | 
			
		||||
decrypt.success=File decrittografato con successo.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=Questa funzione è disponibile anche nella nostra <a href="{0}">pagina multi-strumento</a>. Scoprila per un'interfaccia utente pagina per pagina migliorata e funzionalità aggiuntive!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -56,12 +56,12 @@ userNotFoundMessage=ユーザーが見つかりません。
 | 
			
		||||
incorrectPasswordMessage=現在のパスワードが正しくありません。
 | 
			
		||||
usernameExistsMessage=新しいユーザー名はすでに存在します。
 | 
			
		||||
invalidUsernameMessage=ユーザー名が無効です。ユーザー名には文字、数字、およびそれに続く特殊文字 @._+- のみを含めることができます。または、有効な電子メール アドレスである必要があります。
 | 
			
		||||
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
 | 
			
		||||
invalidPasswordMessage=パスワードは空にすることはできません。また、先頭・末尾にスペースを含めることもできません。
 | 
			
		||||
confirmPasswordErrorMessage=新しいパスワードと新しいパスワードの確認は一致する必要があります。
 | 
			
		||||
deleteCurrentUserMessage=現在ログインしているユーザーは削除できません。
 | 
			
		||||
deleteUsernameExistsMessage=そのユーザー名は存在しないため削除できません。
 | 
			
		||||
downgradeCurrentUserMessage=現在のユーザーの役割をダウングレードできません
 | 
			
		||||
disabledCurrentUserMessage=The current user cannot be disabled
 | 
			
		||||
disabledCurrentUserMessage=現在のユーザーを無効にすることはできません
 | 
			
		||||
downgradeCurrentUserLongMessage=現在のユーザーの役割をダウングレードできません。したがって、現在のユーザーは表示されません。
 | 
			
		||||
userAlreadyExistsOAuthMessage=ユーザーは既にOAuth2ユーザーとして存在します。
 | 
			
		||||
userAlreadyExistsWebMessage=ユーザーは既にWebユーザーとして存在します。
 | 
			
		||||
@ -76,12 +76,12 @@ donate=寄付する
 | 
			
		||||
color=色
 | 
			
		||||
sponsor=スポンサー
 | 
			
		||||
info=Info
 | 
			
		||||
pro=Pro
 | 
			
		||||
page=Page
 | 
			
		||||
pages=Pages
 | 
			
		||||
loading=Loading...
 | 
			
		||||
addToDoc=Add to Document
 | 
			
		||||
reset=Reset
 | 
			
		||||
pro=pro
 | 
			
		||||
page=ページ
 | 
			
		||||
pages=ページ
 | 
			
		||||
loading=読込中...
 | 
			
		||||
addToDoc=ドキュメントに追加
 | 
			
		||||
reset=リセット
 | 
			
		||||
 | 
			
		||||
legal.privacy=プライバシーポリシー
 | 
			
		||||
legal.terms=利用規約
 | 
			
		||||
@ -92,7 +92,7 @@ legal.impressum=著作権利者情報
 | 
			
		||||
###############
 | 
			
		||||
#   Pipeline  #
 | 
			
		||||
###############
 | 
			
		||||
pipeline.header=パイプラインメニュー (Alpha)
 | 
			
		||||
pipeline.header=パイプラインメニュー (Beta)
 | 
			
		||||
pipeline.uploadButton=カスタムのアップロード
 | 
			
		||||
pipeline.configureButton=設定
 | 
			
		||||
pipeline.defaultOption=カスタム
 | 
			
		||||
@ -117,21 +117,21 @@ pipelineOptions.validateButton=検証
 | 
			
		||||
########################
 | 
			
		||||
#  ENTERPRISE EDITION  #
 | 
			
		||||
########################
 | 
			
		||||
enterpriseEdition.button=Upgrade to Pro
 | 
			
		||||
enterpriseEdition.warning=This feature is only available to Pro users.
 | 
			
		||||
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
 | 
			
		||||
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
 | 
			
		||||
enterpriseEdition.button=Proにアップグレード
 | 
			
		||||
enterpriseEdition.warning=この機能はProユーザーのみが利用できます。
 | 
			
		||||
enterpriseEdition.yamlAdvert=Stirling PDF Proは、YAML構成ファイルやその他のSSO機能をサポートしています。
 | 
			
		||||
enterpriseEdition.ssoAdvert=より多くのユーザー管理機能をお探しですか? Stirling PDF Proをご覧ください
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#################
 | 
			
		||||
#  Analytics    #
 | 
			
		||||
#################
 | 
			
		||||
analytics.title=Do you want make Stirling PDF better?
 | 
			
		||||
analytics.paragraph1=Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.
 | 
			
		||||
analytics.paragraph2=Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.
 | 
			
		||||
analytics.enable=Enable analytics
 | 
			
		||||
analytics.disable=Disable analytics
 | 
			
		||||
analytics.settings=You can change the settings for analytics in the config/settings.yml file
 | 
			
		||||
analytics.title=Stirling PDFをもっと良くしたいですか?
 | 
			
		||||
analytics.paragraph1=Stirling PDFでは、製品の改善に役立つ分析機能をオプトインしています。個人情報やファイルの内容を追跡することはありません。
 | 
			
		||||
analytics.paragraph2=Stirling-PDFの成長を支援しユーザーをより深く理解できるように分析を有効にすることを検討してください。
 | 
			
		||||
analytics.enable=分析を有効にする
 | 
			
		||||
analytics.disable=分析を無効にする
 | 
			
		||||
analytics.settings=config/settings.ymlファイルでアナリティクスの設定を変更できます。
 | 
			
		||||
 | 
			
		||||
#############
 | 
			
		||||
#  NAVBAR   #
 | 
			
		||||
@ -142,14 +142,14 @@ navbar.language=言語
 | 
			
		||||
navbar.settings=設定
 | 
			
		||||
navbar.allTools=ツール
 | 
			
		||||
navbar.multiTool=マルチツール
 | 
			
		||||
navbar.search=Search
 | 
			
		||||
navbar.search=検索
 | 
			
		||||
navbar.sections.organize=整理
 | 
			
		||||
navbar.sections.convertTo=PDFへ変換
 | 
			
		||||
navbar.sections.convertFrom=PDFから変換
 | 
			
		||||
navbar.sections.security=署名とセキュリティ
 | 
			
		||||
navbar.sections.advance=アドバンスド
 | 
			
		||||
navbar.sections.edit=閲覧と編集
 | 
			
		||||
navbar.sections.popular=Popular
 | 
			
		||||
navbar.sections.popular=人気
 | 
			
		||||
 | 
			
		||||
#############
 | 
			
		||||
#  SETTINGS #
 | 
			
		||||
@ -208,7 +208,7 @@ adminUserSettings.user=ユーザー
 | 
			
		||||
adminUserSettings.addUser=新しいユーザを追加
 | 
			
		||||
adminUserSettings.deleteUser=ユーザの削除
 | 
			
		||||
adminUserSettings.confirmDeleteUser=ユーザを本当に削除しますか?
 | 
			
		||||
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
 | 
			
		||||
adminUserSettings.confirmChangeUserStatus=ユーザーを無効/有効にする必要がありますか?
 | 
			
		||||
adminUserSettings.usernameInfo=ユーザー名には、文字、数字、および次の特殊文字 @._+- のみを含めることができます。または、有効な電子メール アドレスである必要があります。
 | 
			
		||||
adminUserSettings.roles=役割
 | 
			
		||||
adminUserSettings.role=役割
 | 
			
		||||
@ -247,8 +247,8 @@ database.fileNotFound=ファイルが見つかりません
 | 
			
		||||
database.fileNullOrEmpty=ファイルは null または空であってはなりません
 | 
			
		||||
database.failedImportFile=ファイルのインポートに失敗
 | 
			
		||||
 | 
			
		||||
session.expired=Your session has expired. Please refresh the page and try again.
 | 
			
		||||
session.refreshPage=Refresh Page
 | 
			
		||||
session.expired=セッションが期限切れです。ページを更新してもう一度お試しください。
 | 
			
		||||
session.refreshPage=ページを更新
 | 
			
		||||
 | 
			
		||||
#############
 | 
			
		||||
# HOME-PAGE #
 | 
			
		||||
@ -488,52 +488,52 @@ overlay-pdfs.tags=Overlay
 | 
			
		||||
 | 
			
		||||
home.split-by-sections.title=PDFをセクションで分割
 | 
			
		||||
home.split-by-sections.desc=PDFの各ページを縦横に分割します。
 | 
			
		||||
split-by-sections.tags=Section Split, Divide, Customize
 | 
			
		||||
split-by-sections.tags=Section Split, Divide, Customize,Customise
 | 
			
		||||
 | 
			
		||||
home.AddStampRequest.title=PDFにスタンプを追加
 | 
			
		||||
home.AddStampRequest.desc=設定した位置にテキストや画像のスタンプを追加できます
 | 
			
		||||
AddStampRequest.tags=Stamp, Add image, center image, Watermark, PDF, Embed, Customize
 | 
			
		||||
AddStampRequest.tags=Stamp, Add image, center image, Watermark, PDF, Embed, Customize,Customise
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
home.PDFToBook.title=PDFを書籍に変換
 | 
			
		||||
home.PDFToBook.desc=calibreを使用してPDFを書籍/コミック形式に変換します
 | 
			
		||||
PDFToBook.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
 | 
			
		||||
PDFToBook.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf
 | 
			
		||||
 | 
			
		||||
home.BookToPDF.title=PDFを書籍に変換
 | 
			
		||||
home.BookToPDF.desc=calibreを使用してPDFを書籍/コミック形式に変換します
 | 
			
		||||
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
 | 
			
		||||
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf
 | 
			
		||||
 | 
			
		||||
home.removeImagePdf.title=画像の削除
 | 
			
		||||
home.removeImagePdf.desc=PDFから画像を削除してファイルサイズを小さくします
 | 
			
		||||
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
home.splitPdfByChapters.title=Split PDF by Chapters
 | 
			
		||||
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
 | 
			
		||||
home.splitPdfByChapters.title=PDFをチャプターごとに分割
 | 
			
		||||
home.splitPdfByChapters.desc=チャプターの構造に基づいてPDFを複数のファイルに分割します
 | 
			
		||||
splitPdfByChapters.tags=split,chapters,bookmarks,organize
 | 
			
		||||
 | 
			
		||||
home.validateSignature.title=Validate PDF Signature
 | 
			
		||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
 | 
			
		||||
home.validateSignature.title=PDF署名の検証
 | 
			
		||||
home.validateSignature.desc=PDF文書のデジタル署名と証明書を検証します
 | 
			
		||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
 | 
			
		||||
 | 
			
		||||
#replace-invert-color
 | 
			
		||||
replace-color.title=Replace-Invert-Color
 | 
			
		||||
replace-color.header=Replace-Invert Color PDF
 | 
			
		||||
home.replaceColorPdf.title=Replace and Invert Color
 | 
			
		||||
home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size
 | 
			
		||||
replace-color.title=色の置換・反転
 | 
			
		||||
replace-color.header=PDFの色の置換・反転
 | 
			
		||||
home.replaceColorPdf.title=色の置換と反転
 | 
			
		||||
home.replaceColorPdf.desc=PDF内のテキストと背景の色を置き換え、PDFのフルカラーを反転してファイルサイズを縮小します。
 | 
			
		||||
replaceColorPdf.tags=Replace Color,Page operations,Back end,server side
 | 
			
		||||
replace-color.selectText.1=Replace or Invert color Options
 | 
			
		||||
replace-color.selectText.2=Default(Default high contrast colors)
 | 
			
		||||
replace-color.selectText.3=Custom(Customized colors)
 | 
			
		||||
replace-color.selectText.4=Full-Invert(Invert all colors)
 | 
			
		||||
replace-color.selectText.5=High contrast color options
 | 
			
		||||
replace-color.selectText.6=white text on black background
 | 
			
		||||
replace-color.selectText.7=Black text on white background
 | 
			
		||||
replace-color.selectText.8=Yellow text on black background
 | 
			
		||||
replace-color.selectText.9=Green text on black background
 | 
			
		||||
replace-color.selectText.10=Choose text Color
 | 
			
		||||
replace-color.selectText.11=Choose background Color
 | 
			
		||||
replace-color.submit=Replace
 | 
			
		||||
replace-color.selectText.1=色の置換または反転オプション
 | 
			
		||||
replace-color.selectText.2=デフォルト(デフォルトの高コントラスト色)
 | 
			
		||||
replace-color.selectText.3=カスタム(カスタマイズされた色)
 | 
			
		||||
replace-color.selectText.4=フル反転(すべての色を反転)
 | 
			
		||||
replace-color.selectText.5=高コントラストカラーオプション
 | 
			
		||||
replace-color.selectText.6=黒背景に白文字
 | 
			
		||||
replace-color.selectText.7=白背景に黒文字
 | 
			
		||||
replace-color.selectText.8=黒背景に黄色文字
 | 
			
		||||
replace-color.selectText.9=黒背景に緑文字
 | 
			
		||||
replace-color.selectText.10=テキストの色を選択
 | 
			
		||||
replace-color.selectText.11=背景色を選択
 | 
			
		||||
replace-color.submit=置換
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -560,9 +560,9 @@ login.oauth2AccessDenied=アクセス拒否
 | 
			
		||||
login.oauth2InvalidTokenResponse=無効なトークン応答
 | 
			
		||||
login.oauth2InvalidIdToken=無効なIDトークン
 | 
			
		||||
login.userIsDisabled=ユーザーは非アクティブ化されており、現在このユーザー名でのログインはブロックされています。管理者に連絡してください。
 | 
			
		||||
login.alreadyLoggedIn=You are already logged in to
 | 
			
		||||
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
 | 
			
		||||
login.toManySessions=You have too many active sessions
 | 
			
		||||
login.alreadyLoggedIn=すでにログインしています
 | 
			
		||||
login.alreadyLoggedIn2=デバイスからログアウトしてもう一度お試しください。
 | 
			
		||||
login.toManySessions=アクティブなセッションが多すぎます
 | 
			
		||||
 | 
			
		||||
#auto-redact
 | 
			
		||||
autoRedact.title=自動塗りつぶし
 | 
			
		||||
@ -578,8 +578,8 @@ autoRedact.submitButton=送信
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#showJS
 | 
			
		||||
showJS.title=JavaScriptを表示
 | 
			
		||||
showJS.header=JavaScriptを表示
 | 
			
		||||
showJS.title=Javascriptを表示
 | 
			
		||||
showJS.header=Javascriptを表示
 | 
			
		||||
showJS.downloadJS=Javascriptをダウンロード
 | 
			
		||||
showJS.submit=表示
 | 
			
		||||
 | 
			
		||||
@ -757,7 +757,7 @@ certSign.showSig=署名を表示
 | 
			
		||||
certSign.reason=理由
 | 
			
		||||
certSign.location=場所
 | 
			
		||||
certSign.name=名前
 | 
			
		||||
certSign.showLogo=Show Logo
 | 
			
		||||
certSign.showLogo=ロゴを表示
 | 
			
		||||
certSign.submit=PDFに署名
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -792,9 +792,9 @@ compare.highlightColor.2=ハイライトカラー 2:
 | 
			
		||||
compare.document.1=ドキュメント 1
 | 
			
		||||
compare.document.2=ドキュメント 2
 | 
			
		||||
compare.submit=比較
 | 
			
		||||
compare.complex.message=One or both of the provided documents are large files, accuracy of comparison may be reduced
 | 
			
		||||
compare.large.file.message=One or Both of the provided documents are too large to process
 | 
			
		||||
compare.no.text.message=One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison.
 | 
			
		||||
compare.complex.message=提供された文書の一方または両方が大きなファイルであるため、比較の精度が低下する可能性があります。
 | 
			
		||||
compare.large.file.message=提供された文書の1つまたは両方が大きすぎて処理できません
 | 
			
		||||
compare.no.text.message=選択したPDFの1つまたは両方にテキストコンテンツがありません。比較するには、テキストを含むPDFを選択してください。
 | 
			
		||||
 | 
			
		||||
#BookToPDF
 | 
			
		||||
BookToPDF.title=書籍やコミックをPDFに変換
 | 
			
		||||
@ -803,8 +803,8 @@ BookToPDF.credit=calibreを使用
 | 
			
		||||
BookToPDF.submit=変換
 | 
			
		||||
 | 
			
		||||
#PDFToBook
 | 
			
		||||
PDFToBook.title=書籍をPDFに変換
 | 
			
		||||
PDFToBook.header=書籍をPDFに変換
 | 
			
		||||
PDFToBook.title=PDFを書籍に変換
 | 
			
		||||
PDFToBook.header=PDFを書籍に変換
 | 
			
		||||
PDFToBook.selectText.1=フォーマット
 | 
			
		||||
PDFToBook.credit=calibreを使用
 | 
			
		||||
PDFToBook.submit=変換
 | 
			
		||||
@ -817,17 +817,17 @@ sign.draw=署名を書く
 | 
			
		||||
sign.text=テキスト入力
 | 
			
		||||
sign.clear=クリア
 | 
			
		||||
sign.add=追加
 | 
			
		||||
sign.saved=Saved Signatures
 | 
			
		||||
sign.save=Save Signature
 | 
			
		||||
sign.personalSigs=Personal Signatures
 | 
			
		||||
sign.sharedSigs=Shared Signatures
 | 
			
		||||
sign.noSavedSigs=No saved signatures found
 | 
			
		||||
sign.addToAll=Add to all pages
 | 
			
		||||
sign.delete=Delete
 | 
			
		||||
sign.first=First page
 | 
			
		||||
sign.last=Last page
 | 
			
		||||
sign.next=Next page
 | 
			
		||||
sign.previous=Previous page
 | 
			
		||||
sign.saved=保存された署名
 | 
			
		||||
sign.save=署名を保存
 | 
			
		||||
sign.personalSigs=個人署名
 | 
			
		||||
sign.sharedSigs=共有署名
 | 
			
		||||
sign.noSavedSigs=保存された署名が見つかりません
 | 
			
		||||
sign.addToAll=すべてのページに追加
 | 
			
		||||
sign.delete=削除
 | 
			
		||||
sign.first=最初のページ
 | 
			
		||||
sign.last=最後のページ
 | 
			
		||||
sign.next=次のページ
 | 
			
		||||
sign.previous=前のページ
 | 
			
		||||
 | 
			
		||||
#repair
 | 
			
		||||
repair.title=修復
 | 
			
		||||
@ -944,29 +944,39 @@ pdfOrganiser.placeholder=(例:1,3,2または4-8,2,10-12または2n-1)
 | 
			
		||||
multiTool.title=PDFマルチツール
 | 
			
		||||
multiTool.header=PDFマルチツール
 | 
			
		||||
multiTool.uploadPrompts=ファイル名
 | 
			
		||||
multiTool.selectAll=Select All
 | 
			
		||||
multiTool.deselectAll=Deselect All
 | 
			
		||||
multiTool.selectPages=Page Select
 | 
			
		||||
multiTool.selectedPages=Selected Pages
 | 
			
		||||
multiTool.page=Page
 | 
			
		||||
multiTool.deleteSelected=Delete Selected
 | 
			
		||||
multiTool.downloadAll=Export
 | 
			
		||||
multiTool.downloadSelected=Export Selected
 | 
			
		||||
multiTool.selectAll=すべて選択
 | 
			
		||||
multiTool.deselectAll=選択を解除
 | 
			
		||||
multiTool.selectPages=ページ選択
 | 
			
		||||
multiTool.selectedPages=選択したページ
 | 
			
		||||
multiTool.page=ページ
 | 
			
		||||
multiTool.deleteSelected=選択項目を削除
 | 
			
		||||
multiTool.downloadAll=エクスポート
 | 
			
		||||
multiTool.downloadSelected=選択項目をエクスポート
 | 
			
		||||
 | 
			
		||||
multiTool.insertPageBreak=Insert Page Break
 | 
			
		||||
multiTool.addFile=Add File
 | 
			
		||||
multiTool.rotateLeft=Rotate Left
 | 
			
		||||
multiTool.rotateRight=Rotate Right
 | 
			
		||||
multiTool.split=Split
 | 
			
		||||
multiTool.moveLeft=Move Left
 | 
			
		||||
multiTool.moveRight=Move Right
 | 
			
		||||
multiTool.delete=Delete
 | 
			
		||||
multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
multiTool.insertPageBreak=改ページを挿入
 | 
			
		||||
multiTool.addFile=ファイルを追加
 | 
			
		||||
multiTool.rotateLeft=左回転
 | 
			
		||||
multiTool.rotateRight=右回転
 | 
			
		||||
multiTool.split=分割
 | 
			
		||||
multiTool.moveLeft=左に移動
 | 
			
		||||
multiTool.moveRight=右に移動
 | 
			
		||||
multiTool.delete=削除
 | 
			
		||||
multiTool.dragDropMessage=選択されたページ
 | 
			
		||||
multiTool.undo=元に戻す
 | 
			
		||||
multiTool.redo=やり直す
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=このファイルはパスワードで保護されています。パスワードを入力してください:
 | 
			
		||||
decrypt.cancelled=PDFの操作がキャンセルされました: {0}
 | 
			
		||||
decrypt.noPassword=暗号化されたPDFにパスワードが指定されていません: {0}
 | 
			
		||||
decrypt.invalidPassword=正しいパスワードでもう一度お試しください。
 | 
			
		||||
decrypt.invalidPasswordHeader=PDFのパスワードが正しくないか、暗号化がサポートされていません: {0}
 | 
			
		||||
decrypt.unexpectedError=ファイルの処理中にエラーが発生しました。もう一度お試しください。
 | 
			
		||||
decrypt.serverError=復号化中にサーバーエラーが発生しました: {0}
 | 
			
		||||
decrypt.success=ファイルの暗号化が正常に完了しました。
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
multiTool-advert.message=この機能は、<a href="{0}">マルチツール</a>でもご利用いただけます。強化されたページごとのUIと追加機能についてはこちらをご覧ください。
 | 
			
		||||
 | 
			
		||||
#view pdf
 | 
			
		||||
viewPdf.title=PDFを表示
 | 
			
		||||
@ -1122,7 +1132,7 @@ pdfToPDFA.header=PDFをPDF/Aに変換
 | 
			
		||||
pdfToPDFA.credit=本サービスはPDF/Aの変換にqpdfを使用しています。
 | 
			
		||||
pdfToPDFA.submit=変換
 | 
			
		||||
pdfToPDFA.tip=現在、一度に複数の入力に対して機能しません
 | 
			
		||||
pdfToPDFA.outputFormat=Output format
 | 
			
		||||
pdfToPDFA.outputFormat=出力形式
 | 
			
		||||
pdfToPDFA.pdfWithDigitalSignature=PDFにはデジタル署名が含まれています。これは次の手順で削除されます。
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1228,8 +1238,8 @@ licenses.license=ライセンス
 | 
			
		||||
survey.nav=アンケート
 | 
			
		||||
survey.title=Stirling-PDFのアンケート
 | 
			
		||||
survey.description=Stirling-PDFには追跡機能がないため、Stirling-PDFをより良くするために皆様の意見を聞かせてください!
 | 
			
		||||
survey.changes=Stirling-PDF has changed since the last survey! To find out more please check our blog post here:
 | 
			
		||||
survey.changes2=With these changes we are getting paid business support and funding
 | 
			
		||||
survey.changes=Stirling-PDFは前回の調査から変更されました。詳細についてはこちらのブログ投稿をご覧ください。
 | 
			
		||||
survey.changes2=これらの変更により私たちは有償のビジネスサポートと資金援助を受けています
 | 
			
		||||
survey.please=アンケートにご協力ください!
 | 
			
		||||
survey.disabled=(アンケートのポップアップは、次の更新では無効になりますが、ページの下部に表示されます。)
 | 
			
		||||
survey.button=アンケートに答える
 | 
			
		||||
@ -1257,61 +1267,61 @@ removeImage.removeImage=画像の削除
 | 
			
		||||
removeImage.submit=画像を削除
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
splitByChapters.title=Split PDF by Chapters
 | 
			
		||||
splitByChapters.header=Split PDF by Chapters
 | 
			
		||||
splitByChapters.bookmarkLevel=Bookmark Level
 | 
			
		||||
splitByChapters.includeMetadata=Include Metadata
 | 
			
		||||
splitByChapters.allowDuplicates=Allow Duplicates
 | 
			
		||||
splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure.
 | 
			
		||||
splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.).
 | 
			
		||||
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
 | 
			
		||||
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
 | 
			
		||||
splitByChapters.submit=Split PDF
 | 
			
		||||
splitByChapters.title=PDFをチャプターごとに分割
 | 
			
		||||
splitByChapters.header=PDFをチャプターごとに分割
 | 
			
		||||
splitByChapters.bookmarkLevel=ブックマークレベル
 | 
			
		||||
splitByChapters.includeMetadata=メタデータを含める
 | 
			
		||||
splitByChapters.allowDuplicates=重複を許可する
 | 
			
		||||
splitByChapters.desc.1=このツールは、チャプター構造に基づいてPDFファイルを複数のPDFに分割します。
 | 
			
		||||
splitByChapters.desc.2=ブックマークレベル:分割に使用するブックマークのレベルを選択します(最上位レベルの場合は0、第2レベルの場合は1など)。
 | 
			
		||||
splitByChapters.desc.3=メタデータを含める:チェックすると、元のPDFのメタデータが各分割PDFに含まれます。
 | 
			
		||||
splitByChapters.desc.4=重複を許可:チェックすると同じページ上の複数のブックマークから個別のPDFを作成できます。
 | 
			
		||||
splitByChapters.submit=PDFを分割
 | 
			
		||||
 | 
			
		||||
#File Chooser
 | 
			
		||||
fileChooser.click=Click
 | 
			
		||||
fileChooser.or=or
 | 
			
		||||
fileChooser.dragAndDrop=Drag & Drop
 | 
			
		||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
 | 
			
		||||
fileChooser.click=クリック
 | 
			
		||||
fileChooser.or=または
 | 
			
		||||
fileChooser.dragAndDrop=ドラッグ&ドロップ
 | 
			
		||||
fileChooser.hoveredDragAndDrop=ファイルをここにドラッグ&ドロップ
 | 
			
		||||
 | 
			
		||||
#release notes
 | 
			
		||||
releases.footer=Releases
 | 
			
		||||
releases.title=Release Notes
 | 
			
		||||
releases.header=Release Notes
 | 
			
		||||
releases.current.version=Current Release
 | 
			
		||||
releases.note=Release notes are only available in English
 | 
			
		||||
releases.footer=リリース
 | 
			
		||||
releases.title=リリースノート
 | 
			
		||||
releases.header=リリースノート
 | 
			
		||||
releases.current.version=現在のリリース
 | 
			
		||||
releases.note=リリースノートは英語でのみで提供されています
 | 
			
		||||
 | 
			
		||||
#Validate Signature
 | 
			
		||||
validateSignature.title=Validate PDF Signatures
 | 
			
		||||
validateSignature.header=Validate Digital Signatures
 | 
			
		||||
validateSignature.selectPDF=Select signed PDF file
 | 
			
		||||
validateSignature.submit=Validate Signatures
 | 
			
		||||
validateSignature.results=Validation Results
 | 
			
		||||
validateSignature.status=Status
 | 
			
		||||
validateSignature.signer=Signer
 | 
			
		||||
validateSignature.date=Date
 | 
			
		||||
validateSignature.reason=Reason
 | 
			
		||||
validateSignature.location=Location
 | 
			
		||||
validateSignature.noSignatures=No digital signatures found in this document
 | 
			
		||||
validateSignature.status.valid=Valid
 | 
			
		||||
validateSignature.status.invalid=Invalid
 | 
			
		||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
 | 
			
		||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
 | 
			
		||||
validateSignature.cert.expired=Certificate has expired
 | 
			
		||||
validateSignature.cert.revoked=Certificate has been revoked
 | 
			
		||||
validateSignature.signature.info=Signature Information
 | 
			
		||||
validateSignature.signature=Signature
 | 
			
		||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
 | 
			
		||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
 | 
			
		||||
validateSignature.cert.info=Certificate Details
 | 
			
		||||
validateSignature.cert.issuer=Issuer
 | 
			
		||||
validateSignature.cert.subject=Subject
 | 
			
		||||
validateSignature.cert.serialNumber=Serial Number
 | 
			
		||||
validateSignature.cert.validFrom=Valid From
 | 
			
		||||
validateSignature.cert.validUntil=Valid Until
 | 
			
		||||
validateSignature.cert.algorithm=Algorithm
 | 
			
		||||
validateSignature.cert.keySize=Key Size
 | 
			
		||||
validateSignature.cert.version=Version
 | 
			
		||||
validateSignature.cert.keyUsage=Key Usage
 | 
			
		||||
validateSignature.cert.selfSigned=Self-Signed
 | 
			
		||||
validateSignature.cert.bits=bits
 | 
			
		||||
validateSignature.title=PDF署名の検証
 | 
			
		||||
validateSignature.header=デジタル署名の検証
 | 
			
		||||
validateSignature.selectPDF=署名済みPDFファイルを選択
 | 
			
		||||
validateSignature.submit=署名の検証
 | 
			
		||||
validateSignature.results=検証結果
 | 
			
		||||
validateSignature.status=状態
 | 
			
		||||
validateSignature.signer=署名者
 | 
			
		||||
validateSignature.date=日付
 | 
			
		||||
validateSignature.reason=理由
 | 
			
		||||
validateSignature.location=場所
 | 
			
		||||
validateSignature.noSignatures=この文書にはデジタル署名が見つかりません
 | 
			
		||||
validateSignature.status.valid=有効
 | 
			
		||||
validateSignature.status.invalid=無効
 | 
			
		||||
validateSignature.chain.invalid=証明書チェーンの検証に失敗しました - 署名者の身元を確認できません
 | 
			
		||||
validateSignature.trust.invalid=証明書が信頼ストアにありません - ソースを検証できません
 | 
			
		||||
validateSignature.cert.expired=証明書の有効期限が切れています
 | 
			
		||||
validateSignature.cert.revoked=証明書は取り消されました
 | 
			
		||||
validateSignature.signature.info=署名情報
 | 
			
		||||
validateSignature.signature=署名
 | 
			
		||||
validateSignature.signature.mathValid=署名は数学的には有効ですが:
 | 
			
		||||
validateSignature.selectCustomCert=カスタム証明書ファイル X.509 (オプション)
 | 
			
		||||
validateSignature.cert.info=証明書の詳細
 | 
			
		||||
validateSignature.cert.issuer=発行者
 | 
			
		||||
validateSignature.cert.subject=主題
 | 
			
		||||
validateSignature.cert.serialNumber=シリアルナンバー
 | 
			
		||||
validateSignature.cert.validFrom=有効開始日
 | 
			
		||||
validateSignature.cert.validUntil=有効期限
 | 
			
		||||
validateSignature.cert.algorithm=アルゴリズム
 | 
			
		||||
validateSignature.cert.keySize=キーサイズ
 | 
			
		||||
validateSignature.cert.version=バージョン
 | 
			
		||||
validateSignature.cert.keyUsage=キーの使用法
 | 
			
		||||
validateSignature.cert.selfSigned=自己署名
 | 
			
		||||
validateSignature.cert.bits=ビット
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,8 +3,8 @@
 | 
			
		||||
###########
 | 
			
		||||
# the direction that the language is written (ltr = left to right, rtl = right to left)
 | 
			
		||||
language.direction=ltr
 | 
			
		||||
addPageNumbers.fontSize=Font Size
 | 
			
		||||
addPageNumbers.fontName=Font Name
 | 
			
		||||
addPageNumbers.fontSize=字体大小
 | 
			
		||||
addPageNumbers.fontName=字体名称
 | 
			
		||||
pdfPrompt=选择 PDF
 | 
			
		||||
multiPdfPrompt=选择多个 PDF(2个或更多)
 | 
			
		||||
multiPdfDropPrompt=选择(或拖拽)所需的 PDF
 | 
			
		||||
@ -79,14 +79,14 @@ info=信息
 | 
			
		||||
pro=Pro
 | 
			
		||||
page=Page
 | 
			
		||||
pages=Pages
 | 
			
		||||
loading=Loading...
 | 
			
		||||
loading=加载中...
 | 
			
		||||
addToDoc=Add to Document
 | 
			
		||||
reset=Reset
 | 
			
		||||
reset=重置
 | 
			
		||||
 | 
			
		||||
legal.privacy=Privacy Policy
 | 
			
		||||
legal.terms=Terms and Conditions
 | 
			
		||||
legal.accessibility=Accessibility
 | 
			
		||||
legal.cookie=Cookie Policy
 | 
			
		||||
legal.privacy=隐私政策
 | 
			
		||||
legal.terms=服务条款
 | 
			
		||||
legal.accessibility=无障碍
 | 
			
		||||
legal.cookie=Cookie 政策
 | 
			
		||||
legal.impressum=Impressum
 | 
			
		||||
 | 
			
		||||
###############
 | 
			
		||||
@ -117,8 +117,8 @@ pipelineOptions.validateButton=验证
 | 
			
		||||
########################
 | 
			
		||||
#  ENTERPRISE EDITION  #
 | 
			
		||||
########################
 | 
			
		||||
enterpriseEdition.button=Upgrade to Pro
 | 
			
		||||
enterpriseEdition.warning=This feature is only available to Pro users.
 | 
			
		||||
enterpriseEdition.button=升级到 Pro 版本
 | 
			
		||||
enterpriseEdition.warning=此功能仅适用于 Pro 版本
 | 
			
		||||
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
 | 
			
		||||
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
 | 
			
		||||
 | 
			
		||||
@ -247,8 +247,8 @@ database.fileNotFound=未找到文件
 | 
			
		||||
database.fileNullOrEmpty=文件不能为空
 | 
			
		||||
database.failedImportFile=导入文件失败
 | 
			
		||||
 | 
			
		||||
session.expired=Your session has expired. Please refresh the page and try again.
 | 
			
		||||
session.refreshPage=Refresh Page
 | 
			
		||||
session.expired=您的会话已过期。请刷新页面并重试。
 | 
			
		||||
session.refreshPage=刷新页面
 | 
			
		||||
 | 
			
		||||
#############
 | 
			
		||||
# HOME-PAGE #
 | 
			
		||||
@ -266,7 +266,7 @@ home.multiTool.desc=合并、旋转、重新排列和删除PDF页面
 | 
			
		||||
multiTool.tags=多工具,多操作,用户界面,点击拖动,前端,客户端
 | 
			
		||||
 | 
			
		||||
home.merge.title=合并
 | 
			
		||||
home.merge.desc=轻松合并多个PDF为一个。
 | 
			
		||||
home.merge.desc=轻松将多个 PDF 合并成一个。
 | 
			
		||||
merge.tags=合并,页面操作,后端,服务器端
 | 
			
		||||
 | 
			
		||||
home.split.title=拆分
 | 
			
		||||
@ -508,21 +508,21 @@ home.removeImagePdf.desc=删除图像减少PDF大小
 | 
			
		||||
removeImagePdf.tags=删除图像, 页面操作, 后端, 服务端
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
home.splitPdfByChapters.title=Split PDF by Chapters
 | 
			
		||||
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
 | 
			
		||||
splitPdfByChapters.tags=split,chapters,bookmarks,organize
 | 
			
		||||
home.splitPdfByChapters.title=按章节拆分 PDF
 | 
			
		||||
home.splitPdfByChapters.desc=根据其章节结构将 PDF 拆分为多个文件。
 | 
			
		||||
splitPdfByChapters.tags=分割,章节,书签,组织
 | 
			
		||||
 | 
			
		||||
home.validateSignature.title=Validate PDF Signature
 | 
			
		||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
 | 
			
		||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
 | 
			
		||||
home.validateSignature.title=验证 PDF 签名
 | 
			
		||||
home.validateSignature.desc=验证 PDF 文档中的数字签名和证书
 | 
			
		||||
validateSignature.tags=签名,验证,验证,PDF,证书,数字签名,验证签名,验证证书
 | 
			
		||||
 | 
			
		||||
#replace-invert-color
 | 
			
		||||
replace-color.title=Replace-Invert-Color
 | 
			
		||||
replace-color.header=Replace-Invert Color PDF
 | 
			
		||||
home.replaceColorPdf.title=Replace and Invert Color
 | 
			
		||||
home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size
 | 
			
		||||
replace-color.title=替换-反转-颜色
 | 
			
		||||
replace-color.header=替换-反转 PDF 颜色
 | 
			
		||||
home.replaceColorPdf.title=替换和反转颜色
 | 
			
		||||
home.replaceColorPdf.desc=替换 PDF 中文本和背景的颜色,并将PDF全色反转以减小文件大小
 | 
			
		||||
replaceColorPdf.tags=Replace Color,Page operations,Back end,server side
 | 
			
		||||
replace-color.selectText.1=Replace or Invert color Options
 | 
			
		||||
replace-color.selectText.1=替换或反转颜色选项
 | 
			
		||||
replace-color.selectText.2=Default(Default high contrast colors)
 | 
			
		||||
replace-color.selectText.3=Custom(Customized colors)
 | 
			
		||||
replace-color.selectText.4=Full-Invert(Invert all colors)
 | 
			
		||||
@ -560,9 +560,9 @@ login.oauth2AccessDenied=拒绝访问
 | 
			
		||||
login.oauth2InvalidTokenResponse=无效的 Token 响应
 | 
			
		||||
login.oauth2InvalidIdToken=无效的 Token
 | 
			
		||||
login.userIsDisabled=用户被禁用,登录已被阻止。请联系管理员。
 | 
			
		||||
login.alreadyLoggedIn=You are already logged in to
 | 
			
		||||
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
 | 
			
		||||
login.toManySessions=You have too many active sessions
 | 
			
		||||
login.alreadyLoggedIn=您已经登录到了
 | 
			
		||||
login.alreadyLoggedIn2=设备,请注销设备后重试。
 | 
			
		||||
login.toManySessions=你已经有太多的会话了。请注销一些设备后重试。
 | 
			
		||||
 | 
			
		||||
#auto-redact
 | 
			
		||||
autoRedact.title=自动删除
 | 
			
		||||
@ -737,7 +737,7 @@ pageLayout.submit=提交
 | 
			
		||||
scalePages.title=调整页面缩放比例
 | 
			
		||||
scalePages.header=调整页面缩放比例
 | 
			
		||||
scalePages.pageSize=文档页面的尺寸。
 | 
			
		||||
scalePages.keepPageSize=Original Size
 | 
			
		||||
scalePages.keepPageSize=保持页面原尺寸
 | 
			
		||||
scalePages.scaleFactor=页面的缩放级别(裁剪)。
 | 
			
		||||
scalePages.submit=提交
 | 
			
		||||
 | 
			
		||||
@ -757,7 +757,7 @@ certSign.showSig=显示签名
 | 
			
		||||
certSign.reason=原因
 | 
			
		||||
certSign.location=位置
 | 
			
		||||
certSign.name=名称
 | 
			
		||||
certSign.showLogo=Show Logo
 | 
			
		||||
certSign.showLogo=显示 Logo
 | 
			
		||||
certSign.submit=给 PDF 签名
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -792,9 +792,9 @@ compare.highlightColor.2=高亮颜色 2:
 | 
			
		||||
compare.document.1=文档 1
 | 
			
		||||
compare.document.2=文档 2
 | 
			
		||||
compare.submit=比较
 | 
			
		||||
compare.complex.message=One or both of the provided documents are large files, accuracy of comparison may be reduced
 | 
			
		||||
compare.large.file.message=One or Both of the provided documents are too large to process
 | 
			
		||||
compare.no.text.message=One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison.
 | 
			
		||||
compare.complex.message=提供的一份或两份文件是大文件,比较的准确性可能会降低。
 | 
			
		||||
compare.large.file.message=提供的文件中有一份或两份过大,无法处理。
 | 
			
		||||
compare.no.text.message=所选的 PDF 文件中有一个或两个没有文本内容。请选择包含文本的 PDF 文件进行对比。
 | 
			
		||||
 | 
			
		||||
#BookToPDF
 | 
			
		||||
BookToPDF.title=电子书和漫画转换成 PDF
 | 
			
		||||
@ -817,17 +817,17 @@ sign.draw=绘制签名
 | 
			
		||||
sign.text=文本输入
 | 
			
		||||
sign.clear=清除
 | 
			
		||||
sign.add=添加
 | 
			
		||||
sign.saved=Saved Signatures
 | 
			
		||||
sign.save=Save Signature
 | 
			
		||||
sign.personalSigs=Personal Signatures
 | 
			
		||||
sign.saved=已保存签名
 | 
			
		||||
sign.save=保存签名
 | 
			
		||||
sign.personalSigs=个人签名
 | 
			
		||||
sign.sharedSigs=Shared Signatures
 | 
			
		||||
sign.noSavedSigs=No saved signatures found
 | 
			
		||||
sign.addToAll=Add to all pages
 | 
			
		||||
sign.delete=Delete
 | 
			
		||||
sign.first=First page
 | 
			
		||||
sign.last=Last page
 | 
			
		||||
sign.next=Next page
 | 
			
		||||
sign.previous=Previous page
 | 
			
		||||
sign.noSavedSigs=未找到已保存的签名
 | 
			
		||||
sign.addToAll=添加到所有页面
 | 
			
		||||
sign.delete=删除
 | 
			
		||||
sign.first=首页
 | 
			
		||||
sign.last=末页
 | 
			
		||||
sign.next=下一页
 | 
			
		||||
sign.previous=上一页
 | 
			
		||||
 | 
			
		||||
#repair
 | 
			
		||||
repair.title=修复
 | 
			
		||||
@ -944,29 +944,39 @@ pdfOrganiser.placeholder=(例如:1,3,2 或 4-8,2,10-12 或 2n-1)
 | 
			
		||||
multiTool.title=PDF 多功能工具
 | 
			
		||||
multiTool.header=PDF 多功能工具
 | 
			
		||||
multiTool.uploadPrompts=文件名
 | 
			
		||||
multiTool.selectAll=Select All
 | 
			
		||||
multiTool.deselectAll=Deselect All
 | 
			
		||||
multiTool.selectAll=选择所有
 | 
			
		||||
multiTool.deselectAll=取消选择所有
 | 
			
		||||
multiTool.selectPages=Page Select
 | 
			
		||||
multiTool.selectedPages=Selected Pages
 | 
			
		||||
multiTool.selectedPages=已选择的页面
 | 
			
		||||
multiTool.page=Page
 | 
			
		||||
multiTool.deleteSelected=Delete Selected
 | 
			
		||||
multiTool.downloadAll=Export
 | 
			
		||||
multiTool.downloadSelected=Export Selected
 | 
			
		||||
multiTool.deleteSelected=删除已选
 | 
			
		||||
multiTool.downloadAll=导出全部
 | 
			
		||||
multiTool.downloadSelected=导出已选
 | 
			
		||||
 | 
			
		||||
multiTool.insertPageBreak=Insert Page Break
 | 
			
		||||
multiTool.addFile=Add File
 | 
			
		||||
multiTool.rotateLeft=Rotate Left
 | 
			
		||||
multiTool.rotateRight=Rotate Right
 | 
			
		||||
multiTool.split=Split
 | 
			
		||||
multiTool.moveLeft=Move Left
 | 
			
		||||
multiTool.moveRight=Move Right
 | 
			
		||||
multiTool.delete=Delete
 | 
			
		||||
multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.insertPageBreak=插入分页符
 | 
			
		||||
multiTool.addFile=添加文件
 | 
			
		||||
multiTool.rotateLeft=向左旋转
 | 
			
		||||
multiTool.rotateRight=向右旋转
 | 
			
		||||
multiTool.split=分割
 | 
			
		||||
multiTool.moveLeft=向做移动
 | 
			
		||||
multiTool.moveRight=向右移动
 | 
			
		||||
multiTool.delete=删除
 | 
			
		||||
multiTool.dragDropMessage=选择页面
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=此文件受密码保护。请输入密码:
 | 
			
		||||
decrypt.cancelled=PDF 操作已取消: {0}
 | 
			
		||||
decrypt.noPassword=未提供加密 PDF 的密码: {0}
 | 
			
		||||
decrypt.invalidPassword=请使用正确的密码重试。
 | 
			
		||||
decrypt.invalidPasswordHeader=密码错误或不支持的 PDF 加密: {0}
 | 
			
		||||
decrypt.unexpectedError=处理文件时发生错误。请再试一次。
 | 
			
		||||
decrypt.serverError=服务器解密时发生错误: {0}
 | 
			
		||||
decrypt.success=文件解密成功。
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
multiTool-advert.message=此功能也适用于我们的“<a href="{0}">多功能工具页面</a>”。查看它以获得增强的逐页 UI 以及其他功能!
 | 
			
		||||
 | 
			
		||||
#view pdf
 | 
			
		||||
viewPdf.title=浏览 PDF
 | 
			
		||||
@ -1225,11 +1235,11 @@ licenses.version=版本
 | 
			
		||||
licenses.license=许可证
 | 
			
		||||
 | 
			
		||||
#survey
 | 
			
		||||
survey.nav=调查
 | 
			
		||||
survey.title=Stirling-PDF调查
 | 
			
		||||
survey.nav=问卷调查
 | 
			
		||||
survey.title=Stirling-PDF 问卷调查
 | 
			
		||||
survey.description=Stirling-PDF 没有跟踪器,所以我们希望听取用户的意见来改进 Stirling-PDF!
 | 
			
		||||
survey.changes=Stirling-PDF has changed since the last survey! To find out more please check our blog post here:
 | 
			
		||||
survey.changes2=With these changes we are getting paid business support and funding
 | 
			
		||||
survey.changes=自上次调查以来,Stirling-PDF 已经发生了变化!要了解更多信息,请在此处查看我们的博客文章:
 | 
			
		||||
survey.changes2=通过这些变化,我们得到了商业支持和资金援助。
 | 
			
		||||
survey.please=请考虑参加我们的调查!
 | 
			
		||||
survey.disabled=(调查弹出窗口将在后续更新中被禁用,但可在页脚处查看)
 | 
			
		||||
survey.button=参与调查
 | 
			
		||||
@ -1257,29 +1267,29 @@ removeImage.removeImage=删除图像
 | 
			
		||||
removeImage.submit=删除图像
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
splitByChapters.title=Split PDF by Chapters
 | 
			
		||||
splitByChapters.header=Split PDF by Chapters
 | 
			
		||||
splitByChapters.bookmarkLevel=Bookmark Level
 | 
			
		||||
splitByChapters.includeMetadata=Include Metadata
 | 
			
		||||
splitByChapters.allowDuplicates=Allow Duplicates
 | 
			
		||||
splitByChapters.title=按章节拆分 PDF
 | 
			
		||||
splitByChapters.header=按章节拆分 PDF
 | 
			
		||||
splitByChapters.bookmarkLevel=书签级别
 | 
			
		||||
splitByChapters.includeMetadata=包含元数据
 | 
			
		||||
splitByChapters.allowDuplicates=允许重复
 | 
			
		||||
splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure.
 | 
			
		||||
splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.).
 | 
			
		||||
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
 | 
			
		||||
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
 | 
			
		||||
splitByChapters.submit=Split PDF
 | 
			
		||||
splitByChapters.submit=拆分 PDF
 | 
			
		||||
 | 
			
		||||
#File Chooser
 | 
			
		||||
fileChooser.click=Click
 | 
			
		||||
fileChooser.or=or
 | 
			
		||||
fileChooser.dragAndDrop=Drag & Drop
 | 
			
		||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
 | 
			
		||||
fileChooser.click=单击
 | 
			
		||||
fileChooser.or=或
 | 
			
		||||
fileChooser.dragAndDrop=拖放文件
 | 
			
		||||
fileChooser.hoveredDragAndDrop=拖放文件到此处
 | 
			
		||||
 | 
			
		||||
#release notes
 | 
			
		||||
releases.footer=Releases
 | 
			
		||||
releases.title=Release Notes
 | 
			
		||||
releases.header=Release Notes
 | 
			
		||||
releases.current.version=Current Release
 | 
			
		||||
releases.note=Release notes are only available in English
 | 
			
		||||
releases.footer=版本
 | 
			
		||||
releases.title=版本说明
 | 
			
		||||
releases.header=版本说明
 | 
			
		||||
releases.current.version=当前版本
 | 
			
		||||
releases.note=版本说明仅提供英文版本
 | 
			
		||||
 | 
			
		||||
#Validate Signature
 | 
			
		||||
validateSignature.title=Validate PDF Signatures
 | 
			
		||||
 | 
			
		||||
@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
 | 
			
		||||
multiTool.undo=Undo
 | 
			
		||||
multiTool.redo=Redo
 | 
			
		||||
 | 
			
		||||
#decrypt
 | 
			
		||||
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
 | 
			
		||||
decrypt.cancelled=Operation cancelled for PDF: {0}
 | 
			
		||||
decrypt.noPassword=No password provided for encrypted PDF: {0}
 | 
			
		||||
decrypt.invalidPassword=Please try again with the correct password.
 | 
			
		||||
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
 | 
			
		||||
decrypt.unexpectedError=There was an error processing the file. Please try again.
 | 
			
		||||
decrypt.serverError=Server error while decrypting: {0}
 | 
			
		||||
decrypt.success=File decrypted successfully.
 | 
			
		||||
 | 
			
		||||
#multiTool-advert
 | 
			
		||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -65,6 +65,7 @@
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  margin: -20px;
 | 
			
		||||
  padding: 20px;
 | 
			
		||||
  box-sizing:content-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.feature-group-container.animated-group {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										116
									
								
								src/main/resources/static/js/DecryptFiles.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/main/resources/static/js/DecryptFiles.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,116 @@
 | 
			
		||||
export class DecryptFile {
 | 
			
		||||
  async decryptFile(file, requiresPassword) {
 | 
			
		||||
    try {
 | 
			
		||||
      const formData = new FormData();
 | 
			
		||||
      formData.append('fileInput', file);
 | 
			
		||||
      if (requiresPassword) {
 | 
			
		||||
        const password = prompt(`${window.decrypt.passwordPrompt}`);
 | 
			
		||||
 | 
			
		||||
        if (password === null) {
 | 
			
		||||
          // User cancelled
 | 
			
		||||
          console.error(`Password prompt cancelled for PDF: ${file.name}`);
 | 
			
		||||
          return null; // No file to return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!password) {
 | 
			
		||||
          // No password provided
 | 
			
		||||
          console.error(`No password provided for encrypted PDF: ${file.name}`);
 | 
			
		||||
          this.showErrorBanner(
 | 
			
		||||
            `${window.decrypt.noPassword.replace('{0}', file.name)}`,
 | 
			
		||||
            '',
 | 
			
		||||
            `${window.decrypt.unexpectedError}`
 | 
			
		||||
          );
 | 
			
		||||
          return null; // No file to return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        formData.append('password', password);
 | 
			
		||||
      }
 | 
			
		||||
      // Send decryption request
 | 
			
		||||
      const response = await fetch('/api/v1/security/remove-password', {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        body: formData,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (response.ok) {
 | 
			
		||||
        const decryptedBlob = await response.blob();
 | 
			
		||||
        this.removeErrorBanner();
 | 
			
		||||
        return new File([decryptedBlob], file.name, {type: 'application/pdf'});
 | 
			
		||||
      } else {
 | 
			
		||||
        const errorText = await response.text();
 | 
			
		||||
        console.error(`${window.decrypt.invalidPassword} ${errorText}`);
 | 
			
		||||
        this.showErrorBanner(
 | 
			
		||||
          `${window.decrypt.invalidPassword}`,
 | 
			
		||||
          errorText,
 | 
			
		||||
          `${window.decrypt.invalidPasswordHeader.replace('{0}', file.name)}`
 | 
			
		||||
        );
 | 
			
		||||
        return null; // No file to return
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // Handle network or unexpected errors
 | 
			
		||||
      console.error(`Failed to decrypt PDF: ${file.name}`, error);
 | 
			
		||||
      this.showErrorBanner(
 | 
			
		||||
        `${window.decrypt.unexpectedError.replace('{0}', file.name)}`,
 | 
			
		||||
        `${error.message || window.decrypt.unexpectedError}`,
 | 
			
		||||
        error
 | 
			
		||||
      );
 | 
			
		||||
      return null; // No file to return
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async checkFileEncrypted(file) {
 | 
			
		||||
    try {
 | 
			
		||||
      if (file.type !== 'application/pdf') {
 | 
			
		||||
        return {isEncrypted: false, requiresPassword: false};
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
 | 
			
		||||
      const arrayBuffer = await file.arrayBuffer();
 | 
			
		||||
      const arrayBufferForPdfLib = arrayBuffer.slice(0);
 | 
			
		||||
 | 
			
		||||
      const loadingTask = pdfjsLib.getDocument({
 | 
			
		||||
        data: arrayBuffer,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await loadingTask.promise;
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        //Uses PDFLib.PDFDocument to check if unpassworded but encrypted
 | 
			
		||||
        const pdfDoc = await PDFLib.PDFDocument.load(arrayBufferForPdfLib);
 | 
			
		||||
        return {isEncrypted: false, requiresPassword: false};
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        if (error.message.includes('Input document to `PDFDocument.load` is encrypted')) {
 | 
			
		||||
          return {isEncrypted: true, requiresPassword: false};
 | 
			
		||||
        }
 | 
			
		||||
        console.error('Error checking encryption:', error);
 | 
			
		||||
        throw new Error('Failed to determine if the file is encrypted.');
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      if (error.name === 'PasswordException') {
 | 
			
		||||
        if (error.code === pdfjsLib.PasswordResponses.NEED_PASSWORD) {
 | 
			
		||||
          return {isEncrypted: true, requiresPassword: true};
 | 
			
		||||
        } else if (error.code === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) {
 | 
			
		||||
          return {isEncrypted: true, requiresPassword: false};
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      console.error('Error checking encryption:', error);
 | 
			
		||||
      throw new Error('Failed to determine if the file is encrypted.');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  showErrorBanner(message, stackTrace, error) {
 | 
			
		||||
    const errorContainer = document.getElementById('errorContainer');
 | 
			
		||||
    errorContainer.style.display = 'block'; // Display the banner
 | 
			
		||||
    errorContainer.querySelector('.alert-heading').textContent = error;
 | 
			
		||||
    errorContainer.querySelector('p').textContent = message;
 | 
			
		||||
    document.querySelector('#traceContent').textContent = stackTrace;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeErrorBanner() {
 | 
			
		||||
    const errorContainer = document.getElementById('errorContainer');
 | 
			
		||||
    errorContainer.style.display = 'none'; // Hide the banner
 | 
			
		||||
    errorContainer.querySelector('.alert-heading').textContent = '';
 | 
			
		||||
    errorContainer.querySelector('p').textContent = '';
 | 
			
		||||
    document.querySelector('#traceContent').textContent = '';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								src/main/resources/static/js/download.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/main/resources/static/js/download.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
document.getElementById('download-pdf').addEventListener('click', async () => {
 | 
			
		||||
  const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
 | 
			
		||||
  let decryptedFile = modifiedPdf;
 | 
			
		||||
  let isEncrypted = false;
 | 
			
		||||
  let requiresPassword = false;
 | 
			
		||||
  await this.decryptFile
 | 
			
		||||
    .checkFileEncrypted(decryptedFile)
 | 
			
		||||
    .then((result) => {
 | 
			
		||||
      isEncrypted = result.isEncrypted;
 | 
			
		||||
      requiresPassword = result.requiresPassword;
 | 
			
		||||
    })
 | 
			
		||||
    .catch((error) => {
 | 
			
		||||
      console.error(error);
 | 
			
		||||
    });
 | 
			
		||||
  if (decryptedFile.type === 'application/pdf' && isEncrypted) {
 | 
			
		||||
    decryptedFile = await this.decryptFile.decryptFile(decryptedFile, requiresPassword);
 | 
			
		||||
    if (!decryptedFile) {
 | 
			
		||||
      throw new Error('File decryption failed.');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const modifiedPdfBytes = await modifiedPdf.save();
 | 
			
		||||
  const blob = new Blob([modifiedPdfBytes], {type: 'application/pdf'});
 | 
			
		||||
  const link = document.createElement('a');
 | 
			
		||||
  link.href = URL.createObjectURL(blob);
 | 
			
		||||
  link.download = originalFileName + '_signed.pdf';
 | 
			
		||||
  link.click();
 | 
			
		||||
});
 | 
			
		||||
@ -42,7 +42,7 @@
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
      firstErrorOccurred = false;
 | 
			
		||||
      const url = this.action;
 | 
			
		||||
      const files = $('#fileInput-input')[0].files;
 | 
			
		||||
      let files = $('#fileInput-input')[0].files;
 | 
			
		||||
      const formData = new FormData(this);
 | 
			
		||||
      const submitButton = document.getElementById('submitBtn');
 | 
			
		||||
      const showGameBtn = document.getElementById('show-game-btn');
 | 
			
		||||
@ -71,6 +71,16 @@
 | 
			
		||||
      }, 5000);
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        if (!url.includes('remove-password')) {
 | 
			
		||||
          // Check if any PDF files are encrypted and handle decryption if necessary
 | 
			
		||||
          const decryptedFiles = await checkAndDecryptFiles(url, files);
 | 
			
		||||
          files = decryptedFiles;
 | 
			
		||||
          // Append decrypted files to formData
 | 
			
		||||
          decryptedFiles.forEach((file, index) => {
 | 
			
		||||
            formData.set(`fileInput`, file);
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        submitButton.textContent = 'Processing...';
 | 
			
		||||
        submitButton.disabled = true;
 | 
			
		||||
 | 
			
		||||
@ -133,6 +143,98 @@
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function checkAndDecryptFiles(url, files) {
 | 
			
		||||
    const decryptedFiles = [];
 | 
			
		||||
    pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
 | 
			
		||||
 | 
			
		||||
    // Extract the base URL
 | 
			
		||||
    const baseUrl = new URL(url);
 | 
			
		||||
    let removePasswordUrl = `${baseUrl.origin}`;
 | 
			
		||||
 | 
			
		||||
    // Check if there's a path before /api/
 | 
			
		||||
    const apiIndex = baseUrl.pathname.indexOf('/api/');
 | 
			
		||||
    if (apiIndex > 0) {
 | 
			
		||||
      removePasswordUrl += baseUrl.pathname.substring(0, apiIndex);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Append the new endpoint
 | 
			
		||||
    removePasswordUrl += '/api/v1/security/remove-password';
 | 
			
		||||
 | 
			
		||||
    console.log(`Remove password URL: ${removePasswordUrl}`);
 | 
			
		||||
 | 
			
		||||
    for (const file of files) {
 | 
			
		||||
      console.log(`Processing file: ${file.name}`);
 | 
			
		||||
      if (file.type !== 'application/pdf') {
 | 
			
		||||
        console.log(`Skipping non-PDF file: ${file.name}`);
 | 
			
		||||
        decryptedFiles.push(file);
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      try {
 | 
			
		||||
        const arrayBuffer = await file.arrayBuffer();
 | 
			
		||||
        const loadingTask = pdfjsLib.getDocument({data: arrayBuffer});
 | 
			
		||||
 | 
			
		||||
        console.log(`Attempting to load PDF: ${file.name}`);
 | 
			
		||||
        const pdf = await loadingTask.promise;
 | 
			
		||||
        console.log(`File is not encrypted: ${file.name}`);
 | 
			
		||||
        decryptedFiles.push(file); // If no error, file is not encrypted
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        if (error.name === 'PasswordException' && error.code === 1) {
 | 
			
		||||
          console.log(`PDF requires password: ${file.name}`, error);
 | 
			
		||||
          console.log(`Attempting to remove password from PDF: ${file.name} with password.`);
 | 
			
		||||
          const password = prompt(`${window.translations.decrypt.passwordPrompt}`);
 | 
			
		||||
 | 
			
		||||
          if (!password) {
 | 
			
		||||
            console.error(`No password provided for encrypted PDF: ${file.name}`);
 | 
			
		||||
            showErrorBanner(
 | 
			
		||||
              `${window.translations.decrypt.noPassword.replace('{0}', file.name)}`,
 | 
			
		||||
              `${window.translations.decrypt.unexpectedError}`
 | 
			
		||||
            );
 | 
			
		||||
            throw error;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          try {
 | 
			
		||||
            // Prepare FormData for the decryption request
 | 
			
		||||
            const formData = new FormData();
 | 
			
		||||
            formData.append('fileInput', file);
 | 
			
		||||
            formData.append('password', password);
 | 
			
		||||
 | 
			
		||||
            // Use handleSingleDownload to send the request
 | 
			
		||||
            const decryptionResult = await fetch(removePasswordUrl, {method: 'POST', body: formData});
 | 
			
		||||
 | 
			
		||||
            if (decryptionResult && decryptionResult.blob) {
 | 
			
		||||
              const decryptedBlob = await decryptionResult.blob();
 | 
			
		||||
              const decryptedFile = new File([decryptedBlob], file.name, {type: 'application/pdf'});
 | 
			
		||||
 | 
			
		||||
              /*    // Create a link element to download the file
 | 
			
		||||
            const link = document.createElement('a');
 | 
			
		||||
            link.href = URL.createObjectURL(decryptedBlob);
 | 
			
		||||
            link.download = 'test.pdf';
 | 
			
		||||
            document.body.appendChild(link);
 | 
			
		||||
            link.click();
 | 
			
		||||
            document.body.removeChild(link);
 | 
			
		||||
*/
 | 
			
		||||
              decryptedFiles.push(decryptedFile);
 | 
			
		||||
              console.log(`Successfully decrypted PDF: ${file.name}`);
 | 
			
		||||
            } else {
 | 
			
		||||
              throw new Error('Decryption failed: No valid response from server');
 | 
			
		||||
            }
 | 
			
		||||
          } catch (decryptError) {
 | 
			
		||||
            console.error(`Failed to decrypt PDF: ${file.name}`, decryptError);
 | 
			
		||||
            showErrorBanner(
 | 
			
		||||
              `${window.translations.invalidPasswordHeader.replace('{0}', file.name)}`,
 | 
			
		||||
              `${window.translations.invalidPassword}`
 | 
			
		||||
            );
 | 
			
		||||
            throw decryptError;
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          console.log(`Error loading PDF: ${file.name}`, error);
 | 
			
		||||
          throw error;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return decryptedFiles;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function handleSingleDownload(url, formData, isMulti = false, isZip = false) {
 | 
			
		||||
    const startTime = performance.now();
 | 
			
		||||
    const file = formData.get('fileInput');
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
const DraggableUtils = {
 | 
			
		||||
  boxDragContainer: document.getElementById("box-drag-container"),
 | 
			
		||||
  pdfCanvas: document.getElementById("pdf-canvas"),
 | 
			
		||||
  boxDragContainer: document.getElementById('box-drag-container'),
 | 
			
		||||
  pdfCanvas: document.getElementById('pdf-canvas'),
 | 
			
		||||
  nextId: 0,
 | 
			
		||||
  pdfDoc: null,
 | 
			
		||||
  pageIndex: 0,
 | 
			
		||||
@ -9,19 +9,17 @@ const DraggableUtils = {
 | 
			
		||||
  lastInteracted: null,
 | 
			
		||||
 | 
			
		||||
  init() {
 | 
			
		||||
    interact(".draggable-canvas")
 | 
			
		||||
    interact('.draggable-canvas')
 | 
			
		||||
      .draggable({
 | 
			
		||||
        listeners: {
 | 
			
		||||
          move: (event) => {
 | 
			
		||||
            const target = event.target;
 | 
			
		||||
            const x = (parseFloat(target.getAttribute("data-bs-x")) || 0)
 | 
			
		||||
              + event.dx;
 | 
			
		||||
            const y = (parseFloat(target.getAttribute("data-bs-y")) || 0)
 | 
			
		||||
              + event.dy;
 | 
			
		||||
            const x = (parseFloat(target.getAttribute('data-bs-x')) || 0) + event.dx;
 | 
			
		||||
            const y = (parseFloat(target.getAttribute('data-bs-y')) || 0) + event.dy;
 | 
			
		||||
 | 
			
		||||
            target.style.transform = `translate(${x}px, ${y}px)`;
 | 
			
		||||
            target.setAttribute("data-bs-x", x);
 | 
			
		||||
            target.setAttribute("data-bs-y", y);
 | 
			
		||||
            target.setAttribute('data-bs-x', x);
 | 
			
		||||
            target.setAttribute('data-bs-y', y);
 | 
			
		||||
 | 
			
		||||
            this.onInteraction(target);
 | 
			
		||||
            //update the last interacted element
 | 
			
		||||
@ -34,8 +32,8 @@ const DraggableUtils = {
 | 
			
		||||
        listeners: {
 | 
			
		||||
          move: (event) => {
 | 
			
		||||
            var target = event.target;
 | 
			
		||||
            var x = parseFloat(target.getAttribute("data-bs-x")) || 0;
 | 
			
		||||
            var y = parseFloat(target.getAttribute("data-bs-y")) || 0;
 | 
			
		||||
            var x = parseFloat(target.getAttribute('data-bs-x')) || 0;
 | 
			
		||||
            var y = parseFloat(target.getAttribute('data-bs-y')) || 0;
 | 
			
		||||
 | 
			
		||||
            // check if control key is pressed
 | 
			
		||||
            if (event.ctrlKey) {
 | 
			
		||||
@ -44,8 +42,7 @@ const DraggableUtils = {
 | 
			
		||||
              let width = event.rect.width;
 | 
			
		||||
              let height = event.rect.height;
 | 
			
		||||
 | 
			
		||||
              if (Math.abs(event.deltaRect.width) >= Math.abs(
 | 
			
		||||
                event.deltaRect.height)) {
 | 
			
		||||
              if (Math.abs(event.deltaRect.width) >= Math.abs(event.deltaRect.height)) {
 | 
			
		||||
                height = width / aspectRatio;
 | 
			
		||||
              } else {
 | 
			
		||||
                width = height * aspectRatio;
 | 
			
		||||
@ -55,19 +52,18 @@ const DraggableUtils = {
 | 
			
		||||
              event.rect.height = height;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            target.style.width = event.rect.width + "px";
 | 
			
		||||
            target.style.height = event.rect.height + "px";
 | 
			
		||||
            target.style.width = event.rect.width + 'px';
 | 
			
		||||
            target.style.height = event.rect.height + 'px';
 | 
			
		||||
 | 
			
		||||
            // translate when resizing from top or left edges
 | 
			
		||||
            x += event.deltaRect.left;
 | 
			
		||||
            y += event.deltaRect.top;
 | 
			
		||||
 | 
			
		||||
            target.style.transform = "translate(" + x + "px," + y + "px)";
 | 
			
		||||
            target.style.transform = 'translate(' + x + 'px,' + y + 'px)';
 | 
			
		||||
 | 
			
		||||
            target.setAttribute("data-bs-x", x);
 | 
			
		||||
            target.setAttribute("data-bs-y", y);
 | 
			
		||||
            target.textContent = Math.round(event.rect.width) + "\u00D7"
 | 
			
		||||
              + Math.round(event.rect.height);
 | 
			
		||||
            target.setAttribute('data-bs-x', x);
 | 
			
		||||
            target.setAttribute('data-bs-y', y);
 | 
			
		||||
            target.textContent = Math.round(event.rect.width) + '\u00D7' + Math.round(event.rect.height);
 | 
			
		||||
 | 
			
		||||
            this.onInteraction(target);
 | 
			
		||||
          },
 | 
			
		||||
@ -95,8 +91,8 @@ const DraggableUtils = {
 | 
			
		||||
        const stepY = target.offsetHeight * 0.05;
 | 
			
		||||
 | 
			
		||||
        // Get the current x and y coordinates
 | 
			
		||||
        let x = (parseFloat(target.getAttribute('data-bs-x')) || 0);
 | 
			
		||||
        let y = (parseFloat(target.getAttribute('data-bs-y')) || 0);
 | 
			
		||||
        let x = parseFloat(target.getAttribute('data-bs-x')) || 0;
 | 
			
		||||
        let y = parseFloat(target.getAttribute('data-bs-y')) || 0;
 | 
			
		||||
 | 
			
		||||
        // Check which key was pressed and update the coordinates accordingly
 | 
			
		||||
        switch (event.key) {
 | 
			
		||||
@ -135,15 +131,15 @@ const DraggableUtils = {
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  createDraggableCanvas() {
 | 
			
		||||
    const createdCanvas = document.createElement("canvas");
 | 
			
		||||
    const createdCanvas = document.createElement('canvas');
 | 
			
		||||
    createdCanvas.id = `draggable-canvas-${this.nextId++}`;
 | 
			
		||||
    createdCanvas.classList.add("draggable-canvas");
 | 
			
		||||
    createdCanvas.classList.add('draggable-canvas');
 | 
			
		||||
 | 
			
		||||
    const x = 0;
 | 
			
		||||
    const y = 20;
 | 
			
		||||
    createdCanvas.style.transform = `translate(${x}px, ${y}px)`;
 | 
			
		||||
    createdCanvas.setAttribute("data-bs-x", x);
 | 
			
		||||
    createdCanvas.setAttribute("data-bs-y", y);
 | 
			
		||||
    createdCanvas.setAttribute('data-bs-x', x);
 | 
			
		||||
    createdCanvas.setAttribute('data-bs-y', y);
 | 
			
		||||
 | 
			
		||||
    //Click element in order to enable arrow keys
 | 
			
		||||
    createdCanvas.addEventListener('click', () => {
 | 
			
		||||
@ -186,29 +182,29 @@ const DraggableUtils = {
 | 
			
		||||
          newHeight = newHeight * scaleMultiplier;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        createdCanvas.style.width = newWidth + "px";
 | 
			
		||||
        createdCanvas.style.height = newHeight + "px";
 | 
			
		||||
        createdCanvas.style.width = newWidth + 'px';
 | 
			
		||||
        createdCanvas.style.height = newHeight + 'px';
 | 
			
		||||
 | 
			
		||||
        var myContext = createdCanvas.getContext("2d");
 | 
			
		||||
        var myContext = createdCanvas.getContext('2d');
 | 
			
		||||
        myContext.drawImage(myImage, 0, 0);
 | 
			
		||||
        resolve(createdCanvas);
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  deleteAllDraggableCanvases() {
 | 
			
		||||
    this.boxDragContainer.querySelectorAll(".draggable-canvas").forEach((el) => el.remove());
 | 
			
		||||
    this.boxDragContainer.querySelectorAll('.draggable-canvas').forEach((el) => el.remove());
 | 
			
		||||
  },
 | 
			
		||||
  async addAllPagesDraggableCanvas(element) {
 | 
			
		||||
    if (element) {
 | 
			
		||||
      let currentPage = this.pageIndex
 | 
			
		||||
      let currentPage = this.pageIndex;
 | 
			
		||||
      if (!this.elementAllPages.includes(element)) {
 | 
			
		||||
        this.elementAllPages.push(element)
 | 
			
		||||
        this.elementAllPages.push(element);
 | 
			
		||||
        element.style.filter = 'sepia(1) hue-rotate(90deg) brightness(1.2)';
 | 
			
		||||
        let newElement = {
 | 
			
		||||
          "element": element,
 | 
			
		||||
          "offsetWidth": element.width,
 | 
			
		||||
          "offsetHeight": element.height
 | 
			
		||||
        }
 | 
			
		||||
          element: element,
 | 
			
		||||
          offsetWidth: element.width,
 | 
			
		||||
          offsetHeight: element.height,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let pagesMap = this.documentsMap.get(this.pdfDoc);
 | 
			
		||||
 | 
			
		||||
@ -216,21 +212,20 @@ const DraggableUtils = {
 | 
			
		||||
          pagesMap = {};
 | 
			
		||||
          this.documentsMap.set(this.pdfDoc, pagesMap);
 | 
			
		||||
        }
 | 
			
		||||
        let page = this.pageIndex
 | 
			
		||||
        let page = this.pageIndex;
 | 
			
		||||
 | 
			
		||||
        for (let pageIndex = 0; pageIndex < this.pdfDoc.numPages; pageIndex++) {
 | 
			
		||||
 | 
			
		||||
          if (pagesMap[`${pageIndex}-offsetWidth`]) {
 | 
			
		||||
            if (!pagesMap[pageIndex].includes(newElement)) {
 | 
			
		||||
              pagesMap[pageIndex].push(newElement);
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            pagesMap[pageIndex] = []
 | 
			
		||||
            pagesMap[pageIndex].push(newElement)
 | 
			
		||||
            pagesMap[pageIndex] = [];
 | 
			
		||||
            pagesMap[pageIndex].push(newElement);
 | 
			
		||||
            pagesMap[`${pageIndex}-offsetWidth`] = pagesMap[`${page}-offsetWidth`];
 | 
			
		||||
            pagesMap[`${pageIndex}-offsetHeight`] = pagesMap[`${page}-offsetHeight`];
 | 
			
		||||
          }
 | 
			
		||||
          await this.goToPage(pageIndex)
 | 
			
		||||
          await this.goToPage(pageIndex);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        const index = this.elementAllPages.indexOf(element);
 | 
			
		||||
@ -247,17 +242,17 @@ const DraggableUtils = {
 | 
			
		||||
        for (let pageIndex = 0; pageIndex < this.pdfDoc.numPages; pageIndex++) {
 | 
			
		||||
          if (pagesMap[`${pageIndex}-offsetWidth`] && pageIndex != currentPage) {
 | 
			
		||||
            const pageElements = pagesMap[pageIndex];
 | 
			
		||||
            pageElements.forEach(elementPage => {
 | 
			
		||||
              const elementIndex = pageElements.findIndex(elementPage => elementPage['element'].id === element.id);
 | 
			
		||||
            pageElements.forEach((elementPage) => {
 | 
			
		||||
              const elementIndex = pageElements.findIndex((elementPage) => elementPage['element'].id === element.id);
 | 
			
		||||
              if (elementIndex !== -1) {
 | 
			
		||||
                pageElements.splice(elementIndex, 1);
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
          await this.goToPage(pageIndex)
 | 
			
		||||
          await this.goToPage(pageIndex);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      await this.goToPage(currentPage)
 | 
			
		||||
      await this.goToPage(currentPage);
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  deleteDraggableCanvas(element) {
 | 
			
		||||
@ -271,7 +266,7 @@ const DraggableUtils = {
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  getLastInteracted() {
 | 
			
		||||
    return this.boxDragContainer.querySelector(".draggable-canvas:last-of-type");
 | 
			
		||||
    return this.boxDragContainer.querySelector('.draggable-canvas:last-of-type');
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  storePageContents() {
 | 
			
		||||
@ -280,7 +275,7 @@ const DraggableUtils = {
 | 
			
		||||
      pagesMap = {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const elements = [...this.boxDragContainer.querySelectorAll(".draggable-canvas")];
 | 
			
		||||
    const elements = [...this.boxDragContainer.querySelectorAll('.draggable-canvas')];
 | 
			
		||||
    const draggablesData = elements.map((el) => {
 | 
			
		||||
      return {
 | 
			
		||||
        element: el,
 | 
			
		||||
@ -291,8 +286,8 @@ const DraggableUtils = {
 | 
			
		||||
    elements.forEach((el) => this.boxDragContainer.removeChild(el));
 | 
			
		||||
 | 
			
		||||
    pagesMap[this.pageIndex] = draggablesData;
 | 
			
		||||
    pagesMap[this.pageIndex + "-offsetWidth"] = this.pdfCanvas.offsetWidth;
 | 
			
		||||
    pagesMap[this.pageIndex + "-offsetHeight"] = this.pdfCanvas.offsetHeight;
 | 
			
		||||
    pagesMap[this.pageIndex + '-offsetWidth'] = this.pdfCanvas.offsetWidth;
 | 
			
		||||
    pagesMap[this.pageIndex + '-offsetHeight'] = this.pdfCanvas.offsetHeight;
 | 
			
		||||
 | 
			
		||||
    this.documentsMap.set(this.pdfDoc, pagesMap);
 | 
			
		||||
  },
 | 
			
		||||
@ -329,7 +324,7 @@ const DraggableUtils = {
 | 
			
		||||
 | 
			
		||||
    // render the page onto the canvas
 | 
			
		||||
    var renderContext = {
 | 
			
		||||
      canvasContext: this.pdfCanvas.getContext("2d"),
 | 
			
		||||
      canvasContext: this.pdfCanvas.getContext('2d'),
 | 
			
		||||
      viewport: page.getViewport({scale: 1}),
 | 
			
		||||
    };
 | 
			
		||||
    await page.render(renderContext).promise;
 | 
			
		||||
@ -369,7 +364,7 @@ const DraggableUtils = {
 | 
			
		||||
    const pagesMap = this.documentsMap.get(this.pdfDoc);
 | 
			
		||||
 | 
			
		||||
    for (let pageIdx in pagesMap) {
 | 
			
		||||
      if (pageIdx.includes("offset")) {
 | 
			
		||||
      if (pageIdx.includes('offset')) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      console.log(typeof pageIdx);
 | 
			
		||||
@ -377,9 +372,8 @@ const DraggableUtils = {
 | 
			
		||||
      const page = pdfDocModified.getPage(parseInt(pageIdx));
 | 
			
		||||
      let draggablesData = pagesMap[pageIdx];
 | 
			
		||||
 | 
			
		||||
      const offsetWidth = pagesMap[pageIdx + "-offsetWidth"];
 | 
			
		||||
      const offsetHeight = pagesMap[pageIdx + "-offsetHeight"];
 | 
			
		||||
 | 
			
		||||
      const offsetWidth = pagesMap[pageIdx + '-offsetWidth'];
 | 
			
		||||
      const offsetHeight = pagesMap[pageIdx + '-offsetHeight'];
 | 
			
		||||
 | 
			
		||||
      for (const draggableData of draggablesData) {
 | 
			
		||||
        // embed the draggable canvas
 | 
			
		||||
@ -389,8 +383,8 @@ const DraggableUtils = {
 | 
			
		||||
        const pdfImageObject = await pdfDocModified.embedPng(draggableImgBytes);
 | 
			
		||||
 | 
			
		||||
        // calculate the position in the pdf document
 | 
			
		||||
        const tansform = draggableElement.style.transform.replace(/[^.,-\d]/g, "");
 | 
			
		||||
        const transformComponents = tansform.split(",");
 | 
			
		||||
        const tansform = draggableElement.style.transform.replace(/[^.,-\d]/g, '');
 | 
			
		||||
        const transformComponents = tansform.split(',');
 | 
			
		||||
        const draggablePositionPixels = {
 | 
			
		||||
          x: parseFloat(transformComponents[0]),
 | 
			
		||||
          y: parseFloat(transformComponents[1]),
 | 
			
		||||
@ -429,9 +423,8 @@ const DraggableUtils = {
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        //Defining the image if the page has a 0-degree angle
 | 
			
		||||
        let x = draggablePositionPdf.x
 | 
			
		||||
        let y = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height
 | 
			
		||||
 | 
			
		||||
        let x = draggablePositionPdf.x;
 | 
			
		||||
        let y = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height;
 | 
			
		||||
 | 
			
		||||
        //Defining the image position if it is at other angles
 | 
			
		||||
        if (normalizedAngle === 90) {
 | 
			
		||||
@ -451,7 +444,7 @@ const DraggableUtils = {
 | 
			
		||||
          y: y,
 | 
			
		||||
          width: draggablePositionPdf.width,
 | 
			
		||||
          height: draggablePositionPdf.height,
 | 
			
		||||
          rotate: rotation
 | 
			
		||||
          rotate: rotation,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@ -460,6 +453,6 @@ const DraggableUtils = {
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
  DraggableUtils.init();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,19 @@
 | 
			
		||||
import FileIconFactory from "./file-icon-factory.js";
 | 
			
		||||
import FileUtils from "./file-utils.js";
 | 
			
		||||
import FileIconFactory from './file-icon-factory.js';
 | 
			
		||||
import FileUtils from './file-utils.js';
 | 
			
		||||
import UUID from './uuid.js';
 | 
			
		||||
 | 
			
		||||
import {DecryptFile} from './DecryptFiles.js';
 | 
			
		||||
let isScriptExecuted = false;
 | 
			
		||||
if (!isScriptExecuted) {
 | 
			
		||||
  isScriptExecuted = true;
 | 
			
		||||
  document.addEventListener("DOMContentLoaded", function () {
 | 
			
		||||
    document.querySelectorAll(".custom-file-chooser").forEach(setupFileInput);
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', function () {
 | 
			
		||||
    document.querySelectorAll('.custom-file-chooser').forEach(setupFileInput);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function setupFileInput(chooser) {
 | 
			
		||||
  const elementId = chooser.getAttribute("data-bs-element-id");
 | 
			
		||||
  const filesSelected = chooser.getAttribute("data-bs-files-selected");
 | 
			
		||||
  const pdfPrompt = chooser.getAttribute("data-bs-pdf-prompt");
 | 
			
		||||
  const elementId = chooser.getAttribute('data-bs-element-id');
 | 
			
		||||
  const filesSelected = chooser.getAttribute('data-bs-files-selected');
 | 
			
		||||
  const pdfPrompt = chooser.getAttribute('data-bs-pdf-prompt');
 | 
			
		||||
  const inputContainerId = chooser.getAttribute('data-bs-element-container-id');
 | 
			
		||||
 | 
			
		||||
  let inputContainer = document.getElementById(inputContainerId);
 | 
			
		||||
@ -26,7 +25,7 @@ function setupFileInput(chooser) {
 | 
			
		||||
  inputContainer.addEventListener('click', (e) => {
 | 
			
		||||
    let inputBtn = document.getElementById(elementId);
 | 
			
		||||
    inputBtn.click();
 | 
			
		||||
  })
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const dragenterListener = function () {
 | 
			
		||||
    dragCounter++;
 | 
			
		||||
@ -63,7 +62,7 @@ function setupFileInput(chooser) {
 | 
			
		||||
    const files = dt.files;
 | 
			
		||||
 | 
			
		||||
    const fileInput = document.getElementById(elementId);
 | 
			
		||||
    if (fileInput?.hasAttribute("multiple")) {
 | 
			
		||||
    if (fileInput?.hasAttribute('multiple')) {
 | 
			
		||||
      pushFileListTo(files, allFiles);
 | 
			
		||||
    } else if (fileInput) {
 | 
			
		||||
      allFiles = [files[0]];
 | 
			
		||||
@ -78,7 +77,7 @@ function setupFileInput(chooser) {
 | 
			
		||||
 | 
			
		||||
    dragCounter = 0;
 | 
			
		||||
 | 
			
		||||
    fileInput.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: {source: 'drag-drop'} }));
 | 
			
		||||
    fileInput.dispatchEvent(new CustomEvent('change', {bubbles: true, detail: {source: 'drag-drop'}}));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function pushFileListTo(fileList, container) {
 | 
			
		||||
@ -87,7 +86,7 @@ function setupFileInput(chooser) {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
 | 
			
		||||
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
 | 
			
		||||
    document.body.addEventListener(eventName, preventDefaults, false);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@ -96,37 +95,50 @@ function setupFileInput(chooser) {
 | 
			
		||||
    e.stopPropagation();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  document.body.addEventListener("dragenter", dragenterListener);
 | 
			
		||||
  document.body.addEventListener("dragleave", dragleaveListener);
 | 
			
		||||
  document.body.addEventListener("drop", dropListener);
 | 
			
		||||
  document.body.addEventListener('dragenter', dragenterListener);
 | 
			
		||||
  document.body.addEventListener('dragleave', dragleaveListener);
 | 
			
		||||
  document.body.addEventListener('drop', dropListener);
 | 
			
		||||
 | 
			
		||||
  $("#" + elementId).on("change", function (e) {
 | 
			
		||||
  $('#' + elementId).on('change', async function (e) {
 | 
			
		||||
    let element = e.target;
 | 
			
		||||
    const isDragAndDrop = e.detail?.source == 'drag-drop';
 | 
			
		||||
 | 
			
		||||
    if (element instanceof HTMLInputElement && element.hasAttribute("multiple")) {
 | 
			
		||||
    if (element instanceof HTMLInputElement && element.hasAttribute('multiple')) {
 | 
			
		||||
      allFiles = isDragAndDrop ? allFiles : [...allFiles, ...element.files];
 | 
			
		||||
    } else {
 | 
			
		||||
      allFiles = Array.from(isDragAndDrop ? allFiles : [element.files[0]]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    allFiles = allFiles.map(file => {
 | 
			
		||||
    allFiles = await Promise.all(
 | 
			
		||||
      allFiles.map(async (file) => {
 | 
			
		||||
        let decryptedFile = file;
 | 
			
		||||
        try {
 | 
			
		||||
          const decryptFile = new DecryptFile();
 | 
			
		||||
          const {isEncrypted, requiresPassword} = await decryptFile.checkFileEncrypted(file);
 | 
			
		||||
          if (file.type === 'application/pdf' && isEncrypted) {
 | 
			
		||||
            decryptedFile = await decryptFile.decryptFile(file, requiresPassword);
 | 
			
		||||
            if (!decryptedFile) throw new Error('File decryption failed.');
 | 
			
		||||
          }
 | 
			
		||||
          decryptedFile.uniqueId = UUID.uuidv4();
 | 
			
		||||
          return decryptedFile;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          console.error(`Error decrypting file: ${file.name}`, error);
 | 
			
		||||
          if (!file.uniqueId) file.uniqueId = UUID.uuidv4();
 | 
			
		||||
          return file;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
    if (!isDragAndDrop) {
 | 
			
		||||
      let dataTransfer = toDataTransfer(allFiles);
 | 
			
		||||
      element.files = dataTransfer.files;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleFileInputChange(this);
 | 
			
		||||
    this.dispatchEvent(new CustomEvent("file-input-change", { bubbles: true }));
 | 
			
		||||
    this.dispatchEvent(new CustomEvent('file-input-change', {bubbles: true, detail: {elementId, allFiles}}));
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function toDataTransfer(files) {
 | 
			
		||||
    let dataTransfer = new DataTransfer();
 | 
			
		||||
    files.forEach(file => dataTransfer.items.add(file));
 | 
			
		||||
    files.forEach((file) => dataTransfer.items.add(file));
 | 
			
		||||
    return dataTransfer;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -136,7 +148,7 @@ function setupFileInput(chooser) {
 | 
			
		||||
 | 
			
		||||
    const filesInfo = files.map((f) => ({name: f.name, size: f.size, uniqueId: f.uniqueId}));
 | 
			
		||||
 | 
			
		||||
    const selectedFilesContainer = $(inputContainer).siblings(".selected-files");
 | 
			
		||||
    const selectedFilesContainer = $(inputContainer).siblings('.selected-files');
 | 
			
		||||
    selectedFilesContainer.empty();
 | 
			
		||||
    filesInfo.forEach((info) => {
 | 
			
		||||
      let fileContainerClasses = 'small-file-container d-flex flex-column justify-content-center align-items-center';
 | 
			
		||||
@ -167,28 +179,26 @@ function setupFileInput(chooser) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function showOrHideSelectedFilesContainer(files) {
 | 
			
		||||
    if (files && files.length > 0)
 | 
			
		||||
      chooser.style.setProperty('--selected-files-display', 'flex');
 | 
			
		||||
    else
 | 
			
		||||
    chooser.style.setProperty('--selected-files-display', 'none');
 | 
			
		||||
    if (files && files.length > 0) chooser.style.setProperty('--selected-files-display', 'flex');
 | 
			
		||||
    else chooser.style.setProperty('--selected-files-display', 'none');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function removeFileListener(e) {
 | 
			
		||||
    const fileId = (e.target).getAttribute('data-file-id');
 | 
			
		||||
    const fileId = e.target.getAttribute('data-file-id');
 | 
			
		||||
 | 
			
		||||
    let inputElement = document.getElementById(elementId);
 | 
			
		||||
    removeFileById(fileId, inputElement);
 | 
			
		||||
 | 
			
		||||
    showOrHideSelectedFilesContainer(allFiles);
 | 
			
		||||
 | 
			
		||||
    inputElement.dispatchEvent(new CustomEvent("file-input-change", { bubbles: true }));
 | 
			
		||||
    inputElement.dispatchEvent(new CustomEvent('file-input-change', {bubbles: true}));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function removeFileById(fileId, inputElement) {
 | 
			
		||||
    let fileContainer = document.getElementById(fileId);
 | 
			
		||||
    fileContainer.remove();
 | 
			
		||||
 | 
			
		||||
    allFiles = allFiles.filter(v => v.uniqueId != fileId);
 | 
			
		||||
    allFiles = allFiles.filter((v) => v.uniqueId != fileId);
 | 
			
		||||
    let dataTransfer = toDataTransfer(allFiles);
 | 
			
		||||
 | 
			
		||||
    if (inputElement) inputElement.files = dataTransfer.files;
 | 
			
		||||
@ -207,23 +217,19 @@ function setupFileInput(chooser) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function createFileInfoContainer(info) {
 | 
			
		||||
    let fileInfoContainer = document.createElement("div");
 | 
			
		||||
    let fileInfoContainer = document.createElement('div');
 | 
			
		||||
    let fileInfoContainerClasses = 'file-info d-flex flex-column align-items-center justify-content-center';
 | 
			
		||||
 | 
			
		||||
    $(fileInfoContainer).addClass(fileInfoContainerClasses);
 | 
			
		||||
 | 
			
		||||
    $(fileInfoContainer).append(
 | 
			
		||||
      `<div title="${info.name}">${info.name}</div>`
 | 
			
		||||
    );
 | 
			
		||||
    $(fileInfoContainer).append(`<div title="${info.name}">${info.name}</div>`);
 | 
			
		||||
    let fileSizeWithUnits = FileUtils.transformFileSize(info.size);
 | 
			
		||||
    $(fileInfoContainer).append(
 | 
			
		||||
      `<div title="${info.size}">${fileSizeWithUnits}</div>`
 | 
			
		||||
    );
 | 
			
		||||
    $(fileInfoContainer).append(`<div title="${info.size}">${fileSizeWithUnits}</div>`);
 | 
			
		||||
    return fileInfoContainer;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  //Listen for event of file being removed and the filter it out of the allFiles array
 | 
			
		||||
  document.addEventListener("fileRemoved", function (e) {
 | 
			
		||||
  document.addEventListener('fileRemoved', function (e) {
 | 
			
		||||
    const fileId = e.detail;
 | 
			
		||||
    let inputElement = document.getElementById(elementId);
 | 
			
		||||
    removeFileById(fileId, inputElement);
 | 
			
		||||
 | 
			
		||||
@ -268,7 +268,7 @@ document.addEventListener("DOMContentLoaded", function () {
 | 
			
		||||
    const parent = header.parentNode;
 | 
			
		||||
    const container = header.parentNode.querySelector(".feature-group-container");
 | 
			
		||||
    if (parent.id !== "groupFavorites") {
 | 
			
		||||
      container.style.maxHeight = container.clientHeight + "px";
 | 
			
		||||
      container.style.maxHeight = container.scrollHeight + "px";
 | 
			
		||||
    }
 | 
			
		||||
    header.onclick = () => {
 | 
			
		||||
      expandCollapseToggle(parent);
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ import {SplitAllCommand} from './commands/split.js';
 | 
			
		||||
import {UndoManager} from './UndoManager.js';
 | 
			
		||||
import {PageBreakCommand} from './commands/page-break.js';
 | 
			
		||||
import {AddFilesCommand} from './commands/add-page.js';
 | 
			
		||||
import {DecryptFile} from '../DecryptFiles.js';
 | 
			
		||||
 | 
			
		||||
class PdfContainer {
 | 
			
		||||
  fileName;
 | 
			
		||||
@ -40,6 +41,8 @@ class PdfContainer {
 | 
			
		||||
    this.removeAllElements = this.removeAllElements.bind(this);
 | 
			
		||||
    this.resetPages = this.resetPages.bind(this);
 | 
			
		||||
 | 
			
		||||
    this.decryptFile = new DecryptFile();
 | 
			
		||||
 | 
			
		||||
    this.undoManager = undoManager || new UndoManager();
 | 
			
		||||
 | 
			
		||||
    this.pdfAdapters = pdfAdapters;
 | 
			
		||||
@ -165,7 +168,6 @@ class PdfContainer {
 | 
			
		||||
      input.click();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async addFilesFromFiles(files, nextSiblingElement, pages) {
 | 
			
		||||
    this.fileName = files[0].name;
 | 
			
		||||
    for (var i = 0; i < files.length; i++) {
 | 
			
		||||
@ -173,17 +175,37 @@ class PdfContainer {
 | 
			
		||||
      let processingTime,
 | 
			
		||||
        errorMessage = null,
 | 
			
		||||
        pageCount = 0;
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const file = files[i];
 | 
			
		||||
        if (file.type === 'application/pdf') {
 | 
			
		||||
          const {renderer, pdfDocument} = await this.loadFile(file);
 | 
			
		||||
        let decryptedFile = files[i];
 | 
			
		||||
        let isEncrypted = false;
 | 
			
		||||
        let requiresPassword = false;
 | 
			
		||||
        await this.decryptFile
 | 
			
		||||
          .checkFileEncrypted(decryptedFile)
 | 
			
		||||
          .then((result) => {
 | 
			
		||||
            isEncrypted = result.isEncrypted;
 | 
			
		||||
            requiresPassword = result.requiresPassword;
 | 
			
		||||
          })
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
            console.error(error);
 | 
			
		||||
          });
 | 
			
		||||
        if (decryptedFile.type === 'application/pdf' && isEncrypted) {
 | 
			
		||||
          decryptedFile = await this.decryptFile.decryptFile(decryptedFile, requiresPassword);
 | 
			
		||||
          if (!decryptedFile) {
 | 
			
		||||
            throw new Error('File decryption failed.');
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (decryptedFile.type === 'application/pdf') {
 | 
			
		||||
          const {renderer, pdfDocument} = await this.loadFile(decryptedFile);
 | 
			
		||||
          pageCount = renderer.pageCount || 0;
 | 
			
		||||
          pages = await this.addPdfFile(renderer, pdfDocument, nextSiblingElement, pages);
 | 
			
		||||
        } else if (file.type.startsWith('image/')) {
 | 
			
		||||
          pages = await this.addImageFile(file, nextSiblingElement, pages);
 | 
			
		||||
        } else if (decryptedFile.type.startsWith('image/')) {
 | 
			
		||||
          pages = await this.addImageFile(decryptedFile, nextSiblingElement, pages);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        processingTime = Date.now() - startTime;
 | 
			
		||||
        this.captureFileProcessingEvent(true, file, processingTime, null, pageCount);
 | 
			
		||||
        this.captureFileProcessingEvent(true, decryptedFile, processingTime, null, pageCount);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        processingTime = Date.now() - startTime;
 | 
			
		||||
        errorMessage = error.message || 'Unknown error';
 | 
			
		||||
@ -194,6 +216,7 @@ class PdfContainer {
 | 
			
		||||
    document.querySelectorAll('.enable-on-file').forEach((element) => {
 | 
			
		||||
      element.disabled = false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return pages;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										47
									
								
								src/main/resources/static/js/pages/add-image.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/main/resources/static/js/pages/add-image.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
			
		||||
document.getElementById('download-pdf').addEventListener('click', async () => {
 | 
			
		||||
  const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
 | 
			
		||||
  const modifiedPdfBytes = await modifiedPdf.save();
 | 
			
		||||
  const blob = new Blob([modifiedPdfBytes], {type: 'application/pdf'});
 | 
			
		||||
  const link = document.createElement('a');
 | 
			
		||||
  link.href = URL.createObjectURL(blob);
 | 
			
		||||
  link.download = originalFileName + '_addedImage.pdf';
 | 
			
		||||
  link.click();
 | 
			
		||||
});
 | 
			
		||||
let originalFileName = '';
 | 
			
		||||
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
 | 
			
		||||
  const fileInput = event.target;
 | 
			
		||||
  fileInput.addEventListener('file-input-change', async (e) => {
 | 
			
		||||
    const {allFiles} = e.detail;
 | 
			
		||||
    if (allFiles && allFiles.length > 0) {
 | 
			
		||||
      const file = allFiles[0];
 | 
			
		||||
      originalFileName = file.name.replace(/\.[^/.]+$/, '');
 | 
			
		||||
      const pdfData = await file.arrayBuffer();
 | 
			
		||||
      pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
 | 
			
		||||
      const pdfDoc = await pdfjsLib.getDocument({data: pdfData}).promise;
 | 
			
		||||
      await DraggableUtils.renderPage(pdfDoc, 0);
 | 
			
		||||
 | 
			
		||||
      document.querySelectorAll('.show-on-file-selected').forEach((el) => {
 | 
			
		||||
        el.style.cssText = '';
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
  document.querySelectorAll('.show-on-file-selected').forEach((el) => {
 | 
			
		||||
    el.style.cssText = 'display:none !important';
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const imageUpload = document.querySelector('input[name=image-upload]');
 | 
			
		||||
imageUpload.addEventListener('change', (e) => {
 | 
			
		||||
  if (!e.target.files) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  for (const imageFile of e.target.files) {
 | 
			
		||||
    var reader = new FileReader();
 | 
			
		||||
    reader.readAsDataURL(imageFile);
 | 
			
		||||
    reader.onloadend = function (e) {
 | 
			
		||||
      DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										253
									
								
								src/main/resources/static/js/pages/adjust-contrast.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										253
									
								
								src/main/resources/static/js/pages/adjust-contrast.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,253 @@
 | 
			
		||||
var canvas = document.getElementById('contrast-pdf-canvas');
 | 
			
		||||
var context = canvas.getContext('2d');
 | 
			
		||||
var originalImageData = null;
 | 
			
		||||
var allPages = [];
 | 
			
		||||
var pdfDoc = null;
 | 
			
		||||
var pdf = null; // This is the current PDF document
 | 
			
		||||
 | 
			
		||||
async function renderPDFAndSaveOriginalImageData(file) {
 | 
			
		||||
  var fileReader = new FileReader();
 | 
			
		||||
  fileReader.onload = async function () {
 | 
			
		||||
    var data = new Uint8Array(this.result);
 | 
			
		||||
    pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
 | 
			
		||||
    pdf = await pdfjsLib.getDocument({data: data}).promise;
 | 
			
		||||
 | 
			
		||||
    // Get the number of pages in the PDF
 | 
			
		||||
    var numPages = pdf.numPages;
 | 
			
		||||
    allPages = Array.from({length: numPages}, (_, i) => i + 1);
 | 
			
		||||
 | 
			
		||||
    // Create a new PDF document
 | 
			
		||||
    pdfDoc = await PDFLib.PDFDocument.create();
 | 
			
		||||
    // Render the first page in the viewer
 | 
			
		||||
    await renderPageAndAdjustImageProperties(1);
 | 
			
		||||
    document.getElementById('sliders-container').style.display = 'block';
 | 
			
		||||
  };
 | 
			
		||||
  fileReader.readAsArrayBuffer(file);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This function is now async and returns a promise
 | 
			
		||||
function renderPageAndAdjustImageProperties(pageNum) {
 | 
			
		||||
  return new Promise(async function (resolve, reject) {
 | 
			
		||||
    var page = await pdf.getPage(pageNum);
 | 
			
		||||
    var scale = 1.5;
 | 
			
		||||
    var viewport = page.getViewport({scale: scale});
 | 
			
		||||
 | 
			
		||||
    canvas.height = viewport.height;
 | 
			
		||||
    canvas.width = viewport.width;
 | 
			
		||||
 | 
			
		||||
    var renderContext = {
 | 
			
		||||
      canvasContext: context,
 | 
			
		||||
      viewport: viewport,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    var renderTask = page.render(renderContext);
 | 
			
		||||
    renderTask.promise.then(function () {
 | 
			
		||||
      originalImageData = context.getImageData(0, 0, canvas.width, canvas.height);
 | 
			
		||||
      adjustImageProperties();
 | 
			
		||||
      resolve();
 | 
			
		||||
    });
 | 
			
		||||
    canvas.classList.add('fixed-shadow-canvas');
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function adjustImageProperties() {
 | 
			
		||||
  var contrast = parseFloat(document.getElementById('contrast-slider').value);
 | 
			
		||||
  var brightness = parseFloat(document.getElementById('brightness-slider').value);
 | 
			
		||||
  var saturation = parseFloat(document.getElementById('saturation-slider').value);
 | 
			
		||||
 | 
			
		||||
  contrast /= 100; // normalize to range [0, 2]
 | 
			
		||||
  brightness /= 100; // normalize to range [0, 2]
 | 
			
		||||
  saturation /= 100; // normalize to range [0, 2]
 | 
			
		||||
 | 
			
		||||
  if (originalImageData) {
 | 
			
		||||
    var newImageData = context.createImageData(originalImageData.width, originalImageData.height);
 | 
			
		||||
    newImageData.data.set(originalImageData.data);
 | 
			
		||||
 | 
			
		||||
    for (var i = 0; i < newImageData.data.length; i += 4) {
 | 
			
		||||
      var r = newImageData.data[i];
 | 
			
		||||
      var g = newImageData.data[i + 1];
 | 
			
		||||
      var b = newImageData.data[i + 2];
 | 
			
		||||
      // Adjust contrast
 | 
			
		||||
      r = adjustContrastForPixel(r, contrast);
 | 
			
		||||
      g = adjustContrastForPixel(g, contrast);
 | 
			
		||||
      b = adjustContrastForPixel(b, contrast);
 | 
			
		||||
      // Adjust brightness
 | 
			
		||||
      r = adjustBrightnessForPixel(r, brightness);
 | 
			
		||||
      g = adjustBrightnessForPixel(g, brightness);
 | 
			
		||||
      b = adjustBrightnessForPixel(b, brightness);
 | 
			
		||||
      // Adjust saturation
 | 
			
		||||
      var rgb = adjustSaturationForPixel(r, g, b, saturation);
 | 
			
		||||
      newImageData.data[i] = rgb[0];
 | 
			
		||||
      newImageData.data[i + 1] = rgb[1];
 | 
			
		||||
      newImageData.data[i + 2] = rgb[2];
 | 
			
		||||
    }
 | 
			
		||||
    context.putImageData(newImageData, 0, 0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function rgbToHsl(r, g, b) {
 | 
			
		||||
  (r /= 255), (g /= 255), (b /= 255);
 | 
			
		||||
 | 
			
		||||
  var max = Math.max(r, g, b),
 | 
			
		||||
    min = Math.min(r, g, b);
 | 
			
		||||
  var h,
 | 
			
		||||
    s,
 | 
			
		||||
    l = (max + min) / 2;
 | 
			
		||||
 | 
			
		||||
  if (max === min) {
 | 
			
		||||
    h = s = 0; // achromatic
 | 
			
		||||
  } else {
 | 
			
		||||
    var d = max - min;
 | 
			
		||||
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
 | 
			
		||||
 | 
			
		||||
    switch (max) {
 | 
			
		||||
      case r:
 | 
			
		||||
        h = (g - b) / d + (g < b ? 6 : 0);
 | 
			
		||||
        break;
 | 
			
		||||
      case g:
 | 
			
		||||
        h = (b - r) / d + 2;
 | 
			
		||||
        break;
 | 
			
		||||
      case b:
 | 
			
		||||
        h = (r - g) / d + 4;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h /= 6;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [h, s, l];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function hslToRgb(h, s, l) {
 | 
			
		||||
  var r, g, b;
 | 
			
		||||
 | 
			
		||||
  if (s === 0) {
 | 
			
		||||
    r = g = b = l; // achromatic
 | 
			
		||||
  } else {
 | 
			
		||||
    var hue2rgb = function hue2rgb(p, q, t) {
 | 
			
		||||
      if (t < 0) t += 1;
 | 
			
		||||
      if (t > 1) t -= 1;
 | 
			
		||||
      if (t < 1 / 6) return p + (q - p) * 6 * t;
 | 
			
		||||
      if (t < 1 / 2) return q;
 | 
			
		||||
      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
 | 
			
		||||
      return p;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
 | 
			
		||||
    var p = 2 * l - q;
 | 
			
		||||
 | 
			
		||||
    r = hue2rgb(p, q, h + 1 / 3);
 | 
			
		||||
    g = hue2rgb(p, q, h);
 | 
			
		||||
    b = hue2rgb(p, q, h - 1 / 3);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [r * 255, g * 255, b * 255];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function adjustContrastForPixel(pixel, contrast) {
 | 
			
		||||
  // Normalize to range [-0.5, 0.5]
 | 
			
		||||
  var normalized = pixel / 255 - 0.5;
 | 
			
		||||
 | 
			
		||||
  // Apply contrast
 | 
			
		||||
  normalized *= contrast;
 | 
			
		||||
 | 
			
		||||
  // Denormalize back to [0, 255]
 | 
			
		||||
  return (normalized + 0.5) * 255;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function clamp(value, min, max) {
 | 
			
		||||
  return Math.min(Math.max(value, min), max);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function adjustSaturationForPixel(r, g, b, saturation) {
 | 
			
		||||
  var hsl = rgbToHsl(r, g, b);
 | 
			
		||||
 | 
			
		||||
  // Adjust saturation
 | 
			
		||||
  hsl[1] = clamp(hsl[1] * saturation, 0, 1);
 | 
			
		||||
 | 
			
		||||
  // Convert back to RGB
 | 
			
		||||
  var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
 | 
			
		||||
 | 
			
		||||
  // Return adjusted RGB values
 | 
			
		||||
  return rgb;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function adjustBrightnessForPixel(pixel, brightness) {
 | 
			
		||||
  return Math.max(0, Math.min(255, pixel * brightness));
 | 
			
		||||
}
 | 
			
		||||
let inputFileName = '';
 | 
			
		||||
async function downloadPDF() {
 | 
			
		||||
  for (var i = 0; i < allPages.length; i++) {
 | 
			
		||||
    await renderPageAndAdjustImageProperties(allPages[i]);
 | 
			
		||||
    const pngImageBytes = canvas.toDataURL('image/png');
 | 
			
		||||
    const pngImage = await pdfDoc.embedPng(pngImageBytes);
 | 
			
		||||
    const pngDims = pngImage.scale(1);
 | 
			
		||||
 | 
			
		||||
    // Create a blank page matching the dimensions of the image
 | 
			
		||||
    const page = pdfDoc.addPage([pngDims.width, pngDims.height]);
 | 
			
		||||
 | 
			
		||||
    // Draw the PNG image
 | 
			
		||||
    page.drawImage(pngImage, {
 | 
			
		||||
      x: 0,
 | 
			
		||||
      y: 0,
 | 
			
		||||
      width: pngDims.width,
 | 
			
		||||
      height: pngDims.height,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Serialize the PDFDocument to bytes (a Uint8Array)
 | 
			
		||||
  const pdfBytes = await pdfDoc.save();
 | 
			
		||||
 | 
			
		||||
  // Create a Blob
 | 
			
		||||
  const blob = new Blob([pdfBytes.buffer], {type: 'application/pdf'});
 | 
			
		||||
 | 
			
		||||
  // Create download link
 | 
			
		||||
  const downloadLink = document.createElement('a');
 | 
			
		||||
  downloadLink.href = URL.createObjectURL(blob);
 | 
			
		||||
  let newFileName = inputFileName ? inputFileName.replace('.pdf', '') : 'download';
 | 
			
		||||
  newFileName += '_adjusted_color.pdf';
 | 
			
		||||
 | 
			
		||||
  downloadLink.download = newFileName;
 | 
			
		||||
  downloadLink.click();
 | 
			
		||||
 | 
			
		||||
  // After download, reset the viewer and clear stored data
 | 
			
		||||
  allPages = []; // Clear the pages
 | 
			
		||||
  originalImageData = null; // Clear the image data
 | 
			
		||||
 | 
			
		||||
  // Go back to page 1 and render it in the viewer
 | 
			
		||||
  if (pdf !== null) {
 | 
			
		||||
    renderPageAndAdjustImageProperties(1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Event listeners
 | 
			
		||||
document.getElementById('fileInput-input').addEventListener('change', function (e) {
 | 
			
		||||
  const fileInput = event.target;
 | 
			
		||||
  fileInput.addEventListener('file-input-change', async (e) => {
 | 
			
		||||
    const {allFiles} = e.detail;
 | 
			
		||||
    if (allFiles && allFiles.length > 0) {
 | 
			
		||||
      const file = allFiles[0];
 | 
			
		||||
      inputFileName = file.name;
 | 
			
		||||
      renderPDFAndSaveOriginalImageData(file);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
document.getElementById('contrast-slider').addEventListener('input', function () {
 | 
			
		||||
  document.getElementById('contrast-val').textContent = this.value;
 | 
			
		||||
  adjustImageProperties();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
document.getElementById('brightness-slider').addEventListener('input', function () {
 | 
			
		||||
  document.getElementById('brightness-val').textContent = this.value;
 | 
			
		||||
  adjustImageProperties();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
document.getElementById('saturation-slider').addEventListener('input', function () {
 | 
			
		||||
  document.getElementById('saturation-val').textContent = this.value;
 | 
			
		||||
  adjustImageProperties();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
document.getElementById('download-button').addEventListener('click', function () {
 | 
			
		||||
  downloadPDF();
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										150
									
								
								src/main/resources/static/js/pages/change-metadata.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								src/main/resources/static/js/pages/change-metadata.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,150 @@
 | 
			
		||||
const deleteAllCheckbox = document.querySelector('#deleteAll');
 | 
			
		||||
let inputs = document.querySelectorAll('input');
 | 
			
		||||
const customMetadataDiv = document.getElementById('customMetadata');
 | 
			
		||||
const otherMetadataEntriesDiv = document.getElementById('otherMetadataEntries');
 | 
			
		||||
 | 
			
		||||
deleteAllCheckbox.addEventListener('change', function (event) {
 | 
			
		||||
  inputs.forEach((input) => {
 | 
			
		||||
    // If it's the deleteAllCheckbox or any file input, skip
 | 
			
		||||
    if (input === deleteAllCheckbox || input.type === 'file') {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // Disable or enable based on the checkbox state
 | 
			
		||||
    input.disabled = deleteAllCheckbox.checked;
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const customModeCheckbox = document.getElementById('customModeCheckbox');
 | 
			
		||||
const addMetadataBtn = document.getElementById('addMetadataBtn');
 | 
			
		||||
const customMetadataFormContainer = document.getElementById('customMetadataEntries');
 | 
			
		||||
var count = 1;
 | 
			
		||||
const fileInput = document.querySelector('#fileInput-input');
 | 
			
		||||
const authorInput = document.querySelector('#author');
 | 
			
		||||
const creationDateInput = document.querySelector('#creationDate');
 | 
			
		||||
const creatorInput = document.querySelector('#creator');
 | 
			
		||||
const keywordsInput = document.querySelector('#keywords');
 | 
			
		||||
const modificationDateInput = document.querySelector('#modificationDate');
 | 
			
		||||
const producerInput = document.querySelector('#producer');
 | 
			
		||||
const subjectInput = document.querySelector('#subject');
 | 
			
		||||
const titleInput = document.querySelector('#title');
 | 
			
		||||
const trappedInput = document.querySelector('#trapped');
 | 
			
		||||
var lastPDFFileMeta = null;
 | 
			
		||||
var lastPDFFile = null;
 | 
			
		||||
 | 
			
		||||
fileInput.addEventListener('change', async function () {
 | 
			
		||||
  fileInput.addEventListener('file-input-change', async (e) => {
 | 
			
		||||
    const {allFiles} = e.detail;
 | 
			
		||||
    if (allFiles && allFiles.length > 0) {
 | 
			
		||||
      const file = allFiles[0];
 | 
			
		||||
      while (otherMetadataEntriesDiv.firstChild) {
 | 
			
		||||
        otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
 | 
			
		||||
      }
 | 
			
		||||
      while (customMetadataFormContainer.firstChild) {
 | 
			
		||||
        customMetadataFormContainer.removeChild(customMetadataFormContainer.firstChild);
 | 
			
		||||
      }
 | 
			
		||||
      var url = URL.createObjectURL(file);
 | 
			
		||||
      pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
 | 
			
		||||
      const pdf = await pdfjsLib.getDocument(url).promise;
 | 
			
		||||
      const pdfMetadata = await pdf.getMetadata();
 | 
			
		||||
      lastPDFFile = pdfMetadata?.info;
 | 
			
		||||
      console.log(pdfMetadata);
 | 
			
		||||
      if (!pdfMetadata?.info?.Custom || pdfMetadata?.info?.Custom.size == 0) {
 | 
			
		||||
        customModeCheckbox.disabled = true;
 | 
			
		||||
        customModeCheckbox.checked = false;
 | 
			
		||||
      } else {
 | 
			
		||||
        customModeCheckbox.disabled = false;
 | 
			
		||||
      }
 | 
			
		||||
      authorInput.value = pdfMetadata?.info?.Author;
 | 
			
		||||
      creationDateInput.value = convertDateFormat(pdfMetadata?.info?.CreationDate);
 | 
			
		||||
      creatorInput.value = pdfMetadata?.info?.Creator;
 | 
			
		||||
      keywordsInput.value = pdfMetadata?.info?.Keywords;
 | 
			
		||||
      modificationDateInput.value = convertDateFormat(pdfMetadata?.info?.ModDate);
 | 
			
		||||
      producerInput.value = pdfMetadata?.info?.Producer;
 | 
			
		||||
      subjectInput.value = pdfMetadata?.info?.Subject;
 | 
			
		||||
      titleInput.value = pdfMetadata?.info?.Title;
 | 
			
		||||
      console.log(pdfMetadata?.info);
 | 
			
		||||
      const trappedValue = pdfMetadata?.info?.Trapped;
 | 
			
		||||
      // Get all options in the select element
 | 
			
		||||
      const options = trappedInput.options;
 | 
			
		||||
      // Loop through all options to find the one with a matching value
 | 
			
		||||
      for (let i = 0; i < options.length; i++) {
 | 
			
		||||
        if (options[i].value === trappedValue) {
 | 
			
		||||
          options[i].selected = true;
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      addExtra();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
addMetadataBtn.addEventListener('click', () => {
 | 
			
		||||
  const keyInput = document.createElement('input');
 | 
			
		||||
  keyInput.type = 'text';
 | 
			
		||||
  keyInput.placeholder = 'Key';
 | 
			
		||||
  keyInput.className = 'form-control';
 | 
			
		||||
  keyInput.name = `allRequestParams[customKey${count}]`;
 | 
			
		||||
 | 
			
		||||
  const valueInput = document.createElement('input');
 | 
			
		||||
  valueInput.type = 'text';
 | 
			
		||||
  valueInput.placeholder = 'Value';
 | 
			
		||||
  valueInput.className = 'form-control';
 | 
			
		||||
  valueInput.name = `allRequestParams[customValue${count}]`;
 | 
			
		||||
  count = count + 1;
 | 
			
		||||
 | 
			
		||||
  const formGroup = document.createElement('div');
 | 
			
		||||
  formGroup.className = 'mb-3';
 | 
			
		||||
  formGroup.appendChild(keyInput);
 | 
			
		||||
  formGroup.appendChild(valueInput);
 | 
			
		||||
 | 
			
		||||
  customMetadataFormContainer.appendChild(formGroup);
 | 
			
		||||
});
 | 
			
		||||
function convertDateFormat(dateTimeString) {
 | 
			
		||||
  if (!dateTimeString || dateTimeString.length < 17) {
 | 
			
		||||
    return dateTimeString;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const year = dateTimeString.substring(2, 6);
 | 
			
		||||
  const month = dateTimeString.substring(6, 8);
 | 
			
		||||
  const day = dateTimeString.substring(8, 10);
 | 
			
		||||
  const hour = dateTimeString.substring(10, 12);
 | 
			
		||||
  const minute = dateTimeString.substring(12, 14);
 | 
			
		||||
  const second = dateTimeString.substring(14, 16);
 | 
			
		||||
 | 
			
		||||
  return year + '/' + month + '/' + day + ' ' + hour + ':' + minute + ':' + second;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addExtra() {
 | 
			
		||||
  const event = document.getElementById('customModeCheckbox');
 | 
			
		||||
  if (event.checked && lastPDFFile.Custom != null) {
 | 
			
		||||
    customMetadataDiv.style.display = 'block';
 | 
			
		||||
    for (const [key, value] of Object.entries(lastPDFFile.Custom)) {
 | 
			
		||||
      if (
 | 
			
		||||
        key === 'Author' ||
 | 
			
		||||
        key === 'CreationDate' ||
 | 
			
		||||
        key === 'Creator' ||
 | 
			
		||||
        key === 'Keywords' ||
 | 
			
		||||
        key === 'ModDate' ||
 | 
			
		||||
        key === 'Producer' ||
 | 
			
		||||
        key === 'Subject' ||
 | 
			
		||||
        key === 'Title' ||
 | 
			
		||||
        key === 'Trapped'
 | 
			
		||||
      ) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      const entryDiv = document.createElement('div');
 | 
			
		||||
      entryDiv.className = 'mb-3';
 | 
			
		||||
      entryDiv.innerHTML = `<div class="mb-3"><label class="form-check-label" for="${key}">${key}:</label><input name="${key}" value="${value}" type="text" class="form-control" id="${key}"></div>`;
 | 
			
		||||
      otherMetadataEntriesDiv.appendChild(entryDiv);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    customMetadataDiv.style.display = 'none';
 | 
			
		||||
    while (otherMetadataEntriesDiv.firstChild) {
 | 
			
		||||
      otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customModeCheckbox.addEventListener('change', (event) => {
 | 
			
		||||
  addExtra();
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										159
									
								
								src/main/resources/static/js/pages/crop.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/main/resources/static/js/pages/crop.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,159 @@
 | 
			
		||||
let pdfCanvas = document.getElementById('cropPdfCanvas');
 | 
			
		||||
let overlayCanvas = document.getElementById('overlayCanvas');
 | 
			
		||||
let canvasesContainer = document.getElementById('canvasesContainer');
 | 
			
		||||
canvasesContainer.style.display = 'none';
 | 
			
		||||
let containerRect = canvasesContainer.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
let context = pdfCanvas.getContext('2d');
 | 
			
		||||
let overlayContext = overlayCanvas.getContext('2d');
 | 
			
		||||
 | 
			
		||||
overlayCanvas.width = pdfCanvas.width;
 | 
			
		||||
overlayCanvas.height = pdfCanvas.height;
 | 
			
		||||
 | 
			
		||||
let isDrawing = false; // New flag to check if drawing is ongoing
 | 
			
		||||
 | 
			
		||||
let cropForm = document.getElementById('cropForm');
 | 
			
		||||
let fileInput = document.getElementById('fileInput-input');
 | 
			
		||||
let xInput = document.getElementById('x');
 | 
			
		||||
let yInput = document.getElementById('y');
 | 
			
		||||
let widthInput = document.getElementById('width');
 | 
			
		||||
let heightInput = document.getElementById('height');
 | 
			
		||||
 | 
			
		||||
let pdfDoc = null;
 | 
			
		||||
let currentPage = 1;
 | 
			
		||||
let totalPages = 0;
 | 
			
		||||
 | 
			
		||||
let startX = 0;
 | 
			
		||||
let startY = 0;
 | 
			
		||||
let rectWidth = 0;
 | 
			
		||||
let rectHeight = 0;
 | 
			
		||||
 | 
			
		||||
let pageScale = 1; // The scale which the pdf page renders
 | 
			
		||||
let timeId = null; // timeout id for resizing canvases event
 | 
			
		||||
 | 
			
		||||
function renderPageFromFile(file) {
 | 
			
		||||
  if (file.type === 'application/pdf') {
 | 
			
		||||
    let reader = new FileReader();
 | 
			
		||||
    reader.onload = function (ev) {
 | 
			
		||||
      let typedArray = new Uint8Array(reader.result);
 | 
			
		||||
      pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
 | 
			
		||||
      pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
 | 
			
		||||
        pdfDoc = pdf;
 | 
			
		||||
        totalPages = pdf.numPages;
 | 
			
		||||
        renderPage(currentPage);
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
    reader.readAsArrayBuffer(file);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.addEventListener('resize', function () {
 | 
			
		||||
  clearTimeout(timeId);
 | 
			
		||||
 | 
			
		||||
  timeId = setTimeout(function () {
 | 
			
		||||
    if (fileInput.files.length == 0) return;
 | 
			
		||||
    let canvasesContainer = document.getElementById('canvasesContainer');
 | 
			
		||||
    let containerRect = canvasesContainer.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
    context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
 | 
			
		||||
 | 
			
		||||
    overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
 | 
			
		||||
 | 
			
		||||
    pdfCanvas.width = containerRect.width;
 | 
			
		||||
    pdfCanvas.height = containerRect.height;
 | 
			
		||||
 | 
			
		||||
    overlayCanvas.width = containerRect.width;
 | 
			
		||||
    overlayCanvas.height = containerRect.height;
 | 
			
		||||
 | 
			
		||||
    let file = fileInput.files[0];
 | 
			
		||||
    renderPageFromFile(file);
 | 
			
		||||
  }, 1000);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
fileInput.addEventListener('change', function (e) {
 | 
			
		||||
  fileInput.addEventListener('file-input-change', async (e) => {
 | 
			
		||||
    const {allFiles} = e.detail;
 | 
			
		||||
    if (allFiles && allFiles.length > 0) {
 | 
			
		||||
      canvasesContainer.style.display = 'block'; // set for visual purposes
 | 
			
		||||
      let file = allFiles[0];
 | 
			
		||||
      renderPageFromFile(file);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
cropForm.addEventListener('submit', function (e) {
 | 
			
		||||
  if (xInput.value == '' && yInput.value == '' && widthInput.value == '' && heightInput.value == '') {
 | 
			
		||||
    // Ορίστε συντεταγμένες για ολόκληρη την επιφάνεια του PDF
 | 
			
		||||
    xInput.value = 0;
 | 
			
		||||
    yInput.value = 0;
 | 
			
		||||
    widthInput.value = containerRect.width;
 | 
			
		||||
    heightInput.value = containerRect.height;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
overlayCanvas.addEventListener('mousedown', function (e) {
 | 
			
		||||
  // Clear previously drawn rectangle on the main canvas
 | 
			
		||||
  context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
 | 
			
		||||
  renderPage(currentPage); // Re-render the PDF
 | 
			
		||||
 | 
			
		||||
  // Clear the overlay canvas to ensure old drawings are removed
 | 
			
		||||
  overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
 | 
			
		||||
 | 
			
		||||
  startX = e.offsetX;
 | 
			
		||||
  startY = e.offsetY;
 | 
			
		||||
  isDrawing = true;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
overlayCanvas.addEventListener('mousemove', function (e) {
 | 
			
		||||
  if (!isDrawing) return;
 | 
			
		||||
  overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear previous rectangle
 | 
			
		||||
 | 
			
		||||
  rectWidth = e.offsetX - startX;
 | 
			
		||||
  rectHeight = e.offsetY - startY;
 | 
			
		||||
  overlayContext.strokeStyle = 'red';
 | 
			
		||||
  overlayContext.strokeRect(startX, startY, rectWidth, rectHeight);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
overlayCanvas.addEventListener('mouseup', function (e) {
 | 
			
		||||
  isDrawing = false;
 | 
			
		||||
 | 
			
		||||
  rectWidth = e.offsetX - startX;
 | 
			
		||||
  rectHeight = e.offsetY - startY;
 | 
			
		||||
 | 
			
		||||
  let flippedY = pdfCanvas.height - e.offsetY;
 | 
			
		||||
 | 
			
		||||
  xInput.value = startX / pageScale;
 | 
			
		||||
  yInput.value = flippedY / pageScale;
 | 
			
		||||
  widthInput.value = rectWidth / pageScale;
 | 
			
		||||
  heightInput.value = rectHeight / pageScale;
 | 
			
		||||
 | 
			
		||||
  // Draw the final rectangle on the main canvas
 | 
			
		||||
  context.strokeStyle = 'red';
 | 
			
		||||
  context.strokeRect(startX, startY, rectWidth, rectHeight);
 | 
			
		||||
 | 
			
		||||
  overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear the overlay
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function renderPage(pageNumber) {
 | 
			
		||||
  pdfDoc.getPage(pageNumber).then(function (page) {
 | 
			
		||||
    let canvasesContainer = document.getElementById('canvasesContainer');
 | 
			
		||||
    let containerRect = canvasesContainer.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
    pageScale = containerRect.width / page.getViewport({scale: 1}).width; // The new scale
 | 
			
		||||
 | 
			
		||||
    let viewport = page.getViewport({scale: containerRect.width / page.getViewport({scale: 1}).width});
 | 
			
		||||
 | 
			
		||||
    canvasesContainer.width = viewport.width;
 | 
			
		||||
    canvasesContainer.height = viewport.height;
 | 
			
		||||
 | 
			
		||||
    pdfCanvas.width = viewport.width;
 | 
			
		||||
    pdfCanvas.height = viewport.height;
 | 
			
		||||
 | 
			
		||||
    overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
 | 
			
		||||
    overlayCanvas.height = viewport.height;
 | 
			
		||||
 | 
			
		||||
    let renderContext = {canvasContext: context, viewport: viewport};
 | 
			
		||||
    page.render(renderContext);
 | 
			
		||||
    pdfCanvas.classList.add('shadow-canvas');
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										138
									
								
								src/main/resources/static/js/pages/pdf-to-csv.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								src/main/resources/static/js/pages/pdf-to-csv.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,138 @@
 | 
			
		||||
let pdfCanvas = document.getElementById('cropPdfCanvas');
 | 
			
		||||
let overlayCanvas = document.getElementById('overlayCanvas');
 | 
			
		||||
let canvasesContainer = document.getElementById('canvasesContainer');
 | 
			
		||||
canvasesContainer.style.display = 'none';
 | 
			
		||||
// let paginationBtnContainer = ;
 | 
			
		||||
 | 
			
		||||
let context = pdfCanvas.getContext('2d');
 | 
			
		||||
let overlayContext = overlayCanvas.getContext('2d');
 | 
			
		||||
 | 
			
		||||
let btn1Object = document.getElementById('previous-page-btn');
 | 
			
		||||
let btn2Object = document.getElementById('next-page-btn');
 | 
			
		||||
overlayCanvas.width = pdfCanvas.width;
 | 
			
		||||
overlayCanvas.height = pdfCanvas.height;
 | 
			
		||||
 | 
			
		||||
let fileInput = document.getElementById('fileInput-input');
 | 
			
		||||
 | 
			
		||||
let file;
 | 
			
		||||
 | 
			
		||||
let pdfDoc = null;
 | 
			
		||||
let pageId = document.getElementById('pageId');
 | 
			
		||||
let currentPage = 1;
 | 
			
		||||
let totalPages = 0;
 | 
			
		||||
 | 
			
		||||
let startX = 0;
 | 
			
		||||
let startY = 0;
 | 
			
		||||
let rectWidth = 0;
 | 
			
		||||
let rectHeight = 0;
 | 
			
		||||
 | 
			
		||||
let timeId = null; // timeout id for resizing canvases event
 | 
			
		||||
 | 
			
		||||
btn1Object.addEventListener('click', function (e) {
 | 
			
		||||
  if (currentPage !== 1) {
 | 
			
		||||
    currentPage = currentPage - 1;
 | 
			
		||||
    pageId.value = currentPage;
 | 
			
		||||
 | 
			
		||||
    if (file.type === 'application/pdf') {
 | 
			
		||||
      let reader = new FileReader();
 | 
			
		||||
      reader.onload = function (ev) {
 | 
			
		||||
        let typedArray = new Uint8Array(reader.result);
 | 
			
		||||
        pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
 | 
			
		||||
        pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
 | 
			
		||||
          pdfDoc = pdf;
 | 
			
		||||
          totalPages = pdf.numPages;
 | 
			
		||||
          renderPage(currentPage);
 | 
			
		||||
        });
 | 
			
		||||
      };
 | 
			
		||||
      reader.readAsArrayBuffer(file);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
btn2Object.addEventListener('click', function (e) {
 | 
			
		||||
  if (currentPage !== totalPages) {
 | 
			
		||||
    currentPage = currentPage + 1;
 | 
			
		||||
    pageId.value = currentPage;
 | 
			
		||||
 | 
			
		||||
    if (file.type === 'application/pdf') {
 | 
			
		||||
      let reader = new FileReader();
 | 
			
		||||
      reader.onload = function (ev) {
 | 
			
		||||
        let typedArray = new Uint8Array(reader.result);
 | 
			
		||||
        pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
 | 
			
		||||
        pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
 | 
			
		||||
          pdfDoc = pdf;
 | 
			
		||||
          totalPages = pdf.numPages;
 | 
			
		||||
          renderPage(currentPage);
 | 
			
		||||
        });
 | 
			
		||||
      };
 | 
			
		||||
      reader.readAsArrayBuffer(file);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function renderPageFromFile(file) {
 | 
			
		||||
  if (file.type === 'application/pdf') {
 | 
			
		||||
    let reader = new FileReader();
 | 
			
		||||
    reader.onload = function (ev) {
 | 
			
		||||
      let typedArray = new Uint8Array(reader.result);
 | 
			
		||||
      pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
 | 
			
		||||
      pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
 | 
			
		||||
        pdfDoc = pdf;
 | 
			
		||||
        totalPages = pdf.numPages;
 | 
			
		||||
        renderPage(currentPage);
 | 
			
		||||
      });
 | 
			
		||||
      pageId.value = currentPage;
 | 
			
		||||
    };
 | 
			
		||||
    reader.readAsArrayBuffer(file);
 | 
			
		||||
    document.getElementById('pagination-button-container').style.display = 'flex';
 | 
			
		||||
    document.getElementById('instruction-text').style.display = 'block';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.addEventListener('resize', function () {
 | 
			
		||||
  clearTimeout(timeId);
 | 
			
		||||
  timeId = setTimeout(function () {
 | 
			
		||||
    if (fileInput.files.length == 0) return;
 | 
			
		||||
    let canvasesContainer = document.getElementById('canvasesContainer');
 | 
			
		||||
    let containerRect = canvasesContainer.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
    context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
 | 
			
		||||
 | 
			
		||||
    overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
 | 
			
		||||
 | 
			
		||||
    pdfCanvas.width = containerRect.width;
 | 
			
		||||
    pdfCanvas.height = containerRect.height;
 | 
			
		||||
 | 
			
		||||
    overlayCanvas.width = containerRect.width;
 | 
			
		||||
    overlayCanvas.height = containerRect.height;
 | 
			
		||||
 | 
			
		||||
    let file = fileInput.files[0];
 | 
			
		||||
    renderPageFromFile(file);
 | 
			
		||||
  }, 1000);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
fileInput.addEventListener('change', function (e) {
 | 
			
		||||
  fileInput.addEventListener('file-input-change', async (e) => {
 | 
			
		||||
    const {allFiles} = e.detail;
 | 
			
		||||
    if (allFiles && allFiles.length > 0) {
 | 
			
		||||
      canvasesContainer.style.display = 'block'; // set for visual purposes
 | 
			
		||||
      file = e.target.files[0];
 | 
			
		||||
      renderPageFromFile(file);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function renderPage(pageNumber) {
 | 
			
		||||
  pdfDoc.getPage(pageNumber).then(function (page) {
 | 
			
		||||
    let viewport = page.getViewport({scale: 1.0});
 | 
			
		||||
    pdfCanvas.width = viewport.width;
 | 
			
		||||
    pdfCanvas.height = viewport.height;
 | 
			
		||||
 | 
			
		||||
    overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
 | 
			
		||||
    overlayCanvas.height = viewport.height;
 | 
			
		||||
 | 
			
		||||
    let renderContext = {canvasContext: context, viewport: viewport};
 | 
			
		||||
    page.render(renderContext);
 | 
			
		||||
    pdfCanvas.classList.add('shadow-canvas');
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										213
									
								
								src/main/resources/static/js/pages/sign.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								src/main/resources/static/js/pages/sign.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,213 @@
 | 
			
		||||
window.toggleSignatureView = toggleSignatureView;
 | 
			
		||||
window.previewSignature = previewSignature;
 | 
			
		||||
window.addSignatureFromPreview = addSignatureFromPreview;
 | 
			
		||||
window.addDraggableFromPad = addDraggableFromPad;
 | 
			
		||||
window.addDraggableFromText = addDraggableFromText;
 | 
			
		||||
window.goToFirstOrLastPage = goToFirstOrLastPage;
 | 
			
		||||
 | 
			
		||||
let currentPreviewSrc = null;
 | 
			
		||||
 | 
			
		||||
function toggleSignatureView() {
 | 
			
		||||
  const gridView = document.getElementById('gridView');
 | 
			
		||||
  const listView = document.getElementById('listView');
 | 
			
		||||
  const gridText = document.querySelector('.grid-view-text');
 | 
			
		||||
  const listText = document.querySelector('.list-view-text');
 | 
			
		||||
 | 
			
		||||
  if (gridView.style.display !== 'none') {
 | 
			
		||||
    gridView.style.display = 'none';
 | 
			
		||||
    listView.style.display = 'block';
 | 
			
		||||
    gridText.style.display = 'none';
 | 
			
		||||
    listText.style.display = 'inline';
 | 
			
		||||
  } else {
 | 
			
		||||
    gridView.style.display = 'block';
 | 
			
		||||
    listView.style.display = 'none';
 | 
			
		||||
    gridText.style.display = 'inline';
 | 
			
		||||
    listText.style.display = 'none';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function previewSignature(element) {
 | 
			
		||||
  const src = element.dataset.src;
 | 
			
		||||
  currentPreviewSrc = src;
 | 
			
		||||
 | 
			
		||||
  const filename = element.querySelector('.signature-list-name').textContent;
 | 
			
		||||
 | 
			
		||||
  const previewImage = document.getElementById('previewImage');
 | 
			
		||||
  const previewFileName = document.getElementById('previewFileName');
 | 
			
		||||
 | 
			
		||||
  previewImage.src = src;
 | 
			
		||||
  previewFileName.textContent = filename;
 | 
			
		||||
 | 
			
		||||
  const modal = new bootstrap.Modal(document.getElementById('signaturePreview'));
 | 
			
		||||
  modal.show();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addSignatureFromPreview() {
 | 
			
		||||
  if (currentPreviewSrc) {
 | 
			
		||||
    DraggableUtils.createDraggableCanvasFromUrl(currentPreviewSrc);
 | 
			
		||||
    bootstrap.Modal.getInstance(document.getElementById('signaturePreview')).hide();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let originalFileName = '';
 | 
			
		||||
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
 | 
			
		||||
  const fileInput = event.target;
 | 
			
		||||
  fileInput.addEventListener('file-input-change', async (e) => {
 | 
			
		||||
    const {allFiles} = e.detail;
 | 
			
		||||
    if (allFiles && allFiles.length > 0) {
 | 
			
		||||
      const file = allFiles[0];
 | 
			
		||||
      originalFileName = file.name.replace(/\.[^/.]+$/, '');
 | 
			
		||||
      const pdfData = await file.arrayBuffer();
 | 
			
		||||
      pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
 | 
			
		||||
      const pdfDoc = await pdfjsLib.getDocument({data: pdfData}).promise;
 | 
			
		||||
      await DraggableUtils.renderPage(pdfDoc, 0);
 | 
			
		||||
 | 
			
		||||
      document.querySelectorAll('.show-on-file-selected').forEach((el) => {
 | 
			
		||||
        el.style.cssText = '';
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
  document.querySelectorAll('.show-on-file-selected').forEach((el) => {
 | 
			
		||||
    el.style.cssText = 'display:none !important';
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const imageUpload = document.querySelector('input[name=image-upload]');
 | 
			
		||||
imageUpload.addEventListener('change', (e) => {
 | 
			
		||||
  if (!e.target.files) return;
 | 
			
		||||
  for (const imageFile of e.target.files) {
 | 
			
		||||
    var reader = new FileReader();
 | 
			
		||||
    reader.readAsDataURL(imageFile);
 | 
			
		||||
    reader.onloadend = function (e) {
 | 
			
		||||
      DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const signaturePadCanvas = document.getElementById('drawing-pad-canvas');
 | 
			
		||||
const signaturePad = new SignaturePad(signaturePadCanvas, {
 | 
			
		||||
  minWidth: 1,
 | 
			
		||||
  maxWidth: 2,
 | 
			
		||||
  penColor: 'black',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function addDraggableFromPad() {
 | 
			
		||||
  if (signaturePad.isEmpty()) return;
 | 
			
		||||
  const startTime = Date.now();
 | 
			
		||||
  const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas);
 | 
			
		||||
  console.log(Date.now() - startTime);
 | 
			
		||||
  DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getCroppedCanvasDataUrl(canvas) {
 | 
			
		||||
  let originalCtx = canvas.getContext('2d');
 | 
			
		||||
  let originalWidth = canvas.width;
 | 
			
		||||
  let originalHeight = canvas.height;
 | 
			
		||||
  let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight);
 | 
			
		||||
 | 
			
		||||
  let minX = originalWidth + 1,
 | 
			
		||||
    maxX = -1,
 | 
			
		||||
    minY = originalHeight + 1,
 | 
			
		||||
    maxY = -1,
 | 
			
		||||
    x = 0,
 | 
			
		||||
    y = 0,
 | 
			
		||||
    currentPixelColorValueIndex;
 | 
			
		||||
 | 
			
		||||
  for (y = 0; y < originalHeight; y++) {
 | 
			
		||||
    for (x = 0; x < originalWidth; x++) {
 | 
			
		||||
      currentPixelColorValueIndex = (y * originalWidth + x) * 4;
 | 
			
		||||
      let currentPixelAlphaValue = imageData.data[currentPixelColorValueIndex + 3];
 | 
			
		||||
      if (currentPixelAlphaValue > 0) {
 | 
			
		||||
        if (minX > x) minX = x;
 | 
			
		||||
        if (maxX < x) maxX = x;
 | 
			
		||||
        if (minY > y) minY = y;
 | 
			
		||||
        if (maxY < y) maxY = y;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let croppedWidth = maxX - minX;
 | 
			
		||||
  let croppedHeight = maxY - minY;
 | 
			
		||||
  if (croppedWidth < 0 || croppedHeight < 0) return null;
 | 
			
		||||
  let cuttedImageData = originalCtx.getImageData(minX, minY, croppedWidth, croppedHeight);
 | 
			
		||||
 | 
			
		||||
  let croppedCanvas = document.createElement('canvas'),
 | 
			
		||||
    croppedCtx = croppedCanvas.getContext('2d');
 | 
			
		||||
 | 
			
		||||
  croppedCanvas.width = croppedWidth;
 | 
			
		||||
  croppedCanvas.height = croppedHeight;
 | 
			
		||||
  croppedCtx.putImageData(cuttedImageData, 0, 0);
 | 
			
		||||
 | 
			
		||||
  return croppedCanvas.toDataURL();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function resizeCanvas() {
 | 
			
		||||
  var ratio = Math.max(window.devicePixelRatio || 1, 1);
 | 
			
		||||
  var additionalFactor = 10;
 | 
			
		||||
 | 
			
		||||
  signaturePadCanvas.width = signaturePadCanvas.offsetWidth * ratio * additionalFactor;
 | 
			
		||||
  signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio * additionalFactor;
 | 
			
		||||
  signaturePadCanvas.getContext('2d').scale(ratio * additionalFactor, ratio * additionalFactor);
 | 
			
		||||
 | 
			
		||||
  signaturePad.clear();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
new IntersectionObserver((entries, observer) => {
 | 
			
		||||
  if (entries.some((entry) => entry.intersectionRatio > 0)) {
 | 
			
		||||
    resizeCanvas();
 | 
			
		||||
  }
 | 
			
		||||
}).observe(signaturePadCanvas);
 | 
			
		||||
 | 
			
		||||
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
 | 
			
		||||
 | 
			
		||||
function addDraggableFromText() {
 | 
			
		||||
  const sigText = document.getElementById('sigText').value;
 | 
			
		||||
  const font = document.querySelector('select[name=font]').value;
 | 
			
		||||
  const fontSize = 100;
 | 
			
		||||
 | 
			
		||||
  const canvas = document.createElement('canvas');
 | 
			
		||||
  const ctx = canvas.getContext('2d');
 | 
			
		||||
  ctx.font = `${fontSize}px ${font}`;
 | 
			
		||||
  const textWidth = ctx.measureText(sigText).width;
 | 
			
		||||
  const textHeight = fontSize;
 | 
			
		||||
 | 
			
		||||
  let paragraphs = sigText.split(/\r?\n/);
 | 
			
		||||
 | 
			
		||||
  canvas.width = textWidth;
 | 
			
		||||
  canvas.height = paragraphs.length * textHeight * 1.35; // for tails
 | 
			
		||||
  ctx.font = `${fontSize}px ${font}`;
 | 
			
		||||
 | 
			
		||||
  ctx.textBaseline = 'top';
 | 
			
		||||
 | 
			
		||||
  let y = 0;
 | 
			
		||||
 | 
			
		||||
  paragraphs.forEach((paragraph) => {
 | 
			
		||||
    ctx.fillText(paragraph, 0, y);
 | 
			
		||||
    y += fontSize;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const dataURL = canvas.toDataURL();
 | 
			
		||||
  DraggableUtils.createDraggableCanvasFromUrl(dataURL);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function goToFirstOrLastPage(page) {
 | 
			
		||||
  if (page) {
 | 
			
		||||
    const lastPage = DraggableUtils.pdfDoc.numPages;
 | 
			
		||||
    await DraggableUtils.goToPage(lastPage - 1);
 | 
			
		||||
  } else {
 | 
			
		||||
    await DraggableUtils.goToPage(0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
document.getElementById('download-pdf').addEventListener('click', async () => {
 | 
			
		||||
  const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
 | 
			
		||||
  const modifiedPdfBytes = await modifiedPdf.save();
 | 
			
		||||
  const blob = new Blob([modifiedPdfBytes], {type: 'application/pdf'});
 | 
			
		||||
  const link = document.createElement('a');
 | 
			
		||||
  link.href = URL.createObjectURL(blob);
 | 
			
		||||
  link.download = originalFileName + '_signed.pdf';
 | 
			
		||||
  link.click();
 | 
			
		||||
});
 | 
			
		||||
@ -34,140 +34,8 @@
 | 
			
		||||
                <canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2; width: 100%"></canvas>
 | 
			
		||||
              </div>
 | 
			
		||||
              <script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
 | 
			
		||||
              <script>
 | 
			
		||||
                let pdfCanvas  = document.getElementById('cropPdfCanvas');
 | 
			
		||||
                let overlayCanvas = document.getElementById('overlayCanvas');
 | 
			
		||||
                let canvasesContainer = document.getElementById('canvasesContainer');
 | 
			
		||||
                canvasesContainer.style.display = "none";
 | 
			
		||||
                // let paginationBtnContainer = ;
 | 
			
		||||
 | 
			
		||||
                let context = pdfCanvas.getContext('2d');
 | 
			
		||||
                let overlayContext = overlayCanvas.getContext('2d');
 | 
			
		||||
 | 
			
		||||
                let btn1Object = document.getElementById('previous-page-btn');
 | 
			
		||||
                let btn2Object = document.getElementById('next-page-btn');
 | 
			
		||||
                overlayCanvas.width = pdfCanvas.width;
 | 
			
		||||
                overlayCanvas.height = pdfCanvas.height;
 | 
			
		||||
 | 
			
		||||
                let fileInput = document.getElementById('fileInput-input');
 | 
			
		||||
 | 
			
		||||
                let file;
 | 
			
		||||
 | 
			
		||||
                let pdfDoc = null;
 | 
			
		||||
                let pageId = document.getElementById('pageId');
 | 
			
		||||
                let currentPage = 1;
 | 
			
		||||
                let totalPages = 0;
 | 
			
		||||
 | 
			
		||||
                let startX = 0;
 | 
			
		||||
                let startY = 0;
 | 
			
		||||
                let rectWidth = 0;
 | 
			
		||||
                let rectHeight = 0;
 | 
			
		||||
 | 
			
		||||
                let timeId = null; // timeout id for resizing canvases event
 | 
			
		||||
 | 
			
		||||
                btn1Object.addEventListener('click',function (e){
 | 
			
		||||
                  if (currentPage !== 1) {
 | 
			
		||||
                    currentPage = currentPage - 1;
 | 
			
		||||
                    pageId.value = currentPage;
 | 
			
		||||
 | 
			
		||||
                    if (file.type === 'application/pdf') {
 | 
			
		||||
                      let reader = new FileReader();
 | 
			
		||||
                      reader.onload = function (ev) {
 | 
			
		||||
                        let typedArray = new Uint8Array(reader.result);
 | 
			
		||||
                        pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
 | 
			
		||||
                        pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
 | 
			
		||||
                          pdfDoc = pdf;
 | 
			
		||||
                          totalPages = pdf.numPages;
 | 
			
		||||
                          renderPage(currentPage);
 | 
			
		||||
                        });
 | 
			
		||||
                      };
 | 
			
		||||
                      reader.readAsArrayBuffer(file);
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                btn2Object.addEventListener('click',function (e){
 | 
			
		||||
                  if (currentPage !== totalPages){
 | 
			
		||||
                    currentPage=currentPage+1;
 | 
			
		||||
                    pageId.value = currentPage;
 | 
			
		||||
 | 
			
		||||
                    if (file.type === 'application/pdf') {
 | 
			
		||||
                      let reader = new FileReader();
 | 
			
		||||
                      reader.onload = function(ev) {
 | 
			
		||||
                        let typedArray = new Uint8Array(reader.result);
 | 
			
		||||
                        pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
 | 
			
		||||
                        pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
 | 
			
		||||
                          pdfDoc = pdf;
 | 
			
		||||
                          totalPages = pdf.numPages;
 | 
			
		||||
                          renderPage(currentPage);
 | 
			
		||||
                        });
 | 
			
		||||
                      };
 | 
			
		||||
                      reader.readAsArrayBuffer(file);
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                function renderPageFromFile(file) {
 | 
			
		||||
                  if (file.type === 'application/pdf') {
 | 
			
		||||
                    let reader = new FileReader();
 | 
			
		||||
                    reader.onload = function (ev) {
 | 
			
		||||
                      let typedArray = new Uint8Array(reader.result);
 | 
			
		||||
                      pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
 | 
			
		||||
                      pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
 | 
			
		||||
                        pdfDoc = pdf;
 | 
			
		||||
                        totalPages = pdf.numPages;
 | 
			
		||||
                        renderPage(currentPage);
 | 
			
		||||
                      });
 | 
			
		||||
                      pageId.value = currentPage;
 | 
			
		||||
                    };
 | 
			
		||||
                    reader.readAsArrayBuffer(file);
 | 
			
		||||
                    document.getElementById("pagination-button-container").style.display = "flex";
 | 
			
		||||
                    document.getElementById("instruction-text").style.display = "block";
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                window.addEventListener("resize", function() {
 | 
			
		||||
                  clearTimeout(timeId);
 | 
			
		||||
                  timeId = setTimeout(function () {
 | 
			
		||||
                    if (fileInput.files.length == 0) return;
 | 
			
		||||
                    let canvasesContainer = document.getElementById('canvasesContainer');
 | 
			
		||||
                    let containerRect = canvasesContainer.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
                    context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
 | 
			
		||||
 | 
			
		||||
                    overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
 | 
			
		||||
 | 
			
		||||
                    pdfCanvas.width = containerRect.width;
 | 
			
		||||
                    pdfCanvas.height = containerRect.height;
 | 
			
		||||
 | 
			
		||||
                    overlayCanvas.width = containerRect.width;
 | 
			
		||||
                    overlayCanvas.height = containerRect.height;
 | 
			
		||||
 | 
			
		||||
                    let file = fileInput.files[0];
 | 
			
		||||
                    renderPageFromFile(file);
 | 
			
		||||
                  }, 1000);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                fileInput.addEventListener('change', function(e) {
 | 
			
		||||
                  canvasesContainer.style.display = "block"; // set for visual purposes
 | 
			
		||||
                  file = e.target.files[0];
 | 
			
		||||
                  renderPageFromFile(file);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                function renderPage(pageNumber) {
 | 
			
		||||
                  pdfDoc.getPage(pageNumber).then(function(page) {
 | 
			
		||||
                    let viewport = page.getViewport({ scale: 1.0 });
 | 
			
		||||
                    pdfCanvas.width = viewport.width;
 | 
			
		||||
                    pdfCanvas.height = viewport.height;
 | 
			
		||||
 | 
			
		||||
                    overlayCanvas.width = viewport.width;  // Match overlay canvas size with PDF canvas
 | 
			
		||||
                    overlayCanvas.height = viewport.height;
 | 
			
		||||
 | 
			
		||||
                    let renderContext = { canvasContext: context, viewport: viewport };
 | 
			
		||||
                    page.render(renderContext);
 | 
			
		||||
                    pdfCanvas.classList.add("shadow-canvas");
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
              <script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
 | 
			
		||||
              <script type="module" th:src="@{'/js/pages/pdf-to-csv.js'}">
 | 
			
		||||
              </script>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
@ -32,163 +32,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
 | 
			
		||||
          <script>
 | 
			
		||||
            let pdfCanvas  = document.getElementById('cropPdfCanvas');
 | 
			
		||||
            let overlayCanvas = document.getElementById('overlayCanvas');
 | 
			
		||||
            let canvasesContainer = document.getElementById('canvasesContainer');
 | 
			
		||||
            canvasesContainer.style.display = "none";
 | 
			
		||||
            let containerRect = canvasesContainer.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
            let context = pdfCanvas.getContext('2d');
 | 
			
		||||
            let overlayContext = overlayCanvas.getContext('2d');
 | 
			
		||||
 | 
			
		||||
            overlayCanvas.width = pdfCanvas.width;
 | 
			
		||||
            overlayCanvas.height = pdfCanvas.height;
 | 
			
		||||
 | 
			
		||||
            let isDrawing = false; // New flag to check if drawing is ongoing
 | 
			
		||||
 | 
			
		||||
            let cropForm = document.getElementById('cropForm');
 | 
			
		||||
            let fileInput = document.getElementById('fileInput-input');
 | 
			
		||||
            let xInput = document.getElementById('x');
 | 
			
		||||
            let yInput = document.getElementById('y');
 | 
			
		||||
            let widthInput = document.getElementById('width');
 | 
			
		||||
            let heightInput = document.getElementById('height');
 | 
			
		||||
 | 
			
		||||
            let pdfDoc = null;
 | 
			
		||||
            let currentPage = 1;
 | 
			
		||||
            let totalPages = 0;
 | 
			
		||||
 | 
			
		||||
            let startX = 0;
 | 
			
		||||
            let startY = 0;
 | 
			
		||||
            let rectWidth = 0;
 | 
			
		||||
            let rectHeight = 0;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            let pageScale = 1; // The scale which the pdf page renders
 | 
			
		||||
            let timeId = null; // timeout id for resizing canvases event
 | 
			
		||||
 | 
			
		||||
            function renderPageFromFile(file) {
 | 
			
		||||
              if (file.type === 'application/pdf') {
 | 
			
		||||
                let reader = new FileReader();
 | 
			
		||||
                reader.onload = function(ev) {
 | 
			
		||||
                  let typedArray = new Uint8Array(reader.result);
 | 
			
		||||
                  pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
 | 
			
		||||
                  pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
 | 
			
		||||
                    pdfDoc = pdf;
 | 
			
		||||
                    totalPages = pdf.numPages;
 | 
			
		||||
                    renderPage(currentPage);
 | 
			
		||||
                  });
 | 
			
		||||
                };
 | 
			
		||||
                reader.readAsArrayBuffer(file);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            window.addEventListener("resize", function() {
 | 
			
		||||
              clearTimeout(timeId);
 | 
			
		||||
 | 
			
		||||
              timeId = setTimeout(function () {
 | 
			
		||||
                 if (fileInput.files.length == 0) return;
 | 
			
		||||
                 let canvasesContainer = document.getElementById('canvasesContainer');
 | 
			
		||||
                 let containerRect = canvasesContainer.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
                 context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
 | 
			
		||||
 | 
			
		||||
                 overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
 | 
			
		||||
 | 
			
		||||
                 pdfCanvas.width = containerRect.width;
 | 
			
		||||
                 pdfCanvas.height = containerRect.height;
 | 
			
		||||
 | 
			
		||||
                 overlayCanvas.width = containerRect.width;
 | 
			
		||||
                 overlayCanvas.height = containerRect.height;
 | 
			
		||||
 | 
			
		||||
                 let file = fileInput.files[0];
 | 
			
		||||
                 renderPageFromFile(file);
 | 
			
		||||
             }, 1000);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            fileInput.addEventListener('change', function(e) {
 | 
			
		||||
              canvasesContainer.style.display = "block"; // set for visual purposes
 | 
			
		||||
              let file = e.target.files[0];
 | 
			
		||||
              renderPageFromFile(file);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            cropForm.addEventListener('submit', function(e) {
 | 
			
		||||
              if (xInput.value == "" && yInput.value == "" && widthInput.value == "" && heightInput.value == "") {
 | 
			
		||||
                // Ορίστε συντεταγμένες για ολόκληρη την επιφάνεια του PDF
 | 
			
		||||
                xInput.value = 0;
 | 
			
		||||
                yInput.value = 0;
 | 
			
		||||
                widthInput.value = containerRect.width;
 | 
			
		||||
                heightInput.value = containerRect.height;
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            overlayCanvas.addEventListener('mousedown', function(e) {
 | 
			
		||||
              // Clear previously drawn rectangle on the main canvas
 | 
			
		||||
              context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
 | 
			
		||||
              renderPage(currentPage); // Re-render the PDF
 | 
			
		||||
 | 
			
		||||
              // Clear the overlay canvas to ensure old drawings are removed
 | 
			
		||||
              overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
 | 
			
		||||
 | 
			
		||||
              startX = e.offsetX;
 | 
			
		||||
              startY = e.offsetY;
 | 
			
		||||
              isDrawing = true;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            overlayCanvas.addEventListener('mousemove', function(e) {
 | 
			
		||||
              if (!isDrawing) return;
 | 
			
		||||
              overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear previous rectangle
 | 
			
		||||
 | 
			
		||||
              rectWidth = e.offsetX - startX;
 | 
			
		||||
              rectHeight = e.offsetY - startY;
 | 
			
		||||
              overlayContext.strokeStyle = 'red';
 | 
			
		||||
              overlayContext.strokeRect(startX, startY, rectWidth, rectHeight);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            overlayCanvas.addEventListener('mouseup', function(e) {
 | 
			
		||||
              isDrawing = false;
 | 
			
		||||
 | 
			
		||||
              rectWidth = e.offsetX - startX;
 | 
			
		||||
              rectHeight = e.offsetY - startY;
 | 
			
		||||
 | 
			
		||||
              let flippedY = pdfCanvas.height - e.offsetY;
 | 
			
		||||
 | 
			
		||||
              xInput.value = startX / pageScale;
 | 
			
		||||
              yInput.value = flippedY / pageScale;
 | 
			
		||||
              widthInput.value = rectWidth / pageScale;
 | 
			
		||||
              heightInput.value = rectHeight /pageScale;
 | 
			
		||||
 | 
			
		||||
              // Draw the final rectangle on the main canvas
 | 
			
		||||
              context.strokeStyle = 'red';
 | 
			
		||||
              context.strokeRect(startX, startY, rectWidth, rectHeight);
 | 
			
		||||
 | 
			
		||||
              overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear the overlay
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            function renderPage(pageNumber) {
 | 
			
		||||
              pdfDoc.getPage(pageNumber).then(function(page) {
 | 
			
		||||
                let canvasesContainer = document.getElementById('canvasesContainer');
 | 
			
		||||
                let containerRect = canvasesContainer.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
                pageScale = containerRect.width / page.getViewport({ scale: 1 }).width; // The new scale
 | 
			
		||||
 | 
			
		||||
                let viewport = page.getViewport({ scale: containerRect.width / page.getViewport({ scale: 1 }).width });
 | 
			
		||||
 | 
			
		||||
                canvasesContainer.width =viewport.width;
 | 
			
		||||
                canvasesContainer.height = viewport.height;
 | 
			
		||||
 | 
			
		||||
                pdfCanvas.width = viewport.width;
 | 
			
		||||
                pdfCanvas.height = viewport.height;
 | 
			
		||||
 | 
			
		||||
                overlayCanvas.width = viewport.width;  // Match overlay canvas size with PDF canvas
 | 
			
		||||
                overlayCanvas.height = viewport.height;
 | 
			
		||||
 | 
			
		||||
                let renderContext = { canvasContext: context, viewport: viewport };
 | 
			
		||||
                page.render(renderContext);
 | 
			
		||||
                pdfCanvas.classList.add("shadow-canvas");
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
          </script>
 | 
			
		||||
          <script type="module" th:src="@{'/js/pages/crop.js'}"></script>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@
 | 
			
		||||
            <p th:text="#{error.contactTip}"></p>
 | 
			
		||||
            <div id="button-group">
 | 
			
		||||
              <a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" class="btn btn-primary" target="_blank" th:text="#{error.github}"></a>
 | 
			
		||||
              <a href="https://discord.gg/Cn8pWhQRxZ" id="discord-button" class="btn btn-primary" target="_blank" th:text="#{joinDiscord}"></a>
 | 
			
		||||
              <a href="https://discord.gg/HYmhKj45pU" id="discord-button" class="btn btn-primary" target="_blank" th:text="#{joinDiscord}"></a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <a th:href="@{'/'}" id="home-button" class="home-button btn btn-primary"  th:text="#{goHomepage}"></a>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
@ -203,7 +203,17 @@
 | 
			
		||||
                </script>
 | 
			
		||||
                <script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
 | 
			
		||||
                <script th:src="@{'/js/downloader.js'}"></script>
 | 
			
		||||
 | 
			
		||||
                <script>
 | 
			
		||||
                  window.decrypt = {
 | 
			
		||||
                    passwordPrompt: '[[#{decrypt.passwordPrompt}]]',
 | 
			
		||||
                    cancelled: '[[#{decrypt.cancelled}]]',
 | 
			
		||||
                    noPassword: '[[#{decrypt.noPassword}]]',
 | 
			
		||||
                    invalidPassword: '[[#{decrypt.invalidPassword}]]',
 | 
			
		||||
                    invalidPasswordHeader: '[[#{decrypt.invalidPasswordHeader}]]',
 | 
			
		||||
                    unexpectedError: '[[#{decrypt.unexpectedError}]]',
 | 
			
		||||
                    serverError: '[[#{decrypt.serverError}]]',
 | 
			
		||||
                    success: '[[#{decrypt.success}]]',
 | 
			
		||||
              };</script>
 | 
			
		||||
                <div class="custom-file-chooser mb-3" th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-element-container-id=${name+'-input-container'}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}">
 | 
			
		||||
                  <div class="mb-3 d-flex flex-row justify-content-center align-items-center flex-wrap input-container" th:name="${name}+'-input'" th:id="${name}+'-input-container'" th:data-text="#{fileChooser.hoveredDragAndDrop}">
 | 
			
		||||
                      <label class="file-input-btn d-none">
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@
 | 
			
		||||
      </div>
 | 
			
		||||
      <!-- Buttons to submit a ticket on GitHub and join Discord server -->
 | 
			
		||||
      <a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" target="_blank" th:text="#{error.github}"></a>
 | 
			
		||||
      <a href="https://discord.gg/Cn8pWhQRxZ" id="discord-button" target="_blank" th:text="#{joinDiscord}"></a>
 | 
			
		||||
      <a href="https://discord.gg/HYmhKj45pU" id="discord-button" target="_blank" th:text="#{joinDiscord}"></a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <script>
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@
 | 
			
		||||
                      <p th:text="#{error.contactTip}"></p>
 | 
			
		||||
                      <div id="button-group">
 | 
			
		||||
                        <a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" target="_blank" th:text="#{error.githubSubmit}"></a>
 | 
			
		||||
                        <a href="https://discord.gg/Cn8pWhQRxZ" id="discord-button" target="_blank" th:text="#{error.discordSubmit}"></a>
 | 
			
		||||
                        <a href="https://discord.gg/HYmhKj45pU" id="discord-button" target="_blank" th:text="#{error.discordSubmit}"></a>
 | 
			
		||||
                      </div>
 | 
			
		||||
                      <a th:href="@{'/'}" class="home-button" th:text="#{goHomepage}"></a>
 | 
			
		||||
                      <a data-bs-dismiss="modal" class="home-button" th:text="#{close}"></a>
 | 
			
		||||
 | 
			
		||||
@ -482,6 +482,10 @@ document.addEventListener("DOMContentLoaded", function() {
 | 
			
		||||
        localStorage.setItem('surveyVersion', surveyVersion);
 | 
			
		||||
        modal.hide();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (localStorage.getItem('dontShowSurvey')) {
 | 
			
		||||
        modal.hide();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  </script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -22,46 +22,9 @@
 | 
			
		||||
              <!-- pdf selector -->
 | 
			
		||||
              <div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
 | 
			
		||||
              <script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
 | 
			
		||||
              <script>
 | 
			
		||||
                let originalFileName = '';
 | 
			
		||||
                document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
 | 
			
		||||
                  const file = event.target.files[0];
 | 
			
		||||
                  if (file) {
 | 
			
		||||
                    originalFileName = file.name.replace(/\.[^/.]+$/, "");
 | 
			
		||||
                    const pdfData = await file.arrayBuffer();
 | 
			
		||||
                    pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
 | 
			
		||||
                    const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
 | 
			
		||||
                    await DraggableUtils.renderPage(pdfDoc, 0);
 | 
			
		||||
 | 
			
		||||
                    document.querySelectorAll(".show-on-file-selected").forEach(el => {
 | 
			
		||||
                      el.style.cssText = '';
 | 
			
		||||
                    });
 | 
			
		||||
                  }
 | 
			
		||||
                });
 | 
			
		||||
                document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
                  document.querySelectorAll(".show-on-file-selected").forEach(el => {
 | 
			
		||||
                    el.style.cssText = "display:none !important";
 | 
			
		||||
                  })
 | 
			
		||||
                });
 | 
			
		||||
              </script>
 | 
			
		||||
 | 
			
		||||
              <script type="module" th:src="@{'/js/pages/add-image.js'}"></script>
 | 
			
		||||
              <div class="tab-group show-on-file-selected">
 | 
			
		||||
                  <div th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}"></div>
 | 
			
		||||
                  <script>
 | 
			
		||||
                    const imageUpload = document.querySelector('input[name=image-upload]');
 | 
			
		||||
                    imageUpload.addEventListener('change', e => {
 | 
			
		||||
                      if(!e.target.files) {
 | 
			
		||||
                        return;
 | 
			
		||||
                      }
 | 
			
		||||
                      for (const imageFile of e.target.files) {
 | 
			
		||||
                        var reader = new FileReader();
 | 
			
		||||
                        reader.readAsDataURL(imageFile);
 | 
			
		||||
                        reader.onloadend = function (e) {
 | 
			
		||||
                          DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
 | 
			
		||||
                        };
 | 
			
		||||
                      }
 | 
			
		||||
                    });
 | 
			
		||||
                  </script>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <!-- draggables box -->
 | 
			
		||||
@ -93,17 +56,6 @@
 | 
			
		||||
              <div class="margin-auto-parent">
 | 
			
		||||
                <button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center" th:text="#{downloadPdf}"></button>
 | 
			
		||||
              </div>
 | 
			
		||||
              <script>
 | 
			
		||||
                document.getElementById("download-pdf").addEventListener('click', async() => {
 | 
			
		||||
                  const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
 | 
			
		||||
                  const modifiedPdfBytes = await modifiedPdf.save();
 | 
			
		||||
                  const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
 | 
			
		||||
                  const link = document.createElement('a');
 | 
			
		||||
                  link.href = URL.createObjectURL(blob);
 | 
			
		||||
                  link.download = originalFileName + '_addedImage.pdf';
 | 
			
		||||
                  link.click();
 | 
			
		||||
                });
 | 
			
		||||
              </script>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
@ -55,247 +55,7 @@
 | 
			
		||||
              </form>
 | 
			
		||||
              <script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
 | 
			
		||||
              <script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
 | 
			
		||||
              <script>
 | 
			
		||||
                var canvas = document.getElementById('contrast-pdf-canvas');
 | 
			
		||||
                var context = canvas.getContext('2d');
 | 
			
		||||
                var originalImageData = null;
 | 
			
		||||
                var allPages = [];
 | 
			
		||||
                var pdfDoc = null;
 | 
			
		||||
                var pdf = null; // This is the current PDF document
 | 
			
		||||
 | 
			
		||||
                async function renderPDFAndSaveOriginalImageData(file) {
 | 
			
		||||
                  var fileReader = new FileReader();
 | 
			
		||||
                  fileReader.onload = async function() {
 | 
			
		||||
                    var data = new Uint8Array(this.result);
 | 
			
		||||
                    pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
 | 
			
		||||
                    pdf = await pdfjsLib.getDocument({data: data}).promise;
 | 
			
		||||
 | 
			
		||||
                    // Get the number of pages in the PDF
 | 
			
		||||
                    var numPages = pdf.numPages;
 | 
			
		||||
                    allPages = Array.from({length: numPages}, (_, i) => i + 1);
 | 
			
		||||
 | 
			
		||||
                    // Create a new PDF document
 | 
			
		||||
                    pdfDoc = await PDFLib.PDFDocument.create();
 | 
			
		||||
                    // Render the first page in the viewer
 | 
			
		||||
                    await renderPageAndAdjustImageProperties(1);
 | 
			
		||||
                    document.getElementById("sliders-container").style.display = "block";
 | 
			
		||||
                  };
 | 
			
		||||
                  fileReader.readAsArrayBuffer(file);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // This function is now async and returns a promise
 | 
			
		||||
                function renderPageAndAdjustImageProperties(pageNum) {
 | 
			
		||||
                  return new Promise(async function(resolve, reject) {
 | 
			
		||||
                    var page = await pdf.getPage(pageNum);
 | 
			
		||||
                    var scale = 1.5;
 | 
			
		||||
                    var viewport = page.getViewport({ scale: scale });
 | 
			
		||||
 | 
			
		||||
                    canvas.height = viewport.height;
 | 
			
		||||
                    canvas.width = viewport.width;
 | 
			
		||||
 | 
			
		||||
                    var renderContext = {
 | 
			
		||||
                      canvasContext: context,
 | 
			
		||||
                      viewport: viewport
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    var renderTask = page.render(renderContext);
 | 
			
		||||
                    renderTask.promise.then(function () {
 | 
			
		||||
                      originalImageData = context.getImageData(0, 0, canvas.width, canvas.height);
 | 
			
		||||
                      adjustImageProperties();
 | 
			
		||||
                      resolve();
 | 
			
		||||
                    });
 | 
			
		||||
                    canvas.classList.add("fixed-shadow-canvas");
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                function adjustImageProperties() {
 | 
			
		||||
                  var contrast = parseFloat(document.getElementById('contrast-slider').value);
 | 
			
		||||
                  var brightness = parseFloat(document.getElementById('brightness-slider').value);
 | 
			
		||||
                  var saturation = parseFloat(document.getElementById('saturation-slider').value);
 | 
			
		||||
 | 
			
		||||
                  contrast /= 100; // normalize to range [0, 2]
 | 
			
		||||
                  brightness /= 100; // normalize to range [0, 2]
 | 
			
		||||
                  saturation /= 100; // normalize to range [0, 2]
 | 
			
		||||
 | 
			
		||||
                  if (originalImageData) {
 | 
			
		||||
                    var newImageData = context.createImageData(originalImageData.width, originalImageData.height);
 | 
			
		||||
                    newImageData.data.set(originalImageData.data);
 | 
			
		||||
 | 
			
		||||
                    for(var i=0; i<newImageData.data.length; i+=4) {
 | 
			
		||||
                      var r = newImageData.data[i];
 | 
			
		||||
                      var g = newImageData.data[i+1];
 | 
			
		||||
                      var b = newImageData.data[i+2];
 | 
			
		||||
                      // Adjust contrast
 | 
			
		||||
                      r = adjustContrastForPixel(r, contrast);
 | 
			
		||||
                      g = adjustContrastForPixel(g, contrast);
 | 
			
		||||
                      b = adjustContrastForPixel(b, contrast);
 | 
			
		||||
                      // Adjust brightness
 | 
			
		||||
                      r = adjustBrightnessForPixel(r, brightness);
 | 
			
		||||
                      g = adjustBrightnessForPixel(g, brightness);
 | 
			
		||||
                      b = adjustBrightnessForPixel(b, brightness);
 | 
			
		||||
                      // Adjust saturation
 | 
			
		||||
                      var rgb = adjustSaturationForPixel(r, g, b, saturation);
 | 
			
		||||
                      newImageData.data[i] = rgb[0];
 | 
			
		||||
                      newImageData.data[i+1] = rgb[1];
 | 
			
		||||
                      newImageData.data[i+2] = rgb[2];
 | 
			
		||||
                    }
 | 
			
		||||
                    context.putImageData(newImageData, 0, 0);
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                function rgbToHsl(r, g, b) {
 | 
			
		||||
                  r /= 255, g /= 255, b /= 255;
 | 
			
		||||
 | 
			
		||||
                  var max = Math.max(r, g, b), min = Math.min(r, g, b);
 | 
			
		||||
                  var h, s, l = (max + min) / 2;
 | 
			
		||||
 | 
			
		||||
                  if (max === min) {
 | 
			
		||||
                    h = s = 0; // achromatic
 | 
			
		||||
                  } else {
 | 
			
		||||
                    var d = max - min;
 | 
			
		||||
                    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
 | 
			
		||||
 | 
			
		||||
                    switch (max) {
 | 
			
		||||
                      case r: h = (g - b) / d + (g < b ? 6 : 0); break;
 | 
			
		||||
                      case g: h = (b - r) / d + 2; break;
 | 
			
		||||
                      case b: h = (r - g) / d + 4; break;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    h /= 6;
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  return [h, s, l];
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                function hslToRgb(h, s, l) {
 | 
			
		||||
                  var r, g, b;
 | 
			
		||||
 | 
			
		||||
                  if (s === 0) {
 | 
			
		||||
                    r = g = b = l; // achromatic
 | 
			
		||||
                  } else {
 | 
			
		||||
                    var hue2rgb = function hue2rgb(p, q, t) {
 | 
			
		||||
                      if (t < 0) t += 1;
 | 
			
		||||
                      if (t > 1) t -= 1;
 | 
			
		||||
                      if (t < 1 / 6) return p + (q - p) * 6 * t;
 | 
			
		||||
                      if (t < 1 / 2) return q;
 | 
			
		||||
                      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
 | 
			
		||||
                      return p;
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
 | 
			
		||||
                    var p = 2 * l - q;
 | 
			
		||||
 | 
			
		||||
                    r = hue2rgb(p, q, h + 1 / 3);
 | 
			
		||||
                    g = hue2rgb(p, q, h);
 | 
			
		||||
                    b = hue2rgb(p, q, h - 1 / 3);
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  return [r * 255, g * 255, b * 255];
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                function adjustContrastForPixel(pixel, contrast) {
 | 
			
		||||
                  // Normalize to range [-0.5, 0.5]
 | 
			
		||||
                  var normalized = pixel / 255 - 0.5;
 | 
			
		||||
 | 
			
		||||
                  // Apply contrast
 | 
			
		||||
                  normalized *= contrast;
 | 
			
		||||
 | 
			
		||||
                  // Denormalize back to [0, 255]
 | 
			
		||||
                  return (normalized + 0.5) * 255;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                function clamp(value, min, max) {
 | 
			
		||||
                  return Math.min(Math.max(value, min), max);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                function adjustSaturationForPixel(r, g, b, saturation) {
 | 
			
		||||
                  var hsl = rgbToHsl(r, g, b);
 | 
			
		||||
 | 
			
		||||
                  // Adjust saturation
 | 
			
		||||
                  hsl[1] = clamp(hsl[1] * saturation, 0, 1);
 | 
			
		||||
 | 
			
		||||
                  // Convert back to RGB
 | 
			
		||||
                  var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
 | 
			
		||||
 | 
			
		||||
                  // Return adjusted RGB values
 | 
			
		||||
                  return rgb;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                function adjustBrightnessForPixel(pixel, brightness) {
 | 
			
		||||
                  return Math.max(0, Math.min(255, pixel * brightness));
 | 
			
		||||
                }
 | 
			
		||||
                let inputFileName = '';
 | 
			
		||||
                async function downloadPDF() {
 | 
			
		||||
                  for (var i = 0; i < allPages.length; i++) {
 | 
			
		||||
                    await renderPageAndAdjustImageProperties(allPages[i]);
 | 
			
		||||
                    const pngImageBytes = canvas.toDataURL('image/png');
 | 
			
		||||
                    const pngImage = await pdfDoc.embedPng(pngImageBytes);
 | 
			
		||||
                    const pngDims = pngImage.scale(1);
 | 
			
		||||
 | 
			
		||||
                    // Create a blank page matching the dimensions of the image
 | 
			
		||||
                    const page = pdfDoc.addPage([pngDims.width, pngDims.height]);
 | 
			
		||||
 | 
			
		||||
                    // Draw the PNG image
 | 
			
		||||
                    page.drawImage(pngImage, {
 | 
			
		||||
                      x: 0,
 | 
			
		||||
                      y: 0,
 | 
			
		||||
                      width: pngDims.width,
 | 
			
		||||
                      height: pngDims.height
 | 
			
		||||
                    });
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  // Serialize the PDFDocument to bytes (a Uint8Array)
 | 
			
		||||
                  const pdfBytes = await pdfDoc.save();
 | 
			
		||||
 | 
			
		||||
                  // Create a Blob
 | 
			
		||||
                  const blob = new Blob([pdfBytes.buffer], {type: "application/pdf"});
 | 
			
		||||
 | 
			
		||||
                  // Create download link
 | 
			
		||||
                  const downloadLink = document.createElement('a');
 | 
			
		||||
                  downloadLink.href = URL.createObjectURL(blob);
 | 
			
		||||
                  let newFileName = inputFileName ? inputFileName.replace('.pdf', '') : 'download';
 | 
			
		||||
                  newFileName += '_adjusted_color.pdf';
 | 
			
		||||
 | 
			
		||||
                  downloadLink.download = newFileName;
 | 
			
		||||
                  downloadLink.click();
 | 
			
		||||
 | 
			
		||||
                  // After download, reset the viewer and clear stored data
 | 
			
		||||
                  allPages = [];  // Clear the pages
 | 
			
		||||
                  originalImageData = null;  // Clear the image data
 | 
			
		||||
 | 
			
		||||
                  // Go back to page 1 and render it in the viewer
 | 
			
		||||
                  if (pdf !== null) {
 | 
			
		||||
                    renderPageAndAdjustImageProperties(1);
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Event listeners
 | 
			
		||||
                document.getElementById('fileInput-input').addEventListener('change', function(e) {
 | 
			
		||||
                  if (e.target.files.length > 0) {
 | 
			
		||||
                    inputFileName = e.target.files[0].name;
 | 
			
		||||
                    renderPDFAndSaveOriginalImageData(e.target.files[0]);
 | 
			
		||||
                  }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                document.getElementById('contrast-slider').addEventListener('input', function() {
 | 
			
		||||
                  document.getElementById('contrast-val').textContent = this.value;
 | 
			
		||||
                  adjustImageProperties();
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                document.getElementById('brightness-slider').addEventListener('input', function() {
 | 
			
		||||
                  document.getElementById('brightness-val').textContent = this.value;
 | 
			
		||||
                  adjustImageProperties();
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                document.getElementById('saturation-slider').addEventListener('input', function() {
 | 
			
		||||
                  document.getElementById('saturation-val').textContent = this.value;
 | 
			
		||||
                  adjustImageProperties();
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                document.getElementById('download-button').addEventListener('click', function() {
 | 
			
		||||
                  downloadPDF();
 | 
			
		||||
                });
 | 
			
		||||
              </script>
 | 
			
		||||
              <script type="module" th:src="@{'/js/pages/adjust-contrast.js'}"></script>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
@ -85,140 +85,7 @@
 | 
			
		||||
                <br>
 | 
			
		||||
                <button class="btn btn-primary" type="submit" id="submitBtn" th:text="#{changeMetadata.submit}"></button>
 | 
			
		||||
                <script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
 | 
			
		||||
                <script>
 | 
			
		||||
                  const deleteAllCheckbox = document.querySelector("#deleteAll");
 | 
			
		||||
                  let inputs = document.querySelectorAll("input");
 | 
			
		||||
                  const customMetadataDiv = document.getElementById('customMetadata');
 | 
			
		||||
                  const otherMetadataEntriesDiv = document.getElementById('otherMetadataEntries');
 | 
			
		||||
 | 
			
		||||
                  deleteAllCheckbox.addEventListener("change", function(event) {
 | 
			
		||||
                    inputs.forEach(input => {
 | 
			
		||||
                      // If it's the deleteAllCheckbox or any file input, skip
 | 
			
		||||
                      if (input === deleteAllCheckbox || input.type === "file") {
 | 
			
		||||
                        return;
 | 
			
		||||
                      }
 | 
			
		||||
                      // Disable or enable based on the checkbox state
 | 
			
		||||
                      input.disabled = deleteAllCheckbox.checked;
 | 
			
		||||
                    });
 | 
			
		||||
                  });
 | 
			
		||||
 | 
			
		||||
                  const customModeCheckbox = document.getElementById('customModeCheckbox');
 | 
			
		||||
                  const addMetadataBtn = document.getElementById("addMetadataBtn");
 | 
			
		||||
                  const customMetadataFormContainer = document.getElementById("customMetadataEntries");
 | 
			
		||||
                  var count = 1;
 | 
			
		||||
                  const fileInput = document.querySelector("#fileInput-input");
 | 
			
		||||
                  const authorInput = document.querySelector("#author");
 | 
			
		||||
                  const creationDateInput = document.querySelector("#creationDate");
 | 
			
		||||
                  const creatorInput = document.querySelector("#creator");
 | 
			
		||||
                  const keywordsInput = document.querySelector("#keywords");
 | 
			
		||||
                  const modificationDateInput = document.querySelector("#modificationDate");
 | 
			
		||||
                  const producerInput = document.querySelector("#producer");
 | 
			
		||||
                  const subjectInput = document.querySelector("#subject");
 | 
			
		||||
                  const titleInput = document.querySelector("#title");
 | 
			
		||||
                  const trappedInput = document.querySelector("#trapped");
 | 
			
		||||
                  var lastPDFFileMeta = null;
 | 
			
		||||
                  fileInput.addEventListener("change", async function() {
 | 
			
		||||
                    while (otherMetadataEntriesDiv.firstChild) {
 | 
			
		||||
                      otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
 | 
			
		||||
                    }
 | 
			
		||||
                    while (customMetadataFormContainer.firstChild) {
 | 
			
		||||
                      customMetadataFormContainer.removeChild(customMetadataFormContainer.firstChild);
 | 
			
		||||
                    }
 | 
			
		||||
                    const file = this.files[0];
 | 
			
		||||
                    var url = URL.createObjectURL(file)
 | 
			
		||||
                    pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
 | 
			
		||||
                    const pdf = await pdfjsLib.getDocument(url).promise;
 | 
			
		||||
                    const pdfMetadata = await pdf.getMetadata();
 | 
			
		||||
                    lastPDFFile = pdfMetadata?.info
 | 
			
		||||
                    console.log(pdfMetadata);
 | 
			
		||||
                    if(!pdfMetadata?.info?.Custom || pdfMetadata?.info?.Custom.size == 0) {
 | 
			
		||||
                      customModeCheckbox.disabled = true;
 | 
			
		||||
                      customModeCheckbox.checked = false;
 | 
			
		||||
                    } else {
 | 
			
		||||
                      customModeCheckbox.disabled = false;
 | 
			
		||||
                    }
 | 
			
		||||
                    authorInput.value = pdfMetadata?.info?.Author;
 | 
			
		||||
                    creationDateInput.value = convertDateFormat(pdfMetadata?.info?.CreationDate);
 | 
			
		||||
                    creatorInput.value = pdfMetadata?.info?.Creator;
 | 
			
		||||
                    keywordsInput.value = pdfMetadata?.info?.Keywords;
 | 
			
		||||
                    modificationDateInput.value = convertDateFormat(pdfMetadata?.info?.ModDate);
 | 
			
		||||
                    producerInput.value = pdfMetadata?.info?.Producer;
 | 
			
		||||
                    subjectInput.value = pdfMetadata?.info?.Subject;
 | 
			
		||||
                    titleInput.value = pdfMetadata?.info?.Title;
 | 
			
		||||
                    console.log(pdfMetadata?.info);
 | 
			
		||||
                    const trappedValue = pdfMetadata?.info?.Trapped;
 | 
			
		||||
                    // Get all options in the select element
 | 
			
		||||
                    const options = trappedInput.options;
 | 
			
		||||
                    // Loop through all options to find the one with a matching value
 | 
			
		||||
                    for (let i = 0; i < options.length; i++) {
 | 
			
		||||
                      if (options[i].value === trappedValue) {
 | 
			
		||||
                        options[i].selected = true;
 | 
			
		||||
                        break;
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                    addExtra();
 | 
			
		||||
                  });
 | 
			
		||||
 | 
			
		||||
                  addMetadataBtn.addEventListener("click", () => {
 | 
			
		||||
                    const keyInput = document.createElement("input");
 | 
			
		||||
                    keyInput.type = "text";
 | 
			
		||||
                    keyInput.placeholder = 'Key';
 | 
			
		||||
                    keyInput.className = "form-control";
 | 
			
		||||
                    keyInput.name = `allRequestParams[customKey${count}]`;
 | 
			
		||||
 | 
			
		||||
                    const valueInput = document.createElement("input");
 | 
			
		||||
                    valueInput.type = "text";
 | 
			
		||||
                    valueInput.placeholder = 'Value';
 | 
			
		||||
                    valueInput.className = "form-control";
 | 
			
		||||
                    valueInput.name = `allRequestParams[customValue${count}]`;
 | 
			
		||||
                    count = count + 1;
 | 
			
		||||
 | 
			
		||||
                    const formGroup = document.createElement("div");
 | 
			
		||||
                    formGroup.className = "mb-3";
 | 
			
		||||
                    formGroup.appendChild(keyInput);
 | 
			
		||||
                    formGroup.appendChild(valueInput);
 | 
			
		||||
 | 
			
		||||
                    customMetadataFormContainer.appendChild(formGroup);
 | 
			
		||||
                  });
 | 
			
		||||
                  function convertDateFormat(dateTimeString) {
 | 
			
		||||
                    if (!dateTimeString || dateTimeString.length < 17) {
 | 
			
		||||
                      return dateTimeString;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    const year = dateTimeString.substring(2, 6);
 | 
			
		||||
                    const month = dateTimeString.substring(6, 8);
 | 
			
		||||
                    const day = dateTimeString.substring(8, 10);
 | 
			
		||||
                    const hour = dateTimeString.substring(10, 12);
 | 
			
		||||
                    const minute = dateTimeString.substring(12, 14);
 | 
			
		||||
                    const second = dateTimeString.substring(14, 16);
 | 
			
		||||
 | 
			
		||||
                    return year + "/" + month + "/" + day + " " + hour + ":" + minute + ":" + second;
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  function addExtra() {
 | 
			
		||||
                    const event = document.getElementById("customModeCheckbox");
 | 
			
		||||
                    if (event.checked && lastPDFFile.Custom != null) {
 | 
			
		||||
                      customMetadataDiv.style.display = 'block';
 | 
			
		||||
                      for (const [key, value] of Object.entries(lastPDFFile.Custom)) {
 | 
			
		||||
                        if (key === 'Author' || key === 'CreationDate' || key === 'Creator' || key === 'Keywords' || key === 'ModDate' || key === 'Producer' || key === 'Subject' || key === 'Title' || key === 'Trapped') {
 | 
			
		||||
                          continue;
 | 
			
		||||
                        }
 | 
			
		||||
                        const entryDiv = document.createElement('div');
 | 
			
		||||
                        entryDiv.className = 'mb-3';
 | 
			
		||||
                        entryDiv.innerHTML = `<div class="mb-3"><label class="form-check-label" for="${key}">${key}:</label><input name="${key}" value="${value}" type="text" class="form-control" id="${key}"></div>`;
 | 
			
		||||
                        otherMetadataEntriesDiv.appendChild(entryDiv);
 | 
			
		||||
                      }
 | 
			
		||||
                    } else {
 | 
			
		||||
                      customMetadataDiv.style.display = 'none';
 | 
			
		||||
                      while (otherMetadataEntriesDiv.firstChild) {
 | 
			
		||||
                        otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  customModeCheckbox.addEventListener('change', (event) => {
 | 
			
		||||
                    addExtra();
 | 
			
		||||
                  });
 | 
			
		||||
                <script type="module" th:src="@{'/js/pages/change-metadata.js'}">
 | 
			
		||||
                </script>
 | 
			
		||||
              </form>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@
 | 
			
		||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
 | 
			
		||||
  <head>
 | 
			
		||||
  <th:block th:insert="~{fragments/common :: head(title=#{extractImages.title}, header=#{extractImages.header})}"></th:block>
 | 
			
		||||
  <script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
 | 
			
		||||
  </head>
 | 
			
		||||
 | 
			
		||||
  <body>
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
    <th:block th:insert="~{fragments/common :: head(title=#{replace-color.title}, header=#{replace-color.header})}"></th:block>
 | 
			
		||||
    <script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
 | 
			
		||||
@ -162,10 +162,19 @@
 | 
			
		||||
        insertPageBreak:'[[#{multiTool.insertPageBreak}]]',
 | 
			
		||||
        dragDropMessage:'[[#{multiTool.dragDropMessage}]]',
 | 
			
		||||
        undo: '[[#{multiTool.undo}]]',
 | 
			
		||||
        redo: '[[#{multiTool.redo}]]'
 | 
			
		||||
        redo: '[[#{multiTool.redo}]]',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    window.decrypt = {
 | 
			
		||||
                    passwordPrompt: '[[#{decrypt.passwordPrompt}]]',
 | 
			
		||||
                    cancelled: '[[#{decrypt.cancelled}]]',
 | 
			
		||||
                    noPassword: '[[#{decrypt.noPassword}]]',
 | 
			
		||||
                    invalidPassword: '[[#{decrypt.invalidPassword}]]',
 | 
			
		||||
                    invalidPasswordHeader: '[[#{decrypt.invalidPasswordHeader}]]',
 | 
			
		||||
                    unexpectedError: '[[#{decrypt.unexpectedError}]]',
 | 
			
		||||
                    serverError: '[[#{decrypt.serverError}]]',
 | 
			
		||||
                    success: '[[#{decrypt.success}]]',
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
    const csvInput = document.getElementById("csv-input");
 | 
			
		||||
    csvInput.addEventListener("keydown", function (event) {
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@
 | 
			
		||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
 | 
			
		||||
  <head>
 | 
			
		||||
  <th:block th:insert="~{fragments/common :: head(title=#{split-by-size-or-count.title}, header=#{split-by-size-or-count.header})}"></th:block>
 | 
			
		||||
  <script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
 | 
			
		||||
  </head>
 | 
			
		||||
 | 
			
		||||
  <body>
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@
 | 
			
		||||
      th:href="@{'/css/pipeline.css'}"
 | 
			
		||||
      th:if="${currentPage == 'pipeline'}"
 | 
			
		||||
    />
 | 
			
		||||
    <script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
 | 
			
		||||
    <script th:inline="javascript">
 | 
			
		||||
      const saveSettings = /*[[#{pipelineOptions.saveSettings}]]*/ "";
 | 
			
		||||
      const deletePipelineText = /*[[#{pipeline.deletePrompt}]]*/ "Are you sure you want to delete pipeline";
 | 
			
		||||
 | 
			
		||||
@ -19,9 +19,10 @@
 | 
			
		||||
      }
 | 
			
		||||
    </style>
 | 
			
		||||
  </th:block>
 | 
			
		||||
 | 
			
		||||
  <script th:src="@{'/js/thirdParty/signature_pad.umd.min.js'}"></script>
 | 
			
		||||
  <script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
 | 
			
		||||
  <script type="module" th:src="@{'/js/pages/sign.js'}"></script>
 | 
			
		||||
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
@ -42,75 +43,6 @@
 | 
			
		||||
              th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, disableMultipleFiles=true, accept='application/pdf')}">
 | 
			
		||||
            </div>
 | 
			
		||||
            <script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
 | 
			
		||||
            <script>
 | 
			
		||||
              let currentPreviewSrc = null;
 | 
			
		||||
 | 
			
		||||
              function toggleSignatureView() {
 | 
			
		||||
                const gridView = document.getElementById('gridView');
 | 
			
		||||
                const listView = document.getElementById('listView');
 | 
			
		||||
                const gridText = document.querySelector('.grid-view-text');
 | 
			
		||||
                const listText = document.querySelector('.list-view-text');
 | 
			
		||||
 | 
			
		||||
                if (gridView.style.display !== 'none') {
 | 
			
		||||
                  gridView.style.display = 'none';
 | 
			
		||||
                  listView.style.display = 'block';
 | 
			
		||||
                  gridText.style.display = 'none';
 | 
			
		||||
                  listText.style.display = 'inline';
 | 
			
		||||
                } else {
 | 
			
		||||
                  gridView.style.display = 'block';
 | 
			
		||||
                  listView.style.display = 'none';
 | 
			
		||||
                  gridText.style.display = 'inline';
 | 
			
		||||
                  listText.style.display = 'none';
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              function previewSignature(element) {
 | 
			
		||||
                const src = element.dataset.src;
 | 
			
		||||
                currentPreviewSrc = src;
 | 
			
		||||
 | 
			
		||||
                // Extract filename from the data source path
 | 
			
		||||
                const filename = element.querySelector('.signature-list-name').textContent;
 | 
			
		||||
 | 
			
		||||
                // Update preview modal
 | 
			
		||||
                const previewImage = document.getElementById('previewImage');
 | 
			
		||||
                const previewFileName = document.getElementById('previewFileName');
 | 
			
		||||
 | 
			
		||||
                previewImage.src = src;
 | 
			
		||||
                previewFileName.textContent = filename;
 | 
			
		||||
 | 
			
		||||
                const modal = new bootstrap.Modal(document.getElementById('signaturePreview'));
 | 
			
		||||
                modal.show();
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              function addSignatureFromPreview() {
 | 
			
		||||
                if (currentPreviewSrc) {
 | 
			
		||||
                  DraggableUtils.createDraggableCanvasFromUrl(currentPreviewSrc);
 | 
			
		||||
                  bootstrap.Modal.getInstance(document.getElementById('signaturePreview')).hide();
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
              let originalFileName = '';
 | 
			
		||||
              document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
 | 
			
		||||
                const file = event.target.files[0];
 | 
			
		||||
                if (file) {
 | 
			
		||||
                  originalFileName = file.name.replace(/\.[^/.]+$/, "");
 | 
			
		||||
                  const pdfData = await file.arrayBuffer();
 | 
			
		||||
                  pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
 | 
			
		||||
                  const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
 | 
			
		||||
                  await DraggableUtils.renderPage(pdfDoc, 0);
 | 
			
		||||
 | 
			
		||||
                  document.querySelectorAll(".show-on-file-selected").forEach(el => {
 | 
			
		||||
                    el.style.cssText = '';
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
              });
 | 
			
		||||
              document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
                document.querySelectorAll(".show-on-file-selected").forEach(el => {
 | 
			
		||||
                  el.style.cssText = "display:none !important";
 | 
			
		||||
                });
 | 
			
		||||
              });
 | 
			
		||||
            </script>
 | 
			
		||||
            <div class="tab-group show-on-file-selected">
 | 
			
		||||
              <div class="tab-container" th:title="#{sign.upload}">
 | 
			
		||||
                <div
 | 
			
		||||
@ -237,123 +169,6 @@
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <script>
 | 
			
		||||
              const imageUpload = document.querySelector('input[name=image-upload]');
 | 
			
		||||
              imageUpload.addEventListener('change', e => {
 | 
			
		||||
                if (!e.target.files) return;
 | 
			
		||||
                for (const imageFile of e.target.files) {
 | 
			
		||||
                  var reader = new FileReader();
 | 
			
		||||
                  reader.readAsDataURL(imageFile);
 | 
			
		||||
                  reader.onloadend = function (e) {
 | 
			
		||||
                    DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
 | 
			
		||||
                  };
 | 
			
		||||
                }
 | 
			
		||||
              });
 | 
			
		||||
            </script>
 | 
			
		||||
            <script>
 | 
			
		||||
              const signaturePadCanvas = document.getElementById('drawing-pad-canvas');
 | 
			
		||||
              const signaturePad = new SignaturePad(signaturePadCanvas, {
 | 
			
		||||
                minWidth: 1,
 | 
			
		||||
                maxWidth: 2,
 | 
			
		||||
                penColor: 'black',
 | 
			
		||||
              });
 | 
			
		||||
 | 
			
		||||
              function addDraggableFromPad() {
 | 
			
		||||
                if (signaturePad.isEmpty()) return;
 | 
			
		||||
                const startTime = Date.now();
 | 
			
		||||
                const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas);
 | 
			
		||||
                console.log(Date.now() - startTime);
 | 
			
		||||
                DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl);
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              function getCroppedCanvasDataUrl(canvas) {
 | 
			
		||||
                let originalCtx = canvas.getContext('2d');
 | 
			
		||||
                let originalWidth = canvas.width;
 | 
			
		||||
                let originalHeight = canvas.height;
 | 
			
		||||
                let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight);
 | 
			
		||||
 | 
			
		||||
                let minX = originalWidth + 1, maxX = -1, minY = originalHeight + 1, maxY = -1, x = 0, y = 0, currentPixelColorValueIndex;
 | 
			
		||||
 | 
			
		||||
                for (y = 0; y < originalHeight; y++) {
 | 
			
		||||
                  for (x = 0; x < originalWidth; x++) {
 | 
			
		||||
                    currentPixelColorValueIndex = (y * originalWidth + x) * 4;
 | 
			
		||||
                    let currentPixelAlphaValue = imageData.data[currentPixelColorValueIndex + 3];
 | 
			
		||||
                    if (currentPixelAlphaValue > 0) {
 | 
			
		||||
                      if (minX > x) minX = x;
 | 
			
		||||
                      if (maxX < x) maxX = x;
 | 
			
		||||
                      if (minY > y) minY = y;
 | 
			
		||||
                      if (maxY < y) maxY = y;
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let croppedWidth = maxX - minX;
 | 
			
		||||
                let croppedHeight = maxY - minY;
 | 
			
		||||
                if (croppedWidth < 0 || croppedHeight < 0) return null;
 | 
			
		||||
                let cuttedImageData = originalCtx.getImageData(minX, minY, croppedWidth, croppedHeight);
 | 
			
		||||
 | 
			
		||||
                let croppedCanvas = document.createElement('canvas'),
 | 
			
		||||
                  croppedCtx = croppedCanvas.getContext('2d');
 | 
			
		||||
 | 
			
		||||
                croppedCanvas.width = croppedWidth;
 | 
			
		||||
                croppedCanvas.height = croppedHeight;
 | 
			
		||||
                croppedCtx.putImageData(cuttedImageData, 0, 0);
 | 
			
		||||
 | 
			
		||||
                return croppedCanvas.toDataURL();
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              function resizeCanvas() {
 | 
			
		||||
                var ratio = Math.max(window.devicePixelRatio || 1, 1);
 | 
			
		||||
                var additionalFactor = 10;
 | 
			
		||||
 | 
			
		||||
                signaturePadCanvas.width = signaturePadCanvas.offsetWidth * ratio * additionalFactor;
 | 
			
		||||
                signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio * additionalFactor;
 | 
			
		||||
                signaturePadCanvas.getContext("2d").scale(ratio * additionalFactor, ratio * additionalFactor);
 | 
			
		||||
 | 
			
		||||
                signaturePad.clear();
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              new IntersectionObserver((entries, observer) => {
 | 
			
		||||
                if (entries.some(entry => entry.intersectionRatio > 0)) {
 | 
			
		||||
                  resizeCanvas();
 | 
			
		||||
                }
 | 
			
		||||
              }).observe(signaturePadCanvas);
 | 
			
		||||
 | 
			
		||||
              new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
 | 
			
		||||
            </script>
 | 
			
		||||
            <script>
 | 
			
		||||
              function addDraggableFromText() {
 | 
			
		||||
                const sigText = document.getElementById('sigText').value;
 | 
			
		||||
                const font = document.querySelector('select[name=font]').value;
 | 
			
		||||
                const fontSize = 100;
 | 
			
		||||
 | 
			
		||||
                const canvas = document.createElement('canvas');
 | 
			
		||||
                const ctx = canvas.getContext('2d');
 | 
			
		||||
                ctx.font = `${fontSize}px ${font}`;
 | 
			
		||||
                const textWidth = ctx.measureText(sigText).width;
 | 
			
		||||
                const textHeight = fontSize;
 | 
			
		||||
 | 
			
		||||
                let paragraphs = sigText.split(/\r?\n/);
 | 
			
		||||
 | 
			
		||||
                canvas.width = textWidth;
 | 
			
		||||
                canvas.height = paragraphs.length * textHeight * 1.35; // for tails
 | 
			
		||||
                ctx.font = `${fontSize}px ${font}`;
 | 
			
		||||
 | 
			
		||||
                ctx.textBaseline = 'top';
 | 
			
		||||
 | 
			
		||||
                let y = 0;
 | 
			
		||||
 | 
			
		||||
                paragraphs.forEach(paragraph => {
 | 
			
		||||
                  ctx.fillText(paragraph, 0, y);
 | 
			
		||||
                  y += fontSize;
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                const dataURL = canvas.toDataURL();
 | 
			
		||||
                DraggableUtils.createDraggableCanvasFromUrl(dataURL);
 | 
			
		||||
              }
 | 
			
		||||
            </script>
 | 
			
		||||
 | 
			
		||||
            <!-- draggables box -->
 | 
			
		||||
            <div id="box-drag-container" class="show-on-file-selected">
 | 
			
		||||
              <canvas id="pdf-canvas"></canvas>
 | 
			
		||||
@ -410,35 +225,11 @@
 | 
			
		||||
                </button>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- download button -->
 | 
			
		||||
            <div class="margin-auto-parent">
 | 
			
		||||
              <button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center"
 | 
			
		||||
                th:text="#{downloadPdf}"></button>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <script>
 | 
			
		||||
              async function goToFirstOrLastPage(page) {
 | 
			
		||||
                if (page) {
 | 
			
		||||
                  const lastPage = DraggableUtils.pdfDoc.numPages
 | 
			
		||||
                  await DraggableUtils.goToPage(lastPage - 1)
 | 
			
		||||
                } else {
 | 
			
		||||
                  await DraggableUtils.goToPage(0)
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            </script>
 | 
			
		||||
 | 
			
		||||
            <script>
 | 
			
		||||
              document.getElementById("download-pdf").addEventListener('click', async () => {
 | 
			
		||||
                const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
 | 
			
		||||
                const modifiedPdfBytes = await modifiedPdf.save();
 | 
			
		||||
                const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
 | 
			
		||||
                const link = document.createElement('a');
 | 
			
		||||
                link.href = URL.createObjectURL(blob);
 | 
			
		||||
                link.download = originalFileName + '_signed.pdf';
 | 
			
		||||
                link.click();
 | 
			
		||||
              });
 | 
			
		||||
            </script>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
@ -447,6 +238,7 @@
 | 
			
		||||
  </div>
 | 
			
		||||
  <!-- Link the draggable.js file -->
 | 
			
		||||
  <script th:src="@{'/js/draggable.js'}"></script>
 | 
			
		||||
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user