Merge branch 'main' into patch-97

This commit is contained in:
albanobattistella 2025-08-08 23:04:12 +02:00 committed by GitHub
commit 0e353e09e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 3697 additions and 1714 deletions

2
.github/labels.yml vendored
View File

@ -182,4 +182,4 @@
description: "Pull request has been deployed to a test environment"
- name: "codex"
color: "ededed"
description: "chatgpt AI generated code"
description: "chatgpt AI generated code"

View File

@ -116,47 +116,47 @@ Stirling-PDF currently supports 40 languages!
| Language | Progress |
| -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![63%](https://geps.dev/progress/63) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![63%](https://geps.dev/progress/63) |
| Basque (Euskara) (eu_ES) | ![37%](https://geps.dev/progress/37) |
| Bulgarian (Български) (bg_BG) | ![70%](https://geps.dev/progress/70) |
| Catalan (Català) (ca_CA) | ![69%](https://geps.dev/progress/69) |
| Croatian (Hrvatski) (hr_HR) | ![62%](https://geps.dev/progress/62) |
| Czech (Česky) (cs_CZ) | ![71%](https://geps.dev/progress/71) |
| Danish (Dansk) (da_DK) | ![63%](https://geps.dev/progress/63) |
| Dutch (Nederlands) (nl_NL) | ![61%](https://geps.dev/progress/61) |
| Arabic (العربية) (ar_AR) | ![61%](https://geps.dev/progress/61) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![62%](https://geps.dev/progress/62) |
| Basque (Euskara) (eu_ES) | ![36%](https://geps.dev/progress/36) |
| Bulgarian (Български) (bg_BG) | ![68%](https://geps.dev/progress/68) |
| Catalan (Català) (ca_CA) | ![68%](https://geps.dev/progress/68) |
| Croatian (Hrvatski) (hr_HR) | ![60%](https://geps.dev/progress/60) |
| Czech (Česky) (cs_CZ) | ![70%](https://geps.dev/progress/70) |
| Danish (Dansk) (da_DK) | ![61%](https://geps.dev/progress/61) |
| Dutch (Nederlands) (nl_NL) | ![60%](https://geps.dev/progress/60) |
| 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) | ![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) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![63%](https://geps.dev/progress/63) |
| Irish (Gaeilge) (ga_IE) | ![70%](https://geps.dev/progress/70) |
| Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) |
| Japanese (日本語) (ja_JP) | ![95%](https://geps.dev/progress/95) |
| Korean (한국어) (ko_KR) | ![69%](https://geps.dev/progress/69) |
| Norwegian (Norsk) (no_NB) | ![67%](https://geps.dev/progress/67) |
| Persian (فارسی) (fa_IR) | ![66%](https://geps.dev/progress/66) |
| Polish (Polski) (pl_PL) | ![73%](https://geps.dev/progress/73) |
| Portuguese (Português) (pt_PT) | ![70%](https://geps.dev/progress/70) |
| Portuguese Brazilian (Português) (pt_BR) | ![77%](https://geps.dev/progress/77) |
| Romanian (Română) (ro_RO) | ![59%](https://geps.dev/progress/59) |
| Russian (Русский) (ru_RU) | ![90%](https://geps.dev/progress/90) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![97%](https://geps.dev/progress/97) |
| Simplified Chinese (简体中文) (zh_CN) | ![95%](https://geps.dev/progress/95) |
| Slovakian (Slovensky) (sk_SK) | ![53%](https://geps.dev/progress/53) |
| Slovenian (Slovenščina) (sl_SI) | ![73%](https://geps.dev/progress/73) |
| Spanish (Español) (es_ES) | ![75%](https://geps.dev/progress/75) |
| Swedish (Svenska) (sv_SE) | ![67%](https://geps.dev/progress/67) |
| Thai (ไทย) (th_TH) | ![60%](https://geps.dev/progress/60) |
| Tibetan (བོད་ཡིག་) (bo_CN) | ![66%](https://geps.dev/progress/66) |
| Traditional Chinese (繁體中文) (zh_TW) | ![99%](https://geps.dev/progress/99) |
| Turkish (Türkçe) (tr_TR) | ![82%](https://geps.dev/progress/82) |
| Ukrainian (Українська) (uk_UA) | ![72%](https://geps.dev/progress/72) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![58%](https://geps.dev/progress/58) |
| Malayalam (മലയാളം) (ml_IN) | ![75%](https://geps.dev/progress/75) |
| French (Français) (fr_FR) | ![89%](https://geps.dev/progress/89) |
| German (Deutsch) (de_DE) | ![98%](https://geps.dev/progress/98) |
| Greek (Ελληνικά) (el_GR) | ![67%](https://geps.dev/progress/67) |
| Hindi (हिंदी) (hi_IN) | ![67%](https://geps.dev/progress/67) |
| Hungarian (Magyar) (hu_HU) | ![97%](https://geps.dev/progress/97) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![62%](https://geps.dev/progress/62) |
| Irish (Gaeilge) (ga_IE) | ![68%](https://geps.dev/progress/68) |
| Italian (Italiano) (it_IT) | ![96%](https://geps.dev/progress/96) |
| Japanese (日本語) (ja_JP) | ![93%](https://geps.dev/progress/93) |
| Korean (한국어) (ko_KR) | ![67%](https://geps.dev/progress/67) |
| Norwegian (Norsk) (no_NB) | ![66%](https://geps.dev/progress/66) |
| Persian (فارسی) (fa_IR) | ![64%](https://geps.dev/progress/64) |
| Polish (Polski) (pl_PL) | ![72%](https://geps.dev/progress/72) |
| Portuguese (Português) (pt_PT) | ![69%](https://geps.dev/progress/69) |
| Portuguese Brazilian (Português) (pt_BR) | ![76%](https://geps.dev/progress/76) |
| Romanian (Română) (ro_RO) | ![57%](https://geps.dev/progress/57) |
| Russian (Русский) (ru_RU) | ![88%](https://geps.dev/progress/88) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![95%](https://geps.dev/progress/95) |
| Simplified Chinese (简体中文) (zh_CN) | ![93%](https://geps.dev/progress/93) |
| Slovakian (Slovensky) (sk_SK) | ![51%](https://geps.dev/progress/51) |
| Slovenian (Slovenščina) (sl_SI) | ![71%](https://geps.dev/progress/71) |
| Spanish (Español) (es_ES) | ![74%](https://geps.dev/progress/74) |
| Swedish (Svenska) (sv_SE) | ![65%](https://geps.dev/progress/65) |
| Thai (ไทย) (th_TH) | ![59%](https://geps.dev/progress/59) |
| Tibetan (བོད་ཡིག་) (bo_CN) | ![65%](https://geps.dev/progress/65) |
| Traditional Chinese (繁體中文) (zh_TW) | ![97%](https://geps.dev/progress/97) |
| Turkish (Türkçe) (tr_TR) | ![80%](https://geps.dev/progress/80) |
| Ukrainian (Українська) (uk_UA) | ![71%](https://geps.dev/progress/71) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![57%](https://geps.dev/progress/57) |
| Malayalam (മലയാളം) (ml_IN) | ![73%](https://geps.dev/progress/73) |
## Stirling PDF Enterprise

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

@ -5,8 +5,11 @@ import java.awt.image.*;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Iterator;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import org.springframework.web.multipart.MultipartFile;
@ -115,7 +118,36 @@ public class ImageProcessingUtils {
public static BufferedImage loadImageWithExifOrientation(MultipartFile file)
throws IOException {
BufferedImage image = ImageIO.read(file.getInputStream());
BufferedImage image = null;
String filename = file.getOriginalFilename();
if (filename != null && filename.toLowerCase().endsWith(".psd")) {
// For PSD files, try explicit ImageReader
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("PSD");
if (readers.hasNext()) {
ImageReader reader = readers.next();
try (ImageInputStream iis = ImageIO.createImageInputStream(file.getInputStream())) {
reader.setInput(iis);
image = reader.read(0);
} finally {
reader.dispose();
}
}
if (image == null) {
throw new IOException(
"Unable to read image from file: "
+ filename
+ ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP compression");
}
} else {
// For non-PSD files, use standard ImageIO
image = ImageIO.read(file.getInputStream());
}
if (image == null) {
throw new IOException("Unable to read image from file: " + filename);
}
double orientation = extractImageOrientation(file.getInputStream());
return applyOrientation(image, orientation);
}

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

@ -91,7 +91,7 @@ dependencies {
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion"

View File

@ -366,6 +366,38 @@ navbar.sections.popular=المفضل
settings.title=الإعدادات
settings.update=التحديث متاح
settings.updateAvailable={0} هو الإصدار المثبت حاليًا. إصدار جديد ({1}) متاح.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=إصدار التطبيق:
settings.downloadOption.title=تحديد خيار التنزيل (للتنزيلات ذات الملف الواحد غير المضغوط):
settings.downloadOption.1=فتح في نفس النافذة

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Populyar
settings.title=Parametrlər
settings.update=Yeniləmə mövcuddur
settings.updateAvailable={0} cari quraşdırılmış versiyadır. Yeni ({1}) versiyası mövcuddur.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Proqram Versiyası:
settings.downloadOption.title=Yükləmə versiyasını seçin (Tək fayllı zip olmayan yükləmələr üçün):
settings.downloadOption.1=Eyni pəncərədə açın

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Популярни
settings.title=Настройки
settings.update=Налична актуализация
settings.updateAvailable={0} е текущата инсталирана версия. Налична е нова версия ({1}).
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Версия на приложението:
settings.downloadOption.title=Изберете опция за изтегляне (за изтегляния на един файл без да е архивиран):
settings.downloadOption.1=Отваряне в същия прозорец

View File

@ -366,6 +366,38 @@ navbar.sections.popular=སྤྱི་མོས།
settings.title=སྒྲིག་འགོད།
settings.update=གསར་སྒྱུར་ཡོད།
settings.updateAvailable={0} ནི་ད་ལྟ་སྒྲིག་འཇུག་བྱས་པའི་པར་གཞི་ཡིན། པར་གཞི་གསར་པ་ ({1}) ཡོད།
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=མཉེན་ཆས་པར་གཞི།
settings.downloadOption.title=ཕབ་ལེན་གདམ་ག་འདེམས་རོགས། (ཡིག་ཆ་རྐྱང་པ་ zip མིན་པའི་ཕབ་ལེན་ཆེད།)
settings.downloadOption.1=སྒེའུ་ཁུང་གཅིག་པའི་ནང་ཁ་ཕྱེ།

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Popular
settings.title=Opcions
settings.update=Actualització Disponible
settings.updateAvailable=La versió actual instal·lada és {0}. Una nova versió ({1}) està disponible.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Versió de l'App:
settings.downloadOption.title=Trieu l'opció de descàrrega (per a descàrregues d'un sol fitxer no comprimit):
settings.downloadOption.1=Obre en la mateixa finestra

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Oblíbené
settings.title=Nastavení
settings.update=K dispozici je aktualizace
settings.updateAvailable={0} je aktuálně nainstalovaná verze. Je k dispozici nová verze ({1}).
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Verze aplikace:
settings.downloadOption.title=Vyberte možnost stahování (Pro stahování jednoho souboru bez zipu):
settings.downloadOption.1=Otevřít ve stejném okně

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Populære
settings.title=Indstillinger
settings.update=Opdatering tilgængelig
settings.updateAvailable={0} er den aktuelt installerede version. En ny version ({1}) er tilgængelig.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=App Version:
settings.downloadOption.title=Vælg download mulighed (For enkelt fil ikke-zip downloads):
settings.downloadOption.1=Åbn i samme vindue

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Beliebt
settings.title=Einstellungen
settings.update=Update verfügbar
settings.updateAvailable={0} ist die aktuelle installierte Version. Eine neue Version ({1}) ist verfügbar.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=App-Version:
settings.downloadOption.title=Download-Option wählen (für einzelne Dateien, die keine Zip-Downloads sind):
settings.downloadOption.1=Im selben Fenster öffnen

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Δημοφιλή
settings.title=Ρυθμίσεις
settings.update=Διαθέσιμη ενημέρωση
settings.updateAvailable={0} είναι η τρέχουσα εγκατεστημένη έκδοση. Μια νέα έκδοση ({1}) είναι διαθέσιμη.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Έκδοση εφαρμογής:
settings.downloadOption.title=Επιλογή λήψης (Για μεμονωμένη λήψη αρχείων χωρίς συμπίεση):
settings.downloadOption.1=Άνοιγμα στο ίδιο παράθυρο

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Popular
settings.title=Settings
settings.update=Update available
settings.updateAvailable={0} is the current installed version. A new version ({1}) is available.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=App Version:
settings.downloadOption.title=Choose download option (For single file non zip downloads):
settings.downloadOption.1=Open in same window
@ -569,12 +601,12 @@ rotate.tags=server side
home.imageToPdf.title=Image to PDF
home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF) to PDF.
imageToPdf.tags=conversion,img,jpg,picture,photo
home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF, PSD) to PDF.
imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop
home.pdfToImage.title=PDF to Image
home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF)
pdfToImage.tags=conversion,img,jpg,picture,photo
home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF, PSD)
pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop
home.pdfOrganiser.title=Organise
home.pdfOrganiser.desc=Remove/Rearrange pages in any order

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Popular
settings.title=Settings
settings.update=Update available
settings.updateAvailable={0} is the current installed version. A new version ({1}) is available.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=App Version:
settings.downloadOption.title=Choose download option (For single file non zip downloads):
settings.downloadOption.1=Open in same window
@ -569,12 +601,12 @@ rotate.tags=server side
home.imageToPdf.title=Image to PDF
home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF) to PDF.
imageToPdf.tags=conversion,img,jpg,picture,photo
home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF, PSD) to PDF.
imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop
home.pdfToImage.title=PDF to Image
home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF)
pdfToImage.tags=conversion,img,jpg,picture,photo
home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF, PSD)
pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop
home.pdfOrganiser.title=Organize
home.pdfOrganiser.desc=Remove/Rearrange pages in any order

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Populares
settings.title=Configuración
settings.update=Actualización disponible
settings.updateAvailable={0} es la versión instalada. Hay disponible una versión nueva ({1}).
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Versión de la aplicación:
settings.downloadOption.title=Elegir la opción de descarga (para descargas de un solo archivo sin ZIP):
settings.downloadOption.1=Abrir en la misma ventana

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Popular
settings.title=Ezarpenak
settings.update=Eguneratze eskuragarria
settings.updateAvailable={0} is the current installed version. A new version ({1}) is available.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Aplikazioaren bertsioa:
settings.downloadOption.title=Hautatu deskargatzeko aukera (fitxategi bakarra deskargatzeko ZIP gabe):
settings.downloadOption.1=Ireki leiho berean
@ -570,11 +602,11 @@ rotate.tags=server side
home.imageToPdf.title=Irudia PDF bihurtu
home.imageToPdf.desc=Irudi bat(PNG, JPEG, GIF)PDF bihurtu
imageToPdf.tags=conversion,img,jpg,picture,photo
imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop
home.pdfToImage.title=PDFa irudi bihurtu
home.pdfToImage.desc=PDF bat irudi (PNG, JPEG, GIF) bihurtu
pdfToImage.tags=conversion,img,jpg,picture,photo
pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop
home.pdfOrganiser.title=Antolatzailea
home.pdfOrganiser.desc=Ezabatu/Berrantolatu orrialdeak edozein ordenatan

View File

@ -366,6 +366,38 @@ navbar.sections.popular=محبوب
settings.title=تنظیمات
settings.update=به‌روزرسانی موجود است
settings.updateAvailable={0} نسخه نصب شده فعلی است. یک نسخه جدید ({1}) موجود است.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=نسخه برنامه:
settings.downloadOption.title=گزینه دانلود را انتخاب کنید (برای دانلود یک فایل غیر فشرده):
settings.downloadOption.1=باز کردن در همان پنجره

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Populaire
settings.title=Paramètres
settings.update=Mise à jour disponible
settings.updateAvailable={0} est la version actuellement installée. Une nouvelle version ({1}) est disponible.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Version de l'application :
settings.downloadOption.title=Choisissez l'option de téléchargement (pour les téléchargements à fichier unique non ZIP) :
settings.downloadOption.1=Ouvrir dans la même fenêtre
@ -569,7 +601,7 @@ rotate.tags=pivoter,server side,rotate
home.imageToPdf.title=Image en PDF
home.imageToPdf.desc=Convertissez une image (PNG, JPEG, GIF) en PDF.
home.imageToPdf.desc=Convertissez une image (PNG, JPEG, GIF, PSD) en PDF.
imageToPdf.tags=pdf,conversion,img,jpg,image,photo
home.pdfToImage.title=PDF en image

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Coitianta
settings.title=Socruithe
settings.update=Nuashonrú ar fáil
settings.updateAvailable=Is é {0} an leagan suiteáilte reatha. Tá leagan nua ({1}) ar fáil.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Leagan Aipe:
settings.downloadOption.title=Roghnaigh rogha íoslódála (Le haghaidh íoslódálacha comhad amháin seachas zip):
settings.downloadOption.1=Oscail sa bhfuinneog chéanna

View File

@ -366,6 +366,38 @@ navbar.sections.popular=लोकप्रिय
settings.title=सेटिंग्स
settings.update=अपडेट उपलब्ध है
settings.updateAvailable={0} वर्तमान स्थापित संस्करण है। एक नया संस्करण ({1}) उपलब्ध है।
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=ऐप संस्करण:
settings.downloadOption.title=डाउनलोड विकल्प चुनें (एकल फ़ाइल गैर-ज़िप डाउनलोड के लिए):
settings.downloadOption.1=उसी विंडो में खोलें

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Popularno
settings.title=Postavke
settings.update=Dostupno ažuriranje
settings.updateAvailable={0} je trenutno instalirana verzija. Dostupna je nova verzija ({1}).
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Verzija aplikacije:
settings.downloadOption.title=Odaberite opciju preuzimanja (Za preuzimanje pojedinačnih datoteka bez zip formata):
settings.downloadOption.1=Otvori u istom prozoru

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Népszerű
settings.title=Beállítások
settings.update=Frissítés elérhető
settings.updateAvailable=A jelenlegi telepített verzió: {0}. Új verzió ({1}) érhető el.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Alkalmazás verziója:
settings.downloadOption.title=Letöltési beállítás (egyetlen fájl, nem tömörített letöltések esetén):
settings.downloadOption.1=Megnyitás ugyanabban az ablakban

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Populer
settings.title=Pengaturan
settings.update=Pembaruan tersedia
settings.updateAvailable={0} adalah versi yang terpasang saat ini. Versi baru ({1}) tersedia.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Versi Aplikasi:
settings.downloadOption.title=Pilih opsi unduhan (Untuk unduhan berkas tunggal non zip):
settings.downloadOption.1=Buka di jendela yang sama

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Popolare
settings.title=Impostazioni
settings.update=Aggiornamento disponibile
settings.updateAvailable={0} è la versione attualmente installata. Una nuova versione ({1}) è disponibile.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Versione App:
settings.downloadOption.title=Scegli opzione di download (Per file singoli non compressi):
settings.downloadOption.1=Apri in questa finestra

View File

@ -366,6 +366,38 @@ navbar.sections.popular=人気
settings.title=設定
settings.update=利用可能なアップデート
settings.updateAvailable=バージョン {0} がインストールされています。 新しいバージョン ({1}) が利用可能です。
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Appバージョン:
settings.downloadOption.title=ダウンロードオプションzip以外の単一ファイル:
settings.downloadOption.1=同じウィンドウで開く
@ -570,11 +602,11 @@ rotate.tags=server side
home.imageToPdf.title=画像をPDFに変換
home.imageToPdf.desc=画像 (PNG, JPEG, GIF) をPDFに変換します。
imageToPdf.tags=conversion,img,jpg,picture,photo
imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop
home.pdfToImage.title=PDFを画像に変換
home.pdfToImage.desc=PDFを画像 (PNG, JPEG, GIF) に変換します。
pdfToImage.tags=conversion,img,jpg,picture,photo
pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop
home.pdfOrganiser.title=整理
home.pdfOrganiser.desc=ページの削除/並べ替えします。

View File

@ -366,6 +366,38 @@ navbar.sections.popular=인기
settings.title=설정
settings.update=업데이트 가능
settings.updateAvailable={0}은(는) 현재 설치된 버전입니다. 새 버전({1})이 사용 가능합니다.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=앱 버전:
settings.downloadOption.title=다운로드 옵션 선택 (단일 파일 비압축 다운로드용):
settings.downloadOption.1=같은 창에서 열기

View File

@ -366,6 +366,38 @@ navbar.sections.popular=ജനപ്രിയം
settings.title=ക്രമീകരണങ്ങൾ
settings.update=അപ്ഡേറ്റ് ലഭ്യമാണ്
settings.updateAvailable={0} നിലവിൽ ഇൻസ്റ്റാൾ ചെയ്ത പതിപ്പാണ്. ഒരു പുതിയ പതിപ്പ് ({1}) ലഭ്യമാണ്.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=ആപ്പ് പതിപ്പ്:
settings.downloadOption.title=ഡൗൺലോഡ് ഓപ്ഷൻ തിരഞ്ഞെടുക്കുക (സിംഗിൾ ഫയൽ നോൺ-സിപ്പ് ഡൗൺലോഡുകൾക്ക്):
settings.downloadOption.1=ഒരേ വിൻഡോയിൽ തുറക്കുക

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Popular
settings.title=Instellingen
settings.update=Update beschikbaar
settings.updateAvailable={0} is de huidig geïnstalleerde versie. Een nieuwe versie ({1}) is beschikbaar.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=App versie:
settings.downloadOption.title=Kies download optie (Voor enkelvoudige bestanddownloads zonder zip):
settings.downloadOption.1=Open in hetzelfde venster

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Populært
settings.title=Innstillinger
settings.update=Oppdatering tilgjengelig
settings.updateAvailable={0} er den nåværende installerte versjonen. En ny versjon ({1}) er tilgjengelig.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=App Versjon:
settings.downloadOption.title=Velg nedlastingsalternativ (For enkeltfil ikke-zip nedlastinger):
settings.downloadOption.1=Åpne i samme vindu

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Popularne
settings.title=Ustawienia
settings.update=Dostępna aktualizacja
settings.updateAvailable=Wersja {0} jest obecenia zainstalowana, dostępna jest nowa wersja ({1}).
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Wersja aplikacji:
settings.downloadOption.title=Wybierz opcję pobierania (w przypadku pobierania pojedynczych plików innych niż ZIP):
settings.downloadOption.1=Otwórz w tym samym oknie

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Populares
settings.title=Configurações
settings.update=Atualização disponível
settings.updateAvailable={0} é a versão atualmente instalada. Uma nova versão ({1}) está disponível.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Versão do Aplicativo:
settings.downloadOption.title=Escolha a opção de download (para download de arquivo único, não compactados):
settings.downloadOption.1=Abrir na mesma janela

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Popular
settings.title=Definições
settings.update=Atualização disponível
settings.updateAvailable={0} é a versão atual instalada. Uma nova versão ({1}) está disponível.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Versão da Aplicação:
settings.downloadOption.title=Escolha a opção de download (Para downloads de ficheiro único não zipado):
settings.downloadOption.1=Abrir na mesma janela

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Popular
settings.title=Setări
settings.update=Actualizare disponibilă
settings.updateAvailable={0} este versiunea instalată curent. O nouă versiune ({1}) este disponibilă.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Versiune aplicație:
settings.downloadOption.title=Alege opțiunea de descărcare (pentru descărcarea unui singur fișier non-zip):
settings.downloadOption.1=Deschide în aceeași fereastră

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Популярное
settings.title=Настройки
settings.update=Доступно обновление
settings.updateAvailable=Текущая установленная версия - {0}. Доступна новая версия ({1}).
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Версия приложения:
settings.downloadOption.title=Выберите вариант загрузки (для одиночных файлов без архивации):
settings.downloadOption.1=Открыть в том же окне

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Popular
settings.title=Nastavenia
settings.update=Dostupná aktualizácia
settings.updateAvailable={0} je aktuálne nainštalovaná verzia. Nová verzia ({1}) je dostupná.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Verzia aplikácie:
settings.downloadOption.title=Vyberte možnosť sťahovania (Pre jednotlivé neskomprimované súbory):
settings.downloadOption.1=Otvoriť v rovnakom okne

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Priljubljeno
settings.title=Nastavitve
settings.update=Na voljo je posodobitev
settings.updateAvailable={0} je trenutno nameščena različica. Na voljo je nova različica ({1}).
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Različica aplikacije:
settings.downloadOption.title=Izberi možnost prenosa (za prenose ene datoteke brez zip):
settings.downloadOption.1=Odpri v istem oknu

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Popularno
settings.title=Podešavanja
settings.update=Dostupno ažuriranje
settings.updateAvailable={0} je trenutno instalirana verzija. Nova verzija ({1}) je dostupna.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Verzija aplikacije:
settings.downloadOption.title=Odaberi opciju preuzimanja (Za preuzimanje pojedinačnih fajlova bez zip formata):
settings.downloadOption.1=Otvori u istom prozoru

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Populära
settings.title=Inställningar
settings.update=Uppdatering tillgänglig
settings.updateAvailable={0} är den aktuella installerade versionen. En ny version ({1}) finns tillgänglig.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Appversion:
settings.downloadOption.title=Välj nedladdningsalternativ (för nedladdning av en fil utan zip):
settings.downloadOption.1=Öppnas i samma fönster

View File

@ -366,6 +366,38 @@ navbar.sections.popular=ยอดนิยม
settings.title=การตั้งค่า
settings.update=มีการอัปเดต
settings.updateAvailable={0} คือเวอร์ชันที่ติดตั้งในปัจจุบัน มีเวอร์ชันใหม่ ({1}) พร้อมให้บริการ
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=เวอร์ชันแอป:
settings.downloadOption.title=เลือกตัวเลือกการดาวน์โหลด (สำหรับการดาวน์โหลดไฟล์เดียวที่ไม่ใช่ zip):
settings.downloadOption.1=เปิดในหน้าต่างเดียวกัน

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Popular
settings.title=Ayarlar
settings.update=Güncelleme mevcut
settings.updateAvailable={0} mevcut kurulu sürümdür. Yeni bir sürüm ({1}) mevcuttur.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Uygulama Sürümü:
settings.downloadOption.title=İndirme seçeneği seçin (Zip olmayan tek dosya indirmeler için):
settings.downloadOption.1=Aynı pencerede aç

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Популярне
settings.title=Налаштування
settings.update=Доступне оновлення
settings.updateAvailable=Зараз встановлена версія {0}. Нова версія ({1}) доступна.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Версія додатку:
settings.downloadOption.title=Виберіть варіант завантаження (для завантаження одного файлу без zip):
settings.downloadOption.1=Відкрити в тому ж вікні

View File

@ -366,6 +366,38 @@ navbar.sections.popular=Popular
settings.title=Cài đặt
settings.update=Có bản cập nhật
settings.updateAvailable={0} là phiên bản hiện tại đã cài đặt. Một phiên bản mới ({1}) đã có sẵn.
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=Phiên bản ứng dụng:
settings.downloadOption.title=Chọn tùy chọn tải xuống (Đối với tải xuống tệp đơn không nén):
settings.downloadOption.1=Mở trong cùng cửa sổ

View File

@ -366,6 +366,38 @@ navbar.sections.popular=热门
settings.title=设置
settings.update=有可用的更新
settings.updateAvailable=当前版本为 {0},新版本 ({1}) 可用。
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=应用程序版本:
settings.downloadOption.title=选择下载选项(单个文件非压缩文件):
settings.downloadOption.1=在同一窗口打开

View File

@ -366,6 +366,38 @@ navbar.sections.popular=熱門功能
settings.title=設定
settings.update=有更新可用
settings.updateAvailable=目前安裝的版本是 {0}。有新版本({1})可供使用。
# Update modal and notification strings
update.urgentUpdateAvailable=🚨 Update Available
update.updateAvailable=Update Available
update.modalTitle=Update Available
update.current=Current
update.latest=Latest
update.latestStable=Latest Stable
update.priority=Priority
update.recommendedAction=Recommended Action
update.breakingChangesDetected=⚠️ Breaking Changes Detected
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
update.migrationGuides=Migration Guides:
update.viewGuide=View Guide
update.loadingDetailedInfo=Loading detailed version information...
update.close=Close
update.viewAllReleases=View All Releases
update.downloadLatest=Download Latest
update.availableUpdates=Available Updates:
update.unableToLoadDetails=Unable to load detailed version information.
update.version=Version
# Update priority levels
update.priority.urgent=URGENT
update.priority.normal=NORMAL
update.priority.minor=MINOR
update.priority.low=LOW
# Breaking changes text
update.breakingChanges=Breaking Changes:
update.breakingChangesDefault=This version contains breaking changes
update.migrationGuide=Migration Guide
settings.appVersion=應用程式版本:
settings.downloadOption.title=選擇下載選項(適用於單一檔案非壓縮下載):
settings.downloadOption.1=在同一視窗中開啟

View File

@ -336,6 +336,12 @@
"moduleLicense": "The BSD License",
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
},
{
"moduleName": "com.twelvemonkeys.imageio:imageio-psd",
"moduleVersion": "3.12.0",
"moduleLicense": "The BSD License",
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
},
{
"moduleName": "com.twelvemonkeys.imageio:imageio-tiff",
"moduleVersion": "3.12.0",
@ -623,21 +629,21 @@
{
"moduleName": "io.swagger.core.v3:swagger-annotations-jakarta",
"moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-annotations",
"moduleVersion": "2.2.34",
"moduleVersion": "2.2.35",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "io.swagger.core.v3:swagger-core-jakarta",
"moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-core",
"moduleVersion": "2.2.34",
"moduleVersion": "2.2.35",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "io.swagger.core.v3:swagger-models-jakarta",
"moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-models",
"moduleVersion": "2.2.34",
"moduleVersion": "2.2.35",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
@ -980,13 +986,13 @@
},
{
"moduleName": "org.commonmark:commonmark",
"moduleVersion": "0.25.0",
"moduleVersion": "0.25.1",
"moduleLicense": "BSD-2-Clause",
"moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause"
},
{
"moduleName": "org.commonmark:commonmark-ext-gfm-tables",
"moduleVersion": "0.25.0",
"moduleVersion": "0.25.1",
"moduleLicense": "BSD-2-Clause",
"moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause"
},

View File

@ -16,21 +16,96 @@ function compareVersions(version1, version2) {
return 0;
}
async function getLatestReleaseVersion() {
const url = "https://api.github.com/repos/Stirling-Tools/Stirling-PDF/releases/latest";
function getDownloadUrl() {
// Only show download for non-Docker installations
if (machineType === 'Docker' || machineType === 'Kubernetes') {
return null;
}
const baseUrl = 'https://files.stirlingpdf.com/';
// Determine file based on machine type and security
if (machineType === 'Server-jar') {
return baseUrl + (activeSecurity ? 'Stirling-PDF-with-login.jar' : 'Stirling-PDF.jar');
}
// Client installations
if (machineType.startsWith('Client-')) {
const os = machineType.replace('Client-', ''); // win, mac, unix
const type = activeSecurity ? '-server-security' : '-server';
if (os === 'unix') {
return baseUrl + os + type + '.jar';
} else if (os === 'win') {
return baseUrl + os + '-installer.exe';
} else if (os === 'mac') {
return baseUrl + os + '-installer.dmg';
}
}
return null;
}
// Function to get translated priority text
function getTranslatedPriority(priority) {
switch(priority?.toLowerCase()) {
case 'urgent': return updatePriorityUrgent;
case 'normal': return updatePriorityNormal;
case 'minor': return updatePriorityMinor;
case 'low': return updatePriorityLow;
default: return priority?.toUpperCase() || 'NORMAL';
}
}
async function getUpdateSummary() {
// Map Java License enum to API types
let type = 'normal';
if (licenseType === 'PRO') {
type = 'pro';
} else if (licenseType === 'ENTERPRISE') {
type = 'enterprise';
}
const url = `https://supabase.stirling.com/functions/v1/updates?from=${currentVersion}&type=${type}&login=${activeSecurity}&summary=true`;
console.log("Fetching update summary from:", url);
try {
const response = await fetch(url);
console.log("Response status:", response.status);
if (response.status === 200) {
const data = await response.json();
return data.tag_name ? data.tag_name.substring(1) : "";
return data;
} else {
// If the status is not 200, try to get the version from build.gradle
return await getCurrentVersionFromBypass();
console.error("Failed to fetch update summary from Supabase:", response.status);
return null;
}
} catch (error) {
console.error("Failed to fetch latest version from GitHub:", error);
// If an error occurs, try to get the version from build.gradle
return await getCurrentVersionFromBypass();
console.error("Failed to fetch update summary from Supabase:", error);
return null;
}
}
async function getFullUpdateInfo() {
// Map Java License enum to API types
let type = 'normal';
if (licenseType === 'PRO') {
type = 'pro';
} else if (licenseType === 'ENTERPRISE') {
type = 'enterprise';
}
const url = `https://supabase.stirling.com/functions/v1/updates?from=${currentVersion}&type=${type}&login=${activeSecurity}&summary=false`;
console.log("Fetching full update info from:", url);
try {
const response = await fetch(url);
console.log("Full update response status:", response.status);
if (response.status === 200) {
const data = await response.json();
return data;
} else {
console.error("Failed to fetch full update info from Supabase:", response.status);
return null;
}
} catch (error) {
console.error("Failed to fetch full update info from Supabase:", error);
return null;
}
}
@ -60,6 +135,7 @@ async function checkForUpdate() {
var updateLinkLegacy = document.getElementById("update-link-legacy") || null;
if (updateBtn !== null) {
updateBtn.style.display = "none";
updateBtn.classList.remove("btn-danger", "btn-warning", "btn-outline-primary");
}
if (updateLink !== null) {
updateLink.style.display = "none";
@ -71,19 +147,47 @@ async function checkForUpdate() {
}
}
const latestVersion = await getLatestReleaseVersion();
console.log("latestVersion=" + latestVersion);
const updateSummary = await getUpdateSummary();
if (!updateSummary) {
console.log("No update summary available");
return;
}
console.log("updateSummary=", updateSummary);
console.log("currentVersion=" + currentVersion);
console.log("compareVersions(latestVersion, currentVersion) > 0)=" + compareVersions(latestVersion, currentVersion));
if (latestVersion && compareVersions(latestVersion, currentVersion) > 0) {
console.log("latestVersion=" + updateSummary.latest_version);
if (updateSummary.latest_version && compareVersions(updateSummary.latest_version, currentVersion) > 0) {
const priority = updateSummary.max_priority || 'normal';
if (updateBtn != null) {
document.getElementById("update-btn").style.display = "block";
// Style button based on priority
if (priority === 'urgent') {
updateBtn.classList.add("btn-danger");
updateBtn.innerHTML = urgentUpdateAvailable;
} else if (priority === 'normal') {
updateBtn.classList.add("btn-warning");
updateBtn.innerHTML = updateAvailableText;
} else {
updateBtn.classList.add("btn-outline-primary");
updateBtn.innerHTML = updateAvailableText;
}
// Store summary for initial display
updateBtn.setAttribute('data-update-summary', JSON.stringify(updateSummary));
updateBtn.style.display = "block";
// Add click handler for update details modal
updateBtn.onclick = function(e) {
e.preventDefault();
showUpdateModal();
};
}
if (updateLink !== null) {
document.getElementById("update-link").style.display = "flex";
}
if (updateLinkLegacy !== null) {
document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '<b>' + currentVersion + '</b>').replace("{1}", '<b>' + latestVersion + '</b>');
document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '<b>' + currentVersion + '</b>').replace("{1}", '<b>' + updateSummary.latest_version + '</b>');
if (updateLinkLegacy.classList.contains("visually-hidden")) {
updateLinkLegacy.classList.remove("visually-hidden");
}
@ -99,6 +203,188 @@ async function checkForUpdate() {
}
}
async function showUpdateModal() {
// Close settings modal if open
const settingsModal = bootstrap.Modal.getInstance(document.getElementById('settingsModal'));
if (settingsModal) {
settingsModal.hide();
}
// Get summary data from button
const updateBtn = document.getElementById("update-btn");
const summaryData = JSON.parse(updateBtn.getAttribute('data-update-summary'));
// Utility function to escape HTML special characters
function escapeHtml(str) {
if (typeof str !== 'string') return str;
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/\//g, '&#x2F;');
}
// Create initial modal with loading state
const initialModalHtml = `
<div class="modal fade" id="updateModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable" role="document" style="max-height: 80vh;">
<div class="modal-content" style="max-height: 80vh;">
<div class="modal-header">
<h5 class="modal-title" id="updateModalLabel">${updateModalTitle}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="modal-body" id="updateModalBody" style="max-height: 60vh; overflow-y: auto;">
<div class="update-summary mb-4">
<div class="row mb-3">
<div class="${summaryData.latest_stable_version ? 'col-4' : 'col-6'} text-center">
<small class="text-muted">${updateCurrent}</small><br>
<strong>${escapeHtml(currentVersion)}</strong>
</div>
<div class="${summaryData.latest_stable_version ? 'col-4' : 'col-6'} text-center">
<small class="text-muted">${updateLatest}</small><br>
<strong class="text-primary">${escapeHtml(summaryData.latest_version)}</strong>
</div>
${summaryData.latest_stable_version ? `
<div class="col-4 text-center">
<small class="text-muted">${updateLatestStable}</small><br>
<strong class="text-success">${escapeHtml(summaryData.latest_stable_version)}</strong>
</div>
` : ''}
</div>
<div class="alert ${summaryData.max_priority === 'urgent' ? 'alert-danger' : 'alert-warning'}" role="alert">
<strong>${updatePriority}:</strong> ${getTranslatedPriority(summaryData.max_priority)}
${summaryData.recommended_action ? `<br><strong>${updateRecommendedAction}:</strong> ${escapeHtml(summaryData.recommended_action)}` : ''}
</div>
</div>
${summaryData.any_breaking ? `
<div class="alert alert-warning" role="alert">
<h6><strong>${updateBreakingChangesDetected}</strong></h6>
<p>${updateBreakingChangesMessage}</p>
</div>
` : ''}
${summaryData.migration_guides && summaryData.migration_guides.length > 0 ? `
<div class="migration-guides mb-4">
<h6>${updateMigrationGuides}</h6>
<ul class="list-group">
${summaryData.migration_guides.map(guide => `
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<strong>${updateVersion} ${escapeHtml(guide.version)}:</strong> ${escapeHtml(guide.notes)}
</div>
<a href="${escapeHtml(guide.url)}" target="_blank" class="btn btn-sm btn-outline-primary">${updateViewGuide}</a>
</li>
`).join('')}
</ul>
</div>
` : ''}
<div class="text-center">
<div class="spinner-border text-primary" role="status" id="loadingSpinner">
<span class="visually-hidden">${updateLoadingDetailedInfo}</span>
</div>
<p class="mt-2">${updateLoadingDetailedInfo}</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${updateClose}</button>
<a href="https://github.com/Stirling-Tools/Stirling-PDF/releases" target="_blank" class="btn btn-outline-primary">${updateViewAllReleases}</a>
${getDownloadUrl() ? `<a href="${escapeHtml(getDownloadUrl())}" class="btn btn-success" target="_blank">${updateDownloadLatest}</a>` : ''}
</div>
</div>
</div>
</div>
`;
// Remove existing modal if present
const existingModal = document.getElementById('updateModal');
if (existingModal) {
existingModal.remove();
}
// Add modal to body
document.body.insertAdjacentHTML('beforeend', initialModalHtml);
// Show modal
const modal = new bootstrap.Modal(document.getElementById('updateModal'));
modal.show();
// Fetch full update info
const fullUpdateInfo = await getFullUpdateInfo();
// Update modal with full information
const modalBody = document.getElementById('updateModalBody');
if (fullUpdateInfo && fullUpdateInfo.new_versions) {
const storedMode = localStorage.getItem("dark-mode");
const isDarkMode = storedMode === "on" ||
(storedMode === null && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
const darkClasses = isDarkMode ? {
accordionItem: 'bg-dark border-secondary text-light',
accordionButton: 'bg-dark text-light border-secondary',
accordionBody: 'bg-dark text-light'
} : {
accordionItem: '',
accordionButton: '',
accordionBody: ''
};
const detailedVersionsHtml = `
<div class="detailed-versions mt-4">
<h6>${updateAvailableUpdates}</h6>
<div class="accordion" id="versionsAccordion">
${fullUpdateInfo.new_versions.map((version, index) => `
<div class="accordion-item" style="border-color: var(--md-sys-color-outline);">
<h2 class="accordion-header" id="heading${index}">
<button class="accordion-button ${index === 0 ? '' : 'collapsed'}" style="color: var(--md-sys-color-on-surface); background-color:
var(--md-sys-color-surface);" type="button" data-bs-toggle="collapse"
data-bs-target="#collapse${index}" aria-expanded="${index === 0 ? 'true' : 'false'}" aria-controls="collapse${index}">
<div class="d-flex justify-content-between w-100 me-3">
<span><strong>${updateVersion} ${version.version}</strong></span>
<span class="badge ${version.priority === 'urgent' ? 'bg-danger' : version.priority === 'normal' ? 'bg-warning' : 'bg-secondary'}">${getTranslatedPriority(version.priority)}</span>
</div>
</button>
</h2>
<div id="collapse${index}" class="accordion-collapse collapse ${index === 0 ? 'show' : ''}"
aria-labelledby="heading${index}" data-bs-parent="#versionsAccordion">
<div class="accordion-body" style="color: var(--md-sys-color-on-surface); background-color:
var(--md-sys-color-surface-bright);">
<h6>${version.announcement.title}</h6>
<p>${version.announcement.message}</p>
${version.compatibility.breaking_changes ? `
<div class="alert alert-warning alert-sm" role="alert">
<small><strong> ${updateBreakingChanges}</strong> ${version.compatibility.breaking_description || updateBreakingChangesDefault}</small>
${version.compatibility.migration_guide_url ? `<br><a href="${version.compatibility.migration_guide_url}" target="_blank" class="btn btn-sm btn-outline-warning mt-1">${updateMigrationGuide}</a>` : ''}
</div>
` : ''}
</div>
</div>
</div>
`).join('')}
</div>
</div>
`;
// Remove loading spinner and add detailed info
const spinner = document.getElementById('loadingSpinner');
if (spinner) {
spinner.parentElement.remove();
}
modalBody.insertAdjacentHTML('beforeend', detailedVersionsHtml);
} else {
// Remove loading spinner if failed to load
const spinner = document.getElementById('loadingSpinner');
if (spinner) {
spinner.parentElement.innerHTML = `<p class="text-muted">${updateUnableToLoadDetails}</p>`;
}
}
}
document.addEventListener("DOMContentLoaded", (event) => {
checkForUpdate();
});

View File

@ -22,7 +22,7 @@
<form id="imageToPDFForm" method="post" enctype="multipart/form-data"
th:action="@{'/api/v1/convert/img/pdf'}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='image/*', inputText=#{imgPrompt})}">
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='image/*,.psd', inputText=#{imgPrompt})}">
</div>
<div class="mb-3">
<label for="fitOption" th:text="#{imageToPDF.selectLabel}">Fit Options</label>

View File

@ -11,9 +11,44 @@
</script>
<script th:inline="javascript">
const currentVersion = /*[[${@appVersion}]]*/ '';
const licenseType = /*[[${@license}]]*/ '';
const machineType = /*[[${@machineType}]]*/ '';
const activeSecurity = /*[[${@activeSecurity}]]*/ false;
const noFavourites = /*[[#{noFavourites}]]*/ '';
console.log(noFavourites);
const updateAvailable = /*[[#{settings.updateAvailable}]]*/ '';
// Update notification i18n constants
const urgentUpdateAvailable = /*[[#{update.urgentUpdateAvailable}]]*/ '🚨 Update Available';
const updateAvailableText = /*[[#{update.updateAvailable}]]*/ 'Update Available';
const updateModalTitle = /*[[#{update.modalTitle}]]*/ 'Update Available';
const updateCurrent = /*[[#{update.current}]]*/ 'Current';
const updateLatest = /*[[#{update.latest}]]*/ 'Latest';
const updateLatestStable = /*[[#{update.latestStable}]]*/ 'Latest Stable';
const updatePriority = /*[[#{update.priority}]]*/ 'Priority';
const updateRecommendedAction = /*[[#{update.recommendedAction}]]*/ 'Recommended Action';
const updateBreakingChangesDetected = /*[[#{update.breakingChangesDetected}]]*/ '⚠️ Breaking Changes Detected';
const updateBreakingChangesMessage = /*[[#{update.breakingChangesMessage}]]*/ 'This update contains breaking changes. Please review the migration guides below.';
const updateMigrationGuides = /*[[#{update.migrationGuides}]]*/ 'Migration Guides:';
const updateViewGuide = /*[[#{update.viewGuide}]]*/ 'View Guide';
const updateLoadingDetailedInfo = /*[[#{update.loadingDetailedInfo}]]*/ 'Loading detailed version information...';
const updateClose = /*[[#{update.close}]]*/ 'Close';
const updateViewAllReleases = /*[[#{update.viewAllReleases}]]*/ 'View All Releases';
const updateDownloadLatest = /*[[#{update.downloadLatest}]]*/ 'Download Latest';
const updateAvailableUpdates = /*[[#{update.availableUpdates}]]*/ 'Available Updates:';
const updateUnableToLoadDetails = /*[[#{update.unableToLoadDetails}]]*/ 'Unable to load detailed version information.';
const updateVersion = /*[[#{update.version}]]*/ 'Version';
// Update priority levels
const updatePriorityUrgent = /*[[#{update.priority.urgent}]]*/ 'URGENT';
const updatePriorityNormal = /*[[#{update.priority.normal}]]*/ 'NORMAL';
const updatePriorityMinor = /*[[#{update.priority.minor}]]*/ 'MINOR';
const updatePriorityLow = /*[[#{update.priority.low}]]*/ 'LOW';
// Breaking changes text
const updateBreakingChanges = /*[[#{update.breakingChanges}]]*/ 'Breaking Changes:';
const updateBreakingChangesDefault = /*[[#{update.breakingChangesDefault}]]*/ 'This version contains breaking changes';
const updateMigrationGuide = /*[[#{update.migrationGuide}]]*/ 'Migration Guide';
</script>
<script th:src="@{'/js/homecard.js'}"></script>
<script th:src="@{'/js/githubVersion.js'}"></script>

View File

@ -58,7 +58,7 @@ repositories {
allprojects {
group = 'stirling.software'
version = '1.1.1'
version = '1.2.0'
configurations.configureEach {
exclude group: 'commons-logging', module: 'commons-logging'