Compare commits

..

2 Commits

Author SHA1 Message Date
Anthony Stirling
ffcd589035 Merge branch 'main' into updatesOct22 2024-10-22 00:26:22 +01:00
Anthony Stirling
909c79e856 Default terms and conditions to stirlingpdf.com 2024-10-22 00:25:21 +01:00
15 changed files with 350 additions and 384 deletions

View File

@@ -182,7 +182,7 @@ Stirling PDF currently supports 38!
| Dutch (Nederlands) (nl_NL) | ![88%](https://geps.dev/progress/88) | | Dutch (Nederlands) (nl_NL) | ![88%](https://geps.dev/progress/88) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![88%](https://geps.dev/progress/88) | | French (Français) (fr_FR) | ![85%](https://geps.dev/progress/85) |
| German (Deutsch) (de_DE) | ![94%](https://geps.dev/progress/94) | | German (Deutsch) (de_DE) | ![94%](https://geps.dev/progress/94) |
| Greek (Ελληνικά) (el_GR) | ![75%](https://geps.dev/progress/75) | | Greek (Ελληνικά) (el_GR) | ![75%](https://geps.dev/progress/75) |
| Hindi (हिंदी) (hi_IN) | ![72%](https://geps.dev/progress/72) | | Hindi (हिंदी) (hi_IN) | ![72%](https://geps.dev/progress/72) |

View File

@@ -22,7 +22,7 @@ ext {
} }
group = "stirling.software" group = "stirling.software"
version = "0.30.1" version = "0.30.0"
java { java {
// 17 is lowest but we support and recommend 21 // 17 is lowest but we support and recommend 21

View File

@@ -1,5 +1,5 @@
apiVersion: v2 apiVersion: v2
appVersion: 0.30.1 appVersion: 0.30.0
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

View File

@@ -7,15 +7,16 @@ 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")

View File

@@ -15,7 +15,6 @@ 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;
@@ -163,14 +162,12 @@ 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

View File

@@ -32,7 +32,6 @@ 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");
} }
} }

View File

@@ -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=Populaire navbar.sections.popular=Popular
############# #############
# 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=Supprimer l'utilisateur adminUserSettings.deleteUser=Delete User
adminUserSettings.confirmDeleteUser=Voulez vous vraiment supprimer l'utilisateur ? adminUserSettings.confirmDeleteUser=Should the user be deleted?
adminUserSettings.confirmChangeUserStatus=Voulez vous vraiment déactiver/réactiver l'utilisateur ? adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
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 lutilisateur à changer son nom dutil
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=Éditer son propre profil adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=Utilisateur activé adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=Utilisateur désactivé adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Utilisateurs actifs : adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Utilisateurs désactivés : adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Utilisateurs au total : adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Dernière requête adminUserSettings.lastRequest=Last Request
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=Votre session a expiré. Veuillez recharger la page et réessayer. session.expired=Your session has expired. Please refresh the page and try again.
############# #############
# 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 nimporte quel fichier en PDF (DOCX, PNG, XLS, PPT, TXT, etc.). home.fileToPDF.desc=Convertissez presque nimporte quel fichiers en PDF (DOCX, PNG, XLS, PPT, TXT et plus).
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=Supprimer la signature par certificat home.removeCertSign.title=Remove Certificate Sign
home.removeCertSign.desc=Supprimez la signature par certificat d'un PDF home.removeCertSign.desc=Remove certificate signature from PDF
removeCertSign.tags=signer,chiffrer,certificat,authenticate,PEM,P12,official,decrypt removeCertSign.tags=authenticate,PEM,P12,official,decrypt
home.pageLayout.title=Fusionner des pages home.pageLayout.title=Fusionner des pages
home.pageLayout.desc=Fusionnez plusieurs pages dun PDF en une seule. home.pageLayout.desc=Fusionnez plusieurs pages dun 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=Supprimer les images home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Supprimez les images d'un PDF pour réduire sa taille home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Images,Remove Image,Page operations,Back end,server side removeImagePdf.tags=Remove Image,Page operations,Back end,server side
home.splitPdfByChapters.title=Séparer un PDF par chapitres home.splitPdfByChapters.title=Split PDF by Chapters
home.splitPdfByChapters.desc=Séparez un PDF en fichiers multiples en fonction de sa structure par chapitres. home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
splitPdfByChapters.tags=séparer,chapitres,split,chapters,bookmarks,organize splitPdfByChapters.tags=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=Utilise Calibre BookToPDF.credit=Utiliser 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=Utilise Calibre PDFToBook.credit=Utiliser 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=Le PDF contient une signature numérique. Elle sera supprimée dans l'étape suivante. pdfToPDFA.pdfWithDigitalSignature=The PDF contains a digital signature. This will be removed in the next step.
#PDFToWord #PDFToWord

View File

@@ -749,8 +749,8 @@
}, },
{ {
"moduleName": "org.commonmark:commonmark", "moduleName": "org.commonmark:commonmark",
"moduleVersion": "0.24.0", "moduleVersion": "0.23.0",
"moduleLicense": "BSD-2-Clause", "moduleLicense": "BSD 2-Clause License",
"moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause" "moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause"
}, },
{ {

View File

@@ -1,9 +1,7 @@
select#font-select, select#font-select,
select#font-select option { select#font-select option {
height: 60px; height: 60px; /* Adjust as needed */
/* Adjust as needed */ font-size: 30px; /* Adjust as needed */
font-size: 30px;
/* Adjust as needed */
} }
.drawing-pad-container { .drawing-pad-container {
@@ -15,12 +13,10 @@ 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;
@@ -28,37 +24,16 @@ select#font-select option {
width: 100%; width: 100%;
display: flex; display: flex;
gap: 5px; gap: 5px;
z-index: 5;
} }
.draggable-buttons-box > button {
.draggable-buttons-box>button { z-index: 10;
z-index: 4;
background-color: rgba(13, 110, 253, 0.1); background-color: rgba(13, 110, 253, 0.1);
} }
.draggable-canvas { .draggable-canvas {
border: 2px solid #3498db; border: 1px solid red;
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 */
} }

View File

@@ -1,36 +0,0 @@
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);
}

View File

@@ -0,0 +1,39 @@
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;

View File

@@ -360,7 +360,7 @@
</div> </div>
</li> </li>
<li class="nav-item" th:if="${!@runningEE}"> <li class="nav-item">
<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>

View File

@@ -374,9 +374,9 @@
<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 justify-content-between"> <div class="modal-footer">
<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> <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>
</div> </div>
</div> </div>
</div> </div>
@@ -426,34 +426,24 @@
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, 10, 15, 22, 30, 50, 75, 100, 150, 200]; const viewThresholds = [5, 15, 30, 50, 75, 100, 150, 200];
// Check if survey version changed and reset page views if it did
const storedVersion = localStorage.getItem('surveyVersion');
if (storedVersion && storedVersion !== surveyVersion) {
localStorage.setItem('pageViews', '0');
}
let pageViews = parseInt(localStorage.getItem('pageViews') || '0'); let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
pageViews++; pageViews++;
localStorage.setItem('pageViews', pageViews.toString()); localStorage.setItem('pageViews', pageViews.toString());
function shouldShowSurvey() { function shouldShowSurvey() {
if (localStorage.getItem('dontShowSurvey') === 'true' || if (localStorage.getItem('dontShowSurvey') === 'true' || localStorage.getItem('surveyTaken') === 'true') {
localStorage.getItem('surveyTaken') === 'true') {
return false; return false;
} }
// If survey version changed and we hit a threshold, show the survey if (localStorage.getItem('surveyVersion') !== surveyVersion) {
if (localStorage.getItem('surveyVersion') !== surveyVersion &&
viewThresholds.includes(pageViews)) {
return true; return true;
} }
@@ -479,7 +469,7 @@ document.addEventListener("DOMContentLoaded", function() {
localStorage.setItem('surveyVersion', surveyVersion); localStorage.setItem('surveyVersion', surveyVersion);
modal.hide(); modal.hide();
}); });
}); });
</script> </script>

View File

@@ -80,6 +80,7 @@
<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';
@@ -92,6 +93,7 @@
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(

View File

@@ -1,11 +1,8 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" <html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
xmlns:th="https://www.thymeleaf.org"> <head>
<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 {
@@ -14,17 +11,18 @@
} }
#font-select option[value="[[${font.name}]]"] { #font-select option[value="[[${font.name}]]"] {
font-family: "[[${font.name}]]", font-family: "[[${font.name}]]", cursive;
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/signature_pad.umd.min.js'}"></script>
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script> <script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
</head> </head>
<body> <body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
@@ -38,9 +36,7 @@
</div> </div>
<!-- pdf selector --> <!-- pdf selector -->
<div <div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, disableMultipleFiles=true, accept='application/pdf')}"></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 type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script> <script>
let originalFileName = ''; let originalFileName = '';
@@ -49,31 +45,30 @@
if (file) { if (file) {
originalFileName = file.name.replace(/\.[^/.]+$/, ""); originalFileName = file.name.replace(/\.[^/.]+$/, "");
const pdfData = await file.arrayBuffer(); const pdfData = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'; pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise; const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
await DraggableUtils.renderPage(pdfDoc, 0); await DraggableUtils.renderPage(pdfDoc, 0);
document.querySelectorAll(".show-on-file-selected").forEach(el => { document.querySelectorAll(".show-on-file-selected").forEach(el => {
el.style.cssText = ''; el.style.cssText = '';
}); })
} }
}); });
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".show-on-file-selected").forEach(el => { document.querySelectorAll(".show-on-file-selected").forEach(el => {
el.style.cssText = "display:none !important"; el.style.cssText = "display:none !important";
}); })
}); });
</script> </script>
<div class="tab-group show-on-file-selected"> <div class="tab-group show-on-file-selected">
<div class="tab-container" th:title="#{sign.upload}"> <div class="tab-container" th:title="#{sign.upload}">
<div <div th:replace="~{fragments/common :: fileSelector(name='image-upload', multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}"></div>
th:replace="~{fragments/common :: fileSelector(name='image-upload', multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
</div>
<script> <script>
const imageUpload = document.querySelector('input[name=image-upload]'); const imageUpload = document.querySelector('input[name=image-upload]');
imageUpload.addEventListener('change', e => { imageUpload.addEventListener('change', e => {
if (!e.target.files) return; if(!e.target.files) {
return;
}
for (const imageFile of e.target.files) { for (const imageFile of e.target.files) {
var reader = new FileReader(); var reader = new FileReader();
reader.readAsDataURL(imageFile); reader.readAsDataURL(imageFile);
@@ -84,14 +79,11 @@
}); });
</script> </script>
</div> </div>
<div class="tab-container drawing-pad-container" th:title="#{sign.draw}"> <div class="tab-container drawing-pad-container" th:title="#{sign.draw}">
<canvas id="drawing-pad-canvas"></canvas> <canvas id="drawing-pad-canvas"></canvas>
<br> <br>
<button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()" <button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()" th:text="#{sign.clear}"></button>
th:text="#{sign.clear}"></button> <button id="save-signature" class="btn btn-outline-success mt-2" onclick="addDraggableFromPad()" th:text="#{sign.add}"></button>
<button id="save-signature" class="btn btn-outline-success mt-2" onclick="addDraggableFromPad()"
th:text="#{sign.add}"></button>
<script> <script>
const signaturePadCanvas = document.getElementById('drawing-pad-canvas'); const signaturePadCanvas = document.getElementById('drawing-pad-canvas');
const signaturePad = new SignaturePad(signaturePadCanvas, { const signaturePad = new SignaturePad(signaturePadCanvas, {
@@ -99,20 +91,20 @@
maxWidth: 2, maxWidth: 2,
penColor: 'black', penColor: 'black',
}); });
function addDraggableFromPad() { function addDraggableFromPad() {
if (signaturePad.isEmpty()) return; if (signaturePad.isEmpty()) return;
const startTime = Date.now(); const startTime = Date.now();
const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas); const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas)
console.log(Date.now() - startTime); console.log(Date.now() - startTime);
DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl); DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl);
} }
function getCroppedCanvasDataUrl(canvas) { function getCroppedCanvasDataUrl(canvas) {
// code is from: https://github.com/szimek/signature_pad/issues/49#issuecomment-1104035775
let originalCtx = canvas.getContext('2d'); let originalCtx = canvas.getContext('2d');
let originalWidth = canvas.width; let originalWidth = canvas.width;
let originalHeight = canvas.height; let originalHeight = canvas.height;
let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight); let imageData = originalCtx.getImageData(0,0, originalWidth, originalHeight);
let minX = originalWidth + 1, maxX = -1, minY = originalHeight + 1, maxY = -1, x = 0, y = 0, currentPixelColorValueIndex; let minX = originalWidth + 1, maxX = -1, minY = originalHeight + 1, maxY = -1, x = 0, y = 0, currentPixelColorValueIndex;
@@ -143,8 +135,10 @@
return croppedCanvas.toDataURL(); return croppedCanvas.toDataURL();
} }
function resizeCanvas() { 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 ratio = Math.max(window.devicePixelRatio || 1, 1);
var additionalFactor = 10; var additionalFactor = 10;
@@ -152,30 +146,31 @@
signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio * additionalFactor; signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio * additionalFactor;
signaturePadCanvas.getContext("2d").scale(ratio * additionalFactor, 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(); signaturePad.clear();
} }
new IntersectionObserver((entries, observer) => { new IntersectionObserver((entries, observer) => {
if (entries.some(entry => entry.intersectionRatio > 0)) { if (entries.some(entry => entry.intersectionRatio > 0)) {
resizeCanvas(); resizeCanvas();
} }
}).observe(signaturePadCanvas); }).observe(signaturePadCanvas);
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas); new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
</script> </script>
</div> </div>
<div class="tab-container" th:title="#{sign.text}"> <div class="tab-container" th:title="#{sign.text}">
<label class="form-check-label" for="sigText" th:text="#{text}"></label> <label class="form-check-label" for="sigText" th:text="#{text}"></label>
<textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea> <textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea>
<label th:text="#{font}"></label> <label th:text="#{font}"></label>
<select class="form-control" name="font" id="font-select"> <select class="form-control" name="font" id="font-select">
<option th:each="font : ${fonts}" th:value="${font.name}" th:text="${font.name}" <option th:each="font : ${fonts}" th:value="${font.name}" th:text="${font.name}" th:class="${font.name.toLowerCase()+'-font'}">
th:class="${font.name.toLowerCase()+'-font'}"></option> </option>
</select> </select>
<div class="margin-auto-parent"> <div class="margin-auto-parent">
<button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center" <button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromText()" th:text="#{sign.add}"></button>
onclick="addDraggableFromText()" th:text="#{sign.add}"></button>
</div> </div>
<script> <script>
function addDraggableFromText() { function addDraggableFromText() {
@@ -192,7 +187,7 @@
let paragraphs = sigText.split(/\r?\n/); let paragraphs = sigText.split(/\r?\n/);
canvas.width = textWidth; canvas.width = textWidth;
canvas.height = paragraphs.length * textHeight * 1.35; // for tails canvas.height = paragraphs.length * textHeight*1.35; //for tails
ctx.font = `${fontSize}px ${font}`; ctx.font = `${fontSize}px ${font}`;
ctx.textBaseline = 'top'; ctx.textBaseline = 'top';
@@ -208,6 +203,27 @@
DraggableUtils.createDraggableCanvasFromUrl(dataURL); DraggableUtils.createDraggableCanvasFromUrl(dataURL);
} }
</script> </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>
</div> </div>
@@ -217,31 +233,20 @@
<script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script> <script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
<script th:src="@{'/js/draggable-utils.js'}"></script> <script th:src="@{'/js/draggable-utils.js'}"></script>
<div class="draggable-buttons-box ignore-rtl"> <div class="draggable-buttons-box ignore-rtl">
<button class="btn btn-outline-secondary" <button class="btn btn-outline-secondary" onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
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">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" <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"/>
viewBox="0 0 16 16"> <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"/>
<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> </svg>
</button> </button>
<button class="btn btn-outline-secondary" <button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()" style="margin-left:auto">
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()" <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-left" viewBox="0 0 16 16">
style="margin-left:auto"> <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 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> </svg>
</button> </button>
<button class="btn btn-outline-secondary" <button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
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">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" <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"/>
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> </svg>
</button> </button>
</div> </div>
@@ -249,12 +254,11 @@
<!-- download button --> <!-- download button -->
<div class="margin-auto-parent"> <div class="margin-auto-parent">
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center" <button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center" th:text="#{downloadPdf}"></button>
th:text="#{downloadPdf}"></button>
</div> </div>
<script> <script>
document.getElementById("download-pdf").addEventListener('click', async () => { document.getElementById("download-pdf").addEventListener('click', async() => {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument(); const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
const modifiedPdfBytes = await modifiedPdf.save(); const modifiedPdfBytes = await modifiedPdf.save();
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' }); const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
@@ -268,12 +272,7 @@
</div> </div>
</div> </div>
</div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block> <th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
</body>
<!-- Link the draggable.js file -->
<script src="/path/to/your/draggable.js"></script>
</body>
</html> </html>