Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package net.sf.jsignpdf.utils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Map;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Expands normal JSignPdf placeholders and optional formatted timestamp
* placeholders such as ${timestamp:yyyy.MM.dd}.
*/
public final class TextTimestampSubstitutor {
private static final String BASE_TIMESTAMP_PATTERN = "yyyy.MM.dd HH:mm:ss z";
private static final Pattern FORMATTED_TIMESTAMP = Pattern.compile("\\$\\{timestamp:([^}]+)}");
private static final DateTimeFormatter WALL_TIME_FORMAT =
DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss");

private TextTimestampSubstitutor() {
}

public static String replace(Object template, Map<?, ?> replacements) {
if (template == null) {
return null;
}

String result = String.valueOf(template);
Object timestamp = replacements == null ? null : replacements.get("timestamp");
if (timestamp != null && result.contains("${timestamp:")) {
result = expandFormattedTimestamps(result, String.valueOf(timestamp));
}

if (replacements == null) {
return result;
}
for (Map.Entry<?, ?> entry : replacements.entrySet()) {
if (entry.getKey() == null || entry.getValue() == null) {
continue;
}
String placeholder = "${" + entry.getKey() + "}";
result = result.replace(placeholder, String.valueOf(entry.getValue()));
}
return result;
}

private static String expandFormattedTimestamps(String template, String baseTimestamp) {
Matcher matcher = FORMATTED_TIMESTAMP.matcher(template);
if (!matcher.find()) {
return template;
}

Date date = null;
TimeZone timeZone = null;
try {
SimpleDateFormat baseFormat = new SimpleDateFormat(BASE_TIMESTAMP_PATTERN);
baseFormat.setLenient(false);
date = baseFormat.parse(baseTimestamp);
timeZone = resolveTimestampOffset(baseTimestamp, date);
} catch (ParseException ignored) {
// Keep formatted placeholders unchanged if the base timestamp cannot be parsed.
}

matcher.reset();
StringBuffer output = new StringBuffer();
while (matcher.find()) {
String replacement = matcher.group(0);
if (date != null) {
try {
SimpleDateFormat requestedFormat = new SimpleDateFormat(matcher.group(1));
if (timeZone != null) {
requestedFormat.setTimeZone(timeZone);
}
replacement = requestedFormat.format(date);
} catch (IllegalArgumentException ignored) {
// Invalid format: leave the original placeholder visible.
}
}
matcher.appendReplacement(output, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(output);
return output.toString();
}

/**
* SimpleDateFormat parses a zone token into the resulting instant but does
* not reliably retain that parsed zone on its Calendar. Reconstruct the
* effective offset from the wall-clock part and the parsed instant so a
* formatted placeholder keeps the same local date/time as ${timestamp}.
*/
private static TimeZone resolveTimestampOffset(String baseTimestamp, Date instant) {
try {
if (baseTimestamp.length() < 19) {
return TimeZone.getDefault();
}
LocalDateTime wallTime = LocalDateTime.parse(
baseTimestamp.substring(0, 19), WALL_TIME_FORMAT);
long wallAsUtcMillis = wallTime.toInstant(ZoneOffset.UTC).toEpochMilli();
long offsetMillis = wallAsUtcMillis - instant.getTime();
long maxReasonableOffset = 18L * 60L * 60L * 1000L;
if (Math.abs(offsetMillis) <= maxReasonableOffset) {
return new SimpleTimeZone((int) offsetMillis, "timestamp-offset");
}
} catch (RuntimeException ignored) {
// Fall back to the JVM zone below.
}
return TimeZone.getDefault();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,8 @@ ssl.keymanager.init=Initializing key manager from keystore file {0}.
#
# JavaFX GUI
#
jfx.gui.outputSuffix.label=Suffix:
jfx.gui.outputSuffix.prompt=signed
jfx.gui.engine.label=Engine:
jfx.gui.engine.tooltip=Signing engine used to sign the document.
jfx.gui.engine.unsupported=This option is not supported by the selected engine.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@
import net.sf.jsignpdf.types.ServerAuthentication;
import net.sf.jsignpdf.utils.AppConfig;
import net.sf.jsignpdf.utils.KeyStoreUtils;
import net.sf.jsignpdf.utils.TextTimestampSubstitutor;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
Expand Down Expand Up @@ -938,7 +938,7 @@ private String buildSignatureText(BasicSignerOptions options, Certificate[] chai
replacements.put(L2TEXT_PLACEHOLDER_LOCATION, StringUtils.defaultString(location));
replacements.put(L2TEXT_PLACEHOLDER_REASON, StringUtils.defaultString(reason));
replacements.put(L2TEXT_PLACEHOLDER_CONTACT, StringUtils.defaultString(options.getContact()));
return StrSubstitutor.replace(options.getL2Text(), replacements);
return TextTimestampSubstitutor.replace(options.getL2Text(), replacements);
}

private String extractCN(X509Certificate cert) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,18 @@
import net.sf.jsignpdf.utils.AppConfig;
import net.sf.jsignpdf.utils.KeyStoreUtils;
import net.sf.jsignpdf.utils.PKCS11Utils;
import net.sf.jsignpdf.utils.TextTimestampSubstitutor;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.StrSubstitutor;

import org.openpdf.text.DocumentException;
import org.openpdf.text.Element;
import org.openpdf.text.Font;
import org.openpdf.text.Image;
import org.openpdf.text.Phrase;
import org.openpdf.text.Rectangle;
import org.openpdf.text.pdf.ColumnText;
import org.openpdf.text.pdf.AcroFields;
import org.openpdf.text.pdf.OcspClientBouncyCastle;
import org.openpdf.text.pdf.PdfDate;
Expand All @@ -63,6 +67,7 @@
import org.openpdf.text.pdf.PdfSignature;
import org.openpdf.text.pdf.PdfSignatureAppearance;
import org.openpdf.text.pdf.PdfStamper;
import org.openpdf.text.pdf.PdfTemplate;
import org.openpdf.text.pdf.PdfString;
import org.openpdf.text.pdf.PdfWriter;
import org.openpdf.text.pdf.TSAClientBouncyCastle;
Expand Down Expand Up @@ -336,7 +341,7 @@
replacements.put(L2TEXT_PLACEHOLDER_LOCATION, StringUtils.defaultString(location));
replacements.put(L2TEXT_PLACEHOLDER_REASON, StringUtils.defaultString(reason));
replacements.put(L2TEXT_PLACEHOLDER_CONTACT, StringUtils.defaultString(contact));
final String l2text = StrSubstitutor.replace(options.getL2Text(), replacements);
final String l2text = TextTimestampSubstitutor.replace(options.getL2Text(), replacements);
sap.setLayer2Text(l2text);
}
final org.openpdf.text.pdf.BaseFont l2BaseFont = OpenPdfFonts.getL2BaseFont();
Expand Down Expand Up @@ -369,6 +374,9 @@
Rectangle signitureRect = computeSignatureRectangle(reader.getPageSize(page), options);
sap.setVisibleSignature(signitureRect, page, null);
}
if (renderMode == RenderMode.DESCRIPTION_ONLY) {
configureDescriptionLayer2(sap);
}
}

LOGGER.info(RES.get("console.processing"));
Expand All @@ -393,7 +401,7 @@
// PdfPKCS7.getAuthenticatedAttributeBytes
final int contentEstimated = (int) (Constants.DEFVAL_SIG_SIZE + 2L * crlInfo.getByteCount());
final Map<PdfName, Integer> exc = new HashMap<PdfName, Integer>();
exc.put(PdfName.CONTENTS, new Integer(contentEstimated * 2 + 2));

Check warning on line 404 in engines/openpdf/src/main/java/net/sf/jsignpdf/engine/openpdf/OpenPdfSigningEngine.java

View workflow job for this annotation

GitHub Actions / build (ubuntu-24.04)

Integer(int) in java.lang.Integer has been deprecated and marked for removal
sap.preClose(exc);

String provider = PKCS11Utils.getProviderNameForKeystoreType(options.getKsType());
Expand Down Expand Up @@ -498,6 +506,55 @@
return finished;
}

/**
* Builds the description-only layer 2 appearance over the complete signature rectangle.
*
* <p>OpenPDF 3.0.5 reserves the top 30% of the description-only layer for the legacy
* layer-4 status text. JSignPdf already controls the complete visible appearance, so that
* reservation can clip multiline layer-2 text in short signature rectangles. Creating layer 2
* explicitly keeps the public OpenPDF dependency unchanged while matching the full-rectangle
* layout used by JSignPdf's preview.</p>
*/
private void configureDescriptionLayer2(final PdfSignatureAppearance sap) throws DocumentException {
final PdfTemplate layer = sap.getLayer(2);
final Rectangle rect = sap.getRect();

final Image background = sap.getImage();
if (background != null) {
final float imageScale = sap.getImageScale();
if (imageScale == 0) {
layer.addImage(background, rect.getWidth(), 0, 0, rect.getHeight(), 0, 0);
} else {
float usableScale = imageScale;
if (imageScale < 0) {
usableScale = Math.min(rect.getWidth() / background.getWidth(),
rect.getHeight() / background.getHeight());
}
final float width = background.getWidth() * usableScale;
final float height = background.getHeight() * usableScale;
final float x = (rect.getWidth() - width) / 2;
final float y = (rect.getHeight() - height) / 2;
layer.addImage(background, width, 0, 0, height, x, y);
}
}

final Font configuredFont = sap.getLayer2Font();
final Font font = configuredFont == null ? new Font() : new Font(configuredFont);
float size = font.getSize();
final String text = StringUtils.defaultString(sap.getLayer2Text());
final Rectangle dataRect = new Rectangle(0, 0, rect.getWidth(), rect.getHeight());
if (size <= 0) {
final Rectangle fitRect = new Rectangle(dataRect.getWidth(), dataRect.getHeight());
size = PdfSignatureAppearance.fitText(font, text, fitRect, 12, sap.getRunDirection());
}

final ColumnText column = new ColumnText(layer);
column.setRunDirection(sap.getRunDirection());
column.setSimpleColumn(new Phrase(text, font), dataRect.getLeft(), dataRect.getBottom(),
dataRect.getRight(), dataRect.getTop(), size, Element.ALIGN_LEFT);
column.go();
}

private Rectangle computeSignatureRectangle(Rectangle pageRect, BasicSignerOptions options) {
float pgWidth = pageRect.getWidth();
float pgHeighth = pageRect.getHeight();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package net.sf.jsignpdf.fx.control;

import java.io.File;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;

/** Text field for the suffix of the next signed PDF (e.g. DL or EM). */
public final class OutputSuffixField extends TextField {
public OutputSuffixField() {
super();
textProperty().addListener((obs, oldValue, newValue) -> {
OutputSuffixSupport.setUserValue(newValue);
refreshVisibleOutput();
});
}

private void refreshVisibleOutput() {
String output = OutputSuffixSupport.suggestedForLastInput();
if (output == null) return;
Scene scene = getScene();
if (scene == null) return;
Node node = scene.lookup("#lblOutputPath");
if (!(node instanceof Label label)) return;
label.setText("→ " + new File(output).getName());
Tooltip tooltip = label.getTooltip();
if (tooltip == null) {
tooltip = new Tooltip(output);
label.setTooltip(tooltip);
} else {
tooltip.setText(output);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package net.sf.jsignpdf.fx.control;

import java.io.File;
import java.util.Locale;
import net.sf.jsignpdf.utils.AppConfig;

/** Session-only output filename suffix state shared by the UI field and signing flow. */
public final class OutputSuffixSupport {
private static volatile String token = "";
private static volatile boolean touched = false;
private static volatile String lastInputPath;

private OutputSuffixSupport() {}

public static void setUserValue(String value) {
token = normalizeToken(value);
touched = true;
}

public static String suggestedFor(File inputFile) {
if (inputFile == null) {
lastInputPath = null;
return null;
}
lastInputPath = inputFile.getAbsolutePath();
return appendCurrentSuffix(lastInputPath);
}

public static String resolveForSign(String inputPath, String currentOutPath) {
if (!touched && currentOutPath != null && !currentOutPath.isBlank()) {
return currentOutPath;
}
if (inputPath == null || inputPath.isBlank()) {
return currentOutPath;
}
lastInputPath = inputPath;
return appendCurrentSuffix(inputPath);
}

public static String suggestedForLastInput() {
String input = lastInputPath;
return input == null || input.isBlank() ? null : appendCurrentSuffix(input);
}

private static String appendCurrentSuffix(String inputPath) {
String ext = ".pdf";
String base = inputPath;
if (inputPath.toLowerCase(Locale.ROOT).endsWith(ext)) {
base = inputPath.substring(0, inputPath.length() - ext.length());
ext = inputPath.substring(inputPath.length() - ext.length());
}
String suffix = token.isEmpty() ? AppConfig.defaultOutSuffix() : "_" + token;
return base + suffix + ext;
}

private static String normalizeToken(String value) {
if (value == null) return "";
String s = value.trim();
if (s.toLowerCase(Locale.ROOT).endsWith(".pdf")) {
s = s.substring(0, s.length() - 4).trim();
}
while (s.startsWith("_")) s = s.substring(1);
s = s.trim().replaceAll("\\s+", "_");
while (s.contains("__")) s = s.replace("__", "_");
return s;
}
}
Loading
Loading