fix(compress-pdf): land close to the target size, honestly (#522)

Target-size compression had only a coarse DPI lever, so it undershot badly (a 350KB target could land at 216KB) and silently missed unreachable targets. Adds JPEG quality as a second lever (forced re-encode so it bites on JPEG scans), folds both into one monotonic quality axis that target-size binary-searches, reports targetMet honestly in the panel across 21 locales, and flips the tool to async for the extra passes. Quality-mode output sizes shift intentionally (slider now drives JPEG quality at full resolution in its top half).
This commit is contained in:
SnapOtter
2026-07-16 15:08:21 +08:00
committed by GitHub
parent f858c4cea0
commit 7d938af1f9
30 changed files with 344 additions and 65 deletions
@@ -10,8 +10,15 @@ export function CompressPdfSettings() {
const { t } = useTranslation();
const s = t.toolSettings["compress-pdf"];
const { files } = useFileStore();
const { processFiles, processAllFiles, processing, error, progress } =
useToolProcessor("compress-pdf");
const {
processFiles,
processAllFiles,
processing,
error,
progress,
resultPayload,
processedSize,
} = useToolProcessor("compress-pdf");
const [settings, setSettings] = useState<Record<string, unknown>>({});
const hasFile = files.length > 0;
@@ -21,6 +28,12 @@ export function CompressPdfSettings() {
settings.mode === "quality" ||
(settings.mode === "targetSize" && Number(settings.targetSizeKb) > 0);
// Honest reporting for target-size mode: whether we actually hit the ceiling.
const targetMet = resultPayload?.targetMet as boolean | undefined;
const targetKb = resultPayload?.targetKb as number | undefined;
const targetLabel = targetKb != null ? `${Math.round(targetKb)} KB` : "";
const achievedLabel = processedSize != null ? `${Math.round(processedSize / 1024)} KB` : "";
const handleProcess = () => {
if (hasMultiple) {
processAllFiles(files, settings);
@@ -34,8 +47,23 @@ export function CompressPdfSettings() {
{/* Same quality / target-size controls as the image compress tool */}
<CompressControls onChange={setSettings} />
{settings.mode === "targetSize" && (
<p className="text-[11px] text-muted-foreground">{s.bestEffortHint}</p>
)}
{error && <p className="text-xs text-red-500">{error}</p>}
{targetMet === false && (
<p className="text-xs text-amber-600">
{format(s.targetMissed, { target: targetLabel, size: achievedLabel })}
</p>
)}
{targetMet === true && (
<p className="text-xs text-green-600">
{format(s.targetReached, { target: targetLabel, size: achievedLabel })}
</p>
)}
{processing ? (
<ProgressCard
active={processing}