From 1300772f3663693b2c5fed3a7b3061b8857ac493 Mon Sep 17 00:00:00 2001 From: Seedgou Date: Mon, 13 Nov 2023 18:43:18 +0800 Subject: [PATCH 01/32] add canvas scan function --- src/utils/canvas-scan/index.ts | 2 ++ src/utils/canvas-scan/noise.svg | 17 +++++++++ src/utils/canvas-scan/scan.ts | 61 +++++++++++++++++++++++++++++++++ src/utils/canvas-scan/types.ts | 23 +++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 src/utils/canvas-scan/index.ts create mode 100644 src/utils/canvas-scan/noise.svg create mode 100644 src/utils/canvas-scan/scan.ts create mode 100644 src/utils/canvas-scan/types.ts diff --git a/src/utils/canvas-scan/index.ts b/src/utils/canvas-scan/index.ts new file mode 100644 index 00000000..11de0943 --- /dev/null +++ b/src/utils/canvas-scan/index.ts @@ -0,0 +1,2 @@ +export { type ScanConfig, defaultConfig, colorspaces } from "./types"; +export { scanCanvas } from "./scan"; diff --git a/src/utils/canvas-scan/noise.svg b/src/utils/canvas-scan/noise.svg new file mode 100644 index 00000000..872af524 --- /dev/null +++ b/src/utils/canvas-scan/noise.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/utils/canvas-scan/scan.ts b/src/utils/canvas-scan/scan.ts new file mode 100644 index 00000000..d726b66f --- /dev/null +++ b/src/utils/canvas-scan/scan.ts @@ -0,0 +1,61 @@ +import type { ScanConfig } from "./types"; +import noiseSVG from "./noise.svg?url"; + +export async function scanCanvas( + canvas: HTMLCanvasElement, + page: Blob, + config: ScanConfig +): Promise { + const ctx = canvas.getContext("2d"); + if (!ctx) { + throw new Error("Canvas not supported"); + } + + // load blob into image + const img = new Image(); + img.src = URL.createObjectURL(page); + await new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = reject; + }); + canvas.width = img.width; + canvas.height = img.height; + + // fill white + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // add blur + ctx.filter = `blur(${config.blur}px)`; + if (config.colorspace === "gray") { + ctx.filter += " grayscale(1)"; + } + + // rotate + ctx.translate(canvas.width / 2, canvas.height / 2); + ctx.rotate( + ((config.rotate + config.rotate_var * Math.random()) * Math.PI) / 180 + ); + ctx.translate(-canvas.width / 2, -canvas.height / 2); + + ctx.drawImage(img, 0, 0); + + const noiseImg = new Image(); + noiseImg.src = noiseSVG; + await new Promise((resolve) => (noiseImg.onload = resolve)); + + // add noise + ctx.drawImage( + noiseImg, + -canvas.width, + -canvas.height, + canvas.width * 2, + canvas.height * 2 + ); + + if (config.border) { + ctx.strokeStyle = "black"; + ctx.lineWidth = 1; + ctx.strokeRect(0, 0, canvas.width, canvas.height); + } +} diff --git a/src/utils/canvas-scan/types.ts b/src/utils/canvas-scan/types.ts new file mode 100644 index 00000000..cdf97f64 --- /dev/null +++ b/src/utils/canvas-scan/types.ts @@ -0,0 +1,23 @@ +export const colorspaces = ["gray", "sRGB"] as const; + +export interface ScanConfig { + rotate: number; + rotate_var: number; + colorspace: typeof colorspaces[number]; + blur: number; + noise: number; + border: boolean; + scale: number; + output_format: "image/png" | "image/jpeg"; +} + +export const defaultConfig: ScanConfig = { + rotate: 1, + rotate_var: 0.5, + colorspace: "gray", + blur: 0.5, + noise: 0.25, + border: false, + scale: 2, + output_format: "image/jpeg", +}; From 6af1580f73135ed66d0314e4abbac0171f80a194 Mon Sep 17 00:00:00 2001 From: Seedgou Date: Mon, 13 Nov 2023 18:44:42 +0800 Subject: [PATCH 02/32] add canvas scan --- src/router/index.ts | 5 +++ src/views/CanvasScanView.vue | 84 ++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 src/views/CanvasScanView.vue diff --git a/src/router/index.ts b/src/router/index.ts index cb03cf86..a1d05ef9 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -14,6 +14,11 @@ const router = createRouter({ name: "scan", component: () => import("@/views/ScanView.vue"), }, + { + path: "/canvas-scan", + name: "canvas-scan", + component: () => import("@/views/CanvasScanView.vue"), + }, // catch all redirect to / { path: "/:pathMatch(.*)*", diff --git a/src/views/CanvasScanView.vue b/src/views/CanvasScanView.vue new file mode 100644 index 00000000..ab77fbbc --- /dev/null +++ b/src/views/CanvasScanView.vue @@ -0,0 +1,84 @@ + + + From bbf812fb3f7e03c6f95eae84f94fe820a3bb933e Mon Sep 17 00:00:00 2001 From: Seedgou Date: Mon, 13 Nov 2023 19:03:41 +0800 Subject: [PATCH 03/32] canvas scan settings --- .../canvas-scan-settings/ScanSettingsCard.vue | 62 ++++++++++++++++++ .../settings/BlurSetting.vue | 38 +++++++++++ .../settings/BorderSetting.vue | 40 ++++++++++++ .../settings/ColorspaceSetting.vue | 65 +++++++++++++++++++ .../settings/NoiseSetting.vue | 60 +++++++++++++++++ .../settings/RotateSetting.vue | 44 +++++++++++++ .../settings/RotateVarianceSetting.vue | 62 ++++++++++++++++++ .../settings/ScaleSetting.vue | 38 +++++++++++ .../settings/dark-noise.svg | 1 + .../canvas-scan-settings/settings/noise.svg | 1 + 10 files changed, 411 insertions(+) create mode 100644 src/components/canvas-scan/canvas-scan-settings/ScanSettingsCard.vue create mode 100644 src/components/canvas-scan/canvas-scan-settings/settings/BlurSetting.vue create mode 100644 src/components/canvas-scan/canvas-scan-settings/settings/BorderSetting.vue create mode 100644 src/components/canvas-scan/canvas-scan-settings/settings/ColorspaceSetting.vue create mode 100644 src/components/canvas-scan/canvas-scan-settings/settings/NoiseSetting.vue create mode 100644 src/components/canvas-scan/canvas-scan-settings/settings/RotateSetting.vue create mode 100644 src/components/canvas-scan/canvas-scan-settings/settings/RotateVarianceSetting.vue create mode 100644 src/components/canvas-scan/canvas-scan-settings/settings/ScaleSetting.vue create mode 100644 src/components/canvas-scan/canvas-scan-settings/settings/dark-noise.svg create mode 100644 src/components/canvas-scan/canvas-scan-settings/settings/noise.svg diff --git a/src/components/canvas-scan/canvas-scan-settings/ScanSettingsCard.vue b/src/components/canvas-scan/canvas-scan-settings/ScanSettingsCard.vue new file mode 100644 index 00000000..76cef796 --- /dev/null +++ b/src/components/canvas-scan/canvas-scan-settings/ScanSettingsCard.vue @@ -0,0 +1,62 @@ + + + diff --git a/src/components/canvas-scan/canvas-scan-settings/settings/BlurSetting.vue b/src/components/canvas-scan/canvas-scan-settings/settings/BlurSetting.vue new file mode 100644 index 00000000..6103635f --- /dev/null +++ b/src/components/canvas-scan/canvas-scan-settings/settings/BlurSetting.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/components/canvas-scan/canvas-scan-settings/settings/BorderSetting.vue b/src/components/canvas-scan/canvas-scan-settings/settings/BorderSetting.vue new file mode 100644 index 00000000..8e999b36 --- /dev/null +++ b/src/components/canvas-scan/canvas-scan-settings/settings/BorderSetting.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/components/canvas-scan/canvas-scan-settings/settings/ColorspaceSetting.vue b/src/components/canvas-scan/canvas-scan-settings/settings/ColorspaceSetting.vue new file mode 100644 index 00000000..5dfe4db6 --- /dev/null +++ b/src/components/canvas-scan/canvas-scan-settings/settings/ColorspaceSetting.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/components/canvas-scan/canvas-scan-settings/settings/NoiseSetting.vue b/src/components/canvas-scan/canvas-scan-settings/settings/NoiseSetting.vue new file mode 100644 index 00000000..b72f025c --- /dev/null +++ b/src/components/canvas-scan/canvas-scan-settings/settings/NoiseSetting.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/src/components/canvas-scan/canvas-scan-settings/settings/RotateSetting.vue b/src/components/canvas-scan/canvas-scan-settings/settings/RotateSetting.vue new file mode 100644 index 00000000..3c40e5ed --- /dev/null +++ b/src/components/canvas-scan/canvas-scan-settings/settings/RotateSetting.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/components/canvas-scan/canvas-scan-settings/settings/RotateVarianceSetting.vue b/src/components/canvas-scan/canvas-scan-settings/settings/RotateVarianceSetting.vue new file mode 100644 index 00000000..4fa548ca --- /dev/null +++ b/src/components/canvas-scan/canvas-scan-settings/settings/RotateVarianceSetting.vue @@ -0,0 +1,62 @@ + + + diff --git a/src/components/canvas-scan/canvas-scan-settings/settings/ScaleSetting.vue b/src/components/canvas-scan/canvas-scan-settings/settings/ScaleSetting.vue new file mode 100644 index 00000000..c2a9cfbf --- /dev/null +++ b/src/components/canvas-scan/canvas-scan-settings/settings/ScaleSetting.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/components/canvas-scan/canvas-scan-settings/settings/dark-noise.svg b/src/components/canvas-scan/canvas-scan-settings/settings/dark-noise.svg new file mode 100644 index 00000000..1336662b --- /dev/null +++ b/src/components/canvas-scan/canvas-scan-settings/settings/dark-noise.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/canvas-scan/canvas-scan-settings/settings/noise.svg b/src/components/canvas-scan/canvas-scan-settings/settings/noise.svg new file mode 100644 index 00000000..b27e76c1 --- /dev/null +++ b/src/components/canvas-scan/canvas-scan-settings/settings/noise.svg @@ -0,0 +1 @@ + \ No newline at end of file From 4ddde059d18e3cfc96b6c5d56c679da6fa7ccadd Mon Sep 17 00:00:00 2001 From: Seedgou Date: Mon, 13 Nov 2023 19:14:19 +0800 Subject: [PATCH 04/32] add PDF upload --- src/components/pdf-upload/PDFUpload.vue | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/components/pdf-upload/PDFUpload.vue diff --git a/src/components/pdf-upload/PDFUpload.vue b/src/components/pdf-upload/PDFUpload.vue new file mode 100644 index 00000000..a25871fc --- /dev/null +++ b/src/components/pdf-upload/PDFUpload.vue @@ -0,0 +1,52 @@ + + + From 323a14c5bc16960485182010b380386fd0c1109f Mon Sep 17 00:00:00 2001 From: Seedgou Date: Mon, 13 Nov 2023 19:52:57 +0800 Subject: [PATCH 05/32] add pdf preview canvas --- .../preview/pdf-preview/PDFPreviewCanvas.vue | 55 +++++++++++++++++++ src/utils/pdf/draw-on-canvas.ts | 32 +++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/components/canvas-scan/preview/pdf-preview/PDFPreviewCanvas.vue create mode 100644 src/utils/pdf/draw-on-canvas.ts diff --git a/src/components/canvas-scan/preview/pdf-preview/PDFPreviewCanvas.vue b/src/components/canvas-scan/preview/pdf-preview/PDFPreviewCanvas.vue new file mode 100644 index 00000000..d79bfd68 --- /dev/null +++ b/src/components/canvas-scan/preview/pdf-preview/PDFPreviewCanvas.vue @@ -0,0 +1,55 @@ + + + diff --git a/src/utils/pdf/draw-on-canvas.ts b/src/utils/pdf/draw-on-canvas.ts new file mode 100644 index 00000000..711fa9da --- /dev/null +++ b/src/utils/pdf/draw-on-canvas.ts @@ -0,0 +1,32 @@ +import type { PDFDocumentProxy } from "pdfjs-dist/types/src/pdf"; + +export async function drawOnCanvas( + canvas: HTMLCanvasElement, + pdfDocument: PDFDocumentProxy, + page: number, + scale: number, + options?: { + signal?: AbortSignal; + } +): Promise { + console.log(pdfDocument); + const pdfPage = await pdfDocument.getPage(page); + + if (options?.signal?.aborted) { + throw new DOMException("Aborted", "AbortError"); + } + + const viewport = pdfPage.getViewport({ scale }); + const width = viewport.width; + const height = viewport.height; + + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + + const renderTask = pdfPage.render({ + canvasContext: ctx as object, + viewport, + }); + await renderTask.promise; +} From 8cf4c00c2d8152c0b03f3c341c310df75e5d28e1 Mon Sep 17 00:00:00 2001 From: Seedgou Date: Mon, 13 Nov 2023 20:00:52 +0800 Subject: [PATCH 06/32] add side-by-side preview --- .../canvas-scan/preview/SideBySidePreview.vue | 22 +++++++++++++++++++ .../preview/pdf-preview/PDFPreviewCanvas.vue | 9 +++++++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/components/canvas-scan/preview/SideBySidePreview.vue diff --git a/src/components/canvas-scan/preview/SideBySidePreview.vue b/src/components/canvas-scan/preview/SideBySidePreview.vue new file mode 100644 index 00000000..864675e9 --- /dev/null +++ b/src/components/canvas-scan/preview/SideBySidePreview.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/components/canvas-scan/preview/pdf-preview/PDFPreviewCanvas.vue b/src/components/canvas-scan/preview/pdf-preview/PDFPreviewCanvas.vue index d79bfd68..352045b4 100644 --- a/src/components/canvas-scan/preview/pdf-preview/PDFPreviewCanvas.vue +++ b/src/components/canvas-scan/preview/pdf-preview/PDFPreviewCanvas.vue @@ -1,5 +1,5 @@ + + From c77d972a6e0a1784a61fb5a90b9ed9443aeb5ba0 Mon Sep 17 00:00:00 2001 From: Seedgou Date: Mon, 13 Nov 2023 20:32:44 +0800 Subject: [PATCH 07/32] ImagePreview --- .../canvas-scan/preview/ImagePreview.vue | 25 ++++ .../preview/pdf-preview/PDFPreviewCanvas.vue | 62 --------- src/utils/pdf-new/PDF.ts | 118 ++++++++++++++++++ src/utils/pdf-new/getDocument.ts | 5 + src/utils/pdf-new/index.ts | 2 + src/utils/pdf-new/standardFontDataFactory.ts | 38 ++++++ 6 files changed, 188 insertions(+), 62 deletions(-) create mode 100644 src/components/canvas-scan/preview/ImagePreview.vue delete mode 100644 src/components/canvas-scan/preview/pdf-preview/PDFPreviewCanvas.vue create mode 100644 src/utils/pdf-new/PDF.ts create mode 100644 src/utils/pdf-new/getDocument.ts create mode 100644 src/utils/pdf-new/index.ts create mode 100644 src/utils/pdf-new/standardFontDataFactory.ts diff --git a/src/components/canvas-scan/preview/ImagePreview.vue b/src/components/canvas-scan/preview/ImagePreview.vue new file mode 100644 index 00000000..c39b5513 --- /dev/null +++ b/src/components/canvas-scan/preview/ImagePreview.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/src/components/canvas-scan/preview/pdf-preview/PDFPreviewCanvas.vue b/src/components/canvas-scan/preview/pdf-preview/PDFPreviewCanvas.vue deleted file mode 100644 index 352045b4..00000000 --- a/src/components/canvas-scan/preview/pdf-preview/PDFPreviewCanvas.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - - - diff --git a/src/utils/pdf-new/PDF.ts b/src/utils/pdf-new/PDF.ts new file mode 100644 index 00000000..fd7d6cf2 --- /dev/null +++ b/src/utils/pdf-new/PDF.ts @@ -0,0 +1,118 @@ +import type { PDFDocumentProxy } from "pdfjs-dist/types/src/pdf"; +import { standardFontDataFactory } from "./standardFontDataFactory"; + +export interface PDFPageInfo { + blob: Blob; + page: number; + height: number; + width: number; + scale: number; + dpi: number; +} + +export interface PDFInfoType { + source: string; + filename: string; +} + +export interface PDFRenderer { + renderPage(page: number, scale: number): Promise; + getNumPages(): Promise; +} + +export class PDF implements PDFRenderer { + private readonly pdf: File; + + private pdfDocument?: PDFDocumentProxy; + private readonly initPromise: Promise; + private readonly pagePromises: Map> = new Map(); + + constructor(pdf: File) { + this.pdf = pdf; + this.initPromise = this.init(); + } + + async init() { + const { getDocument } = await import("./getDocument"); + const url = URL.createObjectURL(this.pdf); + const pdfDocument = await getDocument({ + url: url, + StandardFontDataFactory: standardFontDataFactory, + }).promise; + this.pdfDocument = pdfDocument; + } + + private async getDocument(): Promise { + await this.initPromise; + if (!this.pdfDocument) { + throw new Error("PDF document is not initialized"); + } + return this.pdfDocument; + } + + async getNumPages(): Promise { + await this.initPromise; + return this.pdfDocument.numPages; + } + + async renderPage(page: number, scale: number): Promise { + const promise = this.pagePromises.get(`${page}-${scale}`); + if (promise) { + return await promise; + } + + const pageInfoPromise = this.renderPageRaw(page, scale); + this.pagePromises.set(`${page}-${scale}`, pageInfoPromise); + + return await pageInfoPromise; + } + + private async renderPageRaw( + page: number, + scale: number + ): Promise { + await this.initPromise; + + const dpi = scale * 72; + const pdfDocument = await this.getDocument(); + const pdfPage = await pdfDocument.getPage(page); + const viewport = pdfPage.getViewport({ scale }); + const width = viewport.width; + const height = viewport.height; + + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + const renderTask = pdfPage.render({ + canvasContext: ctx as object, + viewport, + }); + await renderTask.promise; + + const blob: Blob = await new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + } else { + reject(new Error("Canvas to Blob failed")); + } + }); + }); + + canvas.remove(); + + const pageInfo = { + blob, + page, + height, + width, + scale, + dpi, + }; + + pdfPage.cleanup(); + + return pageInfo; + } +} diff --git a/src/utils/pdf-new/getDocument.ts b/src/utils/pdf-new/getDocument.ts new file mode 100644 index 00000000..b020d26f --- /dev/null +++ b/src/utils/pdf-new/getDocument.ts @@ -0,0 +1,5 @@ +import pdfJsWorkerURL from "pdfjs-dist/build/pdf.worker.min.js?url"; +import { GlobalWorkerOptions, getDocument } from "pdfjs-dist"; +GlobalWorkerOptions.workerSrc = pdfJsWorkerURL; + +export { getDocument }; diff --git a/src/utils/pdf-new/index.ts b/src/utils/pdf-new/index.ts new file mode 100644 index 00000000..36f41de7 --- /dev/null +++ b/src/utils/pdf-new/index.ts @@ -0,0 +1,2 @@ +export { PDF } from "./PDF"; +export type { PDFInfoType } from "./PDF"; diff --git a/src/utils/pdf-new/standardFontDataFactory.ts b/src/utils/pdf-new/standardFontDataFactory.ts new file mode 100644 index 00000000..7ca3423e --- /dev/null +++ b/src/utils/pdf-new/standardFontDataFactory.ts @@ -0,0 +1,38 @@ +export interface StandardFontDataFactoryType { + fetch: (req: { filename: string }) => Promise; +} + +export class standardFontDataFactory { + async fetch(req: { filename: string }): Promise { + const { filename } = req; + const url = getFontURL(filename); + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch font: ${url}`); + } + + return new Uint8Array(await response.arrayBuffer()); + } +} + +function getFontURL(filename: string) { + if (filename.endsWith(".pfb")) { + const base = filename.slice(0, -4); + + return new URL( + `../../../node_modules/pdfjs-dist/standard_fonts/${base}.pfb`, + import.meta.url + ).href; + } + + if (filename.endsWith(".ttf")) { + const base = filename.slice(0, -4); + + return new URL( + `../../../node_modules/pdfjs-dist/standard_fonts/${base}.ttf`, + import.meta.url + ).href; + } + + throw new Error(`Unknown font filename: ${filename}`); +} From 1b54afe1ea7ecccfe8f54ac9b386d10eccbd7fa4 Mon Sep 17 00:00:00 2001 From: Seedgou Date: Mon, 13 Nov 2023 20:45:26 +0800 Subject: [PATCH 08/32] add preview compare --- .../canvas-scan/preview/PreviewCompare.vue | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/components/canvas-scan/preview/PreviewCompare.vue diff --git a/src/components/canvas-scan/preview/PreviewCompare.vue b/src/components/canvas-scan/preview/PreviewCompare.vue new file mode 100644 index 00000000..b12f92ed --- /dev/null +++ b/src/components/canvas-scan/preview/PreviewCompare.vue @@ -0,0 +1,52 @@ + + + From 7ca5d2759a35679f9ee7afe194209095fcc1fd40 Mon Sep 17 00:00:00 2001 From: Seedgou Date: Mon, 13 Nov 2023 21:00:28 +0800 Subject: [PATCH 09/32] add canvas scanner --- .../canvas-scan/preview/PreviewCompare.vue | 25 +++++++ src/utils/canvas-scan/index.ts | 1 + src/utils/canvas-scan/scanner.ts | 40 +++++++++++ src/views/CanvasScanView.vue | 70 ++++++++----------- 4 files changed, 94 insertions(+), 42 deletions(-) create mode 100644 src/utils/canvas-scan/scanner.ts diff --git a/src/components/canvas-scan/preview/PreviewCompare.vue b/src/components/canvas-scan/preview/PreviewCompare.vue index b12f92ed..df59cf75 100644 --- a/src/components/canvas-scan/preview/PreviewCompare.vue +++ b/src/components/canvas-scan/preview/PreviewCompare.vue @@ -3,6 +3,9 @@ + @@ -26,8 +29,17 @@ interface PDFRenderer { getNumPages(): Promise; } +interface ScanRenderer { + renderPage(image: Blob): Promise<{ + blob: Blob; + height: number; + width: number; + }>; +} + const props = defineProps<{ pdfRenderer?: PDFRenderer; + scanRenderer?: ScanRenderer; scale: number; }>(); @@ -49,4 +61,17 @@ const image = computedAsync(async () => { width, }; }); + +const scanImage = computedAsync(async () => { + if (!props.scanRenderer || !image.value.blob) return; + + const { blob, height, width } = await props.scanRenderer.renderPage( + image.value.blob + ); + return { + blob, + height, + width, + }; +}); diff --git a/src/utils/canvas-scan/index.ts b/src/utils/canvas-scan/index.ts index 11de0943..7c7c544f 100644 --- a/src/utils/canvas-scan/index.ts +++ b/src/utils/canvas-scan/index.ts @@ -1,2 +1,3 @@ export { type ScanConfig, defaultConfig, colorspaces } from "./types"; export { scanCanvas } from "./scan"; +export { CanvasScanner } from "./scanner"; diff --git a/src/utils/canvas-scan/scanner.ts b/src/utils/canvas-scan/scanner.ts new file mode 100644 index 00000000..e758ad8b --- /dev/null +++ b/src/utils/canvas-scan/scanner.ts @@ -0,0 +1,40 @@ +import { scanCanvas } from "./scan"; +import type { ScanConfig } from "./types"; + +interface ScanRenderer { + renderPage(image: Blob): Promise<{ + blob: Blob; + height: number; + width: number; + }>; +} + +export class CanvasScanner implements ScanRenderer { + config: ScanConfig; + + constructor(config: ScanConfig) { + this.config = config; + } + + async renderPage(image: Blob): Promise<{ + blob: Blob; + height: number; + width: number; + }> { + const canvas = document.createElement("canvas"); + await scanCanvas(canvas, image, this.config); + const blob = await new Promise((resolve, reject) => + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + } else { + reject(new Error("Canvas to Blob failed")); + } + }, this.config.output_format) + ); + const height = canvas.height; + const width = canvas.width; + canvas.remove(); + return { blob, height, width }; + } +} diff --git a/src/views/CanvasScanView.vue b/src/views/CanvasScanView.vue index ab77fbbc..def681a6 100644 --- a/src/views/CanvasScanView.vue +++ b/src/views/CanvasScanView.vue @@ -11,20 +11,17 @@ responsive="screen" > - + + + + + - @@ -32,20 +29,21 @@ From f7ec99d48164c71da29cc42f493887dd88d80cd2 Mon Sep 17 00:00:00 2001 From: Seedgou Date: Mon, 13 Nov 2023 21:04:15 +0800 Subject: [PATCH 10/32] deep watch scanRenderer --- src/views/CanvasScanView.vue | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/views/CanvasScanView.vue b/src/views/CanvasScanView.vue index def681a6..444859bb 100644 --- a/src/views/CanvasScanView.vue +++ b/src/views/CanvasScanView.vue @@ -34,9 +34,8 @@ import MainContainer from "@/components/MainContainer.vue"; import { type ScanConfig, defaultConfig } from "@/utils/canvas-scan"; import ScanSettingsCard from "@/components/canvas-scan/canvas-scan-settings/ScanSettingsCard.vue"; import PDFUpload from "@/components/pdf-upload/PDFUpload.vue"; -import { ref, computed, onMounted } from "vue"; +import { ref, computed, onMounted, watch } from "vue"; import PDFURL from "@/assets/examples/pdfs/test.pdf"; -import type { PDFInfoType } from "@/utils/pdf"; import BackToIndex from "@/components/buttons/BackToIndex.vue"; import { useHead } from "@vueuse/head"; import { useI18n } from "vue-i18n"; @@ -66,5 +65,12 @@ const pdfRenderer = computed(() => { return new PDF(pdf.value); }); -const scanRenderer = computed(() => new CanvasScanner(config.value)); +const scanRenderer = ref(new CanvasScanner(config.value)); +watch( + config, + (newConfig) => { + scanRenderer.value = new CanvasScanner(newConfig); + }, + { deep: true } +); From 66904587663447b894aac9edb6b4820e4d5505b2 Mon Sep 17 00:00:00 2001 From: Seedgou Date: Mon, 13 Nov 2023 21:09:24 +0800 Subject: [PATCH 11/32] add abort signal to scanner --- .../canvas-scan/preview/PreviewCompare.vue | 16 ++++++++++++-- src/utils/canvas-scan/scan.ts | 14 +++++++++++- src/utils/canvas-scan/scanner.ts | 22 +++++++++++++++++-- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/components/canvas-scan/preview/PreviewCompare.vue b/src/components/canvas-scan/preview/PreviewCompare.vue index df59cf75..71f36de2 100644 --- a/src/components/canvas-scan/preview/PreviewCompare.vue +++ b/src/components/canvas-scan/preview/PreviewCompare.vue @@ -30,7 +30,12 @@ interface PDFRenderer { } interface ScanRenderer { - renderPage(image: Blob): Promise<{ + renderPage( + image: Blob, + options?: { + signal?: AbortSignal; + } + ): Promise<{ blob: Blob; height: number; width: number; @@ -62,11 +67,18 @@ const image = computedAsync(async () => { }; }); +let controller = new AbortController(); + const scanImage = computedAsync(async () => { + controller.abort(); + controller = new AbortController(); if (!props.scanRenderer || !image.value.blob) return; const { blob, height, width } = await props.scanRenderer.renderPage( - image.value.blob + image.value.blob, + { + signal: controller.signal, + } ); return { blob, diff --git a/src/utils/canvas-scan/scan.ts b/src/utils/canvas-scan/scan.ts index d726b66f..5639125a 100644 --- a/src/utils/canvas-scan/scan.ts +++ b/src/utils/canvas-scan/scan.ts @@ -4,8 +4,13 @@ import noiseSVG from "./noise.svg?url"; export async function scanCanvas( canvas: HTMLCanvasElement, page: Blob, - config: ScanConfig + config: ScanConfig, + signal?: AbortSignal ): Promise { + if (signal?.aborted) { + throw new Error("Aborted"); + } + const ctx = canvas.getContext("2d"); if (!ctx) { throw new Error("Canvas not supported"); @@ -18,6 +23,10 @@ export async function scanCanvas( img.onload = resolve; img.onerror = reject; }); + if (signal?.aborted) { + throw new Error("Aborted"); + } + canvas.width = img.width; canvas.height = img.height; @@ -43,6 +52,9 @@ export async function scanCanvas( const noiseImg = new Image(); noiseImg.src = noiseSVG; await new Promise((resolve) => (noiseImg.onload = resolve)); + if (signal?.aborted) { + throw new Error("Aborted"); + } // add noise ctx.drawImage( diff --git a/src/utils/canvas-scan/scanner.ts b/src/utils/canvas-scan/scanner.ts index e758ad8b..4d021a7e 100644 --- a/src/utils/canvas-scan/scanner.ts +++ b/src/utils/canvas-scan/scanner.ts @@ -2,7 +2,12 @@ import { scanCanvas } from "./scan"; import type { ScanConfig } from "./types"; interface ScanRenderer { - renderPage(image: Blob): Promise<{ + renderPage( + image: Blob, + options?: { + signal?: AbortSignal; + } + ): Promise<{ blob: Blob; height: number; width: number; @@ -16,13 +21,26 @@ export class CanvasScanner implements ScanRenderer { this.config = config; } - async renderPage(image: Blob): Promise<{ + async renderPage( + image: Blob, + options?: { + signal?: AbortSignal; + } + ): Promise<{ blob: Blob; height: number; width: number; }> { + if (options?.signal?.aborted) { + throw new Error("Aborted"); + } + const canvas = document.createElement("canvas"); await scanCanvas(canvas, image, this.config); + if (options?.signal?.aborted) { + throw new Error("Aborted"); + } + const blob = await new Promise((resolve, reject) => canvas.toBlob((blob) => { if (blob) { From cbd3e639e3117b07f3f7db5751cbedb264db2c6c Mon Sep 17 00:00:00 2001 From: Seedgou Date: Mon, 13 Nov 2023 21:16:15 +0800 Subject: [PATCH 12/32] add pagination --- .../canvas-scan/preview/PreviewCompare.vue | 31 ++++++++++++++----- .../canvas-scan/preview/PreviewPagination.vue | 27 ++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 src/components/canvas-scan/preview/PreviewPagination.vue diff --git a/src/components/canvas-scan/preview/PreviewCompare.vue b/src/components/canvas-scan/preview/PreviewCompare.vue index 71f36de2..2d6a7ba8 100644 --- a/src/components/canvas-scan/preview/PreviewCompare.vue +++ b/src/components/canvas-scan/preview/PreviewCompare.vue @@ -1,12 +1,19 @@ diff --git a/src/components/canvas-scan/preview/PreviewPagination.vue b/src/components/canvas-scan/preview/PreviewPagination.vue new file mode 100644 index 00000000..be12c062 --- /dev/null +++ b/src/components/canvas-scan/preview/PreviewPagination.vue @@ -0,0 +1,27 @@ + + + From 32777be747669659217e45ee181135075dcadc4d Mon Sep 17 00:00:00 2001 From: Seedgou Date: Tue, 14 Nov 2023 10:37:28 +0800 Subject: [PATCH 13/32] add noise i18n --- src/locale/en/settings.ts | 1 + src/locale/zh-CN/settings.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/locale/en/settings.ts b/src/locale/en/settings.ts index d70d6d2a..221727a9 100644 --- a/src/locale/en/settings.ts +++ b/src/locale/en/settings.ts @@ -1,6 +1,7 @@ export const settings = { settings: "Customization", attenuate: "Noise", + noise: "Noise", blur: "Blur", border: { label: "Border", diff --git a/src/locale/zh-CN/settings.ts b/src/locale/zh-CN/settings.ts index 5eb27664..a598c2a4 100644 --- a/src/locale/zh-CN/settings.ts +++ b/src/locale/zh-CN/settings.ts @@ -1,6 +1,7 @@ export const settings = { settings: "扫描设置", attenuate: "噪点", + noise: "噪点", blur: "模糊", border: { label: "边框", From e77ad805674c51a0a657c4f3c52f6c322f608277 Mon Sep 17 00:00:00 2001 From: Seedgou Date: Tue, 14 Nov 2023 10:45:55 +0800 Subject: [PATCH 14/32] add scan actions card --- .../scan-actions/ScanActionsCard.vue | 26 +++++++++++++++++++ src/views/CanvasScanView.vue | 3 +++ 2 files changed, 29 insertions(+) create mode 100644 src/components/scan-actions/ScanActionsCard.vue diff --git a/src/components/scan-actions/ScanActionsCard.vue b/src/components/scan-actions/ScanActionsCard.vue new file mode 100644 index 00000000..8ffde471 --- /dev/null +++ b/src/components/scan-actions/ScanActionsCard.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/views/CanvasScanView.vue b/src/views/CanvasScanView.vue index 444859bb..c30baa56 100644 --- a/src/views/CanvasScanView.vue +++ b/src/views/CanvasScanView.vue @@ -15,6 +15,8 @@ + + @@ -42,6 +44,7 @@ import { useI18n } from "vue-i18n"; import { PDF } from "@/utils/pdf-new"; import PreviewCompare from "@/components/canvas-scan/preview/PreviewCompare.vue"; import { CanvasScanner } from "@/utils/canvas-scan"; +import ScanActionsCard from "@/components/scan-actions/ScanActionsCard.vue"; const { t } = useI18n(); From 4f28b235ce5a9b7752a88d85cb8dd0f57d34d946 Mon Sep 17 00:00:00 2001 From: Seedgou Date: Tue, 14 Nov 2023 13:27:36 +0800 Subject: [PATCH 15/32] add download Save --- src/composables/save-scanned-pdf/index.ts | 65 +++++++++++++++++++++++ src/views/CanvasScanView.vue | 16 +++++- 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/composables/save-scanned-pdf/index.ts diff --git a/src/composables/save-scanned-pdf/index.ts b/src/composables/save-scanned-pdf/index.ts new file mode 100644 index 00000000..151379a3 --- /dev/null +++ b/src/composables/save-scanned-pdf/index.ts @@ -0,0 +1,65 @@ +import type { Ref } from "vue"; +import { get } from "@vueuse/core"; + +interface PDFRenderer { + renderPage( + page: number, + scale: number + ): Promise<{ + blob: Blob; + }>; + getNumPages(): Promise; +} + +interface ScanRenderer { + renderPage(image: Blob): Promise<{ + blob: Blob; + height: number; + width: number; + }>; +} + +export function useSaveScannedPDF( + pdfRenderer: PDFRenderer | undefined | Ref, + scanRenderer: ScanRenderer | undefined | Ref, + scale: Ref | number +) { + const save = async () => { + const pdf = get(pdfRenderer); + const scan = get(scanRenderer); + const scale_ = get(scale); + + if (!pdf || !scan) { + throw new Error("No PDF or Scan Renderer"); + } + + const numPages = await pdf.getNumPages(); + console.log(`Number of pages: ${numPages} at scale ${scale_}x`); + + // generate pdf pages 1...n + const pages = Array.from({ length: numPages }, (_, i) => i + 1); + + const pdfPages = await Promise.all( + pages.map(async (page) => { + const { blob } = await pdf.renderPage(page, scale_); + return blob; + }) + ); + + // generate scan pages + const scanPages = await Promise.all( + pdfPages.map(async (pdfPage) => { + const info = await scan.renderPage(pdfPage); + return { ...info, dpi: scale_ * 72 }; + }) + ); + + // generate pdf from scan pages + const { imagesToPDF } = await import("@/utils/images-to-pdf"); + const pdfDocument = await imagesToPDF(scanPages); + + return pdfDocument; + }; + + return { save }; +} diff --git a/src/views/CanvasScanView.vue b/src/views/CanvasScanView.vue index c30baa56..a2258e84 100644 --- a/src/views/CanvasScanView.vue +++ b/src/views/CanvasScanView.vue @@ -16,7 +16,7 @@ - + @@ -45,6 +45,7 @@ import { PDF } from "@/utils/pdf-new"; import PreviewCompare from "@/components/canvas-scan/preview/PreviewCompare.vue"; import { CanvasScanner } from "@/utils/canvas-scan"; import ScanActionsCard from "@/components/scan-actions/ScanActionsCard.vue"; +import { useSaveScannedPDF } from "@/composables/save-scanned-pdf"; const { t } = useI18n(); @@ -76,4 +77,17 @@ watch( }, { deep: true } ); + +const scale = computed(() => config.value.scale); + +const { save } = useSaveScannedPDF(pdfRenderer, scanRenderer, scale); +const downloadSave = async () => { + const pdfBlob = await save(); + const url = URL.createObjectURL(pdfBlob); + const link = document.createElement("a"); + link.href = url; + link.download = "scanned.pdf"; + link.click(); + URL.revokeObjectURL(url); +}; From a7aa967846d45b17848d77f1987d4402dbf423d7 Mon Sep 17 00:00:00 2001 From: Seedgou Date: Tue, 14 Nov 2023 13:45:45 +0800 Subject: [PATCH 16/32] add progress --- .../SaveButtonCard.vue} | 13 +++- src/composables/save-scanned-pdf/index.ts | 76 ++++++++++++------- src/views/CanvasScanView.vue | 14 +++- 3 files changed, 68 insertions(+), 35 deletions(-) rename src/components/{scan-actions/ScanActionsCard.vue => save-button/SaveButtonCard.vue} (66%) diff --git a/src/components/scan-actions/ScanActionsCard.vue b/src/components/save-button/SaveButtonCard.vue similarity index 66% rename from src/components/scan-actions/ScanActionsCard.vue rename to src/components/save-button/SaveButtonCard.vue index 8ffde471..fad596f8 100644 --- a/src/components/scan-actions/ScanActionsCard.vue +++ b/src/components/save-button/SaveButtonCard.vue @@ -1,13 +1,13 @@