diff --git a/src/preload.js b/src/preload.js
index 9f510f0..a1aaccb 100644
--- a/src/preload.js
+++ b/src/preload.js
@@ -12,7 +12,7 @@
* @version 4.4.1
*/
-const { contextBridge, ipcRenderer } = require('electron');
+const { contextBridge, ipcRenderer, webUtils } = require('electron');
// Define allowed IPC channels for security
const ALLOWED_SEND_CHANNELS = [
@@ -431,6 +431,21 @@ contextBridge.exposeInMainWorld('electronAPI', {
updateSettings: (settings) => ipcRenderer.send('update-page-settings', settings),
},
+ /**
+ * Resolve a File object chosen via `` to its absolute
+ * path. `File.path` was removed in Electron 32; `webUtils.getPathForFile`
+ * is its replacement. Falls back to `file.path` on older Electron where
+ * webUtils is unavailable.
+ * @param {File} file - File object from a file input's files list
+ * @returns {string | undefined} Absolute filesystem path when resolvable
+ */
+ getFilePath: (file) => {
+ if (webUtils && typeof webUtils.getPathForFile === 'function') {
+ return webUtils.getPathForFile(file);
+ }
+ return file && file.path;
+ },
+
// PDF Operations
pdf: {
processOperation: (data) => ipcRenderer.send('process-pdf-operation', data),
diff --git a/src/renderer.js b/src/renderer.js
index e14b1ad..cff77eb 100644
--- a/src/renderer.js
+++ b/src/renderer.js
@@ -3,7 +3,7 @@
* @version 4.5.0
*/
-const { ipcRenderer } = require('electron');
+const { ipcRenderer, webUtils } = require('electron');
const marked = require('marked');
const { markedHighlight } = require('marked-highlight');
const createDOMPurify = require('dompurify');
@@ -16,6 +16,7 @@ const { showPdfBatchDialog } = require('./renderer/pdf-batch-dialog');
const { showDocumentCompareDialog } = require('./renderer/document-compare-dialog');
const { initExportPresets, refreshExportPresets } = require('./renderer/export-presets');
const { csvToMarkdownTable } = require('./utils/csv-to-markdown-table');
+const { getFilePath } = require('./utils/file-path');
/**
* Toggle body classes that drive the monospace font + ligatures CSS tokens.
@@ -69,6 +70,13 @@ if (typeof window !== 'undefined' && !window.electronAPI) {
ipcRenderer.removeAllListeners(channel);
},
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
+ // File.path was removed in Electron 32 — resolve picker paths via webUtils.
+ getFilePath: (file) => {
+ if (webUtils && typeof webUtils.getPathForFile === 'function') {
+ return webUtils.getPathForFile(file);
+ }
+ return file && file.path;
+ },
file: {
read: (filePath) => ipcRenderer.invoke('read-file', filePath),
write: (filePath, content) =>
@@ -2647,7 +2655,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('template-file-input').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
- document.getElementById('custom-template-path').value = file.path;
+ document.getElementById('custom-template-path').value = getFilePath(file);
}
});
@@ -2725,7 +2733,7 @@ document.addEventListener('DOMContentLoaded', () => {
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
- document.getElementById('bibliography-file').value = file.path;
+ document.getElementById('bibliography-file').value = getFilePath(file);
}
};
input.click();
@@ -2739,7 +2747,7 @@ document.addEventListener('DOMContentLoaded', () => {
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
- document.getElementById('csl-file').value = file.path;
+ document.getElementById('csl-file').value = getFilePath(file);
}
};
input.click();
@@ -3590,8 +3598,9 @@ document.addEventListener('DOMContentLoaded', () => {
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
- converterFilePath = file.path;
- document.getElementById('converter-file-path').value = file.path;
+ const filePath = getFilePath(file);
+ converterFilePath = filePath;
+ document.getElementById('converter-file-path').value = filePath;
}
};
input.click();
@@ -3942,8 +3951,9 @@ document.addEventListener('DOMContentLoaded', () => {
input.onchange = (e) => {
const files = Array.from(e.target.files);
files.forEach((file) => {
- if (!mergeFilePaths.includes(file.path)) {
- mergeFilePaths.push(file.path);
+ const filePath = getFilePath(file);
+ if (!mergeFilePaths.includes(filePath)) {
+ mergeFilePaths.push(filePath);
}
});
updateMergeFilesList();
@@ -4127,8 +4137,9 @@ document.addEventListener('DOMContentLoaded', () => {
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
- document.getElementById(button.inputId).value = file.path;
- onPDFFileSelected(button.inputId, file.path);
+ const filePath = getFilePath(file);
+ document.getElementById(button.inputId).value = filePath;
+ onPDFFileSelected(button.inputId, filePath);
}
};
input.click();
@@ -4137,10 +4148,11 @@ document.addEventListener('DOMContentLoaded', () => {
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
- document.getElementById(button.inputId).value = file.path;
- onPDFFileSelected(button.inputId, file.path);
+ const filePath = getFilePath(file);
+ document.getElementById(button.inputId).value = filePath;
+ onPDFFileSelected(button.inputId, filePath);
if (button.inputId === 'fill-form-input-path') {
- loadFillFormFields(file.path);
+ loadFillFormFields(filePath);
}
}
};
diff --git a/src/renderer/document-compare-dialog.js b/src/renderer/document-compare-dialog.js
index 4d9ae69..0011013 100644
--- a/src/renderer/document-compare-dialog.js
+++ b/src/renderer/document-compare-dialog.js
@@ -9,7 +9,8 @@
* markup appended to document.body on first use, driven by ModalManager, with a
* status line reusing the `info-message` / `warning-message` classes. File
* selection reuses the app's existing convention: a plain ``
- * whose `.path` is read directly (nodeIntegration is enabled for this renderer) —
+ * whose chosen File is resolved to a path via `window.electronAPI.getFilePath`
+ * (webUtils.getPathForFile — `File.path` was removed in Electron 32) —
* no new IPC channel for picking files. File *contents* are read through the
* existing `read-file` invoke channel (the same one the renderer's electronAPI
* adapters use), not through a new privileged renderer-side file path.
@@ -28,6 +29,7 @@
*/
const { ipcRenderer } = require('electron');
+const { getFilePath } = require('../utils/file-path');
const { computeLineDiff } = require('../utils/line-diff');
const COMPARE_ACCEPT = '.md,.markdown,.txt,.text,.log,.json,.xml,.yml,.yaml,.csv';
@@ -140,7 +142,7 @@ function chooseFile(input) {
fileInput.onchange = (e) => {
const file = e.target.files[0];
if (file) {
- input.value = file.path;
+ input.value = getFilePath(file);
clearResult();
}
};
diff --git a/src/renderer/media-operations-dialog.js b/src/renderer/media-operations-dialog.js
index a7a3225..9327529 100644
--- a/src/renderer/media-operations-dialog.js
+++ b/src/renderer/media-operations-dialog.js
@@ -12,8 +12,9 @@
* three media kinds together cover 13 distinct operations.
*
* File selection reuses the app's existing convention: a plain ``
- * whose `.path` is read directly (nodeIntegration is enabled for this renderer), the
- * same approach already used throughout the PDF Editor and Universal Converter
+ * whose chosen File is resolved to a path via `window.electronAPI.getFilePath`
+ * (webUtils.getPathForFile — `File.path` was removed in Electron 32), the same
+ * approach already used throughout the PDF Editor and Universal Converter
* dialogs. No new IPC channel is needed for single-file or save-file pickers. Output
* *folder* selection (used by the video "Extract Frames" operation, and by batch
* mode below) reuses the existing generic `select-folder` / `folder-selected` IPC
@@ -33,6 +34,7 @@
*/
const { ipcRenderer } = require('electron');
+const { getFilePath } = require('../utils/file-path');
const IMAGE_ACCEPT = '.jpg,.jpeg,.png,.webp,.avif,.tiff,.tif,.gif';
const AUDIO_ACCEPT = '.mp3,.wav,.ogg,.flac,.aac,.m4a,.wma';
@@ -447,7 +449,7 @@ function renderFileField(field) {
fileInput.accept = field.accept || '*';
fileInput.onchange = (e) => {
const file = e.target.files[0];
- if (file) input.value = file.path;
+ if (file) input.value = getFilePath(file);
};
fileInput.click();
},
@@ -462,7 +464,7 @@ function renderSaveField(field) {
fileInput.nwsaveas = true;
fileInput.onchange = (e) => {
const file = e.target.files[0];
- if (file) input.value = file.path;
+ if (file) input.value = getFilePath(file);
};
fileInput.click();
},
@@ -588,8 +590,9 @@ function renderFilesField(field) {
fileInput.multiple = true;
fileInput.onchange = (e) => {
Array.from(e.target.files).forEach((file) => {
- if (!mergeFilePaths.includes(file.path)) {
- mergeFilePaths.push(file.path);
+ const filePath = getFilePath(file);
+ if (!mergeFilePaths.includes(filePath)) {
+ mergeFilePaths.push(filePath);
}
});
updateMergeFilesList(listContainer);
diff --git a/src/utils/file-path.js b/src/utils/file-path.js
new file mode 100644
index 0000000..76c35c8
--- /dev/null
+++ b/src/utils/file-path.js
@@ -0,0 +1,30 @@
+/**
+ * File-input path resolution for Electron 41.
+ *
+ * `File.path` was removed in Electron 32, so a plain ``
+ * picker can no longer read the chosen file's absolute path directly. The
+ * replacement is `webUtils.getPathForFile(file)`, exposed to renderers as
+ * `window.electronAPI.getFilePath` (by src/preload.js in preload-loaded
+ * windows, and by the fallback shim in src/renderer.js for the main window,
+ * which does not load the preload script).
+ *
+ * @module utils/file-path
+ */
+
+/**
+ * Resolve a File object chosen via `` to its absolute path.
+ * Falls back to `file.path` when the electronAPI helper is absent
+ * (Electron < 32, or jsdom tests without the bridge).
+ *
+ * @param {File} file - File object from a file input's files list
+ * @returns {string | undefined} Absolute filesystem path when resolvable
+ */
+function getFilePath(file) {
+ const api = typeof window !== 'undefined' ? window.electronAPI : undefined;
+ if (api && typeof api.getFilePath === 'function') {
+ return api.getFilePath(file);
+ }
+ return file && file.path;
+}
+
+module.exports = { getFilePath };
diff --git a/tests/file-path.test.js b/tests/file-path.test.js
new file mode 100644
index 0000000..3328f63
--- /dev/null
+++ b/tests/file-path.test.js
@@ -0,0 +1,38 @@
+/**
+ * Tests for the File-input path resolution helper (Electron 41 migration).
+ * File.path was removed in Electron 32; getFilePath() routes through
+ * window.electronAPI.getFilePath (webUtils.getPathForFile) when the bridge is
+ * available and falls back to file.path otherwise (older Electron, jsdom).
+ */
+
+const { getFilePath } = require('../src/utils/file-path');
+
+describe('getFilePath helper', () => {
+ const originalAPI = window.electronAPI;
+
+ afterEach(() => {
+ window.electronAPI = originalAPI;
+ });
+
+ it('delegates to window.electronAPI.getFilePath when exposed', () => {
+ const file = { path: '/stale/file.path' };
+ window.electronAPI = { getFilePath: jest.fn(() => '/resolved/report.md') };
+
+ expect(getFilePath(file)).toBe('/resolved/report.md');
+ expect(window.electronAPI.getFilePath).toHaveBeenCalledWith(file);
+ });
+
+ it('falls back to file.path when the helper is absent', () => {
+ const file = { path: '/legacy/electron/file.md' };
+ window.electronAPI = { send: jest.fn() }; // no getFilePath on the surface
+
+ expect(getFilePath(file)).toBe('/legacy/electron/file.md');
+ });
+
+ it('falls back to file.path when electronAPI is undefined', () => {
+ const file = { path: '/no-bridge/file.md' };
+ window.electronAPI = undefined;
+
+ expect(getFilePath(file)).toBe('/no-bridge/file.md');
+ });
+});
diff --git a/tests/preload.test.js b/tests/preload.test.js
index e1a5139..08c9ea0 100644
--- a/tests/preload.test.js
+++ b/tests/preload.test.js
@@ -182,4 +182,65 @@ describe('Preload Security', () => {
expect(window.electronAPI.pdf.getPageCount).toBeDefined();
});
});
+
+ // Loads the real src/preload.js against a mocked electron module, following
+ // the jest.mock('electron') pattern used by the renderer dialog tests.
+ describe('getFilePath (webUtils.getPathForFile bridge)', () => {
+ const setupApi = window.electronAPI;
+
+ afterAll(() => {
+ window.electronAPI = setupApi;
+ });
+
+ test('exposes getFilePath and delegates to webUtils.getPathForFile', () => {
+ jest.mock('electron', () => ({
+ contextBridge: {
+ exposeInMainWorld: (key, api) => {
+ window[key] = api;
+ },
+ },
+ ipcRenderer: {
+ send: jest.fn(),
+ invoke: jest.fn(),
+ on: jest.fn(),
+ once: jest.fn(),
+ removeListener: jest.fn(),
+ removeAllListeners: jest.fn(),
+ },
+ webUtils: {
+ getPathForFile: jest.fn((file) => file && `/resolved${file.name}`),
+ },
+ }));
+ const { webUtils } = require('electron');
+ require('../src/preload.js');
+
+ expect(typeof window.electronAPI.getFilePath).toBe('function');
+ const file = { name: '/report.md' };
+ expect(window.electronAPI.getFilePath(file)).toBe('/resolved/report.md');
+ expect(webUtils.getPathForFile).toHaveBeenCalledWith(file);
+ });
+
+ test('falls back to file.path when webUtils is unavailable', () => {
+ jest.mock('electron', () => ({
+ contextBridge: {
+ exposeInMainWorld: (key, api) => {
+ window[key] = api;
+ },
+ },
+ ipcRenderer: {
+ send: jest.fn(),
+ invoke: jest.fn(),
+ on: jest.fn(),
+ once: jest.fn(),
+ removeListener: jest.fn(),
+ removeAllListeners: jest.fn(),
+ },
+ }));
+ require('../src/preload.js');
+
+ expect(typeof window.electronAPI.getFilePath).toBe('function');
+ const file = { path: '/legacy/file.md' };
+ expect(window.electronAPI.getFilePath(file)).toBe('/legacy/file.md');
+ });
+ });
});
diff --git a/tests/setup.js b/tests/setup.js
index 58d1f25..c592b0b 100644
--- a/tests/setup.js
+++ b/tests/setup.js
@@ -11,6 +11,8 @@ global.window.electronAPI = {
once: jest.fn(),
invoke: jest.fn(() => Promise.resolve(null)),
removeAllListeners: jest.fn(),
+ // webUtils.getPathForFile bridge (File.path was removed in Electron 32)
+ getFilePath: jest.fn((file) => file && file.path),
file: {
save: jest.fn(),
saveCurrent: jest.fn(),