Skip to content

feat(pdf-tools): add pdf-tools extension - #405

Open
manuelcontrera wants to merge 3 commits into
vicinaehq:mainfrom
manuelcontrera:feat/add-pdf-tools
Open

manuelcontrera wants to merge 3 commits into
vicinaehq:mainfrom
manuelcontrera:feat/add-pdf-tools

Conversation

@manuelcontrera

Copy link
Copy Markdown

Summary

Adds the PDF Tools extension adapted and enhanced for Vicinae, with full cross-platform support for Linux, macOS, and Windows.

This extension brings a full suite of PDF manipulation, encryption, splitting, vector watermarking, and multi-format image conversion tools directly into Vicinae.

Features

  • Convert Images to PDF (images-to-pdf): Merge and convert images of any format (.png, .jpg, .jpeg, .tiff, .webp, .bmp, .gif, .avif) into a single PDF ordered by natural filename sort ([img1, img2, img10]). Preserves resolution, handles EXIF rotation, and blends alpha transparency cleanly over white.
  • Merge PDF Files (merge): Combine two or more PDF files into a single unified document in the source folder.
  • Protect PDF (protect): Encrypt PDF files with AES-256 password protection in place.
  • Unlock PDF (unlock): Decrypt password-protected PDF files in place.
  • Split PDF by File Size (split-by-file-size): Split large PDFs into parts based on a maximum MB threshold using an optimized binary search.
  • Split PDF by Page Count (split-by-page-count): Split PDFs into parts with a specified number of pages each.
  • Add Watermark (watermark): Pure JS vector watermarking engine via pdf-lib, applying text watermarks across all pages with custom angle (0° or 45°) and opacity.

Cross-Platform File Selection Engine

  • Active File Selection:
    • macOS: Reads selected items directly from Finder via AppleScript.
    • Linux: Detects clipboard selections via wl-paste (Wayland uri-list / gnome-copied-files) and @vicinae/api Clipboard.
    • Windows: Reads copied files from clipboard via PowerShell Get-Clipboard -Format FileDropList and @vicinae/api Clipboard.
  • Interactive File Dialog Fallback:
    • Automatically prompts the native OS file picker when no files were previously selected or copied (zenity / kdialog on Linux, AppleScript dialog on macOS, OpenFileDialog on Windows).

Implementation Notes

  • Tested on Vicinae with @vicinae/api ^0.29.0.
  • Passes vici lint (valid manifest) and vici build with zero errors.
  • External dependencies (qpdf and Python Pillow) are documented with cross-platform installation instructions, and provide friendly error toasts if missing.

Credits

@clankus-aurelius

clankus-aurelius commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Thanks for contributing an extension to Vicinae! 👋

Before publication, this pull request receives two reviews:

  1. An automated review for extension guidelines, safety, error handling, and likely correctness issues.
  2. A final review from a Vicinae maintainer.

✅ Ready for human review. The automated reviewer approved the latest commit and a maintainer has been notified.

No blocking findings remain on the latest commit.

The automated reviewer examines only the current commit. New commits invalidate its previous decision and start another review.

@clankus-aurelius clankus-aurelius added the ai-reviewing Automated extension review is running label Sep 23, 2026

@clankus-aurelius clankus-aurelius left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extension has two publication-blocking data/credential exposure issues, plus cross-platform selection and documentation defects.


Automated review found 2 publication-blocking issues.

This is an AI-generated first pass and may be mistaken. If a finding is unclear or incorrect, reply in the relevant thread and mention @aurelleb.

Comment on lines +112 to +117
}

/**
* Unlock a password-protected PDF file in place.
*/
export async function unlockPDF(filePath: string, password: string): Promise<void> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking — Password is exposed in failure messages

Rule: SECURITY-003

The password is interpolated into the shell command, and non-installation failures pass exec's raw err.message upward. That message includes the invoked command, and callers display it in a failure toast, exposing the entered password.

Suggested resolution: Pass qpdf arguments without constructing a shell command and surface a sanitized error that never includes the password or full command.

Comment on lines +52 to +57
const ext = path.extname(filePath);
const baseName = path.basename(filePath, ext);
const outputPath = path.join(dir, `${baseName} [watermarked].pdf`);

fs.writeFileSync(outputPath, pdfBytes);
return outputPath;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking — Watermark output silently overwrites an existing file

Rule: CORRECTNESS-001

Every run writes to the fixed <name> [watermarked].pdf path with writeFileSync, replacing any existing document at that path without confirmation.

Suggested resolution: Generate a collision-free output name or explicitly confirm replacement before writing.

Comment thread extensions/pdf-tools/src/lib/qpdf.ts Outdated
Comment on lines +10 to +12
*/
function escapeArg(arg: string): string {
return `"${arg.replace(/(["\\$`])/g, "\\$1")}"`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Warning — Shell escaping is not valid on Windows

Rule: CORRECTNESS-001

escapeArg implements POSIX-style backslash escaping, but commands run through cmd.exe on Windows. For example, %NAME% sequences in passwords or paths are expanded, so qpdf receives different values and protect/unlock can fail or use an unintended password.

Suggested resolution: Replace shell-built qpdf commands with execFile/spawn and pass every option and path as a separate argument.

Comment on lines +168 to +176
const mult = multiple ? "with multiple selections allowed" : "";
const typeList = allowedExtensions.map((e) => `"${e.replace(/^\./, "")}"`).join(",");
const script = `choose file with prompt "${prompt}" of type {${typeList}} ${mult}`;
const { stdout } = await execAsync(`osascript -e '${script}'`);
if (!stdout.trim()) {
throw new Error("File selection cancelled");
}
const paths = stdout
.split(",")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Warning — macOS dialog results are not converted to POSIX paths

Rule: CORRECTNESS-001

AppleScript choose file returns aliases, but the result is passed directly to cleanFilePath, which expects filesystem paths. When Finder and clipboard selection are unavailable, valid dialog selections are therefore discarded.

Suggested resolution: Have the AppleScript convert each selected alias to its POSIX path and emit an unambiguous delimiter before calling cleanFilePath.

## Universal File Selection

PDF Tools provides a frictionless file selection experience across all operating systems:
1. **Active Finder / Explorer Selection**: Directly picks files currently selected in macOS Finder.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Warning — Linux file-picker support is overstated

Rule: MANIFEST-001

The README promises a zenity / kdialog fallback, but pickFilesWithDialog only invokes zenity; KDE systems with only kdialog receive a failure.

Suggested resolution: Implement the documented kdialog fallback or remove kdialog from the documented behavior.

Suggested change
1. **Active Finder / Explorer Selection**: Directly picks files currently selected in macOS Finder.
3. **Native File Picker Fallback**: If no files were previously selected or copied, an interactive native file dialog opens automatically (`zenity` on Linux, AppleScript dialog on macOS, `OpenFileDialog` on Windows).

@clankus-aurelius clankus-aurelius added ai-changes-requested Automated review found blocking issues and removed ai-reviewing Automated extension review is running labels Sep 23, 2026
@clankus-aurelius clankus-aurelius added ai-reviewing Automated extension review is running and removed ai-changes-requested Automated review found blocking issues labels Sep 23, 2026

@clankus-aurelius clankus-aurelius left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prior findings are resolved, but the new Linux fallback mishandles normal dialog cancellation.


Automated extension review passed. A maintainer review is still required.

This is an AI-generated first pass and may be mistaken. If a finding is unclear or incorrect, reply in the relevant thread and mention @aurelleb.

Comment on lines +162 to +166
}

try {
const multFlag = multiple ? "--multiple --separate-output" : "";
const patterns = allowedExtensions

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Warning — Cancelling Zenity opens another file picker

Rule: CORRECTNESS-001

Zenity reports cancellation by exiting nonzero, so execAsync rejects before the empty-output check. The resulting process error does not necessarily contain “cancelled”; this catch then launches kdialog, turning a normal cancellation into a second unexpected dialog and potentially a misleading installation error.

Suggested resolution: Recognize Zenity's cancellation exit code and return a cancellation error. Fall back to kdialog only when Zenity is unavailable or fails for a non-cancellation reason; apply equivalent cancellation handling to kdialog.

@clankus-aurelius clankus-aurelius added human-reviewable Automated review passed; ready for maintainer review and removed ai-reviewing Automated extension review is running labels Sep 23, 2026
@clankus-aurelius

Copy link
Copy Markdown
Collaborator

@aurelleb automated review passed for 0233489; this extension is ready for your review.

@clankus-aurelius clankus-aurelius added ai-reviewing Automated extension review is running and removed human-reviewable Automated review passed; ready for maintainer review labels Sep 23, 2026

@clankus-aurelius clankus-aurelius left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous Linux dialog-cancellation issue is resolved. No new actionable problems were introduced by the incremental changes.


Automated extension review passed. A maintainer review is still required.

This is an AI-generated first pass and may be mistaken. If a finding is unclear or incorrect, reply in the relevant thread and mention @aurelleb.

@clankus-aurelius clankus-aurelius added human-reviewable Automated review passed; ready for maintainer review and removed ai-reviewing Automated extension review is running labels Sep 23, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-reviewable Automated review passed; ready for maintainer review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants