diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..4736653d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,91 @@ +# AGENTS.md - JSignPdf + +## Project Overview + +JSignPdf is a Java application for adding digital signatures to PDF documents. It provides both a GUI (JavaFX default, Swing fallback via `-Djsignpdf.swing=true`) and a CLI interface. + +- **Java**: 11+ +- **Build**: Apache Maven multi-module +- **Repository**: https://github.com/intoolswetrust/jsignpdf + +## Build Commands + +```bash +mvn clean install # Build everything (with tests) +mvn clean install -DskipTests # Build without tests +mvn test -Dtest=BasicSigningTest # Run a single test class +``` + +## Module Structure + +``` +jsignpdf-root/ +├── jsignpdf/ # Main application (signing logic + GUI + CLI) +├── installcert/ # Certificate installer utility +├── distribution/ # Packaging: ZIP assembly, Windows installer, docs +└── website/ # Docusaurus documentation site (not a Maven module) +``` + +## Source Code Layout + +All source is under `jsignpdf/src/main/java/net/sf/jsignpdf/`: + +``` +net.sf.jsignpdf +├── Signer.java # Entry point - launches CLI or GUI +├── SignerLogic.java # Core signing engine (no UI dependencies) +├── BasicSignerOptions.java # Central model for all signing configuration +├── SignPdfForm.java # Swing GUI (legacy, .form files are IDE-generated) +├── fx/ # JavaFX GUI (default) +│ ├── JSignPdfApp.java # Application entry point +│ ├── FxLauncher.java # Static launcher called from Signer.main() +│ ├── view/ # FXML files + controllers (MainWindow, settings panels) +│ ├── viewmodel/ # DocumentViewModel, SigningOptionsViewModel, SignaturePlacementViewModel +│ ├── service/ # PdfRenderService, SigningService, KeyStoreService +│ ├── control/ # PdfPageView, SignatureOverlay +│ └── util/ # FxResourceProvider, SwingFxImageConverter, RecentFilesManager +├── crl/ # Certificate Revocation List handling +├── extcsp/ # External crypto providers (CloudFoxy) +├── preview/ # PDF page rendering (Pdf2Image) +├── ssl/ # SSL/TLS initialization +├── types/ # Enums and value types +└── utils/ # KeyStoreUtils, ResourceProvider, PropertyProvider, etc. +``` + +## Architecture + +``` +CLI (SignerOptionsFromCmdLine) ──┐ + ├──> BasicSignerOptions ──> SignerLogic.signFile() +GUI (JavaFX / Swing) ──┘ (model) (signing engine) +``` + +- **`BasicSignerOptions`** is the central model. Both CLI and GUI populate it, then pass it to `SignerLogic`. +- **`SignerLogic`** is the signing engine. It has no UI dependencies. +- **JavaFX GUI** uses MVVM: ViewModels with JavaFX properties, FXML views with `%key` i18n, background services wrapping `SignerLogic` and `Pdf2Image`. + +### i18n + +- Resource bundles: `net/sf/jsignpdf/translations/messages*.properties` +- CLI keys: `console.*`, `hlp.*` +- Swing keys: `gui.*` +- JavaFX keys: `jfx.gui.*` + +### Configuration + +- **User settings**: `~/.JSignPdf` (properties file, passwords encrypted per-user) +- **App config**: `conf/conf.properties` + +## Testing + +- **JUnit 4** - test sources under `jsignpdf/src/test/java/` +- **Signing tests** (`signing/` package): use `SigningTestBase` which creates temp PDFs dynamically +- **JavaFX UI tests** (`fx/FxTranslationsTest`): headless via Monocle, loads FXML with different locales and verifies node text + +## CI/CD (GitHub Actions) + +| Workflow | Trigger | Purpose | +|---|---|---| +| `pr-builder.yaml` | PR/push to master | `mvn verify` with Java 11 | +| `push-snapshots.yaml` | Push to master | Deploy SNAPSHOTs to Maven Central | +| `do-release.yml` | Manual dispatch | Full release | diff --git a/design-doc/jsignpdf-gui-reimplementation-plan.md b/design-doc/jsignpdf-gui-reimplementation-plan.md new file mode 100644 index 00000000..2dc4f839 --- /dev/null +++ b/design-doc/jsignpdf-gui-reimplementation-plan.md @@ -0,0 +1,311 @@ +# JSignPdf JavaFX GUI Redesign - Implementation Plan + +## Context + +JSignPdf's current Swing GUI is form-centric: users fill in text fields and click "Sign It." Modern PDF tools (Adobe Acrobat, Foxit) are document-centric: users open a PDF first, then interact with it visually. This redesign replaces the Swing UI with a JavaFX PDF viewer-style application where the document is the focal point, signature placement is drag-on-preview, and settings live in a collapsible side panel. The business logic (`SignerLogic`, `BasicSignerOptions`) stays untouched. + +--- + +## Main Window Layout + +``` ++------------------------------------------------------------------+ +| Menu: File | View | Signing | Help | ++------------------------------------------------------------------+ +| Toolbar: [Open] | [ZoomIn][ZoomOut][Fit] | [<][Page X/Y][>] | [Place Sig] | [SIGN] | ++------------------------------------------------------------------+ +| Side Panel (collapsible) | PDF Preview Area | +| +------------------------+ | +----------------------------+ | +| | > Certificate | | | | | +| | Keystore type: [___] | | | Rendered PDF page | | +| | File: [____] [Browse]| | | | | +| | Password: [____] | | | [signature rectangle] | | +| | [Load Keys] | | | (draggable/resizable) | | +| | Alias: [_________] | | | | | +| +------------------------+ | +----------------------------+ | +| | > Signature Appearance | | | +| | > Timestamp & Validation| | | +| | > Encryption & Rights | | | ++------------------------------------------------------------------+ +| Status: document.pdf - Page 3/12 - 1 signature [====] Ready | ++------------------------------------------------------------------+ +``` + +## UX Improvements (prioritized) + +**Core (must-have for the new paradigm):** +1. **Document-centric workflow** - open PDF first, then configure and sign +2. **Signature placement directly on PDF preview** - click+drag rectangle, no coordinate dialogs +3. **Drag-to-resize** signature rectangle with corner/edge handles +4. **Page navigation** - toolbar buttons, keyboard (PgUp/PgDn), page number field +5. **Zoom controls** - fit page, fit width, zoom in/out, percentage combo +6. **Drag-and-drop** PDF files onto the window +7. **Status bar** - filename, page count, existing signatures count, signing progress +8. **Progress indication** - ProgressBar in status bar during signing + +**Should-have:** +9. **Signature preview** - live rendering of what the stamp will look like +10. **Existing signatures panel** - list signatures already on the document +11. **Keyboard shortcuts** - Ctrl+O (open), Ctrl+S (sign), Ctrl+Z (undo placement), +/- (zoom) +12. **Undo signature placement** before signing + +**Nice-to-have (future):** +13. **Dark mode** via CSS theme switching +14. **Recent files** in File menu +15. **Page thumbnails** sidebar + +--- + +## Architecture: MVVM + +``` +View (FXML + Controllers) <--> ViewModel (JavaFX Properties) <--> Model (BasicSignerOptions) + | + Service layer (javafx.concurrent.Service) + | + SignerLogic, KeyStoreUtils, Pdf2Image (unchanged) +``` + +### Package Structure + +``` +net.sf.jsignpdf.fx/ + JSignPdfApp.java # extends Application, sets up primary Stage + FxLauncher.java # static launch() called from Signer.main() +net.sf.jsignpdf.fx.view/ + MainWindowController.java # MainWindow.fxml controller + CertificateSettingsController.java + SignatureSettingsController.java + TsaSettingsController.java + EncryptionSettingsController.java + OutputConsoleController.java +net.sf.jsignpdf.fx.viewmodel/ + DocumentViewModel.java # loaded PDF state, page, zoom + SigningOptionsViewModel.java # wraps BasicSignerOptions with FX properties + SignaturePlacementViewModel.java # rectangle position, drag state + AppStateViewModel.java # app-level: doc loaded?, signing in progress?, recent files +net.sf.jsignpdf.fx.service/ + PdfRenderService.java # wraps Pdf2Image -> JavaFX Image (background thread) + SigningService.java # wraps SignerLogic.signFile() (background thread) + KeyStoreService.java # wraps KeyStoreUtils.getKeyAliases() (background thread) +net.sf.jsignpdf.fx.control/ + PdfPageView.java # custom Region displaying rendered PDF page + SignatureOverlay.java # transparent overlay for click-drag signature rectangle +net.sf.jsignpdf.fx.util/ + FxResourceProvider.java # adapts ResourceProvider for JavaFX StringBindings + SwingFxImageConverter.java # BufferedImage <-> javafx.scene.image.Image + RecentFilesManager.java # persists recent file list +``` + +### FXML Files (in `src/main/resources/net/sf/jsignpdf/fx/view/`) +- `MainWindow.fxml` - BorderPane with menu, toolbar, SplitPane, status bar +- `CertificateSettings.fxml` - keystore type, file, password, alias +- `SignatureSettings.fxml` - render mode, L2/L4 text, images, font size +- `TsaSettings.fxml` - TSA, OCSP, CRL, proxy +- `EncryptionSettings.fxml` - encryption type, passwords, rights + +### CSS (in `src/main/resources/net/sf/jsignpdf/fx/styles/`) +- `jsignpdf.css` - light theme +- `jsignpdf-dark.css` - dark theme (future) + +### Key Design Decisions + +**Why MVVM**: JavaFX property bindings are a natural fit. ViewModels are testable without UI. + +**Why keep BasicSignerOptions unchanged**: It's the contract with `SignerLogic` and CLI mode. `SigningOptionsViewModel` is a JavaFX adapter that syncs bidirectionally via `syncToOptions()` / `syncFromOptions()`. + +**Why `javafx-swing` dependency**: Reuses existing `Pdf2Image` (returns `BufferedImage`) via `SwingFXUtils.toFXImage()`. Avoids rewriting PDF rendering backends. + +**Why parallel Swing/JavaFX**: Risk mitigation. Old UI available via `-Djsignpdf.swing=true` flag until JavaFX is proven stable. + +--- + +## Migration Strategy + +In `Signer.java`, the GUI launch becomes: + +```java +if (showGui) { + if (Boolean.getBoolean("jsignpdf.swing")) { + // Legacy Swing (preserved for fallback) + SignPdfForm tmpForm = new SignPdfForm(...); + ... + } else { + // New JavaFX (default) + FxLauncher.launch(tmpOpts); + } +} +``` + +Swing code is NOT deleted until the JavaFX UI ships in at least one stable release. + +--- + +## Maven Changes + +### Dependencies (add to `jsignpdf/pom.xml`) + +```xml + + + org.openjfx + javafx-controls + ${openjfx.version} + + + org.openjfx + javafx-fxml + ${openjfx.version} + + + org.openjfx + javafx-swing + ${openjfx.version} + + + + + org.testfx + testfx-core + ${testfx.version} + test + + + org.testfx + testfx-junit + ${testfx.version} + test + + + org.testfx + openjfx-monocle + jdk-12.0.1+2 + test + +``` + +### Properties (add to root `pom.xml`) +```xml +17.0.2 +4.0.18 +``` + +### Plugin (add to `jsignpdf/pom.xml`) +```xml + + org.openjfx + javafx-maven-plugin + 0.0.8 + + net.sf.jsignpdf.Signer + + +``` + +### Surefire argLine for headless TestFX +```xml +-Dtestfx.robot=glass -Dglass.platform=Monocle -Dmonocle.platform=Headless +``` + +--- + +## Testing Strategy + +### Unit Tests (plain JUnit, no FX runtime needed) +- `SigningOptionsViewModelTest` - verify `syncToOptions()` / `syncFromOptions()` round-trips all fields correctly +- `SignaturePlacementViewModelTest` - verify coordinate calculations, bounds clamping +- `DocumentViewModelTest` - verify page navigation logic, zoom level calculations + +### UI Integration Tests (TestFX) +- `MainWindowTest` - app starts, menu bar renders, toolbar present, status bar shows +- `DocumentLoadTest` - open test PDF, page count in status bar, page navigation works +- `SignaturePlacementTest` - click+drag on preview, verify rectangle coordinates on ViewModel +- `SigningWorkflowTest` - full end-to-end: open PDF, load test keystore, place sig, click Sign, verify output file has valid signature (reuses existing `PdfSignatureValidator`) + +### CI Configuration +- TestFX runs headlessly via Monocle (no display server needed) +- Existing `signing/` JUnit tests remain unchanged and keep running + +--- + +## Phased Implementation Order + +### Phase 1: Foundation +- Add OpenJFX + TestFX dependencies to pom.xml +- Create `JSignPdfApp`, `FxLauncher`, parallel launch in `Signer.java` +- Create `MainWindow.fxml` with BorderPane skeleton: menu bar, toolbar (placeholder buttons), center StackPane with "Drop PDF here" label, status bar +- Create `MainWindowController` with stub handlers +- Create `jsignpdf.css` base stylesheet +- **Verify**: JavaFX app starts and shows the empty window + +### Phase 2: PDF Viewing +- Create `PdfPageView` custom control +- Create `DocumentViewModel` (file, page count, current page, zoom level) +- Create `PdfRenderService` wrapping `Pdf2Image` + `SwingFXUtils` +- Implement File > Open (FileChooser) and drag-and-drop +- Implement page navigation (toolbar, PgUp/PgDn) +- Implement zoom (fit page, fit width, +/-) +- Show document info in status bar +- **Verify**: Can open a PDF, navigate pages, zoom + +### Phase 3: Certificate Configuration +- Create `SigningOptionsViewModel` wrapping `BasicSignerOptions` +- Create `CertificateSettings.fxml` + controller +- Bind keystore type, file, password, alias to ViewModel +- Implement "Load Keys" via `KeyStoreService` +- Wire into MainWindow via `` in the side panel Accordion +- **Verify**: Can select keystore, load keys, pick alias + +### Phase 4: Signature Placement +- Create `SignatureOverlay` with mouse drag handling + resize handles +- Create `SignaturePlacementViewModel` +- Implement "Place Signature" toggle in toolbar +- Show semi-transparent rectangle on PDF with drag/resize +- Coordinate display in side panel +- Undo placement (Ctrl+Z) +- **Verify**: Can place, move, resize, undo signature rectangle + +### Phase 5: Signing Workflow +- Create `SigningService` wrapping `SignerLogic` +- Wire "Sign" button: sync ViewModel -> options -> SigningService +- ProgressBar in status bar +- Success/failure notification +- Output console pane for log viewing +- **Verify**: Full sign workflow works, output PDF has valid signature + +### Phase 6: Advanced Settings +- Create `SignatureSettings.fxml` - render mode, L2/L4 text, images, font size, Acro6 layers +- Create `TsaSettings.fxml` - TSA, OCSP, CRL, proxy +- Create `EncryptionSettings.fxml` - encryption, passwords, rights +- Collapsed by default, "Show Advanced" toggle expands them +- **Verify**: All settings from original UI are accessible + +### Phase 7: Polish & Testing +- Write TestFX tests (MainWindowTest, DocumentLoadTest, SignaturePlacementTest, SigningWorkflowTest) +- Write ViewModel unit tests +- Signature preview (live render of stamp appearance) +- Existing signatures panel +- Keyboard shortcuts +- Final i18n pass - add keys for new UI elements +- **Verify**: All tests pass, headless CI works + +### Phase 8: Cleanup (future release) +- Remove Swing classes (SignPdfForm, VisibleSignatureDialog, TsaDialog, etc.) +- Remove `-Djsignpdf.swing` fallback flag +- Update distribution scripts + +--- + +## Critical Files + +| File | Role | Change | +|------|------|--------| +| `jsignpdf/pom.xml` | Module build | Add OpenJFX, TestFX deps + plugins | +| `pom.xml` (root) | Parent build | Add version properties | +| `Signer.java` | Entry point | Add JavaFX launch path | +| `BasicSignerOptions.java` | Model | **No change** | +| `SignerLogic.java` | Signing logic | **No change** | +| `preview/Pdf2Image.java` | PDF rendering | **No change** (reused by PdfRenderService) | +| `preview/SelectionImage.java` | Rectangle selection | **No change** (reference for SignatureOverlay) | +| `utils/ResourceProvider.java` | i18n | **No change** (wrapped by FxResourceProvider) | +| All files under `net.sf.jsignpdf.fx/` | New JavaFX GUI | **All new** | diff --git a/distribution/doc/ChangeLog.txt b/distribution/doc/ChangeLog.txt index bcd9b714..6267dd41 100644 --- a/distribution/doc/ChangeLog.txt +++ b/distribution/doc/ChangeLog.txt @@ -1,3 +1,8 @@ +2026-03-30: New JavaFX GUI with PDF preview, signature placement, and full signing workflow +2026-03-30: Add i18n support (19 languages) for the new JavaFX GUI +2026-03-30: Add headless JavaFX UI translation tests +2026-03-30: Fix NPE in KeyStoreUtils.getPkInfo when key alias is null +2026-03-30: Fix NPE in SignerLogic.signFile when getPkInfo returns null 2026-02-21: Add integration tests for the PDF signing pipeline (#292) 2026-02-21: Bump org.apache.maven.plugins:maven-assembly-plugin from 3.7.1 to 3.8.0 (#284) 2026-02-21: Bump org.apache.maven.plugins:maven-shade-plugin from 3.5.0 to 3.6.1 (#283) diff --git a/distribution/windows/JSignPdf-swing.l4j.ini b/distribution/windows/JSignPdf-swing.l4j.ini new file mode 100644 index 00000000..053c935d --- /dev/null +++ b/distribution/windows/JSignPdf-swing.l4j.ini @@ -0,0 +1,10 @@ +# Launch4j runtime config +-Xms1g +-Xmx1g +--add-exports jdk.crypto.cryptoki/sun.security.pkcs11=ALL-UNNAMED +--add-exports jdk.crypto.cryptoki/sun.security.pkcs11.wrapper=ALL-UNNAMED +--add-exports java.base/sun.security.action=ALL-UNNAMED +--add-exports java.base/sun.security.rsa=ALL-UNNAMED +--add-opens java.base/sun.security.util=ALL-UNNAMED +-Djsignpdf.home="%EXEDIR%" +-Djsignpdf.swing=true diff --git a/distribution/windows/JSignPdf.iss b/distribution/windows/JSignPdf.iss index 5bf0b6ee..fe7c92ab 100644 --- a/distribution/windows/JSignPdf.iss +++ b/distribution/windows/JSignPdf.iss @@ -20,7 +20,7 @@ VersionInfoVersion={#MyAppVersionWin} VersionInfoCompany=Josef Cacek VersionInfoDescription=JSignPdf adds digital signatures to PDF documents AppPublisher=Josef Cacek -AppSupportURL=http://jsignpdf.sourceforge.net/ +AppSupportURL=http://intoolswetrust.github.io/jsignpdf/ AppVersion={#MyAppVersion} OutputDir={#OutputDir} ;WizardStyle=modern @@ -31,6 +31,7 @@ UninstallDisplayIcon={app}\JSignPdf.exe [Icons] Name: {group}\JSignPdf {#MyAppVersion}; Filename: {app}\JSignPdf.exe; Components: ; WorkingDir: {app} +Name: {group}\JSignPdf {#MyAppVersion} (Swing); Filename: {app}\JSignPdf-swing.exe; Components: ; WorkingDir: {app} Name: {group}\InstallCert Tool; Filename: {app}\InstallCert.exe; Components: ; WorkingDir: {app} Name: {group}\JSignPdf Guide; Filename: {app}\docs\JSignPdf.pdf; Components: Name: {group}\Uninstall {#MyAppName}; Filename: {uninstallexe}; Components: diff --git a/distribution/windows/ant-build-create-launchers.xml b/distribution/windows/ant-build-create-launchers.xml index 72765fcc..f89f43eb 100644 --- a/distribution/windows/ant-build-create-launchers.xml +++ b/distribution/windows/ant-build-create-launchers.xml @@ -33,6 +33,24 @@ + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${argLine} + -Dglass.platform=Monocle -Dmonocle.platform=Headless + -Dprism.order=sw + + + org.apache.maven.plugins maven-dependency-plugin @@ -84,6 +95,23 @@ + + + org.openjfx + javafx-controls + ${openjfx.version} + + + org.openjfx + javafx-fxml + ${openjfx.version} + + + org.openjfx + javafx-swing + ${openjfx.version} + + com.github.librepdf openpdf @@ -124,5 +152,11 @@ com.github.kwart.jsign jsign-pkcs11 + + org.testfx + openjfx-monocle + 11.0.2 + test + diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java b/jsignpdf/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java index 6c26b993..04bea84c 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java @@ -31,7 +31,9 @@ import java.net.InetSocketAddress; import java.net.Proxy; +import java.util.Arrays; import java.util.Locale; +import java.util.Objects; import net.sf.jsignpdf.types.CertificationLevel; import net.sf.jsignpdf.types.HashAlgorithm; @@ -47,14 +49,9 @@ /** * Options for PDF signer. - * - * @author Josef Cacek */ public class BasicSignerOptions { - // private final static Logger LOGGER = - // Logger.getLogger(BasicSignerOptions.class); - protected final PropertyProvider props = PropertyProvider.getInstance(); protected final JSignEncryptor encryptor = new JSignEncryptor(); @@ -289,10 +286,10 @@ public void storeOptions() { props.setProperty(Constants.PROPERTY_STOREPWD, isStorePasswords()); setEncrypted(Constants.EPROPERTY_USERHOME, Constants.USER_HOME); if (isStorePasswords()) { - setEncrypted(Constants.EPROPERTY_KS_PWD, new String(getKsPasswd())); - setEncrypted(Constants.EPROPERTY_KEY_PWD, new String(getKeyPasswd())); - setEncrypted(Constants.EPROPERTY_OWNER_PWD, new String(getPdfOwnerPwd())); - setEncrypted(Constants.EPROPERTY_USER_PWD, new String(getPdfUserPwd())); + setEncrypted(Constants.EPROPERTY_KS_PWD, getKsPasswdStr()); + setEncrypted(Constants.EPROPERTY_KEY_PWD, getKeyPasswdStr()); + setEncrypted(Constants.EPROPERTY_OWNER_PWD, getPdfOwnerPwdStr()); + setEncrypted(Constants.EPROPERTY_USER_PWD, getPdfUserPwdStr()); setEncrypted(Constants.EPROPERTY_TSA_PWD, getTsaPasswd()); setEncrypted(Constants.EPROPERTY_TSA_CERT_PWD, getTsaCertFilePwd()); } else { @@ -1198,4 +1195,138 @@ protected void setCmdLine(String[] cmdLine) { this.cmdLine = cmdLine; } + /** + * Creates a shallow copy of this options instance. Enum and String fields are + * effectively immutable, so shallow copy is sufficient for thread-safety. + * char[] fields are defensively copied. + * + * @return a new BasicSignerOptions with the same field values + */ + public BasicSignerOptions createCopy() { + BasicSignerOptions copy = new BasicSignerOptions(); + copy.setKsType(getKsType()); + copy.setKsFile(getKsFile()); + copy.setKsPasswd(getKsPasswd() != null ? getKsPasswd().clone() : null); + copy.setKeyAlias(getKeyAlias()); + copy.setKeyIndex(getKeyIndex()); + copy.setKeyPasswd(getKeyPasswd() != null ? getKeyPasswd().clone() : null); + copy.setInFile(getInFile()); + copy.setOutFile(getOutFile()); + copy.setSignerName(getSignerName()); + copy.setReason(getReason()); + copy.setLocation(getLocation()); + copy.setContact(getContact()); + copy.setListener(getListener()); + copy.setAppend(isAppend()); + copy.setAdvanced(isAdvanced()); + copy.setPdfEncryption(getPdfEncryption()); + copy.setPdfOwnerPwd(getPdfOwnerPwd() != null ? getPdfOwnerPwd().clone() : null); + copy.setPdfUserPwd(getPdfUserPwd() != null ? getPdfUserPwd().clone() : null); + copy.setPdfEncryptionCertFile(getPdfEncryptionCertFile()); + copy.setCertLevel(getCertLevel()); + copy.setHashAlgorithm(getHashAlgorithm()); + copy.setStorePasswords(isStorePasswords()); + copy.setRightPrinting(getRightPrinting()); + copy.setRightCopy(isRightCopy()); + copy.setRightAssembly(isRightAssembly()); + copy.setRightFillIn(isRightFillIn()); + copy.setRightScreanReaders(isRightScreanReaders()); + copy.setRightModifyAnnotations(isRightModifyAnnotations()); + copy.setRightModifyContents(isRightModifyContents()); + copy.setVisible(isVisible()); + copy.setPage(getPage()); + copy.setPositionLLX(getPositionLLX()); + copy.setPositionLLY(getPositionLLY()); + copy.setPositionURX(getPositionURX()); + copy.setPositionURY(getPositionURY()); + copy.setBgImgScale(getBgImgScale()); + copy.setRenderMode(getRenderMode()); + copy.setL2Text(getL2Text()); + copy.setL4Text(getL4Text()); + copy.setL2TextFontSize(getL2TextFontSize()); + copy.setImgPath(getImgPath()); + copy.setBgImgPath(getBgImgPath()); + copy.setAcro6Layers(isAcro6Layers()); + copy.setTimestamp(isTimestamp()); + copy.setTsaUrl(getTsaUrl()); + copy.setTsaServerAuthn(getTsaServerAuthn()); + copy.setTsaUser(getTsaUser()); + copy.setTsaPasswd(getTsaPasswd()); + copy.setTsaCertFileType(getTsaCertFileType()); + copy.setTsaCertFile(getTsaCertFile()); + copy.setTsaCertFilePwd(getTsaCertFilePwd()); + copy.setTsaPolicy(getTsaPolicy()); + copy.setTsaHashAlg(getTsaHashAlg()); + copy.setOcspEnabled(isOcspEnabled()); + copy.setOcspServerUrl(getOcspServerUrl()); + copy.setCrlEnabled(isCrlEnabled()); + copy.setProxyType(getProxyType()); + copy.setProxyHost(getProxyHost()); + copy.setProxyPort(getProxyPort()); + return copy; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(cmdLine); + result = prime * result + Arrays.hashCode(keyPasswd); + result = prime * result + Arrays.hashCode(ksPasswd); + result = prime * result + Arrays.hashCode(pdfOwnerPwd); + result = prime * result + Arrays.hashCode(pdfUserPwd); + result = prime * result + Objects.hash(acro6Layers, advanced, append, bgImgPath, bgImgScale, certLevel, contact, + crlEnabled, encryptor, hashAlgorithm, imgPath, inFile, keyAlias, keyIndex, ksFile, ksType, l2Text, + l2TextFontSize, l4Text, listener, location, ocspEnabled, ocspServerUrl, outFile, page, pdfEncryption, + pdfEncryptionCertFile, positionLLX, positionLLY, positionURX, positionURY, propertiesFilePath, props, proxyHost, + proxyPort, proxyType, reason, renderMode, rightAssembly, rightCopy, rightFillIn, rightModifyAnnotations, + rightModifyContents, rightPrinting, rightScreanReaders, signerName, storePasswords, timestamp, tsaCertFile, + tsaCertFilePwd, tsaCertFileType, tsaHashAlg, tsaPasswd, tsaPolicy, tsaServerAuthn, tsaUrl, tsaUser, visible); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + BasicSignerOptions other = (BasicSignerOptions) obj; + return acro6Layers == other.acro6Layers && advanced == other.advanced && append == other.append + && Objects.equals(bgImgPath, other.bgImgPath) + && Float.floatToIntBits(bgImgScale) == Float.floatToIntBits(other.bgImgScale) && certLevel == other.certLevel + && Arrays.equals(cmdLine, other.cmdLine) && Objects.equals(contact, other.contact) + && crlEnabled == other.crlEnabled && Objects.equals(encryptor, other.encryptor) + && hashAlgorithm == other.hashAlgorithm && Objects.equals(imgPath, other.imgPath) + && Objects.equals(inFile, other.inFile) && Objects.equals(keyAlias, other.keyAlias) + && keyIndex == other.keyIndex && Arrays.equals(keyPasswd, other.keyPasswd) + && Objects.equals(ksFile, other.ksFile) && Arrays.equals(ksPasswd, other.ksPasswd) + && Objects.equals(ksType, other.ksType) && Objects.equals(l2Text, other.l2Text) + && Float.floatToIntBits(l2TextFontSize) == Float.floatToIntBits(other.l2TextFontSize) + && Objects.equals(l4Text, other.l4Text) && Objects.equals(listener, other.listener) + && Objects.equals(location, other.location) && ocspEnabled == other.ocspEnabled + && Objects.equals(ocspServerUrl, other.ocspServerUrl) && Objects.equals(outFile, other.outFile) + && page == other.page && pdfEncryption == other.pdfEncryption + && Objects.equals(pdfEncryptionCertFile, other.pdfEncryptionCertFile) + && Arrays.equals(pdfOwnerPwd, other.pdfOwnerPwd) && Arrays.equals(pdfUserPwd, other.pdfUserPwd) + && Float.floatToIntBits(positionLLX) == Float.floatToIntBits(other.positionLLX) + && Float.floatToIntBits(positionLLY) == Float.floatToIntBits(other.positionLLY) + && Float.floatToIntBits(positionURX) == Float.floatToIntBits(other.positionURX) + && Float.floatToIntBits(positionURY) == Float.floatToIntBits(other.positionURY) + && Objects.equals(propertiesFilePath, other.propertiesFilePath) && Objects.equals(props, other.props) + && Objects.equals(proxyHost, other.proxyHost) && proxyPort == other.proxyPort && proxyType == other.proxyType + && Objects.equals(reason, other.reason) && renderMode == other.renderMode + && rightAssembly == other.rightAssembly && rightCopy == other.rightCopy && rightFillIn == other.rightFillIn + && rightModifyAnnotations == other.rightModifyAnnotations && rightModifyContents == other.rightModifyContents + && rightPrinting == other.rightPrinting && rightScreanReaders == other.rightScreanReaders + && Objects.equals(signerName, other.signerName) && storePasswords == other.storePasswords + && timestamp == other.timestamp && Objects.equals(tsaCertFile, other.tsaCertFile) + && Objects.equals(tsaCertFilePwd, other.tsaCertFilePwd) + && Objects.equals(tsaCertFileType, other.tsaCertFileType) && Objects.equals(tsaHashAlg, other.tsaHashAlg) + && Objects.equals(tsaPasswd, other.tsaPasswd) && Objects.equals(tsaPolicy, other.tsaPolicy) + && tsaServerAuthn == other.tsaServerAuthn && Objects.equals(tsaUrl, other.tsaUrl) + && Objects.equals(tsaUser, other.tsaUser) && visible == other.visible; + } } diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/Constants.java b/jsignpdf/src/main/java/net/sf/jsignpdf/Constants.java index f83b7f0e..74dfaadd 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/Constants.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/Constants.java @@ -208,6 +208,8 @@ public class Constants { public static final String PROPERTY_PROXY_HOST = "proxy.host"; public static final String PROPERTY_PROXY_PORT = "proxy.port"; + public static final String PROPERTY_RECENT_FILE_PREFIX = "recentFile."; + /** * Property name. */ diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/Signer.java b/jsignpdf/src/main/java/net/sf/jsignpdf/Signer.java index c1b5a9ee..1fbefec7 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/Signer.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/Signer.java @@ -49,6 +49,7 @@ import javax.swing.UIManager; import javax.swing.WindowConstants; +import net.sf.jsignpdf.fx.FxLauncher; import net.sf.jsignpdf.ssl.SSLInitializer; import net.sf.jsignpdf.utils.ConfigProvider; import net.sf.jsignpdf.utils.GuiUtils; @@ -151,15 +152,21 @@ public static void main(String[] args) { } if (showGui) { - try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } catch (Exception e) { - System.err.println("Can't set Look&Feel."); + if (Boolean.getBoolean("jsignpdf.swing")) { + // Legacy Swing GUI (preserved for fallback) + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + System.err.println("Can't set Look&Feel."); + } + SignPdfForm tmpForm = new SignPdfForm(WindowConstants.EXIT_ON_CLOSE, tmpOpts); + tmpForm.pack(); + GuiUtils.center(tmpForm); + tmpForm.setVisible(true); + } else { + // New JavaFX GUI (default) + FxLauncher.launch(tmpOpts); } - SignPdfForm tmpForm = new SignPdfForm(WindowConstants.EXIT_ON_CLOSE, tmpOpts); - tmpForm.pack(); - GuiUtils.center(tmpForm); - tmpForm.setVisible(true); } } diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/SignerLogic.java b/jsignpdf/src/main/java/net/sf/jsignpdf/SignerLogic.java index 56a4f9fa..7c08e2ee 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/SignerLogic.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/SignerLogic.java @@ -152,6 +152,10 @@ public boolean signFile() { } } else { pkInfo = KeyStoreUtils.getPkInfo(options); + if (pkInfo == null) { + LOGGER.info(RES.get("console.certificateChainEmpty")); + return false; + } key = pkInfo.getKey(); chain = pkInfo.getChain(); } diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/FxLauncher.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/FxLauncher.java new file mode 100644 index 00000000..b5b5d682 --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/FxLauncher.java @@ -0,0 +1,22 @@ +package net.sf.jsignpdf.fx; + +import net.sf.jsignpdf.BasicSignerOptions; + +/** + * Static launcher for the JavaFX GUI, called from {@link net.sf.jsignpdf.Signer#main}. + */ +public final class FxLauncher { + + private FxLauncher() { + } + + /** + * Launches the JavaFX application with the given initial options. + * + * @param opts initial signer options (may be null) + */ + public static void launch(BasicSignerOptions opts) { + JSignPdfApp.setInitialOptions(opts); + javafx.application.Application.launch(JSignPdfApp.class); + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/JSignPdfApp.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/JSignPdfApp.java new file mode 100644 index 00000000..fbd04873 --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/JSignPdfApp.java @@ -0,0 +1,55 @@ +package net.sf.jsignpdf.fx; + +import java.util.ResourceBundle; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.stage.Stage; +import net.sf.jsignpdf.BasicSignerOptions; +import net.sf.jsignpdf.Constants; +import net.sf.jsignpdf.fx.view.MainWindowController; + +/** + * JavaFX Application entry point for JSignPdf. + */ +public class JSignPdfApp extends Application { + + private static BasicSignerOptions initialOptions; + + static void setInitialOptions(BasicSignerOptions opts) { + initialOptions = opts; + } + + @Override + public void start(Stage primaryStage) throws Exception { + ResourceBundle bundle = ResourceBundle.getBundle(Constants.RESOURCE_BUNDLE_BASE); + FXMLLoader loader = new FXMLLoader( + getClass().getResource("/net/sf/jsignpdf/fx/view/MainWindow.fxml"), bundle); + Parent root = loader.load(); + + MainWindowController controller = loader.getController(); + controller.setStage(primaryStage); + + // Create options and load persisted configuration + BasicSignerOptions opts = initialOptions != null ? initialOptions : new BasicSignerOptions(); + opts.loadOptions(); + controller.initFromOptions(opts); + + Scene scene = new Scene(root, 1100, 750); + scene.getStylesheets().add( + getClass().getResource("/net/sf/jsignpdf/fx/styles/jsignpdf.css").toExternalForm()); + + primaryStage.setTitle("JSignPdf " + Constants.VERSION); + primaryStage.getIcons().add( + new Image(getClass().getResourceAsStream("/net/sf/jsignpdf/signedpdf32.png"))); + primaryStage.setScene(scene); + + // Store configuration on window close + primaryStage.setOnCloseRequest(event -> controller.storeAndCleanup()); + + primaryStage.show(); + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/control/PdfPageView.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/control/PdfPageView.java new file mode 100644 index 00000000..71911a3d --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/control/PdfPageView.java @@ -0,0 +1,63 @@ +package net.sf.jsignpdf.fx.control; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Region; + +/** + * Custom Region that displays a rendered PDF page with zoom support. + * The image is scaled by the zoomLevel property. + */ +public class PdfPageView extends Region { + + private final ImageView imageView = new ImageView(); + private final ObjectProperty pageImage = new SimpleObjectProperty<>(); + private final DoubleProperty zoomLevel = new SimpleDoubleProperty(1.0); + + public PdfPageView() { + getChildren().add(imageView); + imageView.setPreserveRatio(true); + imageView.setSmooth(true); + + // Bind image + imageView.imageProperty().bind(pageImage); + + // Update size when image or zoom changes + pageImage.addListener((obs, o, n) -> updateSize()); + zoomLevel.addListener((obs, o, n) -> updateSize()); + + getStyleClass().add("pdf-page-view"); + } + + private void updateSize() { + Image img = pageImage.get(); + if (img != null) { + double zoom = zoomLevel.get(); + double w = img.getWidth() * zoom; + double h = img.getHeight() * zoom; + imageView.setFitWidth(w); + imageView.setFitHeight(h); + setPrefSize(w, h); + setMinSize(w, h); + setMaxSize(w, h); + } + } + + @Override + protected void layoutChildren() { + imageView.relocate(0, 0); + } + + // --- Properties --- + public ObjectProperty pageImageProperty() { return pageImage; } + public Image getPageImage() { return pageImage.get(); } + public void setPageImage(Image image) { pageImage.set(image); } + + public DoubleProperty zoomLevelProperty() { return zoomLevel; } + public double getZoomLevel() { return zoomLevel.get(); } + public void setZoomLevel(double zoom) { zoomLevel.set(zoom); } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/control/SignatureOverlay.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/control/SignatureOverlay.java new file mode 100644 index 00000000..e43bf33a --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/control/SignatureOverlay.java @@ -0,0 +1,268 @@ +package net.sf.jsignpdf.fx.control; + +import javafx.beans.binding.Bindings; +import javafx.scene.Cursor; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import net.sf.jsignpdf.fx.viewmodel.SignaturePlacementViewModel; + +/** + * Transparent overlay pane for click-drag signature rectangle placement + * on top of the PDF page view. + */ +public class SignatureOverlay extends Pane { + + private static final double HANDLE_SIZE = 8; + + private final SignaturePlacementViewModel viewModel; + private final Rectangle sigRect = new Rectangle(); + private final Rectangle[] handles = new Rectangle[4]; // corners: TL, TR, BL, BR + + // Drag state + private double dragStartX, dragStartY; + private double dragStartRelX, dragStartRelY, dragStartRelW, dragStartRelH; + private DragMode dragMode = DragMode.NONE; + + private enum DragMode { NONE, CREATE, MOVE, RESIZE_TL, RESIZE_TR, RESIZE_BL, RESIZE_BR } + + public SignatureOverlay(SignaturePlacementViewModel viewModel) { + this.viewModel = viewModel; + setPickOnBounds(true); + + // Setup signature rectangle + sigRect.getStyleClass().add("signature-rect"); + sigRect.setVisible(false); + getChildren().add(sigRect); + + // Setup corner handles + for (int i = 0; i < 4; i++) { + handles[i] = new Rectangle(HANDLE_SIZE, HANDLE_SIZE); + handles[i].getStyleClass().add("signature-handle"); + handles[i].setVisible(false); + getChildren().add(handles[i]); + } + + // Mouse handlers + setOnMousePressed(this::onMousePressed); + setOnMouseDragged(this::onMouseDragged); + setOnMouseReleased(this::onMouseReleased); + setOnMouseMoved(this::onMouseMoved); + + // Bind visibility to placed state + viewModel.placedProperty().addListener((obs, o, n) -> { + sigRect.setVisible(n); + for (Rectangle h : handles) h.setVisible(n); + if (!n) setCursor(Cursor.DEFAULT); + }); + + // Bind rectangle position/size to ViewModel + viewModel.relXProperty().addListener((obs, o, n) -> updateRectPosition()); + viewModel.relYProperty().addListener((obs, o, n) -> updateRectPosition()); + viewModel.relWidthProperty().addListener((obs, o, n) -> updateRectPosition()); + viewModel.relHeightProperty().addListener((obs, o, n) -> updateRectPosition()); + + // Update when overlay size changes + widthProperty().addListener((obs, o, n) -> updateRectPosition()); + heightProperty().addListener((obs, o, n) -> updateRectPosition()); + } + + private void updateRectPosition() { + double w = getWidth(); + double h = getHeight(); + if (w <= 0 || h <= 0) return; + + double rx = viewModel.getRelX() * w; + double ry = viewModel.getRelY() * h; + double rw = viewModel.getRelWidth() * w; + double rh = viewModel.getRelHeight() * h; + + sigRect.setX(rx); + sigRect.setY(ry); + sigRect.setWidth(rw); + sigRect.setHeight(rh); + + // TL + handles[0].setX(rx - HANDLE_SIZE / 2); + handles[0].setY(ry - HANDLE_SIZE / 2); + // TR + handles[1].setX(rx + rw - HANDLE_SIZE / 2); + handles[1].setY(ry - HANDLE_SIZE / 2); + // BL + handles[2].setX(rx - HANDLE_SIZE / 2); + handles[2].setY(ry + rh - HANDLE_SIZE / 2); + // BR + handles[3].setX(rx + rw - HANDLE_SIZE / 2); + handles[3].setY(ry + rh - HANDLE_SIZE / 2); + } + + private void onMousePressed(MouseEvent e) { + double mx = e.getX(); + double my = e.getY(); + double w = getWidth(); + double h = getHeight(); + + dragStartX = mx; + dragStartY = my; + dragStartRelX = viewModel.getRelX(); + dragStartRelY = viewModel.getRelY(); + dragStartRelW = viewModel.getRelWidth(); + dragStartRelH = viewModel.getRelHeight(); + + if (viewModel.isPlaced()) { + // Check if clicking on a handle + DragMode handleMode = getHandleAt(mx, my); + if (handleMode != DragMode.NONE) { + dragMode = handleMode; + e.consume(); + return; + } + // Check if clicking inside the rectangle (move) + double rx = viewModel.getRelX() * w; + double ry = viewModel.getRelY() * h; + double rw = viewModel.getRelWidth() * w; + double rh = viewModel.getRelHeight() * h; + if (mx >= rx && mx <= rx + rw && my >= ry && my <= ry + rh) { + dragMode = DragMode.MOVE; + e.consume(); + return; + } + } + + // Require Shift to replace an existing rectangle + if (viewModel.isPlaced() && !e.isShiftDown()) { + dragMode = DragMode.NONE; + e.consume(); + return; + } + + // Start creating a new rectangle + dragMode = DragMode.CREATE; + viewModel.setRelX(mx / w); + viewModel.setRelY(my / h); + viewModel.setRelWidth(0.02); + viewModel.setRelHeight(0.02); + // Update drag start values to match the new rectangle + dragStartRelW = 0.02; + dragStartRelH = 0.02; + viewModel.setPlaced(true); + e.consume(); + } + + private void onMouseDragged(MouseEvent e) { + if (dragMode == DragMode.NONE) return; + + double mx = e.getX(); + double my = e.getY(); + double w = getWidth(); + double h = getHeight(); + double dx = (mx - dragStartX) / w; + double dy = (my - dragStartY) / h; + + switch (dragMode) { + case CREATE: + // Normalize origin and extent so dragging in any direction works + double endRelX = mx / w; + double endRelY = my / h; + double originX = Math.min(dragStartX / w, endRelX); + double originY = Math.min(dragStartY / h, endRelY); + double extentW = Math.abs(endRelX - dragStartX / w); + double extentH = Math.abs(endRelY - dragStartY / h); + viewModel.setRelX(originX); + viewModel.setRelY(originY); + viewModel.setRelWidth(Math.max(0.02, extentW)); + viewModel.setRelHeight(Math.max(0.02, extentH)); + break; + case RESIZE_BR: + viewModel.setRelWidth(Math.max(0.02, dragStartRelW + dx)); + viewModel.setRelHeight(Math.max(0.02, dragStartRelH + dy)); + break; + case MOVE: + viewModel.setRelX(clamp(dragStartRelX + dx, 0, 1 - viewModel.getRelWidth())); + viewModel.setRelY(clamp(dragStartRelY + dy, 0, 1 - viewModel.getRelHeight())); + break; + case RESIZE_TL: + viewModel.setRelX(clamp(dragStartRelX + dx, 0, dragStartRelX + dragStartRelW - 0.02)); + viewModel.setRelY(clamp(dragStartRelY + dy, 0, dragStartRelY + dragStartRelH - 0.02)); + viewModel.setRelWidth(dragStartRelW - (viewModel.getRelX() - dragStartRelX)); + viewModel.setRelHeight(dragStartRelH - (viewModel.getRelY() - dragStartRelY)); + break; + case RESIZE_TR: + viewModel.setRelWidth(Math.max(0.02, dragStartRelW + dx)); + viewModel.setRelY(clamp(dragStartRelY + dy, 0, dragStartRelY + dragStartRelH - 0.02)); + viewModel.setRelHeight(dragStartRelH - (viewModel.getRelY() - dragStartRelY)); + break; + case RESIZE_BL: + viewModel.setRelX(clamp(dragStartRelX + dx, 0, dragStartRelX + dragStartRelW - 0.02)); + viewModel.setRelWidth(dragStartRelW - (viewModel.getRelX() - dragStartRelX)); + viewModel.setRelHeight(Math.max(0.02, dragStartRelH + dy)); + break; + default: + break; + } + e.consume(); + } + + private void onMouseReleased(MouseEvent e) { + dragMode = DragMode.NONE; + e.consume(); + } + + private void onMouseMoved(MouseEvent e) { + if (!viewModel.isPlaced()) { + setCursor(Cursor.CROSSHAIR); + return; + } + + DragMode handleMode = getHandleAt(e.getX(), e.getY()); + switch (handleMode) { + case RESIZE_TL: + case RESIZE_BR: + setCursor(Cursor.NW_RESIZE); + break; + case RESIZE_TR: + case RESIZE_BL: + setCursor(Cursor.NE_RESIZE); + break; + default: + double w = getWidth(); + double h = getHeight(); + double rx = viewModel.getRelX() * w; + double ry = viewModel.getRelY() * h; + double rw = viewModel.getRelWidth() * w; + double rh = viewModel.getRelHeight() * h; + if (e.getX() >= rx && e.getX() <= rx + rw && e.getY() >= ry && e.getY() <= ry + rh) { + setCursor(Cursor.MOVE); + } else { + setCursor(Cursor.CROSSHAIR); + } + break; + } + } + + private DragMode getHandleAt(double mx, double my) { + if (!viewModel.isPlaced()) return DragMode.NONE; + double tolerance = HANDLE_SIZE; + double w = getWidth(); + double h = getHeight(); + double rx = viewModel.getRelX() * w; + double ry = viewModel.getRelY() * h; + double rw = viewModel.getRelWidth() * w; + double rh = viewModel.getRelHeight() * h; + + if (near(mx, my, rx, ry, tolerance)) return DragMode.RESIZE_TL; + if (near(mx, my, rx + rw, ry, tolerance)) return DragMode.RESIZE_TR; + if (near(mx, my, rx, ry + rh, tolerance)) return DragMode.RESIZE_BL; + if (near(mx, my, rx + rw, ry + rh, tolerance)) return DragMode.RESIZE_BR; + return DragMode.NONE; + } + + private static boolean near(double x1, double y1, double x2, double y2, double tol) { + return Math.abs(x1 - x2) < tol && Math.abs(y1 - y2) < tol; + } + + private static double clamp(double v, double min, double max) { + return Math.max(min, Math.min(max, v)); + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/service/KeyStoreService.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/service/KeyStoreService.java new file mode 100644 index 00000000..38d9c824 --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/service/KeyStoreService.java @@ -0,0 +1,29 @@ +package net.sf.jsignpdf.fx.service; + +import javafx.concurrent.Service; +import javafx.concurrent.Task; +import net.sf.jsignpdf.BasicSignerOptions; +import net.sf.jsignpdf.utils.KeyStoreUtils; + +/** + * Background service that loads key aliases from a keystore. + */ +public class KeyStoreService extends Service { + + private BasicSignerOptions options; + + public void setOptions(BasicSignerOptions options) { + this.options = options; + } + + @Override + protected Task createTask() { + final BasicSignerOptions taskOptions = this.options; + return new Task() { + @Override + protected String[] call() { + return KeyStoreUtils.getKeyAliases(taskOptions); + } + }; + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/service/PdfRenderService.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/service/PdfRenderService.java new file mode 100644 index 00000000..bcd5a1cc --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/service/PdfRenderService.java @@ -0,0 +1,51 @@ +package net.sf.jsignpdf.fx.service; + +import java.awt.image.BufferedImage; + +import javafx.concurrent.Service; +import javafx.concurrent.Task; +import javafx.scene.image.Image; +import net.sf.jsignpdf.BasicSignerOptions; +import net.sf.jsignpdf.fx.util.SwingFxImageConverter; +import net.sf.jsignpdf.preview.Pdf2Image; + +/** + * Background service that renders a PDF page to a JavaFX Image using the existing Pdf2Image class. + */ +public class PdfRenderService extends Service { + + private BasicSignerOptions options; + private int page = 1; + + public void setOptions(BasicSignerOptions options) { + this.options = options; + } + + public void setPage(int page) { + this.page = page; + } + + @Override + protected Task createTask() { + final BasicSignerOptions taskOptions = this.options; + final int taskPage = this.page; + + return new Task() { + @Override + protected Image call() { + // JavaFX Service.cancel() interrupts the running thread. When the service is + // restarted (cancel → reset → start), the new task may inherit a stale interrupt + // flag on the thread because the FX executor can reuse the same pool thread. + // Pdf2Image uses blocking I/O (e.g., RandomAccessFile) that throws + // ClosedByInterruptException if the flag is set, causing the render to fail + // immediately. Clearing the flag here is safe: a "real" interrupt between + // cancel() and this point would only mean a redundant cancellation of an already- + // cancelled cycle — the next restart will set up a fresh task regardless. + Thread.interrupted(); + Pdf2Image p2i = new Pdf2Image(taskOptions); + BufferedImage buffered = p2i.getImageForPage(taskPage); + return SwingFxImageConverter.toFxImage(buffered); + } + }; + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/service/SigningService.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/service/SigningService.java new file mode 100644 index 00000000..98824db0 --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/service/SigningService.java @@ -0,0 +1,30 @@ +package net.sf.jsignpdf.fx.service; + +import javafx.concurrent.Service; +import javafx.concurrent.Task; +import net.sf.jsignpdf.BasicSignerOptions; +import net.sf.jsignpdf.SignerLogic; + +/** + * Background service that wraps SignerLogic.signFile() for JavaFX. + */ +public class SigningService extends Service { + + private BasicSignerOptions options; + + public void setOptions(BasicSignerOptions options) { + this.options = options; + } + + @Override + protected Task createTask() { + final BasicSignerOptions taskOptions = this.options; + return new Task() { + @Override + protected Boolean call() { + SignerLogic logic = new SignerLogic(taskOptions); + return logic.signFile(); + } + }; + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/util/FxResourceProvider.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/util/FxResourceProvider.java new file mode 100644 index 00000000..3a784a96 --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/util/FxResourceProvider.java @@ -0,0 +1,45 @@ +package net.sf.jsignpdf.fx.util; + +import javafx.beans.binding.StringBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import net.sf.jsignpdf.Constants; +import net.sf.jsignpdf.utils.ResourceProvider; + +/** + * Adapts ResourceProvider for JavaFX StringBindings, + * enabling easy binding of i18n strings to JavaFX UI properties. + */ +public final class FxResourceProvider { + + private static final ResourceProvider RES = Constants.RES; + + private FxResourceProvider() { + } + + /** + * Get a localized string by key. + */ + public static String get(String key) { + return RES.get(key); + } + + /** + * Get a localized string with parameters. + */ + public static String get(String key, String... args) { + return RES.get(key, args); + } + + /** + * Create a StringBinding for a resource key. + */ + public static StringBinding createStringBinding(String key) { + return new StringBinding() { + @Override + protected String computeValue() { + return RES.get(key); + } + }; + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/util/RecentFilesManager.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/util/RecentFilesManager.java new file mode 100644 index 00000000..bd578112 --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/util/RecentFilesManager.java @@ -0,0 +1,49 @@ +package net.sf.jsignpdf.fx.util; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import net.sf.jsignpdf.Constants; +import net.sf.jsignpdf.utils.PropertyProvider; + +/** + * Manages a list of recently opened PDF files, persisted via PropertyProvider. + */ +public class RecentFilesManager { + + private static final int MAX_RECENT = 10; + private final PropertyProvider props = PropertyProvider.getInstance(); + + public void addFile(File file) { + List files = getRecentFiles(); + String path = file.getAbsolutePath(); + files.remove(path); + files.add(0, path); + while (files.size() > MAX_RECENT) { + files.remove(files.size() - 1); + } + saveFiles(files); + } + + public List getRecentFiles() { + List result = new ArrayList<>(); + for (int i = 0; i < MAX_RECENT; i++) { + String path = props.getProperty(Constants.PROPERTY_RECENT_FILE_PREFIX + i); + if (path != null && !path.isEmpty() && new File(path).exists()) { + result.add(path); + } + } + return result; + } + + private void saveFiles(List files) { + for (int i = 0; i < MAX_RECENT; i++) { + if (i < files.size()) { + props.setProperty(Constants.PROPERTY_RECENT_FILE_PREFIX + i, files.get(i)); + } else { + props.removeProperty(Constants.PROPERTY_RECENT_FILE_PREFIX + i); + } + } + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/util/SwingFxImageConverter.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/util/SwingFxImageConverter.java new file mode 100644 index 00000000..972b2088 --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/util/SwingFxImageConverter.java @@ -0,0 +1,22 @@ +package net.sf.jsignpdf.fx.util; + +import java.awt.image.BufferedImage; + +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.image.Image; + +/** + * Utility for converting between AWT BufferedImage and JavaFX Image. + */ +public final class SwingFxImageConverter { + + private SwingFxImageConverter() { + } + + public static Image toFxImage(BufferedImage bufferedImage) { + if (bufferedImage == null) { + return null; + } + return SwingFXUtils.toFXImage(bufferedImage, null); + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/CertificateSettingsController.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/CertificateSettingsController.java new file mode 100644 index 00000000..a5564ccd --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/CertificateSettingsController.java @@ -0,0 +1,101 @@ +package net.sf.jsignpdf.fx.view; + +import java.io.File; +import java.util.logging.Level; + +import javafx.collections.FXCollections; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.stage.FileChooser; +import net.sf.jsignpdf.BasicSignerOptions; +import net.sf.jsignpdf.fx.service.KeyStoreService; +import net.sf.jsignpdf.fx.viewmodel.SigningOptionsViewModel; +import net.sf.jsignpdf.utils.KeyStoreUtils; + +import static net.sf.jsignpdf.Constants.LOGGER; +import static net.sf.jsignpdf.Constants.RES; + +/** + * Controller for the certificate/keystore settings panel. + */ +public class CertificateSettingsController { + + @FXML private ComboBox cmbKeystoreType; + @FXML private TextField txtKeystoreFile; + @FXML private Button btnBrowseKeystore; + @FXML private PasswordField txtKeystorePassword; + @FXML private Button btnLoadKeys; + @FXML private ComboBox cmbKeyAlias; + @FXML private PasswordField txtKeyPassword; + @FXML private CheckBox chkStorePasswords; + + private SigningOptionsViewModel viewModel; + private final KeyStoreService keyStoreService = new KeyStoreService(); + + @FXML + private void initialize() { + // Populate keystore types + cmbKeystoreType.setItems(FXCollections.observableArrayList(KeyStoreUtils.getKeyStores())); + + // Setup key loading service callbacks + keyStoreService.setOnSucceeded(e -> { + String[] aliases = keyStoreService.getValue(); + cmbKeyAlias.setItems(FXCollections.observableArrayList(aliases)); + if (aliases.length > 0) { + cmbKeyAlias.getSelectionModel().selectFirst(); + } + }); + keyStoreService.setOnFailed(e -> { + LOGGER.log(Level.WARNING, "Failed to load key aliases", keyStoreService.getException()); + cmbKeyAlias.getItems().clear(); + }); + } + + public void setViewModel(SigningOptionsViewModel vm) { + this.viewModel = vm; + bindToViewModel(); + } + + private void bindToViewModel() { + // Bidirectional bindings + cmbKeystoreType.valueProperty().bindBidirectional(viewModel.ksTypeProperty()); + txtKeystoreFile.textProperty().bindBidirectional(viewModel.ksFileProperty()); + txtKeystorePassword.textProperty().bindBidirectional(viewModel.ksPasswordProperty()); + cmbKeyAlias.valueProperty().bindBidirectional(viewModel.keyAliasProperty()); + txtKeyPassword.textProperty().bindBidirectional(viewModel.keyPasswordProperty()); + chkStorePasswords.selectedProperty().bindBidirectional(viewModel.storePasswordsProperty()); + } + + @FXML + private void onBrowseKeystore() { + FileChooser fc = new FileChooser(); + fc.setTitle(RES.get("jfx.gui.dialog.selectKeystoreFile")); + fc.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("All Files", "*.*"), + new FileChooser.ExtensionFilter("PKCS12", "*.p12", "*.pfx"), + new FileChooser.ExtensionFilter("JKS", "*.jks")); + File file = fc.showOpenDialog(txtKeystoreFile.getScene().getWindow()); + if (file != null) { + txtKeystoreFile.setText(file.getAbsolutePath()); + } + } + + @FXML + private void onLoadKeys() { + // Create temporary options to load keys + BasicSignerOptions tmpOpts = new BasicSignerOptions(); + tmpOpts.setKsType(cmbKeystoreType.getValue()); + tmpOpts.setKsFile(txtKeystoreFile.getText()); + tmpOpts.setKsPasswd(txtKeystorePassword.getText() != null + ? txtKeystorePassword.getText().toCharArray() : null); + + keyStoreService.cancel(); + keyStoreService.reset(); + keyStoreService.setOptions(tmpOpts); + keyStoreService.start(); + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/EncryptionSettingsController.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/EncryptionSettingsController.java new file mode 100644 index 00000000..b1e4f7ad --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/EncryptionSettingsController.java @@ -0,0 +1,84 @@ +package net.sf.jsignpdf.fx.view; + +import java.io.File; + +import static net.sf.jsignpdf.Constants.RES; + +import javafx.collections.FXCollections; +import javafx.fxml.FXML; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import net.sf.jsignpdf.fx.viewmodel.SigningOptionsViewModel; +import net.sf.jsignpdf.types.PDFEncryption; +import net.sf.jsignpdf.types.PrintRight; + +/** + * Controller for PDF encryption and rights settings. + */ +public class EncryptionSettingsController { + + @FXML private ComboBox cmbEncryption; + @FXML private VBox encryptionDetailsPane; + @FXML private PasswordField txtOwnerPassword; + @FXML private PasswordField txtUserPassword; + @FXML private TextField txtEncCertFile; + + // Rights + @FXML private ComboBox cmbPrintRight; + @FXML private CheckBox chkCopy; + @FXML private CheckBox chkAssembly; + @FXML private CheckBox chkFillIn; + @FXML private CheckBox chkScreenReaders; + @FXML private CheckBox chkModifyAnnotations; + @FXML private CheckBox chkModifyContents; + + private SigningOptionsViewModel viewModel; + + @FXML + private void initialize() { + cmbEncryption.setItems(FXCollections.observableArrayList(PDFEncryption.values())); + cmbPrintRight.setItems(FXCollections.observableArrayList(PrintRight.values())); + + // Toggle encryption details visibility + cmbEncryption.valueProperty().addListener((obs, o, n) -> + encryptionDetailsPane.setVisible(n != null && n != PDFEncryption.NONE)); + encryptionDetailsPane.setVisible(false); + encryptionDetailsPane.managedProperty().bind(encryptionDetailsPane.visibleProperty()); + } + + public void setViewModel(SigningOptionsViewModel vm) { + this.viewModel = vm; + bindToViewModel(); + } + + private void bindToViewModel() { + cmbEncryption.valueProperty().bindBidirectional(viewModel.pdfEncryptionProperty()); + txtOwnerPassword.textProperty().bindBidirectional(viewModel.pdfOwnerPasswordProperty()); + txtUserPassword.textProperty().bindBidirectional(viewModel.pdfUserPasswordProperty()); + txtEncCertFile.textProperty().bindBidirectional(viewModel.pdfEncryptionCertFileProperty()); + + cmbPrintRight.valueProperty().bindBidirectional(viewModel.rightPrintingProperty()); + chkCopy.selectedProperty().bindBidirectional(viewModel.rightCopyProperty()); + chkAssembly.selectedProperty().bindBidirectional(viewModel.rightAssemblyProperty()); + chkFillIn.selectedProperty().bindBidirectional(viewModel.rightFillInProperty()); + chkScreenReaders.selectedProperty().bindBidirectional(viewModel.rightScreenReadersProperty()); + chkModifyAnnotations.selectedProperty().bindBidirectional(viewModel.rightModifyAnnotationsProperty()); + chkModifyContents.selectedProperty().bindBidirectional(viewModel.rightModifyContentsProperty()); + + // Update visibility from initial loaded values + PDFEncryption enc = viewModel.pdfEncryptionProperty().get(); + encryptionDetailsPane.setVisible(enc != null && enc != PDFEncryption.NONE); + } + + @FXML + private void onBrowseEncCert() { + FileChooser fc = new FileChooser(); + fc.setTitle(RES.get("jfx.gui.dialog.selectEncryptionCert")); + File file = fc.showOpenDialog(txtEncCertFile.getScene().getWindow()); + if (file != null) txtEncCertFile.setText(file.getAbsolutePath()); + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java new file mode 100644 index 00000000..87b1adfd --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java @@ -0,0 +1,648 @@ +package net.sf.jsignpdf.fx.view; + +import java.io.File; +import java.util.List; +import java.util.logging.Level; + +import javafx.fxml.FXML; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SplitPane; +import javafx.scene.control.TextField; +import javafx.scene.input.DragEvent; +import javafx.scene.input.Dragboard; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.TransferMode; +import javafx.scene.layout.StackPane; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import net.sf.jsignpdf.BasicSignerOptions; +import net.sf.jsignpdf.Constants; +import net.sf.jsignpdf.PdfExtraInfo; +import javafx.scene.layout.VBox; +import net.sf.jsignpdf.fx.control.PdfPageView; +import net.sf.jsignpdf.fx.control.SignatureOverlay; +import net.sf.jsignpdf.fx.service.PdfRenderService; +import net.sf.jsignpdf.fx.service.SigningService; +import net.sf.jsignpdf.fx.util.RecentFilesManager; +import net.sf.jsignpdf.fx.viewmodel.DocumentViewModel; +import net.sf.jsignpdf.fx.viewmodel.SignaturePlacementViewModel; +import net.sf.jsignpdf.fx.viewmodel.SigningOptionsViewModel; +import net.sf.jsignpdf.types.PageInfo; + +import static net.sf.jsignpdf.Constants.LOGGER; +import static net.sf.jsignpdf.Constants.RES; + +/** + * Controller for the main application window. + */ +public class MainWindowController { + + private Stage stage; + private BasicSignerOptions options; + private final DocumentViewModel documentVM = new DocumentViewModel(); + private final SigningOptionsViewModel signingVM = new SigningOptionsViewModel(); + private final SignaturePlacementViewModel placementVM = new SignaturePlacementViewModel(); + private final PdfRenderService renderService = new PdfRenderService(); + private final SigningService signingService = new SigningService(); + private final RecentFilesManager recentFilesManager = new RecentFilesManager(); + private PdfPageView pdfPageView; + private SignatureOverlay signatureOverlay; + + // Included sub-controllers (fx:id + "Controller" naming convention) + @FXML private VBox certificateSettings; + @FXML private CertificateSettingsController certificateSettingsController; + @FXML private VBox signatureSettings; + @FXML private SignatureSettingsController signatureSettingsController; + @FXML private VBox tsaSettings; + @FXML private TsaSettingsController tsaSettingsController; + @FXML private VBox encryptionSettings; + @FXML private EncryptionSettingsController encryptionSettingsController; + @FXML private VBox outputConsole; + @FXML private OutputConsoleController outputConsoleController; + + // Menu items + @FXML private MenuItem menuOpen; + @FXML private MenuItem menuClose; + @FXML private Menu menuRecentFiles; + @FXML private MenuItem menuSign; + @FXML private MenuItem menuExit; + @FXML private MenuItem menuZoomIn; + @FXML private MenuItem menuZoomOut; + @FXML private MenuItem menuZoomFit; + @FXML private MenuItem menuToggleSidePanel; + @FXML private MenuItem menuAbout; + + // Toolbar + @FXML private Button btnOpen; + @FXML private Button btnZoomIn; + @FXML private Button btnZoomOut; + @FXML private Button btnZoomFit; + @FXML private ComboBox cmbZoom; + @FXML private Button btnPrevPage; + @FXML private TextField txtPageNumber; + @FXML private Label lblPageCount; + @FXML private Button btnNextPage; + @FXML private Button btnSign; + + // Content area + @FXML private SplitPane splitPane; + @FXML private ScrollPane scrollPane; + @FXML private StackPane pdfArea; + @FXML private Label lblDropHint; + + // Status bar + @FXML private Label lblStatus; + @FXML private ProgressBar progressBar; + + public void setStage(Stage stage) { + this.stage = stage; + stage.addEventFilter(KeyEvent.KEY_PRESSED, this::handleKeyPress); + } + + /** + * Initializes the UI from persisted BasicSignerOptions (called after loadOptions()). + * Populates all ViewModel properties from the options so the UI is prefilled. + */ + public void initFromOptions(BasicSignerOptions opts) { + this.options = opts; + signingVM.syncFromOptions(opts); + } + + /** + * Stores current UI state to BasicSignerOptions and persists to disk. + * Called on window close. + */ + public void storeAndCleanup() { + try { + if (options == null) { + options = new BasicSignerOptions(); + } + + // Sync placement rectangle coordinates to the signing ViewModel before persisting + if (placementVM.isPlaced() && documentVM.isDocumentLoaded()) { + signingVM.visibleProperty().set(true); + signingVM.pageProperty().set(documentVM.getCurrentPage()); + + PdfExtraInfo extraInfo = new PdfExtraInfo(options); + PageInfo pageInfo = extraInfo.getPageInfo(documentVM.getCurrentPage()); + if (pageInfo != null) { + float[] coords = placementVM.toPdfCoordinates( + pageInfo.getWidth(), pageInfo.getHeight()); + signingVM.positionLLXProperty().set(coords[0]); + signingVM.positionLLYProperty().set(coords[1]); + signingVM.positionURXProperty().set(coords[2]); + signingVM.positionURYProperty().set(coords[3]); + } + } + + signingVM.syncToOptions(options); + options.storeOptions(); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to store options", e); + } + if (outputConsoleController != null) { + outputConsoleController.dispose(); + } + } + + @FXML + private void initialize() { + // Wire sub-controllers + if (certificateSettingsController != null) { + certificateSettingsController.setViewModel(signingVM); + } + if (signatureSettingsController != null) { + signatureSettingsController.setViewModel(signingVM); + } + if (tsaSettingsController != null) { + tsaSettingsController.setViewModel(signingVM); + } + if (encryptionSettingsController != null) { + encryptionSettingsController.setViewModel(signingVM); + } + + // Setup PDF page view and signature overlay + pdfPageView = new PdfPageView(); + pdfPageView.setVisible(false); + signatureOverlay = new SignatureOverlay(placementVM); + signatureOverlay.setVisible(false); + signatureOverlay.setMouseTransparent(false); + pdfArea.getChildren().add(0, pdfPageView); + pdfArea.getChildren().add(1, signatureOverlay); + + // Bind pdfArea min size to pdfPageView so scrollbars appear when the page + // exceeds the viewport. With fitToWidth/fitToHeight on the ScrollPane, the + // StackPane expands to fill the viewport (centering children) but minSize + // prevents it from shrinking below the rendered page, triggering scrollbars. + pdfArea.minWidthProperty().bind(pdfPageView.prefWidthProperty()); + pdfArea.minHeightProperty().bind(pdfPageView.prefHeightProperty()); + + // Auto-enable visible signature when a rectangle is placed + placementVM.placedProperty().addListener((obs, wasPlaced, isPlaced) -> { + if (isPlaced) { + signingVM.visibleProperty().set(true); + } + updateStatusWithHint(); + }); + + // Clear placement rectangle when visible signature is disabled + signingVM.visibleProperty().addListener((obs, wasVisible, isVisible) -> { + if (!isVisible) { + placementVM.reset(); + } + }); + + // Keep overlay sized to match the pdf page view + signatureOverlay.prefWidthProperty().bind(pdfPageView.prefWidthProperty()); + signatureOverlay.prefHeightProperty().bind(pdfPageView.prefHeightProperty()); + signatureOverlay.minWidthProperty().bind(pdfPageView.minWidthProperty()); + signatureOverlay.minHeightProperty().bind(pdfPageView.minHeightProperty()); + signatureOverlay.maxWidthProperty().bind(pdfPageView.maxWidthProperty()); + signatureOverlay.maxHeightProperty().bind(pdfPageView.maxHeightProperty()); + + progressBar.setVisible(false); + updateStatus(RES.get("jfx.gui.status.ready")); + + cmbZoom.getItems().addAll("50%", "75%", "100%", "125%", "150%", "200%"); + cmbZoom.setValue("100%"); + + // Disable controls that require a loaded document + setDocumentControlsDisabled(true); + + // Bind PDF page view to ViewModel + pdfPageView.pageImageProperty().bind(documentVM.currentPageImageProperty()); + pdfPageView.zoomLevelProperty().bind(documentVM.zoomLevelProperty()); + + // Listen for page changes to trigger re-render + documentVM.currentPageProperty().addListener((obs, oldVal, newVal) -> { + txtPageNumber.setText(String.valueOf(newVal.intValue())); + renderCurrentPage(); + updateNavButtonState(); + }); + + // Zoom combo box changes + cmbZoom.setOnAction(e -> { + String val = cmbZoom.getValue(); + if (val != null) { + try { + double zoom = Double.parseDouble(val.replace("%", "").trim()) / 100.0; + documentVM.setZoomLevel(zoom); + } catch (NumberFormatException ignored) { + } + } + }); + + // Zoom level changes update combo + documentVM.zoomLevelProperty().addListener((obs, oldVal, newVal) -> { + String formatted = Math.round(newVal.doubleValue() * 100) + "%"; + if (!formatted.equals(cmbZoom.getValue())) { + cmbZoom.setValue(formatted); + } + }); + + // Page number text field commit + txtPageNumber.setOnAction(e -> { + try { + int page = Integer.parseInt(txtPageNumber.getText().trim()); + documentVM.setCurrentPage(page); + } catch (NumberFormatException ignored) { + txtPageNumber.setText(String.valueOf(documentVM.getCurrentPage())); + } + }); + + // Setup render service callbacks + renderService.setOnSucceeded(e -> { + documentVM.setCurrentPageImage(renderService.getValue()); + pdfPageView.setVisible(true); + progressBar.setVisible(false); + updateStatusForDocument(); + }); + renderService.setOnFailed(e -> { + LOGGER.log(Level.WARNING, "Failed to render page", renderService.getException()); + progressBar.setVisible(false); + updateStatus(RES.get("jfx.gui.status.renderError")); + }); + + // Signing service callbacks + signingService.setOnSucceeded(e -> { + progressBar.setVisible(false); + boolean success = signingService.getValue(); + if (success) { + updateStatus(RES.get("jfx.gui.status.signingOk")); + showAlert(Alert.AlertType.INFORMATION, + RES.get("jfx.gui.dialog.signingComplete.title"), + RES.get("jfx.gui.dialog.signingComplete.text")); + } else { + updateStatus(RES.get("jfx.gui.status.signingFailed")); + showAlert(Alert.AlertType.ERROR, + RES.get("jfx.gui.dialog.signingFailed.title"), + RES.get("jfx.gui.dialog.signingFailed.text")); + } + }); + signingService.setOnFailed(e -> { + progressBar.setVisible(false); + LOGGER.log(Level.SEVERE, "Signing service error", signingService.getException()); + updateStatus(RES.get("jfx.gui.status.signingFailed") + ": " + + signingService.getException().getMessage()); + showAlert(Alert.AlertType.ERROR, + RES.get("jfx.gui.dialog.signingError.title"), + signingService.getException().getMessage()); + }); + + refreshRecentFilesMenu(); + } + + private void refreshRecentFilesMenu() { + menuRecentFiles.getItems().clear(); + List recentFiles = recentFilesManager.getRecentFiles(); + if (recentFiles.isEmpty()) { + MenuItem empty = new MenuItem(RES.get("jfx.gui.menu.file.recentFiles.empty")); + empty.setDisable(true); + menuRecentFiles.getItems().add(empty); + } else { + for (String path : recentFiles) { + MenuItem item = new MenuItem(path); + item.setMnemonicParsing(false); + item.setOnAction(e -> openDocument(new File(path))); + menuRecentFiles.getItems().add(item); + } + } + } + + private void setDocumentControlsDisabled(boolean disabled) { + btnZoomIn.setDisable(disabled); + btnZoomOut.setDisable(disabled); + btnZoomFit.setDisable(disabled); + cmbZoom.setDisable(disabled); + btnPrevPage.setDisable(disabled); + txtPageNumber.setDisable(disabled); + btnNextPage.setDisable(disabled); + btnSign.setDisable(disabled); + menuSign.setDisable(disabled); + menuClose.setDisable(disabled); + menuZoomIn.setDisable(disabled); + menuZoomOut.setDisable(disabled); + menuZoomFit.setDisable(disabled); + } + + private void updateNavButtonState() { + btnPrevPage.setDisable(!documentVM.canGoPrev()); + btnNextPage.setDisable(!documentVM.canGoNext()); + } + + private void updateStatus(String message) { + lblStatus.setText(message); + } + + private void updateStatusForDocument() { + File file = documentVM.getDocumentFile(); + if (file != null) { + updateStatus(file.getName() + " - Page " + documentVM.getCurrentPage() + + "/" + documentVM.getPageCount()); + } + } + + private void updateStatusWithHint() { + if (documentVM.isDocumentLoaded() && placementVM.isPlaced()) { + updateStatus(RES.get("jfx.gui.status.shiftToReplace")); + } else if (documentVM.isDocumentLoaded()) { + updateStatusForDocument(); + } + } + + private void renderCurrentPage() { + if (options == null || !documentVM.isDocumentLoaded()) { + return; + } + renderService.cancel(); + renderService.reset(); + renderService.setOptions(options); + renderService.setPage(documentVM.getCurrentPage()); + progressBar.setVisible(true); + renderService.start(); + } + + // --- Keyboard handling --- + private void handleKeyPress(KeyEvent event) { + if (!documentVM.isDocumentLoaded()) return; + if (event.getCode() == KeyCode.PAGE_DOWN) { + documentVM.nextPage(); + event.consume(); + } else if (event.getCode() == KeyCode.PAGE_UP) { + documentVM.prevPage(); + event.consume(); + } else if (event.getCode() == KeyCode.Z && event.isShortcutDown() && placementVM.isPlaced()) { + placementVM.reset(); + event.consume(); + } + } + + // --- Menu handlers --- + + @FXML + private void onFileOpen() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(RES.get("jfx.gui.dialog.openPdf.title")); + fileChooser.getExtensionFilters().add( + new FileChooser.ExtensionFilter("PDF Files", "*.pdf")); + File file = fileChooser.showOpenDialog(stage); + if (file != null) { + openDocument(file); + } + } + + @FXML + private void onFileClose() { + closeDocument(); + } + + @FXML + private void onFileExit() { + stage.close(); + } + + @FXML + private void onSign() { + if (options == null || !documentVM.isDocumentLoaded()) { + showAlert(Alert.AlertType.WARNING, + RES.get("jfx.gui.dialog.noDocument.title"), + RES.get("jfx.gui.dialog.noDocument.text")); + return; + } + + // Sync ViewModel to options + signingVM.syncToOptions(options); + + // Apply signature placement coordinates if placed + if (placementVM.isPlaced()) { + options.setVisible(true); + options.setPage(documentVM.getCurrentPage()); + + PdfExtraInfo extraInfo = new PdfExtraInfo(options); + PageInfo pageInfo = extraInfo.getPageInfo(documentVM.getCurrentPage()); + if (pageInfo != null) { + float[] coords = placementVM.toPdfCoordinates( + pageInfo.getWidth(), pageInfo.getHeight()); + options.setPositionLLX(coords[0]); + options.setPositionLLY(coords[1]); + options.setPositionURX(coords[2]); + options.setPositionURY(coords[3]); + } + } + + // Generate output file name if not set + if (options.getOutFile() == null || options.getOutFile().isEmpty()) { + String inFile = options.getInFile(); + String suffix = ".pdf"; + String nameBase = inFile; + if (inFile.toLowerCase().endsWith(suffix)) { + nameBase = inFile.substring(0, inFile.length() - 4); + suffix = inFile.substring(inFile.length() - 4); + } + options.setOutFile(nameBase + Constants.DEFAULT_OUT_SUFFIX + suffix); + } + + // Start signing + signingService.cancel(); + signingService.reset(); + signingService.setOptions(options.createCopy()); + progressBar.setVisible(true); + progressBar.setProgress(-1); // indeterminate + updateStatus(RES.get("jfx.gui.status.signingInProgress")); + signingService.start(); + } + + @FXML + private void onZoomIn() { + documentVM.zoomIn(); + } + + @FXML + private void onZoomOut() { + documentVM.zoomOut(); + } + + @FXML + private void onZoomFit() { + if (!documentVM.isDocumentLoaded() || documentVM.getCurrentPageImage() == null) return; + double imgWidth = documentVM.getCurrentPageImage().getWidth(); + double viewWidth = scrollPane.getViewportBounds().getWidth(); + if (imgWidth > 0 && viewWidth > 0) { + documentVM.setZoomLevel(viewWidth / imgWidth); + } + } + + @FXML + private void onPrevPage() { + documentVM.prevPage(); + } + + @FXML + private void onNextPage() { + documentVM.nextPage(); + } + + @FXML + private void onToggleSidePanel() { + if (splitPane.getItems().size() > 1) { + // Toggle visibility of side panel by manipulating divider position + double pos = splitPane.getDividerPositions()[0]; + if (pos < 0.05) { + splitPane.setDividerPositions(0.28); + } else { + splitPane.setDividerPositions(0.0); + } + } + } + + @FXML + private void onAbout() { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle(RES.get("jfx.gui.menu.help.about")); + alert.setHeaderText("JSignPdf " + Constants.VERSION); + alert.setContentText(RES.get("jfx.gui.dialog.about.description")); + alert.showAndWait(); + } + + // --- Drag and drop --- + + @FXML + private void onDragOver(DragEvent event) { + Dragboard db = event.getDragboard(); + if (db.hasFiles()) { + List files = db.getFiles(); + if (files.size() == 1 && files.get(0).getName().toLowerCase().endsWith(".pdf")) { + event.acceptTransferModes(TransferMode.COPY); + } + } + event.consume(); + } + + @FXML + private void onDragDropped(DragEvent event) { + Dragboard db = event.getDragboard(); + boolean success = false; + if (db.hasFiles()) { + List files = db.getFiles(); + if (files.size() == 1 && files.get(0).getName().toLowerCase().endsWith(".pdf")) { + openDocument(files.get(0)); + success = true; + } + } + event.setDropCompleted(success); + event.consume(); + } + + // --- Document handling --- + + private void openDocument(File file) { + try { + if (options == null) { + options = new BasicSignerOptions(); + options.loadOptions(); + signingVM.syncFromOptions(options); + } + + // Reset visible signature and placement from previous document + signingVM.visibleProperty().set(false); + placementVM.reset(); + + options.setInFile(file.getAbsolutePath()); + + // Get page count + PdfExtraInfo extraInfo = new PdfExtraInfo(options); + int pages = extraInfo.getNumberOfPages(); + if (pages < 1) { + updateStatus(RES.get("jfx.gui.status.readError")); + return; + } + + documentVM.setDocumentFile(file); + documentVM.setPageCount(pages); + documentVM.setZoomLevel(1.0); + + lblDropHint.setVisible(false); + setDocumentControlsDisabled(false); + lblPageCount.setText("/ " + pages); + txtPageNumber.setText("1"); + cmbZoom.setValue("100%"); + + stage.setTitle("JSignPdf " + Constants.VERSION + " - " + file.getName()); + LOGGER.info("Opened document: " + file.getAbsolutePath()); + + recentFilesManager.addFile(file); + refreshRecentFilesMenu(); + + // Show signature overlay and render first page + signatureOverlay.setVisible(true); + documentVM.setCurrentPage(1); + renderCurrentPage(); + updateNavButtonState(); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to open document", e); + updateStatus("Error: " + e.getMessage()); + } + } + + private void closeDocument() { + renderService.cancel(); + signingVM.visibleProperty().set(false); + documentVM.reset(); + placementVM.reset(); + signatureOverlay.setVisible(false); + if (options != null) { + options.setInFile(null); + } + pdfPageView.setVisible(false); + lblDropHint.setVisible(true); + setDocumentControlsDisabled(true); + lblPageCount.setText("/ 0"); + txtPageNumber.setText(""); + updateStatus(RES.get("jfx.gui.status.ready")); + stage.setTitle("JSignPdf " + Constants.VERSION); + } + + /** + * Returns the document view model (used by other controllers). + */ + public DocumentViewModel getDocumentViewModel() { + return documentVM; + } + + private void showAlert(Alert.AlertType type, String title, String content) { + Alert alert = new Alert(type); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(content); + alert.showAndWait(); + } + + /** + * Returns the signature placement view model. + */ + public SignaturePlacementViewModel getPlacementViewModel() { + return placementVM; + } + + /** + * Returns the signing options view model. + */ + public SigningOptionsViewModel getSigningOptionsViewModel() { + return signingVM; + } + + /** + * Returns the current signer options (used by other controllers). + */ + public BasicSignerOptions getOptions() { + return options; + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/OutputConsoleController.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/OutputConsoleController.java new file mode 100644 index 00000000..9e6b9d24 --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/OutputConsoleController.java @@ -0,0 +1,79 @@ +package net.sf.jsignpdf.fx.view; + +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.TextArea; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import static net.sf.jsignpdf.Constants.LOGGER; + +/** + * Controller for the output console panel that captures log messages. + */ +public class OutputConsoleController { + + @FXML private TextArea txtOutput; + + private Handler logHandler; + + @FXML + private void initialize() { + // Remove any previously registered handler (guards against FXML reload leaks) + dispose(); + + // Attach a log handler to capture signing output + logHandler = new Handler() { + @Override + public void publish(LogRecord record) { + if (record == null) return; + StringBuilder sb = new StringBuilder(); + if (record.getLevel() != null + && record.getLevel().intValue() >= Level.WARNING.intValue()) { + sb.append(record.getLevel().getName()).append(" "); + } + if (record.getMessage() != null) { + sb.append(record.getMessage()); + } + if (record.getThrown() != null) { + sb.append("\n"); + StringWriter sw = new StringWriter(); + record.getThrown().printStackTrace(new PrintWriter(sw)); + sb.append(sw); + } + if (sb.length() > 0) { + Platform.runLater(() -> txtOutput.appendText(sb.toString() + "\n")); + } + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + }; + LOGGER.addHandler(logHandler); + } + + @FXML + private void onClear() { + txtOutput.clear(); + } + + public void dispose() { + if (logHandler != null) { + LOGGER.removeHandler(logHandler); + logHandler = null; + } + } + + public void appendMessage(String message) { + Platform.runLater(() -> txtOutput.appendText(message + "\n")); + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/SignatureSettingsController.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/SignatureSettingsController.java new file mode 100644 index 00000000..21f1ae12 --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/SignatureSettingsController.java @@ -0,0 +1,122 @@ +package net.sf.jsignpdf.fx.view; + +import java.io.File; + +import static net.sf.jsignpdf.Constants.RES; + +import javafx.collections.FXCollections; +import javafx.fxml.FXML; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import net.sf.jsignpdf.fx.viewmodel.SigningOptionsViewModel; +import net.sf.jsignpdf.types.CertificationLevel; +import net.sf.jsignpdf.types.HashAlgorithm; +import net.sf.jsignpdf.types.RenderMode; + +/** + * Controller for signature appearance and metadata settings. + */ +public class SignatureSettingsController { + + @FXML private CheckBox chkVisibleSig; + @FXML private VBox visibleSigPane; + @FXML private ComboBox cmbRenderMode; + @FXML private ComboBox cmbHashAlgorithm; + @FXML private ComboBox cmbCertLevel; + @FXML private TextField txtSignerName; + @FXML private TextField txtReason; + @FXML private TextField txtLocation; + @FXML private TextField txtContact; + @FXML private TextField txtL2Text; + @FXML private TextField txtL4Text; + @FXML private TextField txtFontSize; + @FXML private TextField txtImgPath; + @FXML private TextField txtBgImgPath; + @FXML private CheckBox chkAcro6Layers; + @FXML private CheckBox chkAppend; + @FXML private TextField txtOutFile; + + private SigningOptionsViewModel viewModel; + + @FXML + private void initialize() { + cmbRenderMode.setItems(FXCollections.observableArrayList(RenderMode.values())); + cmbHashAlgorithm.setItems(FXCollections.observableArrayList(HashAlgorithm.values())); + cmbCertLevel.setItems(FXCollections.observableArrayList(CertificationLevel.values())); + + // Toggle visible signature details + visibleSigPane.managedProperty().bind(visibleSigPane.visibleProperty()); + chkVisibleSig.selectedProperty().addListener((obs, o, n) -> + visibleSigPane.setVisible(n)); + visibleSigPane.setVisible(false); + } + + public void setViewModel(SigningOptionsViewModel vm) { + this.viewModel = vm; + bindToViewModel(); + } + + private void bindToViewModel() { + chkVisibleSig.selectedProperty().bindBidirectional(viewModel.visibleProperty()); + cmbRenderMode.valueProperty().bindBidirectional(viewModel.renderModeProperty()); + cmbHashAlgorithm.valueProperty().bindBidirectional(viewModel.hashAlgorithmProperty()); + cmbCertLevel.valueProperty().bindBidirectional(viewModel.certLevelProperty()); + txtSignerName.textProperty().bindBidirectional(viewModel.signerNameProperty()); + txtReason.textProperty().bindBidirectional(viewModel.reasonProperty()); + txtLocation.textProperty().bindBidirectional(viewModel.locationProperty()); + txtContact.textProperty().bindBidirectional(viewModel.contactProperty()); + txtL2Text.textProperty().bindBidirectional(viewModel.l2TextProperty()); + txtL4Text.textProperty().bindBidirectional(viewModel.l4TextProperty()); + txtImgPath.textProperty().bindBidirectional(viewModel.imgPathProperty()); + txtBgImgPath.textProperty().bindBidirectional(viewModel.bgImgPathProperty()); + chkAcro6Layers.selectedProperty().bindBidirectional(viewModel.acro6LayersProperty()); + chkAppend.selectedProperty().bindBidirectional(viewModel.appendProperty()); + txtOutFile.textProperty().bindBidirectional(viewModel.outFileProperty()); + + // Font size needs manual sync (String <-> float) + viewModel.l2TextFontSizeProperty().addListener((obs, o, n) -> + txtFontSize.setText(String.valueOf(n.floatValue()))); + txtFontSize.setOnAction(e -> { + try { + viewModel.l2TextFontSizeProperty().set(Float.parseFloat(txtFontSize.getText())); + } catch (NumberFormatException ignored) { + } + }); + + // Update visibility from initial value + visibleSigPane.setVisible(viewModel.visibleProperty().get()); + } + + @FXML + private void onBrowseImage() { + File file = browseImageFile(RES.get("jfx.gui.dialog.selectSignatureImage")); + if (file != null) txtImgPath.setText(file.getAbsolutePath()); + } + + @FXML + private void onBrowseBgImage() { + File file = browseImageFile(RES.get("jfx.gui.dialog.selectBackgroundImage")); + if (file != null) txtBgImgPath.setText(file.getAbsolutePath()); + } + + @FXML + private void onBrowseOutFile() { + FileChooser fc = new FileChooser(); + fc.setTitle(RES.get("jfx.gui.dialog.selectOutputPdf")); + fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("PDF Files", "*.pdf")); + File file = fc.showSaveDialog(txtOutFile.getScene().getWindow()); + if (file != null) txtOutFile.setText(file.getAbsolutePath()); + } + + private File browseImageFile(String title) { + FileChooser fc = new FileChooser(); + fc.setTitle(title); + fc.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg", "*.jpeg", "*.gif"), + new FileChooser.ExtensionFilter("All Files", "*.*")); + return fc.showOpenDialog(txtImgPath.getScene().getWindow()); + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/TsaSettingsController.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/TsaSettingsController.java new file mode 100644 index 00000000..2891c5bf --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/TsaSettingsController.java @@ -0,0 +1,156 @@ +package net.sf.jsignpdf.fx.view; + +import java.io.File; + +import static net.sf.jsignpdf.Constants.RES; + +import javafx.collections.FXCollections; +import javafx.fxml.FXML; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import net.sf.jsignpdf.fx.viewmodel.SigningOptionsViewModel; +import net.sf.jsignpdf.types.ServerAuthentication; + +import java.net.Proxy; + +/** + * Controller for TSA, OCSP, CRL, and proxy settings. + */ +public class TsaSettingsController { + + @FXML private CheckBox chkTsaEnabled; + @FXML private TextField txtTsaUrl; + @FXML private ComboBox cmbTsaAuthn; + @FXML private TextField txtTsaUser; + @FXML private PasswordField txtTsaPassword; + @FXML private TextField txtTsaCertFileType; + @FXML private TextField txtTsaCertFile; + @FXML private PasswordField txtTsaCertFilePassword; + @FXML private TextField txtTsaPolicy; + @FXML private TextField txtTsaHashAlg; + @FXML private VBox tsaDetailsPane; + + @FXML private CheckBox chkOcspEnabled; + @FXML private Label lblOcspServerUrl; + @FXML private TextField txtOcspServerUrl; + @FXML private CheckBox chkCrlEnabled; + + @FXML private ComboBox cmbProxyType; + @FXML private Label lblProxyHost; + @FXML private TextField txtProxyHost; + @FXML private Label lblProxyPort; + @FXML private TextField txtProxyPort; + + private SigningOptionsViewModel viewModel; + + @FXML + private void initialize() { + cmbTsaAuthn.setItems(FXCollections.observableArrayList(ServerAuthentication.values())); + cmbProxyType.setItems(FXCollections.observableArrayList(Proxy.Type.values())); + + // Toggle TSA details visibility + tsaDetailsPane.managedProperty().bind(tsaDetailsPane.visibleProperty()); + chkTsaEnabled.selectedProperty().addListener((obs, o, n) -> + tsaDetailsPane.setVisible(n)); + tsaDetailsPane.setVisible(false); + + // Toggle OCSP URL visibility + chkOcspEnabled.selectedProperty().addListener((obs, o, n) -> { + lblOcspServerUrl.setVisible(n); + txtOcspServerUrl.setVisible(n); + lblOcspServerUrl.setManaged(n); + txtOcspServerUrl.setManaged(n); + }); + lblOcspServerUrl.setVisible(false); + txtOcspServerUrl.setVisible(false); + lblOcspServerUrl.setManaged(false); + txtOcspServerUrl.setManaged(false); + + // Toggle proxy details visibility + cmbProxyType.valueProperty().addListener((obs, o, n) -> { + boolean showDetails = n != null && n != Proxy.Type.DIRECT; + lblProxyHost.setVisible(showDetails); + txtProxyHost.setVisible(showDetails); + lblProxyPort.setVisible(showDetails); + txtProxyPort.setVisible(showDetails); + lblProxyHost.setManaged(showDetails); + txtProxyHost.setManaged(showDetails); + lblProxyPort.setManaged(showDetails); + txtProxyPort.setManaged(showDetails); + }); + lblProxyHost.setVisible(false); + txtProxyHost.setVisible(false); + lblProxyPort.setVisible(false); + txtProxyPort.setVisible(false); + lblProxyHost.setManaged(false); + txtProxyHost.setManaged(false); + lblProxyPort.setManaged(false); + txtProxyPort.setManaged(false); + } + + public void setViewModel(SigningOptionsViewModel vm) { + this.viewModel = vm; + bindToViewModel(); + } + + private void bindToViewModel() { + chkTsaEnabled.selectedProperty().bindBidirectional(viewModel.tsaEnabledProperty()); + txtTsaUrl.textProperty().bindBidirectional(viewModel.tsaUrlProperty()); + cmbTsaAuthn.valueProperty().bindBidirectional(viewModel.tsaServerAuthnProperty()); + txtTsaUser.textProperty().bindBidirectional(viewModel.tsaUserProperty()); + txtTsaPassword.textProperty().bindBidirectional(viewModel.tsaPasswordProperty()); + txtTsaCertFileType.textProperty().bindBidirectional(viewModel.tsaCertFileTypeProperty()); + txtTsaCertFile.textProperty().bindBidirectional(viewModel.tsaCertFileProperty()); + txtTsaCertFilePassword.textProperty().bindBidirectional(viewModel.tsaCertFilePasswordProperty()); + txtTsaPolicy.textProperty().bindBidirectional(viewModel.tsaPolicyProperty()); + txtTsaHashAlg.textProperty().bindBidirectional(viewModel.tsaHashAlgProperty()); + + chkOcspEnabled.selectedProperty().bindBidirectional(viewModel.ocspEnabledProperty()); + txtOcspServerUrl.textProperty().bindBidirectional(viewModel.ocspServerUrlProperty()); + chkCrlEnabled.selectedProperty().bindBidirectional(viewModel.crlEnabledProperty()); + + cmbProxyType.valueProperty().bindBidirectional(viewModel.proxyTypeProperty()); + txtProxyHost.textProperty().bindBidirectional(viewModel.proxyHostProperty()); + + // Proxy port: String <-> int + viewModel.proxyPortProperty().addListener((obs, o, n) -> + txtProxyPort.setText(String.valueOf(n.intValue()))); + txtProxyPort.setOnAction(e -> { + try { + viewModel.proxyPortProperty().set(Integer.parseInt(txtProxyPort.getText())); + } catch (NumberFormatException ignored) { + } + }); + + // Update visibility from initial loaded values + tsaDetailsPane.setVisible(viewModel.tsaEnabledProperty().get()); + boolean ocspOn = viewModel.ocspEnabledProperty().get(); + lblOcspServerUrl.setVisible(ocspOn); + txtOcspServerUrl.setVisible(ocspOn); + lblOcspServerUrl.setManaged(ocspOn); + txtOcspServerUrl.setManaged(ocspOn); + Proxy.Type pt = viewModel.proxyTypeProperty().get(); + boolean proxyOn = pt != null && pt != Proxy.Type.DIRECT; + lblProxyHost.setVisible(proxyOn); + txtProxyHost.setVisible(proxyOn); + lblProxyPort.setVisible(proxyOn); + txtProxyPort.setVisible(proxyOn); + lblProxyHost.setManaged(proxyOn); + txtProxyHost.setManaged(proxyOn); + lblProxyPort.setManaged(proxyOn); + txtProxyPort.setManaged(proxyOn); + } + + @FXML + private void onBrowseTsaCertFile() { + FileChooser fc = new FileChooser(); + fc.setTitle(RES.get("jfx.gui.dialog.selectTsaCertFile")); + File file = fc.showOpenDialog(txtTsaCertFile.getScene().getWindow()); + if (file != null) txtTsaCertFile.setText(file.getAbsolutePath()); + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/DocumentViewModel.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/DocumentViewModel.java new file mode 100644 index 00000000..e5388df6 --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/DocumentViewModel.java @@ -0,0 +1,100 @@ +package net.sf.jsignpdf.fx.viewmodel; + +import java.io.File; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.scene.image.Image; + +/** + * ViewModel holding the state of the currently loaded PDF document. + */ +public class DocumentViewModel { + + private final ObjectProperty documentFile = new SimpleObjectProperty<>(); + private final IntegerProperty pageCount = new SimpleIntegerProperty(0); + private final IntegerProperty currentPage = new SimpleIntegerProperty(1); + private final DoubleProperty zoomLevel = new SimpleDoubleProperty(1.0); + private final ObjectProperty currentPageImage = new SimpleObjectProperty<>(); + private final ReadOnlyBooleanWrapper documentLoaded = new ReadOnlyBooleanWrapper(false); + private final StringProperty statusText = new SimpleStringProperty(""); + + public DocumentViewModel() { + documentFile.addListener((obs, oldVal, newVal) -> + documentLoaded.set(newVal != null)); + } + + // --- Document file --- + public ObjectProperty documentFileProperty() { return documentFile; } + public File getDocumentFile() { return documentFile.get(); } + public void setDocumentFile(File file) { documentFile.set(file); } + + // --- Page count --- + public IntegerProperty pageCountProperty() { return pageCount; } + public int getPageCount() { return pageCount.get(); } + public void setPageCount(int count) { pageCount.set(count); } + + // --- Current page (1-based) --- + public IntegerProperty currentPageProperty() { return currentPage; } + public int getCurrentPage() { return currentPage.get(); } + public void setCurrentPage(int page) { + if (page >= 1 && page <= getPageCount()) { + currentPage.set(page); + } + } + + // --- Zoom level (1.0 = 100%) --- + public DoubleProperty zoomLevelProperty() { return zoomLevel; } + public double getZoomLevel() { return zoomLevel.get(); } + public void setZoomLevel(double zoom) { + zoomLevel.set(Math.max(0.25, Math.min(4.0, zoom))); + } + + // --- Current page image --- + public ObjectProperty currentPageImageProperty() { return currentPageImage; } + public Image getCurrentPageImage() { return currentPageImage.get(); } + public void setCurrentPageImage(Image image) { currentPageImage.set(image); } + + // --- Document loaded (read-only) --- + public ReadOnlyBooleanProperty documentLoadedProperty() { return documentLoaded.getReadOnlyProperty(); } + public boolean isDocumentLoaded() { return documentLoaded.get(); } + + // --- Status text --- + public StringProperty statusTextProperty() { return statusText; } + public String getStatusText() { return statusText.get(); } + public void setStatusText(String text) { statusText.set(text); } + + // --- Navigation helpers --- + public boolean canGoNext() { return getCurrentPage() < getPageCount(); } + public boolean canGoPrev() { return getCurrentPage() > 1; } + + public void nextPage() { + if (canGoNext()) setCurrentPage(getCurrentPage() + 1); + } + + public void prevPage() { + if (canGoPrev()) setCurrentPage(getCurrentPage() - 1); + } + + public void zoomIn() { setZoomLevel(getZoomLevel() + 0.25); } + public void zoomOut() { setZoomLevel(getZoomLevel() - 0.25); } + + public void reset() { + documentFile.set(null); + pageCount.set(0); + currentPage.set(1); + zoomLevel.set(1.0); + currentPageImage.set(null); + statusText.set(""); + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/SignaturePlacementViewModel.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/SignaturePlacementViewModel.java new file mode 100644 index 00000000..5f490030 --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/SignaturePlacementViewModel.java @@ -0,0 +1,121 @@ +package net.sf.jsignpdf.fx.viewmodel; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; + +/** + * ViewModel for the signature placement rectangle on the PDF preview. + * Coordinates are relative (0.0 to 1.0) to the rendered page image. + * Conversion to PDF coordinates (LLX, LLY, URX, URY) is done during signing. + */ +public class SignaturePlacementViewModel { + + private final BooleanProperty placementMode = new SimpleBooleanProperty(false); + private final BooleanProperty placed = new SimpleBooleanProperty(false); + + // Relative coordinates (0.0 - 1.0) on the image + private final DoubleProperty relX = new SimpleDoubleProperty(0); + private final DoubleProperty relY = new SimpleDoubleProperty(0); + private final DoubleProperty relWidth = new SimpleDoubleProperty(0.15); + private final DoubleProperty relHeight = new SimpleDoubleProperty(0.08); + + // --- Placement mode --- + public BooleanProperty placementModeProperty() { return placementMode; } + public boolean isPlacementMode() { return placementMode.get(); } + public void setPlacementMode(boolean mode) { placementMode.set(mode); } + + // --- Placed --- + public BooleanProperty placedProperty() { return placed; } + public boolean isPlaced() { return placed.get(); } + public void setPlaced(boolean p) { placed.set(p); } + + // --- Relative coordinates --- + public DoubleProperty relXProperty() { return relX; } + public double getRelX() { return relX.get(); } + public void setRelX(double v) { + double clamped = clamp(v, 0, 1); + relX.set(clamped); + // Re-clamp width so relX + relWidth never exceeds 1.0 + double maxW = 1 - clamped; + if (relWidth.get() > maxW) { + relWidth.set(Math.max(0.02, maxW)); + } + } + + public DoubleProperty relYProperty() { return relY; } + public double getRelY() { return relY.get(); } + public void setRelY(double v) { + double clamped = clamp(v, 0, 1); + relY.set(clamped); + // Re-clamp height so relY + relHeight never exceeds 1.0 + double maxH = 1 - clamped; + if (relHeight.get() > maxH) { + relHeight.set(Math.max(0.02, maxH)); + } + } + + public DoubleProperty relWidthProperty() { return relWidth; } + public double getRelWidth() { return relWidth.get(); } + public void setRelWidth(double v) { relWidth.set(Math.max(0.02, Math.min(v, 1 - getRelX()))); } + + public DoubleProperty relHeightProperty() { return relHeight; } + public double getRelHeight() { return relHeight.get(); } + public void setRelHeight(double v) { relHeight.set(Math.max(0.02, Math.min(v, 1 - getRelY()))); } + + /** + * Convert relative image coordinates to PDF coordinates. + * PDF origin is bottom-left; image origin is top-left. + * + * @param pageWidth PDF page width in points + * @param pageHeight PDF page height in points + * @return float[4] = {LLX, LLY, URX, URY} + */ + public float[] toPdfCoordinates(float pageWidth, float pageHeight) { + float llx = (float) (getRelX() * pageWidth); + float urx = (float) ((getRelX() + getRelWidth()) * pageWidth); + // PDF Y is inverted: image top=0 -> PDF top=pageHeight + float ury = (float) ((1.0 - getRelY()) * pageHeight); + float lly = (float) ((1.0 - getRelY() - getRelHeight()) * pageHeight); + return new float[]{llx, lly, urx, ury}; + } + + /** + * Set relative image coordinates from PDF coordinates. + * PDF origin is bottom-left; image origin is top-left. + * + * @param llx lower-left X in PDF points + * @param lly lower-left Y in PDF points + * @param urx upper-right X in PDF points + * @param ury upper-right Y in PDF points + * @param pageWidth PDF page width in points + * @param pageHeight PDF page height in points + */ + public void fromPdfCoordinates(float llx, float lly, float urx, float ury, + float pageWidth, float pageHeight) { + double rX = llx / pageWidth; + double rW = (urx - llx) / pageWidth; + // PDF Y is inverted: image top=0 corresponds to PDF top=pageHeight + double rY = 1.0 - ury / pageHeight; + double rH = (ury - lly) / pageHeight; + setRelX(rX); + setRelY(rY); + setRelWidth(rW); + setRelHeight(rH); + setPlaced(true); + } + + public void reset() { + placementMode.set(false); + placed.set(false); + relX.set(0); + relY.set(0); + relWidth.set(0.15); + relHeight.set(0.08); + } + + private static double clamp(double value, double min, double max) { + return Math.max(min, Math.min(max, value)); + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/SigningOptionsViewModel.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/SigningOptionsViewModel.java new file mode 100644 index 00000000..30e574ba --- /dev/null +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/SigningOptionsViewModel.java @@ -0,0 +1,313 @@ +package net.sf.jsignpdf.fx.viewmodel; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.FloatProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleFloatProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import net.sf.jsignpdf.BasicSignerOptions; +import net.sf.jsignpdf.Constants; +import net.sf.jsignpdf.types.CertificationLevel; +import net.sf.jsignpdf.types.HashAlgorithm; +import net.sf.jsignpdf.types.PDFEncryption; +import net.sf.jsignpdf.types.PrintRight; +import net.sf.jsignpdf.types.RenderMode; +import net.sf.jsignpdf.types.ServerAuthentication; + +import java.net.Proxy; + +/** + * JavaFX property adapter wrapping BasicSignerOptions. + * Syncs bidirectionally via syncToOptions() / syncFromOptions(). + */ +public class SigningOptionsViewModel { + + // Certificate settings + private final StringProperty ksType = new SimpleStringProperty(); + private final StringProperty ksFile = new SimpleStringProperty(); + private final StringProperty ksPassword = new SimpleStringProperty(); + private final StringProperty keyAlias = new SimpleStringProperty(); + private final IntegerProperty keyIndex = new SimpleIntegerProperty(Constants.DEFVAL_KEY_INDEX); + private final StringProperty keyPassword = new SimpleStringProperty(); + private final BooleanProperty storePasswords = new SimpleBooleanProperty(false); + + // File settings + private final StringProperty outFile = new SimpleStringProperty(); + private final BooleanProperty append = new SimpleBooleanProperty(Constants.DEFVAL_APPEND); + + // Signature metadata + private final StringProperty signerName = new SimpleStringProperty(); + private final StringProperty reason = new SimpleStringProperty(); + private final StringProperty location = new SimpleStringProperty(); + private final StringProperty contact = new SimpleStringProperty(); + + // Certification & hash + private final ObjectProperty certLevel = new SimpleObjectProperty<>(); + private final ObjectProperty hashAlgorithm = new SimpleObjectProperty<>(); + + // Visible signature + private final BooleanProperty visible = new SimpleBooleanProperty(false); + private final IntegerProperty page = new SimpleIntegerProperty(Constants.DEFVAL_PAGE); + private final FloatProperty positionLLX = new SimpleFloatProperty(Constants.DEFVAL_LLX); + private final FloatProperty positionLLY = new SimpleFloatProperty(Constants.DEFVAL_LLY); + private final FloatProperty positionURX = new SimpleFloatProperty(Constants.DEFVAL_URX); + private final FloatProperty positionURY = new SimpleFloatProperty(Constants.DEFVAL_URY); + private final FloatProperty bgImgScale = new SimpleFloatProperty(Constants.DEFVAL_BG_SCALE); + private final ObjectProperty renderMode = new SimpleObjectProperty<>(); + private final StringProperty l2Text = new SimpleStringProperty(); + private final StringProperty l4Text = new SimpleStringProperty(); + private final FloatProperty l2TextFontSize = new SimpleFloatProperty(Constants.DEFVAL_L2_FONT_SIZE); + private final StringProperty imgPath = new SimpleStringProperty(); + private final StringProperty bgImgPath = new SimpleStringProperty(); + private final BooleanProperty acro6Layers = new SimpleBooleanProperty(Constants.DEFVAL_ACRO6LAYERS); + + // PDF Encryption + private final ObjectProperty pdfEncryption = new SimpleObjectProperty<>(); + private final StringProperty pdfOwnerPassword = new SimpleStringProperty(); + private final StringProperty pdfUserPassword = new SimpleStringProperty(); + private final StringProperty pdfEncryptionCertFile = new SimpleStringProperty(); + + // Rights + private final ObjectProperty rightPrinting = new SimpleObjectProperty<>(); + private final BooleanProperty rightCopy = new SimpleBooleanProperty(true); + private final BooleanProperty rightAssembly = new SimpleBooleanProperty(true); + private final BooleanProperty rightFillIn = new SimpleBooleanProperty(true); + private final BooleanProperty rightScreenReaders = new SimpleBooleanProperty(true); + private final BooleanProperty rightModifyAnnotations = new SimpleBooleanProperty(true); + private final BooleanProperty rightModifyContents = new SimpleBooleanProperty(true); + + // TSA + private final BooleanProperty tsaEnabled = new SimpleBooleanProperty(false); + private final StringProperty tsaUrl = new SimpleStringProperty(); + private final ObjectProperty tsaServerAuthn = new SimpleObjectProperty<>(); + private final StringProperty tsaUser = new SimpleStringProperty(); + private final StringProperty tsaPassword = new SimpleStringProperty(); + private final StringProperty tsaCertFileType = new SimpleStringProperty(); + private final StringProperty tsaCertFile = new SimpleStringProperty(); + private final StringProperty tsaCertFilePassword = new SimpleStringProperty(); + private final StringProperty tsaPolicy = new SimpleStringProperty(); + private final StringProperty tsaHashAlg = new SimpleStringProperty(); + + // OCSP/CRL + private final BooleanProperty ocspEnabled = new SimpleBooleanProperty(false); + private final StringProperty ocspServerUrl = new SimpleStringProperty(); + private final BooleanProperty crlEnabled = new SimpleBooleanProperty(false); + + // Proxy + private final ObjectProperty proxyType = new SimpleObjectProperty<>(Constants.DEFVAL_PROXY_TYPE); + private final StringProperty proxyHost = new SimpleStringProperty(); + private final IntegerProperty proxyPort = new SimpleIntegerProperty(Constants.DEFVAL_PROXY_PORT); + + /** + * Sync values from this ViewModel into a BasicSignerOptions instance. + */ + public void syncToOptions(BasicSignerOptions opts) { + opts.setKsType(ksType.get()); + opts.setKsFile(ksFile.get()); + opts.setKsPasswd(toCharArray(ksPassword.get())); + opts.setKeyAlias(keyAlias.get()); + opts.setKeyIndex(keyIndex.get()); + opts.setKeyPasswd(toCharArray(keyPassword.get())); + opts.setStorePasswords(storePasswords.get()); + opts.setOutFile(outFile.get()); + opts.setAppend(append.get()); + opts.setSignerName(signerName.get()); + opts.setReason(reason.get()); + opts.setLocation(location.get()); + opts.setContact(contact.get()); + opts.setCertLevel(certLevel.get()); + opts.setHashAlgorithm(hashAlgorithm.get()); + + // Visible signature + opts.setVisible(visible.get()); + opts.setPage(page.get()); + opts.setPositionLLX(positionLLX.get()); + opts.setPositionLLY(positionLLY.get()); + opts.setPositionURX(positionURX.get()); + opts.setPositionURY(positionURY.get()); + opts.setBgImgScale(bgImgScale.get()); + opts.setRenderMode(renderMode.get()); + opts.setL2Text(l2Text.get()); + opts.setL4Text(l4Text.get()); + opts.setL2TextFontSize(l2TextFontSize.get()); + opts.setImgPath(imgPath.get()); + opts.setBgImgPath(bgImgPath.get()); + opts.setAcro6Layers(acro6Layers.get()); + + // Encryption + opts.setPdfEncryption(pdfEncryption.get()); + opts.setPdfOwnerPwd(toCharArray(pdfOwnerPassword.get())); + opts.setPdfUserPwd(toCharArray(pdfUserPassword.get())); + opts.setPdfEncryptionCertFile(pdfEncryptionCertFile.get()); + + // Rights + opts.setRightPrinting(rightPrinting.get()); + opts.setRightCopy(rightCopy.get()); + opts.setRightAssembly(rightAssembly.get()); + opts.setRightFillIn(rightFillIn.get()); + opts.setRightScreanReaders(rightScreenReaders.get()); + opts.setRightModifyAnnotations(rightModifyAnnotations.get()); + opts.setRightModifyContents(rightModifyContents.get()); + + // TSA + opts.setTimestamp(tsaEnabled.get()); + opts.setTsaUrl(tsaUrl.get()); + opts.setTsaServerAuthn(tsaServerAuthn.get()); + opts.setTsaUser(tsaUser.get()); + opts.setTsaPasswd(tsaPassword.get()); + opts.setTsaCertFileType(tsaCertFileType.get()); + opts.setTsaCertFile(tsaCertFile.get()); + opts.setTsaCertFilePwd(tsaCertFilePassword.get()); + opts.setTsaPolicy(tsaPolicy.get()); + opts.setTsaHashAlg(tsaHashAlg.get()); + + // OCSP/CRL + opts.setOcspEnabled(ocspEnabled.get()); + opts.setOcspServerUrl(ocspServerUrl.get()); + opts.setCrlEnabled(crlEnabled.get()); + + // Proxy + opts.setProxyType(proxyType.get()); + opts.setProxyHost(proxyHost.get()); + opts.setProxyPort(proxyPort.get()); + } + + /** + * Sync values from a BasicSignerOptions instance into this ViewModel. + */ + public void syncFromOptions(BasicSignerOptions opts) { + ksType.set(opts.getKsType()); + ksFile.set(opts.getKsFile()); + ksPassword.set(fromCharArray(opts.getKsPasswd())); + keyAlias.set(opts.getKeyAlias()); + keyIndex.set(opts.getKeyIndex()); + keyPassword.set(fromCharArray(opts.getKeyPasswd())); + storePasswords.set(opts.isStorePasswords()); + outFile.set(opts.getOutFile()); + append.set(opts.isAppend()); + signerName.set(opts.getSignerName()); + reason.set(opts.getReason()); + location.set(opts.getLocation()); + contact.set(opts.getContact()); + certLevel.set(opts.getCertLevelX()); + hashAlgorithm.set(opts.getHashAlgorithmX()); + + visible.set(opts.isVisible()); + page.set(opts.getPage()); + positionLLX.set(opts.getPositionLLX()); + positionLLY.set(opts.getPositionLLY()); + positionURX.set(opts.getPositionURX()); + positionURY.set(opts.getPositionURY()); + bgImgScale.set(opts.getBgImgScale()); + renderMode.set(opts.getRenderMode()); + l2Text.set(opts.getL2Text()); + l4Text.set(opts.getL4Text()); + l2TextFontSize.set(opts.getL2TextFontSize()); + imgPath.set(opts.getImgPath()); + bgImgPath.set(opts.getBgImgPath()); + acro6Layers.set(opts.isAcro6Layers()); + + pdfEncryption.set(opts.getPdfEncryption()); + pdfOwnerPassword.set(opts.getPdfOwnerPwdStr()); + pdfUserPassword.set(opts.getPdfUserPwdStr()); + pdfEncryptionCertFile.set(opts.getPdfEncryptionCertFile()); + + rightPrinting.set(opts.getRightPrinting()); + rightCopy.set(opts.isRightCopy()); + rightAssembly.set(opts.isRightAssembly()); + rightFillIn.set(opts.isRightFillIn()); + rightScreenReaders.set(opts.isRightScreanReaders()); + rightModifyAnnotations.set(opts.isRightModifyAnnotations()); + rightModifyContents.set(opts.isRightModifyContents()); + + tsaEnabled.set(opts.isTimestamp()); + tsaUrl.set(opts.getTsaUrl()); + tsaServerAuthn.set(opts.getTsaServerAuthn()); + tsaUser.set(opts.getTsaUser()); + tsaPassword.set(opts.getTsaPasswd()); + tsaCertFileType.set(opts.getTsaCertFileType()); + tsaCertFile.set(opts.getTsaCertFile()); + tsaCertFilePassword.set(opts.getTsaCertFilePwd()); + tsaPolicy.set(opts.getTsaPolicy()); + tsaHashAlg.set(opts.getTsaHashAlg()); + + ocspEnabled.set(opts.isOcspEnabled()); + ocspServerUrl.set(opts.getOcspServerUrl()); + crlEnabled.set(opts.isCrlEnabled()); + + proxyType.set(opts.getProxyType()); + proxyHost.set(opts.getProxyHost()); + proxyPort.set(opts.getProxyPort()); + } + + private static char[] toCharArray(String s) { + return s != null ? s.toCharArray() : null; + } + + private static String fromCharArray(char[] c) { + return c != null ? new String(c) : null; + } + + // --- Property accessors --- + public StringProperty ksTypeProperty() { return ksType; } + public StringProperty ksFileProperty() { return ksFile; } + public StringProperty ksPasswordProperty() { return ksPassword; } + public StringProperty keyAliasProperty() { return keyAlias; } + public IntegerProperty keyIndexProperty() { return keyIndex; } + public StringProperty keyPasswordProperty() { return keyPassword; } + public BooleanProperty storePasswordsProperty() { return storePasswords; } + public StringProperty outFileProperty() { return outFile; } + public BooleanProperty appendProperty() { return append; } + public StringProperty signerNameProperty() { return signerName; } + public StringProperty reasonProperty() { return reason; } + public StringProperty locationProperty() { return location; } + public StringProperty contactProperty() { return contact; } + public ObjectProperty certLevelProperty() { return certLevel; } + public ObjectProperty hashAlgorithmProperty() { return hashAlgorithm; } + public BooleanProperty visibleProperty() { return visible; } + public IntegerProperty pageProperty() { return page; } + public FloatProperty positionLLXProperty() { return positionLLX; } + public FloatProperty positionLLYProperty() { return positionLLY; } + public FloatProperty positionURXProperty() { return positionURX; } + public FloatProperty positionURYProperty() { return positionURY; } + public FloatProperty bgImgScaleProperty() { return bgImgScale; } + public ObjectProperty renderModeProperty() { return renderMode; } + public StringProperty l2TextProperty() { return l2Text; } + public StringProperty l4TextProperty() { return l4Text; } + public FloatProperty l2TextFontSizeProperty() { return l2TextFontSize; } + public StringProperty imgPathProperty() { return imgPath; } + public StringProperty bgImgPathProperty() { return bgImgPath; } + public BooleanProperty acro6LayersProperty() { return acro6Layers; } + public ObjectProperty pdfEncryptionProperty() { return pdfEncryption; } + public StringProperty pdfOwnerPasswordProperty() { return pdfOwnerPassword; } + public StringProperty pdfUserPasswordProperty() { return pdfUserPassword; } + public StringProperty pdfEncryptionCertFileProperty() { return pdfEncryptionCertFile; } + public ObjectProperty rightPrintingProperty() { return rightPrinting; } + public BooleanProperty rightCopyProperty() { return rightCopy; } + public BooleanProperty rightAssemblyProperty() { return rightAssembly; } + public BooleanProperty rightFillInProperty() { return rightFillIn; } + public BooleanProperty rightScreenReadersProperty() { return rightScreenReaders; } + public BooleanProperty rightModifyAnnotationsProperty() { return rightModifyAnnotations; } + public BooleanProperty rightModifyContentsProperty() { return rightModifyContents; } + public BooleanProperty tsaEnabledProperty() { return tsaEnabled; } + public StringProperty tsaUrlProperty() { return tsaUrl; } + public ObjectProperty tsaServerAuthnProperty() { return tsaServerAuthn; } + public StringProperty tsaUserProperty() { return tsaUser; } + public StringProperty tsaPasswordProperty() { return tsaPassword; } + public StringProperty tsaCertFileTypeProperty() { return tsaCertFileType; } + public StringProperty tsaCertFileProperty() { return tsaCertFile; } + public StringProperty tsaCertFilePasswordProperty() { return tsaCertFilePassword; } + public StringProperty tsaPolicyProperty() { return tsaPolicy; } + public StringProperty tsaHashAlgProperty() { return tsaHashAlg; } + public BooleanProperty ocspEnabledProperty() { return ocspEnabled; } + public StringProperty ocspServerUrlProperty() { return ocspServerUrl; } + public BooleanProperty crlEnabledProperty() { return crlEnabled; } + public ObjectProperty proxyTypeProperty() { return proxyType; } + public StringProperty proxyHostProperty() { return proxyHost; } + public IntegerProperty proxyPortProperty() { return proxyPort; } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/utils/KeyStoreUtils.java b/jsignpdf/src/main/java/net/sf/jsignpdf/utils/KeyStoreUtils.java index d1e66f96..e3f1183f 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/utils/KeyStoreUtils.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/utils/KeyStoreUtils.java @@ -411,6 +411,9 @@ public static PrivateKeyInfo getPkInfo(BasicSignerOptions options) final KeyStore tmpKs = loadKeyStore(options.getKsType(), options.getKsFile(), options.getKsPasswd()); String tmpAlias = getKeyAliasInternal(options, tmpKs); + if (tmpAlias == null) { + return null; + } LOGGER.info(RES.get("console.getPrivateKey")); final PrivateKey tmpPk = (PrivateKey) tmpKs.getKey(tmpAlias, options.getKeyPasswdX()); LOGGER.info(RES.get("console.getCertChain")); diff --git a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/styles/jsignpdf.css b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/styles/jsignpdf.css new file mode 100644 index 00000000..ad188cd1 --- /dev/null +++ b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/styles/jsignpdf.css @@ -0,0 +1,81 @@ +/* JSignPdf JavaFX Light Theme */ + +.root { + -fx-font-family: "System"; + -fx-font-size: 13px; + -fx-base: #f0f0f0; + -fx-background: #ffffff; +} + +/* Toolbar */ +.main-toolbar { + -fx-padding: 4 8 4 8; + -fx-spacing: 4; +} + +.toolbar-button { + -fx-padding: 4 8 4 8; +} + +.sign-button { + -fx-font-weight: bold; + -fx-text-fill: #2e7d32; +} + +/* Side Panel */ +.side-panel { + -fx-background-color: #f5f5f5; +} + +.placeholder-label { + -fx-text-fill: #888888; + -fx-padding: 16; + -fx-wrap-text: true; +} + +/* PDF Area */ +.pdf-scroll-pane { + -fx-background: #e0e0e0; + -fx-background-color: #e0e0e0; +} + +.pdf-scroll-pane > .viewport { + -fx-background-color: #e0e0e0; +} + +.pdf-area { + -fx-background-color: #e0e0e0; + -fx-alignment: center; +} + +.drop-hint { + -fx-font-size: 18px; + -fx-text-fill: #999999; + -fx-font-style: italic; +} + +/* Status Bar */ +.status-bar { + -fx-background-color: #f0f0f0; + -fx-border-color: #cccccc transparent transparent transparent; + -fx-border-width: 1 0 0 0; +} + +/* Signature overlay */ +.signature-rect { + -fx-stroke: #1565c0; + -fx-stroke-width: 2; + -fx-fill: rgba(21, 101, 192, 0.15); + -fx-stroke-dash-array: 6 4; +} + +.signature-handle { + -fx-fill: #1565c0; + -fx-stroke: #ffffff; + -fx-stroke-width: 1; +} + +/* Accordion in side panel */ +.titled-pane > .title { + -fx-font-weight: bold; +} diff --git a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/CertificateSettings.fxml b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/CertificateSettings.fxml new file mode 100644 index 00000000..ff22c208 --- /dev/null +++ b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/CertificateSettings.fxml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + diff --git a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/EncryptionSettings.fxml b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/EncryptionSettings.fxml new file mode 100644 index 00000000..fd58539d --- /dev/null +++ b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/EncryptionSettings.fxml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml new file mode 100644 index 00000000..db224fde --- /dev/null +++ b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + diff --git a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/OutputConsole.fxml b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/OutputConsole.fxml new file mode 100644 index 00000000..e80a797b --- /dev/null +++ b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/OutputConsole.fxml @@ -0,0 +1,23 @@ + + + + + + + + + + + + +