diff --git a/package.json b/package.json index b2190ba..c00347b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pdfseal", - "version": "1.2.2", + "version": "1.2.3", "private": true, "type": "module", "license": "AGPL-3.0-only", diff --git a/src/config/siteConfig.js b/src/config/siteConfig.js index 220730d..9f219cc 100644 --- a/src/config/siteConfig.js +++ b/src/config/siteConfig.js @@ -59,7 +59,7 @@ export const isOfficialMode = ref(checkInitialOfficialMode()); export const siteConfig = reactive({ // Version Identity - version: import.meta.env.VITE_APP_VERSION || '1.2.2', + version: import.meta.env.VITE_APP_VERSION || '1.2.3', // Brand Identity brandName: import.meta.env.VITE_BRAND_NAME || 'PDFSeal', diff --git a/src/locales/de.json b/src/locales/de.json index 7038ca0..a8cfff1 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -456,10 +456,11 @@ "compress_target_size_subtitle": "Erfüllt strenge Vorgaben für Visa-, Behörden- und Uni-Portale", "compress_target_limit_symbol": "≤", "compress_quick_presets": "Standard-Vorgaben", - "compress_preset_1mb": "Visum / Reisepass", - "compress_preset_2mb": "Behörden / Thesis", - "compress_preset_5mb": "Ausschreibungen / Verträge", - "compress_preset_10mb": "E-Mail-Anhang", + "compress_preset_1mb": "Ausweis / Strikte Portale", + "compress_preset_2mb": "Visum (CEAC / VFS) / Uni", + "compress_preset_5mb": "Kanada IRCC / Bewerbung", + "compress_preset_10mb": "USCIS / Verträge / Thesis", + "compress_preset_20mb": "E-Mail-Sicherheitsgrenze", "compress_target_current_size": "Originalgröße", "compress_target_goal": "Ziellimit", "compress_target_already_smaller": "Bereits kleiner als Ziel; verlustfreie Vektoroptimierung wird angewendet", @@ -1489,6 +1490,14 @@ "next_action_badge_general": "Nahtlose Weiterleitung", "next_action_desc_general": "Datei im Browserspeicher gehalten. Nahtlose Weiterleitung ohne erneuten Upload.", "next_action_desc_merge_paginated": "Zusammengefügte Dokumente haben oft lückenhafte Seitenzahlen. Paginieren Sie das Gesamtdokument fortlaufend neu.", + "next_action_badge_compressed": "Größe optimiert", + "next_action_desc_compress_done": "Dokument erfolgreich komprimiert! Fügen Sie Passwortschutz hinzu oder signieren Sie direkt.", + "next_action_badge_organized": "Seiten neu geordnet", + "next_action_desc_organize_done": "Seiten neu geordnet! Bei gelöschten oder verschobenen Seiten empfiehlt sich eine Neunummerierung.", + "next_action_badge_split": "Teilung bereit", + "next_action_desc_split_done": "Seiten extrahiert! Nummerieren Sie das Teildokument ab Seite 1 neu oder komprimieren Sie es.", + "next_action_badge_protected": "Geschützt", + "next_action_desc_protect_done": "Dokument verschlüsselt und geschützt! Bereit zur sicheren Weitergabe oder Ablage im Tresor.", "loading_file": "Loading file...", "loading_default": "Processing...", "preview_title": "Seiten-HD-Vorschau", @@ -1502,6 +1511,8 @@ "compress_target_bisection": "Bisektionssuche läuft [Iteration {current}/{total}]...", "compress_target_resampling": "Seiten werden mit optimaler Auflösung neu gerendert ({current}/{total})...", "compress_target_finetune": "Feinabstimmung der Skalierungsparameter...", - "compress_target_boost": "Schärfeparameter werden optimiert...", - "compress_target_done": "Zielgrößen-Komprimierung abgeschlossen" + "compress_target_done": "Zielgrößen-Komprimierung abgeschlossen", + "compress_target_optimal_quality_hint": "≤ {target} MB erfüllt · Maximale Schärfe", + "compress_target_converged_hint": "Begrenzt auf ≤ {target} MB", + "compress_target_reassurance_desc": "Dokument hat optimale Druckqualität ({dpi}) ohne unnötigen Dateiballast erreicht. Vollständig konform mit Ihrem Zielgrößenlimit." } \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index 29d5657..4f0d835 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -456,10 +456,11 @@ "compress_target_size_subtitle": "Meets strict limits for visa, government, and thesis portals", "compress_target_limit_symbol": "≤", "compress_quick_presets": "Standard Presets", - "compress_preset_1mb": "Visa / Passport", - "compress_preset_2mb": "Government / Thesis", - "compress_preset_5mb": "Tenders / Contracts", - "compress_preset_10mb": "Email Attachment", + "compress_preset_1mb": "ID / Strict Portals", + "compress_preset_2mb": "Visa (CEAC / VFS) / College", + "compress_preset_5mb": "Canada IRCC / Workday", + "compress_preset_10mb": "USCIS / Contracts / Thesis", + "compress_preset_20mb": "Email Safe Limit", "compress_target_current_size": "Original Size", "compress_target_goal": "Target Cap", "compress_target_already_smaller": "Already below target limit; lossless vector optimization will be used", @@ -1489,6 +1490,14 @@ "next_action_badge_general": "Seamless Relay", "next_action_desc_general": "File held in browser memory. Seamlessly relay to your next task with zero re-uploading.", "next_action_desc_merge_paginated": "Merged documents often have fragmented page numbering. Re-paginate the combined document sequentially.", + "next_action_badge_compressed": "Size Optimized", + "next_action_desc_compress_done": "Document successfully compressed! Add password encryption to lock content, or proceed to sign.", + "next_action_badge_organized": "Pages Reordered", + "next_action_desc_organize_done": "Pages reorganized! If order changed or pages were removed, re-paginate sequentially or proceed to compress.", + "next_action_badge_split": "Split Ready", + "next_action_desc_split_done": "Pages split & extracted! Re-paginate the extracted section from page 1, or relay to compress.", + "next_action_badge_protected": "Secured", + "next_action_desc_protect_done": "Document encrypted and locked! It is protected against tampering and ready to share or archive to Vault.", "loading_file": "Loading file...", "loading_default": "Processing...", "preview_title": "Page HD Preview", @@ -1502,6 +1511,8 @@ "compress_target_bisection": "Bisection search in progress [iteration {current}/{total}]...", "compress_target_resampling": "Resampling pages at optimal resolution ({current}/{total})...", "compress_target_finetune": "Fine-tuning scale parameters to strictly meet target limit...", - "compress_target_boost": "Boosting clarity parameters to maximize visual sharpness...", - "compress_target_done": "Target size compression complete" + "compress_target_done": "Target size compression complete", + "compress_target_optimal_quality_hint": "≤ {target} MB Satisfied · Max Clarity", + "compress_target_converged_hint": "Capped at ≤ {target} MB", + "compress_target_reassurance_desc": "Document reached optimal print-grade clarity ({dpi}) without unnecessary file bulk. Fully compliant with your target size limit." } \ No newline at end of file diff --git a/src/locales/es.json b/src/locales/es.json index 0a6b112..b06b9bc 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -456,10 +456,11 @@ "compress_target_size_subtitle": "Cumple los requisitos estrictos de visados, trámites y tesis", "compress_target_limit_symbol": "≤", "compress_quick_presets": "Estándares habituales", - "compress_preset_1mb": "Visado / Pasaporte", - "compress_preset_2mb": "Trámites / Tesis", - "compress_preset_5mb": "Licitaciones / Contratos", - "compress_preset_10mb": "Adjunto de correo", + "compress_preset_1mb": "Identificación / Portales estrictos", + "compress_preset_2mb": "Visado (CEAC / VFS) / Universidad", + "compress_preset_5mb": "Canadá IRCC / Empleo", + "compress_preset_10mb": "USCIS / Contratos / Tesis", + "compress_preset_20mb": "Límite seguro de correo", "compress_target_current_size": "Tamaño original", "compress_target_goal": "Límite objetivo", "compress_target_already_smaller": "Ya está por debajo del límite; se usará optimización vectorial sin pérdidas", @@ -1489,6 +1490,14 @@ "next_action_badge_general": "Relevo continuo", "next_action_desc_general": "Archivo retenido en memoria. Continúe con la siguiente tarea sin volver a subir.", "next_action_desc_merge_paginated": "Los documentos combinados a menudo tienen numeración fragmentada. Vuelva a numerar el documento completo de forma secuencial.", + "next_action_badge_compressed": "Tamaño optimizado", + "next_action_desc_compress_done": "¡Documento comprimido con éxito! Agregue protección con contraseña o continúe firmando.", + "next_action_badge_organized": "Páginas reorganizadas", + "next_action_desc_organize_done": "¡Páginas reorganizadas! Si eliminó o reordenó páginas, renúmelas secuencialmente.", + "next_action_badge_split": "División lista", + "next_action_desc_split_done": "¡Páginas divididas y extraídas! Renumere la sección extraída desde la página 1 o comprímala.", + "next_action_badge_protected": "Protegido", + "next_action_desc_protect_done": "¡Documento encriptado y bloqueado! Listo para compartir de forma segura o guardar en el almacén.", "loading_file": "Loading file...", "loading_default": "Processing...", "preview_title": "Vista previa HD", @@ -1502,6 +1511,8 @@ "compress_target_bisection": "Búsqueda por bisección en progreso [iteración {current}/{total}]...", "compress_target_resampling": "Remuestreando páginas con resolución óptima ({current}/{total})...", "compress_target_finetune": "Ajustando parámetros para cumplir con el límite objetivo...", - "compress_target_boost": "Optimizando parámetros para maximizar la nitidez...", - "compress_target_done": "Compresión a tamaño objetivo completada" + "compress_target_done": "Compresión a tamaño objetivo completada", + "compress_target_optimal_quality_hint": "≤ {target} MB cumplido · Máxima claridad", + "compress_target_converged_hint": "Limitado a ≤ {target} MB", + "compress_target_reassurance_desc": "El documento alcanzó una calidad óptima de impresión ({dpi}) sin exceso de tamaño. Totalmente compatible con su límite objetivo." } \ No newline at end of file diff --git a/src/locales/fr.json b/src/locales/fr.json index 3e5e626..a196c72 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -456,10 +456,11 @@ "compress_target_size_subtitle": "Respecte les limites strictes pour visas, démarches administratives et thèses", "compress_target_limit_symbol": "≤", "compress_quick_presets": "Standards courants", - "compress_preset_1mb": "Visa / Passeport", - "compress_preset_2mb": "Démarches / Thèse", - "compress_preset_5mb": "Appels d'offres / Contrats", - "compress_preset_10mb": "Pièce jointe e-mail", + "compress_preset_1mb": "Pièce d'identité / Portails stricts", + "compress_preset_2mb": "Visa (CEAC / VFS) / Université", + "compress_preset_5mb": "Canada IRCC / Recrutement", + "compress_preset_10mb": "USCIS / Contrats / Thèse", + "compress_preset_20mb": "Limite sûre d'e-mail", "compress_target_current_size": "Taille originale", "compress_target_goal": "Plafond cible", "compress_target_already_smaller": "Déjà sous la limite ; optimisation vectorielle sans perte utilisée", @@ -1489,6 +1490,14 @@ "next_action_badge_general": "Relais continu", "next_action_desc_general": "Fichier conservé en mémoire. Poursuivez votre tâche sans aucun nouveau téléversement.", "next_action_desc_merge_paginated": "Les documents fusionnés ont souvent une numérotation fragmentée. Re-numérotez le document de manière séquentielle.", + "next_action_badge_compressed": "Taille optimisée", + "next_action_desc_compress_done": "Document compressé avec succès ! Ajoutez un mot de passe ou procédez à la signature.", + "next_action_badge_organized": "Pages réorganisées", + "next_action_desc_organize_done": "Pages réorganisées ! Si l'ordre a changé ou des pages ont été supprimées, re-numérotez-les.", + "next_action_badge_split": "Division prête", + "next_action_desc_split_done": "Pages extraites avec succès ! Re-numérotez la section depuis la page 1 ou compressez-la.", + "next_action_badge_protected": "Sécurisé", + "next_action_desc_protect_done": "Document chiffré et verrouillé ! Prêt pour un partage sécurisé ou archivage dans le coffre-fort.", "loading_file": "Loading file...", "loading_default": "Processing...", "preview_title": "Aperçu HD de la page", @@ -1502,6 +1511,8 @@ "compress_target_bisection": "Recherche par dichotomie en cours [itération {current}/{total}]...", "compress_target_resampling": "Rééchantillonnage des pages à la résolution optimale ({current}/{total})...", "compress_target_finetune": "Ajustement fin des paramètres de mise à l'échelle...", - "compress_target_boost": "Amélioration de la netteté visuelle...", - "compress_target_done": "Compression à la taille cible terminée" + "compress_target_done": "Compression à la taille cible terminée", + "compress_target_optimal_quality_hint": "≤ {target} Mo respecté · Clarté maximale", + "compress_target_converged_hint": "Plafonné à ≤ {target} Mo", + "compress_target_reassurance_desc": "Le document a atteint une netteté d'impression optimale ({dpi}) sans volume superflu, conforme à votre taille cible." } \ No newline at end of file diff --git a/src/locales/ja.json b/src/locales/ja.json index fb4dcd3..69e32e8 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -456,10 +456,11 @@ "compress_target_size_subtitle": "ビザ、政府、論文ポータルの厳しい制限を満たしています", "compress_target_limit_symbol": "≤", "compress_quick_presets": "標準プリセット", - "compress_preset_1mb": "ビザ・パスポート", - "compress_preset_2mb": "政府・論文", - "compress_preset_5mb": "入札・契約", - "compress_preset_10mb": "電子メールの添付ファイル", + "compress_preset_1mb": "身分証 / 厳格ポータル", + "compress_preset_2mb": "ビザ (CEAC / VFS) / 出願", + "compress_preset_5mb": "カナダ IRCC / 採用応募", + "compress_preset_10mb": "USCIS / 契約書 / 論文", + "compress_preset_20mb": "メール安全上限", "compress_target_current_size": "元のサイズ", "compress_target_goal": "ターゲットキャップ", "compress_target_already_smaller": "すでに目標制限を下回っています。ロスレスベクトル最適化が使用されます", @@ -1489,6 +1490,14 @@ "next_action_badge_general": "シームレス中継", "next_action_desc_general": "ブラウザのメモリに保持されているファイル。再アップロードすることなく、次のタスクにシームレスに中継します。", "next_action_desc_merge_paginated": "結合されたドキュメントには、断片的なページ番号が付いていることがよくあります。結合されたドキュメントを順番に再ページ設定します。", + "next_action_badge_compressed": "サイズ最適化済", + "next_action_desc_compress_done": "目標サイズへの圧縮が完了しました!改ざん防止の暗号化や、署名・捺印へ直接進めます。", + "next_action_badge_organized": "ページ整理済", + "next_action_desc_organize_done": "ページの調整と並べ替えが完了しました!必要に応じて通しページ番号の再付与や圧縮に進めます。", + "next_action_badge_split": "分割完了", + "next_action_desc_split_done": "ページの分割・抽出が完了しました!抽出文書に通しページ番号を振るか、圧縮へ進めます。", + "next_action_badge_protected": "保護完了", + "next_action_desc_protect_done": "暗号化と権限ロックが完了しました!安全に共有するか、ローカル保管庫に保存できます。", "loading_file": "ファイルをロード中...", "loading_default": "処理...", "preview_title": "ページの HD プレビュー", @@ -1502,6 +1511,8 @@ "compress_target_bisection": "二等分検索が進行中です [反復{current}/{total}]...", "compress_target_resampling": "最適な解像度でページをリサンプリングしています ({current}/{total})...", "compress_target_finetune": "目標制限を厳密に満たすためにスケール パラメータを微調整します...", - "compress_target_boost": "視覚的な鮮明さを最大化するために明瞭度パラメータをブーストします...", - "compress_target_done": "ターゲットサイズの圧縮が完了しました" + "compress_target_done": "ターゲットサイズの圧縮が完了しました", + "compress_target_optimal_quality_hint": "≤ {target} MB 達成 · 最高画質", + "compress_target_converged_hint": "≤ {target} MB に制限完了", + "compress_target_reassurance_desc": "ドキュメントは不要なファイル容量を増やすことなく、印刷に適した最適な鮮明度({dpi})に達しました。設定した目標サイズ制限に完全に適合しています。" } \ No newline at end of file diff --git a/src/locales/zh.json b/src/locales/zh.json index c2452b5..3b13be8 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -456,10 +456,11 @@ "compress_target_size_subtitle": "满足签证、政务、高校论文等刚性上传限制", "compress_target_limit_symbol": "≤", "compress_quick_presets": "常用标准", - "compress_preset_1mb": "签证 / 护照", - "compress_preset_2mb": "政务 / 论文", - "compress_preset_5mb": "招投标 / 合同", - "compress_preset_10mb": "邮件常用上限", + "compress_preset_1mb": "证件 / 严格表单", + "compress_preset_2mb": "签证 / CEAC / VFS", + "compress_preset_5mb": "加拿大 IRCC / 网申招聘", + "compress_preset_10mb": "USCIS / 标书 / 论文", + "compress_preset_20mb": "邮件安全上限", "compress_target_current_size": "原文件大小", "compress_target_goal": "目标上限", "compress_target_already_smaller": "已低于目标,将自动使用 100% 矢量无损保真", @@ -1489,6 +1490,14 @@ "next_action_badge_general": "无缝接力", "next_action_desc_general": "文件已保留在浏览器内存中,零等待直接流转至下一步处理环节。", "next_action_desc_merge_paginated": "多个原文件拼接后各章节页码已割裂,建议为新文档一键重新编排连续页码。", + "next_action_badge_compressed": "体积已优化", + "next_action_desc_compress_done": "文档已成功压缩至目标体积!如需防止他人篡改内容可一键加密,或直接前往盖章签署。", + "next_action_badge_organized": "排版已重整", + "next_action_desc_organize_done": "页面调整与排序完毕!若删改或调换了次序,建议重新编排规范页码,或直接前往压缩/签署。", + "next_action_badge_split": "拆分已就绪", + "next_action_desc_split_done": "页面拆分与提取完毕!提取出的独立篇章建议重新从第 1 页编排规范页码,或按需进行压缩与外发。", + "next_action_badge_protected": "安全已加固", + "next_action_desc_protect_done": "文档已成功加密与权限锁定!文件已具备防篡改保护,可安全外发或存入本地收纳箱。", "loading_file": "正在加载文件...", "loading_default": "处理中...", "preview_title": "页面高清预览", @@ -1502,6 +1511,8 @@ "compress_target_bisection": "正在智能推演最佳压缩率 (轮次 {current}/{total})...", "compress_target_resampling": "正在以最优分辨率重采样页面 ({current}/{total})...", "compress_target_finetune": "正在微调尺寸参数以严格符合目标大小...", - "compress_target_boost": "正在智能提升清晰度参数以最大化画质...", - "compress_target_done": "目标大小压缩完成!" + "compress_target_done": "目标大小压缩完成!", + "compress_target_optimal_quality_hint": "≤ {target} MB 已达标 · 最佳清晰度", + "compress_target_converged_hint": "已控制在 ≤ {target} MB 内", + "compress_target_reassurance_desc": "文档已达到最佳印刷级清晰度({dpi}),无需填充多余体积,已完全符合您设定的目标限制。" } \ No newline at end of file diff --git a/src/tools/CompressTool.vue b/src/tools/CompressTool.vue index 6ec981f..03ed12f 100644 --- a/src/tools/CompressTool.vue +++ b/src/tools/CompressTool.vue @@ -154,9 +154,41 @@ @@ -336,7 +368,8 @@ { mb: 1, label: '1 MB', tip: t('compress_preset_1mb') }, { mb: 2, label: '2 MB', tip: t('compress_preset_2mb') }, { mb: 5, label: '5 MB', tip: t('compress_preset_5mb') }, - { mb: 10, label: '10 MB', tip: t('compress_preset_10mb') } + { mb: 10, label: '10 MB', tip: t('compress_preset_10mb') }, + { mb: 20, label: '20 MB', tip: t('compress_preset_20mb') } ]" :key="preset.mb" @click="targetSizeMb = preset.mb" @@ -512,6 +545,17 @@ const detectedType = ref(null); // 'vector' | 'scanned' const selectedLevel = ref('balanced'); // 'extreme' | 'balanced' | 'target' | 'lossless' const targetSizeMb = ref(2.0); +const isTargetOptimalQuality = computed(() => { + if (selectedLevel.value !== 'target' || !lastExportedFile.value) return false; + const target = Number(targetSizeMb.value) || 2.0; + const compressed = Number(compressedSizeMb.value) || 0; + return compressed > 0 && compressed < target * 0.75; +}); + +const maxDpiText = computed(() => { + return totalPages.value <= 3 ? '300 DPI' : '200 DPI'; +}); + // Diff Preview Modal & Thumbnails State const isDiffModalOpen = ref(false); const originalThumbnailUrl = ref(''); diff --git a/src/utils/pdfCompress.js b/src/utils/pdfCompress.js index baa19e1..d83a484 100644 --- a/src/utils/pdfCompress.js +++ b/src/utils/pdfCompress.js @@ -247,19 +247,18 @@ export async function compressPdfRaster(arrayBuffer, options = {}, onProgress = /** * Maps a normalized quality index t in [0.0, 1.0] monotonically to Canvas scale and JPEG quality. * - * - t = 1.0 (Maximum quality): scale = 2.50 (~180-200 DPI), quality = 0.85 - * - t = 0.5 (Balanced quality): scale = 1.85 (~130-150 DPI), quality = 0.70 - * - t = 0.0 (Extreme compact): scale = 0.75 (~54-72 DPI), quality = 0.35 - * - * Both scale and quality are strictly monotonic increasing with t, guaranteeing mathematical - * convergence in bisection search. + * Dynamic DPI Scaling Heuristic: + * - If numPages <= 3 (e.g., single-page certificates, diplomas, IDs): max scale = 4.17 (~300 DPI) + * - If numPages > 3 (e.g., multi-page reports, long contracts): max scale = 2.50 (~180-200 DPI) * * @param {number} t - Normalized index in [0, 1] + * @param {number} [numPages=4] - Total pages in document (defaults to 4 for backward-compatible 200 DPI baseline) * @returns {{ scale: number, quality: number }} */ -export function paramFromQualityIndex(t) { +export function paramFromQualityIndex(t, numPages = 4) { const clamped = Math.max(0, Math.min(1, Number(t) || 0)); - const scale = Number((0.75 + 1.75 * Math.pow(clamped, 0.65)).toFixed(2)); + const maxScale = (Number(numPages) > 0 && Number(numPages) <= 3) ? 4.17 : 2.50; + const scale = Number((0.75 + (maxScale - 0.75) * Math.pow(clamped, 0.65)).toFixed(2)); const quality = Number((0.35 + 0.50 * Math.pow(clamped, 0.5)).toFixed(2)); return { scale, quality }; } @@ -382,7 +381,7 @@ export async function compressPdfToTargetSize(arrayBuffer, targetSizeMb = 2.0, o } const probeSampleBytes = async (t) => { - const { scale, quality } = paramFromQualityIndex(t); + const { scale, quality } = paramFromQualityIndex(t, numPages); let sampleJpgTotal = 0; for (const page of loadedSamplePages) { @@ -446,7 +445,7 @@ export async function compressPdfToTargetSize(arrayBuffer, targetSizeMb = 2.0, o const clampedFraction = Math.max(0, Math.min(0.95, fraction)); // 95% safety ceiling inside interval optimalT = Math.max(0, Math.min(1, tLow + (tHigh - tLow) * clampedFraction)); } - let currentParams = paramFromQualityIndex(optimalT); + let currentParams = paramFromQualityIndex(optimalT, numPages); logger.info('COMPRESS_TARGET', `Bisection optimal candidate t=${optimalT.toFixed(3)} (scale=${currentParams.scale}, quality=${currentParams.quality})`); // Phase 2: Full Document Rendering with Selected Optimal Parameters diff --git a/src/utils/workflowStrategy.js b/src/utils/workflowStrategy.js index 3edc04d..f91cfc4 100644 --- a/src/utils/workflowStrategy.js +++ b/src/utils/workflowStrategy.js @@ -224,16 +224,16 @@ export function resolveNextActions(sourceTool, file, options = {}) { case 'organize': heroId = 'page_number'; - badgeKey = 'next_action_badge_ready'; - descKey = 'next_action_desc_merge_paginated'; + badgeKey = 'next_action_badge_organized'; + descKey = 'next_action_desc_organize_done'; ctaKey = 'next_action_cta_page_number'; secondaryIds = ['compress', 'sign', 'protect']; break; case 'split': heroId = 'page_number'; - badgeKey = 'next_action_badge_ready'; - descKey = 'next_action_desc_merge_paginated'; + badgeKey = 'next_action_badge_split'; + descKey = 'next_action_desc_split_done'; ctaKey = 'next_action_cta_page_number'; secondaryIds = ['compress', 'sign', 'organize']; break; @@ -296,18 +296,18 @@ export function resolveNextActions(sourceTool, file, options = {}) { case 'compress': heroId = 'protect'; - badgeKey = 'next_action_badge_ready'; - descKey = 'next_action_desc_protect_signed'; + badgeKey = 'next_action_badge_compressed'; + descKey = 'next_action_desc_compress_done'; ctaKey = 'next_action_cta_protect'; secondaryIds = ['sign', 'watermark', 'pdf_to_image']; break; case 'protect': - heroId = 'watermark'; - badgeKey = 'next_action_badge_ready'; - descKey = 'next_action_desc_general'; - ctaKey = 'next_action_cta_watermark'; - secondaryIds = ['compress', 'sign', 'sanitize']; + heroId = 'sign'; + badgeKey = 'next_action_badge_protected'; + descKey = 'next_action_desc_protect_done'; + ctaKey = 'next_action_cta_sign'; + secondaryIds = ['watermark', 'compress', 'sanitize']; break; default: diff --git a/tests/compressTargetSize.test.js b/tests/compressTargetSize.test.js index e89ab44..ef7898d 100644 --- a/tests/compressTargetSize.test.js +++ b/tests/compressTargetSize.test.js @@ -144,4 +144,45 @@ describe('Target Size Compression & Bisection Engine (Sprint 3.2)', () => { expect(proCheck.pass).toBe(true); }); }); + + describe('Target Size Quality Reassurance Badges & i18n Localization', () => { + it('should format reassurance hint and description across English, Chinese and Japanese', async () => { + const { t, setLanguage } = await import('../src/i18n.js'); + + setLanguage('en'); + const enHint = t('compress_target_optimal_quality_hint', { target: '2.0' }); + expect(enHint).toContain('2.0 MB Satisfied'); + expect(t('compress_target_reassurance_desc', { dpi: '300 DPI' })).toContain('300 DPI'); + expect(t('compress_target_reassurance_desc', { dpi: '200 DPI' })).toContain('200 DPI'); + + setLanguage('zh'); + const zhHint = t('compress_target_optimal_quality_hint', { target: '2.0' }); + expect(zhHint).toContain('2.0 MB'); + expect(zhHint).toContain('最佳清晰度'); + expect(t('compress_target_reassurance_desc', { dpi: '300 DPI' })).toContain('300 DPI'); + expect(t('compress_target_reassurance_desc', { dpi: '300 DPI' })).toContain('无需填充多余体积'); + + setLanguage('ja'); + const jaHint = t('compress_target_optimal_quality_hint', { target: '2.0' }); + expect(jaHint).toContain('2.0 MB'); + expect(jaHint).toContain('最高画質'); + expect(t('compress_target_reassurance_desc', { dpi: '300 DPI' })).toContain('300 DPI'); + }); + + it('should dynamically switch maximum scale between 300 DPI (<= 3 pages) and 200 DPI (> 3 pages)', () => { + // Single page document (e.g. 1-page certificate) -> scale 4.17 (300 DPI) + const singlePageMax = paramFromQualityIndex(1, 1); + expect(singlePageMax.scale).toBe(4.17); + expect(singlePageMax.quality).toBe(0.85); + + // 3 pages document -> scale 4.17 (300 DPI) + const threePagesMax = paramFromQualityIndex(1, 3); + expect(threePagesMax.scale).toBe(4.17); + + // 4+ pages document -> scale 2.50 (200 DPI safe baseline) + const multiPageMax = paramFromQualityIndex(1, 5); + expect(multiPageMax.scale).toBe(2.50); + expect(multiPageMax.quality).toBe(0.85); + }); + }); }); diff --git a/tests/workflowStrategy.test.js b/tests/workflowStrategy.test.js index 9b8d8f7..371d662 100644 --- a/tests/workflowStrategy.test.js +++ b/tests/workflowStrategy.test.js @@ -79,6 +79,45 @@ describe('Workflow Strategy Engine (智能情境推荐引擎)', () => { expect(result.heroAction.id).not.toBe('compress'); }); + it('should recommend Protect with compressed badge and compress_done description after Compress tool', () => { + const compressedPdf = { + name: 'visa_compressed.pdf', + size: 1.8 * 1024 * 1024 + }; + + const result = resolveNextActions('compress', compressedPdf); + expect(result.heroAction.id).toBe('protect'); + expect(result.heroAction.badgeKey).toBe('next_action_badge_compressed'); + expect(result.heroAction.descKey).toBe('next_action_desc_compress_done'); + expect(result.heroAction.ctaKey).toBe('next_action_cta_protect'); + expect(result.secondaryActions.map(a => a.id)).not.toContain('compress'); + }); + + it('should recommend Page Number with organize_done description after Organize tool', () => { + const organizedPdf = { + name: 'portfolio_reordered.pdf', + size: 2.2 * 1024 * 1024 + }; + + const result = resolveNextActions('organize', organizedPdf); + expect(result.heroAction.id).toBe('page_number'); + expect(result.heroAction.badgeKey).toBe('next_action_badge_organized'); + expect(result.heroAction.descKey).toBe('next_action_desc_organize_done'); + }); + + it('should recommend Page Number with split_done description after Split tool (single PDF extract)', () => { + const extractedPdf = { + name: 'chapter_1.pdf', + size: 1.2 * 1024 * 1024, + isZip: false + }; + + const result = resolveNextActions('split', extractedPdf); + expect(result.heroAction.id).toBe('page_number'); + expect(result.heroAction.badgeKey).toBe('next_action_badge_split'); + expect(result.heroAction.descKey).toBe('next_action_desc_split_done'); + }); + it('should recommend Page Number as Hero for normal-sized Merge files (<= 10MB) with clean secondary actions', () => { const mergedPdf = { name: 'agreement_merged.pdf',