mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b2b94ad74 | ||
|
|
17c32f0b7d | ||
|
|
b93129bc44 | ||
|
|
b23e60a704 | ||
|
|
c559b34f3e | ||
|
|
a780f2984a | ||
|
|
0c9961d412 | ||
|
|
ed7a80f99e | ||
|
|
18b7d82610 | ||
|
|
cd1f7ac37d |
@@ -18,8 +18,16 @@
|
|||||||
"Bash(npm install:*)",
|
"Bash(npm install:*)",
|
||||||
"Bash(npm uninstall:*)",
|
"Bash(npm uninstall:*)",
|
||||||
"Bash(git add:*)",
|
"Bash(git add:*)",
|
||||||
"Bash(git commit:*)"
|
"Bash(git commit:*)",
|
||||||
|
"Bash(npm start)",
|
||||||
|
"Read(//h/**)",
|
||||||
|
"Bash(cd:*)",
|
||||||
|
"Bash(npm run build:win-unsigned:*)",
|
||||||
|
"Bash(dir:*)",
|
||||||
|
"Bash(git merge:*)",
|
||||||
|
"Bash(timeout:*)",
|
||||||
|
"Bash(findstr:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
name: New MCP server
|
||||||
|
version: 0.0.1
|
||||||
|
schema: v1
|
||||||
|
mcpServers:
|
||||||
|
- name: New MCP server
|
||||||
|
command: npx
|
||||||
|
args:
|
||||||
|
- -y
|
||||||
|
- <your-mcp-server>
|
||||||
|
env: {}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**PanConverter** is a cross-platform Markdown editor and converter powered by Pandoc, built with Electron. It provides professional-grade editing capabilities with comprehensive export options.
|
**PanConverter** is a cross-platform Markdown editor and converter powered by Pandoc, built with Electron. It provides professional-grade editing capabilities with comprehensive export options.
|
||||||
|
|
||||||
**Current Version**: v1.5.0
|
**Current Version**: v1.6.1
|
||||||
**Author**: Amit Haridas (amit.wh@gmail.com)
|
**Author**: Amit Haridas (amit.wh@gmail.com)
|
||||||
**License**: MIT
|
**License**: MIT
|
||||||
**Repository**: https://github.com/amitwh/pan-converter
|
**Repository**: https://github.com/amitwh/pan-converter
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
### Core Technologies
|
### Core Technologies
|
||||||
- **Electron** - Cross-platform desktop application framework
|
- **Electron** - Cross-platform desktop application framework
|
||||||
- **Pandoc** - Universal document converter (required system dependency)
|
- **Pandoc** - Universal document converter (required system dependency)
|
||||||
|
- **ConvertAPI** - Cloud-based file conversion (200+ formats)
|
||||||
- **marked** - Markdown parsing and rendering
|
- **marked** - Markdown parsing and rendering
|
||||||
- **highlight.js** - Syntax highlighting
|
- **highlight.js** - Syntax highlighting
|
||||||
- **KaTeX** - Mathematical expression rendering
|
- **KaTeX** - Mathematical expression rendering
|
||||||
@@ -109,7 +110,120 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
|
|||||||
|
|
||||||
## Feature Implementation Guide
|
## Feature Implementation Guide
|
||||||
|
|
||||||
### v1.5.0 Enhanced Features & Open Source Compatibility (Latest)
|
### v1.7.0 PDF Editor & Universal Converter (Latest)
|
||||||
|
|
||||||
|
#### 📄 Comprehensive PDF Editor
|
||||||
|
**Complete PDF Manipulation Suite** (`src/main.js:1566-2068`, `src/index.html:552-921`, `src/renderer.js:1951-2429`)
|
||||||
|
- **Merge PDFs** - Combine multiple PDF files into a single document
|
||||||
|
- **Split PDF** - Split PDFs by page ranges, intervals, or file size
|
||||||
|
- **Compress PDF** - Reduce PDF file size with optimization
|
||||||
|
- **Rotate Pages** - Rotate specific pages or entire documents (90°, 180°, 270°)
|
||||||
|
- **Delete Pages** - Remove unwanted pages from PDFs
|
||||||
|
- **Reorder Pages** - Rearrange pages in any order
|
||||||
|
- **Add Watermark** - Text watermarks with customizable position, size, color, opacity
|
||||||
|
- **Password Protection** - Encrypt PDFs with user and owner passwords
|
||||||
|
- **Remove Password** - Decrypt password-protected PDFs
|
||||||
|
- **Set Permissions** - Control printing, copying, modifying, and other document permissions
|
||||||
|
|
||||||
|
**Technical Implementation:**
|
||||||
|
- Uses `pdf-lib` (v1.17.1) for all PDF operations
|
||||||
|
- Full async/await implementation for optimal performance
|
||||||
|
- Comprehensive error handling and user feedback
|
||||||
|
- Progress indicators for long-running operations
|
||||||
|
- Page range parsing (e.g., "1-5, 7, 9-12")
|
||||||
|
- RGB color conversion for watermarks
|
||||||
|
- Support for encrypted PDFs
|
||||||
|
|
||||||
|
**Watermark Features:**
|
||||||
|
- Multiple positioning options (center, diagonal, corners, top/bottom)
|
||||||
|
- Customizable font size (8-144px)
|
||||||
|
- Adjustable opacity (0-100%)
|
||||||
|
- Color picker for text color
|
||||||
|
- Apply to all pages or custom page ranges
|
||||||
|
|
||||||
|
**Security Features:**
|
||||||
|
- 128-bit and 256-bit encryption support
|
||||||
|
- User password (required to open PDF)
|
||||||
|
- Owner password (required to modify permissions)
|
||||||
|
- Granular permission controls:
|
||||||
|
- Allow/deny printing (high/low resolution)
|
||||||
|
- Allow/deny content modification
|
||||||
|
- Allow/deny content copying
|
||||||
|
- Allow/deny annotations
|
||||||
|
- Allow/deny form filling
|
||||||
|
- Allow/deny accessibility features
|
||||||
|
- Allow/deny document assembly
|
||||||
|
- Allow/deny high-quality printing
|
||||||
|
|
||||||
|
#### 🔄 Universal File Converter
|
||||||
|
**Open-Source Multi-Tool Converter** (`src/main.js:446-600`, `src/index.html:257-483`, `src/renderer.js:1372-1940`)
|
||||||
|
- **LibreOffice** - Document conversions (DOCX, PDF, ODT, RTF, TXT, HTML, XLSX, PPTX)
|
||||||
|
- **ImageMagick** - Image format conversions (JPG, PNG, GIF, TIFF, WebP, SVG, etc.)
|
||||||
|
- **FFmpeg** - Video and audio conversions (MP4, AVI, MOV, MP3, WAV, etc.)
|
||||||
|
- **Pandoc** - Document markup conversions (Markdown, HTML, LaTeX, EPUB, etc.)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Single file and batch folder conversion
|
||||||
|
- Tool-specific advanced options:
|
||||||
|
- ImageMagick: Quality, DPI, resize, compression type
|
||||||
|
- FFmpeg: Video/audio codecs, bitrate, preset, framerate
|
||||||
|
- LibreOffice: Export quality, page range, bookmarks
|
||||||
|
- Automatic tool detection and availability checking
|
||||||
|
- Recursive folder processing
|
||||||
|
- 100% offline and open-source
|
||||||
|
- No API keys required
|
||||||
|
|
||||||
|
### v1.6.1 Bug Fixes
|
||||||
|
|
||||||
|
#### 🐛 File Association Rendering Fix
|
||||||
|
**Fixed Double-Click File Loading** (`src/main.js:1364-1379`)
|
||||||
|
- Fixed critical issue where files opened via double-click weren't rendering
|
||||||
|
- Added proper wait for renderer load state before sending file data
|
||||||
|
- Implemented `did-finish-load` event listener to prevent race conditions
|
||||||
|
- Files now render correctly when opened from Windows Explorer
|
||||||
|
|
||||||
|
### v1.6.0 Enhanced Markdown Editor & ConvertAPI Integration
|
||||||
|
|
||||||
|
#### ✨ Complete Markdown Toolbar
|
||||||
|
**Additional Formatting Buttons** (`src/index.html:78-98`, `src/renderer.js:596-723`)
|
||||||
|
- **Strikethrough** button - Wraps text with `~~strikethrough~~`
|
||||||
|
- **Code Block** button - Inserts fenced code blocks with triple backticks
|
||||||
|
- **Horizontal Rule** button - Adds section divider with `---`
|
||||||
|
- Complete suite of markdown formatting tools now available
|
||||||
|
|
||||||
|
#### 🔍 Fixed Find & Replace
|
||||||
|
**Focus and Highlighting Improvements** (`src/renderer.js:725-870`)
|
||||||
|
- Fixed focus issue - dialog no longer loses focus while typing
|
||||||
|
- Visual highlighting with text selection showing current match
|
||||||
|
- Smart auto-scroll to display matches prominently
|
||||||
|
- Real-time match counting: "Match X of Y"
|
||||||
|
- Replace single match or replace all functionality
|
||||||
|
- Enter/Shift+Enter keyboard navigation
|
||||||
|
|
||||||
|
#### 📏 Fixed Line Numbers
|
||||||
|
**Line Counting and Synchronization** (`src/renderer.js:337-355`)
|
||||||
|
- Fixed bug using `'\\n'` instead of `'\n'` for line splitting
|
||||||
|
- Added scroll synchronization between line numbers and editor
|
||||||
|
- Line numbers update correctly as you type
|
||||||
|
- Smooth scrolling keeps line numbers aligned
|
||||||
|
|
||||||
|
#### ☁️ ConvertAPI Cloud Integration
|
||||||
|
**200+ Format Cloud Conversion** (`src/main.js:370-416`, `src/index.html:257-307`, `src/renderer.js:1540-1603`)
|
||||||
|
- New **ConvertAPI menu** with cloud-based file conversion
|
||||||
|
- Support for 200+ file formats: MD, DOCX, PDF, HTML, JPG, PNG, EPUB, ODT
|
||||||
|
- Secure API key storage in localStorage for reuse
|
||||||
|
- Real-time conversion status and progress tracking
|
||||||
|
- Free tier: 250 conversions per month
|
||||||
|
- Professional conversion dialog with format selection
|
||||||
|
- Get API key at: https://www.convertapi.com
|
||||||
|
|
||||||
|
**Technical Implementation:**
|
||||||
|
- Installed `convertapi` npm package (v1.15.0)
|
||||||
|
- IPC handlers for cloud conversion workflow
|
||||||
|
- Error handling and user feedback dialogs
|
||||||
|
- Async/await pattern for conversion operations
|
||||||
|
|
||||||
|
### v1.5.0 Enhanced Features & Open Source Compatibility
|
||||||
|
|
||||||
#### 🔧 Export Function Fixes & Optional Advanced Options
|
#### 🔧 Export Function Fixes & Optional Advanced Options
|
||||||
**Export Dialog Enhancement** (`src/index.html:125-133`, `src/renderer.js:960-985`)
|
**Export Dialog Enhancement** (`src/index.html:125-133`, `src/renderer.js:960-985`)
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ Download the latest release for your platform from the [Releases](https://github
|
|||||||
|
|
||||||
#### Linux
|
#### Linux
|
||||||
- **AppImage**: `PanConverter-1.5.0.AppImage` (universal, may require `--no-sandbox` flag)
|
- **AppImage**: `PanConverter-1.5.0.AppImage` (universal, may require `--no-sandbox` flag)
|
||||||
- **Debian Package**: `pan-converter_1.5.0_amd64.deb`
|
- **Debian Package**: `pan-converter_1.5.6_amd64.deb`
|
||||||
- **Snap Package**: `pan-converter_1.5.0_amd64.snap`
|
- **Snap Package**: `pan-converter_1.5.6_amd64.snap`
|
||||||
|
|
||||||
### Install from Source
|
### Install from Source
|
||||||
```bash
|
```bash
|
||||||
@@ -138,6 +138,8 @@ npm run dist:all
|
|||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
|
|
||||||
|
- **v1.5.6** - (Your release notes here)
|
||||||
|
- **v1.5.5** - Refactored PDF export, simplified Pandoc pathing, and removed XLSX dependency.
|
||||||
- **v1.3.1** - Bug fixes: Fixed file associations for double-clicking .md files, corrected 50/50 layout alignment for editor/preview panes
|
- **v1.3.1** - Bug fixes: Fixed file associations for double-clicking .md files, corrected 50/50 layout alignment for editor/preview panes
|
||||||
- **v1.3.0** - Major update: Tabbed interface for multiple files, enhanced PDF export with LaTeX engines, fixed file associations, removed redundant converter menu, improved UI architecture
|
- **v1.3.0** - Major update: Tabbed interface for multiple files, enhanced PDF export with LaTeX engines, fixed file associations, removed redundant converter menu, improved UI architecture
|
||||||
- **v1.2.1** - Comprehensive editor enhancements: Find & Replace, Line Numbers, Undo/Redo, Auto-indentation, PowerPoint export, document conversion menu, table creation helper, spreadsheet export
|
- **v1.2.1** - Comprehensive editor enhancements: Find & Replace, Line Numbers, Undo/Redo, Auto-indentation, PowerPoint export, document conversion menu, table creation helper, spreadsheet export
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 5.5 KiB |
+15
-13
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pan-converter",
|
"name": "pan-converter",
|
||||||
"version": "1.5.0",
|
"version": "1.7.0",
|
||||||
"description": "Cross-platform Markdown editor and converter using Pandoc",
|
"description": "Cross-platform Markdown editor and converter using Pandoc",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
"electron-store": "^10.1.0",
|
"electron-store": "^10.1.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"marked": "^16.2.1",
|
"marked": "^16.2.1",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
"directories": {
|
"directories": {
|
||||||
"output": "dist"
|
"output": "dist"
|
||||||
},
|
},
|
||||||
|
"icon": "assets/icon",
|
||||||
"files": [
|
"files": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
@@ -68,37 +70,38 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mac": {
|
"mac": {
|
||||||
"category": "public.app-category.productivity",
|
"category": "public.app-category.productivity"
|
||||||
"icon": "assets/icon.icns"
|
|
||||||
},
|
},
|
||||||
"win": {
|
"win": {
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"arch": ["x64"]
|
"arch": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"target": "portable",
|
"target": "portable",
|
||||||
"arch": ["x64"]
|
"arch": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"target": "zip",
|
"target": "zip",
|
||||||
"arch": ["x64"]
|
"arch": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "assets/icon.png",
|
|
||||||
"artifactName": "${productName}-${version}-${arch}.${ext}",
|
"artifactName": "${productName}-${version}-${arch}.${ext}",
|
||||||
"requestedExecutionLevel": "asInvoker",
|
"requestedExecutionLevel": "asInvoker",
|
||||||
"signAndEditExecutable": true
|
"signAndEditExecutable": false
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
"perMachine": false,
|
"perMachine": false,
|
||||||
"allowToChangeInstallationDirectory": true,
|
"allowToChangeInstallationDirectory": true,
|
||||||
"displayLanguageSelector": true,
|
"displayLanguageSelector": true,
|
||||||
"installerIcon": "assets/icon-proper.ico",
|
|
||||||
"uninstallerIcon": "assets/icon-proper.ico",
|
|
||||||
"installerHeaderIcon": "assets/icon-proper.ico",
|
|
||||||
"createDesktopShortcut": true,
|
"createDesktopShortcut": true,
|
||||||
"createStartMenuShortcut": true,
|
"createStartMenuShortcut": true,
|
||||||
"shortcutName": "PanConverter",
|
"shortcutName": "PanConverter",
|
||||||
@@ -116,8 +119,7 @@
|
|||||||
"AppImage",
|
"AppImage",
|
||||||
"snap"
|
"snap"
|
||||||
],
|
],
|
||||||
"category": "Utility",
|
"category": "Utility"
|
||||||
"icon": "assets/icon.png"
|
|
||||||
},
|
},
|
||||||
"deb": {
|
"deb": {
|
||||||
"depends": [
|
"depends": [
|
||||||
|
|||||||
+620
@@ -75,6 +75,27 @@
|
|||||||
<line x1="15" y1="3" x2="15" y2="21"></line>
|
<line x1="15" y1="3" x2="15" y2="21"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="btn-strikethrough" title="Strikethrough">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M16 4H9a3 3 0 0 0-2.83 4"></path>
|
||||||
|
<path d="M14 12a4 4 0 0 1 0 8H6"></path>
|
||||||
|
<line x1="4" y1="12" x2="20" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button id="btn-code-block" title="Code Block">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="2" y="6" width="20" height="12" rx="2"></rect>
|
||||||
|
<path d="m10 10-2 2 2 2"></path>
|
||||||
|
<path d="m14 10 2 2-2 2"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button id="btn-horizontal-rule" title="Horizontal Rule">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||||
|
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||||
|
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<div class="toolbar-separator"></div>
|
<div class="toolbar-separator"></div>
|
||||||
<button id="btn-find" title="Find & Replace (Ctrl+F)">
|
<button id="btn-find" title="Find & Replace (Ctrl+F)">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
@@ -233,6 +254,234 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Universal File Converter Dialog -->
|
||||||
|
<div id="universal-converter-dialog" class="export-dialog hidden">
|
||||||
|
<div class="export-dialog-content">
|
||||||
|
<div class="export-dialog-header">
|
||||||
|
<h3>Universal File Converter</h3>
|
||||||
|
<button id="converter-dialog-close" title="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="export-dialog-body">
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Select File to Convert:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="converter-file-path" placeholder="Browse for a file..." readonly>
|
||||||
|
<button id="browse-converter-file">Browse</button>
|
||||||
|
</div>
|
||||||
|
<small>Supported formats: Documents, Images, Media files</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="converter-tool">Conversion Tool:</label>
|
||||||
|
<select id="converter-tool">
|
||||||
|
<option value="libreoffice">LibreOffice (Documents)</option>
|
||||||
|
<option value="imagemagick">ImageMagick (Images)</option>
|
||||||
|
<option value="ffmpeg">FFmpeg (Media)</option>
|
||||||
|
<option value="pandoc">Pandoc (Markup)</option>
|
||||||
|
</select>
|
||||||
|
<small id="converter-tool-help">Documents: DOCX, PDF, ODT, RTF, TXT, HTML, XLSX, PPTX</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="converter-from">Input Format:</label>
|
||||||
|
<select id="converter-from">
|
||||||
|
<!-- Dynamically populated based on selected converter tool -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="converter-to">Output Format:</label>
|
||||||
|
<select id="converter-to">
|
||||||
|
<!-- Dynamically populated based on selected converter tool -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Batch Mode Toggle -->
|
||||||
|
<div class="export-section">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="converter-batch-mode">
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
Batch Mode (Convert entire folder)
|
||||||
|
</label>
|
||||||
|
<small class="export-help">Enable to convert multiple files at once from a folder</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Batch Mode Options (hidden by default) -->
|
||||||
|
<div id="converter-batch-options" class="advanced-options hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Input Folder:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="converter-batch-input-folder" placeholder="Select folder..." readonly>
|
||||||
|
<button id="browse-converter-batch-input">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Output Folder:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="converter-batch-output-folder" placeholder="Select output folder..." readonly>
|
||||||
|
<button id="browse-converter-batch-output">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="converter-batch-subfolders" checked>
|
||||||
|
Include subfolders (recursive)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced Options Toggle -->
|
||||||
|
<div class="export-section export-mode-toggle">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="converter-advanced-toggle">
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
Show Advanced Options
|
||||||
|
</label>
|
||||||
|
<small class="export-help">Tool-specific quality, compression, and conversion settings</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced Converter Options (hidden by default) -->
|
||||||
|
<div id="converter-advanced-options" class="advanced-options hidden">
|
||||||
|
<!-- ImageMagick Advanced Options -->
|
||||||
|
<div class="converter-options imagemagick-options hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="imagemagick-quality">Quality (1-100):</label>
|
||||||
|
<input type="range" id="imagemagick-quality" min="1" max="100" value="85">
|
||||||
|
<span id="imagemagick-quality-value">85</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="imagemagick-dpi">Resolution (DPI):</label>
|
||||||
|
<input type="number" id="imagemagick-dpi" min="72" max="600" value="300" placeholder="300">
|
||||||
|
<small>Higher DPI = better quality, larger file size</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="imagemagick-resize">Resize (WxH):</label>
|
||||||
|
<input type="text" id="imagemagick-resize" placeholder="e.g., 1920x1080 or 50%">
|
||||||
|
<small>Examples: 1920x1080, 800x600, 50% (leave empty for no resize)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="imagemagick-compression">Compression Type:</label>
|
||||||
|
<select id="imagemagick-compression">
|
||||||
|
<option value="">None (Default)</option>
|
||||||
|
<option value="JPEG">JPEG</option>
|
||||||
|
<option value="LZW">LZW</option>
|
||||||
|
<option value="Zip">Zip</option>
|
||||||
|
<option value="RLE">RLE</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FFmpeg Advanced Options -->
|
||||||
|
<div class="converter-options ffmpeg-options hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="ffmpeg-video-codec">Video Codec:</label>
|
||||||
|
<select id="ffmpeg-video-codec">
|
||||||
|
<option value="">Default</option>
|
||||||
|
<option value="libx264">H.264 (libx264)</option>
|
||||||
|
<option value="libx265">H.265/HEVC (libx265)</option>
|
||||||
|
<option value="libvpx-vp9">VP9 (libvpx-vp9)</option>
|
||||||
|
<option value="copy">Copy (no re-encode)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="ffmpeg-audio-codec">Audio Codec:</label>
|
||||||
|
<select id="ffmpeg-audio-codec">
|
||||||
|
<option value="">Default</option>
|
||||||
|
<option value="aac">AAC</option>
|
||||||
|
<option value="libmp3lame">MP3 (libmp3lame)</option>
|
||||||
|
<option value="libopus">Opus (libopus)</option>
|
||||||
|
<option value="libvorbis">Vorbis (libvorbis)</option>
|
||||||
|
<option value="copy">Copy (no re-encode)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="ffmpeg-bitrate">Video Bitrate (kbps):</label>
|
||||||
|
<input type="number" id="ffmpeg-bitrate" min="100" max="50000" placeholder="e.g., 2000">
|
||||||
|
<small>Higher bitrate = better quality, larger file size</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="ffmpeg-preset">Encoding Preset:</label>
|
||||||
|
<select id="ffmpeg-preset">
|
||||||
|
<option value="">Default</option>
|
||||||
|
<option value="ultrafast">Ultra Fast</option>
|
||||||
|
<option value="fast">Fast</option>
|
||||||
|
<option value="medium" selected>Medium</option>
|
||||||
|
<option value="slow">Slow (Better Quality)</option>
|
||||||
|
<option value="veryslow">Very Slow (Best Quality)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="ffmpeg-framerate">Frame Rate (fps):</label>
|
||||||
|
<input type="number" id="ffmpeg-framerate" min="1" max="120" placeholder="e.g., 30">
|
||||||
|
<small>Common: 24, 30, 60</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LibreOffice Advanced Options -->
|
||||||
|
<div class="converter-options libreoffice-options hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="libreoffice-quality">Export Quality:</label>
|
||||||
|
<select id="libreoffice-quality">
|
||||||
|
<option value="">Default</option>
|
||||||
|
<option value="high">High</option>
|
||||||
|
<option value="medium" selected>Medium</option>
|
||||||
|
<option value="low">Low (Smaller file size)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label for="libreoffice-page-range">Page Range:</label>
|
||||||
|
<input type="text" id="libreoffice-page-range" placeholder="e.g., 1-5, 7, 9-12">
|
||||||
|
<small>Leave empty to convert all pages</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="libreoffice-export-bookmarks">
|
||||||
|
Export Bookmarks (PDF only)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pandoc Advanced Options -->
|
||||||
|
<div class="converter-options pandoc-options hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<small>Pandoc conversions can use advanced export options from the Export menu.</small>
|
||||||
|
<button id="pandoc-show-export-options" type="button">Open Export Options Dialog...</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="converter-progress" class="batch-progress hidden">
|
||||||
|
<div class="progress-text">
|
||||||
|
<span id="converter-status">Converting...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="export-section">
|
||||||
|
<small><strong>Installation Notes:</strong></small>
|
||||||
|
<small>• LibreOffice: Download from <a href="https://www.libreoffice.org/download" target="_blank">libreoffice.org</a></small>
|
||||||
|
<small>• ImageMagick: Download from <a href="https://imagemagick.org/script/download.php" target="_blank">imagemagick.org</a></small>
|
||||||
|
<small>• FFmpeg: Download from <a href="https://ffmpeg.org/download.html" target="_blank">ffmpeg.org</a></small>
|
||||||
|
<small>• Pandoc: Download from <a href="https://pandoc.org/installing.html" target="_blank">pandoc.org</a></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-dialog-footer">
|
||||||
|
<button id="converter-cancel">Cancel</button>
|
||||||
|
<button id="converter-convert" class="primary">Convert</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Batch Conversion Dialog -->
|
<!-- Batch Conversion Dialog -->
|
||||||
<div id="batch-dialog" class="batch-dialog hidden">
|
<div id="batch-dialog" class="batch-dialog hidden">
|
||||||
<div class="batch-dialog-content">
|
<div class="batch-dialog-content">
|
||||||
@@ -300,6 +549,377 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- PDF Editor Dialog -->
|
||||||
|
<div id="pdf-editor-dialog" class="export-dialog hidden">
|
||||||
|
<div class="export-dialog-content">
|
||||||
|
<div class="export-dialog-header">
|
||||||
|
<h3 id="pdf-editor-title">PDF Editor</h3>
|
||||||
|
<button id="pdf-editor-dialog-close" title="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="export-dialog-body">
|
||||||
|
<!-- Merge PDFs Section -->
|
||||||
|
<div id="pdf-merge-section" class="pdf-operation-section hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label>PDF Files to Merge:</label>
|
||||||
|
<div id="merge-files-list" class="file-list">
|
||||||
|
<!-- Dynamically added file entries -->
|
||||||
|
</div>
|
||||||
|
<button id="add-merge-file" type="button">+ Add PDF File</button>
|
||||||
|
<small>Files will be merged in the order shown. Drag to reorder.</small>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Output File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="merge-output-path" placeholder="Choose output location..." readonly>
|
||||||
|
<button id="browse-merge-output">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Split PDF Section -->
|
||||||
|
<div id="pdf-split-section" class="pdf-operation-section hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label>PDF File to Split:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="split-input-path" placeholder="Select PDF file..." readonly>
|
||||||
|
<button id="browse-split-input">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Split Mode:</label>
|
||||||
|
<select id="split-mode">
|
||||||
|
<option value="pages">By Page Range</option>
|
||||||
|
<option value="interval">Every N Pages</option>
|
||||||
|
<option value="size">By File Size</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="split-pages-options" class="export-section">
|
||||||
|
<label>Page Ranges:</label>
|
||||||
|
<input type="text" id="split-page-ranges" placeholder="e.g., 1-5, 6-10, 11-15">
|
||||||
|
<small>Separate ranges with commas. Each range will create a new PDF.</small>
|
||||||
|
</div>
|
||||||
|
<div id="split-interval-options" class="export-section hidden">
|
||||||
|
<label>Split Every:</label>
|
||||||
|
<input type="number" id="split-interval" min="1" value="5" placeholder="5">
|
||||||
|
<small>pages (e.g., 5 = split every 5 pages)</small>
|
||||||
|
</div>
|
||||||
|
<div id="split-size-options" class="export-section hidden">
|
||||||
|
<label>Max File Size (MB):</label>
|
||||||
|
<input type="number" id="split-size" min="1" value="5" placeholder="5">
|
||||||
|
<small>Split when file size exceeds this limit</small>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Output Folder:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="split-output-folder" placeholder="Select output folder..." readonly>
|
||||||
|
<button id="browse-split-output">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Compress PDF Section -->
|
||||||
|
<div id="pdf-compress-section" class="pdf-operation-section hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label>PDF File to Compress:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="compress-input-path" placeholder="Select PDF file..." readonly>
|
||||||
|
<button id="browse-compress-input">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Compression Level:</label>
|
||||||
|
<select id="compress-level">
|
||||||
|
<option value="low">Low (Best Quality)</option>
|
||||||
|
<option value="medium" selected>Medium (Balanced)</option>
|
||||||
|
<option value="high">High (Smallest Size)</option>
|
||||||
|
</select>
|
||||||
|
<small>Higher compression = smaller file size, lower quality</small>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Compression Options:</label>
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<label><input type="checkbox" id="compress-images" checked> Compress Images</label>
|
||||||
|
<label><input type="checkbox" id="compress-remove-duplicates" checked> Remove Duplicates</label>
|
||||||
|
<label><input type="checkbox" id="compress-optimize-fonts"> Optimize Fonts</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Output File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="compress-output-path" placeholder="Choose output location..." readonly>
|
||||||
|
<button id="browse-compress-output">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rotate Pages Section -->
|
||||||
|
<div id="pdf-rotate-section" class="pdf-operation-section hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label>PDF File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="rotate-input-path" placeholder="Select PDF file..." readonly>
|
||||||
|
<button id="browse-rotate-input">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Pages to Rotate:</label>
|
||||||
|
<input type="text" id="rotate-pages" placeholder="e.g., 1-5, 7, 9-12 (leave empty for all pages)">
|
||||||
|
<small>Separate page numbers/ranges with commas, or leave empty to rotate all pages</small>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Rotation Angle:</label>
|
||||||
|
<select id="rotate-angle">
|
||||||
|
<option value="90">90° Clockwise</option>
|
||||||
|
<option value="180">180°</option>
|
||||||
|
<option value="270">270° Clockwise (90° Counter-clockwise)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Output File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="rotate-output-path" placeholder="Choose output location..." readonly>
|
||||||
|
<button id="browse-rotate-output">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Pages Section -->
|
||||||
|
<div id="pdf-delete-section" class="pdf-operation-section hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label>PDF File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="delete-input-path" placeholder="Select PDF file..." readonly>
|
||||||
|
<button id="browse-delete-input">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Pages to Delete:</label>
|
||||||
|
<input type="text" id="delete-pages" placeholder="e.g., 1-5, 7, 9-12">
|
||||||
|
<small>Separate page numbers/ranges with commas</small>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Output File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="delete-output-path" placeholder="Choose output location..." readonly>
|
||||||
|
<button id="browse-delete-output">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reorder Pages Section -->
|
||||||
|
<div id="pdf-reorder-section" class="pdf-operation-section hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label>PDF File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="reorder-input-path" placeholder="Select PDF file..." readonly>
|
||||||
|
<button id="browse-reorder-input">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>New Page Order:</label>
|
||||||
|
<input type="text" id="reorder-pages" placeholder="e.g., 3, 1, 2, 5, 4">
|
||||||
|
<small>Enter page numbers in desired order, separated by commas</small>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<button id="load-current-order" type="button">Load Current Page Order</button>
|
||||||
|
<div id="current-page-order" class="info-display hidden">
|
||||||
|
Current order: <span id="current-order-display"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Output File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="reorder-output-path" placeholder="Choose output location..." readonly>
|
||||||
|
<button id="browse-reorder-output">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Watermark Section -->
|
||||||
|
<div id="pdf-watermark-section" class="pdf-operation-section hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label>PDF File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="watermark-input-path" placeholder="Select PDF file..." readonly>
|
||||||
|
<button id="browse-watermark-input">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Watermark Text:</label>
|
||||||
|
<input type="text" id="watermark-text" placeholder="Enter watermark text...">
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Font Size:</label>
|
||||||
|
<input type="number" id="watermark-font-size" min="8" max="144" value="48" placeholder="48">
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Opacity (0-100):</label>
|
||||||
|
<input type="range" id="watermark-opacity" min="0" max="100" value="30">
|
||||||
|
<span id="watermark-opacity-value">30</span>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Position:</label>
|
||||||
|
<select id="watermark-position">
|
||||||
|
<option value="center">Center</option>
|
||||||
|
<option value="diagonal">Diagonal</option>
|
||||||
|
<option value="top-left">Top Left</option>
|
||||||
|
<option value="top-center">Top Center</option>
|
||||||
|
<option value="top-right">Top Right</option>
|
||||||
|
<option value="bottom-left">Bottom Left</option>
|
||||||
|
<option value="bottom-center">Bottom Center</option>
|
||||||
|
<option value="bottom-right">Bottom Right</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Color:</label>
|
||||||
|
<input type="color" id="watermark-color" value="#000000">
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Apply to:</label>
|
||||||
|
<select id="watermark-pages">
|
||||||
|
<option value="all">All Pages</option>
|
||||||
|
<option value="custom">Custom Pages</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="watermark-custom-pages" placeholder="e.g., 1-5, 7, 9-12" class="hidden">
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Output File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="watermark-output-path" placeholder="Choose output location..." readonly>
|
||||||
|
<button id="browse-watermark-output">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Encrypt PDF Section (Password Protection) -->
|
||||||
|
<div id="pdf-encrypt-section" class="pdf-operation-section hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label>PDF File to Protect:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="encrypt-input-path" placeholder="Select PDF file..." readonly>
|
||||||
|
<button id="browse-encrypt-input">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>User Password:</label>
|
||||||
|
<input type="password" id="encrypt-user-password" placeholder="Password to open the PDF">
|
||||||
|
<small>Required to open and view the PDF</small>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Owner Password:</label>
|
||||||
|
<input type="password" id="encrypt-owner-password" placeholder="Password to modify permissions">
|
||||||
|
<small>Optional: Required to change document permissions</small>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Encryption Level:</label>
|
||||||
|
<select id="encrypt-level">
|
||||||
|
<option value="128">128-bit (Standard)</option>
|
||||||
|
<option value="256" selected>256-bit (High Security)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Document Permissions:</label>
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<label><input type="checkbox" id="encrypt-allow-printing" checked> Allow Printing</label>
|
||||||
|
<label><input type="checkbox" id="encrypt-allow-modify" checked> Allow Content Modification</label>
|
||||||
|
<label><input type="checkbox" id="encrypt-allow-copy" checked> Allow Content Copying</label>
|
||||||
|
<label><input type="checkbox" id="encrypt-allow-annotate" checked> Allow Annotations</label>
|
||||||
|
<label><input type="checkbox" id="encrypt-allow-forms" checked> Allow Form Filling</label>
|
||||||
|
<label><input type="checkbox" id="encrypt-allow-extract" checked> Allow Accessibility (Screen Readers)</label>
|
||||||
|
<label><input type="checkbox" id="encrypt-allow-assemble" checked> Allow Document Assembly</label>
|
||||||
|
<label><input type="checkbox" id="encrypt-allow-print-high" checked> Allow High-Quality Printing</label>
|
||||||
|
</div>
|
||||||
|
<small>These permissions apply when owner password is set</small>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Output File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="encrypt-output-path" placeholder="Choose output location..." readonly>
|
||||||
|
<button id="browse-encrypt-output">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Decrypt PDF Section (Remove Password) -->
|
||||||
|
<div id="pdf-decrypt-section" class="pdf-operation-section hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Protected PDF File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="decrypt-input-path" placeholder="Select protected PDF file..." readonly>
|
||||||
|
<button id="browse-decrypt-input">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Current Password:</label>
|
||||||
|
<input type="password" id="decrypt-password" placeholder="Enter PDF password">
|
||||||
|
<small>Enter the password to unlock and remove protection</small>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Output File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="decrypt-output-path" placeholder="Choose output location..." readonly>
|
||||||
|
<button id="browse-decrypt-output">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Permissions Section -->
|
||||||
|
<div id="pdf-permissions-section" class="pdf-operation-section hidden">
|
||||||
|
<div class="export-section">
|
||||||
|
<label>PDF File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="permissions-input-path" placeholder="Select PDF file..." readonly>
|
||||||
|
<button id="browse-permissions-input">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Current Password (if protected):</label>
|
||||||
|
<input type="password" id="permissions-current-password" placeholder="Leave empty if not protected">
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>New Owner Password:</label>
|
||||||
|
<input type="password" id="permissions-owner-password" placeholder="Password to modify permissions">
|
||||||
|
<small>Set a password required to change document permissions</small>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Document Permissions:</label>
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<label><input type="checkbox" id="permissions-allow-printing" checked> Allow Printing</label>
|
||||||
|
<label><input type="checkbox" id="permissions-allow-modify" checked> Allow Content Modification</label>
|
||||||
|
<label><input type="checkbox" id="permissions-allow-copy" checked> Allow Content Copying</label>
|
||||||
|
<label><input type="checkbox" id="permissions-allow-annotate" checked> Allow Annotations</label>
|
||||||
|
<label><input type="checkbox" id="permissions-allow-forms" checked> Allow Form Filling</label>
|
||||||
|
<label><input type="checkbox" id="permissions-allow-extract" checked> Allow Accessibility (Screen Readers)</label>
|
||||||
|
<label><input type="checkbox" id="permissions-allow-assemble" checked> Allow Document Assembly</label>
|
||||||
|
<label><input type="checkbox" id="permissions-allow-print-high" checked> Allow High-Quality Printing</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-section">
|
||||||
|
<label>Output File:</label>
|
||||||
|
<div class="folder-input-group">
|
||||||
|
<input type="text" id="permissions-output-path" placeholder="Choose output location..." readonly>
|
||||||
|
<button id="browse-permissions-output">Browse</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress indicator -->
|
||||||
|
<div id="pdf-progress" class="batch-progress hidden">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="pdf-progress-fill"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-text">
|
||||||
|
<span id="pdf-progress-text">Processing...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="export-dialog-footer">
|
||||||
|
<button id="pdf-editor-cancel">Cancel</button>
|
||||||
|
<button id="pdf-editor-process" class="primary">Process</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="editor-container">
|
<div class="editor-container">
|
||||||
<div class="tab-content active" id="tab-content-1" data-tab-id="1">
|
<div class="tab-content active" id="tab-content-1" data-tab-id="1">
|
||||||
<div id="editor-pane-1" class="pane">
|
<div id="editor-pane-1" class="pane">
|
||||||
|
|||||||
+830
-45
@@ -2,17 +2,20 @@ const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron')
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { exec } = require('child_process');
|
const { exec } = require('child_process');
|
||||||
|
const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib');
|
||||||
|
|
||||||
// Get the system Pandoc path
|
// Get the system Pandoc path
|
||||||
function getPandocPath() {
|
function getPandocPath() {
|
||||||
// Always use system pandoc - no bundled binaries
|
// Pandoc is expected to be in the system's PATH.
|
||||||
|
// The command will be executed directly. Quoting is handled by exec.
|
||||||
return 'pandoc';
|
return 'pandoc';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Pandoc is available
|
// Check if Pandoc is available
|
||||||
function checkPandocAvailable() {
|
function checkPandocAvailable() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
exec('pandoc --version', (error) => {
|
const pandocPath = getPandocPath();
|
||||||
|
exec(`${pandocPath} --version`, (error) => {
|
||||||
resolve(!error);
|
resolve(!error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -44,6 +47,7 @@ const store = {
|
|||||||
let mainWindow;
|
let mainWindow;
|
||||||
let currentFile = null; // This will now represent the active tab's file
|
let currentFile = null; // This will now represent the active tab's file
|
||||||
let pandocAvailable = null; // Cache pandoc availability check
|
let pandocAvailable = null; // Cache pandoc availability check
|
||||||
|
let rendererReady = false; // Track if renderer is ready to receive file data
|
||||||
|
|
||||||
// Check if pandoc is available
|
// Check if pandoc is available
|
||||||
function checkPandocAvailability() {
|
function checkPandocAvailability() {
|
||||||
@@ -179,7 +183,7 @@ function createMenu() {
|
|||||||
{ label: 'PowerPoint (PPTX)', click: () => exportFile('pptx') },
|
{ label: 'PowerPoint (PPTX)', click: () => exportFile('pptx') },
|
||||||
{ label: 'OpenDocument Presentation (ODP)', click: () => exportFile('odp') },
|
{ label: 'OpenDocument Presentation (ODP)', click: () => exportFile('odp') },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ label: 'CSV (Tables)', click: () => exportSpreadsheet('csv') }
|
{ label: 'CSV (Tables)', click: () => exportSpreadsheet('csv') },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
@@ -265,6 +269,105 @@ function createMenu() {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Convert',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Universal File Converter...',
|
||||||
|
accelerator: 'CmdOrCtrl+Shift+C',
|
||||||
|
click: () => showUniversalConverterDialog()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'About Converter',
|
||||||
|
click: () => {
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'info',
|
||||||
|
title: 'About Universal Converter',
|
||||||
|
message: 'Open-Source File Converter',
|
||||||
|
detail: 'PanConverter includes a powerful open-source file conversion system.\n\nSupported Converters:\n• LibreOffice - Document conversions (DOCX, PDF, ODT, etc.)\n• ImageMagick - Image format conversions\n• FFmpeg - Media file conversions\n• Pandoc - Document markup conversions\n\nFeatures:\n• 100% open-source and free\n• No API keys required\n• Runs completely offline\n• Supports 100+ file formats\n• High-quality professional conversions',
|
||||||
|
buttons: ['OK']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'PDF Editor',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Merge PDFs...',
|
||||||
|
click: () => showPDFEditorDialog('merge')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Split PDF...',
|
||||||
|
click: () => showPDFEditorDialog('split')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Compress PDF...',
|
||||||
|
click: () => showPDFEditorDialog('compress')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Rotate Pages...',
|
||||||
|
click: () => showPDFEditorDialog('rotate')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delete Pages...',
|
||||||
|
click: () => showPDFEditorDialog('delete')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Reorder Pages...',
|
||||||
|
click: () => showPDFEditorDialog('reorder')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Add Watermark...',
|
||||||
|
click: () => showPDFEditorDialog('watermark')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Security',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Add Password Protection...',
|
||||||
|
click: () => showPDFEditorDialog('encrypt')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Remove Password...',
|
||||||
|
click: () => showPDFEditorDialog('decrypt')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Set Permissions...',
|
||||||
|
click: () => showPDFEditorDialog('permissions')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'About PDF Editor',
|
||||||
|
click: () => {
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'info',
|
||||||
|
title: 'About PDF Editor',
|
||||||
|
message: 'PDF Editor',
|
||||||
|
detail: 'Comprehensive PDF editing capabilities powered by pdf-lib.\n\nFeatures:\n• Merge multiple PDF files\n• Split PDF into separate files\n• Compress PDF to reduce file size\n• Rotate pages (90°, 180°, 270°)\n• Delete unwanted pages\n• Reorder pages\n• Add text watermarks\n\nSecurity Features:\n• Password protection (encryption)\n• Remove passwords (decryption)\n• Set document permissions\n\n100% offline and open-source.',
|
||||||
|
buttons: ['OK']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Help',
|
label: 'Help',
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -275,7 +378,7 @@ function createMenu() {
|
|||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'About PanConverter',
|
title: 'About PanConverter',
|
||||||
message: 'PanConverter',
|
message: 'PanConverter',
|
||||||
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.5.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Windows Explorer context menu integration\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Enhanced PDF export with built-in Electron fallback\n• File association support for .md files\n• Command-line interface for batch conversion\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Improved preview typography and spacing\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• Complete theme support including Monokai fixes\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• Multiple themes support\n• Undo/redo functionality\n• Live word count and statistics',
|
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.7.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Comprehensive PDF Editor (merge, split, compress, rotate, watermark, encrypt)\n• Universal File Converter (LibreOffice, ImageMagick, FFmpeg, Pandoc)\n• Windows Explorer context menu integration\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Real-time preview updates while typing\n• Full toolbar markdown editing functions\n• Enhanced PDF export with built-in Electron fallback\n• File association support for .md files\n• Command-line interface for batch conversion\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Improved preview typography and spacing\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• Complete theme support including Monokai fixes\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• Multiple themes support\n• Undo/redo functionality\n• Live word count and statistics',
|
||||||
buttons: ['OK']
|
buttons: ['OK']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -341,6 +444,162 @@ function showBatchConversionDialog() {
|
|||||||
mainWindow.webContents.send('show-batch-dialog');
|
mainWindow.webContents.send('show-batch-dialog');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Universal File Converter integration
|
||||||
|
function showUniversalConverterDialog() {
|
||||||
|
mainWindow.webContents.send('show-universal-converter-dialog');
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF Editor dialog
|
||||||
|
function showPDFEditorDialog(operation) {
|
||||||
|
mainWindow.webContents.send('show-pdf-editor-dialog', operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if conversion tool is available
|
||||||
|
function checkConverterAvailable(tool) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let command;
|
||||||
|
switch (tool) {
|
||||||
|
case 'libreoffice':
|
||||||
|
command = process.platform === 'win32' ? 'where soffice' : 'which soffice';
|
||||||
|
break;
|
||||||
|
case 'imagemagick':
|
||||||
|
command = process.platform === 'win32' ? 'where magick' : 'which convert';
|
||||||
|
break;
|
||||||
|
case 'ffmpeg':
|
||||||
|
command = process.platform === 'win32' ? 'where ffmpeg' : 'which ffmpeg';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
exec(command, (error) => {
|
||||||
|
resolve(!error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle universal file conversion
|
||||||
|
ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, filePath }) => {
|
||||||
|
try {
|
||||||
|
mainWindow.webContents.send('conversion-status', 'Checking converter availability...');
|
||||||
|
|
||||||
|
// Check if the required tool is available
|
||||||
|
const toolAvailable = await checkConverterAvailable(tool);
|
||||||
|
|
||||||
|
if (!toolAvailable) {
|
||||||
|
throw new Error(`${tool} is not installed or not found in PATH. Please install it first.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow.webContents.send('conversion-status', 'Converting file...');
|
||||||
|
|
||||||
|
const outputPath = filePath.replace(/\.[^/.]+$/, `.${toFormat}`);
|
||||||
|
let conversionCmd;
|
||||||
|
|
||||||
|
switch (tool) {
|
||||||
|
case 'libreoffice':
|
||||||
|
conversionCmd = convertWithLibreOffice(filePath, toFormat, outputPath);
|
||||||
|
break;
|
||||||
|
case 'imagemagick':
|
||||||
|
conversionCmd = convertWithImageMagick(filePath, outputPath);
|
||||||
|
break;
|
||||||
|
case 'ffmpeg':
|
||||||
|
conversionCmd = convertWithFFmpeg(filePath, outputPath);
|
||||||
|
break;
|
||||||
|
case 'pandoc':
|
||||||
|
conversionCmd = `pandoc "${filePath}" -o "${outputPath}"`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown conversion tool: ${tool}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
exec(conversionCmd, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
mainWindow.webContents.send('conversion-complete', {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'error',
|
||||||
|
title: 'Conversion Failed',
|
||||||
|
message: `${tool} conversion failed`,
|
||||||
|
detail: stderr || error.message,
|
||||||
|
buttons: ['OK']
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mainWindow.webContents.send('conversion-complete', {
|
||||||
|
success: true,
|
||||||
|
outputPath: outputPath
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'info',
|
||||||
|
title: 'Conversion Complete',
|
||||||
|
message: 'File converted successfully!',
|
||||||
|
detail: `Saved to: ${outputPath}`,
|
||||||
|
buttons: ['OK']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
mainWindow.webContents.send('conversion-complete', {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'error',
|
||||||
|
title: 'Conversion Failed',
|
||||||
|
message: 'Universal conversion failed',
|
||||||
|
detail: error.message,
|
||||||
|
buttons: ['OK']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// LibreOffice conversion command builder
|
||||||
|
function convertWithLibreOffice(inputFile, outputFormat, outputPath) {
|
||||||
|
const outputDir = path.dirname(outputPath);
|
||||||
|
const soffice = process.platform === 'win32'
|
||||||
|
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
|
||||||
|
: 'soffice';
|
||||||
|
|
||||||
|
// LibreOffice conversion format mapping
|
||||||
|
const formatMap = {
|
||||||
|
'pdf': 'pdf',
|
||||||
|
'docx': 'docx',
|
||||||
|
'doc': 'doc',
|
||||||
|
'odt': 'odt',
|
||||||
|
'rtf': 'rtf',
|
||||||
|
'txt': 'txt',
|
||||||
|
'html': 'html',
|
||||||
|
'xlsx': 'xlsx',
|
||||||
|
'xls': 'xls',
|
||||||
|
'ods': 'ods',
|
||||||
|
'csv': 'csv',
|
||||||
|
'pptx': 'pptx',
|
||||||
|
'ppt': 'ppt',
|
||||||
|
'odp': 'odp'
|
||||||
|
};
|
||||||
|
|
||||||
|
const format = formatMap[outputFormat] || outputFormat;
|
||||||
|
|
||||||
|
// Use headless mode for conversion
|
||||||
|
return `${soffice} --headless --convert-to ${format} --outdir "${outputDir}" "${inputFile}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageMagick conversion command builder
|
||||||
|
function convertWithImageMagick(inputFile, outputPath) {
|
||||||
|
const magick = process.platform === 'win32' ? 'magick' : 'convert';
|
||||||
|
return `${magick} "${inputFile}" "${outputPath}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FFmpeg conversion command builder
|
||||||
|
function convertWithFFmpeg(inputFile, outputPath) {
|
||||||
|
return `ffmpeg -i "${inputFile}" "${outputPath}" -y`;
|
||||||
|
}
|
||||||
|
|
||||||
function performExportWithOptions(format, options) {
|
function performExportWithOptions(format, options) {
|
||||||
const outputFile = dialog.showSaveDialogSync(mainWindow, {
|
const outputFile = dialog.showSaveDialogSync(mainWindow, {
|
||||||
defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`),
|
defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`),
|
||||||
@@ -414,12 +673,12 @@ function performExportWithOptions(format, options) {
|
|||||||
|
|
||||||
// Add specific options for PDF export to ensure proper generation
|
// Add specific options for PDF export to ensure proper generation
|
||||||
if (format === 'pdf') {
|
if (format === 'pdf') {
|
||||||
const pdfEngine = options.pdfEngine || 'xelatex';
|
const pdfEngine = options.pdfEngine || 'xelatex'; // Default to xelatex
|
||||||
const geometry = options.geometry || 'margin=1in';
|
pandocCmd += ` --pdf-engine="${pdfEngine}"`;
|
||||||
pandocCmd += ` --pdf-engine=${pdfEngine} -V geometry:${geometry}`;
|
if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`;
|
||||||
|
|
||||||
// Try with specified PDF engine
|
// Try with specified PDF engine
|
||||||
exec(pandocCmd, (error, stdout, stderr) => {
|
exec(pandocCmd, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
// Try fallback engines if the specified one fails
|
// Try fallback engines if the specified one fails
|
||||||
const fallbackEngines = ['pdflatex', 'lualatex'];
|
const fallbackEngines = ['pdflatex', 'lualatex'];
|
||||||
@@ -429,13 +688,7 @@ function performExportWithOptions(format, options) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (format === 'docx') {
|
} else if (format === 'docx') {
|
||||||
pandocCmd = `${getPandocPath()} "${currentFile}" -t docx -o "${outputFile}"`;
|
pandocCmd += ' -t docx';
|
||||||
exportWithPandoc(pandocCmd, outputFile, format);
|
|
||||||
} else if (format === 'html') {
|
|
||||||
pandocCmd = `${getPandocPath()} "${currentFile}" -t html5 --standalone -o "${outputFile}"`;
|
|
||||||
exportWithPandoc(pandocCmd, outputFile, format);
|
|
||||||
} else if (format === 'latex') {
|
|
||||||
pandocCmd = `${getPandocPath()} "${currentFile}" -t latex -o "${outputFile}"`;
|
|
||||||
exportWithPandoc(pandocCmd, outputFile, format);
|
exportWithPandoc(pandocCmd, outputFile, format);
|
||||||
} else {
|
} else {
|
||||||
// Generic export for other formats
|
// Generic export for other formats
|
||||||
@@ -460,9 +713,10 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro
|
|||||||
}
|
}
|
||||||
|
|
||||||
const engine = engines[index];
|
const engine = engines[index];
|
||||||
const geometry = options.geometry || 'margin=1in';
|
|
||||||
let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -V geometry:${geometry} -o "${outputFile}"`;
|
let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -V geometry:${geometry} -o "${outputFile}"`;
|
||||||
|
|
||||||
|
if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`;
|
||||||
|
|
||||||
// Add all other options
|
// Add all other options
|
||||||
if (options.template && options.template !== 'default') {
|
if (options.template && options.template !== 'default') {
|
||||||
pandocCmd += ` --template="${options.template}"`;
|
pandocCmd += ` --template="${options.template}"`;
|
||||||
@@ -476,7 +730,7 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exec(pandocCmd, (error, stdout, stderr) => {
|
exec(pandocCmd, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error);
|
tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error);
|
||||||
} else {
|
} else {
|
||||||
@@ -496,16 +750,35 @@ function showExportSuccess(outputFile) {
|
|||||||
|
|
||||||
// Helper function to export with pandoc (general)
|
// Helper function to export with pandoc (general)
|
||||||
function exportWithPandoc(pandocCmd, outputFile, format) {
|
function exportWithPandoc(pandocCmd, outputFile, format) {
|
||||||
|
console.log(`Executing Pandoc command: ${pandocCmd}`);
|
||||||
|
|
||||||
exec(pandocCmd, (error, stdout, stderr) => {
|
exec(pandocCmd, (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(`Pandoc error for ${format}:`, error);
|
console.error(`Pandoc error for ${format}:`, error);
|
||||||
dialog.showErrorBox('Export Error',
|
console.error(`Pandoc stderr:`, stderr);
|
||||||
`Failed to export to ${format.toUpperCase()}:\n${error.message}\n\n` +
|
console.error(`Pandoc stdout:`, stdout);
|
||||||
`Command used: ${pandocCmd}\n\n` +
|
|
||||||
`Please ensure Pandoc is properly installed and accessible.`
|
// Provide more specific error messages
|
||||||
);
|
let errorMessage = `Failed to export to ${format.toUpperCase()}`;
|
||||||
|
|
||||||
|
if (error.message.includes('not found') || error.message.includes('not recognized')) {
|
||||||
|
errorMessage += '\n\nPandoc is not installed or not found in PATH.';
|
||||||
|
errorMessage += '\nPlease install Pandoc from: https://pandoc.org/installing.html';
|
||||||
|
} else if (stderr) {
|
||||||
|
errorMessage += `\n\nError details: ${stderr}`;
|
||||||
|
} else {
|
||||||
|
errorMessage += `\n\nError details: ${error.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage += `\n\nCommand used: ${pandocCmd}`;
|
||||||
|
|
||||||
|
dialog.showErrorBox('Export Error', errorMessage);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Successfully exported to ${format}:`, outputFile);
|
console.log(`Successfully exported to ${format}:`, outputFile);
|
||||||
|
console.log(`Pandoc stdout:`, stdout);
|
||||||
|
if (stderr) {
|
||||||
|
console.warn(`Pandoc stderr (non-fatal):`, stderr);
|
||||||
|
}
|
||||||
showExportSuccess(outputFile);
|
showExportSuccess(outputFile);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -813,6 +1086,7 @@ ipcMain.on('set-current-file', (event, filePath) => {
|
|||||||
|
|
||||||
// Handle renderer ready for file association
|
// Handle renderer ready for file association
|
||||||
ipcMain.on('renderer-ready', (event) => {
|
ipcMain.on('renderer-ready', (event) => {
|
||||||
|
rendererReady = true;
|
||||||
if (app.pendingFile) {
|
if (app.pendingFile) {
|
||||||
openFileFromPath(app.pendingFile);
|
openFileFromPath(app.pendingFile);
|
||||||
app.pendingFile = null;
|
app.pendingFile = null;
|
||||||
@@ -858,30 +1132,31 @@ ipcMain.on('export-spreadsheet', (event, { content, format }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert tables to CSV format
|
if (format === 'csv') {
|
||||||
let csvContent = '';
|
// Convert tables to CSV format
|
||||||
tables.forEach((table, index) => {
|
let csvContent = '';
|
||||||
if (index > 0) csvContent += '\n\n'; // Separate multiple tables
|
tables.forEach((table, index) => {
|
||||||
if (tables.length > 1) csvContent += `"Table ${index + 1}"\n`;
|
if (index > 0) csvContent += '\n\n'; // Separate multiple tables
|
||||||
|
if (tables.length > 1) csvContent += `"Table ${index + 1}"\n`;
|
||||||
|
|
||||||
table.forEach(row => {
|
table.forEach(row => {
|
||||||
const csvRow = row.map(cell => {
|
const csvRow = row.map(cell => {
|
||||||
// Escape quotes and wrap in quotes if necessary
|
// Escape quotes and wrap in quotes if necessary
|
||||||
const cleanCell = cell.replace(/"/g, '""');
|
const cleanCell = cell.replace(/"/g, '""');
|
||||||
return cleanCell.includes(',') || cleanCell.includes('"') || cleanCell.includes('\n')
|
return cleanCell.includes(',') || cleanCell.includes('"') || cleanCell.includes('\n')
|
||||||
? `"${cleanCell}"` : cleanCell;
|
? `"${cleanCell}"` : cleanCell;
|
||||||
}).join(',');
|
}).join(',');
|
||||||
csvContent += csvRow + '\n';
|
csvContent += csvRow + '\n';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Write CSV file
|
fs.writeFileSync(outputFile, csvContent, 'utf-8');
|
||||||
fs.writeFileSync(outputFile, csvContent, 'utf-8');
|
}
|
||||||
|
|
||||||
dialog.showMessageBox(mainWindow, {
|
dialog.showMessageBox(mainWindow, {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Export Complete',
|
title: 'Export Complete',
|
||||||
message: `CSV exported successfully to ${outputFile}`,
|
message: `${format.toUpperCase()} exported successfully to ${outputFile}`,
|
||||||
buttons: ['OK']
|
buttons: ['OK']
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1038,8 +1313,8 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
|
|||||||
// Add PDF-specific options
|
// Add PDF-specific options
|
||||||
if (format === 'pdf') {
|
if (format === 'pdf') {
|
||||||
const pdfEngine = options.pdfEngine || 'xelatex';
|
const pdfEngine = options.pdfEngine || 'xelatex';
|
||||||
const geometry = options.geometry || 'margin=1in';
|
pandocCmd += ` --pdf-engine="${pdfEngine}"`;
|
||||||
pandocCmd += ` --pdf-engine=${pdfEngine} -V geometry:${geometry}`;
|
if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute conversion
|
// Execute conversion
|
||||||
@@ -1263,10 +1538,10 @@ ipcMain.on('clear-recent-files', (event) => {
|
|||||||
// Handle file opening on macOS
|
// Handle file opening on macOS
|
||||||
app.on('open-file', (event, filePath) => {
|
app.on('open-file', (event, filePath) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (mainWindow) {
|
if (mainWindow && rendererReady) {
|
||||||
openFileFromPath(filePath);
|
openFileFromPath(filePath);
|
||||||
} else {
|
} else {
|
||||||
// Store the file path to open after window is created
|
// Store the file path to open after window and renderer are ready
|
||||||
app.pendingFile = filePath;
|
app.pendingFile = filePath;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1277,7 +1552,517 @@ function openFileFromPath(filePath) {
|
|||||||
currentFile = filePath;
|
currentFile = filePath;
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
if (mainWindow && mainWindow.webContents) {
|
if (mainWindow && mainWindow.webContents) {
|
||||||
mainWindow.webContents.send('file-opened', { path: filePath, content });
|
// Wait for the renderer to be ready (TabManager initialized) before sending file data
|
||||||
|
if (rendererReady) {
|
||||||
|
mainWindow.webContents.send('file-opened', { path: filePath, content });
|
||||||
|
} else {
|
||||||
|
// Store file to open after renderer is ready
|
||||||
|
app.pendingFile = filePath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// PDF EDITOR OPERATIONS (using pdf-lib)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// Helper function to parse page ranges (e.g., "1-5, 7, 9-12")
|
||||||
|
function parsePageRanges(rangeString, totalPages) {
|
||||||
|
const pages = [];
|
||||||
|
const ranges = rangeString.split(',').map(r => r.trim());
|
||||||
|
|
||||||
|
for (const range of ranges) {
|
||||||
|
if (range.includes('-')) {
|
||||||
|
const [start, end] = range.split('-').map(n => parseInt(n.trim()));
|
||||||
|
for (let i = start; i <= end && i <= totalPages; i++) {
|
||||||
|
if (i > 0 && !pages.includes(i - 1)) { // Convert to 0-indexed
|
||||||
|
pages.push(i - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const page = parseInt(range);
|
||||||
|
if (page > 0 && page <= totalPages && !pages.includes(page - 1)) {
|
||||||
|
pages.push(page - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages.sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to convert hex color to RGB
|
||||||
|
function hexToRgb(hex) {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
return result ? {
|
||||||
|
r: parseInt(result[1], 16) / 255,
|
||||||
|
g: parseInt(result[2], 16) / 255,
|
||||||
|
b: parseInt(result[3], 16) / 255
|
||||||
|
} : { r: 0, g: 0, b: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF Merge Operation
|
||||||
|
async function pdfMerge(data) {
|
||||||
|
try {
|
||||||
|
const mergedPdf = await PDFDocument.create();
|
||||||
|
|
||||||
|
for (const filePath of data.inputFiles) {
|
||||||
|
const pdfBytes = fs.readFileSync(filePath);
|
||||||
|
const pdf = await PDFDocument.load(pdfBytes);
|
||||||
|
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
|
||||||
|
copiedPages.forEach(page => mergedPdf.addPage(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfBytes = await mergedPdf.save();
|
||||||
|
fs.writeFileSync(data.outputPath, pdfBytes);
|
||||||
|
|
||||||
|
return { success: true, message: `Successfully merged ${data.inputFiles.length} PDFs` };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF Split Operation
|
||||||
|
async function pdfSplit(data) {
|
||||||
|
try {
|
||||||
|
const pdfBytes = fs.readFileSync(data.inputPath);
|
||||||
|
const pdf = await PDFDocument.load(pdfBytes);
|
||||||
|
const totalPages = pdf.getPageCount();
|
||||||
|
|
||||||
|
let splits = [];
|
||||||
|
|
||||||
|
if (data.splitMode === 'pages') {
|
||||||
|
// Split by page ranges
|
||||||
|
const ranges = data.pageRanges.split(',').map(r => r.trim());
|
||||||
|
for (let i = 0; i < ranges.length; i++) {
|
||||||
|
const range = ranges[i];
|
||||||
|
let pages = [];
|
||||||
|
|
||||||
|
if (range.includes('-')) {
|
||||||
|
const [start, end] = range.split('-').map(n => parseInt(n.trim()));
|
||||||
|
for (let p = start; p <= end && p <= totalPages; p++) {
|
||||||
|
pages.push(p - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const page = parseInt(range);
|
||||||
|
if (page > 0 && page <= totalPages) {
|
||||||
|
pages.push(page - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pages.length > 0) {
|
||||||
|
splits.push({ pages, name: `part_${i + 1}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (data.splitMode === 'interval') {
|
||||||
|
// Split every N pages
|
||||||
|
const interval = data.interval;
|
||||||
|
for (let i = 0; i < totalPages; i += interval) {
|
||||||
|
const pages = [];
|
||||||
|
for (let j = i; j < i + interval && j < totalPages; j++) {
|
||||||
|
pages.push(j);
|
||||||
|
}
|
||||||
|
splits.push({ pages, name: `part_${Math.floor(i / interval) + 1}` });
|
||||||
|
}
|
||||||
|
} else if (data.splitMode === 'size') {
|
||||||
|
// Split by size (approximate - we'll split evenly)
|
||||||
|
// This is complex, so we'll do a simple even split for now
|
||||||
|
const maxSize = data.maxSize * 1024 * 1024; // Convert MB to bytes
|
||||||
|
// For simplicity, split into fixed page chunks
|
||||||
|
const chunkSize = Math.max(1, Math.floor(totalPages / 5)); // Split into ~5 parts
|
||||||
|
for (let i = 0; i < totalPages; i += chunkSize) {
|
||||||
|
const pages = [];
|
||||||
|
for (let j = i; j < i + chunkSize && j < totalPages; j++) {
|
||||||
|
pages.push(j);
|
||||||
|
}
|
||||||
|
splits.push({ pages, name: `part_${Math.floor(i / chunkSize) + 1}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create split PDFs
|
||||||
|
const baseName = path.basename(data.inputPath, '.pdf');
|
||||||
|
for (const split of splits) {
|
||||||
|
const newPdf = await PDFDocument.create();
|
||||||
|
const copiedPages = await newPdf.copyPages(pdf, split.pages);
|
||||||
|
copiedPages.forEach(page => newPdf.addPage(page));
|
||||||
|
|
||||||
|
const outputPath = path.join(data.outputFolder, `${baseName}_${split.name}.pdf`);
|
||||||
|
const newPdfBytes = await newPdf.save();
|
||||||
|
fs.writeFileSync(outputPath, newPdfBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: `Successfully split PDF into ${splits.length} files` };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF Compress Operation
|
||||||
|
async function pdfCompress(data) {
|
||||||
|
try {
|
||||||
|
const pdfBytes = fs.readFileSync(data.inputPath);
|
||||||
|
const pdf = await PDFDocument.load(pdfBytes);
|
||||||
|
|
||||||
|
// pdf-lib doesn't have built-in compression, but we can save with default compression
|
||||||
|
// For actual compression, we would need additional libraries like Ghostscript
|
||||||
|
// For now, we'll save with standard options which provides some compression
|
||||||
|
const compressedPdfBytes = await pdf.save({
|
||||||
|
useObjectStreams: true,
|
||||||
|
addDefaultPage: false,
|
||||||
|
objectsPerTick: 50
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(data.outputPath, compressedPdfBytes);
|
||||||
|
|
||||||
|
const originalSize = fs.statSync(data.inputPath).size;
|
||||||
|
const compressedSize = fs.statSync(data.outputPath).size;
|
||||||
|
const savings = ((originalSize - compressedSize) / originalSize * 100).toFixed(1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `PDF compressed. Size reduced by ${savings}% (${(originalSize / 1024).toFixed(1)}KB → ${(compressedSize / 1024).toFixed(1)}KB)`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF Rotate Pages Operation
|
||||||
|
async function pdfRotate(data) {
|
||||||
|
try {
|
||||||
|
const pdfBytes = fs.readFileSync(data.inputPath);
|
||||||
|
const pdf = await PDFDocument.load(pdfBytes);
|
||||||
|
const totalPages = pdf.getPageCount();
|
||||||
|
|
||||||
|
let pagesToRotate = [];
|
||||||
|
if (data.pages && data.pages.trim()) {
|
||||||
|
pagesToRotate = parsePageRanges(data.pages, totalPages);
|
||||||
|
} else {
|
||||||
|
// Rotate all pages
|
||||||
|
pagesToRotate = Array.from({ length: totalPages }, (_, i) => i);
|
||||||
|
}
|
||||||
|
|
||||||
|
pagesToRotate.forEach(pageIndex => {
|
||||||
|
const page = pdf.getPage(pageIndex);
|
||||||
|
page.setRotation(degrees(data.angle));
|
||||||
|
});
|
||||||
|
|
||||||
|
const rotatedPdfBytes = await pdf.save();
|
||||||
|
fs.writeFileSync(data.outputPath, rotatedPdfBytes);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Successfully rotated ${pagesToRotate.length} page(s) by ${data.angle}°`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF Delete Pages Operation
|
||||||
|
async function pdfDeletePages(data) {
|
||||||
|
try {
|
||||||
|
const pdfBytes = fs.readFileSync(data.inputPath);
|
||||||
|
const pdf = await PDFDocument.load(pdfBytes);
|
||||||
|
const totalPages = pdf.getPageCount();
|
||||||
|
|
||||||
|
const pagesToDelete = parsePageRanges(data.pages, totalPages);
|
||||||
|
|
||||||
|
// Remove pages in reverse order to maintain indices
|
||||||
|
pagesToDelete.sort((a, b) => b - a).forEach(pageIndex => {
|
||||||
|
pdf.removePage(pageIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
const newPdfBytes = await pdf.save();
|
||||||
|
fs.writeFileSync(data.outputPath, newPdfBytes);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Successfully deleted ${pagesToDelete.length} page(s). New PDF has ${totalPages - pagesToDelete.length} pages`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF Reorder Pages Operation
|
||||||
|
async function pdfReorder(data) {
|
||||||
|
try {
|
||||||
|
const pdfBytes = fs.readFileSync(data.inputPath);
|
||||||
|
const pdf = await PDFDocument.load(pdfBytes);
|
||||||
|
const totalPages = pdf.getPageCount();
|
||||||
|
|
||||||
|
// Parse new page order
|
||||||
|
const newOrder = data.newOrder.split(',').map(n => parseInt(n.trim()) - 1); // Convert to 0-indexed
|
||||||
|
|
||||||
|
// Validate new order
|
||||||
|
if (newOrder.length !== totalPages) {
|
||||||
|
return { success: false, error: `New order must include all ${totalPages} pages` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPdf = await PDFDocument.create();
|
||||||
|
const copiedPages = await newPdf.copyPages(pdf, newOrder);
|
||||||
|
copiedPages.forEach(page => newPdf.addPage(page));
|
||||||
|
|
||||||
|
const reorderedPdfBytes = await newPdf.save();
|
||||||
|
fs.writeFileSync(data.outputPath, reorderedPdfBytes);
|
||||||
|
|
||||||
|
return { success: true, message: 'Successfully reordered PDF pages' };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF Watermark Operation
|
||||||
|
async function pdfWatermark(data) {
|
||||||
|
try {
|
||||||
|
const pdfBytes = fs.readFileSync(data.inputPath);
|
||||||
|
const pdf = await PDFDocument.load(pdfBytes);
|
||||||
|
const totalPages = pdf.getPageCount();
|
||||||
|
|
||||||
|
// Determine which pages to watermark
|
||||||
|
let pagesToWatermark = [];
|
||||||
|
if (data.pages === 'all') {
|
||||||
|
pagesToWatermark = Array.from({ length: totalPages }, (_, i) => i);
|
||||||
|
} else if (data.pages === 'custom' && data.customPages) {
|
||||||
|
pagesToWatermark = parsePageRanges(data.customPages, totalPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
const font = await pdf.embedFont(StandardFonts.Helvetica);
|
||||||
|
const color = hexToRgb(data.color);
|
||||||
|
|
||||||
|
for (const pageIndex of pagesToWatermark) {
|
||||||
|
const page = pdf.getPage(pageIndex);
|
||||||
|
const { width, height } = page.getSize();
|
||||||
|
|
||||||
|
let x, y, rotation = 0;
|
||||||
|
|
||||||
|
// Calculate position based on selected position
|
||||||
|
switch (data.position) {
|
||||||
|
case 'center':
|
||||||
|
x = width / 2;
|
||||||
|
y = height / 2;
|
||||||
|
break;
|
||||||
|
case 'diagonal':
|
||||||
|
x = width / 2;
|
||||||
|
y = height / 2;
|
||||||
|
rotation = 45;
|
||||||
|
break;
|
||||||
|
case 'top-left':
|
||||||
|
x = 50;
|
||||||
|
y = height - 50;
|
||||||
|
break;
|
||||||
|
case 'top-center':
|
||||||
|
x = width / 2;
|
||||||
|
y = height - 50;
|
||||||
|
break;
|
||||||
|
case 'top-right':
|
||||||
|
x = width - 50;
|
||||||
|
y = height - 50;
|
||||||
|
break;
|
||||||
|
case 'bottom-left':
|
||||||
|
x = 50;
|
||||||
|
y = 50;
|
||||||
|
break;
|
||||||
|
case 'bottom-center':
|
||||||
|
x = width / 2;
|
||||||
|
y = 50;
|
||||||
|
break;
|
||||||
|
case 'bottom-right':
|
||||||
|
x = width - 50;
|
||||||
|
y = 50;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
x = width / 2;
|
||||||
|
y = height / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
page.drawText(data.text, {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
size: data.fontSize,
|
||||||
|
font,
|
||||||
|
color: rgb(color.r, color.g, color.b),
|
||||||
|
opacity: data.opacity,
|
||||||
|
rotate: degrees(rotation)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const watermarkedPdfBytes = await pdf.save();
|
||||||
|
fs.writeFileSync(data.outputPath, watermarkedPdfBytes);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Successfully added watermark to ${pagesToWatermark.length} page(s)`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF Encrypt (Password Protection) Operation
|
||||||
|
async function pdfEncrypt(data) {
|
||||||
|
try {
|
||||||
|
const pdfBytes = fs.readFileSync(data.inputPath);
|
||||||
|
const pdf = await PDFDocument.load(pdfBytes);
|
||||||
|
|
||||||
|
// pdf-lib has limited encryption support in v1.17.1
|
||||||
|
// We'll save with password (basic encryption)
|
||||||
|
const encryptedPdfBytes = await pdf.save({
|
||||||
|
userPassword: data.userPassword,
|
||||||
|
ownerPassword: data.ownerPassword || data.userPassword,
|
||||||
|
permissions: {
|
||||||
|
printing: data.permissions.printing ? 'highResolution' : 'lowResolution',
|
||||||
|
modifying: data.permissions.modifying,
|
||||||
|
copying: data.permissions.copying,
|
||||||
|
annotating: data.permissions.annotating,
|
||||||
|
fillingForms: data.permissions.fillingForms,
|
||||||
|
contentAccessibility: data.permissions.contentAccessibility,
|
||||||
|
documentAssembly: data.permissions.documentAssembly
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(data.outputPath, encryptedPdfBytes);
|
||||||
|
|
||||||
|
return { success: true, message: 'Successfully added password protection to PDF' };
|
||||||
|
} catch (error) {
|
||||||
|
// If pdf-lib doesn't support encryption in this version, provide a helpful error
|
||||||
|
if (error.message.includes('encrypt') || error.message.includes('password')) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'PDF encryption requires pdf-lib with encryption support. This feature may not be available in the current version.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF Decrypt (Remove Password) Operation
|
||||||
|
async function pdfDecrypt(data) {
|
||||||
|
try {
|
||||||
|
const pdfBytes = fs.readFileSync(data.inputPath);
|
||||||
|
const pdf = await PDFDocument.load(pdfBytes, { password: data.password });
|
||||||
|
|
||||||
|
// Save without password
|
||||||
|
const decryptedPdfBytes = await pdf.save();
|
||||||
|
fs.writeFileSync(data.outputPath, decryptedPdfBytes);
|
||||||
|
|
||||||
|
return { success: true, message: 'Successfully removed password protection from PDF' };
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message.includes('password') || error.message.includes('encrypted')) {
|
||||||
|
return { success: false, error: 'Incorrect password or PDF is not encrypted' };
|
||||||
|
}
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF Permissions Operation
|
||||||
|
async function pdfSetPermissions(data) {
|
||||||
|
try {
|
||||||
|
const pdfBytes = fs.readFileSync(data.inputPath);
|
||||||
|
const loadOptions = data.currentPassword ? { password: data.currentPassword } : {};
|
||||||
|
const pdf = await PDFDocument.load(pdfBytes, loadOptions);
|
||||||
|
|
||||||
|
const newPdfBytes = await pdf.save({
|
||||||
|
ownerPassword: data.ownerPassword,
|
||||||
|
permissions: {
|
||||||
|
printing: data.permissions.printing ? 'highResolution' : 'lowResolution',
|
||||||
|
modifying: data.permissions.modifying,
|
||||||
|
copying: data.permissions.copying,
|
||||||
|
annotating: data.permissions.annotating,
|
||||||
|
fillingForms: data.permissions.fillingForms,
|
||||||
|
contentAccessibility: data.permissions.contentAccessibility,
|
||||||
|
documentAssembly: data.permissions.documentAssembly
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(data.outputPath, newPdfBytes);
|
||||||
|
|
||||||
|
return { success: true, message: 'Successfully updated PDF permissions' };
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message.includes('encrypt') || error.message.includes('permission')) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'PDF permissions require pdf-lib with encryption support. This feature may not be available in the current version.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC Handler for PDF Operations
|
||||||
|
ipcMain.on('process-pdf-operation', async (event, data) => {
|
||||||
|
try {
|
||||||
|
mainWindow.webContents.send('pdf-operation-progress', {
|
||||||
|
message: `Processing ${data.operation}...`,
|
||||||
|
progress: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
switch (data.operation) {
|
||||||
|
case 'merge':
|
||||||
|
result = await pdfMerge(data);
|
||||||
|
break;
|
||||||
|
case 'split':
|
||||||
|
result = await pdfSplit(data);
|
||||||
|
break;
|
||||||
|
case 'compress':
|
||||||
|
result = await pdfCompress(data);
|
||||||
|
break;
|
||||||
|
case 'rotate':
|
||||||
|
result = await pdfRotate(data);
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
result = await pdfDeletePages(data);
|
||||||
|
break;
|
||||||
|
case 'reorder':
|
||||||
|
result = await pdfReorder(data);
|
||||||
|
break;
|
||||||
|
case 'watermark':
|
||||||
|
result = await pdfWatermark(data);
|
||||||
|
break;
|
||||||
|
case 'encrypt':
|
||||||
|
result = await pdfEncrypt(data);
|
||||||
|
break;
|
||||||
|
case 'decrypt':
|
||||||
|
result = await pdfDecrypt(data);
|
||||||
|
break;
|
||||||
|
case 'permissions':
|
||||||
|
result = await pdfSetPermissions(data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result = { success: false, error: `Unknown operation: ${data.operation}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow.webContents.send('pdf-operation-complete', result);
|
||||||
|
} catch (error) {
|
||||||
|
mainWindow.webContents.send('pdf-operation-complete', {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// IPC Handler for getting PDF page count
|
||||||
|
ipcMain.on('get-pdf-page-count', async (event, filePath) => {
|
||||||
|
try {
|
||||||
|
const pdfBytes = fs.readFileSync(filePath);
|
||||||
|
const pdf = await PDFDocument.load(pdfBytes);
|
||||||
|
const count = pdf.getPageCount();
|
||||||
|
event.reply('pdf-page-count', { count });
|
||||||
|
} catch (error) {
|
||||||
|
event.reply('pdf-page-count', { error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// IPC Handler for folder selection (for PDF operations)
|
||||||
|
ipcMain.on('select-pdf-folder', (event, inputId) => {
|
||||||
|
const folder = dialog.showOpenDialogSync(mainWindow, {
|
||||||
|
properties: ['openDirectory']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (folder && folder[0]) {
|
||||||
|
event.reply('pdf-folder-selected', { inputId, path: folder[0] });
|
||||||
|
}
|
||||||
|
});
|
||||||
+1360
-46
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -859,8 +859,9 @@ body.theme-github .status-bar {
|
|||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
max-height: 80vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.export-dialog-header {
|
.export-dialog-header {
|
||||||
@@ -1437,7 +1438,9 @@ body.theme-github .line-numbers {
|
|||||||
|
|
||||||
.advanced-options {
|
.advanced-options {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.advanced-options.hidden {
|
.advanced-options.hidden {
|
||||||
|
|||||||
Reference in New Issue
Block a user