refactor: move modules under app/ directory and update file paths (#3938)

# Description of Changes

- **What was changed:**  
- Renamed top-level directories: `stirling-pdf` → `app/core`, `common` →
`app/common`, `proprietary` → `app/proprietary`.
- Updated all path references in `.gitattributes`, GitHub workflows
(`.github/workflows/*`), scripts (`.github/scripts/*`), `.gitignore`,
Dockerfiles, license files, and template settings to reflect the new
structure.
- Added a new CI job `check-generateOpenApiDocs` to generate and upload
OpenAPI documentation.
- Removed redundant `@Autowired` annotations from `TempFileShutdownHook`
and `UnlockPDFFormsController`.
- Minor formatting and comment adjustments in YAML templates and
resource files.

- **Why the change was made:**  
- To introduce a clear `app/` directory hierarchy for core, common, and
proprietary modules, improving organization and maintainability.
- To ensure continuous integration and Docker builds continue to work
seamlessly with the reorganized structure.
- To automate OpenAPI documentation generation as part of the CI
pipeline.

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
Ludy
2025-07-14 20:53:11 +01:00
committed by GitHub
parent 38b53d7cc1
commit 299d52c517
1155 changed files with 219 additions and 276 deletions
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title='<3')}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,474 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{account.title})}"></th:block>
<link rel="stylesheet" th:href="@{/css/modern-tables.css}">
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div class="data-container">
<div class="data-panel">
<div class="data-header">
<h1 class="data-title">
<span class="data-icon">
<span class="material-symbols-rounded">settings_account_box</span>
</span>
<span th:text="#{account.accountSettings}">User Settings</span>
</h1>
</div>
<div class="data-body">
<div th:if="${messageType}" class="alert alert-danger data-mb-3">
<span th:text="#{${messageType}}">Default message if not found</span>
</div>
<div th:if="${error}" class="alert alert-danger data-mb-3" role="alert">
<span th:text="${error}">Error Message</span>
</div>
<!-- Admin Settings Banner (for admins only) -->
<div th:if="${role == 'ROLE_ADMIN'}" class="data-panel data-mb-3" style="background-color: var(--md-sys-color-secondary-container);">
<div class="data-body" style="display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.5rem; background-color: var(--md-sys-color-secondary-container);">
<div style="display: flex; align-items: center; gap: 1rem;">
<span class="material-symbols-rounded" style="font-size: 2rem; color: var(--md-sys-color-secondary);">
admin_panel_settings
</span>
<div>
<h4 style="margin: 0; color: var(--md-sys-color-secondary);" th:text="#{account.adminTitle}">Administrator Tools</h4>
<p style="margin: 0.25rem 0 0 0; color: var(--md-sys-color-secondary);" th:text="#{account.adminNotif}">You have admin privileges. Access system settings and user management.</p>
</div>
</div>
<a class="data-btn" th:href="@{'/adminSettings'}" role="button" target="_blank"
style="background-color: var(--md-sys-color-secondary); color: var(--md-sys-color-on-secondary); display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.625rem 1.25rem; border-radius: 0.5rem; font-weight: 500; border: none; cursor: pointer; text-decoration: none;">
<span class="material-symbols-rounded">admin_panel_settings</span>
<span th:text="#{account.adminSettings}">Admin Settings</span>
</a>
</div>
</div>
<!-- Account Management Buttons -->
<th:block th:if="not ${oAuth2Login} or not ${saml2Login}">
<div class="data-section-title">Account Management</div>
<div class="data-actions data-actions-start data-mb-3">
<button class="data-btn data-btn-primary" data-bs-toggle="modal" data-bs-target="#changeUsernameModal">
<span class="material-symbols-rounded">edit</span>
<span th:text="#{account.changeUsername}">Change Username</span>
</button>
<button class="data-btn data-btn-primary" data-bs-toggle="modal" data-bs-target="#changePasswordModal">
<span class="material-symbols-rounded">key</span>
<span th:text="#{account.changePassword}">Change Password</span>
</button>
</div>
</th:block>
<!-- API Key Section -->
<div class="data-section-title" th:text="#{account.yourApiKey}">API Key</div>
<div class="data-panel data-mb-3">
<div class="data-header">
<h5 class="data-title">
<span class="data-icon">
<span class="material-symbols-rounded">key</span>
</span>
<span th:text="#{account.yourApiKey}">API Key</span>
</h5>
</div>
<div class="data-body">
<div style="display: flex; gap: 0.5rem;">
<input class="data-form-control" id="apiKey" th:placeholder="#{account.yourApiKey}" readonly style="flex: 1;">
<button class="data-btn data-btn-secondary" id="copyBtn" type="button" onclick="copyToClipboard()" title="Copy to clipboard">
<span class="material-symbols-rounded">content_copy</span>
</button>
<button class="data-btn data-btn-secondary" id="showBtn" type="button" onclick="showApiKey()" title="Show/hide API key">
<span class="material-symbols-rounded" id="eyeIcon">visibility</span>
</button>
<button class="data-btn data-btn-secondary" id="refreshBtn" type="button" onclick="refreshApiKey()" title="Refresh API key">
<span class="material-symbols-rounded">refresh</span>
</button>
</div>
</div>
</div>
<!-- Settings Sync Section -->
<div class="data-section-title" th:text="#{account.syncTitle}">Sync browser settings with Account</div>
<div class="data-panel data-mb-3">
<div class="data-header">
<h5 class="data-title">
<span class="data-icon">
<span class="material-symbols-rounded">sync</span>
</span>
<span th:text="#{account.settingsCompare}">Settings Comparison</span>
</h5>
</div>
<div class="data-body">
<div class="table-responsive">
<table id="settingsTable" class="data-table">
<thead>
<tr>
<th scope="col" th:text="#{account.property}">Property</th>
<th scope="col" th:text="#{account.accountSettings}">Account Setting</th>
<th scope="col" th:text="#{account.webBrowserSettings}">Web Browser Setting</th>
</tr>
</thead>
<tbody>
<!-- This will be dynamically populated by JavaScript -->
</tbody>
</table>
</div>
<div class="data-actions data-mt-3">
<button id="syncToBrowser" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">cloud_download</span>
<span th:text="#{account.syncToBrowser}">Sync Account -> Browser</span>
</button>
<button id="syncToAccount" class="data-btn data-btn-secondary">
<span class="material-symbols-rounded">cloud_upload</span>
<span th:text="#{account.syncToAccount}">Sync Account <- Browser</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Change Username Modal -->
<div class="modal fade" id="changeUsernameModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="formsavechangeusername" th:action="@{'/api/v1/user/change-username'}" method="post" class="modal-content data-modal">
<div class="data-modal-header">
<h5 class="data-modal-title">
<span class="data-icon">
<span class="material-symbols-rounded">edit</span>
</span>
<span th:text="#{account.changeUsername}">Change Username</span>
</h5>
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="data-modal-body">
<div class="data-form-group">
<label for="newUsername" class="data-form-label" th:text="#{account.newUsername}">New Username</label>
<input type="text" class="data-form-control" name="newUsername" id="newUsername" th:placeholder="#{account.newUsername}">
<span id="usernameError" style="display: none; color: var(--md-sys-color-error);" th:text="#{invalidUsernameMessage}">Invalid username!</span>
</div>
<div class="data-form-group">
<label for="currentPasswordChangeUsername" class="data-form-label" th:text="#{password}">Password</label>
<input type="password" class="data-form-control" name="currentPasswordChangeUsername" id="currentPasswordChangeUsername" th:placeholder="#{password}">
</div>
<div class="data-form-actions">
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
<span class="material-symbols-rounded">close</span>
<span th:text="#{cancel}">Cancel</span>
</button>
<button type="submit" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">check</span>
<span th:text="#{account.changeUsername}">Change Username</span>
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Change Password Modal -->
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="formsavechangepassword" th:action="@{'/api/v1/user/change-password'}" method="post" class="modal-content data-modal">
<div class="data-modal-header">
<h5 class="data-modal-title">
<span class="data-icon">
<span class="material-symbols-rounded">key</span>
</span>
<span th:text="#{account.changePassword}">Change Password</span>
</h5>
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="data-modal-body">
<div class="data-form-group">
<label for="currentPassword" class="data-form-label" th:text="#{account.oldPassword}">Old Password</label>
<input type="password" class="data-form-control" name="currentPassword" id="currentPassword" th:placeholder="#{account.oldPassword}">
</div>
<div class="data-form-group">
<label for="newPassword" class="data-form-label" th:text="#{account.newPassword}">New Password</label>
<input type="password" class="data-form-control" name="newPassword" id="newPassword" th:placeholder="#{account.newPassword}">
</div>
<div class="data-form-group">
<label for="confirmNewPassword" class="data-form-label" th:text="#{account.confirmNewPassword}">Confirm New Password</label>
<input type="password" class="data-form-control" name="confirmNewPassword" id="confirmNewPassword" th:placeholder="#{account.confirmNewPassword}">
<span id="confirmPasswordError" style="display: none; color: var(--md-sys-color-error);" th:text="#{confirmPasswordErrorMessage}">New Password and Confirm New Password must match.</span>
</div>
<div class="data-form-actions">
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
<span class="material-symbols-rounded">close</span>
<span th:text="#{cancel}">Cancel</span>
</button>
<button type="submit" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">check</span>
<span th:text="#{account.changePassword}">Change Password</span>
</button>
</div>
</div>
</form>
</div>
</div>
<!-- JavaScript for validation -->
<script th:inline="javascript">
jQuery.validator.addMethod("usernamePattern", function(value, element) {
// Regular expression for user name: Min. 3 characters, max. 50 characters
const regexUsername = /^[a-zA-Z0-9](?!.*[-@._+]{2,})([a-zA-Z0-9@._+-]{1,48})[a-zA-Z0-9]$/;
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
const regexEmail = /^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.[A-Za-z]{2,})$/;
// Check if the field is optional or meets the requirements
return this.optional(element) || regexUsername.test(value) || regexEmail.test(value);
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
$(document).ready(function() {
$.validator.addMethod("passwordMatch", function(value, element) {
return $('#newPassword').val() === $('#confirmNewPassword').val();
}, /*[[#{confirmPasswordErrorMessage}]]*/ "New Password and Confirm New Password must match.");
$('#formsavechangepassword').validate({
rules: {
currentPassword: {
required: true
},
newPassword: {
required: true
},
confirmNewPassword: {
required: true,
passwordMatch: true
}
},
errorPlacement: function(error, element) {
if (element.attr("name") === "newPassword" || element.attr("name") === "confirmNewPassword") {
$("#confirmPasswordError").text(error.text()).show();
} else {
error.insertAfter(element);
}
},
success: function(label, element) {
if ($(element).attr("name") === "newPassword" || $(element).attr("name") === "confirmNewPassword") {
$("#confirmPasswordError").hide();
}
}
});
$('#formsavechangeusername').validate({
rules: {
newUsername: {
required: true,
usernamePattern: true
},
currentPasswordChangeUsername: {
required: true
}
},
messages: {
newUsername: {
usernamePattern: /*[[#{invalidUsernameMessage}]]*/ "Invalid username format"
},
},
errorPlacement: function(error, element) {
if (element.attr("name") === "newUsername") {
$("#usernameError").text(error.text()).show();
} else {
error.insertAfter(element);
}
},
success: function(label, element) {
if ($(element).attr("name") === "newUsername") {
$("#usernameError").hide();
}
}
});
});
</script>
<!-- JavaScript for API Key -->
<script th:inline="javascript">
function copyToClipboard() {
const apiKeyElement = document.getElementById("apiKey");
apiKeyElement.select();
document.execCommand("copy");
}
function showApiKey() {
const apiKeyElement = document.getElementById("apiKey");
const copyBtn = document.getElementById("copyBtn");
const eyeIcon = document.getElementById("eyeIcon");
if (apiKeyElement.type === "password") {
apiKeyElement.type = "text";
eyeIcon.textContent = "visibility_off";
copyBtn.disabled = false; // Enable copy button when API key is visible
} else {
apiKeyElement.type = "password";
eyeIcon.textContent = "visibility";
copyBtn.disabled = true; // Disable copy button when API key is hidden
}
}
document.addEventListener("DOMContentLoaded", async function() {
try {
/*<![CDATA[*/
const urlGetApiKey = /*[[@{/api/v1/user/get-api-key}]]*/ "/api/v1/user/get-api-key";
/*]]>*/
let response = await window.fetchWithCsrf(urlGetApiKey, { method: 'POST' });
if (response.status === 200) {
let apiKey = await response.text();
manageUIState(apiKey);
} else {
manageUIState(null);
}
} catch (error) {
console.error('There was an error:', error);
}
finally {
showApiKey();
}
});
async function refreshApiKey() {
try {
/*<![CDATA[*/
const urlUpdateApiKey = /*[[@{/api/v1/user/update-api-key}]]*/ "/api/v1/user/update-api-key";
/*]]>*/
let response = await window.fetchWithCsrf(urlUpdateApiKey, { method: 'POST' });
if (response.status === 200) {
let apiKey = await response.text();
manageUIState(apiKey);
document.getElementById("apiKey").type = 'text';
document.getElementById("copyBtn").disabled = false;
} else {
alert('Error refreshing API key.');
}
} catch (error) {
console.error('There was an error:', error);
}
}
function manageUIState(apiKey) {
const apiKeyElement = document.getElementById("apiKey");
const showBtn = document.getElementById("showBtn");
const copyBtn = document.getElementById("copyBtn");
if (apiKey && apiKey.trim().length > 0) {
apiKeyElement.value = apiKey;
showBtn.disabled = false;
copyBtn.disabled = false;
} else {
apiKeyElement.value = "";
showBtn.disabled = true;
copyBtn.disabled = true;
}
}
</script>
<!-- JavaScript for Settings Sync -->
<script th:inline="javascript">
document.addEventListener("DOMContentLoaded", async function() {
const settingsTableBody = document.querySelector("#settingsTable tbody");
// Helper function to check if a key should be ignored
function shouldIgnoreKey(key) {
return key === 'debug' ||
key === '0' ||
key === '1' ||
key.includes('pdfjs') ||
key.includes('clientSubmissionOrder') ||
key.includes('lastSubmitTime') ||
key.includes('lastClientId') ||
key.includes('posthog') || key.includes('ssoRedirectAttempts') || key.includes('lastRedirectAttempt') || key.includes('surveyVersion') ||
key.includes('pageViews');
}
/*<![CDATA[*/
var accountSettingsString = /*[[${settings}]]*/ {};
/*]]>*/
var accountSettings = JSON.parse(accountSettingsString);
let allKeys = new Set([...Object.keys(accountSettings), ...Object.keys(localStorage)]);
allKeys.forEach(key => {
if(shouldIgnoreKey(key)) return; // Using our helper function
const accountValue = accountSettings[key] || '-';
const browserValue = localStorage.getItem(key) || '-';
const row = settingsTableBody.insertRow();
const propertyCell = row.insertCell(0);
const accountCell = row.insertCell(1);
const browserCell = row.insertCell(2);
propertyCell.textContent = key;
accountCell.textContent = accountValue;
browserCell.textContent = browserValue;
});
document.getElementById('syncToBrowser').addEventListener('click', function() {
// First, clear the local storage
localStorage.clear();
// Then, set the account settings to local storage
for (let key in accountSettings) {
if(!shouldIgnoreKey(key)) { // Using our helper function
localStorage.setItem(key, accountSettings[key]);
}
}
location.reload(); // Refresh the page after sync
});
document.getElementById('syncToAccount').addEventListener('click', async function() {
/*<![CDATA[*/
const urlUpdateUserSettings = /*[[@{/api/v1/user/updateUserSettings}]]*/ "/api/v1/user/updateUserSettings";
/*]]>*/
let settings = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if(!shouldIgnoreKey(key)) { // Using our helper function
settings[key] = localStorage.getItem(key);
}
}
try {
const response = await window.fetchWithCsrf(urlUpdateUserSettings, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(settings)
});
if (response.ok) {
location.reload();
} else {
alert('Error syncing settings to account');
}
} catch (error) {
console.error('Error:', error);
alert('Error syncing settings to account');
}
});
});
</script>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,458 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}"></th:block>
<link rel="stylesheet" th:href="@{/css/modern-tables.css}">
<style>
.active-user {
color: var(--md-sys-color-tertiary);
font-weight: 600;
}
</style>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div class="data-container">
<div class="data-panel">
<div class="data-header">
<h1 class="data-title">
<span class="data-icon">
<span class="material-symbols-rounded">manage_accounts</span>
</span>
<span th:text="#{adminUserSettings.header}">Admin User Control Settings</span>
</h1>
</div>
<div class="data-body">
<!-- User Stats Banner -->
<div class="data-panel data-mb-3" style="background-color: var(--md-sys-color-primary-container);">
<div class="data-body" style="padding: 1.25rem;">
<div style="display: flex; flex-wrap: wrap; justify-content: space-around; align-items: center; gap: 1.5rem;">
<div style="display: flex; align-items: center; gap: 0.75rem;">
<span class="material-symbols-rounded" style="font-size: 2.25rem; color: var(--md-sys-color-primary);">
group
</span>
<div>
<div style="color: var(--md-sys-color-primary); font-size: 0.875rem; font-weight: 500;" th:text="#{adminUserSettings.totalUsers}">Total Users</div>
<div style="color: var(--md-sys-color-primary); font-size: 1.5rem; font-weight: 700;">
<span th:text="${totalUsers}"></span>
<span th:if="${@runningProOrHigher}" th:text="'/' + ${maxPaidUsers}" style="font-size: 1rem;"></span>
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem;">
<span class="material-symbols-rounded" style="font-size: 2.25rem; color: var(--md-sys-color-primary);">
check_circle
</span>
<div>
<div style="color: var(--md-sys-color-primary); font-size: 0.875rem; font-weight: 500;" th:text="#{adminUserSettings.activeUsers}">Active Users</div>
<div style="color: var(--md-sys-color-primary); font-size: 1.5rem; font-weight: 700;" th:text="${activeUsers}"></div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem;">
<span class="material-symbols-rounded" style="font-size: 2.25rem; color: var(--md-sys-color-primary);">
person_off
</span>
<div>
<div style="color: var(--md-sys-color-primary); font-size: 0.875rem; font-weight: 500;" th:text="#{adminUserSettings.disabledUsers}">Disabled Users</div>
<div style="color: var(--md-sys-color-primary); font-size: 1.5rem; font-weight: 700;" th:text="${disabledUsers}"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Alert Messages -->
<div th:if="${addMessage}" th:class="${#strings.contains(addMessage, 'Successfully') or #strings.contains(addMessage, 'Created') ? 'alert alert-success data-mb-3' : 'alert alert-danger data-mb-3'}">
<span th:text="#{${addMessage}}">Default message if not found</span>
</div>
<div th:if="${changeMessage}" th:class="${#strings.contains(changeMessage, 'Successfully') or #strings.contains(changeMessage, 'Created') ? 'alert alert-success data-mb-3' : 'alert alert-danger data-mb-3'}">
<span th:text="#{${changeMessage}}">Default message if not found</span>
</div>
<div th:if="${deleteMessage}" th:class="${#strings.contains(deleteMessage, 'Successfully') or #strings.contains(deleteMessage, 'Created') ? 'alert alert-success data-mb-3' : 'alert alert-danger data-mb-3'}">
<span th:text="#{${deleteMessage}}">Default message if not found</span>
</div>
<!-- Admin Actions -->
<div class="data-section-title">User Management</div>
<div class="data-actions data-mb-3">
<button
th:data-bs-toggle="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? null : 'modal'"
th:data-bs-target="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? null : '#addUserModal'"
th:class="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? 'data-btn data-btn-danger' : 'data-btn data-btn-primary'"
th:title="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? #{adminUserSettings.maxUsersReached} : #{adminUserSettings.addUser}">
<span class="material-symbols-rounded">person_add</span>
<span th:text="#{adminUserSettings.addUser}">Add New User</span>
</button>
<a th:href="@{'/teams'}" class="data-btn data-btn-secondary" th:title="#{adminUserSettings.teams}">
<span class="material-symbols-rounded">group</span>
<span th:text="#{adminUserSettings.teams}">Manage Teams</span>
</a>
<button
data-bs-toggle="modal"
data-bs-target="#changeUserRoleModal"
class="data-btn data-btn-secondary"
th:title="#{adminUserSettings.changeUserRole}">
<span class="material-symbols-rounded">edit</span>
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
</button>
<a th:href="@{'/usage'}" th:if="${@runningEE}" class="data-btn data-btn-secondary" th:title="#{adminUserSettings.usage}">
<span class="material-symbols-rounded">analytics</span>
<span th:text="#{adminUserSettings.usage}">Usage Statistics</span>
</a>
<a href="/audit" th:if="${@runningEE}" class="data-btn data-btn-secondary" title="Audit Dashboard">
<span class="material-symbols-rounded">security</span>
<span>Audit Dashboard</span>
</a>
</div>
<!-- Users Table -->
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col" th:title="#{username}" class="text-overflow" th:text="#{username}">Username</th>
<th scope="col" th:title="#{adminUserSettings.team}" class="text-overflow" th:text="#{adminUserSettings.team}">Team</th>
<th scope="col" th:title="#{adminUserSettings.role}" class="text-overflow" th:text="#{adminUserSettings.role}">Roles</th>
<th scope="col" th:title="#{adminUserSettings.authenticated}" class="text-overflow" th:text="#{adminUserSettings.authenticated}">Authenticated</th>
<th scope="col" th:title="#{adminUserSettings.lastRequest}" class="text-overflow" th:text="#{adminUserSettings.lastRequest}">Last Request</th>
<th scope="col" th:title="#{adminUserSettings.actions}" class="text-overflow" th:text="#{adminUserSettings.actions}">Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<td th:text="${user.id}" th:title="${user.id}" class="text-overflow"></td>
<td th:text="${user.username}" th:title="${user.username}" class="text-overflow" th:classappend="${userSessions[user.username] ? 'active-user' : ''}"></td>
<td th:text="${user.team != null ? user.team.name : '—'}" th:title="${user.team != null ? user.team.name : '—'}" class="text-overflow"></td>
<td>
<span class="data-badge" style="background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-secondary); padding: 0.25rem 0.5rem; border-radius: 1rem; font-size: 0.875rem; display: inline-flex; align-items: center; gap: 0.25rem;">
<span class="material-symbols-rounded" style="font-size: 1rem;">shield</span>
<span th:text="#{${user.roleName}}" th:title="#{${user.roleName}}" class="text-overflow">Role</span>
</span>
</td>
<td th:text="${user.authenticationType}" th:title="${user.authenticationType}"></td>
<td th:text="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}" th:title="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}"></td>
<td>
<div class="data-action-cell">
<form th:if="${user.username != currentUsername}" th:action="@{'/api/v1/user/admin/deleteUser/' + ${user.username}}" method="post" onsubmit="return confirmDeleteUser()" style="display: inline;">
<button type="submit" th:title="#{adminUserSettings.deleteUser}" class="data-icon-btn data-icon-btn-danger">
<span class="material-symbols-rounded">person_remove</span>
</button>
</form>
<a th:if="${user.username == currentUsername}" th:title="#{adminUserSettings.editOwnProfil}" th:href="@{'/account'}" class="data-icon-btn data-icon-btn-primary">
<span class="material-symbols-rounded">edit</span>
</a>
<form th:action="@{'/api/v1/user/admin/changeUserEnabled/' + ${user.username}}" method="post" onsubmit="return confirmChangeUserStatus()" style="display: inline;">
<input type="hidden" name="enabled" th:value="!${user.enabled}" />
<button type="submit" th:if="${user.enabled}" th:title="#{adminUserSettings.enabledUser}" class="data-icon-btn data-icon-btn-primary">
<span class="material-symbols-rounded">person</span>
</button>
<button type="submit" th:unless="${user.enabled}" th:title="#{adminUserSettings.disabledUser}" class="data-icon-btn data-icon-btn-danger">
<span class="material-symbols-rounded">person_off</span>
</button>
</form>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<p th:if="${!@runningProOrHigher}" class="data-mt-3" th:text="#{enterpriseEdition.ssoAdvert}"></p>
</div>
</div>
</div>
</div>
<!-- Change User Role Modal -->
<div class="modal fade" id="changeUserRoleModal" tabindex="-1" aria-labelledby="changeUserRoleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form th:action="@{'/api/v1/user/admin/changeRole'}" method="post" class="modal-content data-modal">
<div class="data-modal-header">
<h5 class="data-modal-title">
<span class="data-icon">
<span class="material-symbols-rounded">edit</span>
</span>
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
</h5>
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="data-modal-body">
<div class="data-mb-2">
<button class="data-btn data-btn-secondary" data-toggle="tooltip" data-placement="auto" th:title="#{downgradeCurrentUserLongMessage}" style="padding: 0.25rem 0.5rem;">
<span class="material-symbols-rounded">help</span>
<span th:text="#{help}">Help</span>
</button>
</div>
<div class="data-form-group">
<label for="username" class="data-form-label" th:text="#{username}">Username</label>
<select name="username" id="username" class="data-form-control" required>
<option value="" disabled selected th:text="#{selectFilter}">-- Select --</option>
<option th:each="user : ${users}" th:if="${user.username != currentUsername}" th:value="${user.username}" th:text="${user.username}">Username</option>
</select>
</div>
<div class="data-form-group">
<label for="role" class="data-form-label" th:text="#{adminUserSettings.role}">Role</label>
<select name="role" id="role" class="data-form-control" required>
<option value="" disabled selected th:text="#{selectFilter}">-- Select --</option>
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">Role</option>
</select>
</div>
<div class="data-form-group">
<label for="team" class="data-form-label" th:text="#{adminUserSettings.team}">Team</label>
<select name="teamId" id="team" class="data-form-control" required>
<option value="" th:text="#{selectFilter}">-- Select --</option>
<option th:each="team : ${teams}" th:value="${team.id}" th:text="${team.name}"></option>
</select>
</div>
<div class="data-form-actions">
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
<span class="material-symbols-rounded">close</span>
<span th:text="#{cancel}">Cancel</span>
</button>
<button type="submit" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">check</span>
<span th:text="#{adminUserSettings.submit}">Save User</span>
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Add User Modal -->
<div class="modal fade" id="addUserModal" tabindex="-1" style="z-index: 10000;" aria-labelledby="addUserModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="formsaveuser" th:action="@{'/api/v1/user/admin/saveUser'}" method="post" class="modal-content data-modal">
<div class="data-modal-header">
<h5 class="data-modal-title">
<span class="data-icon">
<span class="material-symbols-rounded">person_add</span>
</span>
<span th:text="#{adminUserSettings.addUser}">Add New User</span>
</h5>
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="data-modal-body">
<div class="data-mb-2">
<button class="data-btn data-btn-secondary" data-toggle="tooltip" data-placement="auto" th:title="#{adminUserSettings.usernameInfo}" style="padding: 0.25rem 0.5rem;">
<span class="material-symbols-rounded">help</span>
<span th:text="#{help}">Help</span>
</button>
</div>
<div class="data-form-group">
<label for="username" class="data-form-label" th:text="#{username}">Username</label>
<input type="text" class="data-form-control" name="username" id="username" th:title="#{adminUserSettings.usernameInfo}" required>
<span id="usernameError" style="display: none; color: var(--md-sys-color-error);" th:text="#{invalidUsernameMessage}">Invalid username!</span>
</div>
<div class="data-form-group" id="passwordContainer">
<label for="password" class="data-form-label" th:text="#{password}">Password</label>
<input type="password" class="data-form-control" name="password" id="password" required>
</div>
<div class="data-form-group">
<label for="role" class="data-form-label" th:text="#{adminUserSettings.role}">Role</label>
<select name="role" class="data-form-control" id="role" required>
<option value="" disabled selected th:text="#{selectFilter}">-- Select --</option>
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">Role</option>
</select>
</div>
<div class="data-form-group">
<label for="team" class="data-form-label" th:text="#{adminUserSettings.team}">Team</label>
<select name="teamId" class="data-form-control" required>
<option value="" th:text="#{selectFilter}">-- Select --</option>
<option th:each="team : ${teams}" th:value="${team.id}" th:text="${team.name}"></option>
</select>
</div>
<div class="data-form-group">
<label for="authType" class="data-form-label">Authentication Type</label>
<select id="authType" name="authType" class="data-form-control" required>
<option value="web" selected>WEB</option>
<option value="sso">SSO</option>
</select>
</div>
<div class="data-form-group" id="checkboxContainer">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="forceChange" name="forceChange">
<label class="form-check-label" for="forceChange" th:text="#{adminUserSettings.forceChange}">Force user to change username/password on login</label>
</div>
</div>
<div class="data-form-actions">
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
<span class="material-symbols-rounded">close</span>
<span th:text="#{cancel}">Cancel</span>
</button>
<button type="submit" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">check</span>
<span th:text="#{adminUserSettings.submit}">Save User</span>
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Add Team Modal -->
<div class="modal fade" id="addTeamModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form th:action="@{'/api/v1/team/create'}" method="post" class="modal-content data-modal">
<div class="data-modal-header">
<h5 class="data-modal-title">
<span class="data-icon">
<span class="material-symbols-rounded">group_add</span>
</span>
<span th:text="#{adminUserSettings.createTeam}">Create Team</span>
</h5>
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="data-modal-body">
<div class="data-form-group">
<label for="teamName" class="data-form-label" th:text="#{adminUserSettings.teamName}">Team Name</label>
<input type="text" name="name" id="teamName" class="data-form-control" required />
</div>
<div class="data-form-actions">
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
<span class="material-symbols-rounded">close</span>
<span th:text="#{cancel}">Cancel</span>
</button>
<button type="submit" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">check</span>
<span th:text="#{adminUserSettings.submit}">Create</span>
</button>
</div>
</div>
</form>
</div>
</div>
<script th:inline="javascript">
const delete_confirm_text = /*[[#{adminUserSettings.confirmDeleteUser}]]*/ 'Should the user be deleted?';
const change_confirm_text = /*[[#{adminUserSettings.confirmChangeUserStatus}]]*/ 'Should the user be disabled/enabled?';
function confirmDeleteUser() {
return confirm(delete_confirm_text);
}
function confirmChangeUserStatus() {
return confirm(change_confirm_text);
}
jQuery.validator.addMethod("usernamePattern", function(value, element) {
// Regular expression for user name: Min. 3 characters, max. 50 characters
const regexUsername = /^[a-zA-Z0-9](?!.*[-@._+]{2,})([a-zA-Z0-9@._+-]{1,48})[a-zA-Z0-9]$/;
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
const regexEmail = /^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.[A-Za-z]{2,})$/;
// Check if the field is optional or meets the requirements
return this.optional(element) || regexUsername.test(value) || regexEmail.test(value);
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
$(document).ready(function() {
$('[data-toggle="tooltip"]').tooltip();
$('#formsaveuser').validate({
rules: {
username: {
required: true,
usernamePattern: true
},
password: {
required: true
},
role: {
required: true
},
authType: {
required: true
}
},
messages: {
username: {
usernamePattern: /*[[#{invalidUsernameMessage}]]*/ "Invalid username format"
},
},
errorPlacement: function(error, element) {
if (element.attr("name") === "username") {
$("#usernameError").text(error.text()).show();
} else if (element.attr("name") !== "role" && element.attr("name") !== "authType") {
error.insertAfter(element);
}
},
success: function(label, element) {
if ($(element).attr("name") === "username") {
$("#usernameError").hide();
}
}
});
$('#username').on('input', function() {
var usernameInput = $(this);
var isValid = usernameInput[0].checkValidity();
var errorSpan = $('#usernameError');
if (isValid) {
usernameInput.removeClass('invalid').addClass('valid');
errorSpan.hide();
} else {
usernameInput.removeClass('valid').addClass('invalid');
errorSpan.show();
}
});
$('#authType').on('change', function() {
var authType = $(this).val();
var passwordField = $('#password');
var passwordFieldContainer = $('#passwordContainer');
var checkboxContainer = $('#checkboxContainer');
if (authType === 'sso') {
passwordField.removeAttr('required');
passwordField.prop('disabled', true).val('');
passwordFieldContainer.slideUp('fast');
checkboxContainer.slideUp('fast');
} else {
passwordField.prop('disabled', false);
passwordField.attr('required', 'required');
passwordFieldContainer.slideDown('fast');
checkboxContainer.slideDown('fast');
}
});
});
</script>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{autoSplitPDF.title}, header=#{autoSplitPDF.header})}">
</th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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">
<svg class="material-symbols-rounded tool-header-icon advance">
<use xlink:href="/images/split-auto.svg#icon-split-auto"></use>
</svg>
<span class="tool-header-text" th:text="#{autoSplitPDF.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/auto-split-pdf'}">
<p th:text="#{autoSplitPDF.formPrompt}"></p>
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<div class="form-check ms-3">
<input type="checkbox" name="duplexMode" id="duplexMode">
<label for="duplexMode" th:text=#{autoSplitPDF.duplexMode}></label>
</div>
<p>
<a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button"
aria-expanded="false" aria-controls="info" th:text="#{info}"></a>
</p>
<div class="collapse" id="info">
<!-- Added a brief description -->
<p th:text="#{autoSplitPDF.description}"></p>
<ul>
<li th:text="#{autoSplitPDF.selectText.1}"></li>
<li th:text="#{autoSplitPDF.selectText.2}"></li>
<li th:text="#{autoSplitPDF.selectText.3}"></li>
<li th:text="#{autoSplitPDF.selectText.4}"></li>
</ul>
<p><a th:href="@{'/files/Auto%20Splitter%20Divider%20(with%20instructions).pdf'}" download
th:text="#{autoSplitPDF.dividerDownload2}"></a></p>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{autoSplitPDF.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{changeCreds.title}, header=#{changeCreds.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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-9 bg-card">
<!-- User Settings Title -->
<h2 class="text-center" th:text="#{changeCreds.header}">User Settings</h2>
<hr>
<th:block th:if="${messageType}">
<div class="alert alert-danger">
<span th:text="#{${messageType}}">Default message if not found</span>
</div>
</th:block>
<!-- At the top of the user settings -->
<h3 class="text-center"><span th:text="#{welcome} + ' ' + ${username}">User</span>!</h3>
<!-- Change Username Form -->
<h4 th:text="#{changeCreds.changePassword}">Change password</h4>
<form th:action="@{'api/v1/user/change-password-on-login'}" method="post" id="formsavechangecreds">
<div class="mb-3">
<label for="currentPassword" th:text="#{changeCreds.oldPassword}">Old Password</label>
<input type="password" class="form-control" name="currentPassword" id="currentPassword" th:placeholder="#{changeCreds.oldPassword}">
</div>
<div class="mb-3">
<label for="newPassword" th:text="#{changeCreds.newPassword}">New Password</label>
<input type="password" class="form-control" name="newPassword" id="newPassword" th:placeholder="#{changeCreds.newPassword}">
</div>
<div class="mb-3">
<label for="confirmNewPassword" th:text="#{account.confirmNewPassword}">Confirm New Password</label>
<input type="password" class="form-control" name="confirmNewPassword" id="confirmNewPassword" th:placeholder="#{account.confirmNewPassword}">
</div>
<div class="mb-3">
<span id="confirmPasswordError" style="display: none;" th:text="#{confirmPasswordErrorMessage}">New Password and Confirm New Password must match.</span>
<button type="submit" class="btn btn-primary" th:text="#{changeCreds.submit}">Change credentials!</button>
</div>
</form>
<script th:inline="javascript">
$(document).ready(function() {
$.validator.addMethod("passwordMatch", function(value, element) {
return $('#newPassword').val() === $('#confirmNewPassword').val();
}, /*[[#{confirmPasswordErrorMessage}]]*/ "New Password and Confirm New Password must match.");
$('#formsavechangecreds').validate({
rules: {
currentPassword: {
required: true
},
newPassword: {
required: true
},
confirmNewPassword: {
required: true,
passwordMatch: true
},
errorPlacement: function(error, element) {
if ($(element).attr("name") === "newPassword" || $(element).attr("name") === "confirmNewPassword") {
$("#confirmPasswordError").text(error.text()).show();
} else {
error.insertAfter(element);
}
},
success: function(label, element) {
if ($(element).attr("name") === "newPassword" || $(element).attr("name") === "confirmNewPassword") {
$("#confirmPasswordError").hide();
}
}
}
});
});
</script>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{EMLToPDF.title}, header=#{EMLToPDF.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div class="mt-4"></div>
<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 convertto">email</span>
<span class="tool-header-text" th:text="#{EMLToPDF.header}"></span>
</div>
<p th:text="#{processTimeWarning}"></p>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/eml/pdf'}" class="mt-4">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.eml,message/rfc822')}">
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" name="downloadHtml" id="downloadHtml">
<label class="form-check-label" for="downloadHtml" th:text="#{EMLToPDF.downloadHtml}"></label>
<div class="form-text" th:text="#{EMLToPDF.downloadHtmlHelp}"></div>
</div>
<div id="pdfOnlyOptions">
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" name="includeAttachments" id="includeAttachments" checked>
<label class="form-check-label" for="includeAttachments" th:text="#{EMLToPDF.includeAttachments}"></label>
</div>
<div class="mb-3">
<label for="maxAttachmentSizeMB" class="form-label" th:text="#{EMLToPDF.maxAttachmentSize}"></label>
<input type="number" class="form-control" id="maxAttachmentSizeMB" name="maxAttachmentSizeMB" value="10" min="1" max="100">
</div>
</div>
<div class="mb-4">
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse"
data-bs-target="#info" aria-expanded="false" aria-controls="info" th:text="#{info}">
</button>
<div class="collapse mt-3" id="info">
<div class="card card-body">
<p class="mb-2" th:text="#{EMLToPDF.help}"></p>
<ul class="mb-0">
<li th:text="#{EMLToPDF.troubleshootingTip1}"></li>
<li th:text="#{EMLToPDF.troubleshootingTip2}"></li>
<li th:text="#{EMLToPDF.troubleshootingTip3}"></li>
</ul>
</div>
</div>
</div>
<div>
<br />
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{EMLToPDF.submit}"></button>
</div>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function() {
const downloadHtml = document.getElementById('downloadHtml');
const pdfOnlyOptions = document.getElementById('pdfOnlyOptions');
const submitBtn = document.getElementById('submitBtn');
const submitText = /*[[#{EMLToPDF.submit}]]*/ 'Convert to PDF';
const downloadHtmlText = 'Download HTML intermediate file instead of PDF';
function updateFormState() {
if (pdfOnlyOptions && submitBtn) {
pdfOnlyOptions.style.display = downloadHtml.checked ? 'none' : 'block';
submitBtn.textContent = downloadHtml.checked ? downloadHtmlText : submitText;
}
}
if (downloadHtml) {
downloadHtml.addEventListener('change', updateFormState);
updateFormState();
}
});
</script>
</body>
</html>
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{fileToPDF.title}, header=#{fileToPDF.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 convertto">draft</span>
<span class="tool-header-text" th:text="#{fileToPDF.header}"></span>
</div>
<p th:text="#{processTimeWarning}"></p>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/file/pdf'}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false)}">
</div>
<a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button"
aria-expanded="false" aria-controls="info" th:text="#{fileToPDF.supportedFileTypesInfo}"></a>
<div class="collapse" id="info">
<p class="mt-3" th:text="#{fileToPDF.credit}"></p>
<p class="mt-3" th:text="#{fileToPDF.supportedFileTypes}"></p>
<p>Microsoft Word: (DOC, DOCX, DOT, DOTX)</p>
<p>Microsoft Excel: (CSV, XLS, XLSX, XLT, XLTX, SLK, DIF)</p>
<p>Microsoft PowerPoint: (PPT, PPTX)</p>
<p>OpenDocument Formats: (ODT, OTT, ODS, OTS, ODP, OTP, ODG, OTG)</p>
<p>Plain Text: (TXT, TEXT, XML)</p>
<p>Rich Text Format: (RTF)</p>
<p>Images: (BMP, GIF, JPEG, PNG, TIF, PBM, PGM, PPM, RAS, XBM, XPM, SVG, SVM, WMF)</p>
<p>HTML: (HTML)</p>
<p>Lotus Word Pro: (LWP)</p>
<p>StarOffice: (SDA, SDC, SDD, SDW, STC, STD, STI, STW, SXD, SXG, SXI, SXW)</p>
<p>Other: (DBF, FODS, VSD, VOR, VOR3, VOR4, UOP, PCT, PS, PDF)</p>
<a
href="https://help.libreoffice.org/latest/en-US/text/shared/guide/supported_formats.html">https://help.libreoffice.org/latest/en-US/text/shared/guide/supported_formats.html</a>
</div>
<div>
<br />
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{fileToPDF.submit}"></button>
</div>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{HTMLToPDF.title}, header=#{HTMLToPDF.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 convertto">html</span>
<span class="tool-header-text" th:text="#{HTMLToPDF.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/html/pdf'}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='text/html,application/zip' )}">
</div>
<div class="mb-3">
<label for="zoom" th:text="#{HTMLToPDF.zoom}" class="form-label"></label>
<input type="number" step="0.1" class="form-control" id="zoom" name="zoom" value="1">
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{HTMLToPDF.submit}"></button>
</form>
<p class="mt-3" th:text="#{HTMLToPDF.help}"></p>
<p class="mt-3" th:text="#{HTMLToPDF.credit}"></p>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{imageToPDF.title}, header=#{imageToPDF.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 convertto">picture_as_pdf</span>
<span class="tool-header-text" th:text="#{imageToPDF.header}"></span>
</div>
<form id="imageToPDFForm" method="post" enctype="multipart/form-data"
th:action="@{'/api/v1/convert/img/pdf'}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='image/*', inputText=#{imgPrompt})}">
</div>
<div class="mb-3">
<label for="fitOption" th:text="#{imageToPDF.selectLabel}">Fit Options</label>
<select class="form-control" id="fitOption" name="fitOption">
<option value="fillPage" th:text="#{imageToPDF.fillPage}">Fill Page</option>
<option value="fitDocumentToImage" th:text="#{imageToPDF.fitDocumentToImage}">Fit Document to Image
</option>
<option value="maintainAspectRatio" th:text="#{imageToPDF.maintainAspectRatio}">Maintain Aspect Ratio
</option>
</select>
</div>
<div class="form-check ms-3">
<input type="checkbox" name="autoRotate" id="autoRotate">
<label for="autoRotate" th:text=#{imageToPDF.selectText.2}></label>
</div>
<div class="mb-3">
<label th:text="#{pdfToImage.colorType}"></label>
<select class="form-control" id="colorType" name="colorType">
<option value="color" th:text="#{pdfToImage.color}"></option>
<option value="greyscale" th:text="#{pdfToImage.grey}"></option>
<option value="blackwhite" th:text="#{pdfToImage.blackwhite}"></option>
</select>
</div>
<input type="hidden" id="override" name="override" value="multi">
<div class="mb-3">
<label th:text=#{imageToPDF.selectText.3}></label>
<select class="form-control" id="conversionType" name="conversionType" disabled>
<option value="merge" th:text=#{imageToPDF.selectText.4}></option>
<option value="convert" th:text=#{imageToPDF.selectText.5} selected></option>
</select>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{imageToPDF.submit}"></button>
<script>
$('#fileInput-input').on('change', function (e) {
const fileInput = e.target;
fileInput.addEventListener('file-input-change', async (e) => {
const { allFiles } = e.detail;
var conversionType = document.getElementById("conversionType");
console.log("files.length=" + allFiles.length)
if (allFiles.length > 1) {
conversionType.disabled = false;
} else {
conversionType.disabled = true;
}
});
});
$('#conversionType').change(function () {
var selectedValue = $(this).val();
var override = document.getElementById("override");
console.log("selectedValue=" + selectedValue)
if (selectedValue === 'merge') {
override.value = "single";
} else if (selectedValue === 'convert') {
override.value = "multi";
}
});
document.getElementById("imageToPDFForm").addEventListener("submit", async function (e) {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
const fitOptionSelect = document.getElementById("fitOption");
const fitOptionValue = fitOptionSelect ? fitOptionSelect.value : "fit-to-page"; // Default value
formData.append("fitOption", fitOptionValue);
const overrideInput = document.getElementById("override");
console.log("Override value before submission:", overrideInput.value);
for (let [key, value] of formData.entries()) {
console.log(`${key}:`, value);
}
try {
const response = await fetch(form.action, {
method: "POST",
body: formData
});
if (response.ok) {
console.log("Form successfully submitted");
} else {
console.error("Failed to submit the form");
}
} catch (error) {
console.error("Error submitting the form:", error);
}
});
</script>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{MarkdownToPDF.title}, header=#{MarkdownToPDF.header})}">
</th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 convertto">markdown</span>
<span class="tool-header-text" th:text="#{MarkdownToPDF.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/markdown/pdf'}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.md')}">
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{MarkdownToPDF.submit}"></button>
</form>
<p class="mt-3" th:text="#{MarkdownToPDF.help}"></p>
<p class="mt-3" th:text="#{MarkdownToPDF.credit}"></p>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToCSV.title}, header=#{PDFToCSV.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 convert">csv</span>
<span class="tool-header-text" th:text="#{PDFToCSV.header}"></span>
</div>
<form id="PDFToCSVForm" th:action="@{'/api/v1/convert/pdf/csv'}" method="post" enctype="multipart/form-data">
<input id="pageNumbers" type="hidden" name="pageNumbers">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToCSV.submit}"></button>
</form>
<p id="instruction-text" style="margin: 0; display: none" th:text="#{PDFToCSV.prompt}"></p>
<div style="position: relative; width: auto;" id="canvasesContainer">
<div>
<div style="display:none ;margin: 3px;position: absolute;top: 0;width: 120px;justify-content:space-between;z-index: 10" id="pagination-button-container">
<button id='previous-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> &lt; </button>
<button id='next-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> &gt; </button>
</div>
<canvas id="cropPdfCanvas" style="width: 100%"></canvas>
</div>
<canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2; width: 100%"></canvas>
</div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script type="module" th:src="@{'/js/pages/pdf-to-csv.js'}">
</script>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToHTML.title}, header=#{PDFToHTML.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 convert">html</span>
<span class="tool-header-text" th:text="#{PDFToHTML.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/html'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToHTML.submit}"></button>
</form>
<p class="mt-3" th:text="#{PDFToHTML.credit}"></p>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{pdfToImage.title}, header=#{pdfToImage.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 convert">photo_library</span>
<span class="tool-header-text" th:text="#{pdfToImage.header}"></span>
</div>
<p th:text="#{processTimeWarning}"></p>
<p th:if="${!isPython}" th:text="#{pdfToImage.info}">Python is not installed. Required for WebP conversion.
</p>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/img'}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<div class="mb-3">
<label th:text="#{pdfToImage.selectText}"></label>
<select class="form-control" name="imageFormat">
<option value="png">PNG</option>
<option value="jpg">JPG</option>
<option value="gif">GIF</option>
<option value="tiff">TIFF</option>
<option value="bmp">BMP</option>
<option th:if="${isPython}" value="webp">WEBP</option>
</select>
</div>
<div class="mb-3">
<label th:text="#{pdfToImage.singleOrMultiple}"></label>
<select class="form-control" name="singleOrMultiple">
<option value="multiple" th:text="#{pdfToImage.multi}"></option>
<option value="single" th:text="#{pdfToImage.single}"></option>
</select>
</div>
<div class="mb-3" id="singleOptionSection">
<label for="pageNumbers" th:text="#{pageSelectionPrompt}"></label>
<input type="text" name="pageNumbers" class="form-control" id="pageNumbers" value="all"
th:placeholder="#{pdfToImage.placeholder}" required>
</div>
<div class="mb-3">
<label th:text="#{pdfToImage.colorType}"></label>
<select class="form-control" name="colorType">
<option value="color" th:text="#{pdfToImage.color}"></option>
<option value="greyscale" th:text="#{pdfToImage.grey}"></option>
<option value="blackwhite" th:text="#{pdfToImage.blackwhite}"></option>
</select>
</div>
<div class="mb-3">
<label for="dpi">DPI:</label>
<input type="number" name="dpi" class="form-control" id="dpi" min="1" step="1" value="300" required>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfToImage.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToMarkdown.title}, header=#{PDFToMarkdown.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 convert">markdown_copy</span>
<span class="tool-header-text" th:text="#{PDFToMarkdown.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/markdown'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.pdf')}"></div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToMarkdown.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{pdfToPDFA.title}, header=#{pdfToPDFA.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 convert">picture_as_pdf</span>
<span class="tool-header-text" th:text="#{pdfToPDFA.header}"></span>
</div>
<p th:text="#{pdfToPDFA.tip}"></p>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/pdfa'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<div class="mb-3">
<label for="outputFormat" th:text="#{pdfToPDFA.outputFormat}"></label>
<select class="form-control" name="outputFormat" id="outputFormat">
<option value="pdfa-1">PDF/A-1b</option>
<option value="pdfa">PDF/A-2b</option>
</select>
</div>
<div id="result" class="alert-warning"></div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfToPDFA.submit}"></button>
</form>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script th:inline="javascript">
document.getElementById('fileInput-input').addEventListener('change', async () => {
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const fileInput = document.getElementById('fileInput-input');
const resultDiv = document.getElementById('result');
const file = fileInput.files[0];
const arrayBuffer = await file.arrayBuffer();
try {
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
let hasSignature = false;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const annotations = await page.getAnnotations({ intent: 'display' });
annotations.forEach(annotation => {
console.log(annotation)
if (annotation.subtype === 'Widget' && annotation.fieldType === 'Sig') {
hasSignature = true;
}
});
}
if (hasSignature) {
/*<![CDATA[*/
resultDiv.textContent = /*[[#{pdfToPDFA.pdfWithDigitalSignature}]]*/ "The PDF contains a digital signature. This will be removed in the next step.";
/*]]>*/
}
} catch (error) {
resultDiv.textContent = 'Error reading the PDF: ' + error.message;
}
});
</script>
<p class="mt-3" th:text="#{pdfToPDFA.credit}"></p>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block
th:insert="~{fragments/common :: head(title=#{PDFToPresentation.title}, header=#{PDFToPresentation.header})}">
</th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 convert">slideshow</span>
<span class="tool-header-text" th:text="#{PDFToPresentation.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/presentation'}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<div class="mb-3">
<label th:text="#{PDFToPresentation.selectText.1}"></label>
<select class="form-control" name="outputFormat">
<option value="pptx">PPTX</option>
<option value="ppt">PPT</option>
<option value="odp">ODP</option>
</select>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary"
th:text="#{PDFToPresentation.submit}"></button>
</form>
<p class="mt-3" th:text="#{PDFToPresentation.credit}"></p>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToText.title}, header=#{PDFToText.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 convert">text_fields</span>
<span class="tool-header-text" th:text="#{PDFToText.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/text'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<div class="mb-3">
<label th:text="#{PDFToText.selectText.1}"></label>
<select class="form-control" name="outputFormat">
<option th:if="${@endpointConfiguration.isEndpointEnabled('pdf-to-rtf')}" value="rtf">RTF</option>
<option value="txt">TXT</option>
</select>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToText.submit}"></button>
</form>
<p th:if="${@endpointConfiguration.isEndpointEnabled('pdf-to-rtf')}" class="mt-3" th:text="#{PDFToText.credit}"></p>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToWord.title}, header=#{PDFToWord.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 convert">description</span>
<span class="tool-header-text" th:text="#{PDFToWord.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/word'}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<div class="mb-3">
<label th:text="#{PDFToWord.selectText.1}"></label>
<select class="form-control" name="outputFormat">
<option value="docx">DocX</option>
<option value="doc">Doc</option>
<option value="odt">Odt</option>
</select>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToWord.submit}"></button>
</form>
<p class="mt-3" th:text="#{PDFToWord.credit}"></p>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{PDFToXML.title}, header=#{PDFToXML.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 convert">code</span>
<span class="tool-header-text" th:text="#{PDFToXML.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/pdf/xml'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{PDFToXML.submit}"></button>
</form>
<p class="mt-3" th:text="#{PDFToXML.credit}"></p>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{URLToPDF.title}, header=#{URLToPDF.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 convertto">link</span>
<span class="tool-header-text" th:text="#{URLToPDF.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/convert/url/pdf'}">
<input type="text" class="form-control" id="urlInput" name="urlInput" placeholder="http://">
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{URLToPDF.submit}"></button>
</form>
<p class="mt-3" th:text="#{URLToPDF.credit}"></p>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{crop.title}, header=#{crop.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 organize">crop</span>
<span class="tool-header-text" th:text="#{crop.header}"></span>
</div>
<form id="cropForm" th:action="@{'/api/v1/general/crop'}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<input id="x" type="hidden" name="x">
<input id="y" type="hidden" name="y">
<input id="width" type="hidden" name="width">
<input id="height" type="hidden" name="height">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{crop.submit}"></button>
</form>
<div id="canvasesContainer" style="position: relative; margin: 20px 0; width: auto;">
<canvas id="cropPdfCanvas" style="width: 100%"></canvas>
<canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2; width: 100%"></canvas>
</div>
</div>
</div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script type="module" th:src="@{'/js/pages/crop.js'}"></script>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{database.title}, header=#{database.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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-9 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon organize">database</span>
<span class="tool-header-text" th:text="#{database.title}">Database Im-/Export</span>
</div>
<p th:if="${error}" th:text="#{'database.' + ${error}}" class="alert alert-danger text-center"></p>
<p th:if="${infoMessage}" th:text="#{'database.' + ${infoMessage}}" class="alert alert-success text-center"></p>
<div th:if="${databaseVersion!='Unknown'}" class="bg-card mt-3 mb-3">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th scope="col" th:text="#{database.fileName}">File Name</th>
<th scope="col" th:text="#{database.creationDate}">Creation Date</th>
<th scope="col" th:text="#{database.fileSize}">File Size</th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr th:each="backup : ${backupFiles}">
<td th:text="${backup.fileName}"></td>
<td th:text="${backup.formattedCreationDate}"></td>
<td th:text="${backup.formattedFileSize}"></td>
<td th:title="#{database.deleteBackupFile}"><a th:href="@{'/api/v1/database/delete/' + ${backup.fileName}}" style="color: red;"><span class="material-symbols-rounded">delete</span></a></td>
<td th:title="#{database.importBackupFile}"><a th:href="@{'/api/v1/database/import-database-file/' + ${backup.fileName}}" style="color: var(--md-nav-section-color-organize);"><span class="material-symbols-rounded">backup</span></a></td>
<td th:title="#{database.downloadBackupFile}"><a th:href="@{'/api/v1/database/download/' + ${backup.fileName}}"><span class="material-symbols-rounded">download</span></a></td>
</tr>
</tbody>
</table>
<h6 class="text-end"><span class="badge bg-dark" th:text="${databaseVersion}">DB-Version</span></h6>
<a th:title="#{database.createBackupFile}" th:text="#{database.createBackupFile}" th:href="@{'/api/v1/database/createDatabaseBackup'}" class="btn btn-outline-primary">Create Backup File</a>
</div>
<hr>
<form th:if="${databaseVersion!='Unknown'}" th:action="@{'/api/v1/database/import-database'}" method="post" enctype="multipart/form-data" class="bg-card mt-3 mb-3">
<div style="background: var(--md-sys-color-error-container);border-radius: 2rem;" class="mb-3 p-3">
<p th:text="#{database.info_1}"></p>
<p th:text="#{database.info_2}"></p>
</div>
<div class="custom-file-chooser">
<div class="mb-3">
<input type="file" class="form-control" th:name="fileInput" id="fileInput-input" accept=".sql" required>
</div>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{database.submit}">Import Backup</button>
</form>
</div>
</div>
</div>
</div>
<script th:inline="javascript">
const pdfPasswordPrompt = /*[[#{error.pdfPassword}]]*/ '';
const multiple = /*[[${multiple}]]*/ false;
const remoteCall = /*[[${remoteCall}]]*/ true;
</script>
<script th:src="@{'/js/fileInput.js'}"></script>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{editTableOfContents.title}, header=#{editTableOfContents.header})}">
</th:block>
<link rel="stylesheet" th:href="@{'/css/edit-table-of-contents.css'}">
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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-8 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon edit">bookmark_add</span>
<span class="tool-header-text" th:text="#{editTableOfContents.header}"></span>
</div>
<form th:action="@{'/api/v1/general/edit-table-of-contents'}" method="post" enctype="multipart/form-data" id="editTocForm">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="replaceExisting" name="replaceExisting" checked>
<label class="form-check-label" for="replaceExisting"
th:text="#{editTableOfContents.replaceExisting}"></label>
<input type="hidden" name="replaceExisting" value="false" />
</div>
<div class="bookmark-editor">
<h5 th:text="#{editTableOfContents.editorTitle}"></h5>
<p th:text="#{editTableOfContents.editorDesc}"></p>
<div id="bookmarks-container">
<!-- Bookmarks will be added here dynamically -->
</div>
<div class="bookmark-actions">
<button type="button" id="addBookmarkBtn" class="btn btn-outline-primary" th:text="#{editTableOfContents.addBookmark}"></button>
</div>
<!-- Hidden field to store JSON data -->
<input type="hidden" id="bookmarkData" name="bookmarkData" value="[]">
</div>
<p>
<a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button"
aria-expanded="false" aria-controls="info" th:text="#{info}"></a>
</p>
<div class="collapse" id="info">
<p th:text="#{editTableOfContents.desc.1}"></p>
<p th:text="#{editTableOfContents.desc.2}"></p>
<p th:text="#{editTableOfContents.desc.3}"></p>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{editTableOfContents.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script th:src="@{'/js/pages/edit-table-of-contents.js'}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Bootstrap tooltips
if (typeof $ !== 'undefined') {
$('[data-bs-toggle="tooltip"]').tooltip();
}
});
</script>
</body>
</html>
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{error.404.head})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div class="container">
<div th:insert="~{fragments/errorBanner.html :: errorBanner}"></div>
<div class="bg-card text-center">
<h1 class="display-2" th:text="#{oops}"></h1>
<p class="lead" th:if="${param.status == '404'}" th:text="#{error.404.1}"></p>
<p class="lead" th:unless="${param.status == '404'}" th:text="#{error.404.2}"></p>
<br>
<h2 th:text="#{error.needHelp}"></h2>
<p th:text="#{error.contactTip}"></p>
<div id="button-group">
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" class="btn btn-primary" target="_blank" th:text="#{error.github}"></a>
<a href="https://discord.gg/HYmhKj45pU" id="discord-button" class="btn btn-primary" target="_blank" th:text="#{joinDiscord}"></a>
</div>
<a th:href="@{'/'}" id="home-button" class="home-button btn btn-primary" th:text="#{goHomepage}"></a>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}">
</th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<th:block th:insert="~{fragments/multi-toolAdvert.html :: multi-toolAdvert}"></th:block>
<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 organize">upload</span>
<span class="tool-header-text" th:text="#{pageExtracter.header}"></span>
</div>
<form th:action="@{'/api/v1/general/rearrange-pages'}" method="post" enctype="multipart/form-data">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<input type="hidden" id="customMode" name="customMode" value="">
<div class="mb-3">
<label for="pageOrder" th:text="#{pageOrderPrompt}"></label>
<input type="text" class="form-control" id="pageOrder" name="pageNumbers"
th:placeholder="#{pageExtracter.placeholder}" required>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pageExtracter.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,21 @@
<div th:fragment="card" class="feature-card hidden" th:id="${id}" th:if="${@endpointConfiguration.isEndpointEnabled(cardLink)} "
th:data-bs-tags="${tags}"
th:data-bs-link="@{${endpoint}}">
<a th:href="${cardLink}">
<div class="d-flex align-items-center">
<div id="tool-icon" class="icon" alt="icon" th:class="@{${toolGroup}}">
<span class="material-symbols-rounded nav-icon" th:text="@{${toolIcon}}"></span>
</div>
<div id="tool-text">
<h5 class="card-title" th:text="${cardTitle}"></h5>
<p class="card-text" th:text="${cardText}"></p>
</div>
</div>
</a>
<span class="material-symbols-rounded favorite-icon" th:data-endpoint="@{${endpoint}}" th:onclick="'addToFavorites(\'' + @{${cardLink}} + '\')'">
star
</span>
</div>
@@ -0,0 +1,426 @@
<th:block th:fragment="head">
<!-- Title -->
<title th:text="${@appName} + (${title} != null and ${title} != '' ? ' - ' + ${title} : '')"></title>
<!-- Metadata -->
<meta charset="utf-8">
<meta name="description" th:content="${@appName} + (${header} != null and ${header} != '' ? ' - ' + ${header} : '')">
<meta name="msapplication-TileColor" content="#2d89ef">
<meta name="theme-color" content="#ffffff">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Icons -->
<link rel="apple-touch-icon" sizes="180x180" th:href="@{'/apple-touch-icon.png'}">
<link rel="icon" type="image/png" sizes="32x32" th:href="@{'/favicon-32x32.png'}">
<link rel="icon" type="image/png" sizes="16x16" th:href="@{'/favicon-16x16.png'}">
<link rel="manifest" th:href="@{'/site.webmanifest'}" crossorigin="use-credentials">
<link rel="mask-icon" th:href="@{'/safari-pinned-tab.svg'}" color="#ca2b2a">
<link rel="shortcut icon" th:href="@{'/favicon.ico'}">
<meta name="apple-mobile-web-app-title" content="Stirling PDF">
<meta name="application-name" content="Stirling PDF">
<meta name="msapplication-TileColor" content="#00aba9">
<meta name="theme-color" content="#ffffff">
<script>
window.stirlingPDF = window.stirlingPDF || {};
// Capture true system DPI at page load (before any zoom interactions)
const systemDPR = window.devicePixelRatio || 1;
// Determine if this is actually a high DPI screen at page load
const isHighDPI = systemDPR > 1.4;
function scaleNav() {
const currentDPR = window.devicePixelRatio || 1;
const browserZoom = currentDPR / systemDPR;
// Counter-scale to maintain same visual size
const isMobile = window.innerWidth <= 768 && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
let baseScale = isMobile ? 3 : (isHighDPI ? 2.2 : 1.1); // Prioritize mobile scaling over high DPI
const navScale = baseScale / currentDPR;
// Dropdowns at 80% (20% smaller)
const dropdownScale = 0.8;
const navbar = document.querySelector('.navbar');
if (navbar) {
// RTL support - check document direction
const isRTL = document.documentElement.dir === 'rtl' || document.documentElement.getAttribute('dir') === 'rtl';
const translateX = isRTL ? '50%' : '-50%';
navbar.style.transform = `translateX(${translateX}) scale(${navScale})`;
navbar.style.transformOrigin = 'top center';
navbar.style.width = `${100 / navScale}%`;
if (isRTL) {
navbar.style.right = '50%';
navbar.style.left = 'auto';
} else {
navbar.style.left = '50%';
navbar.style.right = 'auto';
}
// Adjust bottom margin based on scale to prevent overlap/gaps
const baseHeight = 60; // Assume base navbar height
const scaledHeight = baseHeight * navScale;
const marginAdjustment = scaledHeight - baseHeight;
navbar.style.marginBottom = `${marginAdjustment}px`;
// Adjust responsive breakpoint based on effective width after scaling
const effectiveWidth = window.innerWidth / navScale;
if (effectiveWidth >= 1200) {
navbar.classList.add('navbar-expand-lg');
navbar.classList.remove('navbar-expand-xl');
} else {
navbar.classList.add('navbar-expand-xl');
navbar.classList.remove('navbar-expand-lg');
}
console.log('DPR:', currentDPR, 'isHighDPI:', isHighDPI, 'baseScale:', baseScale, 'navScale:', navScale, 'effective width:', effectiveWidth);
}
// Set CSS custom property for mobile navbar scaling (for sidebar positioning)
// Use the ACTUAL scaled height, not a fixed assumption
const baseHeight = 60;
const actualScaledHeight = baseHeight * navScale;
document.documentElement.style.setProperty('--navbar-height', `${actualScaledHeight}px`);
setTimeout(() => {
const dropdowns = document.querySelectorAll('.dropdown-menu');
dropdowns.forEach(dropdown => {
dropdown.style.transform = `scale(${dropdownScale})`;
// Use different transform origins based on dropdown position
const parentItem = dropdown.closest('.nav-item');
const navbar = dropdown.closest('.navbar-nav');
// Check if this is a right-aligned dropdown (language, favorites, search, etc.)
const isRightAligned = navbar && navbar.classList.contains('flex-nowrap') &&
!parentItem?.closest('.dropdown-mega');
dropdown.style.transformOrigin = isRightAligned ? 'top right' : 'top left';
});
console.log('Applied dropdown scale:', dropdownScale);
}, 100);
}
let lastDPR = window.devicePixelRatio;
document.addEventListener('DOMContentLoaded', scaleNav);
window.addEventListener('resize', scaleNav);
setInterval(() => {
const currentDPR = window.devicePixelRatio;
if (Math.abs(currentDPR - lastDPR) > 0.01) {
lastDPR = currentDPR;
scaleNav();
}
}, 100);
</script>
<script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<!-- jQuery -->
<script th:src="@{'/js/thirdParty/jquery.min.js'}"></script>
<script th:src="@{'/js/thirdParty/jquery.validate.min.js'}"></script>
<script th:src="@{'/js/thirdParty/jszip.min.js'}" th:if="${currentPage != 'home'}"></script>
<!-- Bootstrap -->
<script th:src="@{'/js/thirdParty/popper.min.js'}"></script>
<script th:src="@{'/js/thirdParty/bootstrap.min.js'}"></script>
<link rel="stylesheet" th:href="@{'/css/bootstrap.min.css'}">
<!-- Bootstrap Icons -->
<link rel="stylesheet" th:href="@{'/css/bootstrap-icons.min.css'}">
<!-- Pixel, doesn't collect any PII-->
<img th:if="${!@disablePixel}" referrerpolicy="no-referrer-when-downgrade"
th:src="'https://pixel.stirlingpdf.com/a.png?x-pxid=4f5fa02f-a065-4efb-bb2c-24509a4b6b92'
+ '&machineType=' + ${@machineType}
+ '&appVersion=' + ${@appVersion}
+ '&licenseType=' + ${@license}
+ '&loginEnabled=' + ${@loginEnabled}"
style="position: absolute; visibility: hidden;" />
<!-- Custom -->
<link rel="stylesheet" th:href="@{'/css/general.css'}">
<link rel="stylesheet" th:href="@{'/css/theme/theme.css'}">
<link rel="stylesheet" th:href="@{'/css/theme/componentes.css'}">
<link rel="stylesheet" th:href="@{'/css/theme/theme.light.css'}" id="light-mode-styles">
<link rel="stylesheet" th:href="@{'/css/theme/theme.dark.css'}" id="dark-mode-styles">
<link rel="stylesheet" th:href="@{'/css/rainbow-mode.css'}" id="rainbow-mode-styles" disabled>
<link rel="stylesheet" th:href="@{'/css/tab-container.css'}">
<link rel="stylesheet" th:href="@{'/css/navbar.css'}">
<link rel="stylesheet" th:href="@{'/css/error.css'}" th:if="${error}">
<link rel="stylesheet" th:href="@{'/css/home.css'}" th:if="${currentPage == 'home'}">
<link rel="stylesheet" th:href="@{'/css/home-legacy.css'}" th:if="${currentPage == 'home-legacy'}">
<link rel="stylesheet" th:href="@{'/css/account.css'}" th:if="${currentPage == 'account'}">
<link rel="stylesheet" th:href="@{'/css/licenses.css'}" th:if="${currentPage == 'licenses'}">
<link rel="stylesheet" th:href="@{'/css/multi-tool.css'}" th:if="${currentPage == 'multi-tool'}">
<link rel="stylesheet" th:href="@{'/css/rotate-pdf.css'}" th:if="${currentPage == 'rotate-pdf'}">
<link rel="stylesheet" th:href="@{'/css/stamp.css'}" th:if="${currentPage == 'stamp'}">
<link rel="stylesheet" th:href="@{'/css/fileSelect.css'}" th:if="${currentPage != 'home'}">
<link rel="stylesheet" th:href="@{'/css/footer.css'}">
<link rel="preload" th:href="@{'/fonts/google-symbol.woff2'}" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="stylesheet" th:href="@{'/css/cookieconsent.css'}">
<link rel="stylesheet" th:href="@{'/css/cookieconsentCustomisation.css'}">
<script th:src="@{'/js/thirdParty/fontfaceobserver.standalone.js'}"></script>
<!-- Google MD Icons -->
<link rel="stylesheet" th:href="@{'/css/theme/font.css'}">
<!-- Help Modal -->
<link rel="stylesheet" th:href="@{'/css/errorBanner.css'}" th:if="${currentPage != 'home'}">
<script th:src="@{'/js/cacheFormInputs.js'}" th:if="${currentPage != 'home'}"></script>
<script th:src="@{'/js/tab-container.js'}"></script>
<script th:src="@{'/js/darkmode.js'}"></script>
<script th:src="@{'/js/csrf.js'}"></script>
<script th:inline="javascript">
function UpdatePosthogConsent(){
if(typeof(posthog) == "undefined") {
return;
}
window.CookieConsent.acceptedCategory('analytics')?
posthog.opt_in_capturing() : posthog.opt_out_capturing();
}
const stirlingPDFLabel = /*[[${@StirlingPDFLabel}]]*/ '';
const analyticsEnabled = /*[[${@analyticsEnabled}]]*/ false;
const cookieBannerPopUpTitle = /*[[#{cookieBanner.popUp.title}]]*/ "How we use Cookies";
const cookieBannerPopUpDescription1 = /*[[#{cookieBanner.popUp.description.1}]]*/ "";
const cookieBannerPopUpDescription2 = /*[[#{cookieBanner.popUp.description.2}]]*/ "";
const cookieBannerPopUpAcceptAllBtn = /*[[#{cookieBanner.popUp.acceptAllBtn}]]*/ "";
const cookieBannerPopUpAcceptNecessaryBtn = /*[[#{cookieBanner.popUp.acceptNecessaryBtn}]]*/ "";
const cookieBannerPopUpShowPreferencesBtn = /*[[#{cookieBanner.popUp.showPreferencesBtn}]]*/ "";
const cookieBannerPreferencesModalTitle = /*[[#{cookieBanner.preferencesModal.title}]]*/ "";
const cookieBannerPreferencesModalAcceptAllBtn = /*[[#{cookieBanner.preferencesModal.acceptAllBtn}]]*/ "";
const cookieBannerPreferencesModalAcceptNecessaryBtn = /*[[#{cookieBanner.preferencesModal.acceptNecessaryBtn}]]*/ "";
const cookieBannerPreferencesModalSavePreferencesBtn = /*[[#{cookieBanner.preferencesModal.savePreferencesBtn}]]*/ "";
const cookieBannerPreferencesModalCloseIconLabel = /*[[#{cookieBanner.preferencesModal.closeIconLabel}]]*/ "";
const cookieBannerPreferencesModalServiceCounterLabel = /*[[#{cookieBanner.preferencesModal.serviceCounterLabel}]]*/ "";
const cookieBannerPreferencesModalSubtitle = /*[[#{cookieBanner.preferencesModal.subtitle}]]*/ "";
const cookieBannerPreferencesModalDescription1 = /*[[#{cookieBanner.preferencesModal.description.1}]]*/ "";
const cookieBannerPreferencesModalDescription2= /*[[#{cookieBanner.preferencesModal.description.2}]]*/ "";
const cookieBannerPreferencesModalDescription3 = /*[[#{cookieBanner.preferencesModal.description.3}]]*/ "";
const cookieBannerPreferencesModalNecessaryTitle1 = /*[[#{cookieBanner.preferencesModal.necessary.title.1}]]*/ "";
const cookieBannerPreferencesModalNecessaryTitle2 = /*[[#{cookieBanner.preferencesModal.necessary.title.2}]]*/ "";
const cookieBannerPreferencesModalNecessaryDescription = /*[[#{cookieBanner.preferencesModal.necessary.description}]]*/ "";
const cookieBannerPreferencesModalAnalyticsTitle = /*[[#{cookieBanner.preferencesModal.analytics.title}]]*/ "";
const cookieBannerPreferencesModalAnalyticsDescription = /*[[#{cookieBanner.preferencesModal.analytics.description}]]*/ "";
if (analyticsEnabled) {
!function (t, e) {
var o, n, p, r;
e.__SV || (window.posthog = e, e._i = [], e.init = function (i, s, a) {
function g(t, e) {
var o = e.split(".");
2 == o.length && (t = t[o[0]], e = o[1]), t[e] = function () {
t.push([e].concat(Array.prototype.slice.call(arguments, 0)))
}
}
(p = t.createElement("script")).type = "text/javascript", p.async = !0, p.src = s.api_host + "/static/array.js", (r = t.getElementsByTagName("script")[0]).parentNode.insertBefore(p, r);
var u = e;
for (void 0 !== a ? u = e[a] = [] : a = "posthog", u.people = u.people || [], u.toString = function (t) {
var e = "posthog";
return "posthog" !== a && (e += "." + a), t || (e += " (stub)"), e
}, u.people.toString = function () {
return u.toString(1) + ".people (stub)"
}, o = "capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "), n = 0; n < o.length; n++) g(u, o[n]);
e._i.push([i, s, a])
}, e.__SV = 1)
}(document, window.posthog || []);
posthog.init('phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq', {
api_host: 'https://eu.i.posthog.com',
persistence: 'localStorage',
person_profiles: 'always',
mask_all_text: true,
mask_all_element_attributes: true,
opt_out_capturing_by_default: true
})
const baseUrl = window.location.hostname;
posthog.register_once({
'hostname': baseUrl,
'UUID': /*[[${@UUID}]]*/ '',
'app_version': /*[[${@appVersion}]]*/ '',
});
}
window.addEventListener("cc:onConsent", UpdatePosthogConsent);
window.addEventListener("cc:onChange", UpdatePosthogConsent);
</script>
</th:block>
<th:block th:fragment="game">
<dialog id="game-container-wrapper" class="game-container-wrapper" data-bs-modal>
<script th:inline="javascript">
console.log("loaded game");
$(document).ready(function () {
// Find the file input within the form
var fileInput = $('input[type="file"]');
// Find the closest enclosing form of the file input
var form = fileInput.closest('form');
// Find the submit button within the form
var submitButton = form.find('button[type="submit"], input[type="submit"]');
const boredWaitingText = /*[[#{bored}]]*/ 'Bored Waiting?';
const downloadCompleteText = /*[[#{downloadComplete}]]*/ 'Download Complete';
window.downloadCompleteText = downloadCompleteText;
// Create the 'show-game-btn' button
var gameButton = $('<button type="button" class="btn btn-primary" id="show-game-btn" style="display:none;">' + boredWaitingText + '</button>');
// Insert the 'show-game-btn' just above the submit button
submitButton.before(gameButton);
function loadGameScript(callback) {
console.log('loadGameScript called');
const script = document.createElement('script');
script.src = 'js/game.js';
script.onload = callback;
document.body.appendChild(script);
}
let gameScriptLoaded = false;
const gameDialog = document.getElementById('game-container-wrapper');
$('#show-game-btn').on('click', function () {
console.log('Show game button clicked');
if (!gameScriptLoaded) {
console.log('Show game button load');
loadGameScript(function () {
console.log('Game script loaded');
window.initializeGame();
gameScriptLoaded = true;
});
} else {
window.resetGame();
}
gameDialog.showModal();
});
gameDialog.addEventListener("click", e => {
const dialogDimensions = gameDialog.getBoundingClientRect()
if (
e.clientX < dialogDimensions.left ||
e.clientX > dialogDimensions.right ||
e.clientY < dialogDimensions.top ||
e.clientY > dialogDimensions.bottom
) {
gameDialog.close();
}
})
})
</script>
<div id="game-container">
<div id="lives">Lives: 3</div>
<div id="score">Score: 0</div>
<div id="high-score">High Score: 0</div>
<div id="level">Level: 1</div>
<img th:src="@{'/favicon.svg'}" class="player" id="player" alt="favicon">
</div>
<link rel="stylesheet" th:href="@{'/css/game.css'}">
</dialog>
</th:block>
<th:block th:fragment="fileSelector(name, multipleInputsForSingleRequest)"
th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}, remoteCall=${remoteCall} ?: true, disableMultipleFiles=${disableMultipleFiles} ?: false, showUploads=${showUploads} ?: true, notRequired=${notRequired} ?: false">
<script th:inline="javascript">
(function () {
window.stirlingPDF.pdfPasswordPrompt = /*[[#{error.pdfPassword}]]*/ '';
window.stirlingPDF.multipleInputsForSingleRequest = /*[[${multipleInputsForSingleRequest}]]*/ false;
window.stirlingPDF.disableMultipleFiles = /*[[${disableMultipleFiles}]]*/ false;
window.stirlingPDF.remoteCall = /*[[${remoteCall}]]*/ true;
window.stirlingPDF.sessionExpired = /*[[#{session.expired}]]*/ '';
window.stirlingPDF.refreshPage = /*[[#{session.refreshPage}]]*/ 'Refresh Page';
window.stirlingPDF.error = /*[[#{error}]]*/ "Error";
window.stirlingPDF.uploadLimitReadable = /*[[${@uploadLimitService.getReadableUploadLimit()}]]*/ 'Unlimited';
window.stirlingPDF.uploadLimit = /*[[${@uploadLimitService.getUploadLimit()}]]*/ 0;
window.stirlingPDF.uploadLimitExceededSingular = /*[[#{uploadLimitExceededSingular}]]*/ 'is too large. Maximum allowed size is';
window.stirlingPDF.uploadLimitExceededPlural = /*[[#{uploadLimitExceededPlural}]]*/ 'are too large. Maximum allowed size is';
window.stirlingPDF.pdfCorruptedMessage = /*[[#{error.pdfInvalid}]]*/ 'The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the \'Repair PDF\' feature to fix the file before proceeding.';
window.stirlingPDF.tryRepairMessage = /*[[#{error.tryRepair}]]*/ 'Try using the Repair PDF feature to fix corrupted files.';
})();
</script>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script th:src="@{'/js/downloader.js'}"></script>
<script>
window.decrypt = {
passwordPrompt: '[[#{decrypt.passwordPrompt}]]',
cancelled: '[[#{decrypt.cancelled}]]',
noPassword: '[[#{decrypt.noPassword}]]',
invalidPassword: '[[#{decrypt.invalidPassword}]]',
invalidPasswordHeader: '[[#{decrypt.invalidPasswordHeader}]]',
unexpectedError: '[[#{decrypt.unexpectedError}]]',
serverError: '[[#{decrypt.serverError}]]',
success: '[[#{decrypt.success}]]',
};
window.fileInput = {
dragAndDropPDF: '[[#{fileChooser.dragAndDropPDF}]]',
dragAndDropImage: '[[#{fileChooser.dragAndDropImage}]]',
addAttachments: '[[#{fileChooser.addAttachments}]]',
extractPDF: '[[#{fileChooser.extractPDF}]]',
loading: '[[#{loading}]]'
};</script>
<div class="custom-file-chooser mb-3"
th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-element-container-id=${name+'-input-container'}, data-bs-show-uploads=${showUploads}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}, data-bs-no-file-selected=#{noFileSelected}">
<div class="mb-3 d-flex flex-column justify-content-center align-items-center flex-wrap input-container"
th:name="${name}+'-input'" th:id="${name}+'-input-container'" th:data-text="#{fileChooser.hoveredDragAndDrop}">
<label class="file-input-btn d-none">
<input type="file" class="form-control"
th:name="${name}"
th:id="${name}+'-input'"
th:accept="${accept == null ? '*/*': ((accept == '*/*') ? accept : (accept + ',.zip'))}"
th:attr="multiple=${!disableMultipleFiles}"
th:required="${notRequired} ? null : 'required'">
Browse
</label>
<div class="d-flex flex-column align-items-center">
<div class="text-center" id="fileInputText" style="word-wrap: break-word; hyphens: auto; line-height: 1.2;">
<span th:text="#{fileChooser.click}"></span>
<span th:text="#{fileChooser.or}" style="margin: 0 5px;"></span>
<span th:text="#{fileChooser.dragAndDrop}" id="dragAndDrop"></span>
</div>
<hr th:if="${@GoogleDriveEnabled == true}" class="horizontal-divider" >
</div>
<div th:if="${@GoogleDriveEnabled == true}" th:id="${name}+'-google-drive-button'" class="google-drive-button" th:attr="data-name=${name}, data-multiple=${!disableMultipleFiles}, data-accept=${accept}" >
<img th:src="@{'/images/google-drive.svg'}" alt="google drive">
</div>
</div>
<div class="selected-files flex-wrap"></div>
<div class="text-muted small mt-0 text-end w-100" th:if="${@uploadLimitService.getUploadLimit() != 0}">
<span th:text="#{uploadLimit}">Maximum file size: </span>
<span th:text="${@uploadLimitService.getReadableUploadLimit()}"></span>
</div>
</div>
<div class="progressBarContainer" style="display: none; position: relative;">
<div class="progress" style="height: 1rem;">
<div class="progressBar progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
<script th:src="@{'/js/fileInput.js'}" type="module"></script>
<div th:if="${@GoogleDriveEnabled == true}" >
<script type="text/javascript" th:src="@{'/js/googleFilePicker.js'}"></script>
<script async defer src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script>
<script async defer src="https://accounts.google.com/gsi/client" onload="gisLoaded()"></script>
<script th:inline="javascript">
window.stirlingPDF.GoogleDriveClientId = /*[[${@GoogleDriveConfig.getClientId()}]]*/ null;
window.stirlingPDF.GoogleDriveApiKey = /*[[${@GoogleDriveConfig.getApiKey()}]]*/ null;
window.stirlingPDF.GoogleDriveAppId = /*[[${@GoogleDriveConfig.getAppId()}]]*/ null;
</script>
</div>
</th:block>
@@ -0,0 +1,75 @@
<th:block th:fragment="errorBanner">
<br th:if="${message}">
<div id="errorContainer" th:if="${message}" class="alert alert-danger alert-dismissible alert-container fade show" role="alert">
<div class="alert-header d-flex">
<span class="alert-heading" th:text="'Error: ' + ${status} + ' ' + ${error}"></span>
<button type="button" class="btn btn-danger" th:if="${trace}" onclick="toggletrace()" th:text="#{error.showStack}"></button>
<button type="button" class="btn btn-secondary" th:if="${trace}" onclick="copytrace()" th:text="#{error.copyStack}"></button>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" onclick="dismissError()">
<span class="material-symbols-rounded">
close
</span>
</button>
</div>
<p th:text="${message} + ' for path: ' + ${path}"></p>
<!-- Stack trace section -->
<div id="trace" th:if="${trace}" style="max-height: 0; overflow: hidden;">
<div>
<pre id="traceContent" th:text="${trace}"></pre>
</div>
<!-- Buttons to submit a ticket on GitHub and join Discord server -->
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" target="_blank" th:text="#{error.github}"></a>
<a href="https://discord.gg/HYmhKj45pU" id="discord-button" target="_blank" th:text="#{joinDiscord}"></a>
</div>
</div>
<script>
var traceVisible = false;
function toggletrace() {
var traceDiv = document.getElementById("trace");
if (!traceVisible) {
traceDiv.style.maxHeight = "500px";
traceVisible = true;
} else {
traceDiv.style.maxHeight = "0px";
traceVisible = false;
}
adjustContainerHeight();
}
function copytrace() {
var flip = false
if (!traceVisible) {
toggletrace()
flip = true
}
var traceContent = document.getElementById("traceContent");
var range = document.createRange();
range.selectNode(traceContent);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
document.execCommand("copy");
window.getSelection().removeAllRanges();
if (flip) {
toggletrace()
}
}
function dismissError() {
var errorContainer = document.getElementById("errorContainer");
errorContainer.style.display = "none";
errorContainer.style.height = "0";
}
function adjustContainerHeight() {
var errorContainer = document.getElementById("errorContainer");
var traceDiv = document.getElementById("trace");
if (traceVisible) {
errorContainer.style.height = errorContainer.scrollHeight - traceDiv.scrollHeight + traceDiv.offsetHeight + "px";
} else {
errorContainer.style.height = "auto";
}
}
</script>
</th:block>
@@ -0,0 +1,53 @@
<th:block th:fragment="errorBannerPerPage">
<div id="errorContainer" class="alert alert-danger alert-dismissible fade show" role="alert" style="display: none;">
<h4 class="alert-heading" th:text="#{error}"></h4>
<p></p>
<button type="button" class="btn btn-danger" onclick="toggletrace()" th:text="#{error.showStack}"></button>
<button type="button" class="btn btn-secondary" onclick="copytrace()" th:text="#{error.copyStack}"></button>
<button type="button" class="btn btn-info" onclick="showHelp()" th:text="#{help}"></button>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" onclick="dismissError()">
<span class="material-symbols-rounded">
close
</span>
</button>
<!-- Stack trace section -->
<div id="trace" style="max-height: 0; overflow: hidden;">
<div>
<pre id="traceContent"></pre>
</div>
</div>
</div>
<div class="modal fade" id="helpModal" tabindex="-1" role="dialog" aria-labelledby="helpModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document" id="helpModalDialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="helpModalLabel" th:text="#{help}"></h5>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" onclick="dismissError()">
<span class="material-symbols-rounded">
close
</span>
</button>
</div>
<div class="modal-body">
<div class="container">
<div id="support-section">
<h1 class="display-2" th:text="#{oops}"></h1>
<p class="lead" th:text="#{error.sorry}"></p>
<br>
<h2 th:text="#{error.needHelp}"></h2>
<p th:text="#{error.contactTip}"></p>
<div id="button-group">
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" target="_blank" th:text="#{error.githubSubmit}"></a>
<a href="https://discord.gg/HYmhKj45pU" id="discord-button" target="_blank" th:text="#{error.discordSubmit}"></a>
</div>
<a th:href="@{'/'}" class="home-button" th:text="#{goHomepage}"></a>
<a data-bs-dismiss="modal" class="home-button" th:text="#{close}"></a>
</div>
</div>
</div>
</div>
</div>
</div>
<script th:src="@{'/js/errorBanner.js'}"></script>
</th:block>
@@ -0,0 +1,3 @@
<div th:fragment="featureGroupHeader" class="feature-group-header">
<h6 class="menu-title" th:text="${groupTitle}"></h6>
</div>
@@ -0,0 +1,6 @@
<div th:fragment="featureGroupHeader" class="feature-group-header">
<h3 class="menu-title" th:text="${groupTitle}"></h3>
<span class="material-symbols-rounded header-expand-button">
chevron_right
</span>
</div>
@@ -0,0 +1,26 @@
<footer th:fragment="footer" id="footer" class="text-center">
<script type="module" th:src="@{'/js/thirdParty/cookieconsent-config.js'}"></script>
<div class="footer-center">
<!-- Links section -->
<div class="d-flex justify-content-center">
<ul class="list-unstyled footer-link-list">
<li><a class="footer-link px-2" id="licenses" target="_blank" th:href="@{'/licenses'}" th:text="#{licenses.nav}">Licenses</a></li>
<li><a class="footer-link px-2" id="releases" target="_blank" th:href="@{'/releases'}" th:text="#{releases.footer}">Releases</a></li>
<li><a class="footer-link px-2" id="survey" target="_blank" href="https://stirlingpdf.info/s/cm28y3niq000o56dv7liv8wsu" th:text="#{survey.nav}">Survey</a></li>
<li th:if="${@privacyPolicy != ''}"><a class="footer-link px-2" target="_blank" th:href="${@privacyPolicy}" th:text="#{legal.privacy}">privacyPolicy</a></li>
<li th:if="${@termsAndConditions != ''}"><a class="footer-link px-2" target="_blank" th:href="${@termsAndConditions}" th:text="#{legal.terms}">termsAndConditions</a></li>
<li th:if="${@accessibilityStatement != ''}"><a class="footer-link px-2" target="_blank" th:href="${@accessibilityStatement}" th:text="#{legal.accessibility}">accessibilityStatement</a></li>
<li th:if="${@cookiePolicy != ''}"><a class="footer-link px-2" target="_blank" th:href="${@cookiePolicy}" th:text="#{legal.cookie}">cookiePolicy</a></li>
<li th:if="${@impressum != ''}"><a class="footer-link px-2" target="_blank" th:href="${@impressum}" th:text="#{legal.impressum}">impressum</a></li>
<li><a class="footer-link px-2" id="cookieBanner" target="_blank" th:text="#{legal.showCookieBanner}" onClick="CookieConsent.show(true)">Cookie Preferences</a></li>
</ul>
</div>
<!-- powered by section -->
<div class="footer-powered-by">
<span th:text="#{poweredBy}"></span>
<a href="https://stirlingpdf.com" class="stirling-link">Stirling PDF</a>
</div>
</div>
</footer>
@@ -0,0 +1,7 @@
<th:block th:fragment="languageEntry(code, name)">
<div th:if="${code} == 'en_GB' or ${#lists.isEmpty(@languages) or #lists.contains(@languages, code)}" class="lang-dropdown-item-wrapper">
<a
class="dropdown-item lang_dropdown-item" href="" th:data-bs-language-code="@{${code}}" th:text="${name}">
</a>
</div>
</th:block>
@@ -0,0 +1,44 @@
<th:block th:fragment="langs">
<div th:replace="~{fragments/languageEntry :: languageEntry ('bg_BG', 'Български')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('ar_AR', 'العربية')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('ca_CA', 'Català')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('zh_CN', '简体中文')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('zh_TW', '繁體中文')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('bo_CN', 'བོད་ཡིག')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('az_AZ', 'Azərbaycan Dili')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('da_DK', 'Dansk')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('de_DE', 'Deutsch')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('en_GB', 'English (GB)')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('en_US', 'English (US)')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('eu_ES', 'Euskara')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('es_ES', 'Español')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('fr_FR', 'Français')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('id_ID', 'Bahasa Indonesia')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('ga_IE', 'Gaeilge')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('it_IT', 'Italiano')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('nl_NL', 'Nederlands')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('fa_IR', 'پارسی')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('pl_PL', 'Polski')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('pt_BR', 'Português (BR)')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('pt_PT', 'Português (PT)')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('ro_RO', 'Română')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('sk_SK', 'Slovenčina')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('sl_SI', 'Slovenščina')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('sv_SE', 'Svenska')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('tr_TR', 'Türkçe')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('ru_RU', 'Русский')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('ko_KR', '한국어')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('ja_JP', '日本語')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('el_GR', 'Ελληνικά')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('hu_HU', 'Magyar')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('hi_IN', 'हिन्दी')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('sr_LATN_RS', 'Srpski')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('uk_UA', 'Українська')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('cs_CZ', 'Česky')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('hr_HR', 'Hrvatski')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('no_NB', 'Norsk')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('th_TH', 'ไทย')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('vi_VN', 'Tiếng Việt')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('ml_IN', 'മലയാളം')}"></div>
</th:block>
@@ -0,0 +1,65 @@
<div th:fragment="multi-toolAdvert" class="mx-auto">
<div id="multi-toolAdvert" class="multi-toolAdvert">
<div>
<span th:utext="#{multiTool-advert.message(|multi-tool|)}"></span>
<button id="closeMultiToolAdvert" style="position: absolute;
inset-inline-end: 12px;
inset-block-start: 10px;
border: none;
background: transparent;
color: white;
cursor: pointer;
font-size: 1.2rem;
line-height: 1;" aria-label="Close">&times;</button>
</div>
</div>
<style>
.multi-toolAdvert {
margin-bottom: 10px;
margin-inline-start: 50%;
max-width: 52rem;
z-index: 0;
background-color: var(--md-sys-color-surface-5);
border-radius: 2rem;
padding-block: 10px;
padding-inline: 20px 27px;
font-size: 0.9rem;
display: none;
justify-content: center;
}
.multi-toolAdvert a {
color: #007bff;
font-weight: bold;
}
.multi-toolAdvert a:hover {
text-decoration: underline;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
const advert = document.getElementById('multi-toolAdvert');
const closeBtn = document.getElementById('closeMultiToolAdvert');
const cacheKey = `closeMultiToolAdvert_${window.location.pathname}`;
const isRTL = document.documentElement.dir === 'rtl';
if (localStorage.getItem(cacheKey) !== 'true') {
advert.style.display = 'flex';
}
closeBtn.addEventListener('click', () => {
advert.style.display = 'none';
localStorage.setItem(cacheKey, 'true');
});
if (isRTL) {
advert.style.transform = 'translateX(50%)'; // Flip direction for RTL
} else {
advert.style.transform = 'translateX(-50%)';
}
});
</script>
</div>
@@ -0,0 +1,291 @@
<th:block th:fragment="navElements">
<div id="groupOrganize" class="feature-group">
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.organize})}">
</div>
<div class="nav-group-container">
<div
th:replace="~{fragments/navbarEntry :: navbarEntry ('multi-tool', 'construction', 'home.multiTool.title', 'home.multiTool.desc', 'multiTool.tags', 'organize')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry ('merge-pdfs', 'add_to_photos', 'home.merge.title', 'home.merge.desc', 'merge.tags', 'organize')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry ('split-pdfs', 'cut', 'home.split.title', 'home.split.desc', 'split.tags', 'organize')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('rotate-pdf', 'rotate_right', 'home.rotate.title', 'home.rotate.desc', 'rotate.tags', 'organize')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('crop', 'crop', 'home.crop.title', 'home.crop.desc', 'crop.tags', 'organize')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-organizer', 'format_list_bulleted', 'home.pdfOrganiser.title', 'home.pdfOrganiser.desc', 'pdfOrganiser.tags', 'organize')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('remove-pages', 'delete', 'home.removePages.title', 'home.removePages.desc', 'removePages.tags', 'organize')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('multi-page-layout', 'dashboard', 'home.pageLayout.title', 'home.pageLayout.desc', 'pageLayout.tags', 'organize')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('scale-pages', 'fullscreen', 'home.scalePages.title', 'home.scalePages.desc', 'scalePages.tags', 'organize')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('extract-page', 'upload', 'home.extractPage.title', 'home.extractPage.desc', 'extractPage.tags', 'organize')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-single-page', 'looks_one', 'home.PdfToSinglePage.title', 'home.PdfToSinglePage.desc', 'PdfToSinglePage.tags', 'organize')}">
</div>
</div>
</div>
<div id="groupConvertTo" class="feature-group">
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.convertTo})}">
</div>
<div class="nav-group-container">
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('img-to-pdf', 'picture_as_pdf', 'home.imageToPdf.title', 'home.imageToPdf.desc', 'imageToPdf.tags', 'convertto')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('url-to-pdf', 'link', 'home.URLToPDF.title', 'home.URLToPDF.desc', 'URLToPDF.tags', 'convertto')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('html-to-pdf', 'html', 'home.HTMLToPDF.title', 'home.HTMLToPDF.desc', 'HTMLToPDF.tags', 'convertto')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('markdown-to-pdf', 'markdown', 'home.MarkdownToPDF.title', 'home.MarkdownToPDF.desc', 'MarkdownToPDF.tags', 'convertto')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('eml-to-pdf', 'email', 'home.EMLToPDF.title', 'home.EMLToPDF.desc', 'EMLToPDF.tags', 'convertto')}">
</div>
</div>
</div>
<div id="groupConvertFrom" class="feature-group">
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.convertFrom})}">
</div>
<div class="nav-group-container">
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-img', 'photo_library', 'home.pdfToImage.title', 'home.pdfToImage.desc', 'pdfToImage.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-pdfa', 'picture_as_pdf', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc', 'pdfToPDFA.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-word', 'description', 'home.PDFToWord.title', 'home.PDFToWord.desc', 'PDFToWord.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-presentation', 'slideshow', 'home.PDFToPresentation.title', 'home.PDFToPresentation.desc', 'PDFToPresentation.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-text', 'text_fields', 'home.PDFToText.title', 'home.PDFToText.desc', 'PDFToText.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-html', 'html', 'home.PDFToHTML.title', 'home.PDFToHTML.desc', 'PDFToHTML.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-xml', 'code', 'home.PDFToXML.title', 'home.PDFToXML.desc', 'PDFToXML.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-csv', 'csv', 'home.tableExtraxt.title', 'home.tableExtraxt.desc', 'tableExtraxt.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-markdown', 'markdown_copy', 'home.PDFToMarkdown.title', 'home.PDFToMarkdown.desc', 'PDFToMarkdown.tags', 'convert')}">
</div>
</div>
</div>
<div id="convertGroup" class="feature-group">
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.convertTo})}">
</div>
<div class="nav-group-container">
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('img-to-pdf', 'picture_as_pdf', 'home.imageToPdf.title', 'home.imageToPdf.desc', 'imageToPdf.tags', 'convertto')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('url-to-pdf', 'link', 'home.URLToPDF.title', 'home.URLToPDF.desc', 'URLToPDF.tags', 'convertto')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('html-to-pdf', 'html', 'home.HTMLToPDF.title', 'home.HTMLToPDF.desc', 'HTMLToPDF.tags', 'convertto')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('markdown-to-pdf', 'markdown', 'home.MarkdownToPDF.title', 'home.MarkdownToPDF.desc', 'MarkdownToPDF.tags', 'convertto')}">
</div>
</div>
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.convertFrom})}">
</div>
<div class="nav-group-container">
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-img', 'photo_library', 'home.pdfToImage.title', 'home.pdfToImage.desc', 'pdfToImage.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-pdfa', 'picture_as_pdf', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc', 'pdfToPDFA.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-word', 'description', 'home.PDFToWord.title', 'home.PDFToWord.desc', 'PDFToWord.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-presentation', 'slideshow', 'home.PDFToPresentation.title', 'home.PDFToPresentation.desc', 'PDFToPresentation.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-text', 'text_fields', 'home.PDFToText.title', 'home.PDFToText.desc', 'PDFToText.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-html', 'html', 'home.PDFToHTML.title', 'home.PDFToHTML.desc', 'PDFToHTML.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-xml', 'code', 'home.PDFToXML.title', 'home.PDFToXML.desc', 'PDFToXML.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-csv', 'csv', 'home.tableExtraxt.title', 'home.tableExtraxt.desc', 'tableExtraxt.tags', 'convert')}">
</div>
</div>
</div>
<div id="groupSecurity" class="feature-group">
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.security})}">
</div>
<div class="nav-group-container">
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('add-password', 'lock', 'home.addPassword.title', 'home.addPassword.desc', 'addPassword.tags', 'security')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('remove-password', 'lock_open_right', 'home.removePassword.title', 'home.removePassword.desc', 'removePassword.tags', 'security')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('change-permissions', 'encrypted', 'home.permissions.title', 'home.permissions.desc', 'permissions.tags', 'security')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('sign', 'signature', 'home.sign.title', 'home.sign.desc', 'sign.tags', 'security')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('cert-sign', 'workspace_premium', 'home.certSign.title', 'home.certSign.desc', 'certSign.tags', 'security')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('validate-signature', 'verified', 'home.validateSignature.title', 'home.validateSignature.desc', 'validateSignature.tags', 'security')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('remove-cert-sign', 'remove_moderator', 'home.removeCertSign.title', 'home.removeCertSign.desc', 'removeCertSign.tags', 'security')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('sanitize-pdf', 'sanitizer', 'home.sanitizePdf.title', 'home.sanitizePdf.desc', 'sanitizePdf.tags', 'security')}">
</div>
<div
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('auto-redact', '/images/redact-auto.svg#icon-redact-auto', 'home.autoRedact.title', 'home.autoRedact.desc', 'autoRedact.tags', 'security')}">
</div>
<div
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('redact', '/images/redact-manual.svg#icon-redact-manual', 'home.redact.title', 'home.redact.desc', 'redact.tags', 'security')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('stamp', 'approval', 'home.AddStampRequest.title', 'home.AddStampRequest.desc', 'AddStampRequest.tags', 'security')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('add-watermark', 'water_drop', 'home.watermark.title', 'home.watermark.desc', 'watermark.tags', 'security')}">
</div>
</div>
</div>
<div id="groupView" class="feature-group">
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.edit})}">
</div>
<div class="nav-group-container">
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('view-pdf', 'menu_book', 'home.viewPdf.title', 'home.viewPdf.desc', 'viewPdf.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('add-page-numbers', '123', 'home.add-page-numbers.title', 'home.add-page-numbers.desc', 'add-page-numbers.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('add-image', 'text_fields', 'home.addImage.title', 'home.addImage.desc', 'addImage.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('change-metadata', 'assignment', 'home.changeMetadata.title', 'home.changeMetadata.desc', 'changeMetadata.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('ocr-pdf', 'quick_reference_all', 'home.ocr.title', 'home.ocr.desc', 'ocr.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('extract-images', 'photo_library', 'home.extractImages.title', 'home.extractImages.desc', 'extractImages.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('flatten', 'layers_clear', 'home.flatten.title', 'home.flatten.desc', 'flatten.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('remove-blanks', 'scan_delete', 'home.removeBlanks.title', 'home.removeBlanks.desc', 'removeBlanks.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('remove-annotations', 'thread_unread', 'home.removeAnnotations.title', 'home.removeAnnotations.desc', 'removeAnnotations.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('compare', 'compare', 'home.compare.title', 'home.compare.desc', 'compare.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('get-info-on-pdf', 'info', 'home.getPdfInfo.title', 'home.getPdfInfo.desc', 'getPdfInfo.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('remove-image-pdf', 'remove_selection', 'home.removeImagePdf.title', 'home.removeImagePdf.desc', 'removeImagePdf.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('replace-and-invert-color-pdf', 'format_color_fill', 'home.replaceColorPdf.title', 'home.replaceColorPdf.desc', 'replaceColorPdf.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('unlock-pdf-forms', 'preview_off', 'home.unlockPDFForms.title', 'home.unlockPDFForms.desc', 'unlockPDFForms.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('add-attachments', 'attachment', 'home.attachments.title', 'home.attachments.desc', 'attachments.tags', 'other')}">
</div>
</div>
</div>
<div id="groupAdvanced" class="feature-group">
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.advance})}">
</div>
<div class="nav-group-container">
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pipeline', 'family_history', 'home.pipeline.title', 'home.pipeline.desc', 'pipeline.tags', 'advance')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('adjust-contrast', 'palette', 'home.adjust-contrast.title', 'home.adjust-contrast.desc', 'adjust-contrast.tags', 'advance')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('compress-pdf', 'zoom_in_map', 'home.compressPdfs.title', 'home.compressPdfs.desc', 'compressPdfs.tags', 'advance')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('extract-image-scans', 'scanner', 'home.ScannerImageSplit.title', 'home.ScannerImageSplit.desc', 'ScannerImageSplit.tags', 'advance')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('repair', 'build', 'home.repair.title', 'home.repair.desc', 'repair.tags', 'advance')}">
</div>
<div
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('auto-rename', '/images/rename.svg#icon-rename', 'home.auto-rename.title', 'home.auto-rename.desc', 'auto-rename.tags', 'advance')}">
</div>
<div
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('auto-split-pdf', '/images/split-auto.svg#icon-split-auto', 'home.autoSplitPDF.title', 'home.autoSplitPDF.desc', 'autoSplitPDF.tags', 'advance')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('show-javascript', 'javascript', 'home.showJS.title', 'home.showJS.desc', 'showJS.tags', 'advance')}">
</div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry('fake-scan', 'scanner', 'fakeScan.title', 'fakeScan.description', 'fakeScan.tags', 'advance')}"></div>
<div
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('split-by-size-or-count', '/images/split-size.svg#icon-split-size', 'home.autoSizeSplitPDF.title', 'home.autoSizeSplitPDF.desc', 'autoSizeSplitPDF.tags', 'advance')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('overlay-pdf', 'layers', 'home.overlay-pdfs.title', 'home.overlay-pdfs.desc', 'overlay-pdfs.tags', 'advance')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('split-pdf-by-sections', 'grid_on', 'home.split-by-sections.title', 'home.split-by-sections.desc', 'split-by-sections.tags', 'advance')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('edit-table-of-contents', 'bookmark_add', 'home.editTableOfContents.title', 'home.editTableOfContents.desc', 'editTableOfContents.tags', 'advance')}">
</div>
<div
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('split-pdf-by-chapters', '/images/split-chapters.svg#icon-split-chapters', 'home.splitPdfByChapters.title', 'home.splitPdfByChapters.desc', 'splitPdfByChapters.tags', 'advance')}">
</div>
</div>
</div>
</th:block>
@@ -0,0 +1,283 @@
<div th:fragment="navbar" class="mx-auto" style="position: sticky; top:0; z-index:10000; width:100%">
<script th:src="@{'/js/languageSelection.js'}"></script>
<script th:src="@{'/js/navbar.js'}"></script>
<script th:src="@{'/js/additionalLanguageCode.js'}"></script>
<script th:inline="javascript">
// Initializing the scripts
initLanguageSettings();
document.addEventListener('DOMContentLoaded', function () {
toolsManager();
});
</script>
<script th:inline="javascript">
const currentVersion = /*[[${@appVersion}]]*/ '';
const noFavourites = /*[[#{noFavourites}]]*/ '';
console.log(noFavourites);
const updateAvailable = /*[[#{settings.updateAvailable}]]*/ '';
</script>
<script th:src="@{'/js/homecard.js'}"></script>
<script th:src="@{'/js/githubVersion.js'}"></script>
<form th:action="@{'/dummyFormToPopulateCSRF'}" method="post" enctype="multipart/form-data"></form>
<nav class="navbar navbar-expand-xl" style="
background: var(--md-nav-background);
border-bottom-style: solid;
border-bottom-width: 1px;
border-color: var(--md-nav-color-on-separator)">
<div class="container " style="max-width: 100%;">
<a class="navbar-brand" th:href="${@contextPath}" style="display: flex;">
<img class="main-icon" th:src="@{'/favicon.svg'}" alt="icon">
<span class="icon-text" th:text="${@navBarText}"></span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="material-symbols-rounded">
menu
</span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto flex-nowrap">
<!-- All Tools -->
<li id="navItemToHide" class="nav-item dropdown dropdown-mega position-static">
<a class="nav-link" id="navbarDropdown-1" href="#" role="button" data-bs-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<span class="material-symbols-rounded">
apps
</span>
<span class="icon-text" th:data-text="#{navbar.allTools}" th:text="#{navbar.allTools}"></span>
</a>
<div class="dropdown-menu dropdown-menu-tp" aria-labelledby="navbarDropdown-1">
<div class="dropdown-menu-wrapper" style="justify-content: center; display:flex">
<div class="feature-rows">
<th:block th:insert="~{fragments/navElements.html :: navElements}"></th:block>
</div>
</div>
</div>
</li>
<li class="nav-item" th:if="${@endpointConfiguration.isEndpointEnabled('multi-tool')}">
<a class="nav-link" href="#" th:href="@{'/multi-tool'}"
th:classappend="${currentPage}=='multi-tool' ? 'active' : ''" th:title="#{home.multiTool.desc}">
<span class="material-symbols-rounded">
construction
</span>
<span class="icon-text" th:data-text="#{navbar.multiTool}" th:text="#{navbar.multiTool}"></span>
</a>
</li>
<li class="nav-item" th:if="${@endpointConfiguration.isEndpointEnabled('pipeline')}">
<a class="nav-link" href="#" th:href="@{'/pipeline'}"
th:classappend="${currentPage}=='pipeline' ? 'active' : ''" th:title="#{home.pipeline.desc}">
<span class="material-symbols-rounded">
family_history
</span>
<span class="icon-text" th:data-text="#{home.pipeline.title}" th:text="#{home.pipeline.title}"></span>
</a>
</li>
<li class="nav-item" th:if="${@endpointConfiguration.isEndpointEnabled('compress-pdf')}">
<a class="nav-link" href="#" title="#{home.compressPdfs.title}" th:href="@{'/compress-pdf'}"
th:classappend="${currentPage}=='compress-pdf' ? 'active' : ''" th:title="#{home.compressPdfs.desc}">
<span class="material-symbols-rounded">
zoom_in_map
</span>
<span class="icon-text" th:data-text="#{home.compressPdfs.title}"
th:text="#{home.compressPdfs.title}"></span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" th:href="@{'/view-pdf'}"
th:classappend="${currentPage}=='view-pdf' ? 'active' : ''" th:title="#{home.viewPdf.desc}">
<span class="material-symbols-rounded">
menu_book
</span>
<span class="icon-text" th:data-text="#{home.viewPdf.title}" th:text="#{home.viewPdf.title}"></span>
</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link" href="#" th:href="@{'/merge-pdfs'}"
th:classappend="${currentPage}=='merge-pdfs' ? 'active' : ''" th:title="#{home.merge.desc}">
<span class="material-symbols-rounded">
add_to_photos
</span>
<span class="icon-text" th:data-text="#{home.merge.title}" th:text="#{home.merge.title}"></span>
</a>
</li> -->
</ul>
<ul class="navbar-nav flex-nowrap">
<li class="nav-item dropdown">
<a class="nav-link" id="navbarDropdown-5" href="#" role="button" data-bs-toggle="dropdown"
aria-haspopup="true" aria-expanded="false" th:title="#{navbar.favorite}">
<span class="material-symbols-rounded">
star
</span>
<span class="icon-text icon-hide" th:data-text="#{navbar.favorite}" th:text="#{navbar.favorite}"></span>
</a>
<div class="dropdown-menu dropdown-menu-tp dropdown-mw-28" aria-labelledby="navbarDropdown-5">
<div class="dropdown-menu-wrapper px-xl-2 px-2" id="favoritesDropdown">
<!-- Dropdown items will be added here by JavaScript -->
</div>
</div>
</li>
<li class="nav-item">
<a class="nav-link" id="dark-mode-toggle" href="#" th:title="#{navbar.darkmode}">
<span class="material-symbols-rounded" id="dark-mode-icon">
dark_mode
</span>
<span class="icon-text icon-hide" id="dark-mode-text" th:data-text="#{navbar.darkmode}"
th:text="#{navbar.darkmode}"></span>
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link" href="#" id="languageDropdown" role="button" data-bs-toggle="dropdown"
aria-haspopup="true" aria-expanded="false" th:title="#{navbar.language}">
<span class="material-symbols-rounded">
language
</span>
<span class="icon-text icon-hide" th:data-text="#{navbar.language}" th:text="#{navbar.language}"></span>
</a>
<div class="dropdown-menu dropdown-menu-tp" aria-labelledby="languageDropdown">
<div class="dropdown-menu-wrapper px-xl-2 px-2">
<div id="languageSelection" class="scrollable-y lang_dropdown-mw scalable-languages-container">
<th:block th:insert="~{fragments/languages :: langs}"></th:block>
</div>
</div>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link" href="#" id="searchDropdown" role="button" data-bs-toggle="dropdown"
aria-haspopup="true" aria-expanded="false" th:title="#{navbar.search}">
<span class="material-symbols-rounded">
search
</span>
<span class="icon-text icon-hide">Search</span>
</a>
<div class="dropdown-menu dropdown-menu-tp" aria-labelledby="searchDropdown">
<div class="dropdown-menu-wrapper px-xl-2 px-2">
<form th:action="@{''}" class="d-flex p-2 search-form" id="searchForm">
<input class="form-control search-input" type="search" th:placeholder="#{navbar.search}"
aria-label="Search" id="navbarSearchInput">
</form>
<!-- Search Results -->
<div id="searchResults" class="search-results scrollable-y dropdown-mw-20"></div>
</div>
</div>
</li>
<li class="nav-item" th:if="${!@runningProOrHigher}">
<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>
</a>
</li>
<li class="nav-item">
<!-- Settings Button -->
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#settingsModal"
th:title="#{navbar.settings}">
<span class="material-symbols-rounded">
settings
</span>
<span class="icon-text icon-hide" th:data-text="#{navbar.settings}" th:text="#{navbar.settings}"></span>
</a>
</li>
</ul>
</div>
</div>
<script th:src="@{'/js/favourites.js'}"></script>
<script th:src="@{'/js/search.js'}"></script>
</nav>
<th:block th:insert="~{fragments/errorBannerPerPage.html :: errorBannerPerPage}"></th:block>
<div class="modal fade" id="settingsModal" tabindex="-1" role="dialog" aria-labelledby="settingsModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="settingsModalLabel" th:text="#{settings.title}"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">
close
</span>
</button>
</div>
<div class="modal-body">
<p class="mb-0" th:utext="#{settings.appVersion} + ' ' + ${@appVersion}"></p>
<div class="d-flex justify-content-between align-items-center mb-3 mt-3">
<div class="footer-center" style="flex-direction: row;">
<a href="https://github.com/Stirling-Tools/Stirling-PDF" class="mx-1" role="button" target="_blank"
th:title="#{visitGithub}">
<img th:src="@{'/images/github.svg'}" alt="github">
</a>
<a href="https://hub.docker.com/r/stirlingtools/stirling-pdf" class="mx-1" role="button" target="_blank"
th:title="#{seeDockerHub}">
<img th:src="@{'/images/docker.svg'}" alt="docker">
</a>
<a href="https://discord.gg/Cn8pWhQRxZ" class="mx-1" role="button" target="_blank"
th:title="#{joinDiscord}">
<img th:src="@{'/images/discord.svg'}" alt="discord">
</a>
</div>
<a th:href="@{'/swagger-ui/index.html'}" class="btn btn-sm btn-outline-primary mx-1" role="button"
target="_blank">API</a>
<a href="https://github.com/Stirling-Tools/Stirling-PDF/releases"
class="btn btn-sm btn-outline-primary mx-1" id="update-btn" th:utext="#{settings.update}" role="button"
target="_blank"></a>
</div>
<div class="mb-3">
<label for="downloadOption" th:utext="#{settings.downloadOption.title}"></label>
<select class="form-control" id="downloadOption">
<option value="sameWindow" th:utext="#{settings.downloadOption.1}"></option>
<option value="newWindow" th:utext="#{settings.downloadOption.2}"></option>
<option value="downloadFile" th:utext="#{settings.downloadOption.3}"></option>
</select>
</div>
<div class="mb-3">
<label for="zipThreshold" th:utext="#{settings.zipThreshold}"></label><br />
<input type="range" class="form-range" min="1" max="9" step="1" id="zipThreshold" value="4">
<span id="zipThresholdValue" class="ms-2"></span>
</div>
<div class="form-check mb-3">
<input type="checkbox" id="boredWaiting" th:title="#{settings.bored.help}">
<label for="boredWaiting" th:text="#{bored}"></label>
</div>
<div class="form-check mb-3">
<input type="checkbox" id="cacheInputs" th:title="#{settings.cacheInputs.help}">
<label for="cacheInputs" th:text="#{settings.cacheInputs.name}"></label>
</div>
<a th:if="${@loginEnabled and @activeSecurity}" th:href="@{'/account'}" class="btn btn-sm btn-outline-primary"
role="button" th:text="#{settings.accountSettings}" target="_blank">Account Settings</a>
</div>
<div class="modal-footer">
<a th:if="${@loginEnabled and @activeSecurity}" class="btn btn-danger" role="button"
th:text="#{settings.signOut}" th:href="@{'/logout'}">Sign Out</a>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
</div>
</div>
</div>
</div>
<script th:src="@{'/js/settings.js'}"></script>
<script th:inline="javascript">
window.onload = function () {
updateFavoritesDropdown();
}
document.addEventListener('DOMContentLoaded', function () {
const navbarLink = document.querySelector(".navbar-brand");
const contentPath = /*[[${@contextPath}]]*/ '';
if (localStorage.getItem("defaultView") === "home-legacy") {
navbarLink.setAttribute("href", contentPath + "home-legacy");
} else {
navbarLink.setAttribute("href", contentPath);
}
});
</script>
</div>
@@ -0,0 +1,18 @@
<th:block th:fragment="navbarEntry(endpoint, toolIcon, titleKey, descKey, tagKey, toolGroup)"
th:if="${@endpointConfiguration.isEndpointEnabled(endpoint)}">
<a th:id="@{${endpoint}}" class="dropdown-item" style="position:relative" th:href="@{${endpoint}}"
th:data-bs-link="@{${endpoint}}"
th:classappend="${endpoint.equals(currentPage)} ? ${toolGroup} + ' active' : '' + ${toolGroup}"
th:data-bs-tags="#{${tagKey}}" th:data-bs-title='#{${titleKey}}'>
<div style="min-height:2.7rem; align-items: center;display: flex;" th:title="#{${descKey}}" class="icon" alt="icon"
th:class="@{${toolGroup}}">
<span class="material-symbols-rounded nav-icon" th:text="@{${toolIcon}}" style=" align-items:center; display: flex; justify-content: center; height:2.7rem; width:2.7rem"></span>
<span class="icon-text" th:text="#{${titleKey}}"></span>
</div>
<span class="material-symbols-rounded favorite-icon" style="display: none;" th:data-endpoint="@{${endpoint}}"
th:onclick="'addToFavorites(\'' + @{${endpoint}} + '\')'">
add
</span>
</a>
</th:block>
@@ -0,0 +1,21 @@
<th:block th:fragment="navbarEntry(endpoint, toolIcon, titleKey, descKey, tagKey, toolGroup)"
th:if="${@endpointConfiguration.isEndpointEnabled(endpoint)}">
<a th:id="@{${endpoint}}" class="dropdown-item" style="position:relative" th:href="@{${endpoint}}"
th:data-bs-link="@{${endpoint}}"
th:classappend="${endpoint.equals(currentPage)} ? ${toolGroup} + ' active' : '' + ${toolGroup}"
th:data-bs-tags="#{${tagKey}}" th:data-bs-title='#{${titleKey}}'>
<div style="min-height:2.5rem;align-items: center;display: flex" th:title="#{${descKey}}" class="icon" alt="icon"
th:class="@{${toolGroup}}">
<svg class="nav-icon"
style="height: 2.7rem; width:2.7rem; align-items:center; display: flex; justify-content: center;">
<use th:xlink:href="@{${toolIcon}}"></use>
</svg>
<span class="icon-text" th:text="#{${titleKey}}"></span>
</div>
<span class="material-symbols-rounded favorite-icon" style="display: none;" th:data-endpoint="@{${endpoint}}"
th:onclick="'addToFavorites(\'' + @{${endpoint}} + '\')'">
star
</span>
</a>
</th:block>
@@ -0,0 +1,531 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title='')}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<!-- Jumbotron -->
<div class="p-5 rounded d-none d-md-block" id="jumbotron">
<div class="container">
<h1 class="display-4 fw-normal" th:text="${@appName}"></h1>
<p class="lead fs-4"
th:text="${@homeText != 'null' and @homeText != null and @homeText != ''} ? ${@homeText} : #{home.desc}">
</p>
</div>
</div>
<br class="d-md-none">
<!-- Features -->
<script th:src="@{'/js/homecard-legacy.js'}"></script>
<div class=" container">
<br>
<span class="material-symbols-rounded search-icon">
search
</span>
<input type="text" id="searchBar" onkeyup="filterCardsLegacy()" th:placeholder="#{home.searchBar}" autofocus>
<div style="display: flex; align-items: center;">
<a href="home" onclick="setAsDefault('home')"
style="text-decoration: none; color: inherit; cursor: pointer; display: flex; align-items: center;">
<span th:text="#{home.newHomePage}">
</span>
<span class="material-symbols-rounded toggle-favourites" style="font-size: 2rem; margin-left: 0.2rem;">
home
</span>
</a>
</div>
<div id="filtersContainer">
<span class="material-symbols-rounded filter-button" onclick="toggleFavoritesOnly()">
star
</span>
<span class="material-symbols-rounded filter-button" onclick="expandCollapseAll(true)">
expand_all
</span>
<span class="material-symbols-rounded filter-button" onclick="expandCollapseAll(false)">
collapse_all
</span>
<span class="material-symbols-rounded filter-button hidden" onclick="switchViewMode()">
dashboard
</span>
</div>
<div class="features-container">
<div th:if="${@shouldShow}" class="feature-card favorite update-notice visually-hidden" id="update-link-legacy">
<a href="https://github.com/Stirling-Tools/Stirling-PDF/releases" target="_blank" rel="noopener">
<div class="d-flex align-items-center">
<div id="tool-icon" class="advance" alt="icon">
<span class="material-symbols-rounded nav-icon">update</span>
</div>
<div id="tool-text">
<h5 class="card-title" th:text="#{settings.update}"></h5>
<p class="card-text" id="app-update"></p>
</div>
</div>
</a>
</div>
<div id="groupFavorites" class="feature-group-legacy">
<div
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.favorite})}">
</div>
<div class="feature-group-container">
</div>
</div>
<div id="popularTools" class="feature-group-legacy">
<div
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.popular})}">
</div>
<div class="feature-group-container">
<div
th:replace="~{fragments/card :: card(id='view-pdf', cardTitle=#{home.viewPdf.title}, cardText=#{home.viewPdf.desc}, cardLink='view-pdf', toolIcon='menu_book', tags=#{viewPdf.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', toolIcon='construction', tags=#{multiTool.tags}, toolGroup='organize')}">
</div>
<div
th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', toolIcon='family_history', tags=#{pipeline.tags}, toolGroup='advance')}">
</div>
<div
th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', toolIcon='zoom_in_map', tags=#{compressPdfs.tags}, toolGroup='advance')}">
</div>
</div>
</div>
<div id="groupOrganize" class="feature-group-legacy">
<div
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.organize})}">
</div>
<div class="feature-group-container">
<div
th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', toolIcon='construction', tags=#{multiTool.tags}, toolGroup='organize')}">
</div>
<div
th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', toolIcon='add_to_photos', tags=#{merge.tags}, toolGroup='organize')}">
</div>
<div
th:replace="~{fragments/card :: card(id='split-pdfs', cardTitle=#{home.split.title}, cardText=#{home.split.desc}, cardLink='split-pdfs', toolIcon='cut', tags=#{split.tags}, toolGroup='organize')}">
</div>
<div
th:replace="~{fragments/card :: card(id='rotate-pdf', cardTitle=#{home.rotate.title}, cardText=#{home.rotate.desc}, cardLink='rotate-pdf', toolIcon='rotate_right', tags=#{rotate.tags}, toolGroup='organize')}">
</div>
<div
th:replace="~{fragments/card :: card(id='crop', cardTitle=#{home.crop.title}, cardText=#{home.crop.desc}, cardLink='crop', toolIcon='crop', tags=#{crop.tags}, toolGroup='organize')}">
</div>
<div
th:replace="~{fragments/card :: card(id='pdf-organizer', cardTitle=#{home.pdfOrganiser.title}, cardText=#{home.pdfOrganiser.desc}, cardLink='pdf-organizer', toolIcon='format_list_bulleted', tags=#{pdfOrganiser.tags}, toolGroup='organize')}">
</div>
<div
th:replace="~{fragments/card :: card(id='remove-pages', cardTitle=#{home.removePages.title}, cardText=#{home.removePages.desc}, cardLink='remove-pages', toolIcon='delete', tags=#{removePages.tags}, toolGroup='organize')}">
</div>
<div
th:replace="~{fragments/card :: card(id='multi-page-layout', cardTitle=#{home.pageLayout.title}, cardText=#{home.pageLayout.desc}, cardLink='multi-page-layout', toolIcon='dashboard', tags=#{pageLayout.tags}, toolGroup='organize')}">
</div>
<div
th:replace="~{fragments/card :: card(id='scale-pages', cardTitle=#{home.scalePages.title}, cardText=#{home.scalePages.desc}, cardLink='scale-pages', toolIcon='fullscreen', tags=#{scalePages.tags}, toolGroup='organize')}">
</div>
<div
th:replace="~{fragments/card :: card(id='extract-page', cardTitle=#{home.extractPage.title}, cardText=#{home.extractPage.desc}, cardLink='extract-page', toolIcon='upload', tags=#{extractPage.tags}, toolGroup='organize')}">
</div>
<div
th:replace="~{fragments/card :: card(id='pdf-to-single-page', cardTitle=#{home.PdfToSinglePage.title}, cardText=#{home.PdfToSinglePage.desc}, cardLink='pdf-to-single-page', toolIcon='looks_one', tags=#{PdfToSinglePage.tags}, toolGroup='organize')}">
</div>
</div>
</div>
<div id="groupConvertTo" class="feature-group-legacy">
<div
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.convertTo})}">
</div>
<div class="feature-group-container">
<div
th:replace="~{fragments/card :: card(id='img-to-pdf', cardTitle=#{home.imageToPdf.title}, cardText=#{home.imageToPdf.desc}, cardLink='img-to-pdf', toolIcon='picture_as_pdf', tags=#{imageToPdf.tags}, toolGroup='image')}">
</div>
<div
th:replace="~{fragments/card :: card(id='file-to-pdf', cardTitle=#{home.fileToPDF.title}, cardText=#{home.fileToPDF.desc}, cardLink='file-to-pdf', toolIcon='draft', tags=#{fileToPDF.tags}, toolGroup='convert')}">
</div>
<div
th:replace="~{fragments/card :: card(id='url-to-pdf', cardTitle=#{home.URLToPDF.title}, cardText=#{home.URLToPDF.desc}, cardLink='url-to-pdf', toolIcon='link', tags=#{URLToPDF.tags}, toolGroup='convert')}">
</div>
<div
th:replace="~{fragments/card :: card(id='html-to-pdf', cardTitle=#{home.HTMLToPDF.title}, cardText=#{home.HTMLToPDF.desc}, cardLink='html-to-pdf', toolIcon='html', tags=#{HTMLToPDF.tags}, toolGroup='convert')}">
</div>
<div
th:replace="~{fragments/card :: card(id='markdown-to-pdf', cardTitle=#{home.MarkdownToPDF.title}, cardText=#{home.MarkdownToPDF.desc}, cardLink='markdown-to-pdf', toolIcon='markdown', tags=#{MarkdownToPDF.tags}, toolGroup='convert')}">
</div>
</div>
</div>
<div id="groupConvertFrom" class="feature-group-legacy">
<div
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.convertFrom})}">
</div>
<div class="feature-group-container">
<div
th:replace="~{fragments/card :: card(id='pdf-to-img', cardTitle=#{home.pdfToImage.title}, cardText=#{home.pdfToImage.desc}, cardLink='pdf-to-img', toolIcon='photo_library', tags=#{pdfToImage.tags}, toolGroup='image')}">
</div>
<div
th:replace="~{fragments/card :: card(id='pdf-to-pdfa', cardTitle=#{home.pdfToPDFA.title}, cardText=#{home.pdfToPDFA.desc}, cardLink='pdf-to-pdfa', toolIcon='picture_as_pdf', tags=#{pdfToPDFA.tags}, toolGroup='convert')}">
</div>
<div
th:replace="~{fragments/card :: card(id='pdf-to-word', cardTitle=#{home.PDFToWord.title}, cardText=#{home.PDFToWord.desc}, cardLink='pdf-to-word', toolIcon='description', tags=#{PDFToWord.tags}, toolGroup='word')}">
</div>
<div
th:replace="~{fragments/card :: card(id='pdf-to-presentation', cardTitle=#{home.PDFToPresentation.title}, cardText=#{home.PDFToPresentation.desc}, cardLink='pdf-to-presentation', toolIcon='slideshow', tags=#{PDFToPresentation.tags}, toolGroup='ppt')}">
</div>
<div
th:replace="~{fragments/card :: card(id='pdf-to-text', cardTitle=#{home.PDFToText.title}, cardText=#{home.PDFToText.desc}, cardLink='pdf-to-text', toolIcon='text_fields', tags=#{PDFToText.tags}, toolGroup='convert')}">
</div>
<div
th:replace="~{fragments/card :: card(id='pdf-to-html', cardTitle=#{home.PDFToHTML.title}, cardText=#{home.PDFToHTML.desc}, cardLink='pdf-to-html', toolIcon='html', tags=#{PDFToHTML.tags}, toolGroup='convert')}">
</div>
<div
th:replace="~{fragments/card :: card(id='pdf-to-xml', cardTitle=#{home.PDFToXML.title}, cardText=#{home.PDFToXML.desc}, cardLink='pdf-to-xml', toolIcon='code', tags=#{PDFToXML.tags}, toolGroup='convert')}">
</div>
<div
th:replace="~{fragments/card :: card(id='pdf-to-csv', cardTitle=#{home.tableExtraxt.title}, cardText=#{home.tableExtraxt.desc}, cardLink='pdf-to-csv', toolIcon='csv', tags=#{tableExtraxt.tags}, toolGroup='convert')}">
</div>
</div>
</div>
<div id="groupSecurity" class="feature-group-legacy">
<div
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.security})}">
</div>
<div class="feature-group-container">
<div
th:replace="~{fragments/card :: card(id='add-password', cardTitle=#{home.addPassword.title}, cardText=#{home.addPassword.desc}, cardLink='add-password', toolIcon='lock', tags=#{addPassword.tags}, toolGroup='security')}">
</div>
<div
th:replace="~{fragments/card :: card(id='remove-password', cardTitle=#{home.removePassword.title}, cardText=#{home.removePassword.desc}, cardLink='remove-password', toolIcon='lock_open_right', tags=#{removePassword.tags}, toolGroup='security')}">
</div>
<div
th:replace="~{fragments/card :: card(id='change-permissions', cardTitle=#{home.permissions.title}, cardText=#{home.permissions.desc}, cardLink='change-permissions', toolIcon='encrypted', tags=#{permissions.tags}, toolGroup='security')}">
</div>
<div
th:replace="~{fragments/card :: card(id='sign', cardTitle=#{home.sign.title}, cardText=#{home.sign.desc}, cardLink='sign', toolIcon='signature', tags=#{sign.tags}, toolGroup='sign')}">
</div>
<div
th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', toolIcon='workspace_premium', tags=#{certSign.tags}, toolGroup='security')}">
</div>
<div
th:replace="~{fragments/card :: card(id='validate-signature', cardTitle=#{home.validateSignature.title}, cardText=#{home.validateSignature.desc}, cardLink='validate-signature', toolIcon='verified', tags=#{validateSignature.tags}, toolGroup='security')}">
</div>
<div
th:replace="~{fragments/card :: card(id='remove-cert-sign', cardTitle=#{home.removeCertSign.title}, cardText=#{home.removeCertSign.desc}, cardLink='remove-cert-sign', toolIcon='remove_moderator', tags=#{removeCertSign.tags}, toolGroup='security')}">
</div>
<div
th:replace="~{fragments/card :: card(id='sanitize-pdf', cardTitle=#{home.sanitizePdf.title}, cardText=#{home.sanitizePdf.desc}, cardLink='sanitize-pdf', toolIcon='sanitizer', tags=#{sanitizePdf.tags}, toolGroup='security')}">
</div>
<div
th:replace="~{fragments/card :: card(id='auto-redact', cardTitle=#{home.autoRedact.title}, cardText=#{home.autoRedact.desc}, cardLink='auto-redact', toolIcon='ink_eraser', tags=#{autoRedact.tags}, toolGroup='security')}">
</div>
<div
th:replace="~{fragments/card :: card(id='redact', cardTitle=#{home.redact.title}, cardText=#{home.redact.desc}, cardLink='redact', toolIcon='playlist_remove', tags=#{redact.tags}, toolGroup='security')}">
</div>
<div
th:replace="~{fragments/card :: card(id='stamp', cardTitle=#{home.AddStampRequest.title}, cardText=#{home.AddStampRequest.desc}, cardLink='stamp', toolIcon='approval', tags=#{AddStampRequest.tags}, toolGroup='security')}">
</div>
<div
th:replace="~{fragments/card :: card(id='add-watermark', cardTitle=#{home.watermark.title}, cardText=#{home.watermark.desc}, cardLink='add-watermark', toolIcon='water_drop', tags=#{watermark.tags}, toolGroup='security')}">
</div>
</div>
</div>
<div id="groupView" class="feature-group-legacy">
<div
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.edit})}">
</div>
<div class="feature-group-container">
<div
th:replace="~{fragments/card :: card(id='view-pdf', cardTitle=#{home.viewPdf.title}, cardText=#{home.viewPdf.desc}, cardLink='view-pdf', toolIcon='menu_book', tags=#{viewPdf.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='add-page-numbers', cardTitle=#{home.add-page-numbers.title}, cardText=#{home.add-page-numbers.desc}, cardLink='add-page-numbers', toolIcon='123', tags=#{add-page-numbers.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='add-image', cardTitle=#{home.addImage.title}, cardText=#{home.addImage.desc}, cardLink='add-image', toolIcon='add_photo_alternate', tags=#{addImage.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='change-metadata', cardTitle=#{home.changeMetadata.title}, cardText=#{home.changeMetadata.desc}, cardLink='change-metadata', toolIcon='assignment', tags=#{changeMetadata.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='ocr-pdf', cardTitle=#{home.ocr.title}, cardText=#{home.ocr.desc}, cardLink='ocr-pdf', toolIcon='quick_reference_all', tags=#{ocr.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='extract-images', cardTitle=#{home.extractImages.title}, cardText=#{home.extractImages.desc}, cardLink='extract-images', toolIcon='wallpaper', tags=#{extractImages.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='flatten', cardTitle=#{home.flatten.title}, cardText=#{home.flatten.desc}, cardLink='flatten', toolIcon='layers_clear', tags=#{flatten.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='remove-blanks', cardTitle=#{home.removeBlanks.title}, cardText=#{home.removeBlanks.desc}, cardLink='remove-blanks', toolIcon='scan_delete', tags=#{removeBlanks.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='remove-annotations', cardTitle=#{home.removeAnnotations.title}, cardText=#{home.removeAnnotations.desc}, cardLink='remove-annotations', toolIcon='thread_unread', tags=#{removeAnnotations.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='compare', cardTitle=#{home.compare.title}, cardText=#{home.compare.desc}, cardLink='compare', toolIcon='compare', tags=#{compare.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='get-info-on-pdf', cardTitle=#{home.getPdfInfo.title}, cardText=#{home.getPdfInfo.desc}, cardLink='get-info-on-pdf', toolIcon='info', tags=#{getPdfInfo.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='remove-image-pdf', cardTitle=#{home.removeImagePdf.title}, cardText=#{home.removeImagePdf.desc}, cardLink='remove-image-pdf', toolIcon='remove_selection', tags=#{removeImagePdf.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='replace-and-invert-color-pdf', cardTitle=#{home.replaceColorPdf.title}, cardText=#{home.replaceColorPdf.desc}, cardLink='replace-and-invert-color-pdf', toolIcon='format_color_fill', tags=#{replaceColorPdf.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='unlock-pdf-forms', cardTitle=#{home.unlockPDFForms.title}, cardText=#{home.unlockPDFForms.desc}, cardLink='unlock-pdf-forms', toolIcon='preview_off', tags=#{unlockPDFForms.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('add-attachments', 'attachment', 'home.attachments.title', 'home.attachments.desc', 'attachments.tags', 'other')}">
</div>
</div>
</div>
<div id="groupAdvanced" class="feature-group-legacy">
<div
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.advance})}">
</div>
<div class="feature-group-container">
<div
th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', toolIcon='family_history', tags=#{pipeline.tags}, toolGroup='advance')}">
</div>
<div
th:replace="~{fragments/card :: card(id='adjust-contrast', cardTitle=#{home.adjust-contrast.title}, cardText=#{home.adjust-contrast.desc}, cardLink='adjust-contrast', toolIcon='palette', tags=#{adjust-contrast.tags}, toolGroup='advance')}">
</div>
<div
th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', toolIcon='zoom_in_map', tags=#{compressPdfs.tags}, toolGroup='advance')}">
</div>
<div
th:replace="~{fragments/card :: card(id='extract-image-scans', cardTitle=#{home.ScannerImageSplit.title}, cardText=#{home.ScannerImageSplit.desc}, cardLink='extract-image-scans', toolIcon='scanner', tags=#{ScannerImageSplit.tags}, toolGroup='advance')}">
</div>
<div
th:replace="~{fragments/card :: card(id='repair', cardTitle=#{home.repair.title}, cardText=#{home.repair.desc}, cardLink='repair', toolIcon='build', tags=#{repair.tags}, toolGroup='advance')}">
</div>
<div
th:replace="~{fragments/card :: card(id='auto-rename', cardTitle=#{home.auto-rename.title}, cardText=#{home.auto-rename.desc}, cardLink='auto-rename', toolIcon='text_fields_alt', tags=#{auto-rename.tags}, toolGroup='advance')}">
</div>
<div
th:replace="~{fragments/card :: card(id='auto-split-pdf', cardTitle=#{home.autoSplitPDF.title}, cardText=#{home.autoSplitPDF.desc}, cardLink='auto-split-pdf', toolIcon='cut', tags=#{autoSplitPDF.tags}, toolGroup='advance')}">
</div>
<div
th:replace="~{fragments/card :: card(id='show-javascript', cardTitle=#{home.showJS.title}, cardText=#{home.showJS.desc}, cardLink='show-javascript', toolIcon='javascript', tags=#{showJS.tags}, toolGroup='advance')}">
</div>
<div
th:replace="~{fragments/card :: card(id='split-by-size-or-count', cardTitle=#{home.autoSizeSplitPDF.title}, cardText=#{home.autoSizeSplitPDF.desc}, cardLink='split-by-size-or-count', toolIcon='vertical_split', tags=#{autoSizeSplitPDF.tags}, toolGroup='advance')}">
</div>
<div
th:replace="~{fragments/card :: card(id='overlay-pdf', cardTitle=#{home.overlay-pdfs.title}, cardText=#{home.overlay-pdfs.desc}, cardLink='overlay-pdf', toolIcon='layers', tags=#{overlay-pdfs.tags}, toolGroup='advance')}">
</div>
<div
th:replace="~{fragments/card :: card(id='split-pdf-by-sections', cardTitle=#{home.split-by-sections.title}, cardText=#{home.split-by-sections.desc}, cardLink='split-pdf-by-sections', toolIcon='grid_on', tags=#{split-by-sections.tags}, toolGroup='advance')}">
</div>
<div
th:replace="~{fragments/card :: card(id='split-pdf-by-chapters', cardTitle=#{home.splitPdfByChapters.title}, cardText=#{home.splitPdfByChapters.desc}, cardLink='split-pdf-by-chapters', toolIcon='book', tags=#{splitPdfByChapters.tags}, toolGroup='advance')}">
</div>
</div>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<!-- Survey Modal -->
<div class="modal fade" id="surveyModal" tabindex="-1" role="dialog" aria-labelledby="surveyModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="surveyModalLabel" th:text="#{survey.title}">Stirling-PDF Survey</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p th:text="#{survey.meeting.1}">If you're using Stirling PDF at work, we'd love to speak to you. We're offering free technical support in exchange for a 15 minute user discovery session.</p>
<p th:text="#{survey.meeting.2}">This is a chance to:</p>
<p><span>🛠️</span><span th:text="#{survey.meeting.3}">Get help with deployment, integrations, or troubleshooting</span></p>
<p><span>📢</span><span th:text="#{survey.meeting.4}">Provide direct feedback on performance, edge cases, and feature gaps</span></p>
<p><span>🔍</span><span th:text="#{survey.meeting.5}">Help us refine Stirling PDF for real-world enterprise use</span></p>
<p th:text="#{survey.meeting.6}">If you're interested, you can book time with our team directly.</p>
<p th:text="#{survey.meeting.7}">Looking forward to digging into your use cases and making Stirling PDF even better!</p>
<a href="https://calendly.com/d/cm4p-zz5-yy8/stirling-pdf-15-minute-group-discussion" target="_blank" class="btn btn-primary" id="takeSurvey2" th:text="#{survey.meeting.button}">Book meeting</a>
</br>
</br>
<p th:text="#{survey.meeting.notInterested}">Not a business and/or interested in a meeting?</p>
<p th:text="#{survey.please}">Please consider taking our survey!</p>
<a href="https://stirlingpdf.info/s/cm28y3niq000o56dv7liv8wsu" target="_blank" class="btn btn-primary"
id="takeSurvey" th:text="#{survey.button}">Take Survey</a>
</div>
<div class="modal-footer">
<div class="form-check mb-3">
<input type="checkbox" id="dontShowAgain">
<label for="dontShowAgain" th:text="#{survey.dontShowAgain}">Don't show again</label>
</div>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}">Close</button>
</div>
</div>
</div>
</div>
<!-- Analytics Modal -->
<div class="modal fade" id="analyticsModal" tabindex="-1" role="dialog" aria-labelledby="analyticsModalLabel"
aria-hidden="true" th:if="${@analyticsPrompt}">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="analyticsModalLabel" th:text="#{analytics.title}">Do you want make Stirling PDF
better?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p th:text="#{analytics.paragraph1}">Stirling PDF has opt in analytics to help us improve the product. We do
not track any personal information or file contents.</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>
</div>
<div class="modal-footer justify-content-between">
<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>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<script th:inline="javascript">
/*<![CDATA[*/
const analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;
document.addEventListener('DOMContentLoaded', function () {
if (analyticsPromptBoolean) {
const analyticsModal = new bootstrap.Modal(document.getElementById('analyticsModal'));
analyticsModal.show();
}
});
/*]]>*/
function setAnalytics(enabled) {
fetchWithCsrf('api/v1/settings/update-enable-analytics', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(enabled)
})
.then(response => {
if (response.status === 200) {
console.log('Analytics setting updated successfully');
bootstrap.Modal.getInstance(document.getElementById('analyticsModal')).hide();
} else if (response.status === 208) {
console.log('Analytics setting has already been set. Please edit /config/settings.yml to change it.', response);
alert('Analytics setting has already been set. Please edit /config/settings.yml to change it.');
} else {
throw new Error('Unexpected response status: ' + response.status);
}
})
.catch(error => {
console.error('Error updating analytics setting:', error);
alert('An error occurred while updating the analytics setting. Please try again.');
});
}
document.addEventListener("DOMContentLoaded", function () {
const surveyVersion = "3.0";
const modal = new bootstrap.Modal(document.getElementById('surveyModal'));
const dontShowAgain = document.getElementById('dontShowAgain');
const takeSurveyButton = document.getElementById('takeSurvey');
const viewThresholds = [5, 10, 15, 22, 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');
localStorage.setItem('surveyVersion', surveyVersion);
}
let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
pageViews++;
localStorage.setItem('pageViews', pageViews.toString());
function shouldShowSurvey() {
if (localStorage.getItem('dontShowSurvey') === 'true' ||
localStorage.getItem('surveyTaken') === 'true') {
return false;
}
// If survey version changed and we hit a threshold, show the survey
if (localStorage.getItem('surveyVersion') !== surveyVersion &&
viewThresholds.includes(pageViews)) {
return true;
}
return viewThresholds.includes(pageViews);
}
if (shouldShowSurvey()) {
modal.show();
}
dontShowAgain.addEventListener('change', function () {
if (this.checked) {
localStorage.setItem('dontShowSurvey', 'true');
localStorage.setItem('surveyVersion', surveyVersion);
} else {
localStorage.removeItem('dontShowSurvey');
localStorage.removeItem('surveyVersion');
}
});
takeSurveyButton.addEventListener('click', function () {
localStorage.setItem('surveyTaken', 'true');
localStorage.setItem('surveyVersion', surveyVersion);
modal.hide();
});
if (localStorage.getItem('dontShowSurvey')) {
modal.hide();
}
});
function setAsDefault(value) {
localStorage.setItem('defaultView', value);
console.log(`Default view set to: ${value}`);
}
</script>
</body>
</html>
@@ -0,0 +1,294 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title='')}"></th:block>
</head>
<body>
<div class="page-container" style="max-height:100vh;">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div style="transform-origin: center top; flex:0 1 auto; display:flex; flex-direction:column; align-items:center; justify-content:flex-start;" id="scale-wrap">
<br class="d-md-none">
<!-- Features -->
<script th:src="@{'/js/homecard.js'}"></script>
<div style="
width: 100%;
display: flex;
flex-direction: column;"
>
<div>
<br>
<div style="justify-content: center; display: flex;">
<div style="margin:0 3rem">
<div>
<div
style="display:flex; flex-direction: column; justify-content: center; width:100%; margin-bottom:1rem">
<div style="width:fit-content; margin: 0 auto; padding: 0 3rem">
<div class="lead fs-4"
th:text="${@homeText != 'null' and @homeText != null and @homeText != ''} ? ${@homeText} : #{home.desc}">
</div>
</div>
<div id="groupRecent" style="width: fit-content; margin: 0 auto">
<div
th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.recent})}">
</div>
<div class="recent-features">
<div class="newfeature"
th:insert="~{fragments/navbarEntry :: navbarEntry('add-attachments', 'attachment', 'home.attachments.title', 'home.attachments.desc', 'attachments.tags', 'other')}">
</div>
<div class="newfeature"
th:insert="~{fragments/navbarEntry :: navbarEntry('fake-scan', 'scanner', 'fakeScan.title', 'fakeScan.description', 'fakeScan.tags', 'advance')}">
</div>
<div class="newfeature"
th:insert="~{fragments/navbarEntry :: navbarEntry('compress-pdf', 'zoom_in_map', 'home.compressPdfs.title', 'home.compressPdfs.desc', 'compressPDFs.tags', 'advance')}">
</div>
<div class="newfeature"
th:insert="~{fragments/navbarEntry :: navbarEntry('edit-table-of-contents', 'bookmark_add', 'home.editTableOfContents.title', 'home.editTableOfContents.desc', 'editTableOfContents.tags', 'advance')}">
</div>
</div>
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<input type="text" id="searchBar" onkeyup="filterCards()" th:placeholder="#{home.searchBar}" autofocus>
</div>
<div style="display: flex; column-gap: 3rem; flex-wrap: wrap; margin-left:1rem">
<div
style="height:2.5rem; display: flex; align-items: center; cursor: pointer; justify-content: center;">
<label for="sort-options" th:text="#{home.sortBy}">Sort by:</label>
<select id="sort-options" style="border:none;">
<option value="alphabetical" th:text="#{home.alphabetical}"> </option>
<!-- <option value="personal">Your most used</option> -->
<option value="global" th:text="#{home.globalPopularity}"></option>
<!-- <option value="server">Popularity in organisation</option> -->
</select>
</div>
<div
style="display: flex; align-items: center; flex-wrap: wrap; align-content: flex-start; width: fit-content; max-width: 100%; gap:2rem; justify-content: center;" >
<div th:title="#{home.setFavorites}" style="display: flex; align-items: center; cursor: pointer;"
onclick="toggleFavoritesMode()">
<span class="material-symbols-rounded toggle-favourites"
style="font-size: 2rem; margin-left: 0.2rem;">
star
</span>
</div>
<div onclick="toggleFavoritesView()" th:title="#{home.hideFavorites}" id="favouritesVisibility"
style="display: flex; align-items: center; cursor: pointer;">
<span id="toggle-favourites-icon" class="material-symbols-rounded toggle-favourites"
style="font-size: 2rem; margin-left: 0.2rem;">
visibility
</span>
</div>
<a th:if="${@shouldShow}" href="https://github.com/Stirling-Tools/Stirling-PDF/releases"
target="_blank" id="update-link" rel="noopener" th:title="#{settings.update}"
style="text-decoration: none; color: inherit; cursor: pointer; display: flex; align-items: center;">
<span class="material-symbols-rounded toggle-favourites"
style="font-size: 2rem; margin-left: 0.2rem;">
update
</span>
</a>
</div>
</div>
</div>
</div>
</div>
<div>
</div>
<div style="width:100%; overflow-x:visible;">
<div class="features-container" style=" border-top: 1px;
border-top-style: solid;
border-color: var(--md-nav-color-on-separator);
margin-top: 1rem;
">
<div class="feature-rows">
<div id="groupFavorites" class="feature-group">
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.favorite})}">
</div>
<div class="nav-group-container">
</div>
</div>
<th:block th:insert="~{fragments/navElements.html :: navElements}"></th:block>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</div>
<!-- Survey Modal -->
<div class="modal fade" id="surveyModal" tabindex="-1" role="dialog" aria-labelledby="surveyModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="surveyModalLabel" th:text="#{survey.title}">Stirling-PDF Survey</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p th:text="#{survey.meeting.1}">If you're using Stirling PDF at work, we'd love to speak to you. We're offering free technical support in exchange for a 15 minute user discovery session.</p>
<p th:text="#{survey.meeting.2}">This is a chance to:</p>
<p><span>🛠️</span><span th:text="#{survey.meeting.3}">Get help with deployment, integrations, or troubleshooting</span></p>
<p><span>📢</span><span th:text="#{survey.meeting.4}">Provide direct feedback on performance, edge cases, and feature gaps</span></p>
<p><span>🔍</span><span th:text="#{survey.meeting.5}">Help us refine Stirling PDF for real-world enterprise use</span></p>
<p th:text="#{survey.meeting.6}">If you're interested, you can book time with our team directly.</p>
<p th:text="#{survey.meeting.7}">Looking forward to digging into your use cases and making Stirling PDF even better!</p>
<a href="https://calendly.com/d/crsr-tz6-487" target="_blank" class="btn btn-primary" id="takeSurvey2" th:text="#{survey.meeting.button}">Book meeting</a>
</br>
</br>
<p th:text="#{survey.meeting.notInterested}">Not a business and/or interested in a meeting?</p>
<p th:text="#{survey.please}">Please consider taking our survey!</p>
<a href="https://stirlingpdf.info/s/cm28y3niq000o56dv7liv8wsu" target="_blank" class="btn btn-primary"
id="takeSurvey" th:text="#{survey.button}">Take Survey</a>
</div>
<div class="modal-footer">
<div class="form-check mb-3">
<input type="checkbox" id="dontShowAgain">
<label for="dontShowAgain" th:text="#{survey.dontShowAgain}">Don't show again</label>
</div>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}">Close</button>
</div>
</div>
</div>
</div>
<!-- Analytics Modal -->
<div class="modal fade" id="analyticsModal" tabindex="-1" role="dialog" aria-labelledby="analyticsModalLabel"
aria-hidden="true" th:if="${@analyticsPrompt}">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="analyticsModalLabel" th:text="#{analytics.title}">Do you want make Stirling PDF
better?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p th:text="#{analytics.paragraph1}">Stirling PDF has opt in analytics to help us improve the product. We do
not track any personal information or file contents.</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>
</div>
<div class="modal-footer justify-content-between">
<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>
<style>
.favorite-icon {
cursor: pointer;
width: 0rem;
font-size: 2rem;
}
.toggle-favourites {
cursor: pointer;
}
.toggle-favourites.active {
color: gold;
}
</style>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<script th:inline="javascript">
/*<![CDATA[*/
window.analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;
/*]]>*/
window.showSurvey = /*[[${showSurveyFromDocker}]]*/ true
</script>
<script th:src="@{'/js/pages/home.js'}" th:inline="javascript"></script>
<script>
function scaleStuff() {
const w = 1440;
const h = 1000;
const sx = window.innerWidth / w;
const sy = window.innerHeight / h;
const s = Math.max(0.9, Math.min(sx, sy));
const el = document.getElementById('scale-wrap');
const container = document.querySelector('.page-container');
const nav = document.querySelector('.navbar, nav');
const navH = nav ? nav.offsetHeight : 0;
const space = container.offsetHeight - navH;
const origH = el.scrollHeight;
// Progressive bonus based on how tall the screen is
const aspectRatio = window.innerWidth / window.innerHeight;
const tallScreenBonus = Math.max(0, (1.8 - aspectRatio) * 0.15); // More bonus for taller screens
let finalS = Math.min(s * 0.75, (space * (0.98 + tallScreenBonus)) / origH);
finalS = Math.max(0.7, Math.min(1.0, finalS)); // Never scale above 100%
el.style.transform = `scale(${finalS})`;
el.style.transformOrigin = 'top center';
el.style.height = `${origH * finalS}px`;
// Dynamically adjust features container width based on scale
const featuresContainer = document.querySelector('.features-container');
if (featuresContainer) {
const isRTL = document.documentElement.dir === 'rtl' || document.documentElement.getAttribute('dir') === 'rtl';
const dynamicWidth = Math.min(120, 100 / finalS);
const offset = (dynamicWidth - 100) / 2;
featuresContainer.style.width = `${dynamicWidth}%`;
if (isRTL) {
featuresContainer.style.right = `-${offset}%`;
featuresContainer.style.left = 'auto';
} else {
featuresContainer.style.left = `-${offset}%`;
featuresContainer.style.right = 'auto';
}
}
}
let prevW = window.innerWidth;
let prevH = window.innerHeight;
let prevDPR = window.devicePixelRatio;
function onResize() {
const w = window.innerWidth;
const h = window.innerHeight;
const dpr = window.devicePixelRatio;
if (w !== prevW || h !== prevH) {
if (dpr === prevDPR) {
scaleStuff();
}
}
prevW = w;
prevH = h;
prevDPR = dpr;
}
window.addEventListener('load', () => {
setTimeout(scaleStuff, 100);
});
window.addEventListener('resize', onResize);
</script>
</body>
</html>
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{licenses.title}, header=#{licenses.title})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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-8 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon organize">license</span>
<span class="tool-header-text" th:text="#{licenses.header}">3rd Party licenses</span>
</div>
<table class="table table-striped">
<thead>
<tr>
<th scope="col" th:text="#{licenses.module}">Module</th>
<th scope="col" th:text="#{licenses.version}">Version</th>
<th scope="col" th:text="#{licenses.license}">License</th>
</tr>
</thead>
<tbody>
<tr th:each="dep : ${dependencies}">
<td><a th:href="${dep.moduleUrl}" th:text="${dep.moduleName}"></a></td>
<td th:text="${dep.moduleVersion}"></td>
<td><a th:href="${dep.moduleLicenseUrl}" th:text="${dep.moduleLicense}"></a></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,178 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{login.title}, header=#{login.header})}"></th:block>
<link rel="stylesheet" th:href="@{'/css/login.css'}">
<script th:src="@{'/js/languageSelection.js'}"></script>
<script th:src="@{'/js/additionalLanguageCode.js'}"></script>
</head>
<body>
<div class="your-container-class"></div>
<div class="container-flex">
<main class="form-signin">
<script th:inline="javascript">
const redirectAttempts = parseInt(localStorage.getItem('ssoRedirectAttempts') || '0');
const urlParams = new URLSearchParams(window.location.search);
const hasRedirectError = urlParams.has('error');
const hasLogout = urlParams.has('logout');
const hasMessage = urlParams.has('message');
const MAX_REDIRECT_ATTEMPTS = 3;
document.addEventListener('modeChanged', function(e) {
var mode = e.detail;
document.body.classList.remove("light-mode", "dark-mode", "rainbow-mode"); // remove all mode classes first
switch (mode) {
case "on":
document.body.classList.add("dark-mode");
break;
case "off":
document.body.classList.add("light-mode");
break;
case "rainbow":
document.body.classList.add("rainbow-mode");
break;
}
});
document.addEventListener('DOMContentLoaded', function() {
const runningProOrHigher = /*[[${@runningProOrHigher}]]*/ false;
const SSOAutoLogin = /*[[${@SSOAutoLogin}]]*/ false;
const loginMethod = /*[[${loginMethod}]]*/ 'normal';
const providerList = /*[[${providerList}]]*/ {};
const shouldAutoRedirect = !hasRedirectError &&
!hasLogout &&
!hasMessage &&
redirectAttempts < MAX_REDIRECT_ATTEMPTS &&
loginMethod !== 'normal' && runningProOrHigher && SSOAutoLogin;
console.log('Should redirect:', shouldAutoRedirect, {
'No error': !hasRedirectError,
'No logout': !hasLogout,
'No message': !hasMessage,
'Under max attempts': redirectAttempts < MAX_REDIRECT_ATTEMPTS,
'Is only OAuth2': loginMethod === 'oauth2'
});
if (shouldAutoRedirect && providerList && Object.keys(providerList).length > 0) {
localStorage.setItem('ssoRedirectAttempts', redirectAttempts + 1);
const firstProvider = Object.keys(providerList)[0];
window.location.href = firstProvider;
}
// Reset redirect attempts if successful login or after 1 hour
const lastAttemptTime = parseInt(localStorage.getItem('lastRedirectAttempt') || '0');
if (Date.now() - lastAttemptTime > 3600000) { // 1 hour
localStorage.setItem('ssoRedirectAttempts', '0');
}
localStorage.setItem('lastRedirectAttempt', Date.now().toString());
const defaultLocale = getStoredOrDefaultLocale();
checkUserLanguage(defaultLocale);
const dropdownItems = document.querySelectorAll('.lang_dropdown-item');
let activeItem;
for (let i = 0; i < dropdownItems.length; i++) {
const item = dropdownItems[i];
item.classList.remove('active');
if (item.dataset.bsLanguageCode === defaultLocale) {
item.classList.add('active');
activeItem = item;
}
item.addEventListener('click', handleDropdownItemClick);
}
const dropdown = document.getElementById('languageDropdown');
if (activeItem) {
dropdown.innerHTML = activeItem.innerHTML; // This will set the dropdown button's content to the active language's flag and name
}
});
</script>
<div class="text-center">
<img class="my-4" th:src="@{'/favicon.svg'}" alt="favicon" width="144" height="144">
<h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1>
<div th:if="${altLogin} and (${loginMethod} == 'all' or ${loginMethod} == 'oauth2' or ${loginMethod} == 'saml2')">
<a href="#" class="w-100 btn btn-lg btn-primary" data-bs-toggle="modal" data-bs-target="#loginsModal" th:text="#{login.ssoSignIn}">Login Via SSO</a>
<br>
<br>
<hr />
</div>
<div th:if="${errorOAuth}" class="alert alert-danger text-center">
<div th:if="${errorOAuth}" th:text="#{${errorOAuth}}">OAuth2: Error Message</div>
</div>
<div th:if="${error}" class="alert alert-danger text-center">
<div th:if="${error}" th:text="#{${error}}">OAuth2: Error Message</div>
</div>
<div class="text-danger text-center">
<div th:if="${logoutMessage}" class="alert alert-success" th:text="#{${logoutMessage}}"></div>
<div th:if="${messageType}" class="alert alert-success">
<span th:text="#{${messageType}}">Default message if not found</span>
</div>
</div>
</div>
<form th:if="${loginMethod} == 'all' or ${loginMethod} == 'normal'" th:action="@{'/login'}" method="post">
<h2 class="h5 mb-3 fw-normal" th:text="#{login.signinTitle}">Please sign in</h2>
<div class="form-floating">
<input type="text" class="form-control" id="username" name="username" placeholder="admin">
<label for="username" th:text="#{username}">Username</label>
</div>
<div class="form-floating">
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
<label for="password" th:text="#{password}">Password</label>
</div>
<div class="form-check m-2 mb-3">
<input type="checkbox" name="remember-me" id="remember-me">
<label for="remember-me" th:text="#{login.rememberme}"></label>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary" th:text="#{login.signin}">Sign in</button>
</form>
<div class="mt-3"> <!-- Added a margin-top for spacing -->
<div class="dropdown text-center">
<button class="btn btn-secondary dropdown-toggle" type="button" id="languageDropdown" data-bs-toggle="dropdown" aria-expanded="false">
English (GB)
<!-- Default language placeholder -->
</button>
<div class="dropdown-menu" aria-labelledby="languageDropdown">
<!-- Here's where the fragment will be included -->
<div id="languageSelection" class="scrollable-y" >
<th:block th:replace="~{fragments/languages :: langs}"></th:block>
</div>
</div>
</div>
</div>
</main>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<div th:if="${altLogin}" class="modal fade" id="loginsModal" tabindex="-1" role="dialog" aria-labelledby="loginsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="loginsModalLabel" th:text="#{login.ssoSignIn}"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">
close
</span>
</button>
</div>
<div class="modal-body">
<div class="mb-3" th:each="provider : ${providerList}">
<a th:href="@{|${provider.key}|}" th:text="${provider.value}" class="w-100 btn btn-lg btn-primary">Login Provider</a>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{merge.title}, header=#{merge.header})}"></th:block>
<link rel="stylesheet" th:href="@{'/css/merge.css'}">
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<th:block th:insert="~{fragments/multi-toolAdvert.html :: multi-toolAdvert}"></th:block>
<div class="container" id="dropContainer">
<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 organize">add_to_photos</span>
<span class="tool-header-text" th:text="#{merge.header}"></span>
</div>
<form th:action="@{'api/v1/general/merge-pdfs'}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label th:text="#{multiPdfDropPrompt}" for="fileInput-input"></label>
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=true, accept='application/pdf')}">
</div>
</div>
<div class="mb-3">
<input type="checkbox" name="removeCertSign" id="removeCertSign">
<label for="removeCertSign" th:text="#{merge.removeCertSign}">Remove digital signature in the merged
file?</label>
</div>
<div class="mb-3">
<input type="checkbox" name="generateToc" id="generateToc">
<label for="generateToc" th:text="#{merge.generateToc}">Generate table of contents in the merged file?</label>
</div>
<div class="mb-3">
<ul id="selectedFiles" class="list-group"></ul>
</div>
<div class="mb-3">
<button type="button" id="sortByNameBtn" class="btn btn-info" th:text="#{merge.sortByName}"></button>
<button type="button" id="sortByDateBtn" class="btn btn-info" th:text="#{merge.sortByDate}"></button>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{merge.submit}"></button>
</div>
<div class="mb-3">
<button type="button" id="resetFileInputBtn" class="btn btn-danger" th:text="#{reset}">Reset</button>
</div>
</form>
<span id="pageTranslation" th:text="#{page}" style="display:none;"></span>
<span id="pagesTranslation" th:text="#{pages}" style="display:none;"></span>
<script type="text/javascript">
const pageTranslation = document.getElementById('pageTranslation').innerText; // Get translation for a single page
const pagesTranslation = document.getElementById('pagesTranslation').innerText; // Get translation for multiple pages
</script>
<script type="module">
import * as pdfjsLib from './pdfjs-legacy/pdf.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
</script>
<script th:src="@{'/js/merge.js'}"></script>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{attachments.title}, header=#{attachments.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 other">attachment</span>
<span class="tool-header-text" th:text="#{attachments.header}"></span>
</div>
<form action="#" th:action="@{/api/v1/misc/add-attachments}" method="post" enctype="multipart/form-data">
<!-- PDF file selector -->
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<!-- Attachment files selector -->
<div th:replace="~{fragments/common :: fileSelector(name='attachments', disableMultipleFiles=false, multipleInputsForSingleRequest=true, accept='*/*', inputText=#{attachments.selectFiles})}">
</div>
<!-- Submit button -->
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{attachments.addButton}">Add Attachments</button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{addImage.title}, header=#{addImage.header})}"></th:block>
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
<link rel="stylesheet" th:href="@{'/css/add-image.css'}">
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 other">add_photo_alternate</span>
<span class="tool-header-text" th:text="#{addImage.header}"></span>
</div>
<!-- pdf selector -->
<div
th:replace="~{fragments/common :: fileSelector(name='pdf-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script type="module" th:src="@{'/js/pages/add-image.js'}"></script>
<div class="show-on-file-selected">
<div
th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=false, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
</div>
</div>
<div class="draggable-buttons-box ignore-rtl">
<button class="btn btn-outline-secondary" th:title="#{sign.delete}"
onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())"
style="color: #C02223; border-color: #C02223; background-color: rgba(255, 0, 0, 0.1);">
<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" th:title="#{sign.addToAll}"
onclick="DraggableUtils.addAllPagesDraggableCanvas(DraggableUtils.getLastInteracted())">
<span class="material-symbols-rounded">
content_copy
</span>
</button>
<div id="rotation-controls" class="align-items-center" style="display: none;">
<div class="input-with-icon">
<span class="material-symbols-rounded icon" style="margin-right: 3px;">
screen_rotation
</span>
<input type="number" id="rotation-input" class="form-control form-control-sm me-2" value="0" step="10"
style="width: 6rem" />
</div>
</div>
<button id="ratioToggleBtn" th:title="#{sign.maintainRatio}" class="btn btn-outline-secondary"
onclick="DraggableUtils.toggleMaintainRatio()">
<span class="material-symbols-rounded">
Aspect_Ratio
</span>
</button>
<button class="btn btn-outline-secondary" th:title="#{sign.first}" onclick="goToFirstOrLastPage(false)"
style="margin-left:auto">
<span class="material-symbols-rounded">
keyboard_double_arrow_left
</span>
</button>
<button class="btn btn-outline-secondary" th:title="#{sign.previous}" id="incrementPage"
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">
<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" th:title="#{sign.next}" id="decrementPage"
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>
<button class="btn btn-outline-secondary" th:title="#{sign.last}" onclick="goToFirstOrLastPage(true)">
<span class="material-symbols-rounded">
keyboard_double_arrow_right
</span>
</button>
<button id="download-pdf" th:title="#{downloadPdf}" class="btn btn-outline-secondary"
style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)">
<span class="material-symbols-rounded">
download
</span>
</button>
</div>
<!-- draggables box -->
<div id="box-drag-container" class="show-on-file-selected">
<canvas id="pdf-canvas"></canvas>
<script th:src="@{'/js/draggable-utils.js'}"></script>
</div>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,152 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{addPageNumbers.title}, header=#{addPageNumbers.header})}"></th:block>
<style>
.a4container {
position: relative;
width: 50%;
aspect-ratio: 0.707/1;
border: 1px solid #ddd;
box-sizing: border-box;
background-color: white;
}
.pageNumber {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
font-size: 1em;
color: #333;
cursor: pointer;
background-color: #ccc;
width: 15%;
height: 15%;
transform: translate(-50%, -50%);
}
.pageNumber:hover {
background-color: #eee;
}
#myForm {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.selectedPosition {
background-color: #0a0;
}
.selectedPosition.selectedHovered {
background-color: #006600;
}
</style>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 other">123</span>
<span class="tool-header-text" th:text="#{addPageNumbers.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/add-page-numbers'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<br>
<div class="mb-3">
<label for="customMargin" th:text="#{addPageNumbers.selectText.2}"></label>
<select class="form-control" id="customMargin" name="customMargin">
<option value="small" th:text="#{sizes.small}"></option>
<option value="medium" selected th:text="#{sizes.medium}"></option>
<option value="large" th:text="#{sizes.large}"></option>
<option value="x-large" th:text="#{sizes.x-large}"></option>
</select>
</div>
<div class="mb-3">
<label th:text="#{addPageNumbers.selectText.3}"></label>
<div class="a4container">
<div class="pageNumber" id="1" style="top: 10%; left: 10%;">1</div>
<div class="pageNumber" id="2" style="top: 10%; left: 50%;">2</div>
<div class="pageNumber" id="3" style="top: 10%; left: 90%;">3</div>
<div class="pageNumber" id="4" style="top: 50%; left: 10%;">4</div>
<div class="pageNumber" id="5" style="top: 50%; left: 50%;">5</div>
<div class="pageNumber" id="6" style="top: 50%; left: 90%;">6</div>
<div class="pageNumber" id="7" style="top: 90%; left: 10%;">7</div>
<div class="pageNumber selectedPosition" id="8" style="top: 90%; left: 50%;">8</div>
<div class="pageNumber" id="9" style="top: 90%; left: 90%;">9</div>
</div>
</div>
<input type="hidden" id="numberInput" name="position" value="8">
<div class="mb-3">
<label for="fontSize" th:text="#{addPageNumbers.fontSize}"></label>
<input type="number" class="form-control" id="fontSize" name="fontSize" min="1" value="12" required>
</div>
<div class="mb-3">
<label for="fontType" th:text="#{addPageNumbers.fontName}"></label>
<select class="form-control" id="fontType" name="fontType">
<option value="Times">Times Roman</option>
<option value="Helvetica">Helvetica</option>
<option value="Courier">Courier New</option>
</select>
</div>
<div class="mb-3">
<label for="startingNumber" th:text="#{addPageNumbers.selectText.4}"></label>
<input type="number" class="form-control" id="startingNumber" name="startingNumber" min="1" required value="1">
</div>
<div class="mb-3">
<label for="pagesToNumber" th:text="#{addPageNumbers.selectText.5}"></label>
<input type="text" class="form-control" id="pagesToNumber" name="pagesToNumber" th:placeholder="#{addPageNumbers.numberPagesDesc}">
</div>
<div class="mb-3">
<label for="customText" th:text="#{addPageNumbers.selectText.6}"></label>
<input type="text" class="form-control" id="customText" name="customText" th:placeholder="#{addPageNumbers.customNumberDesc}">
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{addPageNumbers.submit}"></button>
</form>
</div>
</div>
</div>
<script>
let cells = document.querySelectorAll('.pageNumber');
let inputField = document.getElementById('numberInput');
cells.forEach(cell => {
cell.addEventListener('click', function(e) {
cells.forEach(cell => {
cell.classList.remove('selectedPosition'); // Remove selected class from all cells
cell.classList.remove('selectedHovered'); // Also remove selectedHovered class
});
let selectedLocation = e.target.id;
inputField.value = selectedLocation;
e.target.classList.add('selectedPosition'); // Add selected class to clicked cell
e.target.classList.add('selectedHovered'); // Add selectedHovered class
});
cell.addEventListener('mouseenter', function(e) {
if(e.target.classList.contains('selectedPosition')) {
e.target.classList.add('selectedHovered');
}
});
cell.addEventListener('mouseleave', function(e) {
if(e.target.classList.contains('selectedPosition')) {
e.target.classList.remove('selectedHovered');
}
});
});
</script>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{adjustContrast.title}, header=#{adjustContrast.header})}"></th:block>
<style>
#flex-container {
display: flex;
align-items: center;
}
#sliders-container {
padding: 0 20px; /* Add some padding to separate sliders from canvas */
}
</style>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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-12 bg-card">
<form th:action="@{''}">
<div class="row justify-content-center">
<div class="col-md-3">
<div id="sliders-container" style="display:none;">
<h4><span th:text="#{adjustContrast.contrast}"></span> <span id="contrast-val">100</span>%</h4>
<input type="range" min="0" max="200" value="100" id="contrast-slider">
<h4><span th:text="#{adjustContrast.brightness}"></span> <span id="brightness-val">100</span>%</h4>
<input type="range" min="0" max="200" value="100" id="brightness-slider">
<h4><span th:text="#{adjustContrast.saturation}"></span> <span id="saturation-val">100</span>%</h4>
<input type="range" min="0" max="200" value="100" id="saturation-slider">
</div>
</div>
<div class="col-md-7">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon advance">palette</span>
<span class="tool-header-text" th:text="#{adjustContrast.header}"></span>
</div>
<div class="col-md-8">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf', remoteCall='false')}"></div>
</div>
<br>
<canvas id="contrast-pdf-canvas"></canvas>
<div class="mb-3 text-left">
<button id="download-button" class="btn btn-primary" th:text="#{adjustContrast.download}"></button>
</div>
</div>
</div>
</form>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script type="module" th:src="@{'/js/pages/adjust-contrast.js'}"></script>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{autoCrop.title}, header=#{autoCrop.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 advance">crop</span>
<span class="tool-header-text" th:text="#{autoCrop.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/auto-crop'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{autoCrop.submit}"></button>
</form>
<p class="mt-3" th:text="#{autoCrop.credit}"></p>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{auto-rename.title}, header=#{auto-rename.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 >
<svg class="tool-header-icon advance">
<use xlink:href="/images/rename.svg#icon-rename"></use>
</svg>
</span>
<span class="tool-header-text" th:text="#{auto-rename.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/auto-rename'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{auto-rename.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{changeMetadata.title}, header=#{changeMetadata.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 other">assignment</span>
<span class="tool-header-text" th:text="#{changeMetadata.header}"></span>
</div>
<form method="post" id="form1" enctype="multipart/form-data" th:action="@{'/api/v1/misc/update-metadata'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<p class="text-muted" th:text="#{changeMetadata.selectText.1}"></p>
<div class="form-check mb-3-inline ms-3">
<input type="checkbox" id="deleteAll" name="deleteAll">
<label for="deleteAll" th:text="#{changeMetadata.selectText.2}" ></label>
</div>
<div class="form-check mb-3-inline ms-3">
<input type="checkbox" id="customModeCheckbox">
<label for="customModeCheckbox" th:text="#{changeMetadata.selectText.3}"></label>
</div>
<div class="mb-3">
<label class="form-check-label" for="author" th:text="#{changeMetadata.author}"></label>
<input type="text" class="form-control" id="author" name="author">
</div>
<div class="mb-3">
<label class="form-check-label" for="creationDate" th:text="#{changeMetadata.creationDate}"></label>
<input type="text" class="form-control" id="creationDate" name="creationDate" placeholder="2020/12/25 18:30:59">
</div>
<div class="mb-3">
<label class="form-check-label" for="creator" th:text="#{changeMetadata.creator}"></label>
<input type="text" class="form-control" id="creator" name="creator">
</div>
<div class="mb-3">
<label class="form-check-label" for="keywords" th:text="#{changeMetadata.keywords}"></label>
<input type="text" class="form-control" id="keywords" name="keywords">
</div>
<div class="mb-3">
<label class="form-check-label" for="modificationDate" th:text="#{changeMetadata.modDate}"></label>
<input type="text" class="form-control" id="modificationDate" name="modificationDate" placeholder="2020/12/25 18:30:59">
</div>
<div class="mb-3">
<label class="form-check-label" for="producer" th:text="#{changeMetadata.producer}"></label>
<input type="text" class="form-control" id="producer" name="producer">
</div>
<div class="mb-3">
<label class="form-check-label" for="subject" th:text="#{changeMetadata.subject}"></label>
<input type="text" class="form-control" id="subject" name="subject">
</div>
<div class="mb-3">
<label class="form-check-label" for="title" th:text="#{changeMetadata.title}"></label>
<input type="text" class="form-control" id="title" name="title">
</div>
<div class="mb-3">
<label class="form-check-label" for="trapped" th:text="#{changeMetadata.trapped}"></label>
<select class="form-control" id="trapped" name="trapped">
<option value="True" th:text="#{true}"></option>
<option value="False" th:text="#{false}" selected></option>
<option value="Unknown" th:text="#{unknown}"></option>
</select>
</div>
<div id="customMetadata" style="display: none;">
<h3 th:text="#{changeMetadata.selectText.4}"></h3>
<div class="mb-3" id="otherMetadataEntries"></div>
</div>
<div id="customMetadataEntries"></div>
<button type="button" class="btn btn-secondary" id="addMetadataBtn" th:text="#{changeMetadata.selectText.5}"></button>
<br>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{changeMetadata.submit}"></button>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script type="module" th:src="@{'/js/pages/change-metadata.js'}">
</script>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,256 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{compare.title}, header=#{compare.header})}"></th:block>
<style>
.result-column {
border: 1px solid #ccc;
padding: 15px;
margin-bottom: 15px;
overflow-y: auto;
height: calc(100vh - 400px);
white-space: pre-wrap;
}
.flex-container {
display: flex;
flex-direction: row;
}
.color-selector {
display: flex;
flex-direction: row;
align-items: center;
width: 50%;
max-height: 100px;
margin-bottom: 2rem;
}
#color-box1,
#color-box2 {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: none;
background-color: transparent;
}
.spacer1 {
padding-right: calc(var(--bs-gutter-x) * .5);
}
.spacer2 {
padding-left: calc(var(--bs-gutter-x) * .5);
}
</style>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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-9 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon other">compare</span>
<span class="tool-header-text" th:text="#{compare.header}"></span>
</div>
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf', remoteCall='false')}">
</div>
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput2', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf', remoteCall='false')}">
</div>
<div class="row">
<div class="flex-container">
<div class="color-selector spacer1">
<label th:text="#{compare.highlightColor.1}"></label>
<label for="color-box1"></label><input type="color" id="color-box1" value="#ff0000">
</div>
<div class="color-selector spacer2">
<label th:text="#{compare.highlightColor.2}"></label>
<label for="color-box2"></label><input type="color" id="color-box2" value="#008000">
</div>
</div>
</div>
<button class="btn btn-primary" onclick="comparePDFs()" th:text="#{compare.submit}"></button>
<div class="row">
<div class="col-md-6">
<h3 th:text="#{compare.document.1}"></h3>
<div id="result1" class="result-column"></div>
</div>
<div class="col-md-6">
<h3 th:text="#{compare.document.2}"></h3>
<div id="result2" class="result-column"></div>
</div>
</div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script th:inline="javascript">
// get the elements
var result1 = document.getElementById('result1');
var result2 = document.getElementById('result2');
// add event listeners
result1.addEventListener('scroll', function () {
result2.scrollTop = result1.scrollTop;
});
result2.addEventListener('scroll', function () {
result1.scrollTop = result2.scrollTop;
});
async function comparePDFs() {
const file1 = document.getElementById("fileInput-input").files[0];
const file2 = document.getElementById("fileInput2-input").files[0];
var color1 = document.getElementById('color-box1').value;
var color2 = document.getElementById('color-box2').value;
const complexMessage = /*[[#{compare.complex.message}]]*/ 'One or both of the provided documents are large files, accuracy of comparison may be reduced';
const largeFilesMessage = /*[[#{compare.large.file.message}]]*/ 'One or Both of the provided documents are too large to process';
const noTextMessage = /*[[#{compare.no.text.message}]]*/ 'One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison."';
if (!file1 || !file2) {
console.error("Please select two PDF files to compare");
return;
}
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const [pdf1, pdf2] = await Promise.all([
pdfjsLib.getDocument(URL.createObjectURL(file1)).promise,
pdfjsLib.getDocument(URL.createObjectURL(file2)).promise
]);
const extractText = async (pdf) => {
const pages = [];
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
const strings = content.items.map(item => item.str);
pages.push(strings.join(" "));
}
return pages.join(" ");
};
const [text1, text2] = await Promise.all([
extractText(pdf1),
extractText(pdf2)
]);
if (text1.trim() === "" || text2.trim() === "") {
alert(noTextMessage);
return;
}
const resultDiv1 = document.getElementById("result1");
const resultDiv2 = document.getElementById("result2");
const loading = /*[[#{loading}]]*/ 'Loading...';
resultDiv1.innerHTML = loading;
resultDiv2.innerHTML = loading;
// Create a new Worker
const worker = new Worker('./js/compare/pdfWorker.js');
// Post messages to the worker
worker.postMessage({
type: 'SET_COMPLEX_MESSAGE',
message: complexMessage
});
worker.postMessage({
type: 'SET_TOO_LARGE_MESSAGE',
message: largeFilesMessage
});
// Error handling for the worker
worker.onerror = function (error) {
console.error('Worker error:', error);
};
worker.onmessage = function (e) {
const { status, differences, message } = e.data;
if (status === 'error') {
resultDiv1.innerHTML = '';
resultDiv2.innerHTML = '';
alert(message);
return;
}
if (status === 'success' && differences) {
console.log('Differences:', differences);
displayDifferences(differences);
}
if (event.data.status === 'warning') {
console.warn(event.data.message);
alert(event.data.message);
}
};
worker.postMessage({ text1, text2, color1, color2 });
const displayDifferences = (differences) => {
const resultDiv1 = document.getElementById("result1");
const resultDiv2 = document.getElementById("result2");
resultDiv1.innerHTML = "";
resultDiv2.innerHTML = "";
differences.forEach(([color, word]) => {
const span1 = document.createElement("span");
const span2 = document.createElement("span");
if (color === color2) {
span1.style.color = "transparent";
span1.style.userSelect = "none";
span2.style.color = color;
}
// If it's a deletion, show it in in the first document and transparent in the second
else if (color === color1) {
span1.style.color = color;
span2.style.color = "transparent";
span2.style.userSelect = "none";
}
// If it's unchanged, show it in black in both
else {
span1.style.color = color;
span2.style.color = color;
}
span1.textContent = word;
span2.textContent = word;
resultDiv1.appendChild(span1);
resultDiv2.appendChild(span2);
// Add space after each word, or a new line if the word ends with a full stop
const spaceOrNewline1 = document.createElement("span");
const spaceOrNewline2 = document.createElement("span");
if (word.endsWith(".")) {
spaceOrNewline1.innerHTML = "<br>";
spaceOrNewline2.innerHTML = "<br>";
} else {
spaceOrNewline1.textContent = " ";
spaceOrNewline2.textContent = " ";
}
resultDiv1.appendChild(spaceOrNewline1);
resultDiv2.appendChild(spaceOrNewline2);
});
};
}
</script>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{compress.title}, header=#{compress.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 advance">zoom_in_map</span>
<span class="tool-header-text" th:text="#{compress.header}"></span>
</div>
<form action="#" th:action="@{'/api/v1/misc/compress-pdf'}" method="post" enctype="multipart/form-data">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<div class="card mb-3">
<div class="card-body">
<h4 th:text="#{compress.selectText.1}"></h4>
<p th:utext="#{compress.selectText.1.1}"></p>
<label for="optimizeLevel" th:text="#{compress.selectText.2}"></label>
<select name="optimizeLevel" id="optimizeLevel" class="form-control">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5" selected>5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
</select>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" name="grayscale" id="grayscaleCheck" value="true">
<label class="form-check-label" for="grayscaleCheck" th:text="#{compress.grayscale.label}">
Convert images to grayscale for better compression
</label>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<h4 th:text="#{compress.selectText.4}"></h4>
<label for="expectedOutputSize" th:text="#{compress.selectText.5}"></label>
<input type="text" name="expectedOutputSize" id="expectedOutputSize" class="form-control">
</div>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{compress.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{home.ScannerImageSplit.title}, header=#{home.ScannerImageSplit.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 advance">scanner</span>
<span class="tool-header-text" th:text="#{home.ScannerImageSplit.title}"></span>
</div>
<p th:if="${!isPython}" th:text="#{ScannerImageSplit.info}" class="alert alert-success text-center">Python is not installed. It is required to run.</p>
<form th:if="${isPython}" id="multiPdfForm" th:action="@{'/api/v1/misc/extract-image-scans'}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='image/*, application/pdf')}"></div>
<div class="mb-3">
<label for="angleThreshold" th:text="#{ScannerImageSplit.selectText.1}"></label>
<input type="number" class="form-control" id="angleThreshold" name="angle_threshold" value="10">
<small id="angleThresholdHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.2}"></small>
</div>
<div class="mb-3">
<label for="tolerance" th:text="#{ScannerImageSplit.selectText.3}"></label>
<input type="number" class="form-control" id="tolerance" name="tolerance" value="20">
<small id="toleranceHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.4}"></small>
</div>
<div class="mb-3">
<label for="minArea" th:text="#{ScannerImageSplit.selectText.5}"></label>
<input type="number" class="form-control" id="minArea" name="min_area" value="8000">
<small id="minAreaHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.6}"></small>
</div>
<div class="mb-3">
<label for="minContourArea" th:text="#{ScannerImageSplit.selectText.7}"></label>
<input type="number" class="form-control" id="minContourArea" name="min_contour_area" value="500">
<small id="minContourAreaHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.8}"></small>
</div>
<div class="mb-3">
<label for="borderSize" th:text="#{ScannerImageSplit.selectText.9}"></label>
<input type="number" class="form-control" id="borderSize" name="border_size" value="1">
<small id="borderSizeHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.10}"></small>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{genericSubmit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{extractImages.title}, header=#{extractImages.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 other">photo_library</span>
<span class="tool-header-text" th:text="#{extractImages.header}"></span>
</div>
<form id="multiPdfForm" th:action="@{'/api/v1/misc/extract-images'}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<div class="mb-3">
<label th:text="#{extractImages.selectText}"></label>
<select class="form-control" name="format">
<option value="png">PNG</option>
<option value="jpg">JPG</option>
<option value="gif">GIF</option>
</select>
</div>
<div class="mb-3">
<input type="checkbox" name="allowDuplicates" id="allowDuplicates">
<label for="allowDuplicates" th:text="#{extractImages.allowDuplicates}"></label>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{extractImages.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,163 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{fakeScan.title}, header=#{fakeScan.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 advance">scanner</span>
<span class="tool-header-text" th:text="#{fakeScan.title}"></span>
</div>
<form method="post" enctype="multipart/form-data" id="uploadForm" th:action="@{'/api/v1/misc/fake-scan'}">
<input type="hidden" name="advancedEnabled" id="advancedEnabled" value="false">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<br>
<div class="mb-3">
<label for="quality" class="form-label" th:text="#{fakeScan.quality}"></label>
<select class="form-select" id="quality" name="quality">
<option value="low" th:text="#{fakeScan.quality.low}"></option>
<option value="medium" th:text="#{fakeScan.quality.medium}"></option>
<option value="high" th:text="#{fakeScan.quality.high}" selected></option>
</select>
</div>
<div class="mb-3">
<label for="rotation" class="form-label" th:text="#{fakeScan.rotation}"></label>
<select class="form-select" id="rotation" name="rotation">
<option value="none" th:text="#{fakeScan.rotation.none}"></option>
<option value="slight" th:text="#{fakeScan.rotation.slight}" selected></option>
<option value="moderate" th:text="#{fakeScan.rotation.moderate}"></option>
<option value="severe" th:text="#{fakeScan.rotation.severe}"></option>
</select>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="advancedSettingsToggle">
<label class="form-check-label" for="advancedSettingsToggle" th:text="#{fakeScan.advancedSettings}"></label>
</div>
<div id="advancedSettings" style="display:none; border:1px solid #eee; padding:1em; border-radius:8px; margin-bottom:1em;">
<div class="mb-3">
<label for="colorspace" class="form-label" th:text="#{fakeScan.colorspace}"></label>
<select class="form-select" id="colorspace" name="colorspace">
<option value="grayscale" th:text="#{fakeScan.colorspace.grayscale}" selected></option>
<option value="color" th:text="#{fakeScan.colorspace.color}"></option>
</select>
</div>
<div class="mb-3">
<label for="border" class="form-label" th:text="#{fakeScan.border}"></label>
<input type="number" class="form-control" id="border" name="border" min="0" max="100" value="20">
</div>
<div class="mb-3">
<label for="rotate" class="form-label" th:text="#{fakeScan.rotate}"></label>
<input type="number" class="form-control" id="rotate" name="rotate" min="-15" max="15" value="0">
</div>
<div class="mb-3">
<label for="rotateVariance" class="form-label" th:text="#{fakeScan.rotateVariance}"></label>
<input type="number" class="form-control" id="rotateVariance" name="rotateVariance" min="0" max="10" value="2">
</div>
<div class="mb-3">
<label for="brightness" class="form-label" th:text="#{fakeScan.brightness}"></label>
<input type="range" class="form-range" id="brightness" name="brightness" min="0.5" max="1.5" step="0.01" value="1.0">
</div>
<div class="mb-3">
<label for="contrast" class="form-label" th:text="#{fakeScan.contrast}"></label>
<input type="range" class="form-range" id="contrast" name="contrast" min="0.5" max="1.5" step="0.01" value="1.0">
</div>
<div class="mb-3">
<label for="blur" class="form-label" th:text="#{fakeScan.blur}"></label>
<input type="range" class="form-range" id="blur" name="blur" min="0" max="5" step="0.1" value="1.0">
</div>
<div class="mb-3">
<label for="noise" class="form-label" th:text="#{fakeScan.noise}"></label>
<input type="range" class="form-range" id="noise" name="noise" min="0" max="20" step="0.1" value="8.0">
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="yellowish" name="yellowish">
<label class="form-check-label" for="yellowish" th:text="#{fakeScan.yellowish}"></label>
</div>
<div class="mb-3">
<label for="resolution" class="form-label" th:text="#{fakeScan.resolution}"></label>
<input type="number" class="form-control" id="resolution" name="resolution" min="72" max="600" value="300">
</div>
</div>
<div class="mb-3 text-left">
<button type="submit" class="btn btn-primary" th:text="#{fakeScan.submit}" id="submitBtn"></button>
</div>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<script th:inline="javascript">
// Show/hide advanced settings
document.getElementById('advancedSettingsToggle').addEventListener('change', function() {
document.getElementById('advancedSettings').style.display = this.checked ? 'block' : 'none';
});
// Form submission handling
const form = document.getElementById('uploadForm');
if (form) {
form.addEventListener('submit', function(e) {
// If advanced settings are not enabled, remove advanced fields
if (!document.getElementById('advancedSettingsToggle').checked) {
const formData = new FormData(form);
formData.delete('colorspace');
formData.delete('border');
formData.delete('rotate');
formData.delete('rotateVariance');
formData.delete('brightness');
formData.delete('contrast');
formData.delete('blur');
formData.delete('noise');
formData.delete('yellowish');
formData.delete('resolution');
}
});
}
// Initialize advanced settings state
const advancedSettingsToggle = document.getElementById('advancedSettingsToggle');
const advancedSettings = document.getElementById('advancedSettings');
if (advancedSettingsToggle && advancedSettings) {
// Helper: enable/disable all fields in advanced settings
function setAdvancedFieldsEnabled(enabled) {
const fields = advancedSettings.querySelectorAll('input, select');
fields.forEach(field => {
field.disabled = !enabled;
// If enabling and value is empty, set to default
if (enabled && (field.value === '' || field.value == null)) {
if (field.type === 'number' || field.type === 'range') {
field.value = field.defaultValue;
} else if (field.type === 'checkbox') {
field.checked = field.defaultChecked;
} else if (field.tagName === 'SELECT') {
field.value = field.querySelector('option[selected]')?.value || field.options[0].value;
}
}
});
}
// Set initial state
setAdvancedFieldsEnabled(advancedSettingsToggle.checked);
advancedSettings.style.display = advancedSettingsToggle.checked ? 'block' : 'none';
document.getElementById('advancedEnabled').value = advancedSettingsToggle.checked ? 'true' : 'false';
// Add change listener
advancedSettingsToggle.addEventListener('change', function() {
advancedSettings.style.display = this.checked ? 'block' : 'none';
setAdvancedFieldsEnabled(this.checked);
document.getElementById('advancedEnabled').value = this.checked ? 'true' : 'false';
});
}
</script>
</body>
</html>
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{flatten.title}, header=#{flatten.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 other">layers_clear</span>
<span class="tool-header-text" th:text="#{flatten.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/flatten'}" id="pdfForm" class="mb-3">
<div class="custom-file">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="flattenOnlyForms" name="flattenOnlyForms">
<label for="flattenOnlyForms" th:text="#{flatten.flattenOnlyForms}" ></label>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{flatten.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,240 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{ocr.title}, header=#{ocr.header})}"></th:block>
<style>
#languages {
max-height: 400px;
overflow-y: auto;
border: 1px solid var(--md-sys-color-surface-3);
border-radius: 5px;
padding: 10px;
}
</style>
<script>
function handleLangSelection() {
let checkboxes = document.getElementsByName("languages");
let selected = false;
for (let i = 0; i < checkboxes.length; i++) {
if (checkboxes[i].checked) {
selected = true;
checkboxes[i].setAttribute('required', 'false');
}
}
if (selected) {
for (let i = 0; i < checkboxes.length; i++) {
checkboxes[i].removeAttribute('required');
}
}
else {
for (let i = 0; i < checkboxes.length; i++) {
checkboxes[i].setAttribute('required', 'true');
}
}
}
</script>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 other">quick_reference_all</span>
<span class="tool-header-text" th:text="#{ocr.header}"></span>
</div>
<form th:if="${#lists.size(languages) > 0}" action="#" th:action="@{'/api/v1/misc/ocr-pdf'}" method="post" enctype="multipart/form-data" class="mb-3">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<div class="mb-3">
<label for="languages" class="form-label" th:text="#{ocr.selectText.1}"></label>
<hr>
<div id="languages">
<div class="form-check" th:each="language, iterStat : ${languages}">
<input type="checkbox" th:name="languages" th:value="${language}" required th:id="${'language-' + language}" onchange="handleLangSelection()" />
<label th:for="${'language-' + language}" th:text="${language}"></label>
</div>
</div>
<hr>
</div>
<div class="mb-3">
<label th:text="#{ocr.selectText.10}"></label>
<select class="form-control" name="ocrType">
<option value="skip-text" th:text="#{ocr.selectText.6}"></option>
<option selected value="force-ocr" th:text="#{ocr.selectText.7}"></option>
<option value="Normal" th:text="#{ocr.selectText.8}"></option>
</select>
</div>
<br>
<label for="languages" class="form-label" th:text="#{ocr.selectText.9}"></label>
<div class="mb-3">
<label th:text="#{ocr.selectText.12}"></label>
<select class="form-control" name="ocrRenderType">
<option value="hocr">HOCR (Latin/Roman alphabet only)</option>
<option value="sandwich">Sandwich</option>
</select>
</div>
<br>
<div class="mb-3" th:if="${@endpointConfiguration.isGroupEnabled('OCRmyPDF')}">
<label class="form-label">OCR Options</label>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="sidecar" name="sidecar" value="true">
<label class="form-check-label" for="sidecar">Include OCR text in sidecar text file</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="deskew" name="deskew" value="true">
<label class="form-check-label" for="deskew">Deskew input file</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="clean" name="clean" value="true">
<label class="form-check-label" for="clean">Clean input file</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="cleanFinal" name="cleanFinal" value="true">
<label class="form-check-label" for="cleanFinal">Clean final output</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="removeImagesAfter" name="removeImagesAfter" value="true">
<label class="form-check-label" for="removeImagesAfter">Remove images from output PDF</label>
</div>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{ocr.submit}"></button>
</form>
<script th:inline="javascript">
// Get language translations from Thymeleaf messages
const languageTranslations = {};
/*[# th:each="lang : ${languages}"]*/
languageTranslations['[(${lang})]'] = /*[[#{${'lang.' + lang}}]]*/[(${lang})];
/*[/]*/
// Map two-letter language codes to Tesseract language codes
const localeToTesseract = {
'en': 'eng',
'fr': 'fra',
'de': 'deu',
'es': 'spa',
'it': 'ita',
'pt': 'por',
'ru': 'rus',
'zh': 'chi_sim',
'ja': 'jpn',
'ko': 'kor',
'ar': 'ara',
'hi': 'hin',
'nl': 'nld',
'cs': 'ces',
'pl': 'pol',
'tr': 'tur',
'uk': 'ukr',
'vi': 'vie',
'sv': 'swe',
'no': 'nor',
'fi': 'fin',
'da': 'dan',
'el': 'ell',
'he': 'heb',
'hu': 'hun',
'bg': 'bul',
'ro': 'ron',
'hr': 'hrv',
'sk': 'slk',
'id': 'ind',
'th': 'tha',
'sl': 'slv'
};
// Function to get the translated language name
function getTranslatedLanguageName(shortCode) {
return languageTranslations[shortCode] || shortCode;
}
// Function to prioritize languages based on browser locale
function prioritizeLanguages() {
const languageContainer = document.getElementById('languages');
if (!languageContainer) return;
// Update all labels with translated language names
const formChecks = Array.from(languageContainer.getElementsByClassName('form-check'));
if (formChecks.length === 0) return;
formChecks.forEach(element => {
const label = element.querySelector('label');
if (label) {
const langCode = label.getAttribute('for').split('-')[1];
label.textContent = getTranslatedLanguageName(langCode);
}
});
// Get browser/UI language
const browserLanguage = document.documentElement.lang || navigator.language || navigator.userLanguage;
const uiLanguage = document.documentElement.getAttribute('data-language') || browserLanguage;
// Extract primary language code
const primaryLanguageCode = (uiLanguage || '').split(/[-_]/)[0].toLowerCase();
const tesseractPrimaryCode = localeToTesseract[primaryLanguageCode];
// Define priority language array
const priorityLanguages = [];
// Primary language first
if (tesseractPrimaryCode) {
priorityLanguages.push(tesseractPrimaryCode);
}
// English second (if not already added)
if (tesseractPrimaryCode !== 'eng') {
priorityLanguages.push('eng');
}
// Sort the elements
const sortedElements = formChecks.sort((a, b) => {
const aInput = a.querySelector('input');
const bInput = b.querySelector('input');
if (!aInput || !bInput) return 0;
const aLangCode = aInput.id.split('-')[1];
const bLangCode = bInput.id.split('-')[1];
const aIsPriority = priorityLanguages.includes(aLangCode);
const bIsPriority = priorityLanguages.includes(bLangCode);
if (aIsPriority && !bIsPriority) return -1;
if (!aIsPriority && bIsPriority) return 1;
if (aIsPriority && bIsPriority) {
// If both are priority, sort by priority order
return priorityLanguages.indexOf(aLangCode) - priorityLanguages.indexOf(bLangCode);
}
// For non-priority languages, sort alphabetically by full name
return getTranslatedLanguageName(aLangCode).localeCompare(getTranslatedLanguageName(bLangCode));
});
// Clear container and append sorted elements
languageContainer.innerHTML = '';
sortedElements.forEach(element => {
languageContainer.appendChild(element);
});
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
prioritizeLanguages();
});
</script>
<p th:text="#{ocr.help}"></p>
<a href="https://docs.stirlingpdf.com/Advanced%20Configuration/OCR">https://docs.stirlingpdf.com/Advanced%20Configuration/OCR</a>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{printFile.title}, header=#{printFile.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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">
<h2 th:text="#{printFile.header}"></h2>
<form action="#" th:action="@{'/api/v1/misc/print-file'}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf,image/*')}"></div>
<div class="card mb-3">
<div class="card-body">
<h4 th:text="#{printFile.selectText.1}">Select Printer</h4> <!-- Assuming the message code printFile.selectText.3 corresponds to "Select Printer" -->
<label for="printerName" th:text="#{printFile.selectText.2}">Printer Name:</label> <!-- Assuming the message code printFile.selectText.4 corresponds to "Printer Name:" -->
<input type="text" name="printerName" id="printerName" class="form-control">
</div>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{printFile.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{removeAnnotations.title}, header=#{removeAnnotations.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 other">thread_unread</span>
<span class="tool-header-text" th:text="#{removeAnnotations.header}"></span>
</div>
<form id="pdfForm" th:action="@{''}" class="mb-3">
<div class="custom-file">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf', remoteCall='false')}"></div>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removeAnnotations.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script th:src="@{'/js/local-pdf-input-download.js'}"></script>
<script>
document.getElementById('pdfForm').addEventListener('submit', async (e) => {
e.preventDefault();
const { PDFDocument } = PDFLib;
const processFile = async (file) => {
const origFileUrl = URL.createObjectURL(file);
const formPdfBytes = await window.fetchWithCsrf(origFileUrl).then(res => res.arrayBuffer());
const pdfDoc = await PDFDocument.load(formPdfBytes, { ignoreEncryption: true });
const pages = pdfDoc.getPages();
for (let i = 0; i < pages.length; ++i) {
const page = pages[i];
const annotations = page.node.Annots();
if (!annotations) continue;
const ctx = annotations.context;
for (let j = 0; j < annotations.size(); ++j) {
const annotation = annotations.get(j);
ctx.delete(annotation);
}
}
const pdfBytes = await pdfDoc.save();
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
const fileName = (file.name ? file.name.replace('.pdf', '') : 'pdf') + '_removed_annotations.pdf';
return { processedData: pdfBlob, fileName };
};
await downloadFilesWithCallback(processFile);
});
</script>
</body>
</html>
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{removeBlanks.title}, header=#{removeBlanks.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 other">scan_delete</span>
<span class="tool-header-text" th:text="#{removeBlanks.header}"></span>
</div>
<form id="multiPdfForm" th:action="@{'/api/v1/misc/remove-blanks'}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<div class="mb-3">
<label for="threshold" th:text="#{removeBlanks.threshold}"></label>
<input type="number" class="form-control" id="threshold" name="threshold" value="10" min="0" max="255" step="1">
<small id="thresholdHelp" class="form-text text-muted" th:text="#{removeBlanks.thresholdDesc}"></small>
</div>
<div class="mb-3">
<label for="whitePercent" th:text="#{removeBlanks.whitePercent}"></label>
<input type="number" class="form-control" id="whitePercent" name="whitePercent" value="99.9" step="0.1" min="0.1" max="100">
<small id="whitePercentHelp" class="form-text text-muted" th:text="#{removeBlanks.whitePercentDesc}"></small>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removeBlanks.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{repair.title}, header=#{repair.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 advance">build</span>
<span class="tool-header-text" th:text="#{repair.header}"></span>
</div>
<form id="multiPdfForm" th:action="@{'/api/v1/misc/repair'}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{repair.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{replace-color.title}, header=#{replace-color.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 advance">zoom_in_map</span>
<span class="tool-header-text" th:text="#{replace-color.header}"></span>
</div>
<form action="#" th:action="@{'/api/v1/misc/replace-invert-pdf'}" method="post" enctype="multipart/form-data">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<div class="card mb-3">
<div class="card-body">
<h4 th:text="#{replace-color.selectText.1}"></h4>
<select name="replaceAndInvertOption" id="replace-invert" class="form-control">
<option value="HIGH_CONTRAST_COLOR" th:text="#{replace-color.selectText.2}" ></option>
<option value="CUSTOM_COLOR" th:text="#{replace-color.selectText.3}"></option>
<option value="FULL_INVERSION" th:text="#{replace-color.selectText.4}" selected></option>
</select>
</div>
</div>
<div class="card mb-3" id="high-contrast-options" style="display: none">
<div class="card-body">
<h4 th:text="#{replace-color.selectText.5}"></h4>
<select name="highContrastColorCombination" id="high-contrast" class="form-control">
<option value="WHITE_TEXT_ON_BLACK" th:text="#{replace-color.selectText.6}" selected></option>
<option value="BLACK_TEXT_ON_WHITE" th:text="#{replace-color.selectText.7}"></option>
<option value="YELLOW_TEXT_ON_BLACK" th:text="#{replace-color.selectText.8}"></option>
<option value="GREEN_TEXT_ON_BLACK" th:text="#{replace-color.selectText.9}"></option>
</select>
</div>
</div>
<div class="card mb-3" id = "custom-color-1" style="display: none">
<div class="card-body">
<h4 th:text="#{replace-color.selectText.10}"></h4>
<input type="color" name="textColor" id="text-color" class="form-control">
</div>
</div>
<div class="card mb-3" id = "custom-color-2" style="display: none">
<div class="card-body">
<h4 th:text="#{replace-color.selectText.11}"></h4>
<input type="color" name="backGroundColor" id="bg-color" class="form-control">
</div>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{replace-color.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script>
$(document).ready(function(){
$('#replace-invert').change(function() {
var selectedOption = $(this).val();
// Hide all conditional fields by default
$('#high-contrast-options').hide();
$('#custom-color-1').hide();
$('#custom-color-2').hide();
if (selectedOption === "HIGH_CONTRAST_COLOR") {
$('#high-contrast-options').show();
} else if (selectedOption === "CUSTOM_COLOR") {
$('#custom-color-1').show();
$('#custom-color-2').show();
}
});
});
</script>
</body>
</html>
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{showJS.title}, header=#{showJS.header})}"></th:block>
<link th:href="@{'/css/prism.css'}" rel="stylesheet">
<script th:src="@{'/js/thirdParty/prism.js'}"></script>
<style>
/* Add a max-height and make it scrollable */
#script-content {
max-height: 1000px; /* Adjust this to your preferred maximum height */
overflow-y: auto;
}
</style>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 advance">javascript</span>
<span class="tool-header-text" th:text="#{showJS.header}"></span>
</div>
<form id="pdfInfoForm" method="post" enctype="multipart/form-data" th:action="@{'/show-javascript'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, remoteCall='false', accept='application/pdf')}"></div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{showJS.submit}"></button>
</form>
<div class="container mt-4">
<!-- Iterate over each main section in the JSON -->
<div id="script-content">
<!-- JavaScript will populate this section -->
</div>
<!-- Button to download the JSON -->
<a href="#" id="downloadJS" class="btn btn-primary mt-3" style="display: none;" th:text="#{showJS.downloadJS}">Download JSON</a>
</div>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<script>
document.querySelector('#pdfInfoForm').addEventListener('submit', function(event){
event.preventDefault();
// Fetch the formData
const formData = new FormData(event.target);
fetchWithCsrf('api/v1/misc/show-javascript', {
method: 'POST',
body: formData
}).then(response => response.text())
.then(data => {
// Escape < and > characters
let escapedData = data.replace(/</g, '&lt;').replace(/>/g, '&gt;');
// Create the elements manually
let preElement = document.createElement('pre');
let codeElement = document.createElement('code');
codeElement.classList.add('language-javascript');
codeElement.textContent = escapedData; // Use textContent instead of innerHTML
preElement.appendChild(codeElement);
let scriptContent = document.querySelector('#script-content');
// Clear existing content, if any
while (scriptContent.firstChild) {
scriptContent.removeChild(scriptContent.firstChild);
}
scriptContent.appendChild(preElement);
// Highlight the code using Prism.js
Prism.highlightAll();
// Create a blob object from the data and create a URL for it
let blob = new Blob([data], {type: 'application/javascript'});
let url = URL.createObjectURL(blob);
// Set the URL as the href of the download button and provide a download name
let downloadButton = document.querySelector('#downloadJS');
downloadButton.href = url;
downloadButton.download = 'extracted.js';
downloadButton.style.display = 'block';
})
.catch(error => {
console.error('Error:', error);
});
});
</script>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,203 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{AddStampRequest.title}, header=#{AddStampRequest.header})}"></th:block>
<style>
.a4container {
position: relative;
width: 50%;
aspect-ratio: 0.707/1;
border: 1px solid #ddd;
box-sizing: border-box;
background-color: white;
}
</style>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 security">approval</span>
<span class="tool-header-text" th:text="#{AddStampRequest.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/add-stamp'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<br>
<div class="mb-3">
<label for="pageOrder" th:text="#{pageSelectionPrompt}"></label>
<input type="text" class="form-control" id="pageOrder" name="pageNumbers" value="1" placeholder="(e.g. 1,3,2 or 4-8,2,10-12 or 2n-1)" required>
</div>
<div class="mb-3">
<label for="customMargin" class="form-label" th:text="#{AddStampRequest.customMargin}">Custom Margin</label>
<select class="form-select" id="customMargin" name="customMargin">
<option value="small" th:text="#{sizes.small}"></option>
<option value="medium" selected th:text="#{sizes.medium}"></option>
<option value="large" th:text="#{sizes.large}"></option>
<option value="x-large" th:text="#{sizes.x-large}"></option>
</select>
</div>
<div class="mb-3">
<label th:text="#{AddStampRequest.position}"></label>
<div class="a4container">
<div class="pageNumber" id="1" style="top: 10%; left: 10%;">1</div>
<div class="pageNumber" id="2" style="top: 10%; left: 50%;">2</div>
<div class="pageNumber" id="3" style="top: 10%; left: 90%;">3</div>
<div class="pageNumber" id="4" style="top: 50%; left: 10%;">4</div>
<div class="pageNumber" id="5" style="top: 50%; left: 50%;">5</div>
<div class="pageNumber" id="6" style="top: 50%; left: 90%;">6</div>
<div class="pageNumber" id="7" style="top: 90%; left: 10%;">7</div>
<div class="pageNumber selectedPosition" id="8" style="top: 90%; left: 50%;">8</div>
<div class="pageNumber" id="9" style="top: 90%; left: 90%;">9</div>
</div>
</div>
<input type="hidden" id="numberInput" name="position" value="8">
<div class="mb-3">
<label for="stampType" class="form-label" th:text="#{AddStampRequest.stampType}">Stamp Type</label>
<select class="form-select" id="stampType" name="stampType" onchange="toggleFileOption()">
<option value="text" th:text="#{watermark.type.1}">Text</option>
<option value="image" th:text="#{watermark.type.2}">Image</option>
</select>
</div>
<div id="stampTextGroup" class="mb-3">
<label for="stampText" class="form-label" th:text="#{AddStampRequest.stampText}">Stamp Text</label>
<input type="text" class="form-control" id="stampText" name="stampText">
</div>
<div id="stampImageGroup" class="mb-3" style="display: none;">
<label for="stampImage" class="form-label" th:text="#{AddStampRequest.stampImage}">Stamp Image</label>
<input type="file" class="form-control" id="stampImage" name="stampImage" accept="image/*" >
</div>
<div id="alphabetGroup" class="mb-3">
<label for="alphabet" class="form-label" th:text="#{AddStampRequest.alphabet}">Alphabet</label>
<select class="form-select" id="alphabet" name="alphabet">
<option value="roman">Roman</option>
<option value="arabic">العربية</option>
<option value="japanese">日本語</option>
<option value="korean">한국어</option>
<option value="chinese">简体中文</option>
<option value="thai">ไทย</option>
</select>
</div>
<div class="mb-3">
<label for="fontSize" class="form-label" th:text="#{AddStampRequest.fontSize}">Font Size</label>
<input type="number" class="form-control" id="fontSize" name="fontSize" value="30">
</div>
<div class="mb-3">
<label for="rotation" class="form-label" th:text="#{AddStampRequest.rotation}">Rotation</label>
<input type="number" class="form-control" id="rotation" name="rotation" value="0">
</div>
<div class="mb-3">
<label for="opacity" class="form-label" th:text="#{AddStampRequest.opacity}">Opacity</label>
<input type="number" class="form-control" id="opacity" name="opacity" step="0.1" value="0.5">
</div>
<div class="mb-3">
<label for="overrideX" class="form-label" th:text="#{AddStampRequest.overrideX}">Override X</label>
<input type="number" class="form-control" id="overrideX" name="overrideX" value="-1">
</div>
<div class="mb-3">
<label for="overrideY" class="form-label" th:text="#{AddStampRequest.overrideY}">Override Y</label>
<input type="number" class="form-control" id="overrideY" name="overrideY" value="-1">
</div>
<div class="mb-3">
<label for="customColor" class="form-label" th:text="#{AddStampRequest.customColor}">Custom Color</label>
<div class="form-control form-control-color" style="background-color: #d3d3d3;">
<input type="color" id="customColor" name="customColor" value="#d3d3d3">
</div>
<script>
let colorInput = document.getElementById("customColor");
if (colorInput) {
let colorInputContainer = colorInput.parentElement;
if (colorInputContainer) {
colorInput.onchange = function() {
colorInputContainer.style.backgroundColor = colorInput.value;
}
colorInputContainer.style.backgroundColor = colorInput.value;
}
}
</script>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{AddStampRequest.submit}"></button>
</form>
</div>
</div>
</div>
<script>
let cells = document.querySelectorAll('.pageNumber');
let inputField = document.getElementById('numberInput');
cells.forEach(cell => {
cell.addEventListener('click', function(e) {
cells.forEach(cell => {
cell.classList.remove('selectedPosition'); // Remove selected class from all cells
cell.classList.remove('selectedHovered'); // Also remove selectedHovered class
});
let selectedLocation = e.target.id;
inputField.value = selectedLocation;
e.target.classList.add('selectedPosition'); // Add selected class to clicked cell
e.target.classList.add('selectedHovered'); // Add selectedHovered class
});
cell.addEventListener('mouseenter', function(e) {
if(e.target.classList.contains('selectedPosition')) {
e.target.classList.add('selectedHovered');
}
});
cell.addEventListener('mouseleave', function(e) {
if(e.target.classList.contains('selectedPosition')) {
e.target.classList.remove('selectedHovered');
}
});
});
function toggleFileOption() {
const stampImage = document.getElementById('stampImage');
const stampType = document.getElementById('stampType').value;
const stampTextGroup = document.getElementById('stampTextGroup');
const stampImageGroup = document.getElementById('stampImageGroup');
const alphabetGroup = document.getElementById('alphabetGroup');
if (stampType === 'text') {
if (stampImage.hasAttribute('required')) {
stampImage.removeAttribute('required');
}
stampTextGroup.style.display = 'block';
stampImageGroup.style.display = 'none';
alphabetGroup.style.display = 'block';
} else if (stampType === 'image') {
if (!stampImage.hasAttribute('required')) {
stampImage.setAttribute('required', '');
}
stampTextGroup.style.display = 'none';
stampImageGroup.style.display = 'block';
alphabetGroup.style.display = 'none';
}
}
</script>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{unlockPDFForms.title}, header=#{unlockPDFForms.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 other">preview_off</span>
<span class="tool-header-text" th:text="#{unlockPDFForms.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/unlock-pdf-forms'}" id="pdfForm" class="mb-3">
<div class="custom-file">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{unlockPDFForms.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{pageLayout.title}, header=#{pageLayout.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 organize">dashboard</span>
<span class="tool-header-text" th:text="#{pageLayout.header}"></span>
</div>
<form id="multiPdfForm" th:action="@{'/api/v1/general/multi-page-layout'}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<div class="mb-3">
<label for="pagesPerSheet" th:text="#{pageLayout.pagesPerSheet}"></label>
<select class="form-control" id="pagesPerSheet" name="pagesPerSheet">
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="9">9</option>
<option value="16">16</option>
</select>
</div>
<div class="form-check mb-3">
<input type="checkbox" id="addBorder" name="addBorder">
<label for="addBorder" th:text="#{pageLayout.addBorder}"></label>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pageLayout.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{multiTool.title}, header=#{multiTool.header})}"></th:block>
</head>
<body>
<div id="image-highlighter"></div>
<div id="page-container">
<div id="content-wrap">
<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-12">
<div class="bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon advance">construction</span>
<span class="tool-header-text" th:text="#{multiTool.header}"></span>
</div>
<div class="mt-action-bar d-flex flex-wrap">
<div class="mt-filename">
<input type="text" class="form-control" id="filename-input"
th:placeholder="#{multiTool.uploadPrompts}">
</div>
<div class="mt-file-uploader">
<div
th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, accept='image/*, application/pdf', showUploads=false)}">
</div>
</div>
<div id="selected-pages-display" class="selected-pages-container hidden">
<div class="selected-pages-header">
<h5 th:text="#{multiTool.selectedPages}">Selected Pages</h5>
<input type="text" id="csv-input" class="form-control" placeholder="1,3,5-10" value="">
</div>
<ul id="selected-pages-list" class="pages-list"></ul>
</div>
</div>
<div class="multi-tool-container">
<div id="pages-container-wrapper">
<div id="pages-container">
</div>
</div>
</div>
<div class="mt-action-btn">
<button id="undo-btn" th:title="#{multiTool.undo}" class="btn btn-secondary" onclick="undo()"
disabled>
<span class="material-symbols-rounded">
undo
</span>
</button>
<button id="redo-btn" class="btn btn-secondary" th:title="#{multiTool.redo}" onclick="redo()"
disabled>
<span class="material-symbols-rounded">
redo
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.rotateLeft}"
onclick="rotateAll(-90)" disabled>
<span class="material-symbols-rounded">
rotate_left
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.rotateRight}"
onclick="rotateAll(90)" disabled>
<span class="material-symbols-rounded">
rotate_right
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.split}" onclick="splitAll()"
disabled>
<span class="material-symbols-rounded">
cut
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.insertPageBreak}"
onclick="addFilesBlankAll()" disabled>
<span class="material-symbols-rounded">
insert_page_break
</span>
</button>
<button id="select-pages-container" th:title="#{multiTool.selectPages}"
class="btn btn-secondary enable-on-file" onclick="toggleSelectPageVisibility()" disabled>
<span id="select-pages-button" class="material-symbols-rounded">
event_list
</span>
</button>
<button id="deselect-All-Container" th:title="#{multiTool.deselectAll}"
class="btn btn-secondary enable-on-file hidden" onclick="deselectAll()" disabled>
<span class="material-symbols-rounded" id="deselect-icon">deselect</span>
</button>
<button id="select-All-Container" th:title="#{multiTool.selectAll}"
class="btn btn-secondary enable-on-file hidden" onclick="selectAll()" disabled>
<span class="material-symbols-rounded" id="select-icon">select_all</span>
</button>
<button id="delete-button" th:title="#{multiTool.deleteSelected}"
class="btn btn-danger delete hidden" onclick="deleteSelected()">
<span class="material-symbols-rounded">delete</span>
</button>
<button id="export-selected-button" th:title="#{multiTool.downloadSelected}"
style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)"
class="btn btn-primary enable-on-file hidden" onclick="exportPdf(true)" disabled>
<span class="material-symbols-rounded">
file_save
</span>
</button>
<button style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)"
th:title="#{multiTool.downloadAll}" id="export-button" class="btn btn-primary enable-on-file"
onclick="exportPdf(false)" disabled>
<span class="material-symbols-rounded">
download
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="drag-container"></div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script>
window.selectedPages = [];
window.selectPage = false;
window.selectAll = false;
window.translations = {
rotateLeft: '[[#{multiTool.rotateLeft}]]',
rotateRight: '[[#{multiTool.rotateRight}]]',
moveLeft: '[[#{multiTool.moveLeft}]]',
moveRight: '[[#{multiTool.moveRight}]]',
delete: '[[#{multiTool.delete}]]',
split: '[[#{multiTool.split}]]',
addFile: '[[#{multiTool.addFile}]]',
insertPageBreak: '[[#{multiTool.insertPageBreak}]]',
dragDropMessage: '[[#{multiTool.dragDropMessage}]]',
undo: '[[#{multiTool.undo}]]',
redo: '[[#{multiTool.redo}]]',
};
window.decrypt = {
passwordPrompt: '[[#{decrypt.passwordPrompt}]]',
cancelled: '[[#{decrypt.cancelled}]]',
noPassword: '[[#{decrypt.noPassword}]]',
invalidPassword: '[[#{decrypt.invalidPassword}]]',
invalidPasswordHeader: '[[#{decrypt.invalidPasswordHeader}]]',
unexpectedError: '[[#{decrypt.unexpectedError}]]',
serverError: '[[#{decrypt.serverError}]]',
success: '[[#{decrypt.success}]]',
}
window.corrupt = {
pdfInvalid: '[[#{error.pdfInvalid}]]',
tryRepair: '[[#{error.tryRepair}]]',
}
const csvInput = document.getElementById("csv-input");
csvInput.addEventListener("keydown", function (event) {
if (event.key === "Enter") {
updatePagesFromCSV();
}
});
csvInput.addEventListener("blur", function () {
updatePagesFromCSV();
});
</script>
<script type="module">
import { UndoManager } from './js/multitool/UndoManager.js';
import PdfContainer from './js/multitool/PdfContainer.js';
import DragDropManager from "./js/multitool/DragDropManager.js";
import ImageHighlighter from "./js/multitool/ImageHighlighter.js";
import PdfActionsManager from './js/multitool/PdfActionsManager.js';
// enables drag and drop
const pdfUpload = document.querySelector("input[name=pdf-upload]");
pdfUpload.addEventListener("change", async (e) => {
if (!e.target.files) return;
await pdfContainer.handleDroppedFiles( e.target.files);
e.target.dispatchEvent(new CustomEvent('reset', {}));
});
var undoManager = new UndoManager();
const dragDropManager = new DragDropManager('drag-container', 'pages-container');
// enables image highlight on click
const imageHighlighter = new ImageHighlighter('image-highlighter');
// enables the default action buttons on each file
const pdfActionsManager = new PdfActionsManager('pages-container', undoManager);
// Scroll the wrapper horizontally
// Automatically exposes rotateAll, addFiles and exportPdf to the window for the global buttons.
const pdfContainer = new PdfContainer(
'pages-container',
'pages-container-wrapper',
[
dragDropManager,
imageHighlighter,
pdfActionsManager
],
undoManager
)
document.addEventListener('keydown', function (event) {
let targetElementId = event.target.id;
// To avoid undoing/redoing the page when the user is simply undoing/redoing text
const isFilenameInputField = (targetElementId === 'filename-input') && (event.target === document.activeElement);
const isUndo = (event.ctrlKey && event.key === 'z');
const isRedo = (event.ctrlKey && event.key == 'y');
if (isUndo && !isFilenameInputField)
undoManager.undo();
else if (isRedo && !isFilenameInputField) undoManager.redo();
});
</script>
</body>
</html>
@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{split-by-size-or-count.title}, header=#{split-by-size-or-count.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 advance">layers</span>
<span class="tool-header-text" th:text="#{overlay-pdfs.header}"></span>
</div>
<form id="overlayForm" method="post" enctype="multipart/form-data" th:action="@{'/api/v1/general/overlay-pdfs'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<div th:replace="~{fragments/common :: fileSelector(name='overlayFiles', multipleInputsForSingleRequest=true, accept='application/pdf')}"></div>
<label for="overlayMode" th:text="#{overlay-pdfs.mode.label}">Overlay Mode</label>
<select id="overlayMode" name="overlayMode" class="form-control">
<option value="SequentialOverlay" th:text="#{overlay-pdfs.mode.sequential}">Sequential Overlay</option>
<option value="InterleavedOverlay" th:text="#{overlay-pdfs.mode.interleaved}">Interleaved Overlay</option>
<option value="FixedRepeatOverlay" th:text="#{overlay-pdfs.mode.fixedRepeat}">Fixed Repeat Overlay</option>
</select>
<br>
<label for="overlayPosition" th:text="#{overlay-pdfs.position.label}">Overlay Position</label>
<select id="overlayPosition" name="overlayPosition" class="form-control">
<option value="0" th:text="#{overlay-pdfs.position.foreground}">Foreground</option>
<option value="1" th:text="#{overlay-pdfs.position.background}">Background</option>
</select>
<div id="countsContainer" style="display: none;">
<label th:text="#{overlay-pdfs.counts.label}">Overlay Counts</label>
<!-- Inputs for counts will be dynamically added here -->
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{overlay-pdfs.submit}">Submit</button>
</form>
<script>
function updateCountsInputs() {
const mode = document.getElementById('overlayMode').value;
console.log("mode",mode);
const countsContainer = document.getElementById('countsContainer');
console.log("countsContainer",countsContainer);
countsContainer.innerHTML = ''; // Clear previous inputs
if (mode === 'FixedRepeatOverlay') {
const fileInput = document.getElementById('overlayFiles-input');
console.log("fileInput",fileInput);
if(fileInput){
const files = fileInput.files
console.log("files",files);
if(files) {
const fileCount = files.length;
countsContainer.appendChild(document.createElement("br"));
const label = document.createElement('label');
label.setAttribute('th:text', '#{overlay-pdfs.counts.label}');
label.textContent = "Overlay Counts";
countsContainer.appendChild(label);
for (let i = 0; i < fileCount; i++) {
const input = document.createElement('input');
input.type = 'text';
input.name = 'counts';
input.classList.add('form-control');
input.placeholder = 'Count for file ' + (i + 1);
countsContainer.appendChild(input);
}
countsContainer.style.display = 'block';
}
}
} else {
countsContainer.style.display = 'none';
}
}
document.addEventListener('DOMContentLoaded', (event) => {
var fileInput = document.getElementById('overlayFiles-input');
console.log("fileInput2",fileInput);
if (fileInput) {
fileInput.addEventListener('change', updateCountsInputs);
}
});
document.addEventListener('DOMContentLoaded', (event) => {
var overlay = document.getElementById('overlayMode');
console.log("overlay2",overlay);
if (overlay) {
overlay.addEventListener('change', updateCountsInputs);
}
});
</script>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{pdfOrganiser.title}, header=#{pdfOrganiser.header})}">
</th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<th:block th:insert="~{fragments/multi-toolAdvert.html :: multi-toolAdvert}"></th:block>
<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 organize">format_list_bulleted</span>
<span class="tool-header-text" th:text="#{pdfOrganiser.header}"></span>
</div>
<form th:action="@{'/api/v1/general/rearrange-pages'}" method="post" enctype="multipart/form-data">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<div class="mb-3">
<label for="customMode" th:text="#{pdfOrganiser.mode}">Mode</label>
<select class="form-control" id="customMode" name="customMode">
<option value="" th:text="#{pdfOrganiser.mode.1}">Custom Page Order</option>
<option value="REVERSE_ORDER" th:text="#{pdfOrganiser.mode.2}">Reverse Order</option>
<option value="DUPLEX_SORT" th:text="#{pdfOrganiser.mode.3}">Duplex Sort</option>
<option value="BOOKLET_SORT" th:text="#{pdfOrganiser.mode.4}">Booklet Sort</option>
<option value="SIDE_STITCH_BOOKLET_SORT" th:text="#{pdfOrganiser.mode.5}">Side Stitch Booklet Sort
</option>
<option value="ODD_EVEN_SPLIT" th:text="#{pdfOrganiser.mode.6}">Odd-Even Split</option>
<option value="ODD_EVEN_MERGE" th:text="#{pdfOrganiser.mode.10}">Odd-Even Merge</option>
<option value="DUPLICATE" th:text="#{pdfOrganiser.mode.11}">Duplicate all pages</option>
<option value="REMOVE_FIRST" th:text="#{pdfOrganiser.mode.7}">Remove First</option>
<option value="REMOVE_LAST" th:text="#{pdfOrganiser.mode.8}">Remove Last</option>
<option value="REMOVE_FIRST_AND_LAST" th:text="#{pdfOrganiser.mode.9}">Remove First and Last</option>
</select>
</div>
<div class="mb-3">
<label for="pageOrder" th:text="#{pageOrderPrompt}"></label>
<input type="text" class="form-control" id="pageOrder" name="pageNumbers"
th:placeholder="#{pdfOrganiser.placeholder}" required>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfOrganiser.submit}"></button>
</form>
<script>
document.getElementById('customMode').addEventListener('change', function () {
var pageOrderInput = document.getElementById('pageOrder');
if (this.value === "" || this.value === "DUPLICATE") {
pageOrderInput.disabled = false;
} else {
pageOrderInput.disabled = true;
}
});
</script>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{pdfToSinglePage.title}, header=#{pdfToSinglePage.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 organize">looks_one</span>
<span class="tool-header-text" th:text="#{pdfToSinglePage.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/general/pdf-to-single-page'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pdfToSinglePage.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html
th:lang="${#locale.language}"
th:dir="#{language.direction}"
th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org"
>
<head>
<th:block
th:insert="~{fragments/common :: head(title=#{pipeline.title}, header=#{pipeline.header})}"
></th:block>
<link
rel="stylesheet"
th:href="@{'/css/pipeline.css'}"
th:if="${currentPage == 'pipeline'}"
/>
<script th:inline="javascript">
const saveSettings = /*[[#{pipelineOptions.saveSettings}]]*/ "";
const deletePipelineText = /*[[#{pipeline.deletePrompt}]]*/ "Are you sure you want to delete pipeline";
</script>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 advance">family_history</span>
<span class="tool-header-text" th:text="#{pipeline.header}"></span>
</div>
<div class="text-end text-top">
<button
id="uploadPipelineBtn"
class="btn btn-primary"
th:text="#{pipeline.uploadButton}"
></button>
<button
type="button"
class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target="#pipelineSettingsModal"
th:text="#{pipeline.configureButton}"
></button>
</div>
<div class="center-element">
<div class="element-margin">
<select id="pipelineSelect" class="custom-select form-control">
<option
value='{"name":"Custom","pipeline":[],"_examples":{"outputDir":"{outputFolder}/{folderName}","outputFileName":"{filename}-{pipelineName}-{date}-{time}"},"outputDir":"{outputFolder}","outputFileName":"{filename}"}'
th:text="#{pipeline.defaultOption}"
></option>
<th:block th:each="config : ${pipelineConfigsWithNames}">
<option
th:value="${config.json}"
th:text="${config.name}"
></option>
</th:block>
</select>
</div>
<div class="element-margin">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=true, accept='*/*')}"
></div>
</div>
<div class="element-margin text-start">
<button
class="btn btn-primary"
id="submitConfigBtn"
th:text="#{pipeline.submitButton}"
></button>
</div>
<br/>
<a
href="https://docs.stirlingpdf.com/Advanced%20Configuration/Pipeline"
target="_blank"
th:text="#{pipeline.help}"
>Pipeline Help</a
>
<br/>
<a
href="https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning"
target="_blank"
th:text="#{pipeline.scanHelp}"
>Folder Scanning Help</a
>
</div>
</div>
<!-- The Modal -->
<div class="modal" id="pipelineSettingsModal">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content dark-card">
<!-- Modal Header -->
<div class="modal-header">
<h2
class="modal-title"
th:text="#{pipelineOptions.header}"
></h2>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close">
<span class="material-symbols-rounded">
close
</span>
</button>
</div>
<!-- Modal body -->
<div class="modal-body">
<div class="mb-3">
<label
for="pipelineName"
class="form-label"
th:text="#{pipelineOptions.pipelineNameLabel}"
></label>
<input
type="text"
id="pipelineName"
class="form-control"
th:placeholder="#{pipelineOptions.pipelineNamePrompt}"
/>
</div>
<div class="mb-3">
<label
for="operationsDropdown"
th:text="#{pipelineOptions.selectOperation}"
></label>
<select id="operationsDropdown" class="form-select">
<!-- Options will be dynamically populated here -->
</select>
</div>
<div class="mb-3">
<button
id="addOperationBtn"
class="btn btn-primary"
th:text="#{pipelineOptions.addOperationButton}"
></button>
</div>
<h3
id="pipelineHeader"
style="display: none"
th:text="#{pipelineOptions.pipelineHeader}"
></h3>
<ol id="pipelineList" class="list-group">
<!-- Pipeline operations will be dynamically populated here -->
</ol>
<div id="pipelineSettingsContent">
<!-- pipelineSettings will be dynamically populated here -->
</div>
</div>
<!-- Modal footer -->
<div class="modal-footer">
<button
id="deletePipelineBtn"
class="btn btn-danger"
th:text="#{delete}"
></button>
<button
id="saveBrowserPipelineBtn"
class="btn btn-success"
th:text="#{saveToBrowser}"
></button>
<button
id="savePipelineBtn"
class="btn btn-success"
th:text="#{pipelineOptions.saveButton}"
></button>
<button
id="validateButton"
class="btn btn-success"
th:text="#{pipelineOptions.validateButton}"
></button>
<div class="btn-group">
<input
type="file"
id="uploadPipelineInput"
accept=".json"
style="display: none"
/>
</div>
</div>
</div>
</div>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<script th:src="@{'/js/pipeline.js'}"></script>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,511 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{releases.title}, header=#{releases.title})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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-8 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon history">update</span>
<span class="tool-header-text" th:text="#{releases.header}">Release Notes</span>
</div>
<div class="alert alert-info" role="alert">
<strong th:text="#{releases.current.version}">Current Installed Version</strong>:
<span id="currentVersion" th:text="${@appVersion}"></span>
</div>
<div class="alert alert-warning" role="alert">
<span th:text="#{releases.note}">All release notes are only available in English</span>
</div>
<div id="loading" class="text-center my-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div id="error-message" class="alert alert-danger d-none" role="alert">
Failed to load release notes. Please try again later.
</div>
<!-- Release Notes Container -->
<div id="release-notes-container" class="release-notes-container">
<!-- Release notes will be dynamically inserted here -->
</div>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<style>
.release-notes-container {
margin-top: 2rem;
}
.release-card {
border: 1px solid #dee2e6;
border-radius: 0.25rem;
margin-bottom: 1.5rem;
padding: 1rem;
}
.release-card.current-version {
border-color: #28a745;
background-color: rgba(40, 167, 69, 0.05);
}
.release-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.release-header h3 {
margin: 0;
display: flex;
gap: 1rem;
align-items: center;
}
.version {
font-weight: bold;
}
.release-date {
color: #6c757d;
font-size: 0.9em;
}
.release-body {
font-size: 0.9rem;
white-space: pre-wrap;
word-break: break-word;
}
.release-link {
color: #0d6efd;
text-decoration: none;
}
.release-link:hover {
text-decoration: underline;
}
</style>
<script th:inline="javascript">
/*<![CDATA[*/
// Get the current version from the appVersion bean
const appVersion = /*[[${@appVersion}]]*/ '';
// GitHub API configuration
const REPO_OWNER = 'Stirling-Tools';
const REPO_NAME = 'Stirling-PDF';
const GITHUB_API = 'https://api.github.com/repos/' + REPO_OWNER + '/' + REPO_NAME;
const GITHUB_URL = 'https://github.com/' + REPO_OWNER + '/' + REPO_NAME;
const MAX_RELEASES = 8;
// Secure element creation helper
function createElement(tag, attributes = {}, children = []) {
const element = document.createElement(tag);
Object.entries(attributes).forEach(([key, value]) => {
if (typeof value === 'string') {
element.setAttribute(key, value);
}
});
children.forEach(child => {
if (typeof child === 'string') {
element.appendChild(document.createTextNode(child));
} else if (child instanceof Node) {
element.appendChild(child);
}
});
return element;
}
const ALLOWED_TAGS = {
'a': ['href', 'target', 'rel', 'class'],
'img': ['src', 'alt', 'width', 'height', 'style'],
'br': [],
'p': [],
'div': [],
'span': []
};
// Function to safely create HTML elements from string
function createSafeElement(htmlString) {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
function sanitizeNode(node) {
// Safety check for null/undefined
if (!node) return null;
// Handle text nodes
if (node.nodeType === Node.TEXT_NODE) {
return node.cloneNode(true);
}
// Handle element nodes
if (node.nodeType === Node.ELEMENT_NODE) {
const tagName = node.tagName.toLowerCase();
// Check if tag is allowed
if (!ALLOWED_TAGS[tagName]) {
return document.createTextNode(node.textContent);
}
// Create new element
const cleanElement = document.createElement(tagName);
// Copy allowed attributes
const allowedAttributes = ALLOWED_TAGS[tagName];
Array.from(node.attributes).forEach(attr => {
if (allowedAttributes.includes(attr.name)) {
let value = attr.value;
if (attr.name === 'href' || attr.name === 'src') {
try {
value = encodeURI(value);
} catch {
return;
}
}
cleanElement.setAttribute(attr.name, value);
}
});
// Add security attributes for links
if (tagName === 'a') {
cleanElement.setAttribute('rel', 'noopener noreferrer');
}
// Process children
Array.from(node.childNodes).forEach(child => {
const cleanChild = sanitizeNode(child);
if (cleanChild) {
cleanElement.appendChild(cleanChild);
}
});
return cleanElement;
}
// If not text or element node, return null
return null;
}
// Get the actual content from the body
const content = doc.body.children;
if (!content || content.length === 0) {
return null;
}
// If it's a single element, process it directly
if (content.length === 1) {
return sanitizeNode(content[0]);
}
// If multiple elements, wrap them in a div
const wrapper = document.createElement('div');
Array.from(content).forEach(node => {
const cleanNode = sanitizeNode(node);
if (cleanNode) {
wrapper.appendChild(cleanNode);
}
});
return wrapper;
} catch (error) {
console.error('Error parsing HTML:', error);
return null;
}
}
function processGitHubReferences(text) {
if (!text || typeof text !== 'string') {
return text;
}
let currentText = text;
let match;
let lastIndex = 0;
const result = document.createElement('span');
const urlRegex = new RegExp('https://github\\.com/' + REPO_OWNER + '/' + REPO_NAME + '/(?:issues|pull)/(\\d+)', 'g');
while ((match = urlRegex.exec(text)) !== null) {
// Add text before the match
if (match.index > lastIndex) {
result.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
}
// Create link element
const link = document.createElement('a');
link.href = encodeURI(match[0]);
link.textContent = `#${match[1]}`; // Use issue/PR number
link.className = 'release-link';
link.target = '_blank';
link.rel = 'noopener noreferrer';
result.appendChild(link);
lastIndex = match.index + match[0].length;
}
// Add remaining text after last match
if (lastIndex < text.length) {
result.appendChild(document.createTextNode(text.substring(lastIndex)));
}
return result;
}
// Update formatText function to handle processGitHubReferences properly
function formatText(text) {
const container = document.createElement('div');
if (!text || typeof text !== 'string') {
console.error('Invalid input to formatText:', text);
return container;
}
let textWithoutComments;
try {
textWithoutComments = text.replace(
/<!-- Release notes generated using configuration in .github\/release\.yml at main -->/,
''
);
} catch (error) {
console.error('Error in replace operation:', error);
return container;
}
// Split the text into lines
let lines;
try {
lines = textWithoutComments.split('\n');
} catch (error) {
console.error('Error in split operation:', error);
return container;
}
let currentList = null;
lines.forEach(line => {
const trimmedLine = line.trim();
// Skip empty lines but add spacing
if (!trimmedLine) {
if (currentList) {
container.appendChild(currentList);
currentList = null;
}
container.appendChild(document.createElement('br'));
return;
}
// Check if the line is HTML
if (trimmedLine.startsWith('<') && trimmedLine.endsWith('>')) {
if (currentList) {
container.appendChild(currentList);
currentList = null;
}
const safeElement = createSafeElement(trimmedLine);
if (safeElement) {
container.appendChild(safeElement);
} else {
// If HTML parsing fails, treat as plain text
container.appendChild(document.createTextNode(trimmedLine));
}
return;
}
// Check for headers
const headerMatch = trimmedLine.match(/^(#{1,3})\s+(.+)$/);
if (headerMatch) {
if (currentList) {
container.appendChild(currentList);
currentList = null;
}
const headerLevel = headerMatch[1].length;
const headerContent = headerMatch[2];
// Process GitHub references in headers
const processedContent = processGitHubReferences(headerContent);
const header = createElement(`h${headerLevel}`);
header.appendChild(processedContent);
container.appendChild(header);
return;
}
// Check for bullet points
const bulletMatch = trimmedLine.match(/^[-*]\s+(.+)$/);
if (bulletMatch) {
if (!currentList) {
currentList = document.createElement('ul');
}
const listContent = bulletMatch[1];
const listItem = document.createElement('li');
// Process GitHub references in list items
listItem.appendChild(processGitHubReferences(listContent));
currentList.appendChild(listItem);
return;
}
// If we reach here and have a list, append it
if (currentList) {
container.appendChild(currentList);
currentList = null;
}
// Handle regular paragraph
const paragraph = document.createElement('p');
paragraph.appendChild(processGitHubReferences(trimmedLine));
container.appendChild(paragraph);
});
// Append any remaining list
if (currentList) {
container.appendChild(currentList);
}
return container;
}
const MAX_PREVIOUS_RELEASES = 5;
function compareVersions(v1, v2) {
const normalize = v => v.replace(/^v/, '');
const v1Parts = normalize(v1).split('.').map(Number);
const v2Parts = normalize(v2).split('.').map(Number);
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
const v1Part = v1Parts[i] || 0;
const v2Part = v2Parts[i] || 0;
if (v1Part > v2Part) return 1;
if (v1Part < v2Part) return -1;
}
return 0;
}
async function loadReleases() {
const container = document.getElementById('release-notes-container');
const loading = document.getElementById('loading');
const errorMessage = document.getElementById('error-message');
try {
loading.classList.remove('d-none');
errorMessage.classList.add('d-none');
// Clear container safely
while (container.firstChild) {
container.removeChild(container.firstChild);
}
const cachedReleases = sessionStorage.getItem('releases');
let releases;
if (cachedReleases) {
releases = JSON.parse(cachedReleases);
console.log("Read from storage");
} else {
const response = await fetch(GITHUB_API + '/releases');
if (!response.ok) {
if (response.status === 403) {
throw new Error('API rate limit exceeded');
}
if (response.status === 404) {
throw new Error('Repository not found');
}
throw new Error('Failed to fetch releases');
}
releases = await response.json();
sessionStorage.setItem('releases', JSON.stringify(releases));
}
// Sort releases by version number (descending)
releases.sort((a, b) => compareVersions(b.tag_name, a.tag_name));
// Find index of current version
const currentVersionIndex = releases.findIndex(release =>
compareVersions(release.tag_name, 'v' + appVersion) === 0 ||
compareVersions(release.tag_name, appVersion) === 0
);
if (currentVersionIndex === -1) {
container.appendChild(createElement('div', {
class: 'alert alert-warning'
}, ['Current version not found in releases.']));
return;
}
// Get current version and 8 previous releases
const endIndex = Math.min(currentVersionIndex + MAX_PREVIOUS_RELEASES + 1, releases.length);
const relevantReleases = releases.slice(currentVersionIndex, endIndex);
if (relevantReleases.length === 0) {
container.appendChild(createElement('div', {
class: 'alert alert-warning'
}, ['No releases found.']));
return;
}
relevantReleases.forEach((release, index) => {
const isCurrentVersion = index === 0; // First release in the array is current version
const releaseCard = createElement('div', {
class: `release-card ${isCurrentVersion ? 'current-version' : ''}`
});
const header = createElement('div', { class: 'release-header' });
const h3 = createElement('h3', {}, [
createElement('span', { class: 'version' }, [release.tag_name]),
createElement('span', { class: 'release-date' }, [
new Date(release.created_at).toLocaleDateString()
])
]);
header.appendChild(h3);
if (isCurrentVersion) {
header.appendChild(createElement('span', {
class: 'badge bg-success'
}, ['Installed']));
}
releaseCard.appendChild(header);
const body = createElement('div', { class: 'release-body' });
body.appendChild(formatText(release.body || 'No release notes available.'));
releaseCard.appendChild(body);
container.appendChild(releaseCard);
});
} catch (error) {
console.error('Error loading releases:', error);
errorMessage.classList.remove('d-none');
} finally {
loading.classList.add('d-none');
}
}
// Load releases when the page loads
document.addEventListener('DOMContentLoaded', loadReleases);
/*]]>*/
</script>
</body>
</html>
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{removeImage.title}, header=#{removeImage.header})}"></th:block>
<link rel="stylesheet" th:href="@{'/css/removeImage.css'}">
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 word">remove_selection</span>
<span class="tool-header-text" th:text="#{removeImage.header}"></span>
</div>
<form th:action="@{'api/v1/general/remove-image-pdf'}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removeImage.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{pageRemover.title}, header=#{pageRemover.header})}">
</th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<th:block th:insert="~{fragments/multi-toolAdvert.html :: multi-toolAdvert}"></th:block>
<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 organize">delete</span>
<span class="tool-header-text" th:text="#{pageRemover.header}"></span>
</div>
<form th:action="@{'/api/v1/general/remove-pages'}" method="post" enctype="multipart/form-data">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<div class="mb-3">
<label for="fileInput" th:text="#{pageRemover.pagesToDelete}"></label>
<input type="text" class="form-control" id="fileInput" name="pageNumbers"
th:placeholder="#{pageRemover.placeholder}" required>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pageRemover.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script>
document.getElementById('fileInput').addEventListener('input', function () {
this.value = this.value.replace(/\s+/g, '');;
});
</script>
</body>
</html>
@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{rotate.title}, header=#{rotate.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<th:block th:insert="~{fragments/multi-toolAdvert.html :: multi-toolAdvert}"></th:block>
<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 organize">rotate_right</span>
<span class="tool-header-text" th:text="#{rotate.header}"></span>
</div>
<form action="#" th:action="@{'/api/v1/general/rotate-pdf'}" th:object="${rotateForm}" method="post"
enctype="multipart/form-data">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<input type="hidden" id="angleInput" name="angle" value="0">
<div id="editSection" style="display: none">
<div id="previewContainer">
<!-- pdf-preview -->
</div>
<div class="buttonContainer">
<button type="button" class="btn btn-secondary" onclick="rotate(-90)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z" />
<path
d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z" />
</svg>
</button>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{rotate.submit}"></button>
<button type="button" class="btn btn-secondary" onclick="rotate(90)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
<path
d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
</svg>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script>
const angleInput = document.getElementById("angleInput");
const fileInput = document.getElementById("fileInput-input");
const previewContainer = document.getElementById("previewContainer");
// const preview = document.getElementById("pdf-preview");
fileInput.addEventListener("change", async function () {
console.log("loading pdf");
document.querySelector("#editSection").style.display = "block";
const existingPreview = document.getElementById("pdf-preview");
if (existingPreview) {
existingPreview.remove();
}
var url = URL.createObjectURL(fileInput.files[0])
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
const pdf = await pdfjsLib.getDocument(url).promise;
const page = await pdf.getPage(1);
const canvas = document.createElement("canvas");
// set the canvas size to the size of the page
if (page.rotate == 90 || page.rotate == 270) {
canvas.width = page.view[3];
canvas.height = page.view[2];
} else {
canvas.width = page.view[2];
canvas.height = page.view[3];
}
// render the page onto the canvas
var renderContext = {
canvasContext: canvas.getContext("2d"),
viewport: page.getViewport({ scale: 1 })
};
await page.render(renderContext).promise;
const preview = document.createElement("img");
preview.id = "pdf-preview";
preview.alt = "preview";
preview.src = canvas.toDataURL();
previewContainer.appendChild(preview);
});
function rotate(deg) {
const preview = document.getElementById("pdf-preview");
var lastTransform = preview.style.rotate;
if (!lastTransform) {
lastTransform = "0";
}
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ''));
const newAngle = lastAngle + deg;
preview.style.rotate = newAngle + "deg";
angleInput.value = newAngle;
}
</script>
</body>
</html>
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{scalePages.title}, header=#{scalePages.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 organize">fullscreen</span>
<span class="tool-header-text" th:text="#{scalePages.header}"></span>
</div>
<form id="scalePagesFrom" th:action="@{'/api/v1/general/scale-pages'}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<div class="mb-3">
<label for="pageSize" th:text="#{scalePages.pageSize}"></label>
<select class="form-control" id="pageSize" name="pageSize">
<option value="KEEP" th:text="#{scalePages.keepPageSize}" selected></option>
<option value="A0">A0</option>
<option value="A1">A1</option>
<option value="A2">A2</option>
<option value="A3">A3</option>
<option value="A4">A4</option>
<option value="A5">A5</option>
<option value="A6">A6</option>
<option value="LETTER">Letter</option>
<option value="LEGAL">Legal</option>
</select>
</div>
<div class="mb-3">
<label for="scaleFactor" th:text="#{scalePages.scaleFactor}"></label>
<input class="form-control" type="number" id="scaleFactor" name="scaleFactor" step="any" min="0" value="1">
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{scalePages.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{addPassword.title}, header=#{addPassword.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 security">lock</span>
<span class="tool-header-text" th:text="#{addPassword.header}"></span>
</div>
<form th:action="@{'api/v1/security/add-password'}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label th:text="#{addPassword.selectText.1}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
</div>
<div class="mb-3">
<label th:text="#{addPassword.selectText.14}"></label> <input type="password" class="form-control" id="ownerPassword" name="ownerPassword">
<small class="form-text text-muted" th:text="#{addPassword.selectText.15}"></small>
</div>
<div class="mb-3">
<label th:text="#{addPassword.selectText.2}"></label> <input type="password" class="form-control" id="password" name="password">
<small class="form-text text-muted" th:text="#{addPassword.selectText.16}"></small>
</div>
<div class="mb-3">
<label th:text="#{addPassword.selectText.3}"></label> <select class="form-control" id="keyLength" name="keyLength">
<option value="40">40</option>
<option value="128" selected>128</option>
<option value="256">256</option>
</select>
<small class="form-text text-muted" th:text="#{addPassword.selectText.4}"></small>
</div>
<div class="mb-3">
<label class="mb-2" th:text="#{addPassword.selectText.5}"></label>
<div class="form-check ms-3">
<input type="checkbox" id="preventAssembly" name="preventAssembly">
<label for="preventAssembly" th:text="#{addPassword.selectText.6}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="preventExtractContent" name="preventExtractContent">
<label for="preventExtractContent" th:text="#{addPassword.selectText.7}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="preventExtractForAccessibility" name="preventExtractForAccessibility">
<label for="preventExtractForAccessibility" th:text="#{addPassword.selectText.8}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="preventFillInForm" name="preventFillInForm">
<label for="preventFillInForm" th:text="#{addPassword.selectText.9}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="preventModify" name="preventModify">
<label for="preventModify" th:text="#{addPassword.selectText.10}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="preventModifyAnnotations" name="preventModifyAnnotations">
<label for="preventModifyAnnotations" th:text="#{addPassword.selectText.11}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="preventPrinting" name="preventPrinting">
<label for="preventPrinting" th:text="#{addPassword.selectText.12}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="preventPrintingFaithful" name="preventPrintingFaithful">
<label for="preventPrintingFaithful" th:text="#{addPassword.selectText.13}"></label>
</div>
</div>
<br>
<div class="mb-3 text-left">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{addPassword.submit}"></button>
</div>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{watermark.title}, header=#{watermark.header})}"></th:block>
</head>
<body onload="toggleFileOption()">
<div id="page-container">
<div id="content-wrap">
<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 security">water_drop</span>
<span class="tool-header-text" th:text="#{watermark.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'api/v1/security/add-watermark'}">
<div class="mb-3">
<label th:text="#{watermark.selectText.1}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
</div>
<div class="mb-3">
<label th:text="#{watermark.selectText.8}"></label>
<select class="form-control" id="watermarkType" name="watermarkType" onchange="toggleFileOption()">
<option value="text" th:text="#{watermark.type.1}"></option>
<option value="image" th:text="#{watermark.type.2}"></option>
</select>
</div>
<div id="alphabetGroup" class="mb-3">
<label for="fontSize" th:text="#{alphabet} + ':'"></label>
<select class="form-control" name="alphabet" id="alphabet-select">
<option value="roman">Roman</option>
<option value="arabic">العربية</option>
<option value="japanese">日本語</option>
<option value="korean">한국어</option>
<option value="chinese">简体中文</option>
<option value="thai">ไทย</option>
</select>
</div>
<div id="watermarkTextGroup" class="mb-3">
<label for="watermarkText" th:text="#{watermark.selectText.2}"></label>
<input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required>
</div>
<div id="watermarkImageGroup" class="mb-3" style="display: none;">
<label for="watermarkImage" th:text="#{watermark.selectText.9}"></label>
<input type="file" id="watermarkImage" name="watermarkImage" class="form-control-file" accept="image/*">
</div>
<div class="mb-3">
<label for="fontSize" th:text="#{watermark.selectText.3}"></label>
<input type="text" id="fontSize" name="fontSize" class="form-control" value="30">
</div>
<div class="mb-3">
<label for="opacity" th:text="#{watermark.selectText.7}"></label>
<input type="text" id="opacity" name="opacityText" class="form-control" value="50" onblur="updateOpacityValue()">
<input type="hidden" id="opacityReal" name="opacity" value="0.5">
</div>
<script>
const opacityInput = document.getElementById('opacity');
const opacityRealInput = document.getElementById('opacityReal');
const updateOpacityValue = () => {
let percentageValue = parseFloat(opacityInput.value.replace('%', ''));
if (isNaN(percentageValue)) {
percentageValue = 0;
}
percentageValue = Math.min(Math.max(percentageValue, 0), 100);
opacityInput.value = `${percentageValue}`;
opacityRealInput.value = (percentageValue / 100).toFixed(2);
};
const appendPercentageSymbol = () => {
if (!opacityInput.value.endsWith('%')) {
opacityInput.value += '%';
}
};
opacityInput.addEventListener('focus', () => {
opacityInput.value = opacityInput.value.replace('%', '');
});
opacityInput.addEventListener('blur', () => {
updateOpacityValue();
appendPercentageSymbol();
});
// Set initial values
updateOpacityValue();
appendPercentageSymbol();
</script>
<div class="mb-3">
<label for="rotation" th:text="#{watermark.selectText.4}"></label>
<input type="text" id="rotation" name="rotation" class="form-control" value="45">
</div>
<div class="mb-3">
<label for="widthSpacer" th:text="#{watermark.selectText.5}"></label>
<input type="text" id="widthSpacer" name="widthSpacer" class="form-control" value="50">
</div>
<div class="mb-3">
<label for="heightSpacer" th:text="#{watermark.selectText.6}"></label>
<input type="text" id="heightSpacer" name="heightSpacer" class="form-control" value="50">
</div>
<div class="mb-3">
<label for="customColor" class="form-label" th:text="#{watermark.customColor}">Custom
Color</label>
<div class="form-control form-control-color" style="background-color: #d3d3d3;">
<input type="color" id="customColor" name="customColor" value="#d3d3d3">
</div>
<script>
let colorInput = document.getElementById("customColor");
if (colorInput) {
let colorInputContainer = colorInput.parentElement;
if (colorInputContainer) {
colorInput.onchange = function() {
colorInputContainer.style.backgroundColor = colorInput.value;
}
colorInputContainer.style.backgroundColor = colorInput.value;
}
}
</script>
</div>
<div class="mb-3 form-check">
<input type="checkbox" id="convertPDFToImage" name="convertPDFToImage">
<label for="convertPDFToImage" th:text="#{watermark.selectText.10}"></label>
</div>
<div class="mb-3 text-left">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{watermark.submit}"></button>
</div>
</form>
<script>
function toggleFileOption() {
const watermarkType = document.getElementById('watermarkType').value;
const watermarkTextGroup = document.getElementById('watermarkTextGroup');
const watermarkImageGroup = document.getElementById('watermarkImageGroup');
const alphabetGroup = document.getElementById('alphabetGroup'); // This is the new addition
const watermarkText = document.getElementById('watermarkText');
const watermarkImage = document.getElementById('watermarkImage');
if (watermarkType === 'text') {
if (watermarkImage.hasAttribute('required')) {
watermarkImage.removeAttribute('required');
}
watermarkTextGroup.style.display = 'block';
watermarkText.required = true;
watermarkImageGroup.style.display = 'none';
watermarkImage.required = false;
alphabetGroup.style.display = 'block';
} else if (watermarkType === 'image') {
if (watermarkImage.hasAttribute('required')) {
watermarkImage.removeAttribute('required');
}
watermarkTextGroup.style.display = 'none';
watermarkText.required = false;
watermarkImageGroup.style.display = 'block';
watermarkImage.required = true;
alphabetGroup.style.display = 'none';
}
}
</script>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{autoRedact.title}, header=#{autoRedact.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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">
<svg class="material-symbols-rounded tool-header-icon security">
<use xlink:href="/images/redact-auto.svg#icon-redact-auto"></use>
</svg>
<span class="tool-header-text" th:text="#{autoRedact.header}"></span>
</div>
<form th:action="@{'api/v1/security/auto-redact'}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<input type="file" class="form-control" id="fileInput" name="fileInput" required
accept="application/pdf">
</div>
<div class="mb-3">
<label for="listOfText" class="form-label" th:text="#{autoRedact.textsToRedactLabel}"></label>
<textarea class="form-control" id="listOfText" name="listOfText" rows="4" required
th:placeholder="#{autoRedact.textsToRedactPlaceholder}"></textarea>
</div>
<div class="mb-3">
<label for="defaultColor" class="form-label" th:text="#{autoRedact.colorLabel}">Color</label>
<select class="form-control" id="defaultColor" name="defaultColor"
onchange="handleColorChange(this.value)">
<option value="#000000" th:text="#{black}">Black</option>
<option value="#FFFFFF" th:text="#{white}">White</option>
<option value="#FF0000" th:text="#{red}">Red</option>
<option value="#00FF00" th:text="#{green}">Green</option>
<option value="#0000FF" th:text="#{blue}">Blue</option>
<option value="custom" th:text="#{custom}">Custom...</option>
</select>
</div>
<!-- Custom Color Input -->
<div class="mb-3" id="customColorContainer" style="display: none;">
<label for="customColor" class="form-label" th:text="#{autoRedact.colorLabel}">Custom Color</label>
<input type="text" class="form-control" id="customColor" name="redactColor" placeholder="#FF00FF">
</div>
<script>
function handleColorChange(selectedValue) {
if (selectedValue === "custom") {
document.getElementById('customColorContainer').style.display = 'block';
} else {
document.getElementById('customColorContainer').style.display = 'none';
document.getElementById('customColor').value = selectedValue;
}
}
</script>
<div class="mb-3 form-check">
<input type="checkbox" id="useRegex" name="useRegex">
<label for="useRegex" th:text="#{autoRedact.useRegexLabel}"></label>
</div>
<div class="mb-3 form-check">
<input type="checkbox" id="wholeWordSearch" name="wholeWordSearch">
<label for="wholeWordSearch" th:text="#{autoRedact.wholeWordSearchLabel}"></label>
</div>
<div class="mb-3">
<label for="customPadding" class="form-label" th:text="#{autoRedact.customPaddingLabel}"></label>
<input type="number" step="0.1" class="form-control" id="customPadding" name="customPadding"
value="0.1">
</div>
<div class="mb-3 form-check">
<input type="checkbox" id="convertPDFToImage" name="convertPDFToImage" checked>
<label for="convertPDFToImage" th:text="#{autoRedact.convertPDFToImageLabel}"></label>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary"
th:text="#{autoRedact.submitButton}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{certSign.title}, header=#{certSign.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 security">workspace_premium</span>
<span class="tool-header-text" th:text="#{certSign.header}"></span>
</div>
<form th:action="@{'api/v1/security/cert-sign'}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label th:text="#{certSign.selectPDF}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
</div>
<!-- Tell users to use keytool to generate JKS for other formats -->
<div class="mb-3">
<label th:text="#{certSign.jksNote}"></label>
</div>
<div class="mb-3">
<label for="certType" th:text="#{certSign.certType}"></label>
<select class="form-control" id="certType" name="certType">
<option value="" th:text="#{selectFilter}"></option>
<option value="PEM">PEM</option>
<option value="PKCS12">PKCS12</option>
<option value="JKS">JKS</option>
</select>
</div>
<div id="pemGroup" style="display: none;">
<div class="mb-3">
<label th:text="#{certSign.selectKey}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='privateKeyFile', multipleInputsForSingleRequest=false, notRequired=true, accept='.pem,.der')}"></div>
</div>
<div class="mb-3">
<label th:text="#{certSign.selectCert}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='certFile', multipleInputsForSingleRequest=false, notRequired=true, accept='.pem,.der')}"></div>
</div>
</div>
<div class="mb-3" id="p12Group" style="display: none;">
<label th:text="#{certSign.selectP12}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='p12File', notRequired=true, multipleInputsForSingleRequest=false, accept='.p12,.pfx')}"></div>
</div>
<div class="mb-3" id="jksGroup" style="display: none;">
<label th:text="#{certSign.selectJKS}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='jksFile', notRequired=true, multipleInputsForSingleRequest=false, accept='.jks,.keystore')}"></div>
</div>
<div class="mb-3">
<label th:text="#{certSign.password}" for="password"></label>
<input type="password" class="form-control" id="password" name="password">
</div>
<div class="form-check mb-3">
<input type="checkbox" id="showSignature" name="showSignature">
<label th:text="#{certSign.showSig}" for="showSignature"></label>
</div>
<div id="signatureDetails" style="display: none;">
<div class="mb-3">
<label for="reason" th:text="#{certSign.reason}"></label> <input type="text" class="form-control" id="reason" name="reason">
</div>
<div class="mb-3">
<label for="location" th:text="#{certSign.location}"></label> <input type="text" class="form-control" id="location" name="location">
</div>
<div class="mb-3">
<label for="name" th:text="#{certSign.name}"></label> <input type="text" class="form-control" id="name" name="name">
</div>
<div class="mb-3">
<label for="pageNumber" th:text="#{pageNum}"></label> <input type="number" class="form-control" id="pageNumber" name="pageNumber" min="1" value="1">
</div>
<div class="form-check mb-3">
<input type="checkbox" id="showLogo" name="showLogo" checked />
<label th:text="#{certSign.showLogo}" for="showLogo"></label>
</div>
</div>
<div class="mb-3 text-left">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{certSign.submit}"></button>
</div>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script>
document.getElementById('certType').addEventListener('change', function() {
var pemGroup = document.getElementById('pemGroup');
var p12Group = document.getElementById('p12Group');
var jksGroup = document.getElementById('jksGroup');
var valueToGroupMap = {
'PEM': pemGroup,
'PKCS12': p12Group,
'JKS': jksGroup
};
for (var key in valueToGroupMap) {
valueToGroupMap[key].style.display = (this.value === key) ? 'block' : 'none';
}
});
document.getElementById('showSignature').addEventListener('change', function() {
var signatureDetails = document.getElementById('signatureDetails');
if (this.checked) {
signatureDetails.style.display = 'block';
} else {
signatureDetails.style.display = 'none';
}
});
</script>
</body>
</html>
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{permissions.title}, header=#{permissions.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 security">encrypted</span>
<span class="tool-header-text" th:text="#{permissions.header}"></span>
</div>
<p th:text="#{permissions.warning}"></p>
<form th:action="@{'api/v1/security/add-password'}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label th:text="#{permissions.selectText.1}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
</div>
<div class="mb-3">
<label class="mb-2" th:text="#{permissions.selectText.2}"></label>
<div class="form-check ms-3">
<input type="checkbox" id="preventAssembly" name="preventAssembly">
<label for="preventAssembly" th:text="#{permissions.selectText.3}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="preventExtractContent" name="preventExtractContent">
<label for="preventExtractContent" th:text="#{permissions.selectText.4}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="preventExtractForAccessibility" name="preventExtractForAccessibility">
<label for="preventExtractForAccessibility" th:text="#{permissions.selectText.5}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="preventFillInForm" name="preventFillInForm">
<label for="preventFillInForm" th:text="#{permissions.selectText.6}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="preventModify" name="preventModify">
<label for="preventModify" th:text="#{permissions.selectText.7}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="preventModifyAnnotations" name="preventModifyAnnotations">
<label for="preventModifyAnnotations" th:text="#{permissions.selectText.8}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="preventPrinting" name="preventPrinting">
<label for="preventPrinting" th:text="#{permissions.selectText.9}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="preventPrintingFaithful" name="preventPrintingFaithful">
<label for="preventPrintingFaithful" th:text="#{permissions.selectText.10}"></label>
</div>
</div>
<br>
<div class="mb-3 text-left">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{permissions.submit}"></button>
</div>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,594 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{getPdfInfo.title}, header=#{getPdfInfo.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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-7 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon other">info</span>
<span class="tool-header-text" th:text="#{getPdfInfo.header}"></span>
</div>
<form id="pdfInfoForm" method="post" enctype="multipart/form-data" th:action="@{'/api/v1/security/get-info-on-pdf'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, remoteCall='false', accept='application/pdf')}"></div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{getPdfInfo.submit}"></button>
</form>
<div class="container mt-0">
<!-- PDF Summary section -->
<div id="pdf-summary" class="card mt-3 mb-3" style="display: none;">
<div class="card-header">
<h5 class="mb-0" id="summary-heading">PDF Summary</h5>
</div>
<div class="card-body">
<!-- Quick overview of key details -->
<div class="row">
<div class="col-md-6">
<h6 id="summary-basic-info-heading">Basic Information</h6>
<ul class="list-unstyled">
<li><strong>Pages:</strong> <span id="summary-pages">-</span></li>
<li><strong>File Size:</strong> <span id="summary-size">-</span></li>
<li><strong>PDF Version:</strong> <span id="summary-version">-</span></li>
<li><strong>Language:</strong> <span id="summary-language">-</span></li>
</ul>
</div>
<div class="col-md-6">
<h6 id="summary-doc-info-heading">Document Information</h6>
<ul class="list-unstyled">
<li><strong>Title:</strong> <span id="summary-title">-</span></li>
<li><strong>Author:</strong> <span id="summary-author">-</span></li>
<li><strong>Created:</strong> <span id="summary-created">-</span></li>
<li><strong>Modified:</strong> <span id="summary-modified">-</span></li>
</ul>
</div>
</div>
<!-- Security section -->
<div class="mt-4 mb-3">
<h6 id="summary-security-heading">Security Status</h6>
<div class="row">
<div class="col-md-4">
<div id="encryption-status" class="card mb-2 h-100">
<div class="card-body p-2 d-flex align-items-center">
<span id="encryption-icon" class="me-2"><i class="bi bi-lock"></i></span>
<span id="encryption-text" class="small">Encryption: Unknown</span>
</div>
</div>
</div>
<div class="col-md-4">
<div id="permissions-status" class="card mb-2 h-100">
<div class="card-body p-2 d-flex align-items-center">
<span id="permissions-icon" class="me-2"><i class="bi bi-shield"></i></span>
<span id="permissions-text" class="small">Permissions: Unknown</span>
</div>
</div>
</div>
<div class="col-md-4">
<div id="compliance-status" class="card mb-2 h-100">
<div class="card-body p-2 d-flex align-items-center">
<span id="compliance-icon" class="me-2"><i class="bi bi-check-circle"></i></span>
<span id="compliance-text" class="small">Compliance: Unknown</span>
</div>
</div>
</div>
</div>
</div>
<!-- Detailed alerts -->
<div id="summary-alerts" class="mt-3">
<!-- Will be populated with detailed alerts for encryption, permissions, etc. -->
</div>
<!-- Descriptive note about PDF characteristics -->
<div class="card mt-3">
<div class="card-header">
<h6 class="mb-0">PDF Overview</h6>
</div>
<div class="card-body">
<p id="summary-text" class="mb-0"></p>
</div>
</div>
</div>
</div>
<!-- Iterate over each main section in the JSON -->
<div id="json-content">
<!-- JavaScript will populate this section -->
</div>
<!-- Button to download the JSON -->
<a href="#" id="downloadJson" class="btn btn-primary mt-3" style="display: none;" th:text="#{getPdfInfo.downloadJson}">Download JSON</a>
</div>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<script th:inline="javascript">
// Pre-load message translations
const getPdfInfoSummary = /*[[#{getPdfInfo.summary}]]*/ "PDF Summary";
const getPdfInfoSummaryEncrypted = /*[[#{getPdfInfo.summary.encrypted}]]*/ "This PDF is encrypted so may face issues with some applications";
const getPdfInfoSummaryPermissions = /*[[#{getPdfInfo.summary.permissions}]]*/ "This PDF has {0} restricted permissions which may limit what you can do with it";
const getPdfInfoSummaryCompliance = /*[[#{getPdfInfo.summary.compliance}]]*/ "This PDF complies with the {0} standard, meaning it is suitable for {1}";
const getPdfInfoSummaryBasicInfo = /*[[#{getPdfInfo.summary.basicInfo}]]*/ "Basic Information";
const getPdfInfoSummaryDocInfo = /*[[#{getPdfInfo.summary.docInfo}]]*/ "Document Information";
const getPdfInfoSummarySecuritySection = /*[[#{getPdfInfo.summary.security.section}]]*/ "Security Status";
const getPdfInfoSummaryEncryptedAlert = /*[[#{getPdfInfo.summary.encrypted.alert}]]*/ "Encrypted PDF - This document is password protected";
const getPdfInfoSummaryNotEncryptedAlert = /*[[#{getPdfInfo.summary.not.encrypted.alert}]]*/ "Unencrypted PDF - No password protection";
const getPdfInfoSummaryPermissionsAlert = /*[[#{getPdfInfo.summary.permissions.alert}]]*/ "Restricted Permissions - {0} actions are not allowed";
const getPdfInfoSummaryAllPermissionsAlert = /*[[#{getPdfInfo.summary.all.permissions.alert}]]*/ "All Permissions Allowed";
const getPdfInfoSummaryComplianceAlert = /*[[#{getPdfInfo.summary.compliance.alert}]]*/ "{0} Compliant";
const getPdfInfoSummaryNoComplianceAlert = /*[[#{getPdfInfo.summary.no.compliance.alert}]]*/ "No Compliance Standards";
// Update the summary headings
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('summary-heading').textContent = getPdfInfoSummary;
document.getElementById('summary-basic-info-heading').textContent = getPdfInfoSummaryBasicInfo;
document.getElementById('summary-doc-info-heading').textContent = getPdfInfoSummaryDocInfo;
document.getElementById('summary-security-heading').textContent = getPdfInfoSummarySecuritySection;
});
// Pre-load section descriptions
const getPdfInfoSectionBasicInfo = /*[[#{getPdfInfo.section.BasicInfo}]]*/ "Basic Information about the PDF document including file size, page count, and language";
const getPdfInfoSectionMetadata = /*[[#{getPdfInfo.section.Metadata}]]*/ "Document metadata including title, author, creation date and other document properties";
const getPdfInfoSectionDocumentInfo = /*[[#{getPdfInfo.section.DocumentInfo}]]*/ "Technical details about the PDF document structure and version";
const getPdfInfoSectionCompliancy = /*[[#{getPdfInfo.section.Compliancy}]]*/ "PDF standards compliance information (PDF/A, PDF/X, etc.)";
const getPdfInfoSectionEncryption = /*[[#{getPdfInfo.section.Encryption}]]*/ "Security and encryption details of the document";
const getPdfInfoSectionPermissions = /*[[#{getPdfInfo.section.Permissions}]]*/ "Document permission settings that control what actions can be performed";
const getPdfInfoSectionOther = /*[[#{getPdfInfo.section.Other}]]*/ "Additional document components like bookmarks, layers, and embedded files";
const getPdfInfoSectionFormFields = /*[[#{getPdfInfo.section.FormFields}]]*/ "Interactive form fields present in the document";
const getPdfInfoSectionPerPageInfo = /*[[#{getPdfInfo.section.PerPageInfo}]]*/ "Detailed information about each page in the document";
document.getElementById("pdfInfoForm").addEventListener("submit", function(event) {
event.preventDefault();
// Clear previous results when submitting a new form
document.getElementById('json-content').innerHTML = '';
document.getElementById('pdf-summary').style.display = 'none';
document.getElementById('downloadJson').style.display = 'none';
const formData = new FormData(event.target);
fetchWithCsrf('api/v1/security/get-info-on-pdf', {
method: 'POST',
body: formData
}).then(response => response.json()).then(data => {
// Populate and display the enhanced PDF summary
populateSummarySection(data);
displayJsonData(data);
setDownloadLink(data);
document.getElementById("downloadJson").style.display = "block";
}).catch(error => console.error('Error:', error));
// Function to reset all summary elements to default state
function resetSummaryElements() {
// Reset basic information fields
document.getElementById('summary-pages').textContent = '-';
document.getElementById('summary-size').textContent = '-';
document.getElementById('summary-version').textContent = '-';
document.getElementById('summary-language').textContent = '-';
// Reset document information fields
document.getElementById('summary-title').textContent = '-';
document.getElementById('summary-author').textContent = '-';
document.getElementById('summary-created').textContent = '-';
document.getElementById('summary-modified').textContent = '-';
// Reset security status cards
const cards = ['encryption-status', 'permissions-status', 'compliance-status'];
cards.forEach(id => {
const card = document.getElementById(id);
// Remove all classes except the base ones
card.className = 'card mb-2 h-100';
});
// Reset status text and icons
document.getElementById('encryption-icon').innerHTML = '<i class="bi bi-lock"></i>';
document.getElementById('encryption-text').textContent = 'Encryption: Unknown';
document.getElementById('permissions-icon').innerHTML = '<i class="bi bi-shield"></i>';
document.getElementById('permissions-text').textContent = 'Permissions: Unknown';
document.getElementById('compliance-icon').innerHTML = '<i class="bi bi-check-circle"></i>';
document.getElementById('compliance-text').textContent = 'Compliance: Unknown';
// Clear alerts container
document.getElementById('summary-alerts').innerHTML = '';
// Reset summary text
document.getElementById('summary-text').innerHTML = '';
}
// Function to populate the enhanced summary section
function populateSummarySection(data) {
// Reset all elements first
resetSummaryElements();
// Get basic information
if (data.BasicInfo) {
document.getElementById('summary-pages').textContent = data.BasicInfo["Number of pages"] || "-";
// Format file size nicely
let fileSize = data.BasicInfo["FileSizeInBytes"];
if (fileSize) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(fileSize) / Math.log(1024));
fileSize = (fileSize / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
document.getElementById('summary-size').textContent = fileSize;
}
document.getElementById('summary-language').textContent = data.BasicInfo["Language"] || "-";
}
// Get document information
if (data.DocumentInfo) {
document.getElementById('summary-version').textContent = data.DocumentInfo["PDF version"] || "-";
}
// Get metadata
if (data.Metadata) {
document.getElementById('summary-title').textContent = data.Metadata["Title"] || "-";
document.getElementById('summary-author').textContent = data.Metadata["Author"] || "-";
document.getElementById('summary-created').textContent = data.Metadata["CreationDate"] || "-";
document.getElementById('summary-modified').textContent = data.Metadata["ModificationDate"] || "-";
}
// Update security status cards
// Encryption status
const encryptionStatusCard = document.getElementById('encryption-status');
const encryptionIcon = document.getElementById('encryption-icon');
const encryptionText = document.getElementById('encryption-text');
if (data.Encryption && data.Encryption.IsEncrypted) {
encryptionIcon.innerHTML = '<i class="bi bi-lock-fill"></i>';
encryptionText.textContent = getPdfInfoSummaryEncryptedAlert;
} else {
encryptionIcon.innerHTML = '<i class="bi bi-unlock-fill"></i>';
encryptionText.textContent = getPdfInfoSummaryNotEncryptedAlert;
}
// Permissions status
const permissionsStatusCard = document.getElementById('permissions-status');
const permissionsIcon = document.getElementById('permissions-icon');
const permissionsText = document.getElementById('permissions-text');
let restrictedPermissions = [];
if (data.Permissions) {
for (const [permission, state] of Object.entries(data.Permissions)) {
if (state === "Not Allowed") {
restrictedPermissions.push(permission);
}
}
}
if (restrictedPermissions.length > 0) {
permissionsIcon.innerHTML = '<i class="bi bi-shield-lock-fill"></i>';
const formattedAlert = getPdfInfoSummaryPermissionsAlert.replace('{0}', restrictedPermissions.length);
permissionsText.textContent = formattedAlert;
} else {
permissionsIcon.innerHTML = '<i class="bi bi-shield-check"></i>';
permissionsText.textContent = getPdfInfoSummaryAllPermissionsAlert;
}
// Compliance status
const complianceStatusCard = document.getElementById('compliance-status');
const complianceIcon = document.getElementById('compliance-icon');
const complianceText = document.getElementById('compliance-text');
let hasCompliance = false;
let compliantStandards = [];
if (data.Compliancy) {
for (const [standard, compliant] of Object.entries(data.Compliancy)) {
if (compliant === true) {
hasCompliance = true;
const standardName = standard.replace("Is", "").replace("Compliant", "");
compliantStandards.push(standardName);
}
}
}
if (hasCompliance) {
complianceIcon.innerHTML = '<i class="bi bi-check-circle-fill"></i>';
const formattedAlert = getPdfInfoSummaryComplianceAlert.replace('{0}', compliantStandards.join(', '));
complianceText.textContent = formattedAlert;
} else {
complianceIcon.innerHTML = '<i class="bi bi-dash-circle"></i>';
complianceText.textContent = getPdfInfoSummaryNoComplianceAlert;
}
// Create detailed characteristic alerts
const alertsContainer = document.getElementById('summary-alerts');
// Clear previous alerts
alertsContainer.innerHTML = '';
// Create a single comprehensive security details section
let hasSummaryInfo = false;
// Create a consolidated security details card if there are security details worth highlighting
if ((data.Encryption && data.Encryption.IsEncrypted) ||
restrictedPermissions.length > 0 ||
hasCompliance) {
const securityDetailsCard = document.createElement('div');
securityDetailsCard.className = 'card mt-3 mb-3';
const cardHeader = document.createElement('div');
cardHeader.className = 'card-header';
cardHeader.innerHTML = '<h6 class="mb-0">Detailed Security Information</h6>';
securityDetailsCard.appendChild(cardHeader);
const cardBody = document.createElement('div');
cardBody.className = 'card-body';
// Add detailed encryption info
if (data.Encryption && data.Encryption.IsEncrypted) {
const encryptionDiv = document.createElement('div');
encryptionDiv.className = 'mb-3';
encryptionDiv.innerHTML = '<h6>Encryption Details:</h6>';
const encryptionList = document.createElement('ul');
encryptionList.className = 'list-unstyled';
if (data.Encryption.EncryptionAlgorithm) {
encryptionList.innerHTML += `<li><strong>Algorithm:</strong> ${data.Encryption.EncryptionAlgorithm}</li>`;
}
if (data.Encryption.KeyLength) {
encryptionList.innerHTML += `<li><strong>Key Length:</strong> ${data.Encryption.KeyLength} bits</li>`;
}
encryptionDiv.appendChild(encryptionList);
cardBody.appendChild(encryptionDiv);
hasSummaryInfo = true;
}
// Add detailed permissions info
if (restrictedPermissions.length > 0) {
const permissionsDiv = document.createElement('div');
permissionsDiv.className = 'mb-3';
permissionsDiv.innerHTML = '<h6>Restricted Permissions:</h6>';
const permissionsList = document.createElement('ul');
restrictedPermissions.forEach(perm => {
permissionsList.innerHTML += `<li>${perm}</li>`;
});
permissionsDiv.appendChild(permissionsList);
cardBody.appendChild(permissionsDiv);
hasSummaryInfo = true;
}
// Add detailed compliance info
if (hasCompliance) {
const complianceDiv = document.createElement('div');
complianceDiv.className = 'mb-3';
complianceDiv.innerHTML = '<h6>Standards Compliance:</h6>';
const complianceList = document.createElement('ul');
complianceList.className = 'list-unstyled';
compliantStandards.forEach(standard => {
let standardDescription = '';
// Add brief descriptions for standards
if (standard === "PDF/A") {
standardDescription = 'ISO standard for long-term document archiving';
} else if (standard === "PDF/X") {
standardDescription = 'ISO standard for printing and graphic arts exchange';
} else if (standard === "PDF/UA") {
standardDescription = 'ISO standard for universal accessibility';
} else if (standard === "PDF/E") {
standardDescription = 'ISO standard for engineering documents';
} else if (standard === "PDF/VT") {
standardDescription = 'ISO standard for variable and transactional printing';
}
complianceList.innerHTML += `<li><strong>${standard}:</strong> ${standardDescription}</li>`;
});
complianceDiv.appendChild(complianceList);
cardBody.appendChild(complianceDiv);
hasSummaryInfo = true;
}
securityDetailsCard.appendChild(cardBody);
if (hasSummaryInfo) {
alertsContainer.appendChild(securityDetailsCard);
}
}
// Generate a general document summary
const summaryTextElement = document.getElementById('summary-text');
// Create a general summary for the document
let generalSummary = `This is a ${data.BasicInfo["Number of pages"] || "multi"}-page PDF`;
if (data.Metadata && data.Metadata["Title"]) {
generalSummary += ` titled "${data.Metadata["Title"]}"`;
}
if (data.Metadata && data.Metadata["Author"]) {
generalSummary += ` created by ${data.Metadata["Author"]}`;
}
if (data.DocumentInfo && data.DocumentInfo["PDF version"]) {
generalSummary += ` (PDF version ${data.DocumentInfo["PDF version"]})`;
}
// Add security information to the general summary if relevant
if (data.Encryption && data.Encryption.IsEncrypted) {
generalSummary += '. The document is password protected';
if (data.Encryption.EncryptionAlgorithm) {
generalSummary += ` using ${data.Encryption.EncryptionAlgorithm}`;
if (data.Encryption.KeyLength) {
generalSummary += ` (${data.Encryption.KeyLength} bit)`;
}
}
}
if (restrictedPermissions.length > 0) {
generalSummary += `. It has ${restrictedPermissions.length} restricted permissions`;
}
// Add compliance standards if available
if (hasCompliance && compliantStandards.length > 0) {
generalSummary += `. This document complies with the ${compliantStandards.join(', ')} PDF standard${compliantStandards.length > 1 ? 's' : ''}`;
}
generalSummary += '.';
// Remove SummaryData from JSON to avoid duplication
if (data.SummaryData) {
delete data.SummaryData;
}
summaryTextElement.innerHTML = generalSummary;
// Display the summary section
document.getElementById('pdf-summary').style.display = 'block';
}
function generateSummaryFromData(summaryData) {
let summary = [];
// Handle encryption information
if (summaryData.encrypted) {
summary.push(getPdfInfoSummaryEncrypted);
}
// Handle permissions information
if (summaryData.restrictedPermissions && summaryData.restrictedPermissions.length > 0) {
const formattedPermissionsText = getPdfInfoSummaryPermissions.replace('{0}', summaryData.restrictedPermissionsCount);
summary.push(formattedPermissionsText);
}
// Handle standard compliance information
if (summaryData.standardCompliance) {
const formattedComplianceText = getPdfInfoSummaryCompliance
.replace('{0}', summaryData.standardCompliance);
summary.push(formattedComplianceText);
}
return summary.join(' ');
}
});
function displayJsonData(jsonData) {
const jsonContent = document.getElementById('json-content');
while (jsonContent.firstChild) {
jsonContent.removeChild(jsonContent.firstChild);
}
for (const key in jsonData) {
const sectionElem = createJsonSection(key, jsonData[key]);
jsonContent.appendChild(sectionElem);
}
}
function setDownloadLink(jsonData) {
const downloadLink = document.getElementById('downloadJson');
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(jsonData, null, 4));
downloadLink.setAttribute("href", dataStr);
downloadLink.setAttribute("download", "data.json");
}
function createJsonSection(key, value, depth = 0) {
let safeKey = (typeof key === "string") ? key.replace(/[^a-zA-Z0-9]/g, '_') : key;
const card = document.createElement('div');
card.className = 'card mb-3';
const header = document.createElement('div');
header.className = 'card-header';
header.id = `${safeKey}-heading-${depth}`;
const h5Elem = document.createElement('h5');
h5Elem.className = 'mb-0 d-flex align-items-center';
// Create the main content (button or text)
if (key === 'XMPMetadata' && typeof value === "string") {
const buttonElem = createButtonElement(key, safeKey, depth);
h5Elem.appendChild(buttonElem);
} else if (value && typeof value === 'object') {
if (Array.isArray(value) && value.length === 0) {
h5Elem.textContent = `${key}: Empty array`;
} else if (!Array.isArray(value) && Object.keys(value).length === 0) {
h5Elem.textContent = `${key}: Empty object`;
} else {
const buttonElem = createButtonElement(key, safeKey, depth);
h5Elem.appendChild(buttonElem);
}
} else {
h5Elem.textContent = `${key}: ${String(value)}`;
}
// Info buttons removed as requested
header.appendChild(h5Elem);
card.appendChild(header);
const content = document.createElement('div');
content.id = `${safeKey}-content-${depth}`;
content.className = 'collapse';
content.setAttribute('aria-labelledby', `${safeKey}-heading-${depth}`);
if (key === 'XMPMetadata' && typeof value === "string") {
const body = document.createElement('div');
body.className = 'card-body';
const preElem = document.createElement('pre');
preElem.textContent = value; // Not escaping since we're using textContent
body.appendChild(preElem);
content.appendChild(body);
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
const body = document.createElement('div');
body.className = 'card-body';
for (const subKey in value) {
const subElem = createJsonSection(subKey, value[subKey], depth + 1);
body.appendChild(subElem);
}
content.appendChild(body);
} else if (value && typeof value === 'object' && Array.isArray(value)) {
const body = document.createElement('div');
body.className = 'card-body';
value.forEach((val, index) => {
const subElem = createJsonSection(`${key}[${index}]`, val, depth + 1);
body.appendChild(subElem);
});
content.appendChild(body);
}
card.appendChild(content);
return card;
}
function createButtonElement(key, safeKey, depth) {
const buttonElem = document.createElement('button');
buttonElem.className = 'btn btn-link';
buttonElem.type = 'button';
buttonElem.dataset.bsToggle = "collapse";
buttonElem.dataset.bsTarget = `#${safeKey}-content-${depth}`;
buttonElem.setAttribute('aria-expanded', 'true');
buttonElem.setAttribute('aria-controls', `${safeKey}-content-${depth}`);
buttonElem.textContent = key;
return buttonElem;
}
const collapsibleElems = document.querySelectorAll('[data-bs-toggle="collapse"]');
collapsibleElems.forEach(elem => {
new bootstrap.Collapse(elem);
});
</script>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,698 @@
<!DOCTYPE html>
<html dir="ltr" th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
mozdisallowselectionprint xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{redact.title}, header=#{redact.header})}"></th:block>
<script th:src="@{'/js/thirdParty/popper.min.js'}"></script>
<script th:src="@{'/js/thirdParty/bootstrap.min.js'}"></script>
<script th:src="@{'/js/thirdParty/jquery.min.js'}"></script>
<script th:src="@{'/js/thirdParty/jquery.validate.min.js'}"></script>
<link rel="stylesheet" th:href="@{'/css/theme/componentes.css'}">
<link rel="stylesheet" th:href="@{'/css/navbar.css'}">
<!-- This snippet is used in production (included from view-pdf.html) -->
<link rel="resource" type="application/l10n" th:href="@{'/pdfjs-legacy/locale/locale.json'}">
<script th:src="@{'/pdfjs-legacy/pdf.mjs'}" type="module"></script>
<link rel="stylesheet" th:href="@{'/pdfjs-legacy/css/viewer-redact.css'}">
<script th:src="@{'/pdfjs-legacy/js/viewer.mjs'}" type="module"></script>
<script th:src="@{'/js/redact.js'}" type="module"></script>
<link rel="stylesheet" th:href="@{'/css/redact.css'}">
</head>
<body>
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div id="redactionFormContainer" class="container mb-5">
<div class="row justify-content-center">
<div class="col-md-6 bg-card">
<div class="tool-header">
<svg class="material-symbols-rounded tool-header-icon security">
<use xlink:href="/images/redact-manual.svg#icon-redact-manual"></use>
</svg>
<span class="tool-header-text" th:text="#{redact.header}"></span>
</div>
<form th:action="@{'api/v1/security/redact'}" method="post" enctype="multipart/form-data">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, disableMultipleFiles=true, accept='application/pdf')}">
</div>
<div class="mb-3 form-check d-none">
<input type="checkbox" id="convertPDFToImage" name="convertPDFToImage" checked>
<label for="convertPDFToImage" th:text="#{redact.convertPDFToImageLabel}"></label>
</div>
<div class="mb-3 d-none">
<label for="pageNumbers" th:text="#{redact.pageRedactionNumbers.title} + ':'" class="mb-2"></label>
<input type="text" class="form-control" id="pageNumbers" name="pageNumbers"
th:placeholder="#{redact.pageRedactionNumbers.placeholder}">
</div>
<div class="mb-3 d-none">
<label for="pageRedactColor">Page Redaction Color:</label>
<input id="pageRedactColor" name="pageRedactionColor" type="color" class="ms-3" value="#000000">
</div>
<input type="hidden" name="redactions" id="redactions-input">
<button type="submit" id="submitBtn" class="btn btn-primary d-none" th:text="#{redact.submit}"></button>
</form>
</div>
</div>
</div>
<div id="pageBasedRedactionOverlay" class="d-none col-md-4 row justify-content-center align-content-center">
<div class="bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon security">document_scanner</span>
<span class="tool-header-text" th:text="#{redact.pageBasedRedaction}"></span>
</div>
<div class="mb-3">
<label for="pageNumbers" th:text="#{redact.pageRedactionNumbers.title} + ':'" class="mb-2"></label>
<input type="text" class="form-control" data-for="pageNumbers"
th:placeholder="#{redact.pageRedactionNumbers.placeholder}">
</div>
<div class="mb-3 d-flex">
<label for="pageRedactColor" th:inline="text" class="pe-none user-select-none">
[[#{redact.redactionColor.title}]]
<span class="material-symbols-rounded">palette</span>
:
</label>
<input data-for="pageRedactColor" type="color" class="ms-3" value="#000000">
</div>
<button id="closePageRedactionBtn" class="btn btn-danger" th:text="#{close}"></button>
<button id="applyPageRedactionBtn" class="btn btn-success ms-3" th:text="#{apply}"></button>
</div>
</div>
<div id="outerContainer" class="d-none">
<div id="sidebarContainer">
<div id="toolbarSidebar">
<div id="toolbarSidebarLeft">
<div id="sidebarViewButtons" class="splitToolbarButton toggled" role="radiogroup">
<button id="viewThumbnail" th:title="#{redact.showThumbnails}" class="toolbarButton toggled" tabindex="2"
data-l10n-id="pdfjs-thumbs-button" role="radio" aria-checked="true" aria-controls="thumbnailView">
</button>
<button id="viewOutline" th:title="#{redact.showDocumentOutline}" class="toolbarButton" tabindex="3"
data-l10n-id="pdfjs-document-outline-button" role="radio" aria-checked="false"
aria-controls="outlineView">
</button>
<button id="viewAttachments" th:title="#{redact.showAttachments}" class="d-none toolbarButton" tabindex="4"
data-l10n-id="pdfjs-attachments-button" role="radio" aria-checked="false" aria-controls="attachmentsView">
<span data-l10n-id="pdfjs-attachments-button-label">Attachments</span>
</button>
<button id="viewLayers" th:title="#{redact.showLayers}" class="d-none toolbarButton" tabindex="5"
data-l10n-id="pdfjs-layers-button" role="radio" aria-checked="false" aria-controls="layersView">
<span data-l10n-id="pdfjs-layers-button-label">Layers</span>
</button>
</div>
</div>
<div id="toolbarSidebarRight">
<div id="outlineOptionsContainer">
<div class="verticalToolbarSeparator"></div>
<button id="currentOutlineItem" th:title="#{redact.findCurrentOutlineItem}" class="toolbarButton"
disabled="disabled" tabindex="6" data-l10n-id="pdfjs-current-outline-item-button">
</button>
</div>
</div>
</div>
<div id="sidebarContent">
<div id="thumbnailView">
</div>
<div id="outlineView" class="hidden">
</div>
<div id="attachmentsView" class="hidden">
</div>
<div id="layersView" class="hidden">
</div>
</div>
<div id="sidebarResizer"></div>
</div> <!-- sidebarContainer -->
<div id="mainContainer">
<div class="d-none findbar hidden doorHanger" id="findbar">
<div id="findbarInputContainer">
<span class="loadingInput end">
<input id="findInput" class="toolbarField" title="Find" placeholder="Find in document…" tabindex="91"
data-l10n-id="pdfjs-find-input" aria-invalid="false">
</span>
<div class="splitToolbarButton">
<button id="findPrevious" class="toolbarButton" title="Find the previous occurrence of the phrase"
tabindex="92" data-l10n-id="pdfjs-find-previous-button">
<span data-l10n-id="pdfjs-find-previous-button-label">Previous</span>
</button>
<div class="splitToolbarButtonSeparator"></div>
<button id="findNext" class="toolbarButton" title="Find the next occurrence of the phrase" tabindex="93"
data-l10n-id="pdfjs-find-next-button">
<span data-l10n-id="pdfjs-find-next-button-label">Next</span>
</button>
</div>
</div>
<div id="findbarOptionsOneContainer">
<input type="checkbox" id="findHighlightAll" class="toolbarField" tabindex="94">
<label for="findHighlightAll" class="toolbarLabel" data-l10n-id="pdfjs-find-highlight-checkbox">Highlight
All</label>
<input type="checkbox" id="findMatchCase" class="toolbarField" tabindex="95">
<label for="findMatchCase" class="toolbarLabel" data-l10n-id="pdfjs-find-match-case-checkbox-label">Match
Case</label>
</div>
<div id="findbarOptionsTwoContainer">
<input type="checkbox" id="findMatchDiacritics" class="toolbarField" tabindex="96">
<label for="findMatchDiacritics" class="toolbarLabel"
data-l10n-id="pdfjs-find-match-diacritics-checkbox-label">Match Diacritics</label>
<input type="checkbox" id="findEntireWord" class="toolbarField" tabindex="97">
<label for="findEntireWord" class="toolbarLabel" data-l10n-id="pdfjs-find-entire-word-checkbox-label">Whole
Words</label>
</div>
<div id="findbarMessageContainer" aria-live="polite">
<span id="findResultsCount" class="toolbarLabel"></span>
<span id="findMsg" class="toolbarLabel"></span>
</div>
</div> <!-- findbar -->
<div class="editorParamsToolbar hidden doorHangerRight" id="editorHighlightParamsToolbar">
<div id="highlightParamsToolbarContainer" class="editorParamsToolbarContainer">
<div id="editorHighlightColorPicker" class="colorPicker">
<span id="highlightColorPickerLabel" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-highlight-colorpicker-label">Highlight color</span>
</div>
<div id="editorHighlightThickness">
<label for="editorFreeHighlightThickness" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-free-highlight-thickness-input">Thickness</label>
<div class="thicknessPicker">
<input type="range" id="editorFreeHighlightThickness" class="editorParamsSlider"
data-l10n-id="pdfjs-editor-free-highlight-thickness-title" value="12" min="8" max="24" step="1"
tabindex="101">
</div>
</div>
<div id="editorHighlightVisibility">
<div class="divider"></div>
<div class="toggler">
<label for="editorHighlightShowAll" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-highlight-show-all-button-label">Show all</label>
<button id="editorHighlightShowAll" class="toggle-button"
data-l10n-id="pdfjs-editor-highlight-show-all-button" aria-pressed="true" tabindex="102"></button>
</div>
</div>
</div>
</div>
<div class="editorParamsToolbar hidden doorHangerRight" id="editorFreeTextParamsToolbar">
<div class="editorParamsToolbarContainer">
<div class="editorParamsSetter">
<label for="editorFreeTextColor" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-free-text-color-input">Color</label>
<input type="color" id="editorFreeTextColor" class="editorParamsColor" tabindex="103">
</div>
<div class="editorParamsSetter">
<label for="editorFreeTextFontSize" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-free-text-size-input">Size</label>
<input type="range" id="editorFreeTextFontSize" class="editorParamsSlider" value="10" min="5" max="100"
step="1" tabindex="104">
</div>
</div>
</div>
<div class="editorParamsToolbar hidden doorHangerRight" id="editorInkParamsToolbar">
<div class="editorParamsToolbarContainer">
<div class="editorParamsSetter">
<label for="editorInkColor" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-ink-color-input">Color</label>
<input type="color" id="editorInkColor" class="editorParamsColor" tabindex="105">
</div>
<div class="editorParamsSetter">
<label for="editorInkThickness" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-ink-thickness-input">Thickness</label>
<input type="range" id="editorInkThickness" class="editorParamsSlider" value="1" min="1" max="20" step="1"
tabindex="106">
</div>
<div class="editorParamsSetter">
<label for="editorInkOpacity" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-ink-opacity-input">Opacity</label>
<input type="range" id="editorInkOpacity" class="editorParamsSlider" value="100" min="1" max="100" step="1"
tabindex="107">
</div>
</div>
</div>
<div class="editorParamsToolbar hidden doorHangerRight" id="editorStampParamsToolbar">
<div class="editorParamsToolbarContainer">
<button id="editorStampAddImage" class="secondaryToolbarButton" title="Add image" tabindex="108"
data-l10n-id="pdfjs-editor-stamp-add-image-button">
<span class="editorParamsLabel" data-l10n-id="pdfjs-editor-stamp-add-image-button-label">Add
image</span>
</button>
</div>
</div>
<div id="secondaryToolbar" class="secondaryToolbar hidden doorHangerRight">
<div id="secondaryToolbarButtonContainer">
<button id="secondaryOpenFile" class="secondaryToolbarButton visibleMediumView" title="Open File"
tabindex="51" data-l10n-id="pdfjs-open-file-button">
<span data-l10n-id="pdfjs-open-file-button-label">Open</span>
</button>
<button id="secondaryPrint" class="secondaryToolbarButton visibleMediumView" title="Print" tabindex="52"
data-l10n-id="pdfjs-print-button">
<span data-l10n-id="pdfjs-print-button-label">Print</span>
</button>
<button id="secondaryDownload" class="secondaryToolbarButton visibleMediumView" title="Save" tabindex="53"
data-l10n-id="pdfjs-save-button">
<span data-l10n-id="pdfjs-save-button-label">Save</span>
</button>
<div class="horizontalToolbarSeparator"></div>
<button id="presentationMode" class="secondaryToolbarButton" title="Switch to Presentation Mode" tabindex="54"
data-l10n-id="pdfjs-presentation-mode-button">
<span data-l10n-id="pdfjs-presentation-mode-button-label">Presentation Mode</span>
</button>
<a href="#" id="viewBookmark" class="secondaryToolbarButton" title="Current Page (View URL from Current Page)"
tabindex="55" data-l10n-id="pdfjs-bookmark-button">
<span data-l10n-id="pdfjs-bookmark-button-label">Current Page</span>
</a>
<div id="viewBookmarkSeparator" class="horizontalToolbarSeparator"></div>
<button id="firstPage" class="secondaryToolbarButton" title="Go to First Page" tabindex="56"
data-l10n-id="pdfjs-first-page-button">
<span data-l10n-id="pdfjs-first-page-button-label">Go to First Page</span>
</button>
<button id="lastPage" class="secondaryToolbarButton" title="Go to Last Page" tabindex="57"
data-l10n-id="pdfjs-last-page-button">
<span data-l10n-id="pdfjs-last-page-button-label">Go to Last Page</span>
</button>
<div class="horizontalToolbarSeparator"></div>
<button id="pageRotateCw" class="secondaryToolbarButton" title="Rotate Clockwise" tabindex="58"
data-l10n-id="pdfjs-page-rotate-cw-button">
<span data-l10n-id="pdfjs-page-rotate-cw-button-label">Rotate Clockwise</span>
</button>
<button id="pageRotateCcw" class="secondaryToolbarButton" title="Rotate Counterclockwise" tabindex="59"
data-l10n-id="pdfjs-page-rotate-ccw-button">
<span data-l10n-id="pdfjs-page-rotate-ccw-button-label">Rotate Counterclockwise</span>
</button>
<div class="horizontalToolbarSeparator"></div>
<div id="cursorToolButtons" role="radiogroup">
<button id="cursorSelectTool" class="secondaryToolbarButton toggled" title="Enable Text Selection Tool"
tabindex="60" data-l10n-id="pdfjs-cursor-text-select-tool-button" role="radio" aria-checked="true">
<span data-l10n-id="pdfjs-cursor-text-select-tool-button-label">Text Selection Tool</span>
</button>
<button id="cursorHandTool" class="secondaryToolbarButton" title="Enable Hand Tool" tabindex="61"
data-l10n-id="pdfjs-cursor-hand-tool-button" role="radio" aria-checked="false">
<span data-l10n-id="pdfjs-cursor-hand-tool-button-label">Hand Tool</span>
</button>
</div>
<div class="horizontalToolbarSeparator"></div>
<div id="scrollModeButtons" role="radiogroup">
<button id="scrollPage" class="secondaryToolbarButton" title="Use Page Scrolling" tabindex="62"
data-l10n-id="pdfjs-scroll-page-button" role="radio" aria-checked="false">
<span data-l10n-id="pdfjs-scroll-page-button-label">Page Scrolling</span>
</button>
<button id="scrollVertical" class="secondaryToolbarButton toggled" title="Use Vertical Scrolling"
tabindex="63" data-l10n-id="pdfjs-scroll-vertical-button" role="radio" aria-checked="true">
<span data-l10n-id="pdfjs-scroll-vertical-button-label">Vertical Scrolling</span>
</button>
<button id="scrollHorizontal" class="secondaryToolbarButton" title="Use Horizontal Scrolling" tabindex="64"
data-l10n-id="pdfjs-scroll-horizontal-button" role="radio" aria-checked="false">
<span data-l10n-id="pdfjs-scroll-horizontal-button-label">Horizontal Scrolling</span>
</button>
<button id="scrollWrapped" class="secondaryToolbarButton" title="Use Wrapped Scrolling" tabindex="65"
data-l10n-id="pdfjs-scroll-wrapped-button" role="radio" aria-checked="false">
<span data-l10n-id="pdfjs-scroll-wrapped-button-label">Wrapped Scrolling</span>
</button>
</div>
<div class="horizontalToolbarSeparator"></div>
<div id="spreadModeButtons" role="radiogroup">
<button id="spreadNone" class="secondaryToolbarButton toggled" title="Do not join page spreads"
tabindex="66" data-l10n-id="pdfjs-spread-none-button" role="radio" aria-checked="true">
<span data-l10n-id="pdfjs-spread-none-button-label">No Spreads</span>
</button>
<button id="spreadOdd" class="secondaryToolbarButton"
title="Join page spreads starting with odd-numbered pages" tabindex="67"
data-l10n-id="pdfjs-spread-odd-button" role="radio" aria-checked="false">
<span data-l10n-id="pdfjs-spread-odd-button-label">Odd Spreads</span>
</button>
<button id="spreadEven" class="secondaryToolbarButton"
title="Join page spreads starting with even-numbered pages" tabindex="68"
data-l10n-id="pdfjs-spread-even-button" role="radio" aria-checked="false">
<span data-l10n-id="pdfjs-spread-even-button-label">Even Spreads</span>
</button>
</div>
<div class="horizontalToolbarSeparator"></div>
<button id="documentProperties" class="secondaryToolbarButton" title="Document Properties…" tabindex="69"
data-l10n-id="pdfjs-document-properties-button" aria-controls="documentPropertiesDialog">
<span data-l10n-id="pdfjs-document-properties-button-label">Document Properties…</span>
</button>
</div>
</div> <!-- secondaryToolbar -->
<div class="toolbar">
<div id="toolbarContainer">
<div id="toolbarViewer">
<div id="toolbarViewerLeft">
<div id="showMoreBtnContainer" class="splitToolbarButton">
<button id="showMoreBtn" class="btn-primary">
<span id="showMoreBtnIcon" class="material-symbols-rounded">more_horiz</span>
</button>
</div>
<button id="sidebarToggle" class="toolbarButton mt-2" tabindex="11"
data-l10n-id="pdfjs-toggle-sidebar-button" th:title="#{redact.toggleSidebar}" aria-expanded="false"
aria-controls="sidebarContainer">
</button>
<div class="toolbarButtonSpacer d-none"></div>
<button id="viewFind" class="d-none toolbarButton" title="Find in Document" tabindex="12"
data-l10n-id="pdfjs-findbar-button" aria-expanded="false" aria-controls="findbar">
<span data-l10n-id="pdfjs-findbar-button-label">Find</span>
</button>
<div class="splitToolbarButton hiddenSmallView">
<button class="toolbarButton btn-secondary" th:title="#{redact.previousPage}" id="previous"
tabindex="13" data-l10n-id="pdfjs-previous-button">
</button>
<div class="splitToolbarButtonSeparator d-none"></div>
<button class="toolbarButton btn-secondary" th:title="#{redact.nextPage}" id="next" tabindex="14"
data-l10n-id="pdfjs-next-button">
</button>
</div>
<span class="loadingInput start">
<input type="number" id="pageNumber" class="toolbarField" value="1" min="1" tabindex="15"
data-l10n-id="pdfjs-page-input" autocomplete="off">
</span>
<span id="numPages" class="toolbarLabel"></span>
<img class="main-icon user-select-none" th:src="@{'/favicon.svg'}" alt="icon">
</div>
<div id="toolbarViewerMiddle">
<div class="splitToolbarButton">
<button id="zoomOut" th:title="#{redact.zoomOut}" class="toolbarButton btn-primary" tabindex="21"
data-l10n-id="pdfjs-zoom-out-button">
</button>
<div class="splitToolbarButtonSeparator"></div>
<button id="zoomIn" th:title="#{redact.zoomIn}" class="toolbarButton btn-primary" tabindex="22"
data-l10n-id="pdfjs-zoom-in-button">
</button>
</div>
<span id="scaleSelectContainer" class="dropdownToolbarButton">
<select id="scaleSelect" tabindex="23" data-l10n-id="pdfjs-zoom-select">
<option id="pageAutoOption" value="auto" selected="selected" data-l10n-id="pdfjs-page-scale-auto">
Automatic Zoom</option>
<option id="pageActualOption" value="page-actual" data-l10n-id="pdfjs-page-scale-actual">
Actual Size</option>
<option id="pageFitOption" value="page-fit" data-l10n-id="pdfjs-page-scale-fit">Page
Fit
</option>
<option id="pageWidthOption" value="page-width" data-l10n-id="pdfjs-page-scale-width">
Page Width</option>
<option id="customScaleOption" value="custom" disabled="disabled" hidden="true"
data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 0 }'>0%</option>
<option value="0.5" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 50 }'>
50%</option>
<option value="0.75" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 75 }'>75%
</option>
<option value="1" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 100 }'>
100%</option>
<option value="1.25" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 125 }'>125%
</option>
<option value="1.5" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 150 }'>150%
</option>
<option value="2" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 200 }'>
200%</option>
<option value="3" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 300 }'>
300%</option>
<option value="4" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 400 }'>
400%</option>
</select>
</span>
</div>
<div id="redactionsToolbarViewer" class="splitToolbarButton d-flex">
<button id="man-text-select-redact" th:title="#{redact.textBasedRedaction}" class="btn-primary"
tabindex="22">
<span id="text-selection" class="material-symbols-rounded user-select-none pe-none">text_select_start
</span>
</button>
<button id="man-shape-redact" th:title="#{redact.boxRedaction}" class="btn-primary" tabindex="22">
<span id="shape-selection" class="material-symbols-rounded user-select-none pe-none">pageless
</span>
</button>
<button id="redactionsPaletteContainer" th:title="#{redact.colourPicker}" class="btn-primary">
<label id="redactions-palette" class="material-symbols-rounded palette-color text-center"
style="--palette-color: #000000;">
palette
<input type="color" name="color-picker">
</label>
</button>
</div>
<div id="toolbarViewerRight">
<div class="splitToolbarButton">
<button id="pdfToImageBtn" class="btn-success">
<span id="pdfToImageBtnIcon" th:title="#{redact.convertPDFToImageLabel}"
class="material-symbols-rounded">
image
</span>
</button>
<button id="pageBasedRedactionBtn" class="btn-primary">
<span id="pageBasedRedactionBtnIcon" th:title="#{redact.pageBasedRedaction}"
class="material-symbols-rounded">
document_scanner
</span>
</button>
<button id="uploadBtn" class="btn-primary">
<span id="uploadBtnIcon" class="material-symbols-rounded" th:title="#{redact.upload}"> upload </span>
</button>
<button id="downloadBtn" class="btn-primary">
<span id="downloadBtnIcon" th:title="#{redact.export}" class="material-symbols-rounded">
download
</span>
</button>
</div>
<div id="editorModeButtons" class="splitToolbarButton toggled" role="radiogroup">
<button id="editorHighlight" class="toolbarButton" hidden="true" disabled="disabled" title="Highlight"
role="radio" aria-checked="false" aria-controls="editorHighlightParamsToolbar" tabindex="31"
data-l10n-id="pdfjs-editor-highlight-button">
<span data-l10n-id="pdfjs-editor-highlight-button-label">Highlight</span>
</button>
<button id="openFile" class="toolbarButton hiddenMediumView d-none" title="Open File" role="radio"
aria-checked="false" tabindex="33" data-l10n-id="pdfjs-open-file-button">
<span data-l10n-id="pdfjs-open-file-button-label">Open</span>
</button>
<button id="print" class="d-none toolbarButton hiddenMediumView d-none" title="Print" role="radio"
aria-checked="false" tabindex="34" data-l10n-id="pdfjs-print-button">
<span data-l10n-id="pdfjs-print-button-label">Print</span>
</button>
<button id="download" class="d-none toolbarButton hiddenMediumView d-none" title="Save" role="radio"
aria-checked="false" tabindex="35" data-l10n-id="pdfjs-save-button">
<span data-l10n-id="pdfjs-save-button-label">Save</span>
</button>
<button id="editorStamp" class="d-none toolbarButton hiddenMediumView d-none" title="Add or edit images"
role="radio" aria-checked="false" aria-controls="editorStampParamsToolbar" tabindex="36"
data-l10n-id="pdfjs-editor-stamp-button">
<span data-l10n-id="pdfjs-editor-stamp-button-label">Add or edit images</span>
</button>
</div>
<div id="editorModeSeparator" class="d-none verticalToolbarSeparator"></div>
<button id="editorFreeText" class="d-none toolbarButton hiddenMediumView" title="Text" tabindex="41"
data-l10n-id="pdfjs-editor-free-text-button">
<span data-l10n-id="pdfjs-editor-free-text-button-label">Text</span>
</button>
<button id="editorInk" class="d-none toolbarButton hiddenMediumView" title="Draw" tabindex="42"
data-l10n-id="pdfjs-editor-ink-button">
<span data-l10n-id="pdfjs-editor-ink-button-label">Draw</span>
</button>
<div class="verticalToolbarSeparator hiddenMediumView"></div>
<button id="secondaryToolbarToggle" class="toolbarButton d-none" title="Tools" tabindex="43"
data-l10n-id="pdfjs-tools-button">
<span data-l10n-id="pdfjs-tools-button-label">Tools</span>
</button>
</div>
</div>
<div id="loadingBar">
<div class="progress">
<div class="glimmer">
</div>
</div>
</div>
</div>
</div>
<div id="viewerContainer" tabindex="0">
<div id="viewer" class="pdfViewer"></div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</div> <!-- mainContainer -->
<div id="dialogContainer">
<dialog id="passwordDialog">
<div class="row">
<label for="password" id="passwordText" data-l10n-id="pdfjs-password-label">Enter the password to open
this PDF file:</label>
</div>
<div class="row">
<input type="password" id="password" class="toolbarField">
</div>
<div class="buttonRow">
<button id="passwordCancel" class="dialogButton"><span
data-l10n-id="pdfjs-password-cancel-button">Cancel</span></button>
<button id="passwordSubmit" class="dialogButton"><span
data-l10n-id="pdfjs-password-ok-button">OK</span></button>
</div>
</dialog>
<dialog id="documentPropertiesDialog">
<div class="row">
<span id="fileNameLabel" data-l10n-id="pdfjs-document-properties-file-name">File name:</span>
<p id="fileNameField" aria-labelledby="fileNameLabel">-</p>
</div>
<div class="row">
<span id="fileSizeLabel" data-l10n-id="pdfjs-document-properties-file-size">File size:</span>
<p id="fileSizeField" aria-labelledby="fileSizeLabel">-</p>
</div>
<div class="separator"></div>
<div class="row">
<span id="titleLabel" data-l10n-id="pdfjs-document-properties-title">Title:</span>
<p id="titleField" aria-labelledby="titleLabel">-</p>
</div>
<div class="row">
<span id="authorLabel" data-l10n-id="pdfjs-document-properties-author">Author:</span>
<p id="authorField" aria-labelledby="authorLabel">-</p>
</div>
<div class="row">
<span id="subjectLabel" data-l10n-id="pdfjs-document-properties-subject">Subject:</span>
<p id="subjectField" aria-labelledby="subjectLabel">-</p>
</div>
<div class="row">
<span id="keywordsLabel" data-l10n-id="pdfjs-document-properties-keywords">Keywords:</span>
<p id="keywordsField" aria-labelledby="keywordsLabel">-</p>
</div>
<div class="row">
<span id="creationDateLabel" data-l10n-id="pdfjs-document-properties-creation-date">Creation
Date:</span>
<p id="creationDateField" aria-labelledby="creationDateLabel">-</p>
</div>
<div class="row">
<span id="modificationDateLabel" data-l10n-id="pdfjs-document-properties-modification-date">Modification
Date:</span>
<p id="modificationDateField" aria-labelledby="modificationDateLabel">-</p>
</div>
<div class="row">
<span id="creatorLabel" data-l10n-id="pdfjs-document-properties-creator">Creator:</span>
<p id="creatorField" aria-labelledby="creatorLabel">-</p>
</div>
<div class="separator"></div>
<div class="row">
<span id="producerLabel" data-l10n-id="pdfjs-document-properties-producer">PDF Producer:</span>
<p id="producerField" aria-labelledby="producerLabel">-</p>
</div>
<div class="row">
<span id="versionLabel" data-l10n-id="pdfjs-document-properties-version">PDF Version:</span>
<p id="versionField" aria-labelledby="versionLabel">-</p>
</div>
<div class="row">
<span id="pageCountLabel" data-l10n-id="pdfjs-document-properties-page-count">Page Count:</span>
<p id="pageCountField" aria-labelledby="pageCountLabel">-</p>
</div>
<div class="row">
<span id="pageSizeLabel" data-l10n-id="pdfjs-document-properties-page-size">Page Size:</span>
<p id="pageSizeField" aria-labelledby="pageSizeLabel">-</p>
</div>
<div class="separator"></div>
<div class="row">
<span id="linearizedLabel" data-l10n-id="pdfjs-document-properties-linearized">Fast Web View:</span>
<p id="linearizedField" aria-labelledby="linearizedLabel">-</p>
</div>
<div class="buttonRow">
<button id="documentPropertiesClose" class="dialogButton"><span
data-l10n-id="pdfjs-document-properties-close-button">Close</span></button>
</div>
</dialog>
<dialog class="dialog altText" id="altTextDialog" aria-labelledby="dialogLabel"
aria-describedby="dialogDescription">
<div id="altTextContainer" class="mainContainer">
<div id="overallDescription">
<span id="dialogLabel" data-l10n-id="pdfjs-editor-alt-text-dialog-label" class="title">Choose an
option</span>
<span id="dialogDescription" data-l10n-id="pdfjs-editor-alt-text-dialog-description">
Alt text (alternative text) helps when people cant see the image or when it doesnt load.
</span>
</div>
<div id="addDescription">
<div class="radio">
<div class="radioButton">
<input type="radio" id="descriptionButton" name="altTextOption" tabindex="0"
aria-describedby="descriptionAreaLabel" checked>
<label for="descriptionButton" data-l10n-id="pdfjs-editor-alt-text-add-description-label">Add a
description</label>
</div>
<div class="radioLabel">
<span id="descriptionAreaLabel" data-l10n-id="pdfjs-editor-alt-text-add-description-description">
Aim for 1-2 sentences that describe the subject, setting, or actions.
</span>
</div>
</div>
<div class="descriptionArea">
<textarea id="descriptionTextarea"
placeholder="For example, “A young man sits down at a table to eat a meal”"
aria-labelledby="descriptionAreaLabel" data-l10n-id="pdfjs-editor-alt-text-textarea"
tabindex="0"></textarea>
</div>
</div>
<div id="markAsDecorative">
<div class="radio">
<div class="radioButton">
<input type="radio" id="decorativeButton" name="altTextOption" aria-describedby="decorativeLabel">
<label for="decorativeButton" data-l10n-id="pdfjs-editor-alt-text-mark-decorative-label">Mark as
decorative</label>
</div>
<div class="radioLabel">
<span id="decorativeLabel" data-l10n-id="pdfjs-editor-alt-text-mark-decorative-description">
This is used for ornamental images, like borders or watermarks.
</span>
</div>
</div>
</div>
<div id="buttons">
<button id="altTextCancel" class="secondaryButton" tabindex="0"><span
data-l10n-id="pdfjs-editor-alt-text-cancel-button">Cancel</span></button>
<button id="altTextSave" class="primaryButton" tabindex="0"><span
data-l10n-id="pdfjs-editor-alt-text-save-button">Save</span></button>
</div>
</div>
</dialog>
<dialog id="printServiceDialog" style="min-width: 200px;">
<div class="row">
<span data-l10n-id="pdfjs-print-progress-message">Preparing document for printing…</span>
</div>
<div class="row">
<progress value="0" max="100"></progress>
<span data-l10n-id="pdfjs-print-progress-percent" data-l10n-args='{ "progress": 0 }'
class="relative-progress">0%</span>
</div>
<div class="buttonRow">
<button id="printCancel" class="dialogButton"><span
data-l10n-id="pdfjs-print-progress-close-button">Cancel</span></button>
</div>
</dialog>
</div> <!-- dialogContainer -->
</div> <!-- outerContainer -->
<div id="printContainer" class="d-none"></div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</body>
</html>
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{removeCertSign.title}, header=#{removeCertSign.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 security">remove_moderator</span>
<span class="tool-header-text" th:text="#{removeCertSign.header}"></span>
</div>
<form th:action="@{'api/v1/security/remove-cert-sign'}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label th:text="#{removeCertSign.selectPDF}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
</div>
<div class="mb-3 text-left">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removeCertSign.submit}"></button>
</div>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{removePassword.title}, header=#{removePassword.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 security">lock_open_right</span>
<span class="tool-header-text" th:text="#{removePassword.header}"></span>
</div>
<form th:action="@{'api/v1/security/remove-password'}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label th:text="#{removePassword.selectText.1}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
</div>
<div class="mb-3">
<label th:text="#{removePassword.selectText.2}"></label>
<input type="password" class="form-control" id="password" name="password">
</div>
<br>
<div class="mb-3 text-left">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removePassword.submit}"></button>
</div>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{remove-watermark.title}, header=#{remove-watermark.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 security">water_drop</span>
<span class="tool-header-text" th:text="#{remove-watermark.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'api/v1/security/remove-watermark'}">
<div class="mb-3">
<label th:text="#{remove-watermark.selectText.1}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
</div>
<div class="mb-3">
<label for="watermarkText" th:text="#{remove-watermark.selectText.2}"></label>
<input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required />
</div>
<div class="mb-3 text-center">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{remove-watermark.submit}"></button>
</div>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{sanitizePDF.title}, header=#{sanitizePDF.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 security">sanitizer</span>
<span class="tool-header-text" th:text="#{sanitizePDF.header}"></span>
</div>
<form th:action="@{'api/v1/security/sanitize-pdf'}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="removeJavaScript" name="removeJavaScript" checked>
<label for="removeJavaScript" th:text="#{sanitizePDF.selectText.1}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="removeEmbeddedFiles" name="removeEmbeddedFiles" checked>
<label for="removeEmbeddedFiles" th:text="#{sanitizePDF.selectText.2}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="removeXMPMetadata" name="removeXMPMetadata">
<label for="removeXMPMetadata" th:text="#{sanitizePDF.selectText.3}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="removeMetadata" name="removeMetadata">
<label for="removeMetadata" th:text="#{sanitizePDF.selectText.6}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="removeLinks" name="removeLinks">
<label for="removeLinks" th:text="#{sanitizePDF.selectText.4}"></label>
</div>
<div class="form-check ms-3">
<input type="checkbox" id="removeFonts" name="removeFonts">
<label for="removeFonts" th:text="#{sanitizePDF.selectText.5}"></label>
</div>
<br>
<div class="mb-3 text-left">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{sanitizePDF.submit}"></button>
</div>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,265 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{validateSignature.title}, header=#{validateSignature.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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 security">verified</span>
<span class="tool-header-text" th:text="#{validateSignature.header}"></span>
</div>
<form id="pdfForm" th:action="@{'api/v1/security/validate-signature'}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label th:text="#{validateSignature.selectPDF}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, remoteCall='false', accept='application/pdf')}"></div>
</div>
<div class="mb-3">
<label th:text="#{validateSignature.selectCustomCert}" ></label>
<div th:replace="~{fragments/common :: fileSelector(name='certFile', multipleInputsForSingleRequest=false, notRequired=true, remoteCall='false', accept='.cer,.crt,.pem')}"></div>
</div>
<div class="mb-3 text-left">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{validateSignature.submit}"></button>
</div>
</form>
<!-- Results section -->
<div id="results" style="display: none;">
<h4 th:text="#{validateSignature.results}"></h4>
<div id="signatures-list"></div>
</div>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script th:inline="javascript">
const translations = {
signature: /*[[#{validateSignature.signature}]]*/,
signatureInfo: /*[[#{validateSignature.signature.info}]]*/,
certInfo: /*[[#{validateSignature.cert.info}]]*/,
signer: /*[[#{validateSignature.signer}]]*/,
date: /*[[#{validateSignature.date}]]*/,
reason: /*[[#{validateSignature.reason}]]*/,
location: /*[[#{validateSignature.location}]]*/,
noSignatures: /*[[#{validateSignature.noSignatures}]]*/,
statusValid: /*[[#{validateSignature.status.valid}]]*/,
statusInvalid: /*[[#{validateSignature.status.invalid}]]*/,
mathValid: /*[[#{validateSignature.signature.mathValid}]]*/,
chainInvalid: /*[[#{validateSignature.chain.invalid}]]*/,
trustInvalid: /*[[#{validateSignature.trust.invalid}]]*/,
certExpired: /*[[#{validateSignature.cert.expired}]]*/,
certRevoked: /*[[#{validateSignature.cert.revoked}]]*/,
certIssuer: /*[[#{validateSignature.cert.issuer}]]*/,
certSubject: /*[[#{validateSignature.cert.subject}]]*/,
certSerialNumber: /*[[#{validateSignature.cert.serialNumber}]]*/,
certValidFrom: /*[[#{validateSignature.cert.validFrom}]]*/,
certValidUntil: /*[[#{validateSignature.cert.validUntil}]]*/,
certAlgorithm: /*[[#{validateSignature.cert.algorithm}]]*/,
certKeySize: /*[[#{validateSignature.cert.keySize}]]*/,
certBits: /*[[#{validateSignature.cert.bits}]]*/,
certVersion: /*[[#{validateSignature.cert.version}]]*/,
certKeyUsage: /*[[#{validateSignature.cert.keyUsage}]]*/,
certSelfSigned: /*[[#{validateSignature.cert.selfSigned}]]*/,
yes: /*[[#{yes}]]*/,
no: /*[[#{no}]]*/,
selectPDF: /*[[#{validateSignature.selectPDF}]]*/
};
function escapeHtml(unsafe) {
return unsafe
?.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;") || 'N/A';
}
document.querySelector('#pdfForm').addEventListener('submit', async (e) => {
e.preventDefault();
const fileInput = document.getElementById('fileInput-input');
const certInput = document.getElementById('certFile-input');
if (!fileInput.files.length) {
alert(escapeHtml(translations.selectPDF));
return;
}
const results = [];
for (const file of fileInput.files) {
const formData = new FormData();
formData.append('fileInput', file);
if (certInput.files.length > 0) {
formData.append('certFile', certInput.files[0]);
}
try {
const response = await window.fetchWithCsrf(e.target.action, {
method: 'POST',
body: formData
});
const fileResults = await response.json();
fileResults.forEach(result => {
result.fileName = file.name;
});
results.push(...fileResults);
} catch (error) {
results.push({
fileName: file.name,
valid: false,
errorMessage: `${escapeHtml(translations.statusInvalid)}: ${escapeHtml(error.message)}`
});
}
}
displayResults(results);
});
function displayResults(results) {
const resultDiv = document.getElementById('results');
const listDiv = document.getElementById('signatures-list');
listDiv.innerHTML = '';
resultDiv.style.display = 'block';
if (!results || results.length === 0) {
listDiv.innerHTML = `<div class="alert alert-warning">${escapeHtml(translations.noSignatures)}</div>`;
return;
}
results.forEach((result, index) => {
const signatureDiv = document.createElement('div');
signatureDiv.className = 'card mb-3';
let validationClass = 'alert-danger';
let validationIssues = [];
if (!result.valid) {
validationIssues.push(`${escapeHtml(translations.statusInvalid)}: ${escapeHtml(result.errorMessage || '')}`);
} else {
const isFullyValid = result.valid &&
result.chainValid &&
result.trustValid &&
result.notExpired &&
result.notRevoked;
if (isFullyValid) {
validationClass = 'alert-success';
validationIssues.push(escapeHtml(translations.statusValid));
} else {
validationClass = 'alert-warning';
validationIssues.push(escapeHtml(translations.mathValid));
if (!result.chainValid) {
validationIssues.push(escapeHtml(translations.chainInvalid));
}
if (!result.trustValid) {
validationIssues.push(escapeHtml(translations.trustInvalid));
}
if (!result.notExpired) {
validationIssues.push(escapeHtml(translations.certExpired));
}
if (result.trustValid && result.chainValid && !result.notRevoked) {
validationIssues.push(escapeHtml(translations.certRevoked));
}
}
}
let statusMessage = validationIssues[0];
if (validationIssues.length > 1) {
statusMessage += '<ul class="mb-0 mt-2">';
for (let i = 1; i < validationIssues.length; i++) {
statusMessage += `<li>${validationIssues[i]}</li>`;
}
statusMessage += '</ul>';
}
let content = `
<div class="card-body">
${results.length > 1 ? `<h4 class="mb-3">${escapeHtml(translations.signature)} ${index + 1}</h4>` : ''}
<div class="alert ${validationClass}">
${statusMessage}
</div>
<div class="card-text">
<h5>${escapeHtml(translations.signatureInfo)}</h5>
<table class="table table-borderless">
<tr>
<td><strong>${escapeHtml(translations.signer)}:</strong></td>
<td>${escapeHtml(result.signerName)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.date)}:</strong></td>
<td>${escapeHtml(result.signatureDate)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.reason)}:</strong></td>
<td>${escapeHtml(result.reason)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.location)}:</strong></td>
<td>${escapeHtml(result.location)}</td>
</tr>
</table>
<h5>${escapeHtml(translations.certInfo)}</h5>
<table class="table table-borderless">
<tr>
<td><strong>${escapeHtml(translations.certIssuer)}:</strong></td>
<td>${escapeHtml(result.issuerDN)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certSubject)}:</strong></td>
<td>${escapeHtml(result.subjectDN)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certSerialNumber)}:</strong></td>
<td>${escapeHtml(result.serialNumber)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certValidFrom)}:</strong></td>
<td>${escapeHtml(result.validFrom)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certValidUntil)}:</strong></td>
<td>${escapeHtml(result.validUntil)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certAlgorithm)}:</strong></td>
<td>${escapeHtml(result.signatureAlgorithm)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certKeySize)}:</strong></td>
<td>${result.keySize ? escapeHtml(result.keySize) + ' ' + escapeHtml(translations.certBits) : 'N/A'}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certVersion)}:</strong></td>
<td>${escapeHtml(result.version)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certKeyUsage)}:</strong></td>
<td>${result.keyUsages ? result.keyUsages.map(usage => escapeHtml(usage)).join(', ') : 'N/A'}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certSelfSigned)}:</strong></td>
<td>${result.selfSigned ? escapeHtml(translations.yes) : escapeHtml(translations.no)}</td>
</tr>
</table>
</div>
</div>
`;
signatureDiv.innerHTML = content;
listDiv.appendChild(signatureDiv);
});
}
</script>
</body>
</html>
@@ -0,0 +1,241 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{sign.title}, header=#{sign.header})}"></th:block>
<link rel="stylesheet" th:href="@{'/css/sign.css'}">
<th:block th:each="font : ${fonts}">
<style th:inline="text">
@font-face {
font-family: "[[${font.name}]]";
src: url('fonts/[[${font.name}]].[[${font.extension}]]') format('[[${font.type}]]');
}
#font-select option[value="[[${font.name}]]"] {
font-family: "[[${font.name}]]",
cursive
!important;
}
</style>
</th:block>
<script th:src="@{'/js/thirdParty/signature_pad.umd.min.js'}"></script>
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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-7 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 -->
<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>
<div class="tab-group show-on-file-selected">
<div class="tab-container"th:data-title="#{sign.upload}">
<div
th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=false, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
</div>
</div>
<div class="tab-container drawing-pad-container" th:data-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>
<button id="signature-undo-button" class="btn btn-outline-secondary mt-2" th:text="#{sign.undo}"
onclick="undoDraw()"></button>
<button id="signature-redo-button" class="btn btn-outline-secondary mt-2" th:text="#{sign.redo}"
onclick="redoDraw()"></button>
</div>
<div class="tab-container" th:data-title="#{sign.saved}">
<div class="saved-signatures-section" th:if="${not #lists.isEmpty(signatures)}">
<!-- Preview Modal -->
<div class="modal fade" id="signaturePreview" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><span id="previewFileName"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<img id="previewImage" src="" alt="Signature Preview" style="max-width: 100%;">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
th:text="#{close}"></button>
<button type="button" class="btn btn-primary" onclick="addSignatureFromPreview()"
th:text="#{addToDoc}">Add to Document</button>
</div>
</div>
</div>
</div>
<!-- Personal Signatures -->
<div th:if="${not #lists.isEmpty(signatures.?[category == 'Personal'])}">
<h5 th:text="#{sign.personalSigs}"></h5>
<div class="selected-files flex-wrap" style="position: relative; display:flex">
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Personal'}"
class="small-file-container-saved d-flex flex-column justify-content-center align-items-center">
<div class="drag-icon"><svg xmlns="http://www.w3.org/2000/svg" height="24px"
viewBox="0 -960 960 960" width="24px" fill="#e8eaed">
<path
d="M360-160q-33 0-56.5-23.5T280-240q0-33 23.5-56.5T360-320q33 0 56.5 23.5T440-240q0 33-23.5 56.5T360-160Zm240 0q-33 0-56.5-23.5T520-240q0-33 23.5-56.5T600-320q33 0 56.5 23.5T680-240q0 33-23.5 56.5T600-160ZM360-400q-33 0-56.5-23.5T280-480q0-33 23.5-56.5T360-560q33 0 56.5 23.5T440-480q0 33-23.5 56.5T360-400Zm240 0q-33 0-56.5-23.5T520-480q0-33 23.5-56.5T600-560q33 0 56.5 23.5T680-480q0 33-23.5 56.5T600-400ZM360-640q-33 0-56.5-23.5T280-720q0-33 23.5-56.5T360-800q33 0 56.5 23.5T440-720q0 33-23.5 56.5T360-640Zm240 0q-33 0-56.5-23.5T520-720q0-33 23.5-56.5T600-800q33 0 56.5 23.5T680-720q0 33-23.5 56.5T600-640Z">
</path>
</svg></div>
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}" th:alt="${sig.fileName}"
th:data-filename="${sig.fileName}" style="width: 50px; height: 50px; object-fit: cover;" />
<div class="file-info d-flex flex-column align-items-center justify-content-center">
<div class="signature-name" th:text="${sig.fileName}"></div>
</div>
</div>
</div>
</div>
<!-- Shared Signatures -->
<div th:if="${not #lists.isEmpty(signatures.?[category == 'Shared'])}">
<h5 th:text="#{sign.sharedSigs}"></h5>
<div class="selected-files flex-wrap" style="position: relative; display: flex;">
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Shared'}"
class="small-file-container-saved d-flex flex-column justify-content-center align-items-center">
<div class="drag-icon"><svg xmlns="http://www.w3.org/2000/svg" height="24px"
viewBox="0 -960 960 960" width="24px" fill="#e8eaed">
<path
d="M360-160q-33 0-56.5-23.5T280-240q0-33 23.5-56.5T360-320q33 0 56.5 23.5T440-240q0 33-23.5 56.5T360-160Zm240 0q-33 0-56.5-23.5T520-240q0-33 23.5-56.5T600-320q33 0 56.5 23.5T680-240q0 33-23.5 56.5T600-160ZM360-400q-33 0-56.5-23.5T280-480q0-33 23.5-56.5T360-560q33 0 56.5 23.5T440-480q0 33-23.5 56.5T360-400Zm240 0q-33 0-56.5-23.5T520-480q0-33 23.5-56.5T600-560q33 0 56.5 23.5T680-480q0 33-23.5 56.5T600-400ZM360-640q-33 0-56.5-23.5T280-720q0-33 23.5-56.5T360-800q33 0 56.5 23.5T440-720q0 33-23.5 56.5T360-640Zm240 0q-33 0-56.5-23.5T520-720q0-33 23.5-56.5T600-800q33 0 56.5 23.5T680-720q0 33-23.5 56.5T600-640Z">
</path>
</svg></div>
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}" th:alt="${sig.fileName}"
th:data-filename="${sig.fileName}" style="width: 50px; height: 50px; object-fit: cover;" />
<div class="file-info d-flex flex-column align-items-center justify-content-center">
<div class="signature-name" th:text="${sig.fileName}"></div>
</div>
</div>
</div>
</div>
</div>
<div th:if="${#lists.isEmpty(signatures)}" class="text-center p-3">
<p th:text="#{sign.noSavedSigs}">No saved signatures found</p>
</div>
</div>
<div class="tab-container" th:data-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>
<div id="signFontSelection" class="custom-select form-control">
<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>
<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>
</div>
</div>
<script th:src="@{'/js/sign/signature-canvas.js'}"></script>
<!-- draggables box -->
<div class="draggable-buttons-box ignore-rtl">
<button class="btn btn-outline-secondary" th:title="#{sign.delete}"
onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())"
style="border-color: #C02223; color: #C02223; background: rgba(255, 0, 0, 0.1)">
<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" th:title="#{sign.addToAll}"
onclick="DraggableUtils.addAllPagesDraggableCanvas(DraggableUtils.getLastInteracted())">
<span class="material-symbols-rounded">
content_copy
</span>
</button>
<div id="rotation-controls" class="align-items-center" style="display: none;">
<div class="input-with-icon">
<span class="material-symbols-rounded icon" style="margin-right: 3px;">
screen_rotation
</span>
<input type="number" id="rotation-input" class="form-control form-control-sm me-2" value="0" step="10"
style="width: 6rem" />
</div>
</div>
<button id="ratioToggleBtn" th:title="#{sign.maintainRatio}" class="btn btn-outline-secondary"
onclick="DraggableUtils.toggleMaintainRatio()">
<span class="material-symbols-rounded">
Aspect_Ratio
</span>
</button>
<button class="btn btn-outline-secondary" th:title="#{sign.first}" onclick="goToFirstOrLastPage(false)"
style="margin-left:auto">
<span class="material-symbols-rounded">
keyboard_double_arrow_left
</span>
</button>
<button class="btn btn-outline-secondary" th:title="#{sign.previous}" id="incrementPage"
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">
<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" id="decrementPage" th:title="#{sign.next}"
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>
<button class="btn btn-outline-secondary" th:title="#{sign.last}" onclick="goToFirstOrLastPage(true)">
<span class="material-symbols-rounded">
keyboard_double_arrow_right
</span>
</button>
<button id="download-pdf" th:title="#{downloadPdf}" class="btn btn-outline-secondary"
style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)">
<span class="material-symbols-rounded">
download
</span>
</button>
</div>
<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 src="https://cdn.jsdelivr.net/npm/fabric@latest/dist/index.min.js"></script>
<script th:src="@{'/js/draggable-utils.js'}"></script>
<script type="module" th:src="@{'/js/pages/sign.js'}"></script>
</div>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</script>
</body>
</html>
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block
th:insert="~{fragments/common :: head(title=#{split-by-size-or-count.title}, header=#{split-by-size-or-count.header})}">
</th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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">
<svg class="material-symbols-rounded tool-header-icon advance">
<use xlink:href="/images/split-size.svg#icon-split-size"></use>
</svg>
<span class="tool-header-text" th:text="#{split-by-size-or-count.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/general/split-by-size-or-count'}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<label for="splitType" th:text="#{split-by-size-or-count.type.label}">Split Type</label>
<select id="splitType" name="splitType" class="form-control">
<option value="0" th:text="#{split-by-size-or-count.type.size}">Size</option>
<option value="1" th:text="#{split-by-size-or-count.type.pageCount}">Page Count</option>
<option value="2" th:text="#{split-by-size-or-count.type.docCount}">Document Count</option>
</select>
<br>
<label for="splitValue" th:text="#{split-by-size-or-count.value.label}">Split Value</label>
<input type="text" id="splitValue" name="splitValue" class="form-control" required
th:placeholder="#{split-by-size-or-count.value.placeholder}">
<br>
<button type="submit" id="submitBtn" class="btn btn-primary"
th:text="#{split-by-size-or-count.submit}">Submit</button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{splitByChapters.title}, header=#{splitByChapters.header})}">
</th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<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">
<svg class="material-symbols-rounded tool-header-icon advance">
<use xlink:href="/images/split-chapters.svg#icon-split-chapters"></use>
</svg>
<span class="tool-header-text" th:text="#{splitByChapters.header}"></span>
</div>
<form th:action="@{'/api/v1/general/split-pdf-by-chapters'}" method="post" enctype="multipart/form-data">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<div class="mb-3">
<label for="bookmarkLevel" th:text="#{splitByChapters.bookmarkLevel}"></label>
<input type="number" class="form-control" id="bookmarkLevel" name="bookmarkLevel" min="0" value="0"
required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="includeMetadata" name="includeMetadata">
<label class="form-check-label" for="includeMetadata"
th:text="#{splitByChapters.includeMetadata}"></label>
<input type="hidden" name="includeMetadata" value="false" />
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="allowDuplicates" name="allowDuplicates">
<label class="form-check-label" for="allowDuplicates"
th:text="#{splitByChapters.allowDuplicates}"></label>
<input type="hidden" name="allowDuplicates" value="false" />
</div>
<p>
<a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button"
aria-expanded="false" aria-controls="info" th:text="#{info}"></a>
</p>
<div class="collapse" id="info">
<p th:text="#{splitByChapters.desc.1}"></p>
<p th:text="#{splitByChapters.desc.2}"></p>
<p th:text="#{splitByChapters.desc.3}"></p>
<p th:text="#{splitByChapters.desc.4}"></p>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{splitByChapters.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{split-by-sections.title}, header=#{split-by-sections.header})}"></th:block>
<link rel="stylesheet" th:href="@{'/css/split-pdf-by-sections.css'}">
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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 advance">grid_on</span>
<span class="tool-header-text" th:text="#{split-by-sections.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/general/split-pdf-by-sections'}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<label for="horizontalDivisions" th:text="#{split-by-sections.horizontal.label}">Horizontal Divisions</label>
<input type="number" id="horizontalDivisions" name="horizontalDivisions" class="form-control" min="0" max="300" value="0" required th:placeholder="#{split-by-sections.horizontal.placeholder}">
<br>
<label for="verticalDivisions" th:text="#{split-by-sections.vertical.label}">Vertical Divisions</label>
<input type="number" id="verticalDivisions" name="verticalDivisions" class="form-control" min="0" max="300" required value="1" th:placeholder="#{split-by-sections.vertical.placeholder}">
<br>
<div class="mb-3 form-check">
<input type="checkbox" id="merge" name="merge">
<label for="merge" th:text="#{split-by-sections.merge}">merge PDFs into one</label>
</div>
<br>
<div id="pdfVisualAid" class="pdf-visual-aid"></div>
<script>
function updateVisualAid() {
const horizontalDivisions = document.getElementById('horizontalDivisions').value;
const verticalDivisions = document.getElementById('verticalDivisions').value;
const aid = document.getElementById('pdfVisualAid');
if(horizontalDivisions > 300) {
horizontalDivisions = 300
}
if(verticalDivisions > 300) {
verticalDivisions = 300
}
// Clear existing lines
aid.innerHTML = '';
// Add horizontal lines
for (let i = 0; i < horizontalDivisions; i++) {
const line = document.createElement('div');
line.classList.add('line');
line.style.width = '100%';
line.style.height = '1px';
line.style.top = `${((i + 1) / (parseInt(horizontalDivisions) + 1)) * 100}%`;
aid.appendChild(line);
}
// Add vertical lines
for (let i = 0; i < verticalDivisions; i++) {
const line = document.createElement('div');
line.classList.add('line');
line.style.height = '100%';
line.style.width = '1px';
line.style.left = `${((i + 1) / (parseInt(verticalDivisions) + 1)) * 100}%`;
aid.appendChild(line);
}
}
// Event listeners
document.getElementById('horizontalDivisions').addEventListener('input', updateVisualAid);
document.getElementById('verticalDivisions').addEventListener('input', updateVisualAid);
// Initial draw
updateVisualAid();
</script>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{split-by-sections.submit}">Submit</button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{split.title}, header=#{split.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<th:block th:insert="~{fragments/multi-toolAdvert.html :: multi-toolAdvert}"></th:block>
<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 organize">cut</span>
<span class="tool-header-text" th:text="#{split.header}"></span>
</div>
<form th:action="@{'/api/v1/general/split-pages'}" method="post" enctype="multipart/form-data">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<div class="mb-3">
<label for="pageNumbers" th:text="#{split.splitPages}"></label>
<input type="text" class="form-control" id="pageNumbers" name="pageNumbers" placeholder="1,3,5-10"
required>
</div>
<p>
<a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button"
aria-expanded="false" aria-controls="info" th:text="#{info}"></a>
</p>
<div class="collapse" id="info">
<p th:text="#{split.desc.1}"></p>
<p th:text="#{split.desc.2}"></p>
<p th:text="#{split.desc.3}"></p>
<p th:text="#{split.desc.4}"></p>
<p th:text="#{split.desc.5}"></p>
<p th:text="#{split.desc.6}"></p>
<p th:text="#{split.desc.7}"></p>
<p th:text="#{split.desc.8}"></p>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{split.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{endpointStatistics.title}, header=#{endpointStatistics.header})}"></th:block>
<script th:src="@{'/js/thirdParty/chart.umd.min.js'}"></script>
<link rel="stylesheet" th:href="@{'/css/usage.css'}">
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<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-9 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon">analytics</span>
<span class="tool-header-text" th:text="#{endpointStatistics.header}">Endpoint Statistics</span>
</div>
<!-- Statistics Summary Box -->
<div class="stats-box">
<div class="chart-controls">
<button id="top10Btn" class="btn btn-outline-primary active">
<span class="material-symbols-rounded">bar_chart</span>
<span th:text="#{endpointStatistics.top10}">Top 10</span>
</button>
<button id="top20Btn" class="btn btn-outline-primary">
<span class="material-symbols-rounded">data_usage</span>
<span th:text="#{endpointStatistics.top20}">Top 20</span>
</button>
<button id="allBtn" class="btn btn-outline-primary">
<span class="material-symbols-rounded">insights</span>
<span th:text="#{endpointStatistics.all}">All</span>
</button>
<button id="refreshBtn" class="btn btn-outline-secondary">
<span class="material-symbols-rounded">refresh</span>
<span th:text="#{endpointStatistics.refresh}">Refresh</span>
</button>
</div>
<div class="filter-controls">
<label class="filter-checkbox">
<input type="checkbox" id="hideHomeCheckbox" checked>
<span th:text="#{endpointStatistics.includeHomepage}">Include Homepage ('/')</span>
</label>
<label class="filter-checkbox">
<input type="checkbox" id="hideLoginCheckbox" checked>
<span th:text="#{endpointStatistics.includeLoginPage}">Include Login Page ('/login')</span>
</label>
</div>
<div class="my-4" style="color: var(--md-sys-color-on-surface); font-weight: 500;">
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.totalEndpoints} + ':'">Total Endpoints:</strong> <span id="totalEndpoints">0</span></span>
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.totalVisits} + ':'">Total Visits:</strong> <span id="totalVisits">0</span></span>
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.showing} + ':'">Showing:</strong> <span id="currentlyShowing">Top 10</span></span>
<span style="margin: 0 10px;"><strong th:text="#{endpointStatistics.selectedVisits} + ':'">Selected Visits:</strong> <span id="displayedVisits">0</span> (<span id="displayedPercentage">0</span>%)</span>
</div>
</div>
<!-- Chart Container -->
<div class="bg-card mt-3 mb-3">
<div class="chart-container">
<canvas id="endpointChart"></canvas>
</div>
</div>
<!-- Table for detailed data -->
<div class="bg-card mt-3 mb-3">
<table class="endpoint-table">
<thead>
<tr>
<th scope="col" style="width: 5%;">#</th>
<th scope="col" style="width: 55%;" th:text="#{endpointStatistics.endpoint}">Endpoint</th>
<th scope="col" style="width: 20%;" th:text="#{endpointStatistics.visits}">Visits</th>
<th scope="col" style="width: 20%;" th:text="#{endpointStatistics.percentage}">Percentage</th>
</tr>
</thead>
<tbody id="endpointTableBody">
<!-- Table rows will be dynamically generated -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script type="module" th:src="@{'/js/usage.js'}"></script>
<script th:inline="javascript">
const endpointStatsTranslations = {
all: /*[[#{endpointStatistics.all}]]*/ 'All',
top20: /*[[#{endpointStatistics.top20}]]*/ 'Top 20',
loading: /*[[#{endpointStatistics.loading}]]*/ 'Loading...',
failedToLoad: /*[[#{endpointStatistics.failedToLoad}]]*/ 'Failed to load endpoint data. Please try refreshing.',
home: /*[[#{endpointStatistics.home}]]*/ 'Home',
login: /*[[#{endpointStatistics.login}]]*/ 'Login',
top: /*[[#{endpointStatistics.top}]]*/ 'Top ',
numberOfVisits: /*[[#{endpointStatistics.numberOfVisits}]]*/ 'Number of Visits',
visitsTooltip: /*[[#{endpointStatistics.visitsTooltip}]]*/ 'Visits: {0} ({1}% of total)',
retry: /*[[#{endpointStatistics.retry}]]*/ 'Retry'
};
</script>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -0,0 +1,631 @@
<!DOCTYPE html>
<!--
Copyright 2012 Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Adobe CMap resources are covered by their own copyright but the same license:
Copyright 1990-2015 Adobe Systems Incorporated.
See https://github.com/adobe-type-tools/cmap-resources
-->
<html dir="ltr" mozdisallowselectionprint th:lang="${#locale.language}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{redact.title}, header=#{redact.header})}"></th:block>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="google" content="notranslate">
<title>PDF.js viewer</title>
<!-- Bootstrap -->
<script th:src="@{'/js/thirdParty/popper.min.js'}"></script>
<script th:src="@{'/js/thirdParty/bootstrap.min.js'}"></script>
<link rel="stylesheet" th:href="@{'/css/theme/componentes.css'}">
<link rel="stylesheet" th:href="@{'/css/navbar.css'}">
<!-- This snippet is used in production (included from view-pdf.html) -->
<link rel="resource" type="application/l10n" th:href="@{'/pdfjs-legacy/locale/locale.json'}">
<script th:src="@{'/pdfjs-legacy/pdf.mjs'}" type="module"></script>
<link rel="stylesheet" th:href="@{'/pdfjs-legacy/css/viewer.css'}">
<script th:src="@{'/pdfjs-legacy/js/viewer.mjs'}" type="module"></script>
</head>
<body tabindex="1" style="overflow: hidden;">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div id="outerContainer">
<div id="sidebarContainer">
<div id="toolbarSidebar">
<div id="toolbarSidebarLeft">
<div id="sidebarViewButtons" class="splitToolbarButton toggled" role="radiogroup">
<button id="viewThumbnail" class="toolbarButton toggled" title="Show Thumbnails" tabindex="2"
data-l10n-id="pdfjs-thumbs-button" role="radio" aria-checked="true" aria-controls="thumbnailView">
<span data-l10n-id="pdfjs-thumbs-button-label">Thumbnails</span>
</button>
<button id="viewOutline" class="toolbarButton"
title="Show Document Outline (double-click to expand/collapse all items)" tabindex="3"
data-l10n-id="pdfjs-document-outline-button" role="radio" aria-checked="false"
aria-controls="outlineView">
<span data-l10n-id="pdfjs-document-outline-button-label">Document Outline</span>
</button>
<button id="viewAttachments" class="toolbarButton" title="Show Attachments" tabindex="4"
data-l10n-id="pdfjs-attachments-button" role="radio" aria-checked="false" aria-controls="attachmentsView">
<span data-l10n-id="pdfjs-attachments-button-label">Attachments</span>
</button>
<button id="viewLayers" class="toolbarButton"
title="Show Layers (double-click to reset all layers to the default state)" tabindex="5"
data-l10n-id="pdfjs-layers-button" role="radio" aria-checked="false" aria-controls="layersView">
<span data-l10n-id="pdfjs-layers-button-label">Layers</span>
</button>
</div>
</div>
<div id="toolbarSidebarRight">
<div id="outlineOptionsContainer">
<div class="verticalToolbarSeparator"></div>
<button id="currentOutlineItem" class="toolbarButton" disabled="disabled" title="Find Current Outline Item"
tabindex="6" data-l10n-id="pdfjs-current-outline-item-button">
<span data-l10n-id="pdfjs-current-outline-item-button-label">Current Outline Item</span>
</button>
</div>
</div>
</div>
<div id="sidebarContent">
<div id="thumbnailView">
</div>
<div id="outlineView" class="hidden">
</div>
<div id="attachmentsView" class="hidden">
</div>
<div id="layersView" class="hidden">
</div>
</div>
<div id="sidebarResizer"></div>
</div> <!-- sidebarContainer -->
<div id="mainContainer">
<div class="findbar hidden doorHanger" id="findbar">
<div id="findbarInputContainer">
<span class="loadingInput end">
<input id="findInput" class="toolbarField" title="Find" placeholder="Find in document…" tabindex="91"
data-l10n-id="pdfjs-find-input" aria-invalid="false">
</span>
<div class="splitToolbarButton">
<button id="findPrevious" class="toolbarButton" title="Find the previous occurrence of the phrase"
tabindex="92" data-l10n-id="pdfjs-find-previous-button">
<span data-l10n-id="pdfjs-find-previous-button-label">Previous</span>
</button>
<div class="splitToolbarButtonSeparator"></div>
<button id="findNext" class="toolbarButton" title="Find the next occurrence of the phrase" tabindex="93"
data-l10n-id="pdfjs-find-next-button">
<span data-l10n-id="pdfjs-find-next-button-label">Next</span>
</button>
</div>
</div>
<div id="findbarOptionsOneContainer">
<input type="checkbox" id="findHighlightAll" class="toolbarField" tabindex="94">
<label for="findHighlightAll" class="toolbarLabel" data-l10n-id="pdfjs-find-highlight-checkbox">Highlight
All</label>
<input type="checkbox" id="findMatchCase" class="toolbarField" tabindex="95">
<label for="findMatchCase" class="toolbarLabel" data-l10n-id="pdfjs-find-match-case-checkbox-label">Match
Case</label>
</div>
<div id="findbarOptionsTwoContainer">
<input type="checkbox" id="findMatchDiacritics" class="toolbarField" tabindex="96">
<label for="findMatchDiacritics" class="toolbarLabel"
data-l10n-id="pdfjs-find-match-diacritics-checkbox-label">Match Diacritics</label>
<input type="checkbox" id="findEntireWord" class="toolbarField" tabindex="97">
<label for="findEntireWord" class="toolbarLabel" data-l10n-id="pdfjs-find-entire-word-checkbox-label">Whole
Words</label>
</div>
<div id="findbarMessageContainer" aria-live="polite">
<span id="findResultsCount" class="toolbarLabel"></span>
<span id="findMsg" class="toolbarLabel"></span>
</div>
</div> <!-- findbar -->
<div class="editorParamsToolbar hidden doorHangerRight" id="editorHighlightParamsToolbar">
<div id="highlightParamsToolbarContainer" class="editorParamsToolbarContainer">
<div id="editorHighlightColorPicker" class="colorPicker">
<span id="highlightColorPickerLabel" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-highlight-colorpicker-label">Highlight color</span>
</div>
<div id="editorHighlightThickness">
<label for="editorFreeHighlightThickness" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-free-highlight-thickness-input">Thickness</label>
<div class="thicknessPicker">
<input type="range" id="editorFreeHighlightThickness" class="editorParamsSlider"
data-l10n-id="pdfjs-editor-free-highlight-thickness-title" value="12" min="8" max="24" step="1"
tabindex="101">
</div>
</div>
<div id="editorHighlightVisibility">
<div class="divider"></div>
<div class="toggler">
<label for="editorHighlightShowAll" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-highlight-show-all-button-label">Show all</label>
<button id="editorHighlightShowAll" class="toggle-button"
data-l10n-id="pdfjs-editor-highlight-show-all-button" aria-pressed="true" tabindex="102"></button>
</div>
</div>
</div>
</div>
<div class="editorParamsToolbar hidden doorHangerRight" id="editorFreeTextParamsToolbar">
<div class="editorParamsToolbarContainer">
<div class="editorParamsSetter">
<label for="editorFreeTextColor" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-free-text-color-input">Color</label>
<input type="color" id="editorFreeTextColor" class="editorParamsColor" tabindex="103">
</div>
<div class="editorParamsSetter">
<label for="editorFreeTextFontSize" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-free-text-size-input">Size</label>
<input type="range" id="editorFreeTextFontSize" class="editorParamsSlider" value="10" min="5" max="100"
step="1" tabindex="104">
</div>
</div>
</div>
<div class="editorParamsToolbar hidden doorHangerRight" id="editorInkParamsToolbar">
<div class="editorParamsToolbarContainer">
<div class="editorParamsSetter">
<label for="editorInkColor" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-ink-color-input">Color</label>
<input type="color" id="editorInkColor" class="editorParamsColor" tabindex="105">
</div>
<div class="editorParamsSetter">
<label for="editorInkThickness" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-ink-thickness-input">Thickness</label>
<input type="range" id="editorInkThickness" class="editorParamsSlider" value="1" min="1" max="20" step="1"
tabindex="106">
</div>
<div class="editorParamsSetter">
<label for="editorInkOpacity" class="editorParamsLabel"
data-l10n-id="pdfjs-editor-ink-opacity-input">Opacity</label>
<input type="range" id="editorInkOpacity" class="editorParamsSlider" value="100" min="1" max="100" step="1"
tabindex="107">
</div>
</div>
</div>
<div class="editorParamsToolbar hidden doorHangerRight" id="editorStampParamsToolbar">
<div class="editorParamsToolbarContainer">
<button id="editorStampAddImage" class="secondaryToolbarButton" title="Add image" tabindex="108"
data-l10n-id="pdfjs-editor-stamp-add-image-button">
<span class="editorParamsLabel" data-l10n-id="pdfjs-editor-stamp-add-image-button-label">Add image</span>
</button>
</div>
</div>
<div id="secondaryToolbar" class="secondaryToolbar hidden doorHangerRight">
<div id="secondaryToolbarButtonContainer">
<button id="secondaryOpenFile" class="secondaryToolbarButton visibleMediumView" title="Open File"
tabindex="51" data-l10n-id="pdfjs-open-file-button">
<span data-l10n-id="pdfjs-open-file-button-label">Open</span>
</button>
<button id="secondaryPrint" class="secondaryToolbarButton visibleMediumView" title="Print" tabindex="52"
data-l10n-id="pdfjs-print-button">
<span data-l10n-id="pdfjs-print-button-label">Print</span>
</button>
<button id="secondaryDownload" class="secondaryToolbarButton visibleMediumView" title="Save" tabindex="53"
data-l10n-id="pdfjs-save-button">
<span data-l10n-id="pdfjs-save-button-label">Save</span>
</button>
<div class="horizontalToolbarSeparator"></div>
<button id="presentationMode" class="secondaryToolbarButton" title="Switch to Presentation Mode" tabindex="54"
data-l10n-id="pdfjs-presentation-mode-button">
<span data-l10n-id="pdfjs-presentation-mode-button-label">Presentation Mode</span>
</button>
<a href="#" id="viewBookmark" class="secondaryToolbarButton" title="Current Page (View URL from Current Page)"
tabindex="55" data-l10n-id="pdfjs-bookmark-button">
<span data-l10n-id="pdfjs-bookmark-button-label">Current Page</span>
</a>
<div id="viewBookmarkSeparator" class="horizontalToolbarSeparator"></div>
<button id="firstPage" class="secondaryToolbarButton" title="Go to First Page" tabindex="56"
data-l10n-id="pdfjs-first-page-button">
<span data-l10n-id="pdfjs-first-page-button-label">Go to First Page</span>
</button>
<button id="lastPage" class="secondaryToolbarButton" title="Go to Last Page" tabindex="57"
data-l10n-id="pdfjs-last-page-button">
<span data-l10n-id="pdfjs-last-page-button-label">Go to Last Page</span>
</button>
<div class="horizontalToolbarSeparator"></div>
<button id="pageRotateCw" class="secondaryToolbarButton" title="Rotate Clockwise" tabindex="58"
data-l10n-id="pdfjs-page-rotate-cw-button">
<span data-l10n-id="pdfjs-page-rotate-cw-button-label">Rotate Clockwise</span>
</button>
<button id="pageRotateCcw" class="secondaryToolbarButton" title="Rotate Counterclockwise" tabindex="59"
data-l10n-id="pdfjs-page-rotate-ccw-button">
<span data-l10n-id="pdfjs-page-rotate-ccw-button-label">Rotate Counterclockwise</span>
</button>
<div class="horizontalToolbarSeparator"></div>
<div id="cursorToolButtons" role="radiogroup">
<button id="cursorSelectTool" class="secondaryToolbarButton toggled" title="Enable Text Selection Tool"
tabindex="60" data-l10n-id="pdfjs-cursor-text-select-tool-button" role="radio" aria-checked="true">
<span data-l10n-id="pdfjs-cursor-text-select-tool-button-label">Text Selection Tool</span>
</button>
<button id="cursorHandTool" class="secondaryToolbarButton" title="Enable Hand Tool" tabindex="61"
data-l10n-id="pdfjs-cursor-hand-tool-button" role="radio" aria-checked="false">
<span data-l10n-id="pdfjs-cursor-hand-tool-button-label">Hand Tool</span>
</button>
</div>
<div class="horizontalToolbarSeparator"></div>
<div id="scrollModeButtons" role="radiogroup">
<button id="scrollPage" class="secondaryToolbarButton" title="Use Page Scrolling" tabindex="62"
data-l10n-id="pdfjs-scroll-page-button" role="radio" aria-checked="false">
<span data-l10n-id="pdfjs-scroll-page-button-label">Page Scrolling</span>
</button>
<button id="scrollVertical" class="secondaryToolbarButton toggled" title="Use Vertical Scrolling"
tabindex="63" data-l10n-id="pdfjs-scroll-vertical-button" role="radio" aria-checked="true">
<span data-l10n-id="pdfjs-scroll-vertical-button-label">Vertical Scrolling</span>
</button>
<button id="scrollHorizontal" class="secondaryToolbarButton" title="Use Horizontal Scrolling" tabindex="64"
data-l10n-id="pdfjs-scroll-horizontal-button" role="radio" aria-checked="false">
<span data-l10n-id="pdfjs-scroll-horizontal-button-label">Horizontal Scrolling</span>
</button>
<button id="scrollWrapped" class="secondaryToolbarButton" title="Use Wrapped Scrolling" tabindex="65"
data-l10n-id="pdfjs-scroll-wrapped-button" role="radio" aria-checked="false">
<span data-l10n-id="pdfjs-scroll-wrapped-button-label">Wrapped Scrolling</span>
</button>
</div>
<div class="horizontalToolbarSeparator"></div>
<div id="spreadModeButtons" role="radiogroup">
<button id="spreadNone" class="secondaryToolbarButton toggled" title="Do not join page spreads"
tabindex="66" data-l10n-id="pdfjs-spread-none-button" role="radio" aria-checked="true">
<span data-l10n-id="pdfjs-spread-none-button-label">No Spreads</span>
</button>
<button id="spreadOdd" class="secondaryToolbarButton"
title="Join page spreads starting with odd-numbered pages" tabindex="67"
data-l10n-id="pdfjs-spread-odd-button" role="radio" aria-checked="false">
<span data-l10n-id="pdfjs-spread-odd-button-label">Odd Spreads</span>
</button>
<button id="spreadEven" class="secondaryToolbarButton"
title="Join page spreads starting with even-numbered pages" tabindex="68"
data-l10n-id="pdfjs-spread-even-button" role="radio" aria-checked="false">
<span data-l10n-id="pdfjs-spread-even-button-label">Even Spreads</span>
</button>
</div>
<div class="horizontalToolbarSeparator"></div>
<button id="documentProperties" class="secondaryToolbarButton" title="Document Properties…" tabindex="69"
data-l10n-id="pdfjs-document-properties-button" aria-controls="documentPropertiesDialog">
<span data-l10n-id="pdfjs-document-properties-button-label">Document Properties…</span>
</button>
</div>
</div> <!-- secondaryToolbar -->
<div class="toolbar">
<div id="toolbarContainer">
<div id="toolbarViewer">
<div id="toolbarViewerLeft">
<button id="sidebarToggle" class="toolbarButton" title="Toggle Sidebar" tabindex="11"
data-l10n-id="pdfjs-toggle-sidebar-button" aria-expanded="false" aria-controls="sidebarContainer">
<span data-l10n-id="pdfjs-toggle-sidebar-button-label">Toggle Sidebar</span>
</button>
<div class="toolbarButtonSpacer"></div>
<button id="viewFind" class="toolbarButton" title="Find in Document" tabindex="12"
data-l10n-id="pdfjs-findbar-button" aria-expanded="false" aria-controls="findbar">
<span data-l10n-id="pdfjs-findbar-button-label">Find</span>
</button>
<div class="splitToolbarButton hiddenSmallView">
<button class="toolbarButton" title="Previous Page" id="previous" tabindex="13"
data-l10n-id="pdfjs-previous-button">
<span data-l10n-id="pdfjs-previous-button-label">Previous</span>
</button>
<div class="splitToolbarButtonSeparator"></div>
<button class="toolbarButton" title="Next Page" id="next" tabindex="14"
data-l10n-id="pdfjs-next-button">
<span data-l10n-id="pdfjs-next-button-label">Next</span>
</button>
</div>
<span class="loadingInput start">
<input type="number" id="pageNumber" class="toolbarField" title="Page" value="1" min="1" tabindex="15"
data-l10n-id="pdfjs-page-input" autocomplete="off">
</span>
<span id="numPages" class="toolbarLabel"></span>
</div>
<div id="toolbarViewerRight">
<div id="editorModeButtons" class="splitToolbarButton toggled" role="radiogroup">
<a id="backToHome" class="toolbarButton hiddenMediumView" title="Back to Main Page" role="radio"
aria-checked="false" tabindex="31" th:href="@{'/'}">
<span data-l10n-id="pdfjs-open-file-button-label">Back to Main Page</span>
</a>
<button id="editorHighlight" class="toolbarButton hiddenMediumView" title="Highlight" role="radio"
aria-checked="false" tabindex="32" aria-expanded="false" aria-controls="editorHighlightParamsToolbar">
<span data-l10n-id="pdfjs-editor-highlight-button-label">Highlight</span>
</button>
<button id="openFile" class="toolbarButton hiddenMediumView" title="Open File" role="radio"
aria-checked="false" tabindex="33" data-l10n-id="pdfjs-open-file-button">
<span data-l10n-id="pdfjs-open-file-button-label">Open</span>
</button>
<button id="print" class="toolbarButton hiddenMediumView" title="Print" role="radio"
aria-checked="false" tabindex="34" data-l10n-id="pdfjs-print-button">
<span data-l10n-id="pdfjs-print-button-label">Print</span>
</button>
<button id="download" class="toolbarButton hiddenMediumView" title="Save" role="radio"
aria-checked="false" tabindex="35" data-l10n-id="pdfjs-save-button">
<span data-l10n-id="pdfjs-save-button-label">Save</span>
</button>
<button id="editorStamp" class="toolbarButton hiddenMediumView" title="Add or edit images" role="radio"
aria-checked="false" aria-controls="editorStampParamsToolbar" tabindex="36"
data-l10n-id="pdfjs-editor-stamp-button">
<span data-l10n-id="pdfjs-editor-stamp-button-label">Add or edit images</span>
</button>
</div>
<div id="editorModeSeparator" class="verticalToolbarSeparator"></div>
<button id="editorFreeText" class="toolbarButton hiddenMediumView" title="Text" tabindex="41"
data-l10n-id="pdfjs-editor-free-text-button">
<span data-l10n-id="pdfjs-editor-free-text-button-label">Text</span>
</button>
<button id="editorInk" class="toolbarButton hiddenMediumView" title="Draw" tabindex="42"
data-l10n-id="pdfjs-editor-ink-button">
<span data-l10n-id="pdfjs-editor-ink-button-label">Draw</span>
</button>
<div class="verticalToolbarSeparator hiddenMediumView"></div>
<button id="secondaryToolbarToggle" class="toolbarButton" title="Tools" tabindex="43"
data-l10n-id="pdfjs-tools-button">
<span data-l10n-id="pdfjs-tools-button-label">Tools</span>
</button>
</div>
<div id="toolbarViewerMiddle">
<div class="splitToolbarButton">
<button id="zoomOut" class="toolbarButton" title="Zoom Out" tabindex="21"
data-l10n-id="pdfjs-zoom-out-button">
<span data-l10n-id="pdfjs-zoom-out-button-label">Zoom Out</span>
</button>
<div class="splitToolbarButtonSeparator"></div>
<button id="zoomIn" class="toolbarButton" title="Zoom In" tabindex="22"
data-l10n-id="pdfjs-zoom-in-button">
<span data-l10n-id="pdfjs-zoom-in-button-label">Zoom In</span>
</button>
</div>
<span id="scaleSelectContainer" class="dropdownToolbarButton">
<select id="scaleSelect" title="Zoom" tabindex="23" data-l10n-id="pdfjs-zoom-select">
<option id="pageAutoOption" title="" value="auto" selected="selected"
data-l10n-id="pdfjs-page-scale-auto">Automatic Zoom</option>
<option id="pageActualOption" title="" value="page-actual" data-l10n-id="pdfjs-page-scale-actual">
Actual Size</option>
<option id="pageFitOption" title="" value="page-fit" data-l10n-id="pdfjs-page-scale-fit">Page Fit
</option>
<option id="pageWidthOption" title="" value="page-width" data-l10n-id="pdfjs-page-scale-width">Page
Width</option>
<option id="customScaleOption" title="" value="custom" disabled="disabled" hidden="true"
data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 0 }'>0%</option>
<option title="" value="0.5" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 50 }'>
50%</option>
<option title="" value="0.75" data-l10n-id="pdfjs-page-scale-percent"
data-l10n-args='{ "scale": 75 }'>75%</option>
<option title="" value="1" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 100 }'>
100%</option>
<option title="" value="1.25" data-l10n-id="pdfjs-page-scale-percent"
data-l10n-args='{ "scale": 125 }'>125%</option>
<option title="" value="1.5" data-l10n-id="pdfjs-page-scale-percent"
data-l10n-args='{ "scale": 150 }'>150%</option>
<option title="" value="2" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 200 }'>
200%</option>
<option title="" value="3" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 300 }'>
300%</option>
<option title="" value="4" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 400 }'>
400%</option>
</select>
</span>
</div>
</div>
<div id="loadingBar">
<div class="progress">
<div class="glimmer">
</div>
</div>
</div>
</div>
</div>
<div id="viewerContainer" tabindex="0">
<div id="viewer" class="pdfViewer">
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</div> <!-- mainContainer -->
<div id="dialogContainer">
<dialog id="passwordDialog">
<div class="row">
<label for="password" id="passwordText" data-l10n-id="pdfjs-password-label">Enter the password to open this
PDF file:</label>
</div>
<div class="row">
<input type="password" id="password" class="toolbarField">
</div>
<div class="buttonRow">
<button id="passwordCancel" class="dialogButton"><span
data-l10n-id="pdfjs-password-cancel-button">Cancel</span></button>
<button id="passwordSubmit" class="dialogButton"><span
data-l10n-id="pdfjs-password-ok-button">OK</span></button>
</div>
</dialog>
<dialog id="documentPropertiesDialog">
<div class="row">
<span id="fileNameLabel" data-l10n-id="pdfjs-document-properties-file-name">File name:</span>
<p id="fileNameField" aria-labelledby="fileNameLabel">-</p>
</div>
<div class="row">
<span id="fileSizeLabel" data-l10n-id="pdfjs-document-properties-file-size">File size:</span>
<p id="fileSizeField" aria-labelledby="fileSizeLabel">-</p>
</div>
<div class="separator"></div>
<div class="row">
<span id="titleLabel" data-l10n-id="pdfjs-document-properties-title">Title:</span>
<p id="titleField" aria-labelledby="titleLabel">-</p>
</div>
<div class="row">
<span id="authorLabel" data-l10n-id="pdfjs-document-properties-author">Author:</span>
<p id="authorField" aria-labelledby="authorLabel">-</p>
</div>
<div class="row">
<span id="subjectLabel" data-l10n-id="pdfjs-document-properties-subject">Subject:</span>
<p id="subjectField" aria-labelledby="subjectLabel">-</p>
</div>
<div class="row">
<span id="keywordsLabel" data-l10n-id="pdfjs-document-properties-keywords">Keywords:</span>
<p id="keywordsField" aria-labelledby="keywordsLabel">-</p>
</div>
<div class="row">
<span id="creationDateLabel" data-l10n-id="pdfjs-document-properties-creation-date">Creation Date:</span>
<p id="creationDateField" aria-labelledby="creationDateLabel">-</p>
</div>
<div class="row">
<span id="modificationDateLabel" data-l10n-id="pdfjs-document-properties-modification-date">Modification
Date:</span>
<p id="modificationDateField" aria-labelledby="modificationDateLabel">-</p>
</div>
<div class="row">
<span id="creatorLabel" data-l10n-id="pdfjs-document-properties-creator">Creator:</span>
<p id="creatorField" aria-labelledby="creatorLabel">-</p>
</div>
<div class="separator"></div>
<div class="row">
<span id="producerLabel" data-l10n-id="pdfjs-document-properties-producer">PDF Producer:</span>
<p id="producerField" aria-labelledby="producerLabel">-</p>
</div>
<div class="row">
<span id="versionLabel" data-l10n-id="pdfjs-document-properties-version">PDF Version:</span>
<p id="versionField" aria-labelledby="versionLabel">-</p>
</div>
<div class="row">
<span id="pageCountLabel" data-l10n-id="pdfjs-document-properties-page-count">Page Count:</span>
<p id="pageCountField" aria-labelledby="pageCountLabel">-</p>
</div>
<div class="row">
<span id="pageSizeLabel" data-l10n-id="pdfjs-document-properties-page-size">Page Size:</span>
<p id="pageSizeField" aria-labelledby="pageSizeLabel">-</p>
</div>
<div class="separator"></div>
<div class="row">
<span id="linearizedLabel" data-l10n-id="pdfjs-document-properties-linearized">Fast Web View:</span>
<p id="linearizedField" aria-labelledby="linearizedLabel">-</p>
</div>
<div class="buttonRow">
<button id="documentPropertiesClose" class="dialogButton"><span
data-l10n-id="pdfjs-document-properties-close-button">Close</span></button>
</div>
</dialog>
<dialog class="dialog altText" id="altTextDialog" aria-labelledby="dialogLabel"
aria-describedby="dialogDescription">
<div id="altTextContainer" class="mainContainer">
<div id="overallDescription">
<span id="dialogLabel" data-l10n-id="pdfjs-editor-alt-text-dialog-label" class="title">Choose an
option</span>
<span id="dialogDescription" data-l10n-id="pdfjs-editor-alt-text-dialog-description">
Alt text (alternative text) helps when people cant see the image or when it doesnt load.
</span>
</div>
<div id="addDescription">
<div class="radio">
<div class="radioButton">
<input type="radio" id="descriptionButton" name="altTextOption" tabindex="0"
aria-describedby="descriptionAreaLabel" checked>
<label for="descriptionButton" data-l10n-id="pdfjs-editor-alt-text-add-description-label">Add a
description</label>
</div>
<div class="radioLabel">
<span id="descriptionAreaLabel" data-l10n-id="pdfjs-editor-alt-text-add-description-description">
Aim for 1-2 sentences that describe the subject, setting, or actions.
</span>
</div>
</div>
<div class="descriptionArea">
<textarea id="descriptionTextarea"
placeholder="For example, “A young man sits down at a table to eat a meal”"
aria-labelledby="descriptionAreaLabel" data-l10n-id="pdfjs-editor-alt-text-textarea"
tabindex="0"></textarea>
</div>
</div>
<div id="markAsDecorative">
<div class="radio">
<div class="radioButton">
<input type="radio" id="decorativeButton" name="altTextOption" aria-describedby="decorativeLabel">
<label for="decorativeButton" data-l10n-id="pdfjs-editor-alt-text-mark-decorative-label">Mark as
decorative</label>
</div>
<div class="radioLabel">
<span id="decorativeLabel" data-l10n-id="pdfjs-editor-alt-text-mark-decorative-description">
This is used for ornamental images, like borders or watermarks.
</span>
</div>
</div>
</div>
<div id="buttons">
<button id="altTextCancel" class="secondaryButton" tabindex="0"><span
data-l10n-id="pdfjs-editor-alt-text-cancel-button">Cancel</span></button>
<button id="altTextSave" class="primaryButton" tabindex="0"><span
data-l10n-id="pdfjs-editor-alt-text-save-button">Save</span></button>
</div>
</div>
</dialog>
<dialog id="printServiceDialog" style="min-width: 200px;">
<div class="row">
<span data-l10n-id="pdfjs-print-progress-message">Preparing document for printing…</span>
</div>
<div class="row">
<progress value="0" max="100"></progress>
<span data-l10n-id="pdfjs-print-progress-percent" data-l10n-args='{ "progress": 0 }'
class="relative-progress">0%</span>
</div>
<div class="buttonRow">
<button id="printCancel" class="dialogButton"><span
data-l10n-id="pdfjs-print-progress-close-button">Cancel</span></button>
</div>
</dialog>
</div> <!-- dialogContainer -->
</div> <!-- outerContainer -->
<div id="printContainer"></div>
</body>
<style>
*,
::after,
::before {
box-sizing: content-box !important;
}
</style>
</html>