markpdfdown 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { app, ipcMain, dialog, shell, safeStorage, protocol, nativeImage, BrowserWindow } from "electron";
1
+ import { app, ipcMain, dialog, clipboard, nativeImage, shell, safeStorage, protocol, BrowserWindow } from "electron";
2
2
  import path from "path";
3
3
  import fs, { promises } from "fs";
4
4
  import isDev from "electron-is-dev";
@@ -2635,6 +2635,7 @@ const IPC_CHANNELS = {
2635
2635
  FILE: {
2636
2636
  GET_IMAGE_PATH: "file:getImagePath",
2637
2637
  DOWNLOAD_MARKDOWN: "file:downloadMarkdown",
2638
+ COPY_IMAGE_TO_CLIPBOARD: "file:copyImageToClipboard",
2638
2639
  SELECT_DIALOG: "file:selectDialog",
2639
2640
  UPLOAD: "file:upload",
2640
2641
  UPLOAD_FILE_CONTENT: "file:uploadFileContent"
@@ -3444,6 +3445,20 @@ function registerTaskDetailHandlers() {
3444
3445
  );
3445
3446
  console.log("[IPC] TaskDetail handlers registered");
3446
3447
  }
3448
+ async function createImageFromSource(imageSource) {
3449
+ const normalizedSource = imageSource.trim();
3450
+ const lowerSource = normalizedSource.toLowerCase();
3451
+ if (lowerSource.startsWith("data:image/")) {
3452
+ return nativeImage.createFromDataURL(normalizedSource);
3453
+ }
3454
+ if (lowerSource.startsWith("http://") || lowerSource.startsWith("https://")) {
3455
+ throw new Error("Remote image URLs are not allowed");
3456
+ }
3457
+ if (lowerSource.startsWith("file://")) {
3458
+ return nativeImage.createFromPath(fileURLToPath(normalizedSource));
3459
+ }
3460
+ return nativeImage.createFromPath(normalizedSource);
3461
+ }
3447
3462
  function registerFileHandlers() {
3448
3463
  ipcMain.handle(
3449
3464
  IPC_CHANNELS.FILE.GET_IMAGE_PATH,
@@ -3506,6 +3521,25 @@ function registerFileHandlers() {
3506
3521
  }
3507
3522
  }
3508
3523
  );
3524
+ ipcMain.handle(
3525
+ IPC_CHANNELS.FILE.COPY_IMAGE_TO_CLIPBOARD,
3526
+ async (_, imageSource) => {
3527
+ try {
3528
+ if (!imageSource) {
3529
+ return { success: false, error: "Image source is required" };
3530
+ }
3531
+ const image = await createImageFromSource(imageSource);
3532
+ if (image.isEmpty()) {
3533
+ return { success: false, error: "Image data is empty or invalid" };
3534
+ }
3535
+ clipboard.writeImage(image);
3536
+ return { success: true, data: { copied: true } };
3537
+ } catch (error) {
3538
+ console.error("[IPC] file:copyImageToClipboard error:", error);
3539
+ return { success: false, error: error.message || "Failed to copy image" };
3540
+ }
3541
+ }
3542
+ );
3509
3543
  ipcMain.handle(IPC_CHANNELS.FILE.SELECT_DIALOG, async (_, allowOffice) => {
3510
3544
  try {
3511
3545
  const pdfAndImageExtensions = ["pdf", "jpg", "jpeg", "png", "bmp", "gif"];
@@ -4373,6 +4407,88 @@ class CloudService {
4373
4407
  static instance;
4374
4408
  constructor() {
4375
4409
  }
4410
+ extractDownloadFileName(contentDisposition, fallback) {
4411
+ const rfc5987Name = this.parseRFC5987Filename(contentDisposition);
4412
+ if (rfc5987Name) {
4413
+ return this.sanitizeDownloadFileName(rfc5987Name, fallback);
4414
+ }
4415
+ const plainName = this.parsePlainFilename(contentDisposition);
4416
+ if (!plainName) {
4417
+ return this.sanitizeDownloadFileName(fallback, fallback);
4418
+ }
4419
+ const repairedName = this.tryRepairUtf8Mojibake(plainName);
4420
+ return this.sanitizeDownloadFileName(repairedName || plainName, fallback);
4421
+ }
4422
+ parseRFC5987Filename(contentDisposition) {
4423
+ const match = contentDisposition.match(/filename\*\s*=\s*([^;]+)/i);
4424
+ if (!match) return null;
4425
+ const rawValue = match[1]?.trim();
4426
+ if (!rawValue) return null;
4427
+ const unquoted = rawValue.replace(/^"(.*)"$/, "$1");
4428
+ const parts = unquoted.match(/^([^']*)'[^']*'(.*)$/);
4429
+ if (!parts) return null;
4430
+ const charset = (parts[1] || "utf-8").trim().toLowerCase();
4431
+ const encodedValue = parts[2] || "";
4432
+ try {
4433
+ if (charset === "utf-8" || charset === "utf8") {
4434
+ return decodeURIComponent(encodedValue);
4435
+ }
4436
+ const bytes = this.percentDecodeToBytes(encodedValue);
4437
+ if (charset === "iso-8859-1" || charset === "latin1") {
4438
+ return Buffer.from(bytes).toString("latin1");
4439
+ }
4440
+ return Buffer.from(bytes).toString("utf8");
4441
+ } catch {
4442
+ return null;
4443
+ }
4444
+ }
4445
+ parsePlainFilename(contentDisposition) {
4446
+ const match = contentDisposition.match(/filename\s*=\s*("(?:\\.|[^"])*"|[^;]+)/i);
4447
+ if (!match) return null;
4448
+ let value = match[1]?.trim();
4449
+ if (!value) return null;
4450
+ if (value.startsWith('"') && value.endsWith('"')) {
4451
+ value = value.slice(1, -1).replace(/\\"/g, '"');
4452
+ }
4453
+ return value;
4454
+ }
4455
+ percentDecodeToBytes(input) {
4456
+ const bytes = [];
4457
+ for (let i = 0; i < input.length; i++) {
4458
+ const ch = input[i];
4459
+ if (ch === "%" && i + 2 < input.length) {
4460
+ const hex = input.slice(i + 1, i + 3);
4461
+ const parsed = Number.parseInt(hex, 16);
4462
+ if (!Number.isNaN(parsed)) {
4463
+ bytes.push(parsed);
4464
+ i += 2;
4465
+ continue;
4466
+ }
4467
+ }
4468
+ bytes.push(input.charCodeAt(i));
4469
+ }
4470
+ return bytes;
4471
+ }
4472
+ tryRepairUtf8Mojibake(input) {
4473
+ const hasCjk = /[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/.test(input);
4474
+ if (hasCjk) return null;
4475
+ const latinSupplementCount = Array.from(input).filter((ch) => {
4476
+ const code = ch.charCodeAt(0);
4477
+ return code >= 192 && code <= 255;
4478
+ }).length;
4479
+ if (latinSupplementCount < 2) return null;
4480
+ const repaired = Buffer.from(input, "latin1").toString("utf8");
4481
+ if (!repaired) return null;
4482
+ const repairedHasCjk = /[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/.test(repaired);
4483
+ const roundTrip = Buffer.from(repaired, "utf8").toString("latin1") === input;
4484
+ if (repairedHasCjk && roundTrip) {
4485
+ return repaired;
4486
+ }
4487
+ return null;
4488
+ }
4489
+ sanitizeDownloadFileName(input, fallback) {
4490
+ return path.basename(input).replace(/[\u0000-\u001f<>:"|?*]/g, "_") || fallback;
4491
+ }
4376
4492
  normalizeCheckoutStatus(data) {
4377
4493
  if (!data || typeof data !== "object") {
4378
4494
  return null;
@@ -4694,9 +4810,8 @@ class CloudService {
4694
4810
  };
4695
4811
  }
4696
4812
  const contentDisposition = res.headers.get("Content-Disposition") || "";
4697
- const match = contentDisposition.match(/filename="?([^";\n]+)"?/);
4698
- const rawName = match ? match[1] : `task-${id}.pdf`;
4699
- const fileName = path.basename(rawName).replace(/[\u0000-\u001f<>:"|?*]/g, "_") || `task-${id}.pdf`;
4813
+ const fallbackName = `task-${id}.pdf`;
4814
+ const fileName = this.extractDownloadFileName(contentDisposition, fallbackName);
4700
4815
  const buffer = await res.arrayBuffer();
4701
4816
  return { success: true, data: { buffer, fileName } };
4702
4817
  } catch (error) {
@@ -41,7 +41,8 @@ electron.contextBridge.exposeInMainWorld("api", {
41
41
  upload: (taskId, filePath) => electron.ipcRenderer.invoke("file:upload", taskId, filePath),
42
42
  uploadFileContent: (taskId, fileName, fileBuffer) => electron.ipcRenderer.invoke("file:uploadFileContent", taskId, fileName, fileBuffer),
43
43
  getImagePath: (taskId, page) => electron.ipcRenderer.invoke("file:getImagePath", taskId, page),
44
- downloadMarkdown: (taskId) => electron.ipcRenderer.invoke("file:downloadMarkdown", taskId)
44
+ downloadMarkdown: (taskId) => electron.ipcRenderer.invoke("file:downloadMarkdown", taskId),
45
+ copyImageToClipboard: (imageSource) => electron.ipcRenderer.invoke("file:copyImageToClipboard", imageSource)
45
46
  },
46
47
  // ==================== Completion APIs ====================
47
48
  completion: {