Merge branch 'main' into pr-4113

This commit is contained in:
Anthony Stirling 2025-08-08 13:29:19 +01:00 committed by GitHub
commit d18ed73755
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 2868 additions and 1937 deletions

View File

@ -49,7 +49,7 @@
"java.configuration.updateBuildConfiguration": "interactive",
"java.format.enabled": true,
"java.format.settings.profile": "GoogleStyle",
"java.format.settings.google.version": "1.26.0",
"java.format.settings.google.version": "1.28.0",
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
"java.saveActions.cleanup": true,
"java.cleanup.actions": [
@ -79,9 +79,17 @@
".venv*/",
".vscode/",
"bin/",
"app/core/bin/",
"app/common/bin/",
"app/proprietary/bin/",
"build/",
"app/core/build/",
"app/common/build/",
"app/proprietary/build/",
"configs/",
"app/core/configs/",
"customFiles/",
"app/core/customFiles/",
"docs/",
"exampleYmlFiles",
"gradle/",
@ -93,6 +101,9 @@
".git-blame-ignore-revs",
".gitattributes",
".gitignore",
"app/core/.gitignore",
"app/common/.gitignore",
"app/proprietary/.gitignore",
".pre-commit-config.yaml"
],
"java.signatureHelp.enabled": true,

View File

@ -31,18 +31,12 @@ indent_size = 2
# CSS files typically use an indent size of 2 spaces for better readability and alignment with community standards.
indent_size = 2
[*.yaml]
[*.{yml,yaml}]
# YAML files use an indent size of 2 spaces to maintain consistency with common YAML formatting practices.
indent_size = 2
insert_final_newline = false
trim_trailing_whitespace = false
[*.yml]
# YML files follow the same conventions as YAML files, using an indent size of 2 spaces.
indent_size = 2
insert_final_newline = false
trim_trailing_whitespace = false
[*.json]
# JSON files use an indent size of 2 spaces, which is the standard for JSON formatting.
indent_size = 2

23
.github/CODEOWNERS vendored
View File

@ -1,2 +1,21 @@
# All PRs to V1 must be approved by Frooodle
* @Frooodle @reecebrowne @Ludy87 @DarioGii @ConnorYoh @EthanHealy01
# All PRs must be approved by Frooodle or Ludy87
* @Frooodle @Ludy87 @jbrunton96 @ConnorYoh
# Backend
/app/** @DarioGii
#V1 frontend
/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96
/app/core/src/main/resources/templates/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96
#V2 frontend
/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96
#V2 docker
/docker/backend/** @Frooodle @Ludy87 @DarioGii
/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96
/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96
#GHA (All users)
/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96

View File

@ -26,4 +26,4 @@ project: &project
- gradlew
- gradlew.bat
- launch4jConfig.xml
- settings.gradle
- settings.gradle

5
.github/labels.yml vendored
View File

@ -42,6 +42,7 @@
- name: "Front End"
color: "BBD2F1"
description: "Issues or pull requests related to front-end development"
from_name: "frontend"
- name: "github-actions"
description: "Pull requests that update GitHub Actions code"
color: "999999"
@ -77,6 +78,7 @@
- name: "Translation"
color: "9FABF9"
from_name: "translation"
description: "Issues or pull requests related to translation"
- name: "upstream"
color: "DEDEDE"
- name: "v2"
@ -178,3 +180,6 @@
- name: "pr-deployed"
color: "00FF00"
description: "Pull request has been deployed to a test environment"
- name: "codex"
color: "ededed"
description: "chatgpt AI generated code"

View File

@ -196,7 +196,7 @@ jobs:
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}

View File

@ -87,7 +87,7 @@ jobs:
- name: AI PR Title Analysis
if: steps.actor.outputs.is_repo_dev == 'true'
id: ai-title-analysis
uses: actions/ai-inference@9693b137b6566bb66055a713613bf4f0493701eb # v1.2.3
uses: actions/ai-inference@0cbed4a10641c75090de5968e66d70eb4660f751 # v1.2.7
with:
model: openai/gpt-4o
system-prompt-file: ".github/config/system-prompt.txt"

View File

@ -67,13 +67,13 @@ jobs:
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: ghcr.io
username: ${{ github.actor }}

View File

@ -57,7 +57,7 @@ jobs:
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}

View File

@ -2,6 +2,7 @@
"editor.wordSegmenterLocales": "",
"editor.guides.bracketPairs": "active",
"editor.guides.bracketPairsHorizontal": "active",
"editor.defaultFormatter": "EditorConfig.EditorConfig",
"cSpell.enabled": false,
"[feature]": {
"editor.defaultFormatter": "alexkrechik.cucumberautocomplete"
@ -40,7 +41,7 @@
"java.configuration.updateBuildConfiguration": "interactive",
"java.format.enabled": true,
"java.format.settings.profile": "GoogleStyle",
"java.format.settings.google.version": "1.27.0",
"java.format.settings.google.version": "1.28.0",
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
// (DE) Aktiviert Kommentare im Java-Format.
// (EN) Enables comments in Java formatting.

View File

@ -128,7 +128,7 @@ Stirling-PDF currently supports 40 languages!
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![91%](https://geps.dev/progress/91) |
| German (Deutsch) (de_DE) | ![100%](https://geps.dev/progress/100) |
| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) |
| Greek (Ελληνικά) (el_GR) | ![69%](https://geps.dev/progress/69) |
| Hindi (हिंदी) (hi_IN) | ![68%](https://geps.dev/progress/68) |
| Hungarian (Magyar) (hu_HU) | ![99%](https://geps.dev/progress/99) |

View File

@ -311,6 +311,7 @@ public class ApplicationProperties {
private Boolean enableAnalytics;
private Datasource datasource;
private Boolean disableSanitize;
private int maxDPI;
private Boolean enableUrlToPDF;
private Html html = new Html();
private CustomPaths customPaths = new CustomPaths();

View File

@ -0,0 +1,652 @@
package stirling.software.common.util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.regex.Pattern;
import lombok.Data;
import lombok.experimental.UtilityClass;
import stirling.software.common.model.api.converters.EmlToPdfRequest;
@UtilityClass
public class EmlParser {
private static volatile Boolean jakartaMailAvailable = null;
private static volatile Method mimeUtilityDecodeTextMethod = null;
private static volatile boolean mimeUtilityChecked = false;
private static final Pattern MIME_ENCODED_PATTERN =
Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?=");
private static final String DISPOSITION_ATTACHMENT = "attachment";
private static final String TEXT_PLAIN = "text/plain";
private static final String TEXT_HTML = "text/html";
private static final String MULTIPART_PREFIX = "multipart/";
private static final String HEADER_CONTENT_TYPE = "content-type:";
private static final String HEADER_CONTENT_DISPOSITION = "content-disposition:";
private static final String HEADER_CONTENT_TRANSFER_ENCODING = "content-transfer-encoding:";
private static final String HEADER_CONTENT_ID = "Content-ID";
private static final String HEADER_SUBJECT = "Subject:";
private static final String HEADER_FROM = "From:";
private static final String HEADER_TO = "To:";
private static final String HEADER_CC = "Cc:";
private static final String HEADER_BCC = "Bcc:";
private static final String HEADER_DATE = "Date:";
private static synchronized boolean isJakartaMailAvailable() {
if (jakartaMailAvailable == null) {
try {
Class.forName("jakarta.mail.internet.MimeMessage");
Class.forName("jakarta.mail.Session");
Class.forName("jakarta.mail.internet.MimeUtility");
Class.forName("jakarta.mail.internet.MimePart");
Class.forName("jakarta.mail.internet.MimeMultipart");
Class.forName("jakarta.mail.Multipart");
Class.forName("jakarta.mail.Part");
jakartaMailAvailable = true;
} catch (ClassNotFoundException e) {
jakartaMailAvailable = false;
}
}
return jakartaMailAvailable;
}
public static EmailContent extractEmailContent(
byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer)
throws IOException {
EmlProcessingUtils.validateEmlInput(emlBytes);
if (isJakartaMailAvailable()) {
return extractEmailContentAdvanced(emlBytes, request, customHtmlSanitizer);
} else {
return extractEmailContentBasic(emlBytes, request, customHtmlSanitizer);
}
}
private static EmailContent extractEmailContentBasic(
byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) {
String emlContent = new String(emlBytes, StandardCharsets.UTF_8);
EmailContent content = new EmailContent();
content.setSubject(extractBasicHeader(emlContent, HEADER_SUBJECT));
content.setFrom(extractBasicHeader(emlContent, HEADER_FROM));
content.setTo(extractBasicHeader(emlContent, HEADER_TO));
content.setCc(extractBasicHeader(emlContent, HEADER_CC));
content.setBcc(extractBasicHeader(emlContent, HEADER_BCC));
String dateStr = extractBasicHeader(emlContent, HEADER_DATE);
if (!dateStr.isEmpty()) {
content.setDateString(dateStr);
}
String htmlBody = extractHtmlBody(emlContent);
if (htmlBody != null) {
content.setHtmlBody(htmlBody);
} else {
String textBody = extractTextBody(emlContent);
content.setTextBody(textBody != null ? textBody : "Email content could not be parsed");
}
content.getAttachments().addAll(extractAttachmentsBasic(emlContent));
return content;
}
private static EmailContent extractEmailContentAdvanced(
byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) {
try {
Class<?> sessionClass = Class.forName("jakarta.mail.Session");
Class<?> mimeMessageClass = Class.forName("jakarta.mail.internet.MimeMessage");
Method getDefaultInstance =
sessionClass.getMethod("getDefaultInstance", Properties.class);
Object session = getDefaultInstance.invoke(null, new Properties());
Class<?>[] constructorArgs = new Class<?>[] {sessionClass, InputStream.class};
Constructor<?> mimeMessageConstructor =
mimeMessageClass.getConstructor(constructorArgs);
Object message =
mimeMessageConstructor.newInstance(session, new ByteArrayInputStream(emlBytes));
return extractFromMimeMessage(message, request, customHtmlSanitizer);
} catch (ReflectiveOperationException e) {
return extractEmailContentBasic(emlBytes, request, customHtmlSanitizer);
}
}
private static EmailContent extractFromMimeMessage(
Object message, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) {
EmailContent content = new EmailContent();
try {
Class<?> messageClass = message.getClass();
Method getSubject = messageClass.getMethod("getSubject");
String subject = (String) getSubject.invoke(message);
content.setSubject(subject != null ? safeMimeDecode(subject) : "No Subject");
Method getFrom = messageClass.getMethod("getFrom");
Object[] fromAddresses = (Object[]) getFrom.invoke(message);
content.setFrom(buildAddressString(fromAddresses));
extractRecipients(message, messageClass, content);
Method getSentDate = messageClass.getMethod("getSentDate");
content.setDate((Date) getSentDate.invoke(message));
Method getContent = messageClass.getMethod("getContent");
Object messageContent = getContent.invoke(message);
processMessageContent(message, messageContent, content, request, customHtmlSanitizer);
} catch (ReflectiveOperationException | RuntimeException e) {
content.setSubject("Email Conversion");
content.setFrom("Unknown");
content.setTo("Unknown");
content.setCc("");
content.setBcc("");
content.setTextBody("Email content could not be parsed with advanced processing");
}
return content;
}
private static void extractRecipients(
Object message, Class<?> messageClass, EmailContent content) {
try {
Method getRecipients =
messageClass.getMethod(
"getRecipients", Class.forName("jakarta.mail.Message$RecipientType"));
Class<?> recipientTypeClass = Class.forName("jakarta.mail.Message$RecipientType");
Object toType = recipientTypeClass.getField("TO").get(null);
Object[] toRecipients = (Object[]) getRecipients.invoke(message, toType);
content.setTo(buildAddressString(toRecipients));
Object ccType = recipientTypeClass.getField("CC").get(null);
Object[] ccRecipients = (Object[]) getRecipients.invoke(message, ccType);
content.setCc(buildAddressString(ccRecipients));
Object bccType = recipientTypeClass.getField("BCC").get(null);
Object[] bccRecipients = (Object[]) getRecipients.invoke(message, bccType);
content.setBcc(buildAddressString(bccRecipients));
} catch (ReflectiveOperationException e) {
try {
Method getAllRecipients = messageClass.getMethod("getAllRecipients");
Object[] recipients = (Object[]) getAllRecipients.invoke(message);
content.setTo(buildAddressString(recipients));
content.setCc("");
content.setBcc("");
} catch (ReflectiveOperationException ex) {
content.setTo("");
content.setCc("");
content.setBcc("");
}
}
}
private static String buildAddressString(Object[] addresses) {
if (addresses == null || addresses.length == 0) {
return "";
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < addresses.length; i++) {
if (i > 0) builder.append(", ");
builder.append(safeMimeDecode(addresses[i].toString()));
}
return builder.toString();
}
private static void processMessageContent(
Object message,
Object messageContent,
EmailContent content,
EmlToPdfRequest request,
CustomHtmlSanitizer customHtmlSanitizer) {
try {
if (messageContent instanceof String stringContent) {
Method getContentType = message.getClass().getMethod("getContentType");
String contentType = (String) getContentType.invoke(message);
if (contentType != null && contentType.toLowerCase().contains(TEXT_HTML)) {
content.setHtmlBody(stringContent);
} else {
content.setTextBody(stringContent);
}
} else {
Class<?> multipartClass = Class.forName("jakarta.mail.Multipart");
if (multipartClass.isInstance(messageContent)) {
processMultipart(messageContent, content, request, customHtmlSanitizer, 0);
}
}
} catch (ReflectiveOperationException | ClassCastException e) {
content.setTextBody("Email content could not be parsed with advanced processing");
}
}
private static void processMultipart(
Object multipart,
EmailContent content,
EmlToPdfRequest request,
CustomHtmlSanitizer customHtmlSanitizer,
int depth) {
final int MAX_MULTIPART_DEPTH = 10;
if (depth > MAX_MULTIPART_DEPTH) {
content.setHtmlBody("<div class=\"error\">Maximum multipart depth exceeded</div>");
return;
}
try {
Class<?> multipartClass = multipart.getClass();
Method getCount = multipartClass.getMethod("getCount");
int count = (Integer) getCount.invoke(multipart);
Method getBodyPart = multipartClass.getMethod("getBodyPart", int.class);
for (int i = 0; i < count; i++) {
Object part = getBodyPart.invoke(multipart, i);
processPart(part, content, request, customHtmlSanitizer, depth + 1);
}
} catch (ReflectiveOperationException | ClassCastException e) {
content.setHtmlBody("<div class=\"error\">Error processing multipart content</div>");
}
}
private static void processPart(
Object part,
EmailContent content,
EmlToPdfRequest request,
CustomHtmlSanitizer customHtmlSanitizer,
int depth) {
try {
Class<?> partClass = part.getClass();
Method isMimeType = partClass.getMethod("isMimeType", String.class);
Method getContent = partClass.getMethod("getContent");
Method getDisposition = partClass.getMethod("getDisposition");
Method getFileName = partClass.getMethod("getFileName");
Method getContentType = partClass.getMethod("getContentType");
Method getHeader = partClass.getMethod("getHeader", String.class);
Object disposition = getDisposition.invoke(part);
String filename = (String) getFileName.invoke(part);
String contentType = (String) getContentType.invoke(part);
String normalizedDisposition =
disposition != null ? ((String) disposition).toLowerCase() : null;
if ((Boolean) isMimeType.invoke(part, TEXT_PLAIN) && normalizedDisposition == null) {
Object partContent = getContent.invoke(part);
if (partContent instanceof String stringContent) {
content.setTextBody(stringContent);
}
} else if ((Boolean) isMimeType.invoke(part, TEXT_HTML)
&& normalizedDisposition == null) {
Object partContent = getContent.invoke(part);
if (partContent instanceof String stringContent) {
String htmlBody =
customHtmlSanitizer != null
? customHtmlSanitizer.sanitize(stringContent)
: stringContent;
content.setHtmlBody(htmlBody);
}
} else if ((normalizedDisposition != null
&& normalizedDisposition.contains(DISPOSITION_ATTACHMENT))
|| (filename != null && !filename.trim().isEmpty())) {
processAttachment(
part, content, request, getHeader, getContent, filename, contentType);
} else if ((Boolean) isMimeType.invoke(part, "multipart/*")) {
Object multipartContent = getContent.invoke(part);
if (multipartContent != null) {
Class<?> multipartClass = Class.forName("jakarta.mail.Multipart");
if (multipartClass.isInstance(multipartContent)) {
processMultipart(
multipartContent, content, request, customHtmlSanitizer, depth + 1);
}
}
}
} catch (ReflectiveOperationException | RuntimeException e) {
// Continue processing other parts if one fails
}
}
private static void processAttachment(
Object part,
EmailContent content,
EmlToPdfRequest request,
Method getHeader,
Method getContent,
String filename,
String contentType) {
content.setAttachmentCount(content.getAttachmentCount() + 1);
if (filename != null && !filename.trim().isEmpty()) {
EmailAttachment attachment = new EmailAttachment();
attachment.setFilename(safeMimeDecode(filename));
attachment.setContentType(contentType);
try {
String[] contentIdHeaders = (String[]) getHeader.invoke(part, HEADER_CONTENT_ID);
if (contentIdHeaders != null) {
for (String contentIdHeader : contentIdHeaders) {
if (contentIdHeader != null && !contentIdHeader.trim().isEmpty()) {
attachment.setEmbedded(true);
String contentId = contentIdHeader.trim().replaceAll("[<>]", "");
attachment.setContentId(contentId);
break;
}
}
}
} catch (ReflectiveOperationException e) {
}
if ((request != null && request.isIncludeAttachments()) || attachment.isEmbedded()) {
extractAttachmentData(part, attachment, getContent, request);
}
content.getAttachments().add(attachment);
}
}
private static void extractAttachmentData(
Object part, EmailAttachment attachment, Method getContent, EmlToPdfRequest request) {
try {
Object attachmentContent = getContent.invoke(part);
byte[] attachmentData = null;
if (attachmentContent instanceof InputStream inputStream) {
try (InputStream stream = inputStream) {
attachmentData = stream.readAllBytes();
} catch (IOException e) {
if (attachment.isEmbedded()) {
attachmentData = new byte[0];
} else {
throw new RuntimeException(e);
}
}
} else if (attachmentContent instanceof byte[] byteArray) {
attachmentData = byteArray;
} else if (attachmentContent instanceof String stringContent) {
attachmentData = stringContent.getBytes(StandardCharsets.UTF_8);
}
if (attachmentData != null) {
long maxSizeMB = request != null ? request.getMaxAttachmentSizeMB() : 10L;
long maxSizeBytes = maxSizeMB * 1024 * 1024;
if (attachmentData.length <= maxSizeBytes || attachment.isEmbedded()) {
attachment.setData(attachmentData);
attachment.setSizeBytes(attachmentData.length);
} else {
attachment.setSizeBytes(attachmentData.length);
}
}
} catch (ReflectiveOperationException | RuntimeException e) {
// Continue without attachment data
}
}
private static String extractBasicHeader(String emlContent, String headerName) {
try {
String[] lines = emlContent.split("\r?\n");
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
if (line.toLowerCase().startsWith(headerName.toLowerCase())) {
StringBuilder value =
new StringBuilder(line.substring(headerName.length()).trim());
for (int j = i + 1; j < lines.length; j++) {
if (lines[j].startsWith(" ") || lines[j].startsWith("\t")) {
value.append(" ").append(lines[j].trim());
} else {
break;
}
}
return safeMimeDecode(value.toString());
}
if (line.trim().isEmpty()) break;
}
} catch (RuntimeException e) {
// Ignore errors in header extraction
}
return "";
}
private static String extractHtmlBody(String emlContent) {
try {
String lowerContent = emlContent.toLowerCase();
int htmlStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_HTML);
if (htmlStart == -1) return null;
int bodyStart = emlContent.indexOf("\r\n\r\n", htmlStart);
if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n", htmlStart);
if (bodyStart == -1) return null;
bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2;
int bodyEnd = findPartEnd(emlContent, bodyStart);
return emlContent.substring(bodyStart, bodyEnd).trim();
} catch (Exception e) {
return null;
}
}
private static String extractTextBody(String emlContent) {
try {
String lowerContent = emlContent.toLowerCase();
int textStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_PLAIN);
if (textStart == -1) {
int bodyStart = emlContent.indexOf("\r\n\r\n");
if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n");
if (bodyStart != -1) {
bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2;
int bodyEnd = findPartEnd(emlContent, bodyStart);
return emlContent.substring(bodyStart, bodyEnd).trim();
}
return null;
}
int bodyStart = emlContent.indexOf("\r\n\r\n", textStart);
if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n", textStart);
if (bodyStart == -1) return null;
bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2;
int bodyEnd = findPartEnd(emlContent, bodyStart);
return emlContent.substring(bodyStart, bodyEnd).trim();
} catch (RuntimeException e) {
return null;
}
}
private static int findPartEnd(String content, int start) {
String[] lines = content.substring(start).split("\r?\n");
StringBuilder result = new StringBuilder();
for (String line : lines) {
if (line.startsWith("--") && line.length() > 10) break;
result.append(line).append("\n");
}
return start + result.length();
}
private static List<EmailAttachment> extractAttachmentsBasic(String emlContent) {
List<EmailAttachment> attachments = new ArrayList<>();
try {
String[] lines = emlContent.split("\r?\n");
boolean inHeaders = true;
String currentContentType = "";
String currentDisposition = "";
String currentFilename = "";
String currentEncoding = "";
for (String line : lines) {
String lowerLine = line.toLowerCase().trim();
if (line.trim().isEmpty()) {
inHeaders = false;
if (isAttachment(currentDisposition, currentFilename, currentContentType)) {
EmailAttachment attachment = new EmailAttachment();
attachment.setFilename(currentFilename);
attachment.setContentType(currentContentType);
attachment.setTransferEncoding(currentEncoding);
attachments.add(attachment);
}
currentContentType = "";
currentDisposition = "";
currentFilename = "";
currentEncoding = "";
inHeaders = true;
continue;
}
if (!inHeaders) continue;
if (lowerLine.startsWith(HEADER_CONTENT_TYPE)) {
currentContentType = line.substring(HEADER_CONTENT_TYPE.length()).trim();
} else if (lowerLine.startsWith(HEADER_CONTENT_DISPOSITION)) {
currentDisposition = line.substring(HEADER_CONTENT_DISPOSITION.length()).trim();
currentFilename = extractFilenameFromDisposition(currentDisposition);
} else if (lowerLine.startsWith(HEADER_CONTENT_TRANSFER_ENCODING)) {
currentEncoding =
line.substring(HEADER_CONTENT_TRANSFER_ENCODING.length()).trim();
}
}
} catch (RuntimeException e) {
// Continue with empty list
}
return attachments;
}
private static boolean isAttachment(String disposition, String filename, String contentType) {
return (disposition.toLowerCase().contains(DISPOSITION_ATTACHMENT) && !filename.isEmpty())
|| (!filename.isEmpty() && !contentType.toLowerCase().startsWith("text/"))
|| (contentType.toLowerCase().contains("application/") && !filename.isEmpty());
}
private static String extractFilenameFromDisposition(String disposition) {
if (disposition == null || !disposition.contains("filename=")) {
return "";
}
// Handle filename*= (RFC 2231 encoded filename)
if (disposition.toLowerCase().contains("filename*=")) {
int filenameStarStart = disposition.toLowerCase().indexOf("filename*=") + 10;
int filenameStarEnd = disposition.indexOf(";", filenameStarStart);
if (filenameStarEnd == -1) filenameStarEnd = disposition.length();
String extendedFilename =
disposition.substring(filenameStarStart, filenameStarEnd).trim();
extendedFilename = extendedFilename.replaceAll("^\"|\"$", "");
if (extendedFilename.contains("'")) {
String[] parts = extendedFilename.split("'", 3);
if (parts.length == 3) {
return EmlProcessingUtils.decodeUrlEncoded(parts[2]);
}
}
}
// Handle regular filename=
int filenameStart = disposition.toLowerCase().indexOf("filename=") + 9;
int filenameEnd = disposition.indexOf(";", filenameStart);
if (filenameEnd == -1) filenameEnd = disposition.length();
String filename = disposition.substring(filenameStart, filenameEnd).trim();
filename = filename.replaceAll("^\"|\"$", "");
return safeMimeDecode(filename);
}
public static String safeMimeDecode(String headerValue) {
if (headerValue == null || headerValue.trim().isEmpty()) {
return "";
}
if (!mimeUtilityChecked) {
synchronized (EmlParser.class) {
if (!mimeUtilityChecked) {
initializeMimeUtilityDecoding();
}
}
}
if (mimeUtilityDecodeTextMethod != null) {
try {
return (String) mimeUtilityDecodeTextMethod.invoke(null, headerValue.trim());
} catch (ReflectiveOperationException | RuntimeException e) {
// Fall through to custom implementation
}
}
return EmlProcessingUtils.decodeMimeHeader(headerValue.trim());
}
private static void initializeMimeUtilityDecoding() {
try {
Class<?> mimeUtilityClass = Class.forName("jakarta.mail.internet.MimeUtility");
mimeUtilityDecodeTextMethod = mimeUtilityClass.getMethod("decodeText", String.class);
} catch (ClassNotFoundException | NoSuchMethodException e) {
mimeUtilityDecodeTextMethod = null;
}
mimeUtilityChecked = true;
}
@Data
public static class EmailContent {
private String subject;
private String from;
private String to;
private String cc;
private String bcc;
private Date date;
private String dateString; // For basic parsing fallback
private String htmlBody;
private String textBody;
private int attachmentCount;
private List<EmailAttachment> attachments = new ArrayList<>();
public void setHtmlBody(String htmlBody) {
this.htmlBody = htmlBody != null ? htmlBody.replaceAll("\r", "") : null;
}
public void setTextBody(String textBody) {
this.textBody = textBody != null ? textBody.replaceAll("\r", "") : null;
}
}
@Data
public static class EmailAttachment {
private String filename;
private String contentType;
private byte[] data;
private boolean embedded;
private String embeddedFilename;
private long sizeBytes;
private String contentId;
private String disposition;
private String transferEncoding;
public void setData(byte[] data) {
this.data = data;
if (data != null) {
this.sizeBytes = data.length;
}
}
}
}

View File

@ -0,0 +1,601 @@
package stirling.software.common.util;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.experimental.UtilityClass;
import stirling.software.common.model.api.converters.EmlToPdfRequest;
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
@UtilityClass
public class EmlProcessingUtils {
// Style constants
private static final int DEFAULT_FONT_SIZE = 12;
private static final String DEFAULT_FONT_FAMILY = "Helvetica, sans-serif";
private static final float DEFAULT_LINE_HEIGHT = 1.4f;
private static final String DEFAULT_ZOOM = "1.0";
private static final String DEFAULT_TEXT_COLOR = "#202124";
private static final String DEFAULT_BACKGROUND_COLOR = "#ffffff";
private static final String DEFAULT_BORDER_COLOR = "#e8eaed";
private static final String ATTACHMENT_BACKGROUND_COLOR = "#f9f9f9";
private static final String ATTACHMENT_BORDER_COLOR = "#eeeeee";
private static final int EML_CHECK_LENGTH = 8192;
private static final int MIN_HEADER_COUNT_FOR_VALID_EML = 2;
// MIME type detection
private static final Map<String, String> EXTENSION_TO_MIME_TYPE =
Map.of(
".png", "image/png",
".jpg", "image/jpeg",
".jpeg", "image/jpeg",
".gif", "image/gif",
".bmp", "image/bmp",
".webp", "image/webp",
".svg", "image/svg+xml",
".ico", "image/x-icon",
".tiff", "image/tiff",
".tif", "image/tiff");
public static void validateEmlInput(byte[] emlBytes) {
if (emlBytes == null || emlBytes.length == 0) {
throw new IllegalArgumentException("EML file is empty or null");
}
if (isInvalidEmlFormat(emlBytes)) {
throw new IllegalArgumentException("Invalid EML file format");
}
}
private static boolean isInvalidEmlFormat(byte[] emlBytes) {
try {
int checkLength = Math.min(emlBytes.length, EML_CHECK_LENGTH);
String content;
try {
content = new String(emlBytes, 0, checkLength, StandardCharsets.UTF_8);
if (content.contains("\uFFFD")) {
content = new String(emlBytes, 0, checkLength, StandardCharsets.ISO_8859_1);
}
} catch (Exception e) {
content = new String(emlBytes, 0, checkLength, StandardCharsets.ISO_8859_1);
}
String lowerContent = content.toLowerCase(Locale.ROOT);
boolean hasFrom =
lowerContent.contains("from:") || lowerContent.contains("return-path:");
boolean hasSubject = lowerContent.contains("subject:");
boolean hasMessageId = lowerContent.contains("message-id:");
boolean hasDate = lowerContent.contains("date:");
boolean hasTo =
lowerContent.contains("to:")
|| lowerContent.contains("cc:")
|| lowerContent.contains("bcc:");
boolean hasMimeStructure =
lowerContent.contains("multipart/")
|| lowerContent.contains("text/plain")
|| lowerContent.contains("text/html")
|| lowerContent.contains("boundary=");
int headerCount = 0;
if (hasFrom) headerCount++;
if (hasSubject) headerCount++;
if (hasMessageId) headerCount++;
if (hasDate) headerCount++;
if (hasTo) headerCount++;
return headerCount < MIN_HEADER_COUNT_FOR_VALID_EML && !hasMimeStructure;
} catch (RuntimeException e) {
return false;
}
}
public static String generateEnhancedEmailHtml(
EmlParser.EmailContent content,
EmlToPdfRequest request,
CustomHtmlSanitizer customHtmlSanitizer) {
StringBuilder html = new StringBuilder();
html.append(
String.format(
"""
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8">
<title>%s</title>
<style>
""",
sanitizeText(content.getSubject(), customHtmlSanitizer)));
appendEnhancedStyles(html);
html.append(
"""
</style>
</head><body>
""");
html.append(
String.format(
"""
<div class="email-container">
<div class="email-header">
<h1>%s</h1>
<div class="email-meta">
<div><strong>From:</strong> %s</div>
<div><strong>To:</strong> %s</div>
""",
sanitizeText(content.getSubject(), customHtmlSanitizer),
sanitizeText(content.getFrom(), customHtmlSanitizer),
sanitizeText(content.getTo(), customHtmlSanitizer)));
if (content.getCc() != null && !content.getCc().trim().isEmpty()) {
html.append(
String.format(
"<div><strong>CC:</strong> %s</div>\n",
sanitizeText(content.getCc(), customHtmlSanitizer)));
}
if (content.getBcc() != null && !content.getBcc().trim().isEmpty()) {
html.append(
String.format(
"<div><strong>BCC:</strong> %s</div>\n",
sanitizeText(content.getBcc(), customHtmlSanitizer)));
}
if (content.getDate() != null) {
html.append(
String.format(
"<div><strong>Date:</strong> %s</div>\n",
PdfAttachmentHandler.formatEmailDate(content.getDate())));
} else if (content.getDateString() != null && !content.getDateString().trim().isEmpty()) {
html.append(
String.format(
"<div><strong>Date:</strong> %s</div>\n",
sanitizeText(content.getDateString(), customHtmlSanitizer)));
}
html.append("</div></div>\n");
html.append("<div class=\"email-body\">\n");
if (content.getHtmlBody() != null && !content.getHtmlBody().trim().isEmpty()) {
String processedHtml =
processEmailHtmlBody(content.getHtmlBody(), content, customHtmlSanitizer);
html.append(processedHtml);
} else if (content.getTextBody() != null && !content.getTextBody().trim().isEmpty()) {
html.append(
String.format(
"<div class=\"text-body\">%s</div>",
convertTextToHtml(content.getTextBody(), customHtmlSanitizer)));
} else {
html.append("<div class=\"no-content\"><p><em>No content available</em></p></div>");
}
html.append("</div>\n");
if (content.getAttachmentCount() > 0 || !content.getAttachments().isEmpty()) {
appendAttachmentsSection(html, content, request, customHtmlSanitizer);
}
html.append("</div>\n</body></html>");
return html.toString();
}
public static String processEmailHtmlBody(
String htmlBody,
EmlParser.EmailContent emailContent,
CustomHtmlSanitizer customHtmlSanitizer) {
if (htmlBody == null) return "";
String processed =
customHtmlSanitizer != null ? customHtmlSanitizer.sanitize(htmlBody) : htmlBody;
processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*fixed[^;]*;?", "");
processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*absolute[^;]*;?", "");
if (emailContent != null && !emailContent.getAttachments().isEmpty()) {
processed = PdfAttachmentHandler.processInlineImages(processed, emailContent);
}
return processed;
}
public static String convertTextToHtml(
String textBody, CustomHtmlSanitizer customHtmlSanitizer) {
if (textBody == null) return "";
String html =
customHtmlSanitizer != null
? customHtmlSanitizer.sanitize(textBody)
: escapeHtml(textBody);
html = html.replace("\r\n", "\n").replace("\r", "\n");
html = html.replace("\n", "<br>\n");
html =
html.replaceAll(
"(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)",
"<a href=\"$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
html =
html.replaceAll(
"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63})",
"<a href=\"mailto:$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
return html;
}
private static void appendEnhancedStyles(StringBuilder html) {
String css =
String.format(
"""
body {
font-family: %s;
font-size: %dpx;
line-height: %s;
color: %s;
margin: 0;
padding: 16px;
background-color: %s;
}
.email-container {
width: 100%%;
max-width: 100%%;
margin: 0 auto;
}
.email-header {
padding-bottom: 10px;
border-bottom: 1px solid %s;
margin-bottom: 10px;
}
.email-header h1 {
margin: 0 0 10px 0;
font-size: %dpx;
font-weight: bold;
}
.email-meta div {
margin-bottom: 2px;
font-size: %dpx;
}
.email-body {
word-wrap: break-word;
}
.attachment-section {
margin-top: 15px;
padding: 10px;
background-color: %s;
border: 1px solid %s;
border-radius: 3px;
}
.attachment-section h3 {
margin: 0 0 8px 0;
font-size: %dpx;
}
.attachment-item {
padding: 5px 0;
}
.attachment-icon {
margin-right: 5px;
}
.attachment-details, .attachment-type {
font-size: %dpx;
color: #555555;
}
.attachment-inclusion-note, .attachment-info-note {
margin-top: 8px;
padding: 6px;
font-size: %dpx;
border-radius: 3px;
}
.attachment-inclusion-note {
background-color: #e6ffed;
border: 1px solid #d4f7dc;
color: #006420;
}
.attachment-info-note {
background-color: #fff9e6;
border: 1px solid #fff0c2;
color: #664d00;
}
.attachment-link-container {
display: flex;
align-items: center;
padding: 8px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
margin: 4px 0;
}
.attachment-link-container:hover {
background-color: #e9ecef;
}
.attachment-note {
font-size: %dpx;
color: #6c757d;
font-style: italic;
margin-left: 8px;
}
.no-content {
padding: 20px;
text-align: center;
color: #666;
font-style: italic;
}
.text-body {
white-space: pre-wrap;
}
img {
max-width: 100%%;
height: auto;
display: block;
}
""",
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
DEFAULT_LINE_HEIGHT,
DEFAULT_TEXT_COLOR,
DEFAULT_BACKGROUND_COLOR,
DEFAULT_BORDER_COLOR,
DEFAULT_FONT_SIZE + 4,
DEFAULT_FONT_SIZE - 1,
ATTACHMENT_BACKGROUND_COLOR,
ATTACHMENT_BORDER_COLOR,
DEFAULT_FONT_SIZE + 1,
DEFAULT_FONT_SIZE - 2,
DEFAULT_FONT_SIZE - 2,
DEFAULT_FONT_SIZE - 3);
html.append(css);
}
private static void appendAttachmentsSection(
StringBuilder html,
EmlParser.EmailContent content,
EmlToPdfRequest request,
CustomHtmlSanitizer customHtmlSanitizer) {
html.append("<div class=\"attachment-section\">\n");
int displayedAttachmentCount =
content.getAttachmentCount() > 0
? content.getAttachmentCount()
: content.getAttachments().size();
html.append("<h3>Attachments (").append(displayedAttachmentCount).append(")</h3>\n");
if (!content.getAttachments().isEmpty()) {
for (int i = 0; i < content.getAttachments().size(); i++) {
EmlParser.EmailAttachment attachment = content.getAttachments().get(i);
String embeddedFilename =
attachment.getFilename() != null
? attachment.getFilename()
: ("attachment_" + i);
attachment.setEmbeddedFilename(embeddedFilename);
String sizeStr = GeneralUtils.formatBytes(attachment.getSizeBytes());
String contentType =
attachment.getContentType() != null
&& !attachment.getContentType().isEmpty()
? ", " + escapeHtml(attachment.getContentType())
: "";
String attachmentId = "attachment_" + i;
html.append(
String.format(
"""
<div class="attachment-item" id="%s">
<span class="attachment-icon" data-filename="%s">@</span>
<span class="attachment-name">%s</span>
<span class="attachment-details">(%s%s)</span>
</div>
""",
attachmentId,
escapeHtml(embeddedFilename),
escapeHtml(EmlParser.safeMimeDecode(attachment.getFilename())),
sizeStr,
contentType));
}
}
if (request != null && request.isIncludeAttachments()) {
html.append(
"""
<div class="attachment-info-note">
<p><em>Attachments are embedded in the file.</em></p>
</div>
""");
} else {
html.append(
"""
<div class="attachment-info-note">
<p><em>Attachment information displayed - files not included in PDF.</em></p>
</div>
""");
}
html.append("</div>\n");
}
public static HTMLToPdfRequest createHtmlRequest(EmlToPdfRequest request) {
HTMLToPdfRequest htmlRequest = new HTMLToPdfRequest();
if (request != null) {
htmlRequest.setFileInput(request.getFileInput());
}
htmlRequest.setZoom(Float.parseFloat(DEFAULT_ZOOM));
return htmlRequest;
}
public static String detectMimeType(String filename, String existingMimeType) {
if (existingMimeType != null && !existingMimeType.isEmpty()) {
return existingMimeType;
}
if (filename != null) {
String lowerFilename = filename.toLowerCase();
for (Map.Entry<String, String> entry : EXTENSION_TO_MIME_TYPE.entrySet()) {
if (lowerFilename.endsWith(entry.getKey())) {
return entry.getValue();
}
}
}
return "image/png";
}
public static String decodeUrlEncoded(String encoded) {
try {
return java.net.URLDecoder.decode(encoded, StandardCharsets.UTF_8);
} catch (Exception e) {
return encoded; // Return original if decoding fails
}
}
public static String decodeMimeHeader(String encodedText) {
if (encodedText == null || encodedText.trim().isEmpty()) {
return encodedText;
}
try {
StringBuilder result = new StringBuilder();
Pattern concatenatedPattern =
Pattern.compile(
"(=\\?[^?]+\\?[BbQq]\\?[^?]*\\?=)(\\s*=\\?[^?]+\\?[BbQq]\\?[^?]*\\?=)+");
Matcher concatenatedMatcher = concatenatedPattern.matcher(encodedText);
String processedText =
concatenatedMatcher.replaceAll(
match -> match.group().replaceAll("\\s+(?==\\?)", ""));
Pattern mimePattern = Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?=");
Matcher matcher = mimePattern.matcher(processedText);
int lastEnd = 0;
while (matcher.find()) {
result.append(processedText, lastEnd, matcher.start());
String charset = matcher.group(1);
String encoding = matcher.group(2).toUpperCase();
String encodedValue = matcher.group(3);
try {
String decodedValue =
switch (encoding) {
case "B" -> {
String cleanBase64 = encodedValue.replaceAll("\\s", "");
byte[] decodedBytes = Base64.getDecoder().decode(cleanBase64);
Charset targetCharset;
try {
targetCharset = Charset.forName(charset);
} catch (Exception e) {
targetCharset = StandardCharsets.UTF_8;
}
yield new String(decodedBytes, targetCharset);
}
case "Q" -> decodeQuotedPrintable(encodedValue, charset);
default -> matcher.group(0); // Return original if unknown encoding
};
result.append(decodedValue);
} catch (RuntimeException e) {
result.append(matcher.group(0)); // Keep original on decode error
}
lastEnd = matcher.end();
}
result.append(processedText.substring(lastEnd));
return result.toString();
} catch (Exception e) {
return encodedText; // Return original on any parsing error
}
}
private static String decodeQuotedPrintable(String encodedText, String charset) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < encodedText.length(); i++) {
char c = encodedText.charAt(i);
switch (c) {
case '=' -> {
if (i + 2 < encodedText.length()) {
String hex = encodedText.substring(i + 1, i + 3);
try {
int value = Integer.parseInt(hex, 16);
result.append((char) value);
i += 2;
} catch (NumberFormatException e) {
result.append(c);
}
} else if (i + 1 == encodedText.length()
|| (i + 2 == encodedText.length()
&& encodedText.charAt(i + 1) == '\n')) {
if (i + 1 < encodedText.length() && encodedText.charAt(i + 1) == '\n') {
i++; // Skip the newline too
}
} else {
result.append(c);
}
}
case '_' -> result.append(' '); // Space encoding in Q encoding
default -> result.append(c);
}
}
byte[] bytes = result.toString().getBytes(StandardCharsets.ISO_8859_1);
try {
Charset targetCharset = Charset.forName(charset);
return new String(bytes, targetCharset);
} catch (Exception e) {
try {
return new String(bytes, StandardCharsets.UTF_8);
} catch (Exception fallbackException) {
return new String(bytes, StandardCharsets.ISO_8859_1);
}
}
}
public static String escapeHtml(String text) {
if (text == null) return "";
return text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
public static String sanitizeText(String text, CustomHtmlSanitizer customHtmlSanitizer) {
if (customHtmlSanitizer != null) {
return customHtmlSanitizer.sanitize(text);
} else {
return escapeHtml(text);
}
}
public static String simplifyHtmlContent(String htmlContent) {
String simplified = htmlContent.replaceAll("(?i)<script[^>]*>.*?</script>", "");
simplified = simplified.replaceAll("(?i)<style[^>]*>.*?</style>", "");
return simplified;
}
}

View File

@ -0,0 +1,680 @@
package stirling.software.common.util;
import static stirling.software.common.util.AttachmentUtils.setCatalogViewerPreferences;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PageMode;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.multipart.MultipartFile;
import lombok.Data;
import lombok.Getter;
import lombok.experimental.UtilityClass;
import stirling.software.common.service.CustomPDFDocumentFactory;
@UtilityClass
public class PdfAttachmentHandler {
// Note: This class is designed for EML attachments, not general PDF attachments.
private static final String ATTACHMENT_MARKER = "@";
private static final float ATTACHMENT_ICON_WIDTH = 12f;
private static final float ATTACHMENT_ICON_HEIGHT = 14f;
private static final float ANNOTATION_X_OFFSET = 2f;
private static final float ANNOTATION_Y_OFFSET = 10f;
public static byte[] attachFilesToPdf(
byte[] pdfBytes,
List<EmlParser.EmailAttachment> attachments,
CustomPDFDocumentFactory pdfDocumentFactory)
throws IOException {
if (attachments == null || attachments.isEmpty()) {
return pdfBytes;
}
try (PDDocument document = pdfDocumentFactory.load(pdfBytes);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
List<MultipartFile> multipartAttachments = new ArrayList<>(attachments.size());
for (int i = 0; i < attachments.size(); i++) {
EmlParser.EmailAttachment attachment = attachments.get(i);
if (attachment.getData() != null && attachment.getData().length > 0) {
String embeddedFilename =
attachment.getFilename() != null
? attachment.getFilename()
: ("attachment_" + i);
attachment.setEmbeddedFilename(embeddedFilename);
multipartAttachments.add(createMultipartFile(attachment));
}
}
if (!multipartAttachments.isEmpty()) {
Map<Integer, String> indexToFilenameMap =
addAttachmentsToDocumentWithMapping(
document, multipartAttachments, attachments);
setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS);
addAttachmentAnnotationsToDocumentWithMapping(
document, attachments, indexToFilenameMap);
}
document.save(outputStream);
return outputStream.toByteArray();
} catch (RuntimeException e) {
throw new IOException(
"Invalid PDF structure or processing error: " + e.getMessage(), e);
} catch (Exception e) {
throw new IOException("Error attaching files to PDF: " + e.getMessage(), e);
}
}
private static MultipartFile createMultipartFile(EmlParser.EmailAttachment attachment) {
return new MultipartFile() {
@Override
public @NotNull String getName() {
return "attachment";
}
@Override
public String getOriginalFilename() {
return attachment.getFilename() != null
? attachment.getFilename()
: "attachment_" + System.currentTimeMillis();
}
@Override
public String getContentType() {
return attachment.getContentType() != null
? attachment.getContentType()
: "application/octet-stream";
}
@Override
public boolean isEmpty() {
return attachment.getData() == null || attachment.getData().length == 0;
}
@Override
public long getSize() {
return attachment.getData() != null ? attachment.getData().length : 0;
}
@Override
public byte @NotNull [] getBytes() {
return attachment.getData() != null ? attachment.getData() : new byte[0];
}
@Override
public @NotNull InputStream getInputStream() {
byte[] data = attachment.getData();
return new ByteArrayInputStream(data != null ? data : new byte[0]);
}
@Override
public void transferTo(@NotNull File dest) throws IOException, IllegalStateException {
try (FileOutputStream fos = new FileOutputStream(dest)) {
byte[] data = attachment.getData();
if (data != null) {
fos.write(data);
}
}
}
};
}
private static String ensureUniqueFilename(String filename, Set<String> existingNames) {
if (!existingNames.contains(filename)) {
return filename;
}
String baseName;
String extension = "";
int lastDot = filename.lastIndexOf('.');
if (lastDot > 0) {
baseName = filename.substring(0, lastDot);
extension = filename.substring(lastDot);
} else {
baseName = filename;
}
int counter = 1;
String uniqueName;
do {
uniqueName = baseName + "_" + counter + extension;
counter++;
} while (existingNames.contains(uniqueName));
return uniqueName;
}
private static @NotNull PDRectangle calculateAnnotationRectangle(
PDPage page, float x, float y) {
PDRectangle cropBox = page.getCropBox();
// ISO 32000-1:2008 Section 8.3: PDF coordinate system transforms
int rotation = page.getRotation();
float pdfX = x;
float pdfY = cropBox.getHeight() - y;
switch (rotation) {
case 90 -> {
float temp = pdfX;
pdfX = pdfY;
pdfY = cropBox.getWidth() - temp;
}
case 180 -> {
pdfX = cropBox.getWidth() - pdfX;
pdfY = y;
}
case 270 -> {
float temp = pdfX;
pdfX = cropBox.getHeight() - pdfY;
pdfY = temp;
}
default -> {}
}
float iconHeight = ATTACHMENT_ICON_HEIGHT;
float paddingX = 2.0f;
float paddingY = 2.0f;
PDRectangle rect =
new PDRectangle(
pdfX + ANNOTATION_X_OFFSET + paddingX,
pdfY - iconHeight + ANNOTATION_Y_OFFSET + paddingY,
ATTACHMENT_ICON_WIDTH,
iconHeight);
PDRectangle mediaBox = page.getMediaBox();
if (rect.getLowerLeftX() < mediaBox.getLowerLeftX()
|| rect.getLowerLeftY() < mediaBox.getLowerLeftY()
|| rect.getUpperRightX() > mediaBox.getUpperRightX()
|| rect.getUpperRightY() > mediaBox.getUpperRightY()) {
float adjustedX =
Math.max(
mediaBox.getLowerLeftX(),
Math.min(
rect.getLowerLeftX(),
mediaBox.getUpperRightX() - rect.getWidth()));
float adjustedY =
Math.max(
mediaBox.getLowerLeftY(),
Math.min(
rect.getLowerLeftY(),
mediaBox.getUpperRightY() - rect.getHeight()));
rect = new PDRectangle(adjustedX, adjustedY, rect.getWidth(), rect.getHeight());
}
return rect;
}
public static String processInlineImages(
String htmlContent, EmlParser.EmailContent emailContent) {
if (htmlContent == null || emailContent == null) return htmlContent;
Map<String, EmlParser.EmailAttachment> contentIdMap = new HashMap<>();
for (EmlParser.EmailAttachment attachment : emailContent.getAttachments()) {
if (attachment.isEmbedded()
&& attachment.getContentId() != null
&& attachment.getData() != null) {
contentIdMap.put(attachment.getContentId(), attachment);
}
}
if (contentIdMap.isEmpty()) return htmlContent;
Pattern cidPattern =
Pattern.compile(
"(?i)<img[^>]*\\ssrc\\s*=\\s*['\"]cid:([^'\"]+)['\"][^>]*>",
Pattern.CASE_INSENSITIVE);
Matcher matcher = cidPattern.matcher(htmlContent);
StringBuilder result = new StringBuilder();
while (matcher.find()) {
String contentId = matcher.group(1);
EmlParser.EmailAttachment attachment = contentIdMap.get(contentId);
if (attachment != null && attachment.getData() != null) {
String mimeType =
EmlProcessingUtils.detectMimeType(
attachment.getFilename(), attachment.getContentType());
String base64Data = Base64.getEncoder().encodeToString(attachment.getData());
String dataUri = "data:" + mimeType + ";base64," + base64Data;
String replacement =
matcher.group(0).replaceFirst("cid:" + Pattern.quote(contentId), dataUri);
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
} else {
matcher.appendReplacement(result, Matcher.quoteReplacement(matcher.group(0)));
}
}
matcher.appendTail(result);
return result.toString();
}
public static String formatEmailDate(Date date) {
if (date == null) return "";
SimpleDateFormat formatter =
new SimpleDateFormat("EEE, MMM d, yyyy 'at' h:mm a z", Locale.ENGLISH);
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
return formatter.format(date);
}
@Data
public static class MarkerPosition {
private int pageIndex;
private float x;
private float y;
private String character;
private String filename;
public MarkerPosition(int pageIndex, float x, float y, String character, String filename) {
this.pageIndex = pageIndex;
this.x = x;
this.y = y;
this.character = character;
this.filename = filename;
}
}
public static class AttachmentMarkerPositionFinder extends PDFTextStripper {
@Getter private final List<MarkerPosition> positions = new ArrayList<>();
private int currentPageIndex;
protected boolean sortByPosition;
private boolean isInAttachmentSection;
private boolean attachmentSectionFound;
private final StringBuilder currentText = new StringBuilder();
private static final Pattern ATTACHMENT_SECTION_PATTERN =
Pattern.compile("attachments\\s*\\(\\d+\\)", Pattern.CASE_INSENSITIVE);
private static final Pattern FILENAME_PATTERN =
Pattern.compile("@\\s*([^\\s\\(]+(?:\\.[a-zA-Z0-9]+)?)");
public AttachmentMarkerPositionFinder() {
super();
this.currentPageIndex = 0;
this.sortByPosition = false; // Disable sorting to preserve document order
this.isInAttachmentSection = false;
this.attachmentSectionFound = false;
}
@Override
public String getText(PDDocument document) throws IOException {
super.getText(document);
if (sortByPosition) {
positions.sort(
(a, b) -> {
int pageCompare = Integer.compare(a.getPageIndex(), b.getPageIndex());
if (pageCompare != 0) return pageCompare;
return Float.compare(
b.getY(), a.getY()); // Descending Y per PDF coordinate system
});
}
return ""; // Return empty string as we only need positions
}
@Override
protected void startPage(PDPage page) throws IOException {
super.startPage(page);
}
@Override
protected void endPage(PDPage page) throws IOException {
currentPageIndex++;
super.endPage(page);
}
@Override
protected void writeString(String string, List<TextPosition> textPositions)
throws IOException {
String lowerString = string.toLowerCase();
if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) {
isInAttachmentSection = true;
attachmentSectionFound = true;
}
if (isInAttachmentSection
&& (lowerString.contains("</body>")
|| lowerString.contains("</html>")
|| (attachmentSectionFound
&& lowerString.trim().isEmpty()
&& string.length() > 50))) {
isInAttachmentSection = false;
}
if (isInAttachmentSection) {
currentText.append(string);
for (int i = 0; (i = string.indexOf(ATTACHMENT_MARKER, i)) != -1; i++) {
if (i < textPositions.size()) {
TextPosition textPosition = textPositions.get(i);
String filename = extractFilenameAfterMarker(string, i);
MarkerPosition position =
new MarkerPosition(
currentPageIndex,
textPosition.getXDirAdj(),
textPosition.getYDirAdj(),
ATTACHMENT_MARKER,
filename);
positions.add(position);
}
}
}
super.writeString(string, textPositions);
}
@Override
public void setSortByPosition(boolean sortByPosition) {
this.sortByPosition = sortByPosition;
}
private String extractFilenameAfterMarker(String text, int markerIndex) {
String afterMarker = text.substring(markerIndex + 1);
Matcher matcher = FILENAME_PATTERN.matcher("@" + afterMarker);
if (matcher.find()) {
return matcher.group(1);
}
String[] parts = afterMarker.split("[\\s\\(\\)]+");
for (String part : parts) {
part = part.trim();
if (part.length() > 3 && part.contains(".")) {
return part;
}
}
return null;
}
}
private static Map<Integer, String> addAttachmentsToDocumentWithMapping(
PDDocument document,
List<MultipartFile> attachments,
List<EmlParser.EmailAttachment> originalAttachments)
throws IOException {
PDDocumentCatalog catalog = document.getDocumentCatalog();
if (catalog == null) {
throw new IOException("PDF document catalog is not accessible");
}
PDDocumentNameDictionary documentNames = catalog.getNames();
if (documentNames == null) {
documentNames = new PDDocumentNameDictionary(catalog);
catalog.setNames(documentNames);
}
PDEmbeddedFilesNameTreeNode embeddedFilesTree = documentNames.getEmbeddedFiles();
if (embeddedFilesTree == null) {
embeddedFilesTree = new PDEmbeddedFilesNameTreeNode();
documentNames.setEmbeddedFiles(embeddedFilesTree);
}
Map<String, PDComplexFileSpecification> existingNames = embeddedFilesTree.getNames();
if (existingNames == null) {
existingNames = new HashMap<>();
}
Map<Integer, String> indexToFilenameMap = new HashMap<>();
for (int i = 0; i < attachments.size(); i++) {
MultipartFile attachment = attachments.get(i);
String filename = attachment.getOriginalFilename();
if (filename == null || filename.trim().isEmpty()) {
filename = "attachment_" + i;
}
String normalizedFilename =
isAscii(filename)
? filename
: java.text.Normalizer.normalize(
filename, java.text.Normalizer.Form.NFC);
String uniqueFilename =
ensureUniqueFilename(normalizedFilename, existingNames.keySet());
indexToFilenameMap.put(i, uniqueFilename);
PDEmbeddedFile embeddedFile = new PDEmbeddedFile(document, attachment.getInputStream());
embeddedFile.setSize((int) attachment.getSize());
GregorianCalendar currentTime = new GregorianCalendar();
embeddedFile.setCreationDate(currentTime);
embeddedFile.setModDate(currentTime);
String contentType = attachment.getContentType();
if (contentType != null && !contentType.trim().isEmpty()) {
embeddedFile.setSubtype(contentType);
}
PDComplexFileSpecification fileSpecification = new PDComplexFileSpecification();
fileSpecification.setFile(uniqueFilename);
fileSpecification.setFileUnicode(uniqueFilename);
fileSpecification.setEmbeddedFile(embeddedFile);
fileSpecification.setEmbeddedFileUnicode(embeddedFile);
existingNames.put(uniqueFilename, fileSpecification);
}
embeddedFilesTree.setNames(existingNames);
documentNames.setEmbeddedFiles(embeddedFilesTree);
catalog.setNames(documentNames);
return indexToFilenameMap;
}
private static void addAttachmentAnnotationsToDocumentWithMapping(
PDDocument document,
List<EmlParser.EmailAttachment> attachments,
Map<Integer, String> indexToFilenameMap)
throws IOException {
if (document.getNumberOfPages() == 0 || attachments == null || attachments.isEmpty()) {
return;
}
AttachmentMarkerPositionFinder finder = new AttachmentMarkerPositionFinder();
finder.setSortByPosition(false); // Keep document order to maintain pairing
finder.getText(document);
List<MarkerPosition> markerPositions = finder.getPositions();
int annotationsToAdd = Math.min(markerPositions.size(), attachments.size());
for (int i = 0; i < annotationsToAdd; i++) {
MarkerPosition position = markerPositions.get(i);
String filenameNearMarker = position.getFilename();
EmlParser.EmailAttachment matchingAttachment =
findAttachmentByFilename(attachments, filenameNearMarker);
if (matchingAttachment != null) {
String embeddedFilename =
findEmbeddedFilenameForAttachment(matchingAttachment, indexToFilenameMap);
if (embeddedFilename != null) {
PDPage page = document.getPage(position.getPageIndex());
addAttachmentAnnotationToPageWithMapping(
document,
page,
matchingAttachment,
embeddedFilename,
position.getX(),
position.getY(),
i);
} else {
// No embedded filename found for attachment
}
} else {
// No matching attachment found for filename near marker
}
}
}
private static EmlParser.EmailAttachment findAttachmentByFilename(
List<EmlParser.EmailAttachment> attachments, String targetFilename) {
if (targetFilename == null || targetFilename.trim().isEmpty()) {
return null;
}
String normalizedTarget = normalizeFilename(targetFilename);
// First try exact match
for (EmlParser.EmailAttachment attachment : attachments) {
if (attachment.getFilename() != null) {
String normalizedAttachment = normalizeFilename(attachment.getFilename());
if (normalizedAttachment.equals(normalizedTarget)) {
return attachment;
}
}
}
// Then try contains match
for (EmlParser.EmailAttachment attachment : attachments) {
if (attachment.getFilename() != null) {
String normalizedAttachment = normalizeFilename(attachment.getFilename());
if (normalizedAttachment.contains(normalizedTarget)
|| normalizedTarget.contains(normalizedAttachment)) {
return attachment;
}
}
}
return null;
}
private static String findEmbeddedFilenameForAttachment(
EmlParser.EmailAttachment attachment, Map<Integer, String> indexToFilenameMap) {
String attachmentFilename = attachment.getFilename();
if (attachmentFilename == null) {
return null;
}
for (Map.Entry<Integer, String> entry : indexToFilenameMap.entrySet()) {
String embeddedFilename = entry.getValue();
if (embeddedFilename != null
&& (embeddedFilename.equals(attachmentFilename)
|| embeddedFilename.contains(attachmentFilename)
|| attachmentFilename.contains(embeddedFilename))) {
return embeddedFilename;
}
}
return null;
}
private static String normalizeFilename(String filename) {
if (filename == null) return "";
return filename.toLowerCase()
.trim()
.replaceAll("\\s+", " ")
.replaceAll("[^a-zA-Z0-9._-]", "");
}
private static void addAttachmentAnnotationToPageWithMapping(
PDDocument document,
PDPage page,
EmlParser.EmailAttachment attachment,
String embeddedFilename,
float x,
float y,
int attachmentIndex)
throws IOException {
PDAnnotationFileAttachment fileAnnotation = new PDAnnotationFileAttachment();
PDRectangle rect = calculateAnnotationRectangle(page, x, y);
fileAnnotation.setRectangle(rect);
fileAnnotation.setPrinted(false);
fileAnnotation.setHidden(false);
fileAnnotation.setNoView(false);
fileAnnotation.setNoZoom(true);
fileAnnotation.setNoRotate(true);
try {
PDAppearanceDictionary appearance = new PDAppearanceDictionary();
PDAppearanceStream normalAppearance = new PDAppearanceStream(document);
normalAppearance.setBBox(new PDRectangle(0, 0, rect.getWidth(), rect.getHeight()));
appearance.setNormalAppearance(normalAppearance);
fileAnnotation.setAppearance(appearance);
} catch (RuntimeException e) {
fileAnnotation.setAppearance(null);
}
PDEmbeddedFilesNameTreeNode efTree =
document.getDocumentCatalog().getNames().getEmbeddedFiles();
if (efTree != null) {
Map<String, PDComplexFileSpecification> efMap = efTree.getNames();
if (efMap != null) {
PDComplexFileSpecification fileSpec = efMap.get(embeddedFilename);
if (fileSpec != null) {
fileAnnotation.setFile(fileSpec);
} else {
// Could not find embedded file
}
}
}
fileAnnotation.setContents(
"Attachment " + (attachmentIndex + 1) + ": " + attachment.getFilename());
fileAnnotation.setAnnotationName(
"EmbeddedFile_" + attachmentIndex + "_" + embeddedFilename);
page.getAnnotations().add(fileAnnotation);
}
private static boolean isAscii(String str) {
if (str == null) return true;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) > 127) {
return false;
}
}
return true;
}
}

View File

@ -35,6 +35,7 @@ import io.github.pixee.security.Filenames;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory;
@Slf4j
@ -145,13 +146,18 @@ public class PdfUtils {
throws IOException, Exception {
// Validate and limit DPI to prevent excessive memory usage
final int MAX_SAFE_DPI = 500; // Maximum safe DPI to prevent memory issues
if (DPI > MAX_SAFE_DPI) {
int maxSafeDpi = 500; // Default maximum safe DPI
ApplicationProperties properties =
ApplicationContextProvider.getBean(ApplicationProperties.class);
if (properties != null && properties.getSystem() != null) {
maxSafeDpi = properties.getSystem().getMaxDPI();
}
if (DPI > maxSafeDpi) {
throw ExceptionUtils.createIllegalArgumentException(
"error.dpiExceedsLimit",
"DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.",
DPI,
MAX_SAFE_DPI);
maxSafeDpi);
}
try (PDDocument document = pdfDocumentFactory.load(inputStream)) {

View File

@ -8,6 +8,8 @@ import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.util.ApplicationContextProvider;
import stirling.software.common.util.CheckProgramInstall;
@Controller
@ -62,6 +64,13 @@ public class ConverterWebController {
@Hidden
public String pdfToimgForm(Model model) {
boolean isPython = CheckProgramInstall.isPythonAvailable();
ApplicationProperties properties =
ApplicationContextProvider.getBean(ApplicationProperties.class);
if (properties != null && properties.getSystem() != null) {
model.addAttribute("maxDPI", properties.getSystem().getMaxDPI());
} else {
model.addAttribute("maxDPI", 500); // Default value if not set
}
model.addAttribute("isPython", isPython);
model.addAttribute("currentPage", "pdf-to-img");
return "convert/pdf-to-img";

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=نوع اللون
pdfToImage.color=اللون
pdfToImage.grey=تدرج الرمادي
pdfToImage.blackwhite=أبيض وأسود (قد يفقد البيانات!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=تحويل
pdfToImage.info=Python غير مثبت. مطلوب لتحويل WebP.
pdfToImage.placeholder=(مثال: 1,2,8 أو 4,7,12-16 أو 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Rəng Tipi
pdfToImage.color=Rəng
pdfToImage.grey=Boz Tonlama
pdfToImage.blackwhite=Qara və Ağ (Data İtə Bilər)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Çevir
pdfToImage.info=Python Yüklü Deyil.WebP Çevirməsi Üçün Vacibdir
pdfToImage.placeholder=(məsələn, 1,2,8 və ya 4,7,12-16 və ya 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Тип цвят
pdfToImage.color=Цвят
pdfToImage.grey=Скала на сивото
pdfToImage.blackwhite=Черно и бяло (може да загубите данни!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Преобразуване
pdfToImage.info=Python не е инсталиран. Изисква се за конвертиране на WebP.
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=ཚོས་མདོག་གི་རིགས།
pdfToImage.color=ཚོས་མདོག
pdfToImage.grey=སྐྱ་མདོག
pdfToImage.blackwhite=དཀར་ནག (གནས་ཚུལ་བརླག་སྲིད།)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=བསྒྱུར་བ།
pdfToImage.info=Python སྒྲིག་འཇུག་བྱས་མི་འདུག WebP བསྒྱུར་བར་དགོས་མཁོ་ཡིན།
pdfToImage.placeholder=(དཔེར་ན། 1,2,8 ཡང་ན་ 4,7,12-16 ཡང་ན་ 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Tipus de Color
pdfToImage.color=Color
pdfToImage.grey=Escala de Grisos
pdfToImage.blackwhite=Blanc i Negre (Pot perdre dades!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Converteix
pdfToImage.info=Python no està instal·lat. És necessari per a la conversió a WebP.
pdfToImage.placeholder=(p. ex. 1,2,8 o 4,7,12-16 o 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Typ barev
pdfToImage.color=Barevný
pdfToImage.grey=Stupně šedi
pdfToImage.blackwhite=Černobílý (Může dojít ke ztrátě dat!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Převést
pdfToImage.info=Python není nainstalován. Vyžadován pro konverzi do WebP.
pdfToImage.placeholder=(např. 1,2,8 nebo 4,7,12-16 nebo 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Farvetype
pdfToImage.color=Farve
pdfToImage.grey=Gråtone
pdfToImage.blackwhite=Sort og Hvid (Kan miste data!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Konvertér
pdfToImage.info=Python er ikke installeret. Påkrævet for WebP-konvertering.
pdfToImage.placeholder=(f.eks. 1,2,8 eller 4,7,12-16 eller 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Farbtyp
pdfToImage.color=Farbe
pdfToImage.grey=Graustufen
pdfToImage.blackwhite=Schwarzweiß (Datenverlust möglich!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Umwandeln
pdfToImage.info=Python ist nicht installiert. Erforderlich für die WebP-Konvertierung.
pdfToImage.placeholder=(z.B. 1,2,8 oder 4,7,12-16 oder 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Vorhandene Lesezeichen ersetzen (deaktiviere
editTableOfContents.editorTitle=Lesezeichen-Editor
editTableOfContents.editorDesc=Fügen unten Lesezeichen hinzu und ordne sie an. Klicke auf +, um das untergeordnete Lesezeichen hinzuzufügen.
editTableOfContents.addBookmark=Neues Lesezeichen hinzufügen
editTableOfContents.importBookmarksDefault=Importieren
editTableOfContents.importBookmarksFromJsonFile=JSON-Datei hochladen
editTableOfContents.importBookmarksFromClipboard=Aus Zwischenablage einfügen
editTableOfContents.exportBookmarksDefault=Exportieren
editTableOfContents.exportBookmarksAsJson=Als JSON herunterladen
editTableOfContents.exportBookmarksAsText=Als Text kopieren
editTableOfContents.desc.1=Mit diesem Werkzeug können Sie das Inhaltsverzeichnis (Lesezeichen) eines PDF-Dokuments hinzufügen oder bearbeiten.
editTableOfContents.desc.2=Sie können eine hierarchische Struktur erstellen, indem Sie untergeordnete Lesezeichen zu übergeordneten hinzufügen.
editTableOfContents.desc.3=Jedes Lesezeichen benötigt einen Titel und eine Seitenzahl.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Τύπος χρώματος
pdfToImage.color=Έγχρωμο
pdfToImage.grey=Κλίμακα του γκρι
pdfToImage.blackwhite=Ασπρόμαυρο (Μπορεί να χαθούν δεδομένα!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Μετατροπή
pdfToImage.info=Η Python δεν είναι εγκατεστημένη. Απαιτείται για μετατροπή WebP.
pdfToImage.placeholder=(π.χ. 1,2,8 ή 4,7,12-16 ή 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1434,6 +1434,7 @@ pdfToImage.colorType=Colour type
pdfToImage.color=Colour
pdfToImage.grey=Greyscale
pdfToImage.blackwhite=Black and White (May lose data!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Convert
pdfToImage.info=Python is not installed. Required for WebP conversion.
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
@ -1891,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Color type
pdfToImage.color=Color
pdfToImage.grey=Grayscale
pdfToImage.blackwhite=Black and White (May lose data!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Convert
pdfToImage.info=Python is not installed. Required for WebP conversion.
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Tipo de color
pdfToImage.color=Color
pdfToImage.grey=Escala de grises
pdfToImage.blackwhite=Blanco y Negro (¡Puede perder datos!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Convertir
pdfToImage.info=Python no está instalado. Se requiere para la conversión WebP.
pdfToImage.placeholder=(por ejemplo 1,2,8 o 4,7,12-16 o 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Kolore-mota
pdfToImage.color=Kolorea
pdfToImage.grey=Gris-eskala
pdfToImage.blackwhite=Zuria eta Beltza (Datuak galdu ditzake!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Bihurtu
pdfToImage.info=Python is not installed. Required for WebP conversion.
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=نوع رنگ
pdfToImage.color=رنگ
pdfToImage.grey=خاکستری
pdfToImage.blackwhite=سیاه و سفید (ممکن است اطلاعات از دست برود!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=تبدیل
pdfToImage.info=پایتون نصب نشده است. برای تبدیل WebP لازم است.
pdfToImage.placeholder=(مثال: 1,2,8 یا 4,7,12-16 یا 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Type d'impression
pdfToImage.color=Couleur
pdfToImage.grey=Niveaux de gris
pdfToImage.blackwhite=Noir et blanc (peut engendrer une perte de données !)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Convertir
pdfToImage.info=Python n'est pas installé. Nécessaire pour la conversion WebP.
pdfToImage.placeholder=(par exemple : 1,2,8 ou 4,7,12-16 ou 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Remplacer les signets existants (décocher p
editTableOfContents.editorTitle=Éditeur de signets
editTableOfContents.editorDesc=Ajoutez et organisez les signets ci-dessous. Cliquez sur + pour ajouter des signets enfants.
editTableOfContents.addBookmark=Ajouter un nouveau signet
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=Cet outil vous permet d'ajouter ou de modifier la table des matières (signets) dans un document PDF.
editTableOfContents.desc.2=Vous pouvez créer une structure hiérarchique en ajoutant des signets enfants à des signets parents.
editTableOfContents.desc.3=Chaque signet nécessite un titre et un numéro de page cible.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Cineál dath
pdfToImage.color=Dath
pdfToImage.grey=Scála Liath
pdfToImage.blackwhite=Dubh agus Bán (Dfhéadfadh sonraí a chailleadh!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Tiontaigh
pdfToImage.info=Níl Python suiteáilte. Ag teastáil le haghaidh comhshó WebP.
pdfToImage.placeholder=(m.sh. 1,2,8 nó 4,7,12-16 nó 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=रंग प्रकार
pdfToImage.color=रंग
pdfToImage.grey=ग्रेस्केल
pdfToImage.blackwhite=काला और सफेद (डेटा खो सकता है!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=बदलें
pdfToImage.info=Python स्थापित नहीं है। WebP रूपांतरण के लिए आवश्यक है।
pdfToImage.placeholder=(जैसे 1,2,8 या 4,7,12-16 या 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Tip boje
pdfToImage.color=Boja
pdfToImage.grey=Sivi tonovi
pdfToImage.blackwhite=Crno-bijelo (mogu se izgubiti podaci!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Pretvori
pdfToImage.info=Python nije instaliran. Treba je za konverziju na WebP.
pdfToImage.placeholder=(t.j. 1,2,8 ili 4,7,12-16 ili 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Színtípus
pdfToImage.color=Színes
pdfToImage.grey=Szürkeárnyalatos
pdfToImage.blackwhite=Fekete-fehér (adatvesztéssel járhat!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Konvertálás
pdfToImage.info=Python nincs telepítve. WebP konverzióhoz szükséges.
pdfToImage.placeholder=(pl. 1,2,8 vagy 4,7,12-16 vagy 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Meglévő könyvjelzők cseréje (törölje
editTableOfContents.editorTitle=Könyvjelző szerkesztő
editTableOfContents.editorDesc=Könyvjelzők hozzáadása és rendezése lent. Kattintson a + gombra gyermek könyvjelzők hozzáadásához.
editTableOfContents.addBookmark=Új könyvjelző hozzáadása
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=Ez az eszköz lehetővé teszi a tartalomjegyzék (könyvjelzők) hozzáadását vagy szerkesztését egy PDF dokumentumban.
editTableOfContents.desc.2=Hierarchikus struktúrákat hozhat létre, ha gyermek könyvjelzőket ad a szülő könyvjelzőkhöz.
editTableOfContents.desc.3=Minden könyvjelzőhöz szükséges egy cím és egy céloldalszám.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Tipe warna
pdfToImage.color=Warna
pdfToImage.grey=Skala abu-abu
pdfToImage.blackwhite=Black and White (Bisa kehilangan data!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Konversi
pdfToImage.info=Python tidak terinstal. Diperlukan untuk konversi WebP.
pdfToImage.placeholder=(misalnya 1,2,8 atau 4,7,12-16 atau 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Tipo di colore
pdfToImage.color=A colori
pdfToImage.grey=Scala di grigi
pdfToImage.blackwhite=Bianco e Nero (potresti perdere dettagli!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Converti
pdfToImage.info=Python non è installato.È richiesto per la conversione WebP.
pdfToImage.placeholder=(es. 1,2,8 o 4,7,12-16 o 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Sostituisci i segnalibri esistenti (deselezi
editTableOfContents.editorTitle=Editor segnalibri
editTableOfContents.editorDesc=Aggiungi e disponi i segnalibri qui sotto. Fai clic su + per aggiungere segnalibri secondari.
editTableOfContents.addBookmark=Aggiungi nuovo segnalibro
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=Questo strumento consente di aggiungere o modificare il sommario (segnalibri) in un documento PDF.
editTableOfContents.desc.2=È possibile creare una struttura gerarchica aggiungendo segnalibri secondari a quelli principali.
editTableOfContents.desc.3=Ogni segnalibro richiede un titolo e un numero di pagina di destinazione.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=カラーモード
pdfToImage.color=カラー
pdfToImage.grey=グレースケール
pdfToImage.blackwhite=白黒(データが失われる可能性があります!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=変換
pdfToImage.info=Pythonがインストールされていません。WebPの変換に必要です。
pdfToImage.placeholder=(例:1,2,8、4,7,12-16、2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=既存のしおりを置き換える(既
editTableOfContents.editorTitle=しおりエディター
editTableOfContents.editorDesc=以下にしおりを追加して配置します。+をクリックして、子のしおりを追加します。
editTableOfContents.addBookmark=新しいしおりを追加
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=このツールを使用すると、PDFドキュメントに目次しおりを追加または編集できます。
editTableOfContents.desc.2=親しおりに子しおりを追加することで階層構造を作成できます。
editTableOfContents.desc.3=各しおりにはタイトルと対象のページ番号が必要です。

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=색상 유형
pdfToImage.color=컬러
pdfToImage.grey=그레이스케일
pdfToImage.blackwhite=흑백 (데이터 손실 가능성 있음!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=변환
pdfToImage.info=WebP 변환에는 Python이 필요합니다. Python이 설치되지 않았습니다.
pdfToImage.placeholder=(예: 1,2,8 또는 4,7,12-16 또는 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=നിറ തരം
pdfToImage.color=നിറം
pdfToImage.grey=ഗ്രേസ്കെയിൽ
pdfToImage.blackwhite=കറുപ്പും വെളുപ്പും (ഡാറ്റ നഷ്ടപ്പെട്ടേക്കാം!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=പരിവർത്തനം ചെയ്യുക
pdfToImage.info=പൈത്തൺ ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. WebP പരിവർത്തനത്തിന് ആവശ്യമാണ്.
pdfToImage.placeholder=(ഉദാ. 1,2,8 അല്ലെങ്കിൽ 4,7,12-16 അല്ലെങ്കിൽ 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Kleurtype
pdfToImage.color=Kleur
pdfToImage.grey=Grijstinten
pdfToImage.blackwhite=Zwart en wit (kan data verliezen!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Omzetten
pdfToImage.info=Python is niet geïnstalleerd. Vereist voor WebP-conversie.
pdfToImage.placeholder=(bijv. 1,2,8 of 4,7,12-16 of 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Farge type
pdfToImage.color=Farge
pdfToImage.grey=Gråtone
pdfToImage.blackwhite=Svart-hvitt (kan miste data!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Konverter
pdfToImage.info=Python is not installed. Required for WebP conversion.
pdfToImage.placeholder=(f.eks. 1,2,8 eller 4,7,12-16 eller 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Rodzaj koloru
pdfToImage.color=Kolor
pdfToImage.grey=Odcień szarości
pdfToImage.blackwhite=Czarno-biały (może spowodować utratę danych!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Konwertuj
pdfToImage.info=Python nie został zainstalowany. Jest wymagany do konwersji WebP.
pdfToImage.placeholder=(przykład 1,2,8 lub 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Cor de saída:
pdfToImage.color=Colorido
pdfToImage.grey=Escala de Cinza
pdfToImage.blackwhite=Preto e Branco (pode perder informações!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Converter
pdfToImage.info=Python não está instalado. Necessário para conversão WebP.
pdfToImage.placeholder=(por exemplo 1,2,8 ou 4,7,12-16 ou 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Tipo de cor
pdfToImage.color=Cor
pdfToImage.grey=Escala de Cinza
pdfToImage.blackwhite=Preto e Branco (Pode perder dados!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Converter
pdfToImage.info=Python não está instalado. Necessário para conversão WebP.
pdfToImage.placeholder=(ex. 1,2,8 ou 4,7,12-16 ou 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Tip culoare
pdfToImage.color=Culoare
pdfToImage.grey=Scală de gri
pdfToImage.blackwhite=Alb și negru (Poate pierde date!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Convertește
pdfToImage.info=Python nu este instalat. Necesar pentru conversia WebP.
pdfToImage.placeholder=(ex. 1,2,8 sau 4,7,12-16 sau 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Тип цвета
pdfToImage.color=Цветной
pdfToImage.grey=Оттенки серого
pdfToImage.blackwhite=Черно-белый (возможна потеря данных!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Преобразовать
pdfToImage.info=Python не установлен. Требуется для конвертации в WebP.
pdfToImage.placeholder=(например, 1,2,8 или 4,7,12-16 или 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Заменить существующие з
editTableOfContents.editorTitle=Редактор закладок
editTableOfContents.editorDesc=Добавьте и упорядочьте закладки ниже. Нажмите «+», чтобы добавить дочерние закладки.
editTableOfContents.addBookmark=Добавить новую закладку
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=Этот инструмент позволяет вам добавлять или редактировать оглавление (закладки) в PDF-документе.
editTableOfContents.desc.2=Вы можете создать иерархическую структуру, добавив дочерние закладки к родительским.
editTableOfContents.desc.3=Для каждой закладки требуется название и номер целевой страницы.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Typ farby
pdfToImage.color=Farba
pdfToImage.grey=Odtiene šedej
pdfToImage.blackwhite=Čierno-biele (Môže stratiť údaje!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Konvertovať
pdfToImage.info=Python is not installed. Required for WebP conversion.
pdfToImage.placeholder=(napr. 1,2,8 alebo 4,7,12-16 alebo 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Vrsta barve
pdfToImage.color=Barva
pdfToImage.grey=Sivine
pdfToImage.blackwhite=Črno-belo (Lahko izgubite podatke!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Pretvori
pdfToImage.info=Python ni nameščen. Zahtevano za pretvorbo WebP.
pdfToImage.placeholder=(npr. 1,2,8 ali 4,7,12-16 ali 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Režim boja:
pdfToImage.color=Kolor
pdfToImage.grey=Monohromatski
pdfToImage.blackwhite=Crno-belo (Može izgubiti detalje!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Konvertuj
pdfToImage.info=Python nije instaliran. Neophodan je za WebP konverziju.
pdfToImage.placeholder=(npr. 1,2,8 ili 4,7,12-16 ili 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Zameni postojeće obeleživače (isključi d
editTableOfContents.editorTitle=Editor obeleživača
editTableOfContents.editorDesc=Dodaj i rasporedi obeleživače ispod. Klikni + za dodavanje podređenih obeleživača.
editTableOfContents.addBookmark=Dodaj novi obeleživač
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=Ovaj alat omogućava dodavanje ili izmenu sadržaja (obeleživača) u PDF dokumentu.
editTableOfContents.desc.2=Moguće je kreirati hijerarhijsku strukturu dodavanjem podređenih obeleživača nadređenim obeleživačima.
editTableOfContents.desc.3=Svaki obeleživač zahteva naslov i broj ciljne strane.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Färgtyp
pdfToImage.color=Färg
pdfToImage.grey=Gråskala
pdfToImage.blackwhite=Svartvitt (kan förlora data!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Konvertera
pdfToImage.info=Python är inte installerat. Krävs för WebP-konvertering.
pdfToImage.placeholder=(t.ex. 1,2,8 eller 4,7,12-16 eller 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=ประเภทสี
pdfToImage.color=สี
pdfToImage.grey=ระดับสีเทา
pdfToImage.blackwhite=ขาวดำ (อาจสูญเสียข้อมูล!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=แปลง
pdfToImage.info=Python ไม่มีการติดตั้ง จำเป็นสำหรับการแปลง WebP
pdfToImage.placeholder=(เช่น 1,2,8 หรือ 4,7,12-16 หรือ 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Renk türü
pdfToImage.color=Renk
pdfToImage.grey=Gri tonlama
pdfToImage.blackwhite=Siyah ve Beyaz (Veri kaybolabilir!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Dönüştür
pdfToImage.info=Python kurulu değil. WebP dönüşümü için gereklidir.
pdfToImage.placeholder=(örneğin 1,2,8 veya 4,7,12-16 ya da 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Mevcut yer işaretlerini değiştir (var ola
editTableOfContents.editorTitle=Yer İşareti Düzenleyici
editTableOfContents.editorDesc=Aşağıdan yer işaretleri ekleyin ve düzenleyin. Alt yer işareti eklemek için + simgesine tıklayın.
editTableOfContents.addBookmark=Yeni Yer İşareti Ekle
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=Bu araç, bir PDF belgesine içindekiler tablosu (yer işaretleri) eklemenizi veya mevcut olanları düzenlemenizi sağlar.
editTableOfContents.desc.2=Alt yer işaretleri ekleyerek hiyerarşik bir yapı oluşturabilirsiniz.
editTableOfContents.desc.3=Her yer işareti bir başlık ve hedef sayfa numarası gerektirir.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Тип кольору
pdfToImage.color=Колір
pdfToImage.grey=Відтінки сірого
pdfToImage.blackwhite=Чорно-білий (може втратити дані!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Конвертувати
pdfToImage.info=Python не встановлено. Необхідно для конвертації WebP.
pdfToImage.placeholder=(наприклад 1,2,8 або 4,7,12-16 або 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=Loại màu
pdfToImage.color=Màu
pdfToImage.grey=Thang độ xám
pdfToImage.blackwhite=Đen trắng (Có thể mất dữ liệu!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=Chuyển đổi
pdfToImage.info=Python is not installed. Required for WebP conversion.
pdfToImage.placeholder=(ví dụ: 1,2,8 hoặc 4,7,12-16 hoặc 2n-1)
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=颜色类型
pdfToImage.color=颜色
pdfToImage.grey=灰度
pdfToImage.blackwhite=黑白(可能会丢失数据!)。
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=转换
pdfToImage.info=WebP 转换需要安装 Python
pdfToImage.placeholder=例如1,2,8 或 4,7,12-16 或 2n-1
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=替换现有书签(取消勾选则追加
editTableOfContents.editorTitle=书签编辑器
editTableOfContents.editorDesc=在下方添加并排列书签,点击 + 可添加子书签
editTableOfContents.addBookmark=添加书签
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=此工具可用于在 PDF 文档中添加或编辑目录(书签)
editTableOfContents.desc.2=您可以通过为父书签添加子书签来构建层级结构
editTableOfContents.desc.3=每个书签需填写标题和目标页码

View File

@ -1402,6 +1402,7 @@ pdfToImage.colorType=顏色類型
pdfToImage.color=顏色
pdfToImage.grey=灰度
pdfToImage.blackwhite=黑白(可能會遺失資料!)
pdfToImage.dpi=DPI (The server limit is {0} dpi)
pdfToImage.submit=轉換
pdfToImage.info=尚未安裝 Python。需要安裝 Python 才能進行 WebP 轉換。
pdfToImage.placeholder=(例如 1,2,8 或 4,7,12-16 或 2n-1
@ -1859,6 +1860,12 @@ editTableOfContents.replaceExisting=取代現有書籤 (取消勾選以附加到
editTableOfContents.editorTitle=書籤編輯器
editTableOfContents.editorDesc=在下方新增和排列書籤。點選 + 新增子書籤。
editTableOfContents.addBookmark=新增書籤
editTableOfContents.importBookmarksDefault=Import
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
editTableOfContents.exportBookmarksDefault=Export
editTableOfContents.exportBookmarksAsJson=Download as JSON
editTableOfContents.exportBookmarksAsText=Copy as text
editTableOfContents.desc.1=此工具可讓您在 PDF 文件中新增或編輯目錄 (書籤)。
editTableOfContents.desc.2=您可以透過將子書籤新增至父書籤來建立階層式結構。
editTableOfContents.desc.3=每個書籤都需要標題和目標頁碼。

View File

@ -108,6 +108,7 @@ system:
enableAnalytics: null # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true
enableUrlToPDF: false # Set to 'true' to enable URL to PDF, INTERNAL ONLY, known security issues, should not be used externally
disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML)
maxDPI: 500 # Maximum allowed DPI for PDF to image conversion
html:
urlSecurity:
enabled: true # Enable URL security restrictions for HTML processing

View File

@ -156,7 +156,7 @@
.bookmark-actions {
margin-top: 20px;
display: flex;
justify-content: flex-start;
justify-content: space-between;
}
/* Collapse/expand icons */
@ -274,3 +274,25 @@
--bg-empty: var(--md-sys-color-surface-container-low, #24282e);
--border-empty: var(--md-sys-color-outline, #495057);
}
.success-flash {
position: relative;
}
.success-flash::after {
content: "✓";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
font-size: 1.2em;
color: white;
opacity: 0;
animation: fadeOut 1s ease-in-out;
}
@keyframes fadeOut {
0% { opacity: 1; }
100% { opacity: 0; }
}

View File

@ -1,88 +1,117 @@
document.addEventListener('DOMContentLoaded', function() {
const bookmarksContainer = document.getElementById('bookmarks-container');
const addBookmarkBtn = document.getElementById('addBookmarkBtn');
const bookmarkDataInput = document.getElementById('bookmarkData');
document.addEventListener("DOMContentLoaded", function () {
const bookmarksContainer = document.getElementById("bookmarks-container");
const errorMessageContainer = document.getElementById("error-message-container");
const addBookmarkBtn = document.getElementById("addBookmarkBtn");
const bookmarkDataInput = document.getElementById("bookmarkData");
let bookmarks = [];
let counter = 0; // Used for generating unique IDs
// Add event listener to the file input to extract existing bookmarks
document.getElementById('fileInput-input').addEventListener('change', async function(e) {
if (!e.target.files || e.target.files.length === 0) {
// callback function on file input change to extract bookmarks from PDF
async function getBookmarkDataFromPdf(event) {
if (!event.target.files || event.target.files.length === 0) {
return;
}
// Reset bookmarks initially
bookmarks = [];
updateBookmarksUI();
const formData = new FormData();
formData.append('file', e.target.files[0]);
// Show loading indicator
showLoadingIndicator();
formData.append("file", event.target.files[0]);
try {
// Call the API to extract bookmarks using fetchWithCsrf for CSRF protection
const response = await fetchWithCsrf('/api/v1/general/extract-bookmarks', {
method: 'POST',
body: formData
const response = await fetchWithCsrf("/api/v1/general/extract-bookmarks", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Failed to extract bookmarks: ${response.status} ${response.statusText}`);
throw new Error(`Failed to fetch API: ${response.status} ${response.statusText}`);
}
const extractedBookmarks = await response.json();
return extractedBookmarks;
} catch (error) {
throw new Error("Error extracting bookmark data:", error);
}
}
// callback function on file input change to extract bookmarks from JSON
async function getBookmarkDataFromJson(event) {
if (!event.target.files || event.target.files.length === 0) {
return;
}
const file = event.target.files[0];
try {
const fileText = await file.text();
const jsonData = JSON.parse(fileText);
return jsonData;
} catch (error) {
throw new Error(`Error extracting bookmark data: error while reading or parsing JSON file: ${error.message}`);
}
}
// display new bookmark data given by a callback function that loads or fetches the data
async function loadBookmarks(getBookmarkDataCallback) {
// reset bookmarks
bookmarks = [];
updateBookmarksUI();
showLoadingIndicator();
try {
// Get new bookmarks from the callback
const newBookmarks = await getBookmarkDataCallback();
// Convert extracted bookmarks to our format with IDs
if (extractedBookmarks && extractedBookmarks.length > 0) {
bookmarks = extractedBookmarks.map(convertExtractedBookmark);
} else {
showEmptyState();
if (newBookmarks && newBookmarks.length > 0) {
bookmarks = newBookmarks.map(convertExtractedBookmark);
}
} catch (error) {
// Show error message
showErrorMessage('Failed to extract bookmarks. You can still create new ones.');
// Add a default bookmark if no bookmarks and error
if (bookmarks.length === 0) {
showEmptyState();
}
bookmarks = [];
throw new Error(`Error loading bookmarks: ${error}`);
} finally {
// Remove loading indicator
removeLoadingIndicator();
// Update the UI
updateBookmarksUI();
}
}
// Add event listener to the file input to extract existing bookmarks
document.getElementById("fileInput-input").addEventListener("change", async function (event) {
try {
await loadBookmarks(async function () {
return getBookmarkDataFromPdf(event);
});
} catch {
showErrorMessage("Failed to extract bookmarks. You can still create new ones.");
}
});
function showLoadingIndicator() {
const loadingEl = document.createElement('div');
loadingEl.className = 'alert alert-info';
loadingEl.textContent = 'Loading bookmarks from PDF...';
loadingEl.id = 'loading-bookmarks';
bookmarksContainer.innerHTML = '';
const loadingEl = document.createElement("div");
loadingEl.className = "alert alert-info";
loadingEl.textContent = "Loading bookmarks from PDF...";
loadingEl.id = "loading-bookmarks";
errorMessageContainer.innerHTML = "";
bookmarksContainer.innerHTML = "";
bookmarksContainer.appendChild(loadingEl);
}
function removeLoadingIndicator() {
const loadingEl = document.getElementById('loading-bookmarks');
const loadingEl = document.getElementById("loading-bookmarks");
if (loadingEl) {
loadingEl.remove();
}
}
function showErrorMessage(message) {
const errorEl = document.createElement('div');
errorEl.className = 'alert alert-danger';
const errorEl = document.createElement("div");
errorEl.className = "alert alert-danger";
errorEl.textContent = message;
bookmarksContainer.appendChild(errorEl);
errorMessageContainer.appendChild(errorEl);
}
function showEmptyState() {
const emptyStateEl = document.createElement('div');
emptyStateEl.className = 'empty-bookmarks mb-3';
const emptyStateEl = document.createElement("div");
emptyStateEl.className = "empty-bookmarks mb-3";
emptyStateEl.innerHTML = `
<span class="material-symbols-rounded mb-2" style="font-size: 48px;">bookmark_add</span>
<h5>No bookmarks found</h5>
@ -93,8 +122,8 @@ document.addEventListener('DOMContentLoaded', function() {
`;
// Add event listener to the "Add First Bookmark" button
emptyStateEl.querySelector('.btn-add-first-bookmark').addEventListener('click', function() {
addBookmark(null, 'New Bookmark', 1);
emptyStateEl.querySelector(".btn-add-first-bookmark").addEventListener("click", function () {
addBookmark(null, "New Bookmark", 1);
emptyStateEl.remove();
});
@ -106,15 +135,15 @@ document.addEventListener('DOMContentLoaded', function() {
counter++;
const result = {
id: Date.now() + counter, // Generate a unique ID
title: bookmark.title || 'Untitled Bookmark',
title: bookmark.title || "Untitled Bookmark",
pageNumber: bookmark.pageNumber || 1,
children: [],
expanded: false // All bookmarks start collapsed for better visibility
expanded: false, // All bookmarks start collapsed for better visibility
};
// Convert children recursively
if (bookmark.children && bookmark.children.length > 0) {
result.children = bookmark.children.map(child => {
result.children = bookmark.children.map((child) => {
return convertExtractedBookmark(child);
});
}
@ -123,24 +152,24 @@ document.addEventListener('DOMContentLoaded', function() {
}
// Add bookmark button click handler
addBookmarkBtn.addEventListener('click', function(e) {
addBookmarkBtn.addEventListener("click", function (e) {
e.preventDefault();
addBookmark();
});
// Add form submit handler to update JSON data
document.getElementById('editTocForm').addEventListener('submit', function() {
document.getElementById("editTocForm").addEventListener("submit", function () {
updateBookmarkData();
});
function addBookmark(parent = null, title = '', pageNumber = 1) {
function addBookmark(parent = null, title = "", pageNumber = 1) {
counter++;
const newBookmark = {
id: Date.now() + counter,
title: title || 'New Bookmark',
title: title || "New Bookmark",
pageNumber: pageNumber || 1,
children: [],
expanded: false // New bookmarks start collapsed
expanded: false, // New bookmarks start collapsed
};
if (parent === null) {
@ -162,13 +191,13 @@ document.addEventListener('DOMContentLoaded', function() {
setTimeout(() => {
const newElement = document.querySelector(`[data-id="${newBookmark.id}"]`);
if (newElement) {
const titleInput = newElement.querySelector('.bookmark-title');
const titleInput = newElement.querySelector(".bookmark-title");
if (titleInput) {
titleInput.focus();
titleInput.select();
}
// Scroll to the new element
newElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
newElement.scrollIntoView({ behavior: "smooth", block: "center" });
}
}, 50);
}
@ -203,7 +232,7 @@ document.addEventListener('DOMContentLoaded', function() {
function removeBookmark(id) {
// Remove from top level
const index = bookmarks.findIndex(b => b.id === id);
const index = bookmarks.findIndex((b) => b.id === id);
if (index !== -1) {
bookmarks.splice(index, 1);
updateBookmarksUI();
@ -213,7 +242,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Remove from children
function removeFromChildren(bookmarkArray, id) {
for (const bookmark of bookmarkArray) {
const childIndex = bookmark.children.findIndex(b => b.id === id);
const childIndex = bookmark.children.findIndex((b) => b.id === id);
if (childIndex !== -1) {
bookmark.children.splice(childIndex, 1);
return true;
@ -253,7 +282,7 @@ document.addEventListener('DOMContentLoaded', function() {
return {
title: bookmark.title,
pageNumber: bookmark.pageNumber,
children: bookmark.children.map(cleanBookmark)
children: bookmark.children.map(cleanBookmark),
};
}
@ -263,22 +292,22 @@ document.addEventListener('DOMContentLoaded', function() {
}
// Only clear the container if there are no error messages or loading indicators
if (!document.querySelector('#bookmarks-container .alert')) {
bookmarksContainer.innerHTML = '';
if (!document.querySelector("#bookmarks-container .alert")) {
bookmarksContainer.innerHTML = "";
}
// Check if there are bookmarks to display
if (bookmarks.length === 0 && !document.querySelector('.empty-bookmarks')) {
if (bookmarks.length === 0 && !document.querySelector(".empty-bookmarks")) {
showEmptyState();
} else {
// Remove empty state if it exists and there are bookmarks
const emptyState = document.querySelector('.empty-bookmarks');
const emptyState = document.querySelector(".empty-bookmarks");
if (emptyState && bookmarks.length > 0) {
emptyState.remove();
}
// Create bookmark elements
bookmarks.forEach(bookmark => {
bookmarks.forEach((bookmark) => {
const bookmarkElement = createBookmarkElement(bookmark);
bookmarksContainer.appendChild(bookmarkElement);
});
@ -287,15 +316,15 @@ document.addEventListener('DOMContentLoaded', function() {
updateBookmarkData();
// Initialize tooltips for dynamically added elements
if (typeof $ !== 'undefined') {
if (typeof $ !== "undefined") {
$('[data-bs-toggle="tooltip"]').tooltip();
}
}
// Create the main bookmark element with collapsible interface
function createBookmarkElement(bookmark, level = 0) {
const bookmarkEl = document.createElement('div');
bookmarkEl.className = 'bookmark-item';
const bookmarkEl = document.createElement("div");
bookmarkEl.className = "bookmark-item";
bookmarkEl.dataset.id = bookmark.id;
bookmarkEl.dataset.level = level;
@ -304,10 +333,10 @@ document.addEventListener('DOMContentLoaded', function() {
bookmarkEl.appendChild(header);
// Create the content (collapsible part)
const content = document.createElement('div');
content.className = 'bookmark-content';
const content = document.createElement("div");
content.className = "bookmark-content";
if (!bookmark.expanded) {
content.style.display = 'none';
content.style.display = "none";
}
// Main input row
@ -328,48 +357,48 @@ document.addEventListener('DOMContentLoaded', function() {
// Create the header that's always visible
function createBookmarkHeader(bookmark, level) {
const header = document.createElement('div');
header.className = 'bookmark-header';
const header = document.createElement("div");
header.className = "bookmark-header";
if (!bookmark.expanded) {
header.classList.add('collapsed');
header.classList.add("collapsed");
}
// Left side of header with expand/collapse and info
const headerLeft = document.createElement('div');
headerLeft.className = 'd-flex align-items-center';
const headerLeft = document.createElement("div");
headerLeft.className = "d-flex align-items-center";
// Toggle expand/collapse icon with child count
const toggleContainer = document.createElement('div');
toggleContainer.className = 'd-flex align-items-center';
toggleContainer.style.marginRight = '8px';
const toggleContainer = document.createElement("div");
toggleContainer.className = "d-flex align-items-center";
toggleContainer.style.marginRight = "8px";
// Only show toggle if has children
if (bookmark.children && bookmark.children.length > 0) {
// Create toggle icon
const toggleIcon = document.createElement('span');
toggleIcon.className = 'material-symbols-rounded toggle-icon me-1';
toggleIcon.textContent = 'expand_more';
toggleIcon.style.cursor = 'pointer';
const toggleIcon = document.createElement("span");
toggleIcon.className = "material-symbols-rounded toggle-icon me-1";
toggleIcon.textContent = "expand_more";
toggleIcon.style.cursor = "pointer";
toggleContainer.appendChild(toggleIcon);
// Add child count indicator
const childCount = document.createElement('span');
childCount.className = 'badge rounded-pill';
const childCount = document.createElement("span");
childCount.className = "badge rounded-pill";
// Use theme-appropriate badge color
const isDarkMode = document.documentElement.getAttribute('data-bs-theme') === 'dark';
childCount.classList.add(isDarkMode ? 'bg-info' : 'bg-secondary');
childCount.style.fontSize = '0.7rem';
childCount.style.padding = '0.2em 0.5em';
const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark";
childCount.classList.add(isDarkMode ? "bg-info" : "bg-secondary");
childCount.style.fontSize = "0.7rem";
childCount.style.padding = "0.2em 0.5em";
childCount.textContent = bookmark.children.length;
childCount.setAttribute('data-bs-toggle', 'tooltip');
childCount.setAttribute('data-bs-placement', 'top');
childCount.title = `${bookmark.children.length} child bookmark${bookmark.children.length > 1 ? 's' : ''}`;
childCount.setAttribute("data-bs-toggle", "tooltip");
childCount.setAttribute("data-bs-placement", "top");
childCount.title = `${bookmark.children.length} child bookmark${bookmark.children.length > 1 ? "s" : ""}`;
toggleContainer.appendChild(childCount);
} else {
// Add spacer if no children
const spacer = document.createElement('span');
spacer.style.width = '24px';
spacer.style.display = 'inline-block';
const spacer = document.createElement("span");
spacer.style.width = "24px";
spacer.style.display = "inline-block";
toggleContainer.appendChild(spacer);
}
@ -378,65 +407,68 @@ document.addEventListener('DOMContentLoaded', function() {
// Level indicator for nested items
if (level > 0) {
// Add relationship indicator visual line
const relationshipIndicator = document.createElement('div');
relationshipIndicator.className = 'bookmark-relationship-indicator';
const relationshipIndicator = document.createElement("div");
relationshipIndicator.className = "bookmark-relationship-indicator";
const line = document.createElement('div');
line.className = 'relationship-line';
const line = document.createElement("div");
line.className = "relationship-line";
relationshipIndicator.appendChild(line);
const arrow = document.createElement('div');
arrow.className = 'relationship-arrow';
const arrow = document.createElement("div");
arrow.className = "relationship-arrow";
relationshipIndicator.appendChild(arrow);
header.appendChild(relationshipIndicator);
// Text indicator
const levelIndicator = document.createElement('span');
levelIndicator.className = 'bookmark-level-indicator';
const levelIndicator = document.createElement("span");
levelIndicator.className = "bookmark-level-indicator";
levelIndicator.textContent = `Child`;
headerLeft.appendChild(levelIndicator);
}
// Title preview
const titlePreview = document.createElement('span');
titlePreview.className = 'bookmark-title-preview';
const titlePreview = document.createElement("span");
titlePreview.className = "bookmark-title-preview";
titlePreview.textContent = bookmark.title;
headerLeft.appendChild(titlePreview);
// Page number preview
const pagePreview = document.createElement('span');
pagePreview.className = 'bookmark-page-preview';
const pagePreview = document.createElement("span");
pagePreview.className = "bookmark-page-preview";
pagePreview.textContent = `Page ${bookmark.pageNumber}`;
headerLeft.appendChild(pagePreview);
// Right side of header with action buttons
const headerRight = document.createElement('div');
headerRight.className = 'bookmark-actions-header';
const headerRight = document.createElement("div");
headerRight.className = "bookmark-actions-header";
// Quick add buttons with clear visual distinction - using Stirling-PDF's tooltip system
const quickAddChildButton = createButton('subdirectory_arrow_right', 'btn-add-child', 'Add child bookmark', function(e) {
const quickAddChildButton = createButton("subdirectory_arrow_right", "btn-add-child", "Add child bookmark", function (e) {
e.preventDefault();
e.stopPropagation();
addBookmark(bookmark.id);
});
const quickAddSiblingButton = createButton('add', 'btn-add-sibling', 'Add sibling bookmark', function(e) {
const quickAddSiblingButton = createButton("add", "btn-add-sibling", "Add sibling bookmark", function (e) {
e.preventDefault();
e.stopPropagation();
// Find parent of current bookmark
const parentId = findParentBookmark(bookmarks, bookmark.id);
addBookmark(parentId, '', bookmark.pageNumber); // Same level as current bookmark
addBookmark(parentId, "", bookmark.pageNumber); // Same level as current bookmark
});
// Quick remove button
const quickRemoveButton = createButton('delete', 'btn-outline-danger', 'Remove bookmark', function(e) {
const quickRemoveButton = createButton("delete", "btn-outline-danger", "Remove bookmark", function (e) {
e.preventDefault();
e.stopPropagation();
if (confirm('Are you sure you want to remove this bookmark' +
(bookmark.children.length > 0 ? ' and all its children?' : '?'))) {
if (
confirm(
"Are you sure you want to remove this bookmark" + (bookmark.children.length > 0 ? " and all its children?" : "?")
)
) {
removeBookmark(bookmark.id);
}
});
@ -450,9 +482,9 @@ document.addEventListener('DOMContentLoaded', function() {
header.appendChild(headerRight);
// Add click handler for expansion toggle
header.addEventListener('click', function(e) {
header.addEventListener("click", function (e) {
// Only toggle if not clicking on buttons
if (!e.target.closest('button')) {
if (!e.target.closest("button")) {
toggleBookmarkExpanded(bookmark.id);
}
});
@ -461,8 +493,8 @@ document.addEventListener('DOMContentLoaded', function() {
}
function createInputRow(bookmark) {
const row = document.createElement('div');
row.className = 'row';
const row = document.createElement("div");
row.className = "row";
// Title input
row.appendChild(createTitleInputElement(bookmark));
@ -474,26 +506,26 @@ document.addEventListener('DOMContentLoaded', function() {
}
function createTitleInputElement(bookmark) {
const titleCol = document.createElement('div');
titleCol.className = 'col-md-8';
const titleCol = document.createElement("div");
titleCol.className = "col-md-8";
const titleGroup = document.createElement('div');
titleGroup.className = 'mb-3';
const titleGroup = document.createElement("div");
titleGroup.className = "mb-3";
const titleLabel = document.createElement('label');
titleLabel.textContent = 'Title';
titleLabel.className = 'form-label';
const titleLabel = document.createElement("label");
titleLabel.textContent = "Title";
titleLabel.className = "form-label";
const titleInput = document.createElement('input');
titleInput.type = 'text';
titleInput.className = 'form-control bookmark-title';
const titleInput = document.createElement("input");
titleInput.type = "text";
titleInput.className = "form-control bookmark-title";
titleInput.value = bookmark.title;
titleInput.addEventListener('input', function() {
titleInput.addEventListener("input", function () {
bookmark.title = this.value;
updateBookmarkData();
// Also update the preview in the header
const header = titleInput.closest('.bookmark-item').querySelector('.bookmark-title-preview');
const header = titleInput.closest(".bookmark-item").querySelector(".bookmark-title-preview");
if (header) {
header.textContent = this.value;
}
@ -507,27 +539,27 @@ document.addEventListener('DOMContentLoaded', function() {
}
function createPageInputElement(bookmark) {
const pageCol = document.createElement('div');
pageCol.className = 'col-md-4';
const pageCol = document.createElement("div");
pageCol.className = "col-md-4";
const pageGroup = document.createElement('div');
pageGroup.className = 'mb-3';
const pageGroup = document.createElement("div");
pageGroup.className = "mb-3";
const pageLabel = document.createElement('label');
pageLabel.textContent = 'Page';
pageLabel.className = 'form-label';
const pageLabel = document.createElement("label");
pageLabel.textContent = "Page";
pageLabel.className = "form-label";
const pageInput = document.createElement('input');
pageInput.type = 'number';
pageInput.className = 'form-control bookmark-page';
const pageInput = document.createElement("input");
pageInput.type = "number";
pageInput.className = "form-control bookmark-page";
pageInput.value = bookmark.pageNumber;
pageInput.min = 1;
pageInput.addEventListener('input', function() {
pageInput.addEventListener("input", function () {
bookmark.pageNumber = parseInt(this.value) || 1;
updateBookmarkData();
// Also update the preview in the header
const header = pageInput.closest('.bookmark-item').querySelector('.bookmark-page-preview');
const header = pageInput.closest(".bookmark-item").querySelector(".bookmark-page-preview");
if (header) {
header.textContent = `Page ${bookmark.pageNumber}`;
}
@ -541,25 +573,25 @@ document.addEventListener('DOMContentLoaded', function() {
}
function createButton(icon, className, title, clickHandler) {
const button = document.createElement('button');
button.type = 'button';
const button = document.createElement("button");
button.type = "button";
button.className = `btn ${className} btn-bookmark-action`;
button.innerHTML = `<span class="material-symbols-rounded">${icon}</span>`;
// Use Bootstrap tooltips
button.setAttribute('data-bs-toggle', 'tooltip');
button.setAttribute('data-bs-placement', 'top');
button.setAttribute("data-bs-toggle", "tooltip");
button.setAttribute("data-bs-placement", "top");
button.title = title;
button.addEventListener('click', clickHandler);
button.addEventListener("click", clickHandler);
return button;
}
function createChildrenContainer(bookmark, level) {
const childrenContainer = document.createElement('div');
childrenContainer.className = 'bookmark-children';
const childrenContainer = document.createElement("div");
childrenContainer.className = "bookmark-children";
bookmark.children.forEach(child => {
bookmark.children.forEach((child) => {
childrenContainer.appendChild(createBookmarkElement(child, level + 1));
});
@ -568,24 +600,24 @@ document.addEventListener('DOMContentLoaded', function() {
// Update the add bookmark button appearance with clear visual cue
addBookmarkBtn.innerHTML = '<span class="material-symbols-rounded">add</span> Add Top-level Bookmark';
addBookmarkBtn.className = 'btn btn-primary btn-add-bookmark top-level';
addBookmarkBtn.className = "btn btn-primary btn-add-bookmark top-level";
// Use Bootstrap tooltips
addBookmarkBtn.setAttribute('data-bs-toggle', 'tooltip');
addBookmarkBtn.setAttribute('data-bs-placement', 'top');
addBookmarkBtn.title = 'Add a new top-level bookmark';
addBookmarkBtn.setAttribute("data-bs-toggle", "tooltip");
addBookmarkBtn.setAttribute("data-bs-placement", "top");
addBookmarkBtn.title = "Add a new top-level bookmark";
// Add icon to empty state button as well
const updateEmptyStateButton = function() {
const emptyStateBtn = document.querySelector('.btn-add-first-bookmark');
const updateEmptyStateButton = function () {
const emptyStateBtn = document.querySelector(".btn-add-first-bookmark");
if (emptyStateBtn) {
emptyStateBtn.innerHTML = '<span class="material-symbols-rounded">add</span> Add First Bookmark';
emptyStateBtn.setAttribute('data-bs-toggle', 'tooltip');
emptyStateBtn.setAttribute('data-bs-placement', 'top');
emptyStateBtn.title = 'Add first bookmark';
emptyStateBtn.setAttribute("data-bs-toggle", "tooltip");
emptyStateBtn.setAttribute("data-bs-placement", "top");
emptyStateBtn.title = "Add first bookmark";
// Initialize tooltips for the empty state button
if (typeof $ !== 'undefined') {
if (typeof $ !== "undefined") {
$('[data-bs-toggle="tooltip"]').tooltip();
}
}
@ -597,14 +629,147 @@ document.addEventListener('DOMContentLoaded', function() {
updateEmptyStateButton();
}
// Add bookmarks Import/Export functionality
// Import/Export button references
const importDefaultBtn = document.getElementById("importDefaultBtn");
const exportDefaultBtn = document.getElementById("exportDefaultBtn");
const importUploadJsonFileInput = document.getElementById("importUploadJsonFileInput");
const importPasteFromClipboardBtn = document.getElementById("importPasteFromClipboardBtn");
const exportDownloadJsonFileBtn = document.getElementById("exportDownloadJsonFileBtn");
const exportCopyToClipboardBtn = document.getElementById("exportCopyToClipboardBtn");
// display import/export from/to clipboard buttons if supported
if (navigator.clipboard && navigator.clipboard.readText) {
importPasteFromClipboardBtn.parentElement.classList.remove("d-none");
}
if (navigator.clipboard && navigator.clipboard.writeText) {
exportCopyToClipboardBtn.parentElement.classList.remove("d-none");
}
function flashButtonSuccess(button) {
const originalClass = button.className;
button.classList.remove("btn-outline-primary");
button.classList.add("btn-success", "success-flash");
setTimeout(() => {
button.className = originalClass;
}, 1000);
}
// Import handlers
async function handleJsonFileInputChange(event) {
try {
await loadBookmarks(async function () {
return getBookmarkDataFromJson(event);
});
flashButtonSuccess(importDefaultBtn);
} catch (error) {
console.error(`Failed to import bookmarks from JSON file: ${error.message}`);
}
}
async function importBookmarksFromClipboard() {
console.log("Importing bookmarks from clipboard...");
try {
await loadBookmarks(async function () {
const clipboardText = await navigator.clipboard.readText();
if (!clipboardText) return [];
return JSON.parse(clipboardText);
});
flashButtonSuccess(importDefaultBtn);
} catch (error) {
console.error(`Failed to import bookmarks from clipboard: ${error.message}`);
}
}
async function handleBookmarksPasteFromClipboard(event) {
// do not override normal paste behavior on input fields
if (event.target.tagName.toLowerCase() === "input") return;
try {
await loadBookmarks(async function () {
const clipboardText = event.clipboardData?.getData("text/plain");
if (!clipboardText) return [];
return JSON.parse(clipboardText);
});
flashButtonSuccess(importDefaultBtn);
} catch (error) {
console.error(`Failed to import bookmarks from clipboard (ctrl-v): ${error.message}`);
}
}
// Export handlers
async function exportBookmarksToJson() {
console.log("Exporting bookmarks to JSON...");
try {
const bookmarkData = bookmarkDataInput.value;
const blob = new Blob([bookmarkData], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "bookmarks.json";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
flashButtonSuccess(exportDefaultBtn);
} catch (error) {
console.error(`Failed to export bookmarks to JSON: ${error.message}`);
}
}
async function exportBookmarksToClipboard() {
const bookmarkData = bookmarkDataInput.value;
try {
await navigator.clipboard.writeText(bookmarkData);
flashButtonSuccess(exportDefaultBtn);
} catch (error) {
console.error(`Failed to export bookmarks to clipboard: ${error.message}`);
}
}
async function handleBookmarksCopyToClipboard(event) {
// do not override normal copy behavior on input fields
if (event.target.tagName.toLowerCase() === "input") return;
const bookmarkData = bookmarkDataInput.value;
try {
event.clipboardData.setData("text/plain", bookmarkData);
event.preventDefault();
flashButtonSuccess(exportDefaultBtn);
} catch (error) {
console.error(`Failed to export bookmarks to clipboard (ctrl-c): ${error.message}`);
}
}
// register event listeners for import/export functions
importUploadJsonFileInput.addEventListener("change", handleJsonFileInputChange);
importPasteFromClipboardBtn.addEventListener("click", importBookmarksFromClipboard);
exportDownloadJsonFileBtn.addEventListener("click", exportBookmarksToJson);
exportCopyToClipboardBtn.addEventListener("click", exportBookmarksToClipboard);
document.body.addEventListener("copy", handleBookmarksCopyToClipboard);
document.body.addEventListener("paste", handleBookmarksPasteFromClipboard);
// set default actions
// importDefaultBtn is already handled by being a label for the file input
exportDefaultBtn.addEventListener("click", exportBookmarksToJson);
// Listen for theme changes to update badge colors
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'data-bs-theme') {
const isDarkMode = document.documentElement.getAttribute('data-bs-theme') === 'dark';
document.querySelectorAll('.badge').forEach(badge => {
badge.classList.remove('bg-secondary', 'bg-info');
badge.classList.add(isDarkMode ? 'bg-info' : 'bg-secondary');
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.attributeName === "data-bs-theme") {
const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark";
document.querySelectorAll(".badge").forEach((badge) => {
badge.classList.remove("bg-secondary", "bg-info");
badge.classList.add(isDarkMode ? "bg-info" : "bg-secondary");
});
}
});
@ -613,26 +778,26 @@ document.addEventListener('DOMContentLoaded', function() {
observer.observe(document.documentElement, { attributes: true });
// Add visual enhancement to clearly show the top-level/child relationship
document.addEventListener('mouseover', function(e) {
document.addEventListener("mouseover", function (e) {
// When hovering over add buttons, highlight their relationship targets
const button = e.target.closest('.btn-add-child, .btn-add-sibling');
const button = e.target.closest(".btn-add-child, .btn-add-sibling");
if (button) {
if (button.classList.contains('btn-add-child')) {
if (button.classList.contains("btn-add-child")) {
// Highlight parent-child relationship
const bookmarkItem = button.closest('.bookmark-item');
const bookmarkItem = button.closest(".bookmark-item");
if (bookmarkItem) {
bookmarkItem.style.boxShadow = '0 0 0 2px var(--btn-add-child-border, #198754)';
bookmarkItem.style.boxShadow = "0 0 0 2px var(--btn-add-child-border, #198754)";
}
} else if (button.classList.contains('btn-add-sibling')) {
} else if (button.classList.contains("btn-add-sibling")) {
// Highlight sibling relationship
const bookmarkItem = button.closest('.bookmark-item');
const bookmarkItem = button.closest(".bookmark-item");
if (bookmarkItem) {
// Find siblings
const parent = bookmarkItem.parentElement;
const siblings = parent.querySelectorAll(':scope > .bookmark-item');
siblings.forEach(sibling => {
const siblings = parent.querySelectorAll(":scope > .bookmark-item");
siblings.forEach((sibling) => {
if (sibling !== bookmarkItem) {
sibling.style.boxShadow = '0 0 0 2px var(--btn-add-sibling-border, #0d6efd)';
sibling.style.boxShadow = "0 0 0 2px var(--btn-add-sibling-border, #0d6efd)";
}
});
}
@ -640,13 +805,13 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
document.addEventListener('mouseout', function(e) {
document.addEventListener("mouseout", function (e) {
// Remove highlights when not hovering
const button = e.target.closest('.btn-add-child, .btn-add-sibling');
const button = e.target.closest(".btn-add-child, .btn-add-sibling");
if (button) {
// Remove all highlights
document.querySelectorAll('.bookmark-item').forEach(item => {
item.style.boxShadow = '';
document.querySelectorAll(".bookmark-item").forEach((item) => {
item.style.boxShadow = "";
});
}
});

View File

@ -4,6 +4,21 @@
<head>
<th:block th:insert="~{fragments/common :: head(title=#{pdfToImage.title}, header=#{pdfToImage.header})}"></th:block>
<script th:inline="javascript">
document.addEventListener("DOMContentLoaded", function () {
const maxDPI = /*[[${maxDPI}]]*/ 500; // Maximum DPI for PDF to image conversion
const maxDPILabelRaw = /*[[#{pdfToImage.dpi}]]*/ "DPI (The server limit is {0} dpi)";
const maxDPILabel = maxDPILabelRaw.replace("{0}", maxDPI); // Replace with actual value from properties
const dpilabel = document.querySelector('label[for="dpi"]');
if (dpilabel) {
dpilabel.textContent = maxDPILabel;
}
const maxDPIInput = document.getElementById("dpi");
if (maxDPIInput) {
maxDPIInput.setAttribute("max", maxDPI); // Set the maximum value for DPI input
}
});
</script>
</head>
<body>

View File

@ -1,75 +1,169 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
<html th:lang="${#locale.language}"
th:dir="#{language.direction}"
th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<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>
<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>
<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="error-message-container">
<!-- Error messages will be added here dynamically -->
</div>
<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 class="d-flex flex-wrap justify-content-end gap-2">
<!-- Import Split Button -->
<div class="btn-group">
<label
id="importDefaultBtn"
for="importUploadJsonFileInput"
class="btn btn-outline-primary"
style="border-top-left-radius: 1.25rem !important; border-bottom-left-radius: 1.25rem !important;"
th:text="#{editTableOfContents.importBookmarksDefault}">
</label>
<button type="button"
class="btn btn-outline-primary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-expanded="false">
<span class="visually-hidden">Toggle Import Options</span>
</button>
<ul class="dropdown-menu">
<li><label class="dropdown-item"
id="importUploadJsonFileBtn"
for="importUploadJsonFileInput"
style="cursor: pointer;"
th:text="#{editTableOfContents.importBookmarksFromJsonFile}"></label></li>
<li class="d-none"><a class="dropdown-item"
href="#bookmarks-container"
id="importPasteFromClipboardBtn"
th:text="#{editTableOfContents.importBookmarksFromClipboard}"></a></li>
</ul>
</div>
<!-- Export Split Button -->
<div class="btn-group">
<button type="button"
id="exportDefaultBtn"
class="btn btn-outline-primary"
th:text="#{editTableOfContents.exportBookmarksDefault}"></button>
<button type="button"
class="btn btn-outline-primary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-expanded="false">
<span class="visually-hidden">Toggle Export Options</span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item"
href="#bookmarks-container"
id="exportDownloadJsonFileBtn"
th:text="#{editTableOfContents.exportBookmarksAsJson}"></a></li>
<li class="d-none"><a class="dropdown-item"
href="#bookmarks-container"
id="exportCopyToClipboardBtn"
th:text="#{editTableOfContents.exportBookmarksAsText}"></a></li>
</ul>
</div>
</div>
</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>
<!-- Hidden file input for JSON import (outside of form)-->
<input type="file"
id="importUploadJsonFileInput"
accept="application/json"
style="display: none;">
</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>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script th:src="@{'/js/pages/edit-table-of-contents.js'}"></script>
<script>
<script th:src="@{'/js/pages/edit-table-of-contents.js'}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Bootstrap tooltips
if (typeof $ !== 'undefined') {
@ -77,6 +171,6 @@
}
});
</script>
</body>
</body>
</html>

View File

@ -413,9 +413,6 @@
</div>
</div>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<script th:inline="javascript">
/*<![CDATA[*/

View File

@ -208,7 +208,6 @@
color: gold;
}
</style>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<script th:inline="javascript">
/*<![CDATA[*/
window.analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;

View File

@ -98,7 +98,6 @@
</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() {

View File

@ -39,7 +39,6 @@
<!-- 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();

View File

@ -192,7 +192,6 @@
</div>
</div>
</div>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<script th:src="@{'/js/pipeline.js'}"></script>
</div>
</div>

View File

@ -106,7 +106,6 @@
<!-- 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";

View File

@ -30,6 +30,7 @@ ext {
openSamlVersion = "4.3.2"
commonmarkVersion = "0.25.1"
googleJavaFormatVersion = "1.28.0"
junitPlatformVersion = "1.12.2"
tempJrePath = null
}
@ -65,28 +66,11 @@ allprojects {
}
}
tasks.register('writeVersion') {
def propsFile = file("$projectDir/app/common/src/main/resources/version.properties")
def propsDir = propsFile.parentFile
doLast {
if (propsDir.exists()) {
if (propsFile.exists()) {
println "File exists: $propsFile"
} else {
println "$propsFile does not exist. Creating file."
propsFile.createNewFile()
}
} else {
println "Creating directory: $propsDir"
propsDir.mkdirs()
propsFile.createNewFile()
}
def props = new Properties()
props.setProperty("version", version)
props.store(propsFile.newWriter(), null)
}
tasks.register('writeVersion', WriteProperties) {
outputFile = layout.projectDirectory.file('app/common/src/main/resources/version.properties')
println "Writing version.properties to ${outputFile.path}"
comment "${new Date()}"
property 'version', project.provider { project.version.toString() }
}
tasks.named('createExe') {
@ -99,6 +83,7 @@ subprojects {
apply plugin: 'com.diffplug.spotless'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'jacoco'
java {
// 17 is lowest but we support and recommend 21
@ -142,7 +127,7 @@ subprojects {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2'
testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion"
}
tasks.withType(JavaCompile).configureEach {
@ -156,6 +141,16 @@ subprojects {
test {
useJUnitPlatform()
finalizedBy jacocoTestReport
}
jacocoTestReport {
dependsOn test
reports {
xml.required.set(true)
csv.required.set(false)
html.required.set(true)
}
}
tasks.named("processResources") {
@ -573,7 +568,7 @@ dependencies {
}
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2'
testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion"
}
tasks.named("test") {

View File

@ -295,6 +295,7 @@ Stirling-PDF can be customized through environment variables or a `settings.yml`
- Security settings
- UI customization
- Endpoint management
- Maximum DPI for PDF to image conversion (`system.maxDPI`)
When using Docker, pass environment variables using the `-e` flag or in your `docker-compose.yml` file.

View File

@ -2,6 +2,7 @@
org.gradle.parallel=true
# Enables build caching to reuse outputs from previous builds for faster execution
# org.gradle.caching=true
org.gradle.caching=true
org.gradle.build-scan=true
# org.gradle.configuration-cache=true