Compare commits

..

11 Commits

Author SHA1 Message Date
Anthony Stirling
c2062c19ab Merge remote-tracking branch 'origin/main' into bug/remember-me 2024-11-15 21:30:14 +00:00
Anthony Stirling
6760fddd6f Merge branch 'bug/remember-me' of
git@github.com:Stirling-Tools/Stirling-PDF.git into bug/remember-me
2024-11-15 21:26:51 +00:00
a
20a75d577d Merge branch 'bug/remember-me' of git@github.com:Stirling-Tools/Stirling-PDF.git into bug/remember-me 2024-11-15 13:13:34 +00:00
Anthony Stirling
559581c59d Merge branch 'bug/remember-me' of
git@github.com:Stirling-Tools/Stirling-PDF.git into bug/remember-me
2024-11-15 13:12:20 +00:00
Anthony Stirling
e7356a1d38 Merge branch 'main' into bug/remember-me 2024-11-07 20:39:19 +00:00
a
1405e4f5ee Merge branch 'bug/remember-me' of git@github.com:Stirling-Tools/Stirling-PDF.git into bug/remember-me 2024-11-07 20:38:54 +00:00
Anthony Stirling
322e7dee0d exe no longer disable CLI 2024-11-07 20:38:32 +00:00
github-actions[bot]
918e977c6a Update translation files (#2185)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-05 14:26:24 +00:00
Anthony Stirling
a31633e5b8 Merge branch 'main' into bug/remember-me 2024-11-05 14:24:44 +00:00
Anthony Stirling
ed551cec91 remove uselss comment 2024-11-05 14:24:28 +00:00
Anthony Stirling
3f14e77725 fix remmeber me 2024-11-05 14:22:55 +00:00
50 changed files with 369 additions and 250 deletions

View File

@@ -67,7 +67,6 @@ jobs:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
@@ -96,7 +95,6 @@ jobs:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
@@ -124,7 +122,6 @@ jobs:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }}

View File

@@ -190,29 +190,29 @@ Stirling-PDF currently supports 36 languages!
| Language | Progress |
| -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![97%](https://geps.dev/progress/97) |
| Arabic (العربية) (ar_AR) | ![98%](https://geps.dev/progress/98) |
| Basque (Euskara) (eu_ES) | ![55%](https://geps.dev/progress/55) |
| Bulgarian (Български) (bg_BG) | ![96%](https://geps.dev/progress/96) |
| Bulgarian (Български) (bg_BG) | ![97%](https://geps.dev/progress/97) |
| Catalan (Català) (ca_CA) | ![90%](https://geps.dev/progress/90) |
| Croatian (Hrvatski) (hr_HR) | ![98%](https://geps.dev/progress/98) |
| Czech (Česky) (cs_CZ) | ![97%](https://geps.dev/progress/97) |
| Danish (Dansk) (da_DK) | ![96%](https://geps.dev/progress/96) |
| Czech (Česky) (cs_CZ) | ![98%](https://geps.dev/progress/98) |
| Danish (Dansk) (da_DK) | ![97%](https://geps.dev/progress/97) |
| Dutch (Nederlands) (nl_NL) | ![96%](https://geps.dev/progress/96) |
| 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) | ![97%](https://geps.dev/progress/97) |
| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) |
| Greek (Ελληνικά) (el_GR) | ![97%](https://geps.dev/progress/97) |
| German (Deutsch) (de_DE) | ![98%](https://geps.dev/progress/98) |
| Greek (Ελληνικά) (el_GR) | ![98%](https://geps.dev/progress/98) |
| Hindi (हिंदी) (hi_IN) | ![95%](https://geps.dev/progress/95) |
| Hungarian (Magyar) (hu_HU) | ![98%](https://geps.dev/progress/98) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![97%](https://geps.dev/progress/97) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![98%](https://geps.dev/progress/98) |
| Irish (Gaeilge) (ga_IE) | ![88%](https://geps.dev/progress/88) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Japanese (日本語) (ja_JP) | ![85%](https://geps.dev/progress/85) |
| Korean (한국어) (ko_KR) | ![95%](https://geps.dev/progress/95) |
| Norwegian (Norsk) (no_NB) | ![87%](https://geps.dev/progress/87) |
| Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) |
| Japanese (日本語) (ja_JP) | ![86%](https://geps.dev/progress/86) |
| Korean (한국어) (ko_KR) | ![96%](https://geps.dev/progress/96) |
| Norwegian (Norsk) (no_NB) | ![88%](https://geps.dev/progress/88) |
| Polish (Polski) (pl_PL) | ![97%](https://geps.dev/progress/97) |
| Portuguese (Português) (pt_PT) | ![97%](https://geps.dev/progress/97) |
| Portuguese (Português) (pt_PT) | ![98%](https://geps.dev/progress/98) |
| Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) |
| Romanian (Română) (ro_RO) | ![90%](https://geps.dev/progress/90) |
| Russian (Русский) (ru_RU) | ![97%](https://geps.dev/progress/97) |
@@ -225,7 +225,7 @@ Stirling-PDF currently supports 36 languages!
| Traditional Chinese (繁體中文) (zh_TW) | ![98%](https://geps.dev/progress/98) |
| Turkish (Türkçe) (tr_TR) | ![92%](https://geps.dev/progress/92) |
| Ukrainian (Українська) (uk_UA) | ![80%](https://geps.dev/progress/80) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![88%](https://geps.dev/progress/88) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![89%](https://geps.dev/progress/89) |
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)

View File

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

View File

@@ -32,12 +32,12 @@ ignore = [
ignore = [
'AddStampRequest.alphabet',
'AddStampRequest.position',
'home.pipeline.title'
'PDFToBook.selectText.1',
'PDFToText.tags',
'addPageNumbers.selectText.3',
'alphabet',
'certSign.name',
'home.pipeline.title',
'language.direction',
'licenses.version',
'pipeline.title',
@@ -46,6 +46,7 @@ ignore = [
'sponsor',
'text',
'watermark.type.1',
'certSign.name',
]
[el_GR]

View File

@@ -1,18 +1,25 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Scanner;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CertificateUtils {
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
@@ -39,4 +46,84 @@ public class CertificateUtils {
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
}
}
public static X509Certificate getIdPCertificate(Resource certificateResource) throws Exception {
if (certificateResource instanceof UrlResource) {
return extractCertificateFromMetadata(certificateResource);
} else {
// Treat as file resource
return readCertificate(certificateResource);
}
}
private static X509Certificate extractCertificateFromMetadata(Resource metadataResource) throws Exception {
log.info("Attempting to extract certificate from metadata resource: {}", metadataResource.getDescription());
try (InputStream is = metadataResource.getInputStream()) {
String content = new String(is.readAllBytes(), StandardCharsets.UTF_8);
log.info("Retrieved metadata content, length: {}", content.length());
// Find the certificate data
int startIndex = content.indexOf("<ds:X509Certificate>");
int endIndex = content.indexOf("</ds:X509Certificate>");
if (startIndex == -1 || endIndex == -1) {
log.error("Certificate tags not found in metadata");
throw new Exception("Certificate tags not found in metadata");
}
// Extract certificate data
String certData = content.substring(
startIndex + "<ds:X509Certificate>".length(),
endIndex
).trim();
log.info("Found certificate data, length: {}", certData.length());
// Remove any whitespace and newlines from cert data
certData = certData.replaceAll("\\s+", "");
// Reconstruct PEM format with proper line breaks
StringBuilder pemBuilder = new StringBuilder();
pemBuilder.append("-----BEGIN CERTIFICATE-----\n");
// Insert line breaks every 64 characters
int lineLength = 64;
for (int i = 0; i < certData.length(); i += lineLength) {
int end = Math.min(i + lineLength, certData.length());
pemBuilder.append(certData, i, end).append('\n');
}
pemBuilder.append("-----END CERTIFICATE-----");
String pemCert = pemBuilder.toString();
log.debug("Reconstructed PEM certificate:\n{}", pemCert);
try {
ByteArrayInputStream pemStream = new ByteArrayInputStream(pemCert.getBytes(StandardCharsets.UTF_8));
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(pemStream);
log.info("Successfully parsed certificate. Subject: {}", cert.getSubjectX500Principal());
// Optional: check validity dates
cert.checkValidity(); // Throws CertificateExpiredException if expired
log.info("Certificate is valid (not expired)");
return cert;
} catch (Exception e) {
log.error("Failed to parse certificate", e);
throw new Exception("Failed to parse X509 certificate from metadata", e);
}
} catch (Exception e) {
log.error("Error processing metadata resource", e);
throw e;
}
}
}

View File

@@ -16,23 +16,35 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
if (exception instanceof Saml2AuthenticationException) {
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
getRedirectStrategy()
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
} else if (exception instanceof ProviderNotFoundException) {
getRedirectStrategy()
.sendRedirect(
request,
response,
"/login?erroroauth=not_authentication_provider_found");
}
log.error("AuthenticationException: " + exception);
}
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
if (exception instanceof Saml2AuthenticationException saml2Exception) {
Saml2Error error = saml2Exception.getSaml2Error();
// Log detailed information about the SAML error
log.error("SAML Authentication failed with error code: {}", error.getErrorCode());
log.error("Error description: {}", error.getDescription());
// Redirect to login with specific error code
getRedirectStrategy()
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
} else if (exception instanceof ProviderNotFoundException) {
log.error("Authentication failed: No authentication provider found");
getRedirectStrategy()
.sendRedirect(
request,
response,
"/login?erroroauth=not_authentication_provider_found");
} else {
log.error("Unknown AuthenticationException: {}", exception.getMessage());
getRedirectStrategy().sendRedirect(request, response, "/login?erroroauth=unknown_error");
}
}
}

View File

@@ -0,0 +1,67 @@
package stirling.software.SPDF.config.security.saml2;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
public class LoggingSamlAuthenticationProvider implements AuthenticationProvider {
private static final Logger log = LoggerFactory.getLogger(LoggingSamlAuthenticationProvider.class);
private final OpenSaml4AuthenticationProvider delegate;
public LoggingSamlAuthenticationProvider(OpenSaml4AuthenticationProvider delegate) {
this.delegate = delegate;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication instanceof Saml2AuthenticationToken token) {
String samlResponse = token.getSaml2Response();
// Log the raw SAML response
log.info("Raw SAML Response (Base64): {}", samlResponse);
// Decode and log the SAML response XML
try {
String decodedResponse = new String(Base64.getDecoder().decode(samlResponse), StandardCharsets.UTF_8);
log.info("Decoded SAML Response XML:\n{}", decodedResponse);
} catch (IllegalArgumentException e) {
// If decoding fails, its likely already plain XML
log.warn("SAML Response appears to be different format, not Base64-encoded.");
log.debug("SAML Response XML:\n{}", samlResponse);
}
// Delegate the actual authentication to the wrapped OpenSaml4AuthenticationProvider
try {
return delegate.authenticate(authentication);
} catch (Saml2AuthenticationException e) {
log.error("SAML authentication failed: {}");
log.error("Detailed error message: {}", e);
throw e;
}
}
return null;
}
@Override
public boolean supports(Class<?> authentication) {
// Only support Saml2AuthenticationToken
return Saml2AuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@@ -20,6 +20,7 @@ import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import lombok.Data;
import lombok.Getter;
@@ -30,6 +31,7 @@ import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.GeneralUtils;
@Configuration
@ConfigurationProperties(prefix = "")
@@ -134,44 +136,20 @@ public class ApplicationProperties {
private String privateKey;
private String spCert;
public InputStream getIdpMetadataUri() throws IOException {
if (idpMetadataUri.startsWith("classpath:")) {
return new ClassPathResource(idpMetadataUri.substring("classpath".length()))
.getInputStream();
}
try {
URI uri = new URI(idpMetadataUri);
URL url = uri.toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
return connection.getInputStream();
} catch (URISyntaxException e) {
throw new IOException("Invalid URI format: " + idpMetadataUri, e);
}
public Resource getIdpMetadataUri() throws IOException {
return GeneralUtils.filePathToResource(idpMetadataUri);
}
public Resource getSpCert() {
if (spCert.startsWith("classpath:")) {
return new ClassPathResource(spCert.substring("classpath:".length()));
} else {
return new FileSystemResource(spCert);
}
return GeneralUtils.filePathToResource(spCert);
}
public Resource getidpCert() {
if (idpCert.startsWith("classpath:")) {
return new ClassPathResource(idpCert.substring("classpath:".length()));
} else {
return new FileSystemResource(idpCert);
}
return GeneralUtils.filePathToResource(idpCert);
}
public Resource getPrivateKey() {
if (privateKey.startsWith("classpath:")) {
return new ClassPathResource(privateKey.substring("classpath:".length()));
} else {
return new FileSystemResource(privateKey);
}
return GeneralUtils.filePathToResource(privateKey);
}
}

View File

@@ -29,6 +29,10 @@ import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.web.multipart.MultipartFile;
import com.fathzer.soft.javaluator.DoubleEvaluator;
@@ -349,4 +353,23 @@ public class GeneralUtils {
return "GenericID";
}
}
public static Resource filePathToResource(String resourceFile) {
if (resourceFile == null) {
throw new IllegalStateException("file is not configured");
}
if (resourceFile.startsWith("classpath:")) {
return new ClassPathResource(resourceFile.substring("classpath:".length()));
} else if (resourceFile.startsWith("http://") || resourceFile.startsWith("https://")) {
try {
return new UrlResource(resourceFile);
} catch (Exception e) {
throw new RuntimeException("Failed to create URL resource: " + resourceFile, e);
}
} else {
return new FileSystemResource(resourceFile);
}
}
}

View File

@@ -81,7 +81,6 @@ page=صفحة
pages=صفحات
loading=جارٍ التحميل...
addToDoc=إضافة إلى المستند
reset=Reset
legal.privacy=سياسة الخصوصية
legal.terms=شروط الاستخدام

View File

@@ -81,7 +81,6 @@ page=Страница
pages=Страници
loading=Loading...
addToDoc=Add to Document
reset=Reset
legal.privacy=Политика за поверителност
legal.terms=Правила и условия

View File

@@ -81,7 +81,6 @@ page=Pàgina
pages=Pàgines
loading=Carregant...
addToDoc=Afegeix al document
reset=Reset
legal.privacy=Política de Privacitat
legal.terms=Termes i condicions

View File

@@ -81,7 +81,6 @@ page=Strana
pages=Strany
loading=Načítání...
addToDoc=Přidat do dokumentu
reset=Reset
legal.privacy=Politika soukromí
legal.terms=Podmínky použití

View File

@@ -81,7 +81,6 @@ page=Sidenummer
pages=Sideantal
loading=Laster...
addToDoc=Tilføj til Dokument
reset=Reset
legal.privacy=Privacy Policy
legal.terms=Vilkår og betingelser

View File

@@ -81,7 +81,6 @@ page=Seite
pages=Seiten
loading=Laden...
addToDoc=In Dokument hinzufügen
reset=Reset
legal.privacy=Datenschutz
legal.terms=AGB

View File

@@ -81,7 +81,6 @@ page=Σελίδα
pages=Σελίδες
loading=Φόρτωση...
addToDoc=Πρόσθεση στο Εκπομπώματο
reset=Reset
legal.privacy=Πολιτική Προνομίους
legal.terms=Φράσεις Υποχρεωτικότητας

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Página
pages=Páginas
loading=Cargando...
addToDoc=Agregar al Documento
reset=Reset
legal.privacy=Política de Privacidad
legal.terms=Términos y Condiciones

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages
loading=Chargement...
addToDoc=Ajouter au Document
reset=Reset
legal.privacy=Politique de Confidentialité
legal.terms=Conditions Générales

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=पृष्ठ
pages=पृष्ठों
loading=डालिंग...
addToDoc=Add to Document
reset=Reset
legal.privacy=गुप्तता सूचना
legal.terms=शर्तें और प्रवाह

View File

@@ -81,7 +81,6 @@ page=Stranica
pages=Stranice
loading=Učitavanje...
addToDoc=Dodaj u dokument
reset=Reset
legal.privacy=Politika privatnosti
legal.terms=Uspe sodržine

View File

@@ -81,7 +81,6 @@ page=Oldal
pages=Oldalak
loading=Betöltés...
addToDoc=Hozzáadás dokumentumba
reset=Reset
legal.privacy=Adatvédelmi nyilatkozat
legal.terms=Feltételek és feltételek

View File

@@ -81,7 +81,6 @@ page=Halaman
pages=Halaman-halaman
loading=Mengambil data...
addToDoc=Tambahkan ke Dokumen
reset=Reset
legal.privacy=Kebijakan Privasi
legal.terms=Syarat dan Ketentuan

View File

@@ -81,7 +81,6 @@ page=Pagina
pages=Pagine
loading=Caricamento...
addToDoc=Aggiungi al documento
reset=Reset
legal.privacy=Informativa sulla privacy
legal.terms=Termini e Condizioni
@@ -142,7 +141,7 @@ navbar.language=Lingue
navbar.settings=Impostazioni
navbar.allTools=Strumenti
navbar.multiTool=Strumenti multipli
navbar.search=Cerca
navbar.search=Search
navbar.sections.organize=Organizza
navbar.sections.convertTo=Converti in PDF
navbar.sections.convertFrom=Converti da PDF
@@ -945,7 +944,7 @@ multiTool.downloadAll=Esporta
multiTool.downloadSelected=Esporta selezionata
#multiTool-advert
multiTool-advert.message=Questa funzione è disponibile anche nella nostra <a href="{0}">pagina multi-strumento</a>. Scoprila per un'interfaccia utente pagina per pagina migliorata e funzionalità aggiuntive!
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
#view pdf
viewPdf.title=Visualizza PDF

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
reset=Reset
legal.privacy=プライバシーポリシー
legal.terms=利用規約

View File

@@ -81,7 +81,6 @@ page=페이지
pages=페이지
loading=로딩 중...
addToDoc=문서에 추가
reset=Reset
legal.privacy=개인 정보 정책
legal.terms=이용 약관

View File

@@ -81,7 +81,6 @@ page=Pagina
pages=Pagen
loading=Laden...
addToDoc=Toevoegen aan document
reset=Reset
legal.privacy=Privacybeleid
legal.terms=Voorwaarden van gebruik

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Strona
pages=Strony
loading=Loading...
addToDoc=Add to Document
reset=Reset
legal.privacy=Polityka Prywatności
legal.terms=Zasady i Postanowienia

View File

@@ -81,7 +81,6 @@ page=Página
pages=Páginas
loading=Carregando...
addToDoc=Adicionar ao Documento
reset=Reset
legal.privacy=Política de Privacidade
legal.terms=Termos e Condições

View File

@@ -81,7 +81,6 @@ page=Página
pages=Páginas
loading=A carregar...
addToDoc=Adicionar ao Documento
reset=Reset
legal.privacy=Política de Privacidade
legal.terms=Termos e Condições

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Страница
pages=Страницы
loading=Загрузка...
addToDoc=Добавить в документ
reset=Reset
legal.privacy=Политика конфиденциальности
legal.terms=Условия использования

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Sidan
pages=Sidor
loading=Laddar...
addToDoc=Lägg till i dokument
reset=Reset
legal.privacy=Dataprotektionspolicy
legal.terms=Villkor och betingelser

View File

@@ -81,7 +81,6 @@ page=หน้า
pages=หน้า
loading=กำลังโหลด...
addToDoc=เพิ่มเข้าสู่เอกสาร
reset=Reset
legal.privacy=นโยบายความเป็นส่วนตัว
legal.terms=ข้อกำหนดการใช้งาน

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
reset=Reset
legal.privacy=Gizlilik Politikası
legal.terms=Şartlar ve koşullar

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=頁面
pages=頁面
loading=載入中...
addToDoc=新增至文件
reset=Reset
legal.privacy=隱私權政策
legal.terms=使用條款

View File

@@ -96,20 +96,71 @@
});
});
function getPDFPageCount(file) {
try {
if (file.type !== 'application/pdf') {
return null;
}
// Create a URL for the file
const url = URL.createObjectURL(file);
try {
// Ensure the worker is properly set
if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
}
// Load the PDF document
const loadingTask = pdfjsLib.getDocument(url);
const pdf = await loadingTask.promise;
// Get the page count
const pageCount = pdf.numPages;
return pageCount;
} finally {
// Clean up the URL
URL.revokeObjectURL(url);
}
} catch (error) {
console.error('Error counting PDF pages:', error);
return null;
}
}
function trackFileProcessing(file, startTime, success, errorMessage = null) {
const endTime = performance.now();
const processingTime = endTime - startTime;
posthog.capture('file_processing', {
success: success,
file_type: file.type || 'unknown',
file_size: file.size,
processing_time: processingTime,
error_message: errorMessage,
pdf_pages: file.type === 'application/pdf' ? getPDFPageCount(file) : null
});
}
async function handleSingleDownload(url, formData, isMulti = false, isZip = false) {
const startTime = performance.now();
const file = formData.get('fileInput');
try {
const response = await fetch(url, { method: "POST", body: formData });
const contentType = response.headers.get("content-type");
if (!response.ok) {
if (response.status === 401) {
// Handle 401 Unauthorized error
showSessionExpiredPrompt();
trackFileProcessing(file, startTime, false, 'unauthorized');
return;
}
if (contentType && contentType.includes("application/json")) {
console.error("Throwing error banner, response was not okay");
return handleJsonResponse(response);
const jsonResponse = await handleJsonResponse(response);
trackFileProcessing(file, startTime, false, jsonResponse?.error || 'unknown_error');
return jsonResponse;
}
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -118,6 +169,8 @@
let filename = getFilenameFromContentDisposition(contentDisposition);
const blob = await response.blob();
trackFileProcessing(file, startTime, true, null);
if (contentType.includes("application/pdf") || contentType.includes("image/")) {
clearFileInput();
return handleResponse(blob, filename, !isMulti, isZip);
@@ -126,9 +179,11 @@
return handleResponse(blob, filename, false, isZip);
}
} catch (error) {
clearFileInput();
console.error("Error in handleSingleDownload:", error);
trackFileProcessing(file, startTime, false, error.message);
throw error;
}
}
@@ -169,8 +224,7 @@
if (considerViewOptions) {
if (downloadOption === "sameWindow") {
const url = URL.createObjectURL(blob);
window.location.href = url;
return;
return { filename, blob, url };
} else if (downloadOption === "newWindow") {
const url = URL.createObjectURL(blob);
window.open(url, "_blank");

View File

@@ -9,81 +9,81 @@ const DraggableUtils = {
init() {
interact(".draggable-canvas")
.draggable({
listeners: {
move: (event) => {
const target = event.target;
const x = (parseFloat(target.getAttribute("data-bs-x")) || 0)
.draggable({
listeners: {
move: (event) => {
const target = event.target;
const x = (parseFloat(target.getAttribute("data-bs-x")) || 0)
+ event.dx;
const y = (parseFloat(target.getAttribute("data-bs-y")) || 0)
const y = (parseFloat(target.getAttribute("data-bs-y")) || 0)
+ event.dy;
target.style.transform = `translate(${x}px, ${y}px)`;
target.setAttribute("data-bs-x", x);
target.setAttribute("data-bs-y", y);
target.style.transform = `translate(${x}px, ${y}px)`;
target.setAttribute("data-bs-x", x);
target.setAttribute("data-bs-y", y);
this.onInteraction(target);
//update the last interacted element
this.lastInteracted = event.target;
},
this.onInteraction(target);
//update the last interacted element
this.lastInteracted = event.target;
},
})
.resizable({
edges: { left: true, right: true, bottom: true, top: true },
listeners: {
move: (event) => {
var target = event.target;
var x = parseFloat(target.getAttribute("data-bs-x")) || 0;
var y = parseFloat(target.getAttribute("data-bs-y")) || 0;
},
})
.resizable({
edges: { left: true, right: true, bottom: true, top: true },
listeners: {
move: (event) => {
var target = event.target;
var x = parseFloat(target.getAttribute("data-bs-x")) || 0;
var y = parseFloat(target.getAttribute("data-bs-y")) || 0;
// check if control key is pressed
if (event.ctrlKey) {
const aspectRatio = target.offsetWidth / target.offsetHeight;
// preserve aspect ratio
let width = event.rect.width;
let height = event.rect.height;
// check if control key is pressed
if (event.ctrlKey) {
const aspectRatio = target.offsetWidth / target.offsetHeight;
// preserve aspect ratio
let width = event.rect.width;
let height = event.rect.height;
if (Math.abs(event.deltaRect.width) >= Math.abs(
if (Math.abs(event.deltaRect.width) >= Math.abs(
event.deltaRect.height)) {
height = width / aspectRatio;
} else {
width = height * aspectRatio;
}
event.rect.width = width;
event.rect.height = height;
height = width / aspectRatio;
} else {
width = height * aspectRatio;
}
target.style.width = event.rect.width + "px";
target.style.height = event.rect.height + "px";
event.rect.width = width;
event.rect.height = height;
}
// translate when resizing from top or left edges
x += event.deltaRect.left;
y += event.deltaRect.top;
target.style.width = event.rect.width + "px";
target.style.height = event.rect.height + "px";
target.style.transform = "translate(" + x + "px," + y + "px)";
// translate when resizing from top or left edges
x += event.deltaRect.left;
y += event.deltaRect.top;
target.setAttribute("data-bs-x", x);
target.setAttribute("data-bs-y", y);
target.textContent = Math.round(event.rect.width) + "\u00D7"
target.style.transform = "translate(" + x + "px," + y + "px)";
target.setAttribute("data-bs-x", x);
target.setAttribute("data-bs-y", y);
target.textContent = Math.round(event.rect.width) + "\u00D7"
+ Math.round(event.rect.height);
this.onInteraction(target);
},
this.onInteraction(target);
},
},
modifiers: [
interact.modifiers.restrictSize({
min: { width: 5, height: 5 },
}),
],
inertia: true,
});
modifiers: [
interact.modifiers.restrictSize({
min: {width: 5, height: 5},
}),
],
inertia: true,
});
//Arrow key Support for Add-Image and Sign pages
if (window.location.pathname.endsWith('sign') || window.location.pathname.endsWith('add-image')) {
if(window.location.pathname.endsWith('sign') || window.location.pathname.endsWith('add-image')) {
window.addEventListener('keydown', (event) => {
//Check for last interacted element
if (!this.lastInteracted) {
if (!this.lastInteracted){
return;
}
// Get the currently selected element
@@ -288,7 +288,7 @@ const DraggableUtils = {
}
},
parseTransform(element) { },
parseTransform(element) {},
async getOverlayedPdfDocument() {
const pdfBytes = await this.pdfDoc.getData();
const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes, {
@@ -308,7 +308,6 @@ const DraggableUtils = {
const offsetWidth = pagesMap[pageIdx + "-offsetWidth"];
const offsetHeight = pagesMap[pageIdx + "-offsetHeight"];
for (const draggableData of draggablesData) {
// embed the draggable canvas
const draggableElement = draggableData.element;
@@ -325,24 +324,6 @@ const DraggableUtils = {
width: draggableData.offsetWidth,
height: draggableData.offsetHeight,
};
//Auxiliary variables
let widthAdjusted = page.getWidth();
let heightAdjusted = page.getHeight();
const rotation = page.getRotation();
//Normalizing angle
let normalizedAngle = rotation.angle % 360;
if (normalizedAngle < 0) {
normalizedAngle += 360;
}
//Changing the page dimension if the angle is 90 or 270
if (normalizedAngle === 90 || normalizedAngle === 270) {
let widthTemp = widthAdjusted;
widthAdjusted = heightAdjusted;
heightAdjusted = widthTemp;
}
const draggablePositionRelative = {
x: draggablePositionPixels.x / offsetWidth,
y: draggablePositionPixels.y / offsetHeight,
@@ -350,36 +331,18 @@ const DraggableUtils = {
height: draggablePositionPixels.height / offsetHeight,
};
const draggablePositionPdf = {
x: draggablePositionRelative.x * widthAdjusted,
y: draggablePositionRelative.y * heightAdjusted,
width: draggablePositionRelative.width * widthAdjusted,
height: draggablePositionRelative.height * heightAdjusted,
x: draggablePositionRelative.x * page.getWidth(),
y: draggablePositionRelative.y * page.getHeight(),
width: draggablePositionRelative.width * page.getWidth(),
height: draggablePositionRelative.height * page.getHeight(),
};
//Defining the image if the page has a 0-degree angle
let x = draggablePositionPdf.x
let y = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height
//Defining the image position if it is at other angles
if (normalizedAngle === 90) {
x = draggablePositionPdf.y + draggablePositionPdf.height;
y = draggablePositionPdf.x;
} else if (normalizedAngle === 180) {
x = widthAdjusted - draggablePositionPdf.x;
y = draggablePositionPdf.y + draggablePositionPdf.height;
} else if (normalizedAngle === 270) {
x = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height;
y = widthAdjusted - draggablePositionPdf.x;
}
// draw the image
page.drawImage(pdfImageObject, {
x: x,
y: y,
x: draggablePositionPdf.x,
y: page.getHeight() - draggablePositionPdf.y - draggablePositionPdf.height,
width: draggablePositionPdf.width,
height: draggablePositionPdf.height,
rotate: rotation
});
}
}

View File

@@ -76,7 +76,7 @@ async function checkForUpdate() {
document.getElementById("update-btn").style.display = "block";
}
if (updateLink !== null) {
document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '<b>' + currentVersion + '</b>').replace("{1}", '<b>' + latestVersion + '</b>');
document.getElementById("app-update").innerText = updateAvailable.replace("{0}", '<b>' + currentVersion + '</b>').replace("{1}", '<b>' + latestVersion + '</b>');
if (updateLink.classList.contains("visually-hidden")) {
updateLink.classList.remove("visually-hidden");
}

View File

@@ -23,26 +23,23 @@
</form>
<p id="instruction-text" style="margin: 0; display: none" th:text="#{PDFToCSV.prompt}"></p>
<div style="position: relative; width: auto;" id="canvasesContainer">
<div style="position: relative; display: inline-block;">
<div>
<div style="display:none ;margin: 3px;position: absolute;top: 0;width: 120px;justify-content:space-between;z-index: 10" id="pagination-button-container">
<button id='previous-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> &lt; </button>
<button id='next-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> &gt; </button>
</div>
<canvas id="cropPdfCanvas" style="width: 100%"></canvas>
<canvas id="crop-pdf-canvas" style="position: absolute; top: 0; left: 0; z-index: 1;"></canvas>
</div>
<canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2; width: 100%"></canvas>
<canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2;"></canvas>
</div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script>
let pdfCanvas = document.getElementById('cropPdfCanvas');
let pdfCanvas = document.getElementById('crop-pdf-canvas');
let overlayCanvas = document.getElementById('overlayCanvas');
let canvasesContainer = document.getElementById('canvasesContainer');
canvasesContainer.style.display = "none";
// let paginationBtnContainer = ;
let context = pdfCanvas.getContext('2d');
let overlayContext = overlayCanvas.getContext('2d');
let btn1Object = document.getElementById('previous-page-btn');
let btn2Object = document.getElementById('next-page-btn');
@@ -63,8 +60,6 @@
let rectWidth = 0;
let rectHeight = 0;
let timeId = null; // timeout id for resizing canvases event
btn1Object.addEventListener('click',function (e){
if (currentPage !== 1) {
currentPage = currentPage - 1;
@@ -107,13 +102,14 @@
}
});
function renderPageFromFile(file) {
fileInput.addEventListener('change', function(e) {
file = e.target.files[0];
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function (ev) {
reader.onload = function(ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
@@ -121,37 +117,9 @@
pageId.value = currentPage;
};
reader.readAsArrayBuffer(file);
document.getElementById("pagination-button-container").style.display = "flex";
document.getElementById("instruction-text").style.display = "block";
document.getElementById("pagination-button-container").style.display="flex";
document.getElementById("instruction-text").style.display="block";
}
}
window.addEventListener("resize", function() {
clearTimeout(timeId);
timeId = setTimeout(function () {
if (fileInput.files.length == 0) return;
let canvasesContainer = document.getElementById('canvasesContainer');
let containerRect = canvasesContainer.getBoundingClientRect();
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
pdfCanvas.width = containerRect.width;
pdfCanvas.height = containerRect.height;
overlayCanvas.width = containerRect.width;
overlayCanvas.height = containerRect.height;
let file = fileInput.files[0];
renderPageFromFile(file);
}, 1000);
});
fileInput.addEventListener('change', function(e) {
canvasesContainer.style.display = "block"; // set for visual purposes
file = e.target.files[0];
renderPageFromFile(file);
});
function renderPage(pageNumber) {

View File

@@ -201,6 +201,7 @@
window.stirlingPDF.error = /*[[#{error}]]*/ "Error";
})();
</script>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script th:src="@{'/js/downloader.js'}"></script>
<div class="custom-file-chooser" th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}">

View File

@@ -83,6 +83,9 @@
</span>
</button>
</div>
<div class="mb-3">
<button type="button" id="resetFileInputBtn" class="btn btn-danger" onclick="removeAllElements()" th:text="#{reset}">Reset</button>
</div>
<div id="selected-pages-display" class="selected-pages-container hidden">
<div style="display:flex; height:3rem; margin-right:1rem">
<h5 th:text="#{multiTool.selectedPages}" style="white-space: nowrap; margin-right: 1rem;">Selected
@@ -162,4 +165,4 @@
</script>
</body>
</html>
</html>