mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-04 02:20:19 +01:00
Feature/adjust colors contrast tool (#4544)
# Description of Changes - Addition of the "Adjust Colors/Contrast" tool --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolType, useToolOperation } from '../shared/useToolOperation';
|
||||
import { AdjustContrastParameters, defaultParameters } from './useAdjustContrastParameters';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { applyAdjustmentsToCanvas } from '../../../components/tools/adjustContrast/utils';
|
||||
import { pdfWorkerManager } from '../../../services/pdfWorkerManager';
|
||||
import { createFileFromApiResponse } from '../../../utils/fileResponseUtils';
|
||||
|
||||
async function renderPdfPageToCanvas(pdf: any, pageNumber: number, scale: number): Promise<HTMLCanvasElement> {
|
||||
const page = await pdf.getPage(pageNumber);
|
||||
const viewport = page.getViewport({ scale });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Canvas 2D context unavailable');
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// adjustment logic moved to shared util
|
||||
|
||||
// Render, adjust, and assemble all pages of a single PDF into a new PDF
|
||||
async function buildAdjustedPdfForFile(file: File, params: AdjustContrastParameters): Promise<File> {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdf = await pdfWorkerManager.createDocument(arrayBuffer, {});
|
||||
const pageCount = pdf.numPages;
|
||||
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
|
||||
for (let p = 1; p <= pageCount; p++) {
|
||||
const srcCanvas = await renderPdfPageToCanvas(pdf, p, 2);
|
||||
const adjusted = applyAdjustmentsToCanvas(srcCanvas, params);
|
||||
const pngUrl = adjusted.toDataURL('image/png');
|
||||
const res = await fetch(pngUrl);
|
||||
const pngBytes = new Uint8Array(await res.arrayBuffer());
|
||||
const embedded = await newDoc.embedPng(pngBytes);
|
||||
const { width, height } = embedded.scale(1);
|
||||
const page = newDoc.addPage([width, height]);
|
||||
page.drawImage(embedded, { x: 0, y: 0, width, height });
|
||||
}
|
||||
|
||||
const pdfBytes = await newDoc.save();
|
||||
const out = createFileFromApiResponse(pdfBytes, { 'content-type': 'application/pdf' }, file.name);
|
||||
pdfWorkerManager.destroyDocument(pdf);
|
||||
return out;
|
||||
}
|
||||
|
||||
async function processPdfClientSide(params: AdjustContrastParameters, files: File[]): Promise<File[]> {
|
||||
// Limit concurrency to avoid exhausting memory/CPU while still getting speedups
|
||||
// Heuristic: use up to 4 workers on capable machines, otherwise 2-3
|
||||
let CONCURRENCY_LIMIT = 2;
|
||||
if (typeof navigator !== 'undefined' && typeof navigator.hardwareConcurrency === 'number') {
|
||||
if (navigator.hardwareConcurrency >= 8) CONCURRENCY_LIMIT = 4;
|
||||
else if (navigator.hardwareConcurrency >= 4) CONCURRENCY_LIMIT = 3;
|
||||
}
|
||||
CONCURRENCY_LIMIT = Math.min(CONCURRENCY_LIMIT, files.length);
|
||||
|
||||
const mapWithConcurrency = async <T, R>(items: T[], limit: number, worker: (item: T, index: number) => Promise<R>): Promise<R[]> => {
|
||||
const results: R[] = new Array(items.length);
|
||||
let nextIndex = 0;
|
||||
|
||||
const workers = new Array(Math.min(limit, items.length)).fill(0).map(async () => {
|
||||
let current = nextIndex++;
|
||||
while (current < items.length) {
|
||||
results[current] = await worker(items[current], current);
|
||||
current = nextIndex++;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(workers);
|
||||
return results;
|
||||
};
|
||||
|
||||
return mapWithConcurrency(files, CONCURRENCY_LIMIT, (file) => buildAdjustedPdfForFile(file, params));
|
||||
}
|
||||
|
||||
export const adjustContrastOperationConfig = {
|
||||
toolType: ToolType.custom,
|
||||
customProcessor: processPdfClientSide,
|
||||
operationType: 'adjustContrast',
|
||||
defaultParameters,
|
||||
// Single-step settings component for Automate
|
||||
settingsComponentPath: 'components/tools/adjustContrast/AdjustContrastSingleStepSettings',
|
||||
} as const;
|
||||
|
||||
export const useAdjustContrastOperation = () => {
|
||||
const { t } = useTranslation();
|
||||
return useToolOperation<AdjustContrastParameters>({
|
||||
...adjustContrastOperationConfig,
|
||||
getErrorMessage: () => t('adjustContrast.error.failed', 'Failed to adjust colors/contrast')
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||
|
||||
export interface AdjustContrastParameters {
|
||||
contrast: number; // 0-200 (%), 100 = neutral
|
||||
brightness: number; // 0-200 (%), 100 = neutral
|
||||
saturation: number; // 0-200 (%), 100 = neutral
|
||||
red: number; // 0-200 (%), 100 = neutral
|
||||
green: number; // 0-200 (%), 100 = neutral
|
||||
blue: number; // 0-200 (%), 100 = neutral
|
||||
}
|
||||
|
||||
export const defaultParameters: AdjustContrastParameters = {
|
||||
contrast: 100,
|
||||
brightness: 100,
|
||||
saturation: 100,
|
||||
red: 100,
|
||||
green: 100,
|
||||
blue: 100,
|
||||
};
|
||||
|
||||
export type AdjustContrastParametersHook = BaseParametersHook<AdjustContrastParameters>;
|
||||
|
||||
export const useAdjustContrastParameters = (): AdjustContrastParametersHook => {
|
||||
return useBaseParameters<AdjustContrastParameters>({
|
||||
defaultParameters,
|
||||
endpointName: '',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user