fix(web): lecteur PDF — pages blanches après rendu et base minimale stable
Le canvas PDF restait vide après rendu (blank-detected): la détection d'encre reposait sur un seuil de blanc trop sensible et un fond de canvas non maîtrisé. Le rendu passe à un fond sentinel explicite avec classification rendered-ok/blank-detected et un seuil de pixels visibles. Le lecteur est par ailleurs ramené à une base minimale stable: layout plein viewport (app-main-reader), suppression des modes de lecture et des préférences par livre, helpers de pagination/viewport extraits dans readerLayout.ts. Shell, lecteurs CBZ/EPUB et styles alignés sur cette base. Refs #31 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { epubFileName } from "./EpubReader";
|
||||
import { pdfCanvasHasVisibleInk, pdfRenderScale } from "./pdfRender";
|
||||
import { clampReaderPage, containPageSize, nextReaderPage, orientedContainPageSize, previousReaderPage, readableViewportSize, readerPositionLabel } from "./readerLayout";
|
||||
import { classifyPdfCanvas, pdfCanvasHasVisibleContent, pdfCanvasVisible, pdfRenderScale } from "./pdfRender";
|
||||
import { configurePdfWorker, pdfWorkerSrc } from "./pdfWorker";
|
||||
import { readerErrorMessage } from "./ReaderError";
|
||||
|
||||
@ -32,13 +33,80 @@ describe("reader runtime helpers", () => {
|
||||
expect(readerErrorMessage("", "EPUB indisponible")).toBe("EPUB indisponible");
|
||||
});
|
||||
|
||||
it("separates PDF fit-page and fit-width scales", () => {
|
||||
expect(pdfRenderScale("horizontal", { width: 800, height: 600 }, { width: 400, height: 800 })).toBe(0.75);
|
||||
expect(pdfRenderScale("vertical", { width: 800, height: 600 }, { width: 400, height: 800 })).toBe(1.65);
|
||||
it("fits PDF pages by containment inside the reader viewport", () => {
|
||||
expect(pdfRenderScale({ width: 800, height: 600 }, { width: 400, height: 800 })).toBe(0.75);
|
||||
expect(pdfRenderScale({ width: 800, height: 600 }, { width: 800, height: 400 })).toBe(1);
|
||||
});
|
||||
|
||||
it("rejects blank PDF canvas renders", () => {
|
||||
expect(pdfCanvasHasVisibleInk({ data: new Uint8ClampedArray([255, 255, 255, 255, 247, 240, 223, 255]) })).toBe(false);
|
||||
expect(pdfCanvasHasVisibleInk({ data: new Uint8ClampedArray([255, 255, 255, 255, 32, 28, 24, 255]) })).toBe(true);
|
||||
it("does not expose a PDF canvas before a successful page render", () => {
|
||||
expect(pdfCanvasVisible(false, false)).toBe(false);
|
||||
expect(pdfCanvasVisible(true, true)).toBe(false);
|
||||
expect(pdfCanvasVisible(true, false)).toBe(true);
|
||||
});
|
||||
|
||||
it("classifies blank PDF canvas data separately from render failures", () => {
|
||||
expect(classifyPdfCanvas({ data: new Uint8ClampedArray([255, 255, 255, 255, 255, 255, 255, 255]) })).toBe("blank-detected");
|
||||
expect(classifyPdfCanvas({ data: new Uint8ClampedArray([247, 240, 223, 255, 247, 240, 223, 255]) })).toBe("blank-detected");
|
||||
|
||||
const inkPixels = new Uint8ClampedArray(12 * 4);
|
||||
for (let index = 0; index < inkPixels.length; index += 4) {
|
||||
inkPixels[index] = 32;
|
||||
inkPixels[index + 1] = 28;
|
||||
inkPixels[index + 2] = 24;
|
||||
inkPixels[index + 3] = 255;
|
||||
}
|
||||
expect(classifyPdfCanvas({ data: inkPixels })).toBe("rendered-ok");
|
||||
});
|
||||
|
||||
it("does not treat isolated PDF canvas noise as visible content", () => {
|
||||
const noisyPixels = new Uint8ClampedArray([
|
||||
255, 255, 255, 255,
|
||||
32, 28, 24, 255,
|
||||
255, 255, 255, 255
|
||||
]);
|
||||
expect(pdfCanvasHasVisibleContent({ data: noisyPixels })).toBe(false);
|
||||
});
|
||||
|
||||
it("classifies very light but non-empty PDF canvas data as rendered", () => {
|
||||
const lightInkPixels = new Uint8ClampedArray(12 * 4);
|
||||
for (let index = 0; index < lightInkPixels.length; index += 4) {
|
||||
lightInkPixels[index] = 244;
|
||||
lightInkPixels[index + 1] = 244;
|
||||
lightInkPixels[index + 2] = 244;
|
||||
lightInkPixels[index + 3] = 255;
|
||||
}
|
||||
|
||||
expect(classifyPdfCanvas({ data: lightInkPixels })).toBe("rendered-ok");
|
||||
});
|
||||
|
||||
it("contains page media inside the reader viewport without cropping", () => {
|
||||
expect(containPageSize({ width: 1200, height: 700 }, { width: 900, height: 1600 })).toEqual({ width: 393, height: 700 });
|
||||
expect(containPageSize({ width: 1200, height: 700 }, { width: 1600, height: 900 })).toEqual({ width: 1200, height: 675 });
|
||||
});
|
||||
|
||||
it("fits comic pages by intrinsic orientation without cropping", () => {
|
||||
expect(orientedContainPageSize({ width: 1200, height: 700 }, { width: 900, height: 1600 })).toEqual({ width: 393, height: 700 });
|
||||
expect(orientedContainPageSize({ width: 1200, height: 700 }, { width: 1600, height: 900 })).toEqual({ width: 1200, height: 675 });
|
||||
expect(orientedContainPageSize({ width: 500, height: 900 }, { width: 1200, height: 900 })).toEqual({ width: 500, height: 375 });
|
||||
});
|
||||
|
||||
it("ignores unusable reader viewport measurements", () => {
|
||||
expect(readableViewportSize({ width: 0, height: 833 })).toBeNull();
|
||||
expect(readableViewportSize({ width: 1, height: 1 })).toBeNull();
|
||||
expect(readableViewportSize({ width: 1548.8, height: 833.2 })).toEqual({ width: 1548, height: 833 });
|
||||
});
|
||||
|
||||
it("keeps the minimal reader on one clamped page", () => {
|
||||
expect(clampReaderPage(0, 12)).toBe(1);
|
||||
expect(clampReaderPage(8, 12)).toBe(8);
|
||||
expect(clampReaderPage(99, 12)).toBe(12);
|
||||
expect(readerPositionLabel(8, 12)).toBe("Page 8 / 12");
|
||||
});
|
||||
|
||||
it("navigates page by page without leaving document bounds", () => {
|
||||
expect(previousReaderPage(1, 12)).toBe(1);
|
||||
expect(previousReaderPage(8, 12)).toBe(7);
|
||||
expect(nextReaderPage(8, 12)).toBe(9);
|
||||
expect(nextReaderPage(12, 12)).toBe(12);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user