Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
532f7cdbbf | ||
|
|
51c4a60313 | ||
|
|
aa00808219 | ||
|
|
5d40175e18 | ||
|
|
a40fdd5a0b | ||
|
|
6ea7ffc36c | ||
|
|
39e0fd8eef | ||
|
|
cae8cd0aa9 | ||
|
|
04d5ae1912 | ||
|
|
e01ba93cf8 |
@@ -182,7 +182,7 @@ Stirling PDF currently supports 38!
|
|||||||
| Dutch (Nederlands) (nl_NL) |  |
|
| Dutch (Nederlands) (nl_NL) |  |
|
||||||
| English (English) (en_GB) |  |
|
| English (English) (en_GB) |  |
|
||||||
| English (US) (en_US) |  |
|
| English (US) (en_US) |  |
|
||||||
| French (Français) (fr_FR) |  |
|
| French (Français) (fr_FR) |  |
|
||||||
| German (Deutsch) (de_DE) |  |
|
| German (Deutsch) (de_DE) |  |
|
||||||
| Greek (Ελληνικά) (el_GR) |  |
|
| Greek (Ελληνικά) (el_GR) |  |
|
||||||
| Hindi (हिंदी) (hi_IN) |  |
|
| Hindi (हिंदी) (hi_IN) |  |
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ ext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "stirling.software"
|
group = "stirling.software"
|
||||||
version = "0.30.0"
|
version = "0.30.1"
|
||||||
|
|
||||||
java {
|
java {
|
||||||
// 17 is lowest but we support and recommend 21
|
// 17 is lowest but we support and recommend 21
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: 0.30.0
|
appVersion: 0.30.1
|
||||||
description: locally hosted web application that allows you to perform various operations
|
description: locally hosted web application that allows you to perform various operations
|
||||||
on PDF files
|
on PDF files
|
||||||
home: https://github.com/Stirling-Tools/Stirling-PDF
|
home: https://github.com/Stirling-Tools/Stirling-PDF
|
||||||
|
|||||||
@@ -7,20 +7,19 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@Lazy
|
@Lazy
|
||||||
|
@Slf4j
|
||||||
public class EEAppConfig {
|
public class EEAppConfig {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(EEAppConfig.class);
|
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@Autowired private LicenseKeyChecker licenseKeyChecker;
|
@Autowired private LicenseKeyChecker licenseKeyChecker;
|
||||||
|
|
||||||
@Bean(name = "runningEE")
|
@Bean(name = "runningEE")
|
||||||
public boolean runningEnterpriseEdition() {
|
public boolean runningEnterpriseEdition() {
|
||||||
return licenseKeyChecker.getEnterpriseEnabledResult();
|
return licenseKeyChecker.getEnterpriseEnabledResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.context.annotation.Scope;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.ResourceLoader;
|
import org.springframework.core.io.ResourceLoader;
|
||||||
@@ -162,12 +163,14 @@ public class AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "analyticsPrompt")
|
@Bean(name = "analyticsPrompt")
|
||||||
|
@Scope("request")
|
||||||
public boolean analyticsPrompt() {
|
public boolean analyticsPrompt() {
|
||||||
return applicationProperties.getSystem().getEnableAnalytics() == null
|
return applicationProperties.getSystem().getEnableAnalytics() == null
|
||||||
|| "undefined".equals(applicationProperties.getSystem().getEnableAnalytics());
|
|| "undefined".equals(applicationProperties.getSystem().getEnableAnalytics());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "analyticsEnabled")
|
@Bean(name = "analyticsEnabled")
|
||||||
|
@Scope("request")
|
||||||
public boolean analyticsEnabled() {
|
public boolean analyticsEnabled() {
|
||||||
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
|
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
|
||||||
return applicationProperties.getSystem().getEnableAnalytics() != null
|
return applicationProperties.getSystem().getEnableAnalytics() != null
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import org.springframework.core.Ordered;
|
|||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import io.micrometer.common.util.StringUtils;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
@@ -39,4 +40,26 @@ public class InitialSetup {
|
|||||||
applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
|
applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initLegalUrls() throws IOException {
|
||||||
|
// Initialize Terms and Conditions
|
||||||
|
String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
|
||||||
|
if (StringUtils.isEmpty(termsUrl)) {
|
||||||
|
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
|
||||||
|
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl);
|
||||||
|
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Privacy Policy
|
||||||
|
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
|
||||||
|
if (StringUtils.isEmpty(privacyUrl)) {
|
||||||
|
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
|
||||||
|
GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl);
|
||||||
|
applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ public class SecurityConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle SAML
|
// Handle SAML
|
||||||
if (applicationProperties.getSecurity().isSaml2Activ()) {
|
if (applicationProperties.getSecurity().isSaml2Activ() && applicationProperties.getSystem().getEnableAlphaFunctionality()) {
|
||||||
http.authenticationProvider(samlAuthenticationProvider());
|
http.authenticationProvider(samlAuthenticationProvider());
|
||||||
http.saml2Login(
|
http.saml2Login(
|
||||||
saml2 ->
|
saml2 ->
|
||||||
|
|||||||
@@ -45,9 +45,6 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
@Autowired DatabaseBackupInterface databaseBackupHelper;
|
@Autowired DatabaseBackupInterface databaseBackupHelper;
|
||||||
|
|
||||||
public long getTotalUserCount() {
|
|
||||||
return userRepository.count();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle OAUTH2 login and user auto creation.
|
// Handle OAUTH2 login and user auto creation.
|
||||||
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser)
|
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser)
|
||||||
@@ -362,4 +359,9 @@ public class UserService implements UserServiceInterface {
|
|||||||
return principal.toString();
|
return principal.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getTotalUsersCount() {
|
||||||
|
return userRepository.count();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public class SettingsController {
|
|||||||
}
|
}
|
||||||
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
|
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
|
||||||
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
|
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
|
||||||
|
|
||||||
return ResponseEntity.ok("Updated");
|
return ResponseEntity.ok("Updated");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ public interface UserServiceInterface {
|
|||||||
String getApiKeyForUser(String username);
|
String getApiKeyForUser(String username);
|
||||||
|
|
||||||
String getCurrentUsername();
|
String getCurrentUsername();
|
||||||
|
|
||||||
|
long getTotalUsersCount();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import com.posthog.java.PostHog;
|
import com.posthog.java.PostHog;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -26,15 +27,18 @@ public class PostHogService {
|
|||||||
private final PostHog postHog;
|
private final PostHog postHog;
|
||||||
private final String uniqueId;
|
private final String uniqueId;
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
private final UserServiceInterface userService;
|
||||||
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public PostHogService(
|
public PostHogService(
|
||||||
PostHog postHog,
|
PostHog postHog,
|
||||||
@Qualifier("UUID") String uuid,
|
@Qualifier("UUID") String uuid,
|
||||||
ApplicationProperties applicationProperties) {
|
ApplicationProperties applicationProperties, @Autowired(required = false) UserServiceInterface userService) {
|
||||||
this.postHog = postHog;
|
this.postHog = postHog;
|
||||||
this.uniqueId = uuid;
|
this.uniqueId = uuid;
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.userService = userService;
|
||||||
captureSystemInfo();
|
captureSystemInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +137,11 @@ public class PostHogService {
|
|||||||
metrics.put("docker_metrics", getDockerMetrics());
|
metrics.put("docker_metrics", getDockerMetrics());
|
||||||
}
|
}
|
||||||
metrics.put("application_properties", captureApplicationProperties());
|
metrics.put("application_properties", captureApplicationProperties());
|
||||||
|
|
||||||
|
|
||||||
|
if(userService != null) {
|
||||||
|
metrics.put("total_users_created", userService.getTotalUsersCount());
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
metrics.put("error", e.getMessage());
|
metrics.put("error", e.getMessage());
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ navbar.sections.convertFrom=Convertir depuis PDF
|
|||||||
navbar.sections.security=Signature et sécurité
|
navbar.sections.security=Signature et sécurité
|
||||||
navbar.sections.advance=Mode avancé
|
navbar.sections.advance=Mode avancé
|
||||||
navbar.sections.edit=Voir et modifier
|
navbar.sections.edit=Voir et modifier
|
||||||
navbar.sections.popular=Popular
|
navbar.sections.popular=Populaire
|
||||||
|
|
||||||
#############
|
#############
|
||||||
# SETTINGS #
|
# SETTINGS #
|
||||||
@@ -202,9 +202,9 @@ adminUserSettings.header=Administration des paramètres des utilisateurs
|
|||||||
adminUserSettings.admin=Administateur
|
adminUserSettings.admin=Administateur
|
||||||
adminUserSettings.user=Utilisateur
|
adminUserSettings.user=Utilisateur
|
||||||
adminUserSettings.addUser=Ajouter un utilisateur
|
adminUserSettings.addUser=Ajouter un utilisateur
|
||||||
adminUserSettings.deleteUser=Delete User
|
adminUserSettings.deleteUser=Supprimer l'utilisateur
|
||||||
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
adminUserSettings.confirmDeleteUser=Voulez vous vraiment supprimer l'utilisateur ?
|
||||||
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
|
adminUserSettings.confirmChangeUserStatus=Voulez vous vraiment déactiver/réactiver l'utilisateur ?
|
||||||
adminUserSettings.usernameInfo=Le nom d'utilisateur ne peut contenir que des lettres, des chiffres et les caractères spéciaux suivants @._+- ou doit être une adresse e-mail valide.
|
adminUserSettings.usernameInfo=Le nom d'utilisateur ne peut contenir que des lettres, des chiffres et les caractères spéciaux suivants @._+- ou doit être une adresse e-mail valide.
|
||||||
adminUserSettings.roles=Rôles
|
adminUserSettings.roles=Rôles
|
||||||
adminUserSettings.role=Rôle
|
adminUserSettings.role=Rôle
|
||||||
@@ -218,13 +218,13 @@ adminUserSettings.forceChange=Forcer l’utilisateur à changer son nom d’util
|
|||||||
adminUserSettings.submit=Ajouter
|
adminUserSettings.submit=Ajouter
|
||||||
adminUserSettings.changeUserRole=Changer le rôle de l'utilisateur
|
adminUserSettings.changeUserRole=Changer le rôle de l'utilisateur
|
||||||
adminUserSettings.authenticated=Authentifié
|
adminUserSettings.authenticated=Authentifié
|
||||||
adminUserSettings.editOwnProfil=Edit own profile
|
adminUserSettings.editOwnProfil=Éditer son propre profil
|
||||||
adminUserSettings.enabledUser=enabled user
|
adminUserSettings.enabledUser=Utilisateur activé
|
||||||
adminUserSettings.disabledUser=disabled user
|
adminUserSettings.disabledUser=Utilisateur désactivé
|
||||||
adminUserSettings.activeUsers=Active Users:
|
adminUserSettings.activeUsers=Utilisateurs actifs :
|
||||||
adminUserSettings.disabledUsers=Disabled Users:
|
adminUserSettings.disabledUsers=Utilisateurs désactivés :
|
||||||
adminUserSettings.totalUsers=Total Users:
|
adminUserSettings.totalUsers=Utilisateurs au total :
|
||||||
adminUserSettings.lastRequest=Last Request
|
adminUserSettings.lastRequest=Dernière requête
|
||||||
|
|
||||||
|
|
||||||
database.title=Database Import/Export
|
database.title=Database Import/Export
|
||||||
@@ -243,7 +243,7 @@ database.fileNotFound=File not Found
|
|||||||
database.fileNullOrEmpty=File must not be null or empty
|
database.fileNullOrEmpty=File must not be null or empty
|
||||||
database.failedImportFile=Failed Import File
|
database.failedImportFile=Failed Import File
|
||||||
|
|
||||||
session.expired=Your session has expired. Please refresh the page and try again.
|
session.expired=Votre session a expiré. Veuillez recharger la page et réessayer.
|
||||||
|
|
||||||
#############
|
#############
|
||||||
# HOME-PAGE #
|
# HOME-PAGE #
|
||||||
@@ -321,7 +321,7 @@ home.changeMetadata.desc=Modifiez, supprimez ou ajoutez des métadonnées à un
|
|||||||
changeMetadata.tags=métadonnées,titre,auteur,date,création,heure,éditeur,statistiques,title,author,date,creation,time,publisher,producer,stats,metadata
|
changeMetadata.tags=métadonnées,titre,auteur,date,création,heure,éditeur,statistiques,title,author,date,creation,time,publisher,producer,stats,metadata
|
||||||
|
|
||||||
home.fileToPDF.title=Fichier en PDF
|
home.fileToPDF.title=Fichier en PDF
|
||||||
home.fileToPDF.desc=Convertissez presque n’importe quel fichiers en PDF (DOCX, PNG, XLS, PPT, TXT et plus).
|
home.fileToPDF.desc=Convertissez presque n’importe quel fichier en PDF (DOCX, PNG, XLS, PPT, TXT, etc.).
|
||||||
fileToPDF.tags=convertion,transformation,format,document,image,slide,texte,conversion,office,docs,word,excel,powerpoint
|
fileToPDF.tags=convertion,transformation,format,document,image,slide,texte,conversion,office,docs,word,excel,powerpoint
|
||||||
|
|
||||||
home.ocr.title=OCR / Nettoyage des numérisations
|
home.ocr.title=OCR / Nettoyage des numérisations
|
||||||
@@ -390,9 +390,9 @@ home.certSign.title=Signer avec un certificat
|
|||||||
home.certSign.desc=Signez un PDF avec un certificat ou une clé (PEM/P12).
|
home.certSign.desc=Signez un PDF avec un certificat ou une clé (PEM/P12).
|
||||||
certSign.tags=signer,chiffrer,certificat,authenticate,PEM,P12,official,encrypt
|
certSign.tags=signer,chiffrer,certificat,authenticate,PEM,P12,official,encrypt
|
||||||
|
|
||||||
home.removeCertSign.title=Remove Certificate Sign
|
home.removeCertSign.title=Supprimer la signature par certificat
|
||||||
home.removeCertSign.desc=Remove certificate signature from PDF
|
home.removeCertSign.desc=Supprimez la signature par certificat d'un PDF
|
||||||
removeCertSign.tags=authenticate,PEM,P12,official,decrypt
|
removeCertSign.tags=signer,chiffrer,certificat,authenticate,PEM,P12,official,decrypt
|
||||||
|
|
||||||
home.pageLayout.title=Fusionner des pages
|
home.pageLayout.title=Fusionner des pages
|
||||||
home.pageLayout.desc=Fusionnez plusieurs pages d’un PDF en une seule.
|
home.pageLayout.desc=Fusionnez plusieurs pages d’un PDF en une seule.
|
||||||
@@ -498,14 +498,14 @@ home.BookToPDF.title=eBook vers PDF
|
|||||||
home.BookToPDF.desc=Convertit les formats de livres/bandes dessinées en PDF à l'aide de calibre
|
home.BookToPDF.desc=Convertit les formats de livres/bandes dessinées en PDF à l'aide de calibre
|
||||||
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
|
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
|
||||||
|
|
||||||
home.removeImagePdf.title=Remove image
|
home.removeImagePdf.title=Supprimer les images
|
||||||
home.removeImagePdf.desc=Remove image from PDF to reduce file size
|
home.removeImagePdf.desc=Supprimez les images d'un PDF pour réduire sa taille
|
||||||
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
|
removeImagePdf.tags=Images,Remove Image,Page operations,Back end,server side
|
||||||
|
|
||||||
|
|
||||||
home.splitPdfByChapters.title=Split PDF by Chapters
|
home.splitPdfByChapters.title=Séparer un PDF par chapitres
|
||||||
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
|
home.splitPdfByChapters.desc=Séparez un PDF en fichiers multiples en fonction de sa structure par chapitres.
|
||||||
splitPdfByChapters.tags=split,chapters,bookmarks,organize
|
splitPdfByChapters.tags=séparer,chapitres,split,chapters,bookmarks,organize
|
||||||
|
|
||||||
#replace-invert-color
|
#replace-invert-color
|
||||||
replace-color.title=Replace-Invert-Color
|
replace-color.title=Replace-Invert-Color
|
||||||
@@ -786,14 +786,14 @@ compare.submit=Comparer
|
|||||||
#BookToPDF
|
#BookToPDF
|
||||||
BookToPDF.title=Livres et BD vers PDF
|
BookToPDF.title=Livres et BD vers PDF
|
||||||
BookToPDF.header=Livre vers PDF
|
BookToPDF.header=Livre vers PDF
|
||||||
BookToPDF.credit=Utiliser Calibre
|
BookToPDF.credit=Utilise Calibre
|
||||||
BookToPDF.submit=Convertir
|
BookToPDF.submit=Convertir
|
||||||
|
|
||||||
#PDFToBook
|
#PDFToBook
|
||||||
PDFToBook.title=PDF vers Livre
|
PDFToBook.title=PDF vers Livre
|
||||||
PDFToBook.header=PDF vers Livre
|
PDFToBook.header=PDF vers Livre
|
||||||
PDFToBook.selectText.1=Format
|
PDFToBook.selectText.1=Format
|
||||||
PDFToBook.credit=Utiliser Calibre
|
PDFToBook.credit=Utilise Calibre
|
||||||
PDFToBook.submit=Convertir
|
PDFToBook.submit=Convertir
|
||||||
|
|
||||||
#sign
|
#sign
|
||||||
@@ -1076,7 +1076,7 @@ pdfToPDFA.credit=Ce service utilise ghostscript pour la conversion en PDF/A.
|
|||||||
pdfToPDFA.submit=Convertir
|
pdfToPDFA.submit=Convertir
|
||||||
pdfToPDFA.tip=Ne fonctionne actuellement pas pour plusieurs entrées à la fois
|
pdfToPDFA.tip=Ne fonctionne actuellement pas pour plusieurs entrées à la fois
|
||||||
pdfToPDFA.outputFormat=Format de sortie
|
pdfToPDFA.outputFormat=Format de sortie
|
||||||
pdfToPDFA.pdfWithDigitalSignature=The PDF contains a digital signature. This will be removed in the next step.
|
pdfToPDFA.pdfWithDigitalSignature=Le PDF contient une signature numérique. Elle sera supprimée dans l'étape suivante.
|
||||||
|
|
||||||
|
|
||||||
#PDFToWord
|
#PDFToWord
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ security:
|
|||||||
useAsUsername: email # Default is 'email'; custom fields can be used as the username
|
useAsUsername: email # Default is 'email'; custom fields can be used as the username
|
||||||
scopes: openid, profile, email # Specify the scopes for which the application will request permissions
|
scopes: openid, profile, email # Specify the scopes for which the application will request permissions
|
||||||
provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
|
provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
|
||||||
saml2:
|
saml2:
|
||||||
enabled: false
|
enabled: false # Currently in alpha, not recommended for use yet, enableAlphaFunctionality must be set to true
|
||||||
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
|
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
|
||||||
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
|
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
|
||||||
registrationId: stirling
|
registrationId: stirling
|
||||||
@@ -60,19 +60,18 @@ security:
|
|||||||
privateKey: classpath:saml-private-key.key
|
privateKey: classpath:saml-private-key.key
|
||||||
spCert: classpath:saml-public-cert.crt
|
spCert: classpath:saml-public-cert.crt
|
||||||
|
|
||||||
# Enterprise edition settings unused for now please ignore!
|
|
||||||
enterpriseEdition:
|
enterpriseEdition:
|
||||||
enabled: false # set to 'true' to enable enterprise edition
|
enabled: false # set to 'true' to enable enterprise edition
|
||||||
key: 00000000-0000-0000-0000-000000000000
|
key: 00000000-0000-0000-0000-000000000000
|
||||||
CustomMetadata:
|
CustomMetadata:
|
||||||
autoUpdateMetadata: true # set to 'true' to automatically update metadata with below values
|
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
|
||||||
author: username # Supports text such as 'John Doe' or types such as username
|
author: username # Supports text such as 'John Doe' or types such as username to autopopulate with users username
|
||||||
creator: Stirling-PDF # Supports text such as 'Company-PDF'
|
creator: Stirling-PDF # Supports text such as 'Company-PDF'
|
||||||
producer: Stirling-PDF # Supports text such as 'Company-PDF'
|
producer: Stirling-PDF # Supports text such as 'Company-PDF'
|
||||||
|
|
||||||
legal:
|
legal:
|
||||||
termsAndConditions: '' # URL to the terms and conditions of your application (e.g. https://example.com/terms) Empty string to disable or filename to load from local file in static folder
|
termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms) Empty string to disable or filename to load from local file in static folder
|
||||||
privacyPolicy: '' # URL to the privacy policy of your application (e.g. https://example.com/privacy) Empty string to disable or filename to load from local file in static folder
|
privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy) Empty string to disable or filename to load from local file in static folder
|
||||||
accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility) Empty string to disable or filename to load from local file in static folder
|
accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility) Empty string to disable or filename to load from local file in static folder
|
||||||
cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie) Empty string to disable or filename to load from local file in static folder
|
cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie) Empty string to disable or filename to load from local file in static folder
|
||||||
impressum: '' # URL to the impressum of your application (e.g. https://example.com/impressum) Empty string to disable or filename to load from local file in static folder
|
impressum: '' # URL to the impressum of your application (e.g. https://example.com/impressum) Empty string to disable or filename to load from local file in static folder
|
||||||
|
|||||||
@@ -749,8 +749,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"moduleName": "org.commonmark:commonmark",
|
"moduleName": "org.commonmark:commonmark",
|
||||||
"moduleVersion": "0.23.0",
|
"moduleVersion": "0.24.0",
|
||||||
"moduleLicense": "BSD 2-Clause License",
|
"moduleLicense": "BSD-2-Clause",
|
||||||
"moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause"
|
"moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
select#font-select,
|
select#font-select,
|
||||||
select#font-select option {
|
select#font-select option {
|
||||||
height: 60px; /* Adjust as needed */
|
height: 60px;
|
||||||
font-size: 30px; /* Adjust as needed */
|
/* Adjust as needed */
|
||||||
|
font-size: 30px;
|
||||||
|
/* Adjust as needed */
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawing-pad-container {
|
.drawing-pad-container {
|
||||||
@@ -13,10 +15,12 @@ select#font-select option {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#box-drag-container {
|
#box-drag-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggable-buttons-box {
|
.draggable-buttons-box {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -24,16 +28,37 @@ select#font-select option {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
z-index: 5;
|
||||||
}
|
}
|
||||||
.draggable-buttons-box > button {
|
|
||||||
z-index: 10;
|
.draggable-buttons-box>button {
|
||||||
|
z-index: 4;
|
||||||
background-color: rgba(13, 110, 253, 0.1);
|
background-color: rgba(13, 110, 253, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggable-canvas {
|
.draggable-canvas {
|
||||||
border: 1px solid red;
|
border: 2px solid #3498db;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
cursor: grab;
|
||||||
|
transition: transform 0.1s ease-out;
|
||||||
|
background-color: rgba(52, 152, 219, 0.1);
|
||||||
|
/* Light blue background */
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-canvas:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
/* Shadow on active drag */
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-canvas:hover {
|
||||||
|
border: 2px solid #2980b9;
|
||||||
|
/* Darker border on hover */
|
||||||
|
background-color: rgba(52, 152, 219, 0.2);
|
||||||
|
/* Darken background on hover */
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/main/resources/static/js/draggable.js
Normal file
36
src/main/resources/static/js/draggable.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
const draggableElement = document.querySelector('.draggable-canvas');
|
||||||
|
|
||||||
|
// Variables to store the current position of the draggable element
|
||||||
|
let offsetX, offsetY, isDragging = false;
|
||||||
|
|
||||||
|
draggableElement.addEventListener('mousedown', (e) => {
|
||||||
|
// Get the offset when the mouse is clicked inside the element
|
||||||
|
offsetX = e.clientX - draggableElement.getBoundingClientRect().left;
|
||||||
|
offsetY = e.clientY - draggableElement.getBoundingClientRect().top;
|
||||||
|
|
||||||
|
// Set isDragging to true
|
||||||
|
isDragging = true;
|
||||||
|
|
||||||
|
// Add event listeners for mouse movement and release
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
function onMouseMove(e) {
|
||||||
|
if (isDragging) {
|
||||||
|
// Calculate the new position of the element
|
||||||
|
const left = e.clientX - offsetX;
|
||||||
|
const top = e.clientY - offsetY;
|
||||||
|
|
||||||
|
// Move the element by setting its style
|
||||||
|
draggableElement.style.left = `${left}px`;
|
||||||
|
draggableElement.style.top = `${top}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp() {
|
||||||
|
// Stop dragging and remove event listeners
|
||||||
|
isDragging = false;
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
const scrollDivHorizontally = (id) => {
|
|
||||||
var scrollDeltaX = 0; // variable to store the accumulated horizontal scroll delta
|
|
||||||
var scrollDeltaY = 0; // variable to store the accumulated vertical scroll delta
|
|
||||||
var isScrolling = false; // variable to track if scroll is already in progress
|
|
||||||
const divToScroll = document.getElementById(id);
|
|
||||||
|
|
||||||
function scrollLoop() {
|
|
||||||
// Scroll the div horizontally and vertically by a fraction of the accumulated scroll delta
|
|
||||||
divToScroll.scrollLeft += scrollDeltaX * 0.1;
|
|
||||||
divToScroll.scrollTop += scrollDeltaY * 0.1;
|
|
||||||
|
|
||||||
// Reduce the accumulated scroll delta by a fraction
|
|
||||||
scrollDeltaX *= 0.9;
|
|
||||||
scrollDeltaY *= 0.9;
|
|
||||||
|
|
||||||
// If scroll delta is still significant, continue the scroll loop
|
|
||||||
if (Math.abs(scrollDeltaX) > 0.1 || Math.abs(scrollDeltaY) > 0.1) {
|
|
||||||
requestAnimationFrame(scrollLoop);
|
|
||||||
} else {
|
|
||||||
isScrolling = false; // Reset scroll in progress flag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
divToScroll.addEventListener("wheel", function (e) {
|
|
||||||
e.preventDefault(); // prevent default mousewheel behavior
|
|
||||||
|
|
||||||
// Accumulate the horizontal and vertical scroll delta
|
|
||||||
scrollDeltaX -= e.deltaX || e.wheelDeltaX || -e.deltaY || -e.wheelDeltaY;
|
|
||||||
scrollDeltaY -= e.deltaY || e.wheelDeltaY || -e.deltaX || -e.wheelDeltaX;
|
|
||||||
|
|
||||||
// If scroll is not already in progress, start the scroll loop
|
|
||||||
if (!isScrolling) {
|
|
||||||
isScrolling = true;
|
|
||||||
requestAnimationFrame(scrollLoop);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default scrollDivHorizontally;
|
|
||||||
@@ -360,7 +360,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item" th:if="${!@runningEE}">
|
||||||
<a href="https://stirlingpdf.com/pricing" class="nav-link go-pro-link" target="_blank" rel="noopener noreferrer">
|
<a href="https://stirlingpdf.com/pricing" class="nav-link go-pro-link" target="_blank" rel="noopener noreferrer">
|
||||||
<span class="go-pro-badge" th:text="#{enterpriseEdition.button}"></span>
|
<span class="go-pro-badge" th:text="#{enterpriseEdition.button}"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -374,10 +374,10 @@
|
|||||||
<p th:text="#{analytics.paragraph2}">Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.</p>
|
<p th:text="#{analytics.paragraph2}">Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.</p>
|
||||||
<p th:text="#{analytics.settings}">You can change the settings for analytics in the config/settings.yml file</p>
|
<p th:text="#{analytics.settings}">You can change the settings for analytics in the config/settings.yml file</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer justify-content-between">
|
||||||
<button type="button" class="btn btn-primary" th:text="#{analytics.enable}" onclick="setAnalytics(true)">Enable analytics</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="setAnalytics(false)" th:text="#{analytics.disable}">Disable analytics</button>
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="setAnalytics(false)" th:text="#{analytics.disable}">Disable analytics</button>
|
<button type="button" class="btn btn-primary" th:text="#{analytics.enable}" onclick="setAnalytics(true)">Enable analytics</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -426,50 +426,60 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
const surveyVersion = "2.0";
|
const surveyVersion = "2.0";
|
||||||
const modal = new bootstrap.Modal(document.getElementById('surveyModal'));
|
const modal = new bootstrap.Modal(document.getElementById('surveyModal'));
|
||||||
const dontShowAgain = document.getElementById('dontShowAgain');
|
const dontShowAgain = document.getElementById('dontShowAgain');
|
||||||
const takeSurveyButton = document.getElementById('takeSurvey');
|
const takeSurveyButton = document.getElementById('takeSurvey');
|
||||||
|
|
||||||
const viewThresholds = [5, 15, 30, 50, 75, 100, 150, 200];
|
const viewThresholds = [5, 10, 15, 22, 30, 50, 75, 100, 150, 200];
|
||||||
let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
|
|
||||||
|
|
||||||
pageViews++;
|
// Check if survey version changed and reset page views if it did
|
||||||
localStorage.setItem('pageViews', pageViews.toString());
|
const storedVersion = localStorage.getItem('surveyVersion');
|
||||||
|
if (storedVersion && storedVersion !== surveyVersion) {
|
||||||
|
localStorage.setItem('pageViews', '0');
|
||||||
|
}
|
||||||
|
|
||||||
function shouldShowSurvey() {
|
let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
|
||||||
if (localStorage.getItem('dontShowSurvey') === 'true' || localStorage.getItem('surveyTaken') === 'true') {
|
|
||||||
return false;
|
pageViews++;
|
||||||
|
localStorage.setItem('pageViews', pageViews.toString());
|
||||||
|
|
||||||
|
function shouldShowSurvey() {
|
||||||
|
if (localStorage.getItem('dontShowSurvey') === 'true' ||
|
||||||
|
localStorage.getItem('surveyTaken') === 'true') {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localStorage.getItem('surveyVersion') !== surveyVersion) {
|
// If survey version changed and we hit a threshold, show the survey
|
||||||
return true;
|
if (localStorage.getItem('surveyVersion') !== surveyVersion &&
|
||||||
|
viewThresholds.includes(pageViews)) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return viewThresholds.includes(pageViews);
|
return viewThresholds.includes(pageViews);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldShowSurvey()) {
|
if (shouldShowSurvey()) {
|
||||||
modal.show();
|
modal.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
dontShowAgain.addEventListener('change', function() {
|
dontShowAgain.addEventListener('change', function() {
|
||||||
if (this.checked) {
|
if (this.checked) {
|
||||||
localStorage.setItem('dontShowSurvey', 'true');
|
localStorage.setItem('dontShowSurvey', 'true');
|
||||||
localStorage.setItem('surveyVersion', surveyVersion);
|
localStorage.setItem('surveyVersion', surveyVersion);
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('dontShowSurvey');
|
localStorage.removeItem('dontShowSurvey');
|
||||||
localStorage.removeItem('surveyVersion');
|
localStorage.removeItem('surveyVersion');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
takeSurveyButton.addEventListener('click', function() {
|
takeSurveyButton.addEventListener('click', function() {
|
||||||
localStorage.setItem('surveyTaken', 'true');
|
localStorage.setItem('surveyTaken', 'true');
|
||||||
localStorage.setItem('surveyVersion', surveyVersion);
|
localStorage.setItem('surveyVersion', surveyVersion);
|
||||||
modal.hide();
|
modal.hide();
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,6 @@
|
|||||||
<script type="module">
|
<script type="module">
|
||||||
import PdfContainer from './js/multitool/PdfContainer.js';
|
import PdfContainer from './js/multitool/PdfContainer.js';
|
||||||
import DragDropManager from "./js/multitool/DragDropManager.js";
|
import DragDropManager from "./js/multitool/DragDropManager.js";
|
||||||
import scrollDivHorizontally from "./js/multitool/horizontalScroll.js";
|
|
||||||
import ImageHighlighter from "./js/multitool/ImageHighlighter.js";
|
import ImageHighlighter from "./js/multitool/ImageHighlighter.js";
|
||||||
import PdfActionsManager from './js/multitool/PdfActionsManager.js';
|
import PdfActionsManager from './js/multitool/PdfActionsManager.js';
|
||||||
import FileDragManager from './js/multitool/fileInput.js';
|
import FileDragManager from './js/multitool/fileInput.js';
|
||||||
@@ -93,7 +92,6 @@
|
|||||||
const fileDragManager = new FileDragManager();
|
const fileDragManager = new FileDragManager();
|
||||||
|
|
||||||
// Scroll the wrapper horizontally
|
// Scroll the wrapper horizontally
|
||||||
scrollDivHorizontally('pages-container-wrapper');
|
|
||||||
|
|
||||||
// Automatically exposes rotateAll, addFiles and exportPdf to the window for the global buttons.
|
// Automatically exposes rotateAll, addFiles and exportPdf to the window for the global buttons.
|
||||||
const pdfContainer = new PdfContainer(
|
const pdfContainer = new PdfContainer(
|
||||||
@@ -111,4 +109,4 @@
|
|||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||||
<head>
|
xmlns:th="https://www.thymeleaf.org">
|
||||||
|
|
||||||
|
<head>
|
||||||
<th:block th:insert="~{fragments/common :: head(title=#{sign.title}, header=#{sign.header})}"></th:block>
|
<th:block th:insert="~{fragments/common :: head(title=#{sign.title}, header=#{sign.header})}"></th:block>
|
||||||
<link rel="stylesheet" th:href="@{'/css/sign.css'}">
|
<link rel="stylesheet" th:href="@{'/css/sign.css'}">
|
||||||
|
|
||||||
<th:block th:each="font : ${fonts}">
|
<th:block th:each="font : ${fonts}">
|
||||||
<style th:inline="text">
|
<style th:inline="text">
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -11,268 +14,266 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#font-select option[value="[[${font.name}]]"] {
|
#font-select option[value="[[${font.name}]]"] {
|
||||||
font-family: "[[${font.name}]]", cursive;
|
font-family: "[[${font.name}]]",
|
||||||
}
|
cursive;
|
||||||
#font-select option[value='/*[[${font.name}]]*/'] {
|
|
||||||
font-family: '/*[[${font.name}]]*/', cursive;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</th:block>
|
</th:block>
|
||||||
<script th:src="@{'/js/thirdParty/signature_pad.umd.min.js'}"></script>
|
|
||||||
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<script th:src="@{'/js/thirdParty/signature_pad.umd.min.js'}"></script>
|
||||||
<div id="page-container">
|
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
|
||||||
<div id="content-wrap">
|
</head>
|
||||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
|
||||||
<br><br>
|
|
||||||
<div class="container">
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-md-6 bg-card">
|
|
||||||
<div class="tool-header">
|
|
||||||
<span class="material-symbols-rounded tool-header-icon sign">signature</span>
|
|
||||||
<span class="tool-header-text" th:text="#{sign.header}"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- pdf selector -->
|
<body>
|
||||||
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, disableMultipleFiles=true, accept='application/pdf')}"></div>
|
<div id="page-container">
|
||||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
<div id="content-wrap">
|
||||||
<script>
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||||
let originalFileName = '';
|
<br><br>
|
||||||
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
|
<div class="container">
|
||||||
const file = event.target.files[0];
|
<div class="row justify-content-center">
|
||||||
if (file) {
|
<div class="col-md-6 bg-card">
|
||||||
originalFileName = file.name.replace(/\.[^/.]+$/, "");
|
<div class="tool-header">
|
||||||
const pdfData = await file.arrayBuffer();
|
<span class="material-symbols-rounded tool-header-icon sign">signature</span>
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
|
<span class="tool-header-text" th:text="#{sign.header}"></span>
|
||||||
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 th:replace="~{fragments/common :: fileSelector(name='image-upload', 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>
|
|
||||||
<div class="tab-container drawing-pad-container" th:title="#{sign.draw}">
|
|
||||||
<canvas id="drawing-pad-canvas"></canvas>
|
|
||||||
<br>
|
|
||||||
<button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()" th:text="#{sign.clear}"></button>
|
|
||||||
<button id="save-signature" class="btn btn-outline-success mt-2" onclick="addDraggableFromPad()" th:text="#{sign.add}"></button>
|
|
||||||
<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) {
|
|
||||||
// code is from: https://github.com/szimek/signature_pad/issues/49#issuecomment-1104035775
|
|
||||||
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() {
|
|
||||||
// When zoomed out to less than 100%, for some very strange reason,
|
|
||||||
// some browsers report devicePixelRatio as less than 1
|
|
||||||
// and only part of the canvas is cleared then.
|
|
||||||
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);
|
|
||||||
|
|
||||||
// This library does not listen for canvas changes, so after the canvas is automatically
|
|
||||||
// cleared by the browser, SignaturePad#isEmpty might still return false, even though the
|
|
||||||
// canvas looks empty, because the internal data of this library wasn't cleared. To make sure
|
|
||||||
// that the state of this library is consistent with visual state of the canvas, you
|
|
||||||
// have to clear it manually.
|
|
||||||
signaturePad.clear();
|
|
||||||
}
|
|
||||||
new IntersectionObserver((entries, observer) => {
|
|
||||||
if (entries.some(entry => entry.intersectionRatio > 0)) {
|
|
||||||
resizeCanvas();
|
|
||||||
}
|
|
||||||
}).observe(signaturePadCanvas);
|
|
||||||
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
<div class="tab-container" th:title="#{sign.text}">
|
|
||||||
<label class="form-check-label" for="sigText" th:text="#{text}"></label>
|
|
||||||
<textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea>
|
|
||||||
<label th:text="#{font}"></label>
|
|
||||||
<select class="form-control" name="font" id="font-select">
|
|
||||||
<option th:each="font : ${fonts}" th:value="${font.name}" th:text="${font.name}" th:class="${font.name.toLowerCase()+'-font'}">
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<div class="margin-auto-parent">
|
|
||||||
<button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromText()" th:text="#{sign.add}"></button>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
<script>
|
|
||||||
const sigTextInput = document.getElementById('sigText');
|
|
||||||
const fontSelect = document.getElementById('font-select');
|
|
||||||
|
|
||||||
const updateOptionTexts = () => {
|
|
||||||
Array.from(fontSelect.options).forEach(option => {
|
|
||||||
const fontName = option.value.replace(/-regular$/i, '');
|
|
||||||
option.text = sigTextInput.value || fontName;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sigTextInput.addEventListener('input', updateOptionTexts);
|
|
||||||
|
|
||||||
fontSelect.addEventListener('change', (e) => {
|
|
||||||
e.target.style.fontFamily = e.target.value;
|
|
||||||
updateOptionTexts();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Manually trigger the change event
|
|
||||||
fontSelect.dispatchEvent(new Event('change'));
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- draggables box -->
|
|
||||||
<div id="box-drag-container" class="show-on-file-selected">
|
|
||||||
<canvas id="pdf-canvas"></canvas>
|
|
||||||
<script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
|
|
||||||
<script th:src="@{'/js/draggable-utils.js'}"></script>
|
|
||||||
<div class="draggable-buttons-box ignore-rtl">
|
|
||||||
<button class="btn btn-outline-secondary" onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
|
||||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/>
|
|
||||||
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()" style="margin-left:auto">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-left" viewBox="0 0 16 16">
|
|
||||||
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-right" viewBox="0 0 16 16">
|
|
||||||
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
|
||||||
</svg>
|
|
||||||
</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>
|
|
||||||
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>
|
||||||
|
|
||||||
|
<!-- pdf selector -->
|
||||||
|
<div
|
||||||
|
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 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
|
||||||
|
th:replace="~{fragments/common :: fileSelector(name='image-upload', 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>
|
||||||
|
|
||||||
|
<div class="tab-container drawing-pad-container" th:title="#{sign.draw}">
|
||||||
|
<canvas id="drawing-pad-canvas"></canvas>
|
||||||
|
<br>
|
||||||
|
<button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()"
|
||||||
|
th:text="#{sign.clear}"></button>
|
||||||
|
<button id="save-signature" class="btn btn-outline-success mt-2" onclick="addDraggableFromPad()"
|
||||||
|
th:text="#{sign.add}"></button>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-container" th:title="#{sign.text}">
|
||||||
|
<label class="form-check-label" for="sigText" th:text="#{text}"></label>
|
||||||
|
<textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea>
|
||||||
|
<label th:text="#{font}"></label>
|
||||||
|
<select class="form-control" name="font" id="font-select">
|
||||||
|
<option th:each="font : ${fonts}" th:value="${font.name}" th:text="${font.name}"
|
||||||
|
th:class="${font.name.toLowerCase()+'-font'}"></option>
|
||||||
|
</select>
|
||||||
|
<div class="margin-auto-parent">
|
||||||
|
<button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center"
|
||||||
|
onclick="addDraggableFromText()" th:text="#{sign.add}"></button>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- draggables box -->
|
||||||
|
<div id="box-drag-container" class="show-on-file-selected">
|
||||||
|
<canvas id="pdf-canvas"></canvas>
|
||||||
|
<script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
|
||||||
|
<script th:src="@{'/js/draggable-utils.js'}"></script>
|
||||||
|
<div class="draggable-buttons-box ignore-rtl">
|
||||||
|
<button class="btn btn-outline-secondary"
|
||||||
|
onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash"
|
||||||
|
viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z" />
|
||||||
|
<path
|
||||||
|
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary"
|
||||||
|
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()"
|
||||||
|
style="margin-left:auto">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
|
class="bi bi-chevron-left" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary"
|
||||||
|
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
|
class="bi bi-chevron-right" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
|
||||||
|
</svg>
|
||||||
|
</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>
|
||||||
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link the draggable.js file -->
|
||||||
|
<script src="/path/to/your/draggable.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user