mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2024-12-21 19:08:24 +01:00
Feature/save signs (#2127)
* apply fix * Fixes empty th:action * Update build.gradle * fix * formatting * Save signatures * Fix code scanning alert no. 42: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix UserServiceInterface * Merge branch 'feature/saveSigns' of git@github.com:Stirling-Tools/Stirling-PDF.git into feature/saveSigns * 0.31.0 bump and further csrf * formatting * preview name * add * sign doc * Update translation files (#2128) Signed-off-by: GitHub Action <action@github.com> Co-authored-by: GitHub Action <action@github.com> --------- Signed-off-by: GitHub Action <action@github.com> Co-authored-by: Dimitrios Kaitantzidis <james_k23@hotmail.gr> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: a <a> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
parent
ed75fa4e1b
commit
27d2681a97
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,6 +4,7 @@ bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.exe
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
|
@ -166,6 +166,13 @@ Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "pod
|
||||
|
||||
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md
|
||||
|
||||
## Reuse stored files
|
||||
|
||||
Certain functionality like ``Sign`` Supports pre-saved files stored at ``/customFiles/signatures/``, image files placed within here will be accesable to be used via webUI
|
||||
Currently this supports two folder types
|
||||
- ``/customFiles/signatures/ALL_USERS`` accessible to all users, useful for orginasations were many users use same files or for users not using authentication
|
||||
- ``/customFiles/signatures/{username}`` such as ``/customFiles/signatures/froodle`` accessible to only the ``froodle`` username, private for all others
|
||||
|
||||
## Supported Languages
|
||||
|
||||
Stirling PDF currently supports 38!
|
||||
|
@ -22,7 +22,7 @@ ext {
|
||||
}
|
||||
|
||||
group = "stirling.software"
|
||||
version = "0.30.1"
|
||||
version = "0.31.0"
|
||||
|
||||
java {
|
||||
// 17 is lowest but we support and recommend 21
|
||||
|
@ -104,7 +104,32 @@ public class SecurityConfiguration {
|
||||
requestHandler.setCsrfRequestAttributeName(null);
|
||||
http.csrf(
|
||||
csrf ->
|
||||
csrf.csrfTokenRepository(cookieRepo)
|
||||
csrf.ignoringRequestMatchers(
|
||||
request -> {
|
||||
String apiKey = request.getHeader("X-API-Key");
|
||||
|
||||
// If there's no API key, don't ignore CSRF
|
||||
// (return false)
|
||||
if (apiKey == null || apiKey.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate API key using existing UserService
|
||||
try {
|
||||
Optional<User> user =
|
||||
userService.getUserByApiKey(apiKey);
|
||||
// If API key is valid, ignore CSRF (return
|
||||
// true)
|
||||
// If API key is invalid, don't ignore CSRF
|
||||
// (return false)
|
||||
return user.isPresent();
|
||||
} catch (Exception e) {
|
||||
// If there's any error validating the API
|
||||
// key, don't ignore CSRF
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.csrfTokenRepository(cookieRepo)
|
||||
.csrfTokenRequestHandler(requestHandler));
|
||||
}
|
||||
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||
|
@ -96,17 +96,18 @@ public class CertSignController {
|
||||
|
||||
public CreateSignature(KeyStore keystore, char[] pin)
|
||||
throws KeyStoreException,
|
||||
UnrecoverableKeyException,
|
||||
NoSuchAlgorithmException,
|
||||
IOException,
|
||||
CertificateException {
|
||||
UnrecoverableKeyException,
|
||||
NoSuchAlgorithmException,
|
||||
IOException,
|
||||
CertificateException {
|
||||
super(keystore, pin);
|
||||
ClassPathResource resource = new ClassPathResource("static/images/signature.png");
|
||||
imageFile = resource.getFile();
|
||||
}
|
||||
|
||||
public InputStream createVisibleSignature(PDDocument srcDoc, PDSignature signature, Integer pageNumber,
|
||||
Boolean showImage) throws IOException {
|
||||
public InputStream createVisibleSignature(
|
||||
PDDocument srcDoc, PDSignature signature, Integer pageNumber, Boolean showImage)
|
||||
throws IOException {
|
||||
// modified from org.apache.pdfbox.examples.signature.CreateVisibleSignature2
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage page = new PDPage(srcDoc.getPage(pageNumber).getMediaBox());
|
||||
@ -151,8 +152,8 @@ public class CertSignController {
|
||||
extState.setNonStrokingAlphaConstant(0.5f);
|
||||
cs.setGraphicsStateParameters(extState);
|
||||
cs.transform(Matrix.getScaleInstance(0.08f, 0.08f));
|
||||
PDImageXObject img = PDImageXObject.createFromFileByExtension(imageFile,
|
||||
doc);
|
||||
PDImageXObject img =
|
||||
PDImageXObject.createFromFileByExtension(imageFile, doc);
|
||||
cs.drawImage(img, 100, 0);
|
||||
cs.restoreGraphicsState();
|
||||
}
|
||||
@ -200,7 +201,10 @@ public class CertSignController {
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
|
||||
@Operation(summary = "Sign PDF with a Digital Certificate", description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:SISO")
|
||||
@Operation(
|
||||
summary = "Sign PDF with a Digital Certificate",
|
||||
description =
|
||||
"This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
|
||||
throws Exception {
|
||||
MultipartFile pdf = request.getFileInput();
|
||||
@ -229,7 +233,7 @@ public class CertSignController {
|
||||
PrivateKey privateKey = getPrivateKeyFromPEM(privateKeyFile.getBytes(), password);
|
||||
Certificate cert = (Certificate) getCertificateFromPEM(certFile.getBytes());
|
||||
ks.setKeyEntry(
|
||||
"alias", privateKey, password.toCharArray(), new Certificate[] { cert });
|
||||
"alias", privateKey, password.toCharArray(), new Certificate[] {cert});
|
||||
break;
|
||||
case "PKCS12":
|
||||
ks = KeyStore.getInstance("PKCS12");
|
||||
@ -245,7 +249,15 @@ public class CertSignController {
|
||||
|
||||
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
sign(pdfDocumentFactory, pdf.getBytes(), baos, createSignature, showSignature, pageNumber, name, location,
|
||||
sign(
|
||||
pdfDocumentFactory,
|
||||
pdf.getBytes(),
|
||||
baos,
|
||||
createSignature,
|
||||
showSignature,
|
||||
pageNumber,
|
||||
name,
|
||||
location,
|
||||
reason);
|
||||
return WebResponseUtils.boasToWebResponse(
|
||||
baos,
|
||||
@ -274,8 +286,8 @@ public class CertSignController {
|
||||
|
||||
if (showSignature) {
|
||||
SignatureOptions signatureOptions = new SignatureOptions();
|
||||
signatureOptions
|
||||
.setVisualSignature(instance.createVisibleSignature(doc, signature, pageNumber, true));
|
||||
signatureOptions.setVisualSignature(
|
||||
instance.createVisibleSignature(doc, signature, pageNumber, true));
|
||||
signatureOptions.setPage(pageNumber);
|
||||
|
||||
doc.addSignature(signature, instance, signatureOptions);
|
||||
@ -291,19 +303,22 @@ public class CertSignController {
|
||||
|
||||
private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password)
|
||||
throws IOException, OperatorCreationException, PKCSException {
|
||||
try (PEMParser pemParser = new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) {
|
||||
try (PEMParser pemParser =
|
||||
new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) {
|
||||
Object pemObject = pemParser.readObject();
|
||||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
|
||||
PrivateKeyInfo pkInfo;
|
||||
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) {
|
||||
InputDecryptorProvider decProv = new JceOpenSSLPKCS8DecryptorProviderBuilder()
|
||||
.build(password.toCharArray());
|
||||
InputDecryptorProvider decProv =
|
||||
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray());
|
||||
pkInfo = ((PKCS8EncryptedPrivateKeyInfo) pemObject).decryptPrivateKeyInfo(decProv);
|
||||
} else if (pemObject instanceof PEMEncryptedKeyPair) {
|
||||
PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
|
||||
pkInfo = ((PEMEncryptedKeyPair) pemObject)
|
||||
.decryptKeyPair(decProv)
|
||||
.getPrivateKeyInfo();
|
||||
PEMDecryptorProvider decProv =
|
||||
new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
|
||||
pkInfo =
|
||||
((PEMEncryptedKeyPair) pemObject)
|
||||
.decryptKeyPair(decProv)
|
||||
.getPrivateKeyInfo();
|
||||
} else {
|
||||
pkInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo();
|
||||
}
|
||||
|
@ -31,6 +31,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
import stirling.software.SPDF.service.SignatureService;
|
||||
|
||||
@Controller
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class GeneralWebController {
|
||||
@ -171,11 +175,28 @@ public class GeneralWebController {
|
||||
return "split-pdfs";
|
||||
}
|
||||
|
||||
private static final String SIGNATURE_BASE_PATH = "customFiles/static/signatures/";
|
||||
private static final String ALL_USERS_FOLDER = "ALL_USERS";
|
||||
|
||||
@Autowired private SignatureService signatureService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private UserServiceInterface userService;
|
||||
|
||||
@GetMapping("/sign")
|
||||
@Hidden
|
||||
public String signForm(Model model) {
|
||||
String username = "";
|
||||
if (userService != null) {
|
||||
username = userService.getCurrentUsername();
|
||||
}
|
||||
|
||||
// Get signatures from both personal and ALL_USERS folders
|
||||
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
|
||||
|
||||
model.addAttribute("currentPage", "sign");
|
||||
model.addAttribute("fonts", getFontNames());
|
||||
model.addAttribute("signatures", signatures);
|
||||
return "sign";
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,44 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||
import stirling.software.SPDF.service.SignatureService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/api/v1/general/")
|
||||
public class SignatureController {
|
||||
|
||||
@Autowired private SignatureService signatureService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private UserServiceInterface userService;
|
||||
|
||||
@GetMapping("/sign/{fileName}")
|
||||
public ResponseEntity<byte[]> getSignature(@PathVariable(name = "fileName") String fileName)
|
||||
throws IOException {
|
||||
String username = "NON_SECURITY_USER";
|
||||
if (userService != null) {
|
||||
username = userService.getCurrentUsername();
|
||||
}
|
||||
|
||||
// Verify access permission
|
||||
if (!signatureService.hasAccessToFile(username, fileName)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
byte[] imageBytes = signatureService.getSignatureBytes(username, fileName);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.IMAGE_JPEG) // Adjust based on file type
|
||||
.body(imageBytes);
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class SignatureFile {
|
||||
private String fileName;
|
||||
private String category; // "Personal" or "Shared"
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thymeleaf.util.StringUtils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class SignatureService {
|
||||
|
||||
private static final String SIGNATURE_BASE_PATH = "customFiles/signatures/";
|
||||
private static final String ALL_USERS_FOLDER = "ALL_USERS";
|
||||
|
||||
public boolean hasAccessToFile(String username, String fileName) throws IOException {
|
||||
validateFileName(fileName);
|
||||
// Check if file exists in user's personal folder or ALL_USERS folder
|
||||
Path userPath = Paths.get(SIGNATURE_BASE_PATH, username, fileName);
|
||||
Path allUsersPath = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
|
||||
|
||||
return Files.exists(userPath) || Files.exists(allUsersPath);
|
||||
}
|
||||
|
||||
public List<SignatureFile> getAvailableSignatures(String username) {
|
||||
List<SignatureFile> signatures = new ArrayList<>();
|
||||
|
||||
// Get signatures from user's personal folder
|
||||
if (!StringUtils.isEmptyOrWhitespace(username)) {
|
||||
Path userFolder = Paths.get(SIGNATURE_BASE_PATH, username);
|
||||
if (Files.exists(userFolder)) {
|
||||
try {
|
||||
signatures.addAll(getSignaturesFromFolder(userFolder, "Personal"));
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading user signatures folder", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get signatures from ALL_USERS folder
|
||||
Path allUsersFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
||||
if (Files.exists(allUsersFolder)) {
|
||||
try {
|
||||
signatures.addAll(getSignaturesFromFolder(allUsersFolder, "Shared"));
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading shared signatures folder", e);
|
||||
}
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
private List<SignatureFile> getSignaturesFromFolder(Path folder, String category)
|
||||
throws IOException {
|
||||
return Files.list(folder)
|
||||
.filter(path -> isImageFile(path))
|
||||
.map(path -> new SignatureFile(path.getFileName().toString(), category))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public byte[] getSignatureBytes(String username, String fileName) throws IOException {
|
||||
validateFileName(fileName);
|
||||
// First try user's personal folder
|
||||
Path userPath = Paths.get(SIGNATURE_BASE_PATH, username, fileName);
|
||||
if (Files.exists(userPath)) {
|
||||
return Files.readAllBytes(userPath);
|
||||
}
|
||||
|
||||
// Then try ALL_USERS folder
|
||||
Path allUsersPath = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
|
||||
if (Files.exists(allUsersPath)) {
|
||||
return Files.readAllBytes(allUsersPath);
|
||||
}
|
||||
|
||||
throw new FileNotFoundException("Signature file not found");
|
||||
}
|
||||
|
||||
private boolean isImageFile(Path path) {
|
||||
String fileName = path.getFileName().toString().toLowerCase();
|
||||
return fileName.endsWith(".jpg")
|
||||
|| fileName.endsWith(".jpeg")
|
||||
|| fileName.endsWith(".png")
|
||||
|| fileName.endsWith(".gif");
|
||||
}
|
||||
|
||||
private void validateFileName(String fileName) {
|
||||
if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
|
||||
throw new IllegalArgumentException("Invalid filename");
|
||||
}
|
||||
}
|
||||
}
|
@ -30,17 +30,19 @@ public class ImageProcessingUtils {
|
||||
BufferedImage convertedImage;
|
||||
switch (colorType) {
|
||||
case "greyscale":
|
||||
convertedImage = new BufferedImage(
|
||||
sourceImage.getWidth(),
|
||||
sourceImage.getHeight(),
|
||||
BufferedImage.TYPE_BYTE_GRAY);
|
||||
convertedImage =
|
||||
new BufferedImage(
|
||||
sourceImage.getWidth(),
|
||||
sourceImage.getHeight(),
|
||||
BufferedImage.TYPE_BYTE_GRAY);
|
||||
convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null);
|
||||
break;
|
||||
case "blackwhite":
|
||||
convertedImage = new BufferedImage(
|
||||
sourceImage.getWidth(),
|
||||
sourceImage.getHeight(),
|
||||
BufferedImage.TYPE_BYTE_BINARY);
|
||||
convertedImage =
|
||||
new BufferedImage(
|
||||
sourceImage.getWidth(),
|
||||
sourceImage.getHeight(),
|
||||
BufferedImage.TYPE_BYTE_BINARY);
|
||||
convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null);
|
||||
break;
|
||||
default: // full color
|
||||
@ -79,7 +81,8 @@ public class ImageProcessingUtils {
|
||||
public static double extractImageOrientation(InputStream is) throws IOException {
|
||||
try {
|
||||
Metadata metadata = ImageMetadataReader.readMetadata(is);
|
||||
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
||||
ExifSubIFDDirectory directory =
|
||||
metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
||||
if (directory == null) {
|
||||
return 0;
|
||||
}
|
||||
@ -106,10 +109,11 @@ public class ImageProcessingUtils {
|
||||
if (orientation == 0) {
|
||||
return image;
|
||||
}
|
||||
AffineTransform transform = AffineTransform.getRotateInstance(
|
||||
Math.toRadians(orientation),
|
||||
image.getWidth() / 2.0,
|
||||
image.getHeight() / 2.0);
|
||||
AffineTransform transform =
|
||||
AffineTransform.getRotateInstance(
|
||||
Math.toRadians(orientation),
|
||||
image.getWidth() / 2.0,
|
||||
image.getHeight() / 2.0);
|
||||
AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
|
||||
return op.filter(image, null);
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=رسم التوقيع
|
||||
sign.text=إدخال النص
|
||||
sign.clear=مسح
|
||||
sign.add=إضافة
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Страница
|
||||
pages=Страници
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Политика за поверителност
|
||||
legal.terms=Правила и условия
|
||||
@ -808,6 +809,11 @@ sign.draw=Начертайте подпис
|
||||
sign.text=Въвеждане на текст
|
||||
sign.clear=Изчисти
|
||||
sign.add=Добави
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Dibuixa la signatura
|
||||
sign.text=Entrada de text
|
||||
sign.clear=Esborrar
|
||||
sign.add=Afegeix
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Nakreslit podpis
|
||||
sign.text=Vstup textu
|
||||
sign.clear=Vymazat
|
||||
sign.add=Přidat
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Tegn Underskrift
|
||||
sign.text=Tekstinput
|
||||
sign.clear=Ryd
|
||||
sign.add=Tilføj
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Datenschutz
|
||||
legal.terms=AGB
|
||||
@ -808,6 +809,11 @@ sign.draw=Signatur zeichnen
|
||||
sign.text=Texteingabe
|
||||
sign.clear=Leeren
|
||||
sign.add=Signieren
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Σχεδίαση υπογραφής
|
||||
sign.text=Εισαγωγή κειμένου
|
||||
sign.clear=Καθάρισμα
|
||||
sign.add=Προσθήκη
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Draw Signature
|
||||
sign.text=Text Input
|
||||
sign.clear=Clear
|
||||
sign.add=Add
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Draw Signature
|
||||
sign.text=Text Input
|
||||
sign.clear=Clear
|
||||
sign.add=Add
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Página
|
||||
pages=Páginas
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Política de Privacidad
|
||||
legal.terms=Términos y Condiciones
|
||||
@ -808,6 +809,11 @@ sign.draw=Dibujar firma
|
||||
sign.text=Entrada de texto
|
||||
sign.clear=Borrar
|
||||
sign.add=Agregar
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Marraztu sinadura
|
||||
sign.text=Testua sartzea
|
||||
sign.clear=Garbitu
|
||||
sign.add=Gehitu
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Politique de Confidentialité
|
||||
legal.terms=Conditions Générales
|
||||
@ -808,6 +809,11 @@ sign.draw=Dessiner une signature
|
||||
sign.text=Saisir de texte
|
||||
sign.clear=Effacer
|
||||
sign.add=Ajouter
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Tarraing Síniú
|
||||
sign.text=Ionchur Téacs
|
||||
sign.clear=Glan
|
||||
sign.add=Cuir
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=पृष्ठ
|
||||
pages=पृष्ठों
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=हस्ताक्षर बनाएँ
|
||||
sign.text=पाठ इनपुट
|
||||
sign.clear=साफ़ करें
|
||||
sign.add=जोड़ें
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Nacrtaj potpis
|
||||
sign.text=Tekstualni unos
|
||||
sign.clear=Obriši
|
||||
sign.add=Dodaj
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Aláírás rajzolása
|
||||
sign.text=Szöveg beírása
|
||||
sign.clear=Törlés
|
||||
sign.add=Hozzáadás
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Halaman
|
||||
pages=Halaman-halaman
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Kebijakan Privasi
|
||||
legal.terms=Syarat dan Ketentuan
|
||||
@ -808,6 +809,11 @@ sign.draw=Gambar Tanda Tangan
|
||||
sign.text=Masukan Teks
|
||||
sign.clear=Hapus
|
||||
sign.add=Tambah
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Pagina
|
||||
pages=Pagine
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Informativa sulla privacy
|
||||
legal.terms=Termini e Condizioni
|
||||
@ -808,6 +809,11 @@ sign.draw=Disegna Firma
|
||||
sign.text=Testo
|
||||
sign.clear=Cancella
|
||||
sign.add=Aggiungi
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=プライバシーポリシー
|
||||
legal.terms=利用規約
|
||||
@ -808,6 +809,11 @@ sign.draw=署名を書く
|
||||
sign.text=テキスト入力
|
||||
sign.clear=クリア
|
||||
sign.add=追加
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=서명 그리기
|
||||
sign.text=텍스트 입력
|
||||
sign.clear=초기화
|
||||
sign.add=추가
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Handtekening tekenen
|
||||
sign.text=Tekstinvoer
|
||||
sign.clear=Wissen
|
||||
sign.add=Toevoegen
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Tegn signatur
|
||||
sign.text=Tekstinput
|
||||
sign.clear=Slett
|
||||
sign.add=Legg til
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Strona
|
||||
pages=Strony
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Polityka Prywatności
|
||||
legal.terms=Zasady i Postanowienia
|
||||
@ -808,6 +809,11 @@ sign.draw=Narysuj podpis
|
||||
sign.text=Wprowadź tekst
|
||||
sign.clear=Wyczyść
|
||||
sign.add=Dodaj
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Página
|
||||
pages=Páginas
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Política de Privacidade
|
||||
legal.terms=Termos e Condições
|
||||
@ -808,6 +809,11 @@ sign.draw=Desenhar Assinatura
|
||||
sign.text=Inserir texto
|
||||
sign.clear=Limpar
|
||||
sign.add=Adicionar
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Desenhar Assinatura
|
||||
sign.text=Inserir Texto
|
||||
sign.clear=Limpar
|
||||
sign.add=Adicionar
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Desenează Semnătura
|
||||
sign.text=Introdu Textul
|
||||
sign.clear=Curăță
|
||||
sign.add=Adaugă
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Нарисовать подпись
|
||||
sign.text=Ввод текста
|
||||
sign.clear=Очистить
|
||||
sign.add=Добавить
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Kresliť podpis
|
||||
sign.text=Textový vstup
|
||||
sign.clear=Vymazať
|
||||
sign.add=Pridať
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Nacrtaj potpis
|
||||
sign.text=Tekstualni unos
|
||||
sign.clear=Obriši
|
||||
sign.add=Dodaj
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Rita signatur
|
||||
sign.text=Textinmatning
|
||||
sign.clear=Rensa
|
||||
sign.add=Lägg till
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=วาดลายเซ็น
|
||||
sign.text=ป้อนข้อความ
|
||||
sign.clear=ล้าง
|
||||
sign.add=เพิ่ม
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Gizlilik Politikası
|
||||
legal.terms=Şartlar ve koşullar
|
||||
@ -808,6 +809,11 @@ sign.draw=İmza Çiz
|
||||
sign.text=Metin Girişi
|
||||
sign.clear=Temizle
|
||||
sign.add=Ekle
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Намалювати підпис
|
||||
sign.text=Ввід тексту
|
||||
sign.clear=Очистити
|
||||
sign.add=Додати
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=Vẽ chữ ký
|
||||
sign.text=Nhập văn bản
|
||||
sign.clear=Xóa
|
||||
sign.add=Thêm
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=Pro
|
||||
page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@ -808,6 +809,11 @@ sign.draw=绘制签名
|
||||
sign.text=文本输入
|
||||
sign.clear=清除
|
||||
sign.add=添加
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -80,6 +80,7 @@ pro=專業版
|
||||
page=頁面
|
||||
pages=頁面
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
|
||||
legal.privacy=隱私權政策
|
||||
legal.terms=使用條款
|
||||
@ -808,6 +809,11 @@ sign.draw=繪製簽章
|
||||
sign.text=文字輸入
|
||||
sign.clear=清除
|
||||
sign.add=新增
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
|
||||
|
||||
#repair
|
||||
|
@ -62,3 +62,53 @@ select#font-select option {
|
||||
background-color: rgba(52, 152, 219, 0.2);
|
||||
/* Darken background on hover */
|
||||
}
|
||||
.signature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.signature-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.signature-list-item {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.signature-list-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.signature-list-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.signature-list-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.signature-list-details {
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.signature-list-details small:not(:last-child) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
text-align: right;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
29
src/main/resources/static/js/fetch-utils.js
Normal file
29
src/main/resources/static/js/fetch-utils.js
Normal file
@ -0,0 +1,29 @@
|
||||
window.fetchWithCsrf = async function(url, options = {}) {
|
||||
function getCsrfToken() {
|
||||
const cookieValue = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1];
|
||||
|
||||
if (cookieValue) {
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
const csrfElement = document.querySelector('input[name="_csrf"]');
|
||||
return csrfElement ? csrfElement.value : null;
|
||||
}
|
||||
|
||||
// Create a new options object to avoid modifying the passed object
|
||||
const fetchOptions = { ...options };
|
||||
|
||||
// Ensure headers object exists
|
||||
fetchOptions.headers = { ...options.headers };
|
||||
|
||||
// Add CSRF token if available
|
||||
const csrfToken = getCsrfToken();
|
||||
if (csrfToken) {
|
||||
fetchOptions.headers['X-XSRF-TOKEN'] = csrfToken;
|
||||
}
|
||||
|
||||
return fetch(url, fetchOptions);
|
||||
}
|
@ -119,7 +119,7 @@ document.getElementById("submitConfigBtn").addEventListener("click", function ()
|
||||
formData.append("json", pipelineConfigJson);
|
||||
console.log("formData", formData);
|
||||
|
||||
fetch("api/v1/pipeline/handleData", {
|
||||
fetchWithCsrf("api/v1/pipeline/handleData", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
@ -154,7 +154,7 @@ let apiDocs = {};
|
||||
let apiSchemas = {};
|
||||
let operationSettings = {};
|
||||
|
||||
fetch("v1/api-docs")
|
||||
fetchWithCsrf("v1/api-docs")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
apiDocs = data.paths;
|
||||
|
@ -384,7 +384,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
||||
<script th:inline="javascript">
|
||||
|
||||
/*<![CDATA[*/
|
||||
@ -398,7 +398,7 @@
|
||||
});
|
||||
/*]]>*/
|
||||
function setAnalytics(enabled) {
|
||||
fetch('api/v1/settings/update-enable-analytics', {
|
||||
fetchWithCsrf('api/v1/settings/update-enable-analytics', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
@ -20,7 +20,7 @@
|
||||
</div>
|
||||
|
||||
<!-- pdf selector -->
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script>
|
||||
let originalFileName = '';
|
||||
@ -46,7 +46,7 @@
|
||||
</script>
|
||||
|
||||
<div class="tab-group show-on-file-selected">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='image-upload', multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}"></div>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}"></div>
|
||||
<script>
|
||||
const imageUpload = document.querySelector('input[name=image-upload]');
|
||||
imageUpload.addEventListener('change', e => {
|
||||
|
@ -20,6 +20,7 @@
|
||||
<span class="tool-header-text" th:text="#{compress.header}"></span>
|
||||
</div>
|
||||
<form action="#" th:action="@{'/api/v1/misc/compress-pdf'}" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
|
@ -39,6 +39,7 @@
|
||||
<!-- Button to download the JSON -->
|
||||
<a href="#" id="downloadJS" class="btn btn-primary mt-3" style="display: none;" th:text="#{showJS.downloadJS}">Download JSON</a>
|
||||
</div>
|
||||
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
||||
<script>
|
||||
document.querySelector('#pdfInfoForm').addEventListener('submit', function(event){
|
||||
event.preventDefault();
|
||||
@ -46,7 +47,7 @@
|
||||
// Fetch the formData
|
||||
const formData = new FormData(event.target);
|
||||
|
||||
fetch('api/v1/misc/show-javascript', {
|
||||
fetchWithCsrf('api/v1/misc/show-javascript', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => response.text())
|
||||
|
@ -192,6 +192,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
||||
<script th:src="@{'/js/pipeline.js'}"></script>\
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,12 +31,16 @@
|
||||
<a href="#" id="downloadJson" class="btn btn-primary mt-3" style="display: none;" th:text="#{getPdfInfo.downloadJson}">Download JSON</a>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
import { fetchWithCsrf } from 'js/fetch-utils.js';
|
||||
|
||||
|
||||
document.getElementById("pdfInfoForm").addEventListener("submit", function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
|
||||
fetch('api/v1/security/get-info-on-pdf', {
|
||||
fetchWithCsrf('api/v1/security/get-info-on-pdf', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => response.json()).then(data => {
|
||||
|
@ -43,6 +43,53 @@
|
||||
</div>
|
||||
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||
<script>
|
||||
let currentPreviewSrc = null;
|
||||
|
||||
function toggleSignatureView() {
|
||||
const gridView = document.getElementById('gridView');
|
||||
const listView = document.getElementById('listView');
|
||||
const gridText = document.querySelector('.grid-view-text');
|
||||
const listText = document.querySelector('.list-view-text');
|
||||
|
||||
if (gridView.style.display !== 'none') {
|
||||
gridView.style.display = 'none';
|
||||
listView.style.display = 'block';
|
||||
gridText.style.display = 'none';
|
||||
listText.style.display = 'inline';
|
||||
} else {
|
||||
gridView.style.display = 'block';
|
||||
listView.style.display = 'none';
|
||||
gridText.style.display = 'inline';
|
||||
listText.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function previewSignature(element) {
|
||||
const src = element.dataset.src;
|
||||
currentPreviewSrc = src;
|
||||
|
||||
// Extract filename from the data source path
|
||||
const filename = element.querySelector('.signature-list-name').textContent;
|
||||
|
||||
// Update preview modal
|
||||
const previewImage = document.getElementById('previewImage');
|
||||
const previewFileName = document.getElementById('previewFileName');
|
||||
|
||||
previewImage.src = src;
|
||||
previewFileName.textContent = filename;
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('signaturePreview'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function addSignatureFromPreview() {
|
||||
if (currentPreviewSrc) {
|
||||
DraggableUtils.createDraggableCanvasFromUrl(currentPreviewSrc);
|
||||
bootstrap.Modal.getInstance(document.getElementById('signaturePreview')).hide();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let originalFileName = '';
|
||||
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
|
||||
const file = event.target.files[0];
|
||||
@ -68,7 +115,7 @@
|
||||
<div class="tab-group show-on-file-selected">
|
||||
<div class="tab-container" th:title="#{sign.upload}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='image-upload', multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
|
||||
th:replace="~{fragments/common :: fileSelector(name='image-upload', disableMultipleFiles=true, multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
|
||||
</div>
|
||||
<script>
|
||||
const imageUpload = document.querySelector('input[name=image-upload]');
|
||||
@ -165,6 +212,126 @@
|
||||
</script>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="tab-container" th:title="#{sign.saved}">
|
||||
<div class="saved-signatures-section" th:if="${not #lists.isEmpty(signatures)}">
|
||||
<!-- View Toggle Button -->
|
||||
<div class="view-toggle mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="toggleSignatureView()">
|
||||
<span class="material-symbols-rounded grid-view-text">view_list</span>
|
||||
<span class="material-symbols-rounded list-view-text" style="display: none;">grid_view</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Preview Modal -->
|
||||
<div class="modal fade" id="signaturePreview" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><span id="previewFileName"></span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img id="previewImage" src="" alt="Signature Preview" style="max-width: 100%;">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
|
||||
<button type="button" class="btn btn-primary" onclick="addSignatureFromPreview()" th:text="#{addToDoc}">Add to Document</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid View -->
|
||||
<div id="gridView">
|
||||
<!-- Personal Signatures -->
|
||||
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Personal'])}">
|
||||
<h5 th:text="#{sign.personalSigs}"></h5>
|
||||
<div class="signature-grid">
|
||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Personal'}" class="signature-item">
|
||||
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}"
|
||||
th:alt="${sig.fileName}"
|
||||
th:data-filename="${sig.fileName}"
|
||||
style="max-width: 200px; cursor: pointer;"
|
||||
onclick="DraggableUtils.createDraggableCanvasFromUrl(this.src)"/>
|
||||
<div class="signature-name" th:text="${sig.fileName}"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shared Signatures -->
|
||||
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Shared'])}">
|
||||
<h5 th:text="#{sign.sharedSigs}"></h5>
|
||||
<div class="signature-grid">
|
||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Shared'}" class="signature-item">
|
||||
<img th:src="@{'/api/v1/general/sign/' + ${sig.fileName}}"
|
||||
th:alt="${sig.fileName}"
|
||||
th:data-filename="${sig.fileName}"
|
||||
style="max-width: 200px; cursor: pointer;"
|
||||
onclick="DraggableUtils.createDraggableCanvasFromUrl(this.src)"/>
|
||||
<div class="signature-name" th:text="${sig.fileName}"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List View (Initially Hidden) -->
|
||||
<div id="listView" style="display: none;">
|
||||
<!-- Personal Signatures -->
|
||||
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Personal'])}">
|
||||
<h5 th:text="#{sign.personalSigs}"></h5>
|
||||
<div class="signature-list">
|
||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Personal'}"
|
||||
class="signature-list-item"
|
||||
th:data-src="@{'/api/v1/general/sign/' + ${sig.fileName}}"
|
||||
onclick="previewSignature(this)">
|
||||
<div class="signature-list-info">
|
||||
<span th:text="${sig.fileName}" class="signature-list-name"></span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shared Signatures -->
|
||||
<div class="signature-category" th:if="${not #lists.isEmpty(signatures.?[category == 'Shared'])}">
|
||||
<h5 th:text="#{sign.sharedSigs}"></h5>
|
||||
<div class="signature-list">
|
||||
<div th:each="sig : ${signatures}" th:if="${sig.category == 'Shared'}"
|
||||
class="signature-list-item"
|
||||
th:data-src="@{'/api/v1/general/sign/' + ${sig.fileName}}"
|
||||
onclick="previewSignature(this)">
|
||||
<div class="signature-list-info">
|
||||
<span th:text="${sig.fileName}" class="signature-list-name"></span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div th:if="${#lists.isEmpty(signatures)}" class="text-center p-3">
|
||||
<p th:text="#{sign.noSavedSigs}">No saved signatures found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="tab-container" th:title="#{sign.text}">
|
||||
<label class="form-check-label" for="sigText" th:text="#{text}"></label>
|
||||
<textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea>
|
||||
|
Loading…
Reference in New Issue
Block a user