From d39a7ddda73c49c384cd2dfcb31def775e3b6562 Mon Sep 17 00:00:00 2001 From: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Date: Sat, 31 Jan 2026 20:07:45 +0000 Subject: [PATCH 1/6] Bug/pageeditor virtualisation (#5614) --- .../src/core/components/layout/Workbench.tsx | 13 +- .../components/pageEditor/DragDropGrid.tsx | 46 +++++- .../core/components/pageEditor/PageEditor.tsx | 141 +++++++++++++++++- .../components/pageEditor/PageThumbnail.tsx | 41 ----- .../pageEditor/commands/pageCommands.ts | 64 ++++---- .../core/components/pageEditor/constants.ts | 4 +- .../pageEditor/hooks/usePageDocument.ts | 96 +++++++++++- .../src/core/contexts/PageEditorContext.tsx | 43 +++++- .../src/core/hooks/useThumbnailGeneration.ts | 23 ++- .../services/enhancedPDFProcessingService.ts | 22 +-- frontend/src/core/services/fileAnalyzer.ts | 2 +- 11 files changed, 381 insertions(+), 114 deletions(-) diff --git a/frontend/src/core/components/layout/Workbench.tsx b/frontend/src/core/components/layout/Workbench.tsx index a0be189f9e..99d65e83f3 100644 --- a/frontend/src/core/components/layout/Workbench.tsx +++ b/frontend/src/core/components/layout/Workbench.tsx @@ -141,14 +141,15 @@ export default function Workbench() { ); case "pageEditor": - + return ( - <> +
{pageEditorFunctions && ( - + +
)} - + ); default: @@ -207,9 +209,10 @@ export default function Workbench() { {/* Main content area */} {renderMainContent()} diff --git a/frontend/src/core/components/pageEditor/DragDropGrid.tsx b/frontend/src/core/components/pageEditor/DragDropGrid.tsx index 32e76ec7ec..8f8598c292 100644 --- a/frontend/src/core/components/pageEditor/DragDropGrid.tsx +++ b/frontend/src/core/components/pageEditor/DragDropGrid.tsx @@ -37,6 +37,7 @@ interface DragDropGridProps { getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null; zoomLevel?: number; selectedFileIds?: string[]; + onVisibleItemsChange?: (items: T[]) => void; } type DropSide = 'left' | 'right' | null; @@ -198,7 +199,7 @@ interface DraggableItemProps { zoomLevel: number; } -const DraggableItem = ({ item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, activeDragIds, justMoved, getThumbnailData, renderItem, onUpdateDropTarget, zoomLevel }: DraggableItemProps) => { +const DraggableItemInner = ({ item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, activeDragIds, justMoved, getThumbnailData, renderItem, onUpdateDropTarget, zoomLevel }: DraggableItemProps) => { const isPlaceholder = Boolean(item.isPlaceholder); const pageNumber = (item as any).pageNumber ?? index + 1; const { attributes, listeners, setNodeRef: setDraggableRef } = useDraggable({ @@ -252,6 +253,31 @@ const DraggableItem = ({ item, index, itemRefs, boxSelec ); }; +// Memoize to prevent unnecessary re-renders and hook thrashing +const DraggableItem = React.memo(DraggableItemInner, (prevProps, nextProps) => { + // Return true to SKIP re-render (props are equal) + // Return false to RE-RENDER (props changed) + + // Check if item reference or content changed (including thumbnail) + const itemChanged = prevProps.item !== nextProps.item; + + // If item object reference changed, we need to re-render + if (itemChanged) { + return false; // Props changed, re-render needed + } + + // Item reference is same, check other props + return ( + prevProps.item.id === nextProps.item.id && + prevProps.index === nextProps.index && + prevProps.activeId === nextProps.activeId && + prevProps.justMoved === nextProps.justMoved && + prevProps.zoomLevel === nextProps.zoomLevel && + prevProps.activeDragIds.length === nextProps.activeDragIds.length && + prevProps.boxSelectedPageIds.length === nextProps.boxSelectedPageIds.length + ); +}) as typeof DraggableItemInner; + const DragDropGrid = ({ items, renderItem, @@ -259,6 +285,7 @@ const DragDropGrid = ({ getThumbnailData, zoomLevel = 1.0, selectedFileIds, + onVisibleItemsChange, }: DragDropGridProps) => { const itemRefs = useRef>(new Map()); const containerRef = useRef(null); @@ -421,6 +448,21 @@ const DragDropGrid = ({ overscan: OVERSCAN, }); + const virtualRows = rowVirtualizer.getVirtualItems(); + + useEffect(() => { + if (!onVisibleItemsChange) return; + + const visibleItemsForCallback: T[] = []; + virtualRows.forEach((row) => { + const startIndex = row.index * itemsPerRow; + const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length); + visibleItemsForCallback.push(...visibleItems.slice(startIndex, endIndex)); + }); + + onVisibleItemsChange(visibleItemsForCallback); + }, [virtualRows, visibleItems, itemsPerRow, onVisibleItemsChange]); + // Re-measure virtualizer when zoom or items per row changes useEffect(() => { rowVirtualizer.measure(); @@ -719,7 +761,7 @@ const DragDropGrid = ({ margin: '0 auto', }} > - {rowVirtualizer.getVirtualItems().map((virtualRow) => { + {virtualRows.map((virtualRow) => { const startIndex = virtualRow.index * itemsPerRow; const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length); const rowItems = visibleItems.slice(startIndex, endIndex); diff --git a/frontend/src/core/components/pageEditor/PageEditor.tsx b/frontend/src/core/components/pageEditor/PageEditor.tsx index b68cdac3ca..6c8f554ec8 100644 --- a/frontend/src/core/components/pageEditor/PageEditor.tsx +++ b/frontend/src/core/components/pageEditor/PageEditor.tsx @@ -3,7 +3,7 @@ import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core"; import { useFileState, useFileActions } from "@app/contexts/FileContext"; import { useNavigationGuard } from "@app/contexts/NavigationContext"; import { usePageEditor } from "@app/contexts/PageEditorContext"; -import { PageEditorFunctions } from "@app/types/pageEditor"; +import { PageEditorFunctions, PDFPage } from "@app/types/pageEditor"; // Thumbnail generation is now handled by individual PageThumbnail components import '@app/components/pageEditor/PageEditor.module.css'; import PageThumbnail from '@app/components/pageEditor/PageThumbnail'; @@ -23,6 +23,7 @@ import { useUndoManagerState } from "@app/components/pageEditor/hooks/useUndoMan import { usePageSelectionManager } from "@app/components/pageEditor/hooks/usePageSelectionManager"; import { usePageEditorCommands } from "@app/components/pageEditor/hooks/useEditorCommands"; import { usePageEditorExport } from "@app/components/pageEditor/hooks/usePageEditorExport"; +import { useThumbnailGeneration } from "@app/hooks/useThumbnailGeneration"; export interface PageEditorProps { onFunctionsReady?: (functions: PageEditorFunctions) => void; @@ -40,7 +41,27 @@ const PageEditor = ({ const { setHasUnsavedChanges } = useNavigationGuard(); // Get PageEditor coordination functions - const { updateFileOrderFromPages, fileOrder, reorderedPages, clearReorderedPages, updateCurrentPages } = usePageEditor(); + const { + updateFileOrderFromPages, + fileOrder, + reorderedPages, + clearReorderedPages, + updateCurrentPages, + savePersistedDocument, + } = usePageEditor(); + + const [visiblePageIds, setVisiblePageIds] = useState([]); + const thumbnailRequestsRef = useRef>(new Set()); + const { requestThumbnail, getThumbnailFromCache } = useThumbnailGeneration(); + const handleVisibleItemsChange = useCallback((items: PDFPage[]) => { + setVisiblePageIds(prev => { + const ids = items.map(item => item.id); + if (prev.length === ids.length && prev.every((id, index) => id === ids[index])) { + return prev; + } + return ids; + }); + }, []); // Zoom state management const [zoomLevel, setZoomLevel] = useState(1.0); @@ -149,6 +170,21 @@ const PageEditor = ({ updateCurrentPages, }); + const displayDocumentRef = useRef(displayDocument); + useEffect(() => { + displayDocumentRef.current = displayDocument; + }, [displayDocument]); + + useEffect(() => { + return () => { + const doc = displayDocumentRef.current; + if (doc && doc.pages.length > 0) { + const signature = doc.pages.map(page => page.id).join(','); + savePersistedDocument(doc, signature); + } + }; + }, [savePersistedDocument]); + // UI state management const { selectionMode, selectedPageIds, movingPage, isAnimating, splitPositions, exportLoading, @@ -231,6 +267,92 @@ const PageEditor = ({ setSplitPositions, }); + useEffect(() => { + if (!displayDocument || visiblePageIds.length === 0) { + return; + } + + const pending = thumbnailRequestsRef.current.size; + const MAX_CONCURRENT_THUMBNAILS = 12; + const available = Math.max(0, MAX_CONCURRENT_THUMBNAILS - pending); + if (available === 0) { + return; + } + + const toLoad: string[] = []; + for (const pageId of visiblePageIds) { + if (toLoad.length >= available) break; + if (thumbnailRequestsRef.current.has(pageId)) continue; + const page = displayDocument.pages.find(p => p.id === pageId); + if (!page || page.thumbnail) continue; + toLoad.push(pageId); + } + + if (toLoad.length === 0) return; + + toLoad.forEach(pageId => { + const page = displayDocument.pages.find(p => p.id === pageId); + if (!page) return; + + const cached = getThumbnailFromCache(pageId); + if (cached) { + thumbnailRequestsRef.current.add(pageId); + Promise.resolve(cached) + .then(cache => { + setEditedDocument(prev => { + if (!prev) return prev; + const pageIndex = prev.pages.findIndex(p => p.id === pageId); + if (pageIndex === -1) return prev; + + // Only create new page object for the changed page, reuse rest + const updated = [...prev.pages]; + updated[pageIndex] = { ...prev.pages[pageIndex], thumbnail: cache }; + return { ...prev, pages: updated }; + }); + }) + .finally(() => { + thumbnailRequestsRef.current.delete(pageId); + }); + return; + } + + const fileId = page.originalFileId; + if (!fileId) return; + const file = selectors.getFile(fileId); + if (!file) return; + + thumbnailRequestsRef.current.add(pageId); + requestThumbnail(pageId, file, page.originalPageNumber || page.pageNumber) + .then(thumbnail => { + if (thumbnail) { + setEditedDocument(prev => { + if (!prev) return prev; + const pageIndex = prev.pages.findIndex(p => p.id === pageId); + if (pageIndex === -1) return prev; + + // Only create new page object for the changed page, reuse rest + const updated = [...prev.pages]; + updated[pageIndex] = { ...prev.pages[pageIndex], thumbnail }; + return { ...prev, pages: updated }; + }); + } + }) + .catch((error) => { + console.error('[Thumbnail Loading] Error:', error); + }) + .finally(() => { + thumbnailRequestsRef.current.delete(pageId); + }); + }); + }, [ + displayDocument, + visiblePageIds, + selectors, + requestThumbnail, + getThumbnailFromCache, + setEditedDocument, + ]); + // Derived values for right rail and usePageEditorRightRailButtons (must be after displayDocument) const selectedPageCount = selectedPageIds.length; const activeFileIds = selectedFileIds; @@ -345,12 +467,17 @@ const PageEditor = ({ const fileColorIndexMap = useFileColorMap(orderedFileIds); return ( - setIsContainerHovered(true)} onMouseLeave={() => setIsContainerHovered(false)} + style={{ + height: '100%', + overflow: 'auto', + position: 'relative', + width: '100%', + }} > @@ -372,7 +499,7 @@ const PageEditor = ({ )} {displayDocument && ( - + {/* Split Lines Overlay */}
{ const page = displayDocument.pages.find(p => p.id === pageId); if (!page?.thumbnail) return null; @@ -468,7 +596,6 @@ const PageEditor = ({ page={page} index={index} totalPages={displayDocument.pages.length} - originalFile={(page as any).originalFileId ? selectors.getFile((page as any).originalFileId) : undefined} fileColorIndex={fileColorIndex} selectedPageIds={selectedPageIds} selectionMode={selectionMode} @@ -512,7 +639,7 @@ const PageEditor = ({ }} /> - +
); }; diff --git a/frontend/src/core/components/pageEditor/PageThumbnail.tsx b/frontend/src/core/components/pageEditor/PageThumbnail.tsx index 2cc7d38fab..851b0a8d5b 100644 --- a/frontend/src/core/components/pageEditor/PageThumbnail.tsx +++ b/frontend/src/core/components/pageEditor/PageThumbnail.tsx @@ -9,7 +9,6 @@ import DeleteIcon from '@mui/icons-material/Delete'; import ContentCutIcon from '@mui/icons-material/ContentCut'; import AddIcon from '@mui/icons-material/Add'; import { PDFPage, PDFDocument } from '@app/types/pageEditor'; -import { useThumbnailGeneration } from '@app/hooks/useThumbnailGeneration'; import { useFilesModalContext } from '@app/contexts/FilesModalContext'; import { getFileColorWithOpacity } from '@app/components/pageEditor/fileColors'; import styles from '@app/components/pageEditor/PageEditor.module.css'; @@ -22,7 +21,6 @@ interface PageThumbnailProps { page: PDFPage; index: number; totalPages: number; - originalFile?: File; fileColorIndex: number; selectedPageIds: string[]; selectionMode: boolean; @@ -55,7 +53,6 @@ const PageThumbnail: React.FC = ({ page, index: _index, totalPages, - originalFile, fileColorIndex, selectedPageIds, selectionMode, @@ -90,7 +87,6 @@ const PageThumbnail: React.FC = ({ const [thumbnailUrl, setThumbnailUrl] = useState(page.thumbnail); const elementRef = useRef(null); - const { getThumbnailFromCache, requestThumbnail} = useThumbnailGeneration(); const { openFilesModal } = useFilesModalContext(); // Check if this page is currently being dragged @@ -115,43 +111,6 @@ const PageThumbnail: React.FC = ({ } }, [page.thumbnail, thumbnailUrl]); - // Request thumbnail if missing (on-demand, virtualized approach) - useEffect(() => { - let isCancelled = false; - - // If we already have a thumbnail, use it - if (page.thumbnail) { - setThumbnailUrl(page.thumbnail); - return; - } - - // Check cache first - const cachedThumbnail = getThumbnailFromCache(page.id); - if (cachedThumbnail) { - setThumbnailUrl(cachedThumbnail); - return; - } - - // Request thumbnail generation if we have the original file - if (originalFile) { - const pageNumber = page.originalPageNumber; - - requestThumbnail(page.id, originalFile, pageNumber) - .then(thumbnail => { - if (!isCancelled && thumbnail) { - setThumbnailUrl(thumbnail); - } - }) - .catch(error => { - console.warn(`Failed to generate thumbnail for ${page.id}:`, error); - }); - } - - return () => { - isCancelled = true; - }; - }, [page.id, page.thumbnail, originalFile, getThumbnailFromCache, requestThumbnail]); - // Merge refs - combine our ref tracking with dnd-kit's ref const mergedRef = useCallback((element: HTMLDivElement | null) => { // Track in our refs map diff --git a/frontend/src/core/components/pageEditor/commands/pageCommands.ts b/frontend/src/core/components/pageEditor/commands/pageCommands.ts index 065a18ee81..536f1d6671 100644 --- a/frontend/src/core/components/pageEditor/commands/pageCommands.ts +++ b/frontend/src/core/components/pageEditor/commands/pageCommands.ts @@ -748,43 +748,49 @@ export class InsertFilesCommand extends DOMCommand { console.log('Pages:', pages.length); console.log('ArrayBuffer size:', arrayBuffer?.byteLength || 'undefined'); - if (arrayBuffer && arrayBuffer.byteLength > 0) { - // Extract page numbers for all pages from this file - const pageNumbers = pages.map(page => { - const pageNumMatch = page.id.match(/-page-(\d+)$/); - return pageNumMatch ? parseInt(pageNumMatch[1]) : 1; - }); + try { + if (arrayBuffer && arrayBuffer.byteLength > 0) { + // Extract page numbers for all pages from this file + const pageNumbers = pages.map(page => { + const pageNumMatch = page.id.match(/-page-(\d+)$/); + return pageNumMatch ? parseInt(pageNumMatch[1]) : 1; + }); - console.log('Generating thumbnails for page numbers:', pageNumbers); + console.log('Generating thumbnails for page numbers:', pageNumbers); - // Generate thumbnails for all pages from this file at once - const results = await thumbnailGenerationService.generateThumbnails( - fileId, - arrayBuffer, - pageNumbers, - { scale: 0.2, quality: 0.8 } - ); + // Generate thumbnails for all pages from this file at once + const results = await thumbnailGenerationService.generateThumbnails( + fileId, + arrayBuffer, + pageNumbers, + { scale: 0.2, quality: 0.8 } + ); - console.log('Thumbnail generation results:', results.length, 'thumbnails generated'); + console.log('Thumbnail generation results:', results.length, 'thumbnails generated'); - // Update pages with generated thumbnails - for (let i = 0; i < results.length && i < pages.length; i++) { - const result = results[i]; - const page = pages[i]; + // Update pages with generated thumbnails + for (let i = 0; i < results.length && i < pages.length; i++) { + const result = results[i]; + const page = pages[i]; - if (result.success) { - const pageIndex = updatedDocument.pages.findIndex(p => p.id === page.id); - if (pageIndex >= 0) { - updatedDocument.pages[pageIndex].thumbnail = result.thumbnail; - console.log('Updated thumbnail for page:', page.id); + if (result.success) { + const pageIndex = updatedDocument.pages.findIndex(p => p.id === page.id); + if (pageIndex >= 0) { + updatedDocument.pages[pageIndex].thumbnail = result.thumbnail; + console.log('Updated thumbnail for page:', page.id); + } } } - } - // Trigger re-render by updating the document - this.setDocument({ ...updatedDocument }); - } else { - console.error('No valid ArrayBuffer found for file ID:', fileId); + // Trigger re-render by updating the document + this.setDocument({ ...updatedDocument }); + } else { + console.error('No valid ArrayBuffer found for file ID:', fileId); + } + } catch (error) { + console.error('Failed to generate thumbnails for file:', fileId, error); + } finally { + this.fileDataMap.delete(fileId); } } } catch (error) { diff --git a/frontend/src/core/components/pageEditor/constants.ts b/frontend/src/core/components/pageEditor/constants.ts index 13239d722a..28ee92580d 100644 --- a/frontend/src/core/components/pageEditor/constants.ts +++ b/frontend/src/core/components/pageEditor/constants.ts @@ -3,6 +3,6 @@ export const GRID_CONSTANTS = { ITEM_WIDTH: '20rem', // page width ITEM_HEIGHT: '21.5rem', // 20rem + 1.5rem gap ITEM_GAP: '1.5rem', // gap between items - OVERSCAN_SMALL: 4, // Overscan for normal documents - OVERSCAN_LARGE: 8, // Overscan for large documents (>1000 pages) + OVERSCAN_SMALL: 8, // Overscan for normal documents + OVERSCAN_LARGE: 12, // Overscan for large documents (12 rows = ~96 pages pre-rendered) } as const; \ No newline at end of file diff --git a/frontend/src/core/components/pageEditor/hooks/usePageDocument.ts b/frontend/src/core/components/pageEditor/hooks/usePageDocument.ts index 40b823bd1c..85ee311d60 100644 --- a/frontend/src/core/components/pageEditor/hooks/usePageDocument.ts +++ b/frontend/src/core/components/pageEditor/hooks/usePageDocument.ts @@ -1,8 +1,9 @@ -import { useMemo } from 'react'; +import { useMemo, useEffect, useState } from 'react'; import { useFileState } from '@app/contexts/FileContext'; import { usePageEditor } from '@app/contexts/PageEditorContext'; import { PDFDocument, PDFPage } from '@app/types/pageEditor'; import { FileId } from '@app/types/file'; +import { FileAnalyzer } from '@app/services/fileAnalyzer'; export interface PageDocumentHook { document: PDFDocument | null; @@ -16,7 +17,7 @@ export interface PageDocumentHook { */ export function usePageDocument(): PageDocumentHook { const { state, selectors } = useFileState(); - const { fileOrder, currentPages } = usePageEditor(); + const { fileOrder, currentPages, persistedDocument, persistedDocumentSignature } = usePageEditor(); // Use PageEditorContext's fileOrder instead of FileContext's global order // This ensures the page editor respects its own workspace ordering @@ -58,6 +59,63 @@ export function usePageDocument(): PageDocumentHook { const processedFilePages = primaryStirlingFileStub?.processedFile?.pages; const processedFileTotalPages = primaryStirlingFileStub?.processedFile?.totalPages; + const [placeholderDocument, setPlaceholderDocument] = useState(null); + + useEffect(() => { + if (!primaryFileId) { + setPlaceholderDocument(null); + return; + } + + if (primaryStirlingFileStub?.processedFile) { + setPlaceholderDocument(null); + return; + } + + const file = selectors.getFile(primaryFileId); + if (!file) { + setPlaceholderDocument(null); + return; + } + + let canceled = false; + + const loadPlaceholder = async () => { + try { + const analysis = await FileAnalyzer.quickPDFAnalysis(file); + if (canceled) return; + const totalPages = Math.max(1, analysis.pageCount || 1); + const pages: PDFPage[] = Array.from({ length: totalPages }, (_, index) => ({ + id: `placeholder-${primaryFileId}-page-${index + 1}`, + pageNumber: index + 1, + thumbnail: null, + rotation: 0, + selected: false, + originalFileId: primaryFileId, + originalPageNumber: index + 1, + })); + + setPlaceholderDocument({ + id: `placeholder-${primaryFileId}`, + name: selectors.getStirlingFileStub(primaryFileId)?.name ?? file.name, + file, + pages, + totalPages, + }); + } catch { + if (!canceled) { + setPlaceholderDocument(null); + } + } + }; + + loadPlaceholder(); + + return () => { + canceled = true; + }; + }, [primaryFileId, primaryStirlingFileStub?.processedFile, selectors]); + // Compute merged document with stable signature (prevents infinite loops) const currentPagesSignature = useMemo(() => { return currentPages ? currentPages.map(page => page.id).join(',') : ''; @@ -66,7 +124,20 @@ export function usePageDocument(): PageDocumentHook { const mergedPdfDocument = useMemo((): PDFDocument | null => { if (activeFileIds.length === 0) return null; - const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null; + if ( + persistedDocument && + persistedDocumentSignature && + persistedDocumentSignature === currentPagesSignature && + currentPagesSignature.length > 0 + ) { + return persistedDocument; + } + + if (!primaryStirlingFileStub?.processedFile && placeholderDocument) { + return placeholderDocument; + } + + const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null; // If we have file IDs but no file record, something is wrong - return null to show loading if (!primaryStirlingFileStub) { @@ -245,7 +316,24 @@ export function usePageDocument(): PageDocumentHook { }; return mergedDoc; - }, [activeFileIds, selectedActiveFileIds, primaryFileId, primaryStirlingFileStub, processedFilePages, processedFileTotalPages, selectors, activeFilesSignature, selectedFileIdsKey, state.ui.selectedFileIds, allFileIds, currentPagesSignature, currentPages]); + }, [ + activeFileIds, + selectedActiveFileIds, + primaryFileId, + primaryStirlingFileStub, + processedFilePages, + processedFileTotalPages, + selectors, + activeFilesSignature, + selectedFileIdsKey, + state.ui.selectedFileIds, + allFileIds, + currentPagesSignature, + currentPages, + persistedDocument, + persistedDocumentSignature, + placeholderDocument, + ]); // Large document detection for smart loading const isVeryLargeDocument = useMemo(() => { diff --git a/frontend/src/core/contexts/PageEditorContext.tsx b/frontend/src/core/contexts/PageEditorContext.tsx index 6881db1ed0..2acee3ec29 100644 --- a/frontend/src/core/contexts/PageEditorContext.tsx +++ b/frontend/src/core/contexts/PageEditorContext.tsx @@ -1,7 +1,7 @@ import React, { createContext, useContext, useState, useCallback, ReactNode, useMemo, useRef, useEffect } from 'react'; import { FileId } from '@app/types/file'; import { useFileActions, useFileState } from '@app/contexts/FileContext'; -import { PDFPage } from '@app/types/pageEditor'; +import { PDFDocument, PDFPage } from '@app/types/pageEditor'; import { MAX_PAGE_EDITOR_FILES } from '@app/components/pageEditor/fileColors'; // PageEditorFile is now defined locally in consuming components @@ -129,6 +129,10 @@ interface PageEditorContextValue { // Update file order based on page positions (when pages are manually reordered) updateFileOrderFromPages: (pages: PDFPage[]) => void; + persistedDocument: PDFDocument | null; + persistedDocumentSignature: string | null; + savePersistedDocument: (document: PDFDocument, signature: string) => void; + clearPersistedDocument: () => void; } const PageEditorContext = createContext(undefined); @@ -141,6 +145,19 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) { const [currentPages, setCurrentPages] = useState(null); const [reorderedPages, setReorderedPages] = useState(null); + const [persistedDocument, setPersistedDocument] = useState(null); + const [persistedDocumentSignature, setPersistedDocumentSignature] = useState(null); + + const savePersistedDocument = useCallback((document: PDFDocument, signature: string) => { + setPersistedDocument(document); + setPersistedDocumentSignature(signature); + }, []); + + const clearPersistedDocument = useCallback(() => { + setPersistedDocument(null); + setPersistedDocumentSignature(null); + }, []); + // Page editor's own file order (independent of FileContext) const [fileOrder, setFileOrder] = useState([]); @@ -148,6 +165,20 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) { const { actions: fileActions } = useFileActions(); const { state } = useFileState(); + const fileContextSignature = useMemo(() => { + return state.files.ids + .map(id => `${id}:${state.files.byId[id]?.versionNumber ?? 0}`) + .join(','); + }, [state.files.ids, state.files.byId]); + + const prevFileContextSignature = useRef(null); + useEffect(() => { + if (prevFileContextSignature.current !== fileContextSignature) { + prevFileContextSignature.current = fileContextSignature; + clearPersistedDocument(); + } + }, [fileContextSignature, clearPersistedDocument]); + // Keep a ref to always read latest state in stable callbacks const stateRef = useRef(state); useEffect(() => { @@ -203,7 +234,7 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) { } }); }, 100); - }, [state.files.ids, state.files.byId, fileActions]); + }, [state.files.ids, state.files.byId, fileActions]); const updateCurrentPages = useCallback((pages: PDFPage[] | null) => { setCurrentPages(pages); @@ -329,6 +360,10 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) { deselectAll, reorderFiles, updateFileOrderFromPages, + persistedDocument, + persistedDocumentSignature, + savePersistedDocument, + clearPersistedDocument, }), [ currentPages, updateCurrentPages, @@ -341,6 +376,10 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) { deselectAll, reorderFiles, updateFileOrderFromPages, + persistedDocument, + persistedDocumentSignature, + savePersistedDocument, + clearPersistedDocument, ]); return ( diff --git a/frontend/src/core/hooks/useThumbnailGeneration.ts b/frontend/src/core/hooks/useThumbnailGeneration.ts index 48f7b241bd..a2b251786f 100644 --- a/frontend/src/core/hooks/useThumbnailGeneration.ts +++ b/frontend/src/core/hooks/useThumbnailGeneration.ts @@ -20,10 +20,13 @@ let batchTimer: number | null = null; // Track active thumbnail requests to prevent duplicates across components const activeRequests = new Map>(); +// Cache ArrayBuffers to avoid reading the same file multiple times +const fileArrayBufferCache = new Map(); + // Batch processing configuration -const BATCH_SIZE = 20; // Process thumbnails in batches of 20 for better UI responsiveness -const BATCH_DELAY = 100; // Wait 100ms to collect requests before processing -const PRIORITY_BATCH_DELAY = 50; // Faster processing for the first batch (visible pages) +const BATCH_SIZE = 10; // Process thumbnails in batches of 10 for faster initial load +const BATCH_DELAY = 50; // Wait 50ms to collect requests before processing +const PRIORITY_BATCH_DELAY = 10; // Very fast processing for the first batch (visible pages) // Process the queue in batches for better performance async function processRequestQueue() { @@ -68,8 +71,13 @@ async function processRequestQueue() { try { const pageNumbers = requests.map(req => req.pageNumber); - const arrayBuffer = await file.arrayBuffer(); + // Get or create cached ArrayBuffer to avoid reading file multiple times + let arrayBuffer = fileArrayBufferCache.get(file); + if (!arrayBuffer) { + arrayBuffer = await file.arrayBuffer(); + fileArrayBufferCache.set(file, arrayBuffer); + } // Use quickKey for PDF document caching (same metadata, consistent format) const fileId = createQuickKey(file) as FileId; @@ -106,6 +114,10 @@ async function processRequestQueue() { } } finally { isProcessingQueue = false; + // Clean up ArrayBuffer cache when queue is empty + if (requestQueue.length === 0) { + fileArrayBufferCache.clear(); + } } } @@ -163,6 +175,9 @@ export function useThumbnailGeneration() { activeRequests.clear(); isProcessingQueue = false; + // Clear ArrayBuffer cache + fileArrayBufferCache.clear(); + thumbnailGenerationService.destroy(); }, []); diff --git a/frontend/src/core/services/enhancedPDFProcessingService.ts b/frontend/src/core/services/enhancedPDFProcessingService.ts index b084d2eb1d..17aeb015a3 100644 --- a/frontend/src/core/services/enhancedPDFProcessingService.ts +++ b/frontend/src/core/services/enhancedPDFProcessingService.ts @@ -265,17 +265,13 @@ export class EnhancedPDFProcessingService { this.notifyListeners(); } - // Create placeholder pages for remaining pages + // Create placeholder pages for remaining pages without touching PDF.js for (let i = priorityCount + 1; i <= totalPages; i++) { - // Load page just to get rotation - const page = await pdf.getPage(i); - const rotation = page.rotate || 0; - pages.push({ id: `${createQuickKey(file)}-page-${i}`, pageNumber: i, thumbnail: null, // Will be loaded lazily - rotation, + rotation: 0, selected: false }); } @@ -337,17 +333,13 @@ export class EnhancedPDFProcessingService { } } - // Create placeholders for remaining pages + // Create placeholders for remaining pages without invoking PDF.js for (let i = firstChunkEnd + 1; i <= totalPages; i++) { - // Load page just to get rotation - const page = await pdf.getPage(i); - const rotation = page.rotate || 0; - pages.push({ id: `${createQuickKey(file)}-page-${i}`, pageNumber: i, thumbnail: null, - rotation, + rotation: 0, selected: false }); } @@ -377,15 +369,11 @@ export class EnhancedPDFProcessingService { // Create placeholder pages without thumbnails const pages: PDFPage[] = []; for (let i = 1; i <= totalPages; i++) { - // Load page just to get rotation - const page = await pdf.getPage(i); - const rotation = page.rotate || 0; - pages.push({ id: `${createQuickKey(file)}-page-${i}`, pageNumber: i, thumbnail: null, - rotation, + rotation: 0, selected: false }); } diff --git a/frontend/src/core/services/fileAnalyzer.ts b/frontend/src/core/services/fileAnalyzer.ts index e168e530bc..7181489d50 100644 --- a/frontend/src/core/services/fileAnalyzer.ts +++ b/frontend/src/core/services/fileAnalyzer.ts @@ -56,7 +56,7 @@ export class FileAnalyzer { /** * Quick PDF analysis without full processing */ - private static async quickPDFAnalysis(file: File): Promise<{ + static async quickPDFAnalysis(file: File): Promise<{ pageCount: number; isEncrypted: boolean; isCorrupted: boolean; From 36358fc1393d3a4ea233556a9a40392072d78640 Mon Sep 17 00:00:00 2001 From: Ludy Date: Sat, 31 Jan 2026 21:26:21 +0100 Subject: [PATCH 2/6] Update Python dependencies in requirements files (#5627) # Description of Changes --- ## 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) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### 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. --- .github/scripts/requirements_dev.txt | 609 +++++++++---------- .github/scripts/requirements_pre_commit.txt | 36 +- .github/scripts/requirements_sync_readme.txt | 6 +- testing/cucumber/requirements.txt | 212 +++---- 4 files changed, 407 insertions(+), 456 deletions(-) diff --git a/.github/scripts/requirements_dev.txt b/.github/scripts/requirements_dev.txt index 0f42b28a7b..c8bd1cf915 100644 --- a/.github/scripts/requirements_dev.txt +++ b/.github/scripts/requirements_dev.txt @@ -97,9 +97,9 @@ cffi==2.0.0 \ --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf # via weasyprint -cfgv==3.4.0 \ - --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ - --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 +cfgv==3.5.0 \ + --hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \ + --hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132 # via pre-commit cssselect2==0.8.0 \ --hash=sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e \ @@ -109,259 +109,269 @@ distlib==0.4.0 \ --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d # via virtualenv -filelock==3.20.0 \ - --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ - --hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4 +filelock==3.20.3 \ + --hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \ + --hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1 # via virtualenv -fonttools==4.60.1 \ - --hash=sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c \ - --hash=sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc \ - --hash=sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a \ - --hash=sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856 \ - --hash=sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2 \ - --hash=sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259 \ - --hash=sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c \ - --hash=sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce \ - --hash=sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003 \ - --hash=sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272 \ - --hash=sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77 \ - --hash=sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038 \ - --hash=sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea \ - --hash=sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854 \ - --hash=sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2 \ - --hash=sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258 \ - --hash=sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652 \ - --hash=sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08 \ - --hash=sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99 \ - --hash=sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7 \ - --hash=sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914 \ - --hash=sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6 \ - --hash=sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed \ - --hash=sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb \ - --hash=sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217 \ - --hash=sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc \ - --hash=sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f \ - --hash=sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c \ - --hash=sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877 \ - --hash=sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801 \ - --hash=sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85 \ - --hash=sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a \ - --hash=sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb \ - --hash=sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383 \ - --hash=sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401 \ - --hash=sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28 \ - --hash=sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01 \ - --hash=sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036 \ - --hash=sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc \ - --hash=sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac \ - --hash=sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903 \ - --hash=sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3 \ - --hash=sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6 \ - --hash=sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c \ - --hash=sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da \ - --hash=sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299 \ - --hash=sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15 \ - --hash=sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199 \ - --hash=sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf \ - --hash=sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1 \ - --hash=sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537 \ - --hash=sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d \ - --hash=sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c \ - --hash=sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4 \ - --hash=sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9 \ - --hash=sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed \ - --hash=sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987 \ - --hash=sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa +fonttools==4.61.1 \ + --hash=sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87 \ + --hash=sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796 \ + --hash=sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75 \ + --hash=sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d \ + --hash=sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371 \ + --hash=sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b \ + --hash=sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b \ + --hash=sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2 \ + --hash=sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3 \ + --hash=sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9 \ + --hash=sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd \ + --hash=sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c \ + --hash=sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c \ + --hash=sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56 \ + --hash=sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37 \ + --hash=sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0 \ + --hash=sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958 \ + --hash=sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5 \ + --hash=sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118 \ + --hash=sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69 \ + --hash=sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9 \ + --hash=sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261 \ + --hash=sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb \ + --hash=sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47 \ + --hash=sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24 \ + --hash=sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c \ + --hash=sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba \ + --hash=sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c \ + --hash=sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91 \ + --hash=sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1 \ + --hash=sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19 \ + --hash=sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6 \ + --hash=sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5 \ + --hash=sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2 \ + --hash=sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d \ + --hash=sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881 \ + --hash=sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063 \ + --hash=sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7 \ + --hash=sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09 \ + --hash=sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da \ + --hash=sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e \ + --hash=sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e \ + --hash=sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8 \ + --hash=sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa \ + --hash=sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6 \ + --hash=sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e \ + --hash=sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a \ + --hash=sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c \ + --hash=sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7 \ + --hash=sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd # via weasyprint -identify==2.6.15 \ - --hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \ - --hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf +identify==2.6.16 \ + --hash=sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 \ + --hash=sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980 # via pre-commit -nodeenv==1.9.1 \ - --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ - --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 +nodeenv==1.10.0 \ + --hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \ + --hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb # via pre-commit -numpy==2.2.6 \ - --hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \ - --hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \ - --hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \ - --hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \ - --hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \ - --hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \ - --hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \ - --hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \ - --hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \ - --hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \ - --hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \ - --hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \ - --hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \ - --hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \ - --hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \ - --hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \ - --hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \ - --hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \ - --hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \ - --hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \ - --hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \ - --hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \ - --hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \ - --hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \ - --hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \ - --hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \ - --hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \ - --hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \ - --hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \ - --hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \ - --hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \ - --hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \ - --hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \ - --hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \ - --hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \ - --hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \ - --hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \ - --hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \ - --hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \ - --hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \ - --hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \ - --hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \ - --hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \ - --hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \ - --hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \ - --hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \ - --hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \ - --hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \ - --hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \ - --hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \ - --hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \ - --hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \ - --hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \ - --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \ - --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8 +numpy==2.4.1 \ + --hash=sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c \ + --hash=sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba \ + --hash=sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5 \ + --hash=sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8 \ + --hash=sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0 \ + --hash=sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d \ + --hash=sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574 \ + --hash=sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696 \ + --hash=sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5 \ + --hash=sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505 \ + --hash=sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0 \ + --hash=sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162 \ + --hash=sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844 \ + --hash=sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205 \ + --hash=sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4 \ + --hash=sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc \ + --hash=sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d \ + --hash=sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93 \ + --hash=sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01 \ + --hash=sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c \ + --hash=sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f \ + --hash=sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33 \ + --hash=sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82 \ + --hash=sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2 \ + --hash=sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42 \ + --hash=sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509 \ + --hash=sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a \ + --hash=sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e \ + --hash=sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556 \ + --hash=sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a \ + --hash=sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510 \ + --hash=sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295 \ + --hash=sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73 \ + --hash=sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3 \ + --hash=sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9 \ + --hash=sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8 \ + --hash=sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745 \ + --hash=sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2 \ + --hash=sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02 \ + --hash=sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d \ + --hash=sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344 \ + --hash=sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f \ + --hash=sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be \ + --hash=sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425 \ + --hash=sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1 \ + --hash=sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2 \ + --hash=sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2 \ + --hash=sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb \ + --hash=sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9 \ + --hash=sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15 \ + --hash=sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690 \ + --hash=sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0 \ + --hash=sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261 \ + --hash=sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a \ + --hash=sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc \ + --hash=sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f \ + --hash=sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5 \ + --hash=sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df \ + --hash=sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9 \ + --hash=sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2 \ + --hash=sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8 \ + --hash=sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426 \ + --hash=sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b \ + --hash=sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87 \ + --hash=sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220 \ + --hash=sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b \ + --hash=sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3 \ + --hash=sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e \ + --hash=sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501 \ + --hash=sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee \ + --hash=sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7 \ + --hash=sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c # via opencv-python-headless -opencv-python-headless==4.12.0.88 \ - --hash=sha256:1e58d664809b3350c1123484dd441e1667cd7bed3086db1b9ea1b6f6cb20b50e \ - --hash=sha256:236c8df54a90f4d02076e6f9c1cc763d794542e886c576a6fee46ec8ff75a7a9 \ - --hash=sha256:365bb2e486b50feffc2d07a405b953a8f3e8eaa63865bc650034e5c71e7a5154 \ - --hash=sha256:86b413bdd6c6bf497832e346cd5371995de148e579b9774f8eba686dee3f5528 \ - --hash=sha256:aeb4b13ecb8b4a0beb2668ea07928160ea7c2cd2d9b5ef571bbee6bafe9cc8d0 \ - --hash=sha256:cfdc017ddf2e59b6c2f53bc12d74b6b0be7ded4ec59083ea70763921af2b6c09 \ - --hash=sha256:fde2cf5c51e4def5f2132d78e0c08f9c14783cd67356922182c6845b9af87dbd +opencv-python-headless==4.13.0.90 \ + --hash=sha256:0e0c8c9f620802fddc4fa7f471a1d263c7b0dca16cd9e7e2f996bb8bd2128c0c \ + --hash=sha256:12a28674f215542c9bf93338de1b5bffd76996d32da9acb9e739fdb9c8bbd738 \ + --hash=sha256:32255203040dc98803be96362e13f9e4bce20146898222d2e5c242f80de50da5 \ + --hash=sha256:96060fc57a1abb1144b0b8129e2ff3bfcdd0ccd8e8bd05bd85256ff4ed587d3b \ + --hash=sha256:dbc1f4625e5af3a80ebdbd84380227c0f445228588f2521b11af47710caca1ba \ + --hash=sha256:e13790342591557050157713af17a7435ac1b50c65282715093c9297fa045d8f \ + --hash=sha256:eba38bc255d0b7d1969c5bcc90a060ca2b61a3403b613872c750bfa5dfe9e03b \ + --hash=sha256:f46b17ea0aa7e4124ca6ad71143f89233ae9557f61d2326bcdb34329a1ddf9bd # via -r .github/scripts/requirements_dev.in pdf2image==1.17.0 \ --hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \ --hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2 # via -r .github/scripts/requirements_dev.in -pillow==12.0.0 \ - --hash=sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643 \ - --hash=sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e \ - --hash=sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e \ - --hash=sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc \ - --hash=sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642 \ - --hash=sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6 \ - --hash=sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1 \ - --hash=sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b \ - --hash=sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399 \ - --hash=sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba \ - --hash=sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad \ - --hash=sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47 \ - --hash=sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739 \ - --hash=sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b \ - --hash=sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f \ - --hash=sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10 \ - --hash=sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52 \ - --hash=sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d \ - --hash=sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b \ - --hash=sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a \ - --hash=sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9 \ - --hash=sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d \ - --hash=sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098 \ - --hash=sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905 \ - --hash=sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b \ - --hash=sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3 \ - --hash=sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371 \ - --hash=sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953 \ - --hash=sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01 \ - --hash=sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca \ - --hash=sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e \ - --hash=sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7 \ - --hash=sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27 \ - --hash=sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082 \ - --hash=sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e \ - --hash=sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d \ - --hash=sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8 \ - --hash=sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a \ - --hash=sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad \ - --hash=sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3 \ - --hash=sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a \ - --hash=sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d \ - --hash=sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353 \ - --hash=sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee \ - --hash=sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b \ - --hash=sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b \ - --hash=sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a \ - --hash=sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7 \ - --hash=sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef \ - --hash=sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a \ - --hash=sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a \ - --hash=sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257 \ - --hash=sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07 \ - --hash=sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4 \ - --hash=sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c \ - --hash=sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c \ - --hash=sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4 \ - --hash=sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe \ - --hash=sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8 \ - --hash=sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5 \ - --hash=sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6 \ - --hash=sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e \ - --hash=sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8 \ - --hash=sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e \ - --hash=sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275 \ - --hash=sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3 \ - --hash=sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76 \ - --hash=sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227 \ - --hash=sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9 \ - --hash=sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5 \ - --hash=sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79 \ - --hash=sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca \ - --hash=sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa \ - --hash=sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b \ - --hash=sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e \ - --hash=sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197 \ - --hash=sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab \ - --hash=sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79 \ - --hash=sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2 \ - --hash=sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363 \ - --hash=sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0 \ - --hash=sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e \ - --hash=sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782 \ - --hash=sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925 \ - --hash=sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0 \ - --hash=sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b \ - --hash=sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced \ - --hash=sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c \ - --hash=sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344 \ - --hash=sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9 \ - --hash=sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1 +pillow==12.1.0 \ + --hash=sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d \ + --hash=sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc \ + --hash=sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84 \ + --hash=sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de \ + --hash=sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0 \ + --hash=sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef \ + --hash=sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4 \ + --hash=sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82 \ + --hash=sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9 \ + --hash=sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030 \ + --hash=sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0 \ + --hash=sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18 \ + --hash=sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a \ + --hash=sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef \ + --hash=sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b \ + --hash=sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6 \ + --hash=sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179 \ + --hash=sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e \ + --hash=sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72 \ + --hash=sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64 \ + --hash=sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451 \ + --hash=sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd \ + --hash=sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924 \ + --hash=sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616 \ + --hash=sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a \ + --hash=sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94 \ + --hash=sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc \ + --hash=sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8 \ + --hash=sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9 \ + --hash=sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91 \ + --hash=sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a \ + --hash=sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c \ + --hash=sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670 \ + --hash=sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea \ + --hash=sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91 \ + --hash=sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c \ + --hash=sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc \ + --hash=sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0 \ + --hash=sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b \ + --hash=sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65 \ + --hash=sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661 \ + --hash=sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19 \ + --hash=sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1 \ + --hash=sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0 \ + --hash=sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e \ + --hash=sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75 \ + --hash=sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4 \ + --hash=sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8 \ + --hash=sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd \ + --hash=sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7 \ + --hash=sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61 \ + --hash=sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51 \ + --hash=sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551 \ + --hash=sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45 \ + --hash=sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1 \ + --hash=sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644 \ + --hash=sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796 \ + --hash=sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587 \ + --hash=sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304 \ + --hash=sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b \ + --hash=sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8 \ + --hash=sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17 \ + --hash=sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171 \ + --hash=sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3 \ + --hash=sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7 \ + --hash=sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988 \ + --hash=sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a \ + --hash=sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0 \ + --hash=sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c \ + --hash=sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2 \ + --hash=sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14 \ + --hash=sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5 \ + --hash=sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a \ + --hash=sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377 \ + --hash=sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0 \ + --hash=sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5 \ + --hash=sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b \ + --hash=sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d \ + --hash=sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac \ + --hash=sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c \ + --hash=sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554 \ + --hash=sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643 \ + --hash=sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13 \ + --hash=sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09 \ + --hash=sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208 \ + --hash=sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda \ + --hash=sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea \ + --hash=sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e \ + --hash=sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0 \ + --hash=sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831 \ + --hash=sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd # via # -r .github/scripts/requirements_dev.in # pdf2image # weasyprint -platformdirs==4.5.0 \ - --hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \ - --hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3 +platformdirs==4.5.1 \ + --hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \ + --hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31 # via virtualenv -pre-commit==4.3.0 \ - --hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \ - --hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16 +pre-commit==4.5.1 \ + --hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \ + --hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61 # via -r .github/scripts/requirements_dev.in -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pydyf==0.11.0 \ - --hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \ - --hash=sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64 +pydyf==0.12.1 \ + --hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \ + --hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095 # via weasyprint pyphen==0.17.2 \ --hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \ @@ -442,9 +452,9 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via pre-commit -tinycss2==1.4.0 \ - --hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \ - --hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289 +tinycss2==1.5.1 \ + --hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \ + --hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957 # via # cssselect2 # weasyprint @@ -452,17 +462,17 @@ tinyhtml5==2.0.0 \ --hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \ --hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e # via weasyprint -unoserver==3.4 \ - --hash=sha256:3dcf2204013def1d1ddd3671f38b11346bdf349fef9728277462666a8a634419 \ - --hash=sha256:64c24d33d4f65d680a2d9f676518cb28e7fd6c1f9d9a745c33e4a4cb59afdfcd +unoserver==3.6 \ + --hash=sha256:25c360fa194396a89cb79b4edd2735f8e4f0fd8531e59db3952114585bd7df05 \ + --hash=sha256:e446bcb3638c51880f002aaeecab1cf74dfa9df81035f027f7ff2e081b6d7015 # via -r .github/scripts/requirements_dev.in -virtualenv==20.35.4 \ - --hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \ - --hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b +virtualenv==20.36.1 \ + --hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \ + --hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba # via pre-commit -weasyprint==66.0 \ - --hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \ - --hash=sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40 +weasyprint==68.0 \ + --hash=sha256:447f40898b747cb44ac31a5d493d512e7441fd56e13f63744c099383bbf9cda9 \ + --hash=sha256:c2cb40c71b50837c5971f00171c9e4078e8c9912dd7c217f3e90e068f11e8aa1 # via -r .github/scripts/requirements_dev.in webencodings==0.5.1 \ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ @@ -471,86 +481,27 @@ webencodings==0.5.1 \ # cssselect2 # tinycss2 # tinyhtml5 -zopfli==0.2.3.post1 \ - --hash=sha256:0aa5f90d6298bda02a95bc8dc8c3c19004d5a4e44bda00b67ca7431d857b4b54 \ - --hash=sha256:0cc20b02a9531559945324c38302fd4ba763311632d0ec8a1a0aa9c10ea363e6 \ - --hash=sha256:1d8cc06605519e82b16df090e17cb3990d1158861b2872c3117f1168777b81e4 \ - --hash=sha256:1f990634fd5c5c8ced8edddd8bd45fab565123b4194d6841e01811292650acae \ - --hash=sha256:2345e713260a350bea0b01a816a469ea356bc2d63d009a0d777691ecbbcf7493 \ - --hash=sha256:2768c877f76c8a0e7519b1c86c93757f3c01492ddde55751e9988afb7eff64e1 \ - --hash=sha256:29ea74e72ffa6e291b8c6f2504ce6c146b4fe990c724c1450eb8e4c27fd31431 \ - --hash=sha256:34a99592f3d9eb6f737616b5bd74b48a589fdb3cb59a01a50d636ea81d6af272 \ - --hash=sha256:3654bfc927bc478b1c3f3ff5056ed7b20a1a37fa108ca503256d0a699c03bbb1 \ - --hash=sha256:3657e416ffb8f31d9d3424af12122bb251befae109f2e271d87d825c92fc5b7b \ - --hash=sha256:37d011e92f7b9622742c905fdbed9920a1d0361df84142807ea2a528419dea7f \ - --hash=sha256:3827170de28faf144992d3d4dcf8f3998fe3c8a6a6f4a08f1d42c2ec6119d2bb \ - --hash=sha256:39e576f93576c5c223b41d9c780bbb91fd6db4babf3223d2a4fe7bf568e2b5a8 \ - --hash=sha256:3a89277ed5f8c0fb2d0b46d669aa0633123aa7381f1f6118c12f15e0fb48f8ca \ - --hash=sha256:3c163911f8bad94b3e1db0a572e7c28ba681a0c91d0002ea1e4fa9264c21ef17 \ - --hash=sha256:3f0197b6aa6eb3086ae9e66d6dd86c4d502b6c68b0ec490496348ae8c05ecaef \ - --hash=sha256:48dba9251060289101343110ab47c0756f66f809bb4d1ddbb6d5c7e7752115c5 \ - --hash=sha256:4915a41375bdee4db749ecd07d985a0486eb688a6619f713b7bf6fbfd145e960 \ - --hash=sha256:4c1226a7e2c7105ac31503a9bb97454743f55d88164d6d46bc138051b77f609b \ - --hash=sha256:4e50ffac74842c1c1018b9b73875a0d0a877c066ab06bf7cccbaa84af97e754f \ - --hash=sha256:518f1f4ed35dd69ce06b552f84e6d081f07c552b4c661c5312d950a0b764a58a \ - --hash=sha256:5aad740b4d4fcbaaae4887823925166ffd062db3b248b3f432198fc287381d1a \ - --hash=sha256:5f272186e03ad55e7af09ab78055535c201b1a0bcc2944edb1768298d9c483a4 \ - --hash=sha256:5fcfc0dc2761e4fcc15ad5d273b4d58c2e8e059d3214a7390d4d3c8e2aee644e \ - --hash=sha256:60db20f06c3d4c5934b16cfa62a2cc5c3f0686bffe0071ed7804d3c31ab1a04e \ - --hash=sha256:615a8ac9dda265e9cc38b2a76c3142e4a9f30fea4a79c85f670850783bc6feb4 \ - --hash=sha256:6482db9876c68faac2d20a96b566ffbf65ddaadd97b222e4e73641f4f8722fc4 \ - --hash=sha256:6617fb10f9e4393b331941861d73afb119cd847e88e4974bdbe8068ceef3f73f \ - --hash=sha256:676919fba7311125244eb0c4393679ac5fe856e5864a15d122bd815205369fa0 \ - --hash=sha256:6c2d2bc8129707e34c51f9352c4636ca313b52350bbb7e04637c46c1818a2a70 \ - --hash=sha256:71390dbd3fbf6ebea9a5d85ffed8c26ee1453ee09248e9b88486e30e0397b775 \ - --hash=sha256:716cdbfc57bfd3d3e31a58e6246e8190e6849b7dbb7c4ce39ef8bbf0edb8f6d5 \ - --hash=sha256:75a26a2307b10745a83b660c404416e984ee6fca515ec7f0765f69af3ce08072 \ - --hash=sha256:7be5cc6732eb7b4df17305d8a7b293223f934a31783a874a01164703bc1be6cd \ - --hash=sha256:7cce242b5df12b2b172489daf19c32e5577dd2fac659eb4b17f6a6efb446fd5c \ - --hash=sha256:81c341d9bb87a6dbbb0d45d6e272aca80c7c97b4b210f9b6e233bf8b87242f29 \ - --hash=sha256:89899641d4de97dbad8e0cde690040d078b6aea04066dacaab98e0b5a23573f2 \ - --hash=sha256:8d5ab297d660b75c159190ce6d73035502310e40fd35170aed7d1a1aea7ddd65 \ - --hash=sha256:8fbe5bcf10d01aab3513550f284c09fef32f342b36f56bfae2120a9c4d12c130 \ - --hash=sha256:91a2327a4d7e77471fa4fbb26991c6de4a738c6fc6a33e09bb25f56a870a4b7b \ - --hash=sha256:95a260cafd56b8fffa679918937401c80bb38e1681c448b988022e4c3610965d \ - --hash=sha256:96484dc0f48be1c5d7ae9f38ed1ce41e3675fd506b27c11a6607f14b49101e99 \ - --hash=sha256:9a6aec38a989bad7ddd1ef53f1265699e49e294d08231b5313d61293f3cd6237 \ - --hash=sha256:9ba214f4f45bec195ee8559651154d3ac2932470b9d91c5715fc29c013349f8c \ - --hash=sha256:9f4a7ec2770e6af05f5a02733fd3900f30a9cd58e5d6d3727e14c5bcd6e7d587 \ - --hash=sha256:a1cf720896d2ce998bc8e051d4b4ce0d8bec007aab6243102e8e1d22a0b2fb3f \ - --hash=sha256:a241a68581d34d67b40c425cce3d1fd211c092f99d9250947824ccba9f491949 \ - --hash=sha256:a53b18797cdef27e019db595d66c4b077325afe2fd62145953275f53d84ce40c \ - --hash=sha256:a82fc2dbebe6eb908b9c665e71496f8525c1bc4d2e3a7a7722ef2b128b6227c8 \ - --hash=sha256:a86eb88e06bd87e1fff31dac878965c26b0c26db59ddcf78bb0379a954b120de \ - --hash=sha256:aa588b21044f8a74e423d8c8a4c7fc9988501878aacced793467010039c50734 \ - --hash=sha256:b05296e8bc88c92e2b21e0a9bae4740c1551ee613c1d93a51fd28a7a0b2b6fbb \ - --hash=sha256:b0ec13f352ea5ae0fc91f98a48540512eed0767d0ec4f7f3cb92d92797983d18 \ - --hash=sha256:b3df42f52502438ee973042cc551877d24619fa1cd38ef7b7e9ac74200daca8b \ - --hash=sha256:b78008a69300d929ca2efeffec951b64a312e9a811e265ea4a907ab546d79fa6 \ - --hash=sha256:b9026a21b6d41eb0e2e63f5bc1242c3fcc43ecb770963cda99a4307863dac12e \ - --hash=sha256:bbe429fc50686bb2a2608a30843e36fbaa123462a5284f136c7d9e0145220bfd \ - --hash=sha256:bfa1eb759e07d8b7aa7a310a2bc535e127ee70addf90dc8d4b946b593c3e51a8 \ - --hash=sha256:c1e0ed5d84ffa2d677cc9582fc01e61dab2e7ef8b8996e055f0a76167b1b94df \ - --hash=sha256:c4278d1873ce6e803e5d4f8d702fd3026bd67fca744aa98881324d1157ddf748 \ - --hash=sha256:cac2b37ab21c2b36a10b685b1893ebd6b0f83ae26004838ac817680881576567 \ - --hash=sha256:cbe6df25807227519debd1a57ab236f5f6bad441500e85b13903e51f93a43214 \ - --hash=sha256:cd2c002f160502608dcc822ed2441a0f4509c52e86fcfd1a09e937278ed1ca14 \ - --hash=sha256:e0137dd64a493ba6a4be37405cfd6febe650a98cc1e9dca8f6b8c63b1db11b41 \ - --hash=sha256:e63d558847166543c2c9789e6f985400a520b7eacc4b99181668b2c3aeadd352 \ - --hash=sha256:eb45a34f23da4f8bc712b6376ca5396914b0b7c09adbb001dad964eb7f3132f8 \ - --hash=sha256:ecb7572df5372abce8073df078207d9d1749f20b8b136089916a4a0868d56051 \ - --hash=sha256:f12000a6accdd4bf0a3fa6eaa1b1c7a7bc80af0a2edf3f89d770d3dcce1d0e22 \ - --hash=sha256:f7d69c1a7168ad0e9cb864e8663acb232986a0c9c9cb9801f56bf6214f53a54d \ - --hash=sha256:f815fcc2b2a457977724bad97fb4854022980f51ce7b136925e336b530545ae1 \ - --hash=sha256:fc39f5c27f962ec8660d8d20c24762431131b5d8c672b44b0a54cf2b5bcde9b9 +zopfli==0.4.0 \ + --hash=sha256:03181d48e719fcb6cf8340189c61e8f9883d8bbbdf76bf5212a74457f7d083c1 \ + --hash=sha256:18b5f1570f64d4988482e4466f10ef5f2a30f687c19ad62a64560f2152dc89eb \ + --hash=sha256:25e4863b8dc30e5d5309f87c106b0b7d3da4ed0e340b8a52b36d4471e797589f \ + --hash=sha256:7d66337be6d5613dec55213e9ac28f378c41e2cc04fbad4a10748e4df774ca85 \ + --hash=sha256:9097e8e1dfdb7f5aea5464e469946857e80502b6d29ba1b232450916bd4a74d1 \ + --hash=sha256:a8ee992b2549e090cd3f0178bf606dd41a29e0613a04cdf5054224662c72dce6 \ + --hash=sha256:b72a010d205d00b2855acc2302772067362f9ab5a012e3550662aec60d28e6b3 \ + --hash=sha256:b8bdb41fbfdc4738b7bdc09ed7c1e951579fae192391a5e694d59bb186cdbec7 \ + --hash=sha256:c3ba02a9a6ca90481d2b2f68bab038b310d63a1e3b5ae305e95a6599787ed941 \ + --hash=sha256:d1b98ad47c434ef213444a03ef2f826eeec100144d64f6a57504b9893d3931ce \ + --hash=sha256:f67d04280065e24cb9a4174cb6b3d1f763687f8cb2963aa135ad8f57c6995f5a \ + --hash=sha256:f94e4dd7d76b4fe9f5d9229372be20d7f786164eea5152d1af1c34298c3d5975 # via fonttools # The following packages are considered to be unsafe in a requirements file: -pip==25.3 \ - --hash=sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343 \ - --hash=sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd +pip==26.0 \ + --hash=sha256:3ce220a0a17915972fbf1ab451baae1521c4539e778b28127efa79b974aff0fa \ + --hash=sha256:98436feffb9e31bc9339cf369fd55d3331b1580b6a6f1173bacacddcf9c34754 # via -r .github/scripts/requirements_dev.in -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 # via -r .github/scripts/requirements_dev.in diff --git a/.github/scripts/requirements_pre_commit.txt b/.github/scripts/requirements_pre_commit.txt index b98227afb1..9edd189c36 100644 --- a/.github/scripts/requirements_pre_commit.txt +++ b/.github/scripts/requirements_pre_commit.txt @@ -12,25 +12,25 @@ distlib==0.4.0 \ --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d # via virtualenv -filelock==3.20.0 \ - --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ - --hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4 +filelock==3.20.3 \ + --hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \ + --hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1 # via virtualenv -identify==2.6.15 \ - --hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \ - --hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf +identify==2.6.16 \ + --hash=sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 \ + --hash=sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980 # via pre-commit -nodeenv==1.9.1 \ - --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ - --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 +nodeenv==1.10.0 \ + --hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \ + --hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb # via pre-commit -platformdirs==4.5.0 \ - --hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \ - --hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3 +platformdirs==4.5.1 \ + --hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \ + --hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31 # via virtualenv -pre-commit==4.5.0 \ - --hash=sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1 \ - --hash=sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b +pre-commit==4.5.1 \ + --hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \ + --hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61 # via -r .github/scripts/requirements_pre_commit.in pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ @@ -107,7 +107,7 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via pre-commit -virtualenv==20.35.4 \ - --hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \ - --hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b +virtualenv==20.36.1 \ + --hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \ + --hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba # via pre-commit diff --git a/.github/scripts/requirements_sync_readme.txt b/.github/scripts/requirements_sync_readme.txt index a5cf36a683..b68152361d 100644 --- a/.github/scripts/requirements_sync_readme.txt +++ b/.github/scripts/requirements_sync_readme.txt @@ -8,7 +8,7 @@ tomli-w==1.2.0 \ --hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \ --hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021 # via -r .github/scripts/requirements_sync_readme.in -tomlkit==0.13.3 \ - --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ - --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 +tomlkit==0.14.0 \ + --hash=sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680 \ + --hash=sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064 # via -r .github/scripts/requirements_sync_readme.in diff --git a/testing/cucumber/requirements.txt b/testing/cucumber/requirements.txt index 00ccc37c0f..1f952a6651 100644 --- a/testing/cucumber/requirements.txt +++ b/testing/cucumber/requirements.txt @@ -7,10 +7,10 @@ behave==1.3.3 \ --hash=sha256:2b8f4b64ed2ea756a5a2a73e23defc1c4631e9e724c499e46661778453ebaf51 \ --hash=sha256:89bdb62af8fb9f147ce245736a5de69f025e5edfb66f1fbe16c5007493f842c0 - # via -r requirements.in -certifi==2025.11.12 \ - --hash=sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b \ - --hash=sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316 + # via -r testing/cucumber/requirements.in +certifi==2026.1.4 \ + --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ + --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 # via requests charset-normalizer==3.4.4 \ --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ @@ -133,13 +133,13 @@ colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via behave -cucumber-expressions==18.0.1 \ - --hash=sha256:86230d503cdda7ef35a1f2072a882d7d57c740aa4c163c82b07f039b6bc60c42 \ - --hash=sha256:86ce41bf28ee520408416f38022e5a083d815edf04a0bd1dae46d474ca597c60 +cucumber-expressions==19.0.0 \ + --hash=sha256:8eb5ae46dd03dd37fec1163ace1510529501d7d1868ff372c1ab2cd5aa4543a8 \ + --hash=sha256:f452e6c73258c1677043ad67ad5f538c87284d6b502004720510fb6b7452d9c5 # via behave -cucumber-tag-expressions==8.1.0 \ - --hash=sha256:1de26f183b1e8748e881189edd4bcdf4a80d7ed1011ad7b38cf141fcdcc51094 \ - --hash=sha256:acc56dd19b7bd0b931fc7b124ebbb6737def0775be41186ace7f5e566338ce7d +cucumber-tag-expressions==9.0.0 \ + --hash=sha256:36f3eacf49ad24feeb60218db4c51ab114853b3f022f4f3ad790c32b7597faee \ + --hash=sha256:731302c12bd602309596b35e733c1021b517d4948329803c23ca026e26ef4e99 # via behave idna==3.11 \ --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ @@ -155,98 +155,98 @@ parse-type==0.6.6 \ --hash=sha256:3ca79bbe71e170dfccc8ec6c341edfd1c2a0fc1e5cfd18330f93af938de2348c \ --hash=sha256:513a3784104839770d690e04339a8b4d33439fcd5dd99f2e4580f9fc1097bfb2 # via behave -pillow==12.0.0 \ - --hash=sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643 \ - --hash=sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e \ - --hash=sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e \ - --hash=sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc \ - --hash=sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642 \ - --hash=sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6 \ - --hash=sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1 \ - --hash=sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b \ - --hash=sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399 \ - --hash=sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba \ - --hash=sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad \ - --hash=sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47 \ - --hash=sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739 \ - --hash=sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b \ - --hash=sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f \ - --hash=sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10 \ - --hash=sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52 \ - --hash=sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d \ - --hash=sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b \ - --hash=sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a \ - --hash=sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9 \ - --hash=sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d \ - --hash=sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098 \ - --hash=sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905 \ - --hash=sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b \ - --hash=sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3 \ - --hash=sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371 \ - --hash=sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953 \ - --hash=sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01 \ - --hash=sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca \ - --hash=sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e \ - --hash=sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7 \ - --hash=sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27 \ - --hash=sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082 \ - --hash=sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e \ - --hash=sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d \ - --hash=sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8 \ - --hash=sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a \ - --hash=sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad \ - --hash=sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3 \ - --hash=sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a \ - --hash=sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d \ - --hash=sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353 \ - --hash=sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee \ - --hash=sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b \ - --hash=sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b \ - --hash=sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a \ - --hash=sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7 \ - --hash=sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef \ - --hash=sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a \ - --hash=sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a \ - --hash=sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257 \ - --hash=sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07 \ - --hash=sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4 \ - --hash=sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c \ - --hash=sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c \ - --hash=sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4 \ - --hash=sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe \ - --hash=sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8 \ - --hash=sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5 \ - --hash=sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6 \ - --hash=sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e \ - --hash=sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8 \ - --hash=sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e \ - --hash=sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275 \ - --hash=sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3 \ - --hash=sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76 \ - --hash=sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227 \ - --hash=sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9 \ - --hash=sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5 \ - --hash=sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79 \ - --hash=sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca \ - --hash=sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa \ - --hash=sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b \ - --hash=sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e \ - --hash=sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197 \ - --hash=sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab \ - --hash=sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79 \ - --hash=sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2 \ - --hash=sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363 \ - --hash=sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0 \ - --hash=sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e \ - --hash=sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782 \ - --hash=sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925 \ - --hash=sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0 \ - --hash=sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b \ - --hash=sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced \ - --hash=sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c \ - --hash=sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344 \ - --hash=sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9 \ - --hash=sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1 +pillow==12.1.0 \ + --hash=sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d \ + --hash=sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc \ + --hash=sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84 \ + --hash=sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de \ + --hash=sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0 \ + --hash=sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef \ + --hash=sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4 \ + --hash=sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82 \ + --hash=sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9 \ + --hash=sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030 \ + --hash=sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0 \ + --hash=sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18 \ + --hash=sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a \ + --hash=sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef \ + --hash=sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b \ + --hash=sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6 \ + --hash=sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179 \ + --hash=sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e \ + --hash=sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72 \ + --hash=sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64 \ + --hash=sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451 \ + --hash=sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd \ + --hash=sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924 \ + --hash=sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616 \ + --hash=sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a \ + --hash=sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94 \ + --hash=sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc \ + --hash=sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8 \ + --hash=sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9 \ + --hash=sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91 \ + --hash=sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a \ + --hash=sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c \ + --hash=sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670 \ + --hash=sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea \ + --hash=sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91 \ + --hash=sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c \ + --hash=sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc \ + --hash=sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0 \ + --hash=sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b \ + --hash=sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65 \ + --hash=sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661 \ + --hash=sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19 \ + --hash=sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1 \ + --hash=sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0 \ + --hash=sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e \ + --hash=sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75 \ + --hash=sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4 \ + --hash=sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8 \ + --hash=sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd \ + --hash=sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7 \ + --hash=sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61 \ + --hash=sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51 \ + --hash=sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551 \ + --hash=sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45 \ + --hash=sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1 \ + --hash=sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644 \ + --hash=sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796 \ + --hash=sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587 \ + --hash=sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304 \ + --hash=sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b \ + --hash=sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8 \ + --hash=sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17 \ + --hash=sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171 \ + --hash=sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3 \ + --hash=sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7 \ + --hash=sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988 \ + --hash=sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a \ + --hash=sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0 \ + --hash=sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c \ + --hash=sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2 \ + --hash=sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14 \ + --hash=sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5 \ + --hash=sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a \ + --hash=sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377 \ + --hash=sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0 \ + --hash=sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5 \ + --hash=sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b \ + --hash=sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d \ + --hash=sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac \ + --hash=sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c \ + --hash=sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554 \ + --hash=sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643 \ + --hash=sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13 \ + --hash=sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09 \ + --hash=sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208 \ + --hash=sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda \ + --hash=sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea \ + --hash=sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e \ + --hash=sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0 \ + --hash=sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831 \ + --hash=sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd # via reportlab pycryptodome==3.23.0 \ --hash=sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4 \ @@ -290,19 +290,19 @@ pycryptodome==3.23.0 \ --hash=sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa \ --hash=sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be \ --hash=sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7 - # via -r requirements.in + # via -r testing/cucumber/requirements.in pypdf==6.6.2 \ --hash=sha256:0a3ea3b3303982333404e22d8f75d7b3144f9cf4b2970b96856391a516f9f016 \ --hash=sha256:44c0c9811cfb3b83b28f1c3d054531d5b8b81abaedee0d8cb403650d023832ba - # via -r requirements.in + # via -r testing/cucumber/requirements.in reportlab==4.4.9 \ --hash=sha256:68e2d103ae8041a37714e8896ec9b79a1c1e911d68c3bd2ea17546568cf17bfd \ --hash=sha256:7cf487764294ee791a4781f5a157bebce262a666ae4bbb87786760a9676c9378 - # via -r requirements.in + # via -r testing/cucumber/requirements.in requests==2.32.5 \ --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf - # via -r requirements.in + # via -r testing/cucumber/requirements.in six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 From 2ae413c5eaf39bf3e9ce471ad866c3c64d4363f4 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 31 Jan 2026 20:28:59 +0000 Subject: [PATCH 3/6] Stop attempting to refresh Spring tokens in desktop (#5610) # Description of Changes --- ## 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) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### 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. --------- Co-authored-by: James Brunton --- .../api/AuthControllerLoginTest.java | 1 + frontend/src-tauri/src/commands/auth.rs | 114 ++++++++++++++ frontend/src-tauri/src/commands/mod.rs | 3 + frontend/src-tauri/src/lib.rs | 6 + .../src/desktop/services/apiClientSetup.ts | 96 +++++++----- frontend/src/desktop/services/authService.ts | 143 ++++++++++++++++-- 6 files changed, 309 insertions(+), 54 deletions(-) diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/security/controller/api/AuthControllerLoginTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/security/controller/api/AuthControllerLoginTest.java index 29dabe152a..86bcf50e8c 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/security/controller/api/AuthControllerLoginTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/security/controller/api/AuthControllerLoginTest.java @@ -182,6 +182,7 @@ class AuthControllerLoginTest { mockMvc.perform(post("/api/v1/auth/refresh")) .andExpect(status().isOk()) + .andExpect(jsonPath("$.user").exists()) .andExpect(jsonPath("$.session.access_token").value("new-token")) .andExpect(jsonPath("$.session.expires_in").value(3600)); } diff --git a/frontend/src-tauri/src/commands/auth.rs b/frontend/src-tauri/src/commands/auth.rs index 43fa9128ca..65f8f6623d 100644 --- a/frontend/src-tauri/src/commands/auth.rs +++ b/frontend/src-tauri/src/commands/auth.rs @@ -11,8 +11,11 @@ use rand::distributions::Alphanumeric; const STORE_FILE: &str = "connection.json"; const USER_INFO_KEY: &str = "user_info"; +const TOKENS_STORE_FILE: &str = "tokens.json"; +const REFRESH_TOKEN_STORE_KEY: &str = "refresh_token"; const KEYRING_SERVICE: &str = "stirling-pdf"; const KEYRING_TOKEN_KEY: &str = "auth-token"; +const KEYRING_REFRESH_TOKEN_KEY: &str = "refresh-token"; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UserInfo { @@ -31,6 +34,11 @@ fn get_keyring_entry() -> Result { Ok(entry) } +fn get_refresh_token_keyring_entry() -> Result { + Entry::new(KEYRING_SERVICE, KEYRING_REFRESH_TOKEN_KEY) + .map_err(|e| format!("Failed to access keyring: {}", e)) +} + #[tauri::command] pub async fn save_auth_token(_app_handle: AppHandle, token: String) -> Result<(), String> { let trimmed = token.trim(); @@ -101,6 +109,112 @@ pub async fn clear_auth_token(_app_handle: AppHandle) -> Result<(), String> { } } +#[tauri::command] +pub async fn save_refresh_token(app_handle: AppHandle, token: String) -> Result<(), String> { + log::info!("Saving refresh token - trying keyring first"); + + let entry = get_refresh_token_keyring_entry()?; + + // Try keyring (works in production with code signing) + match entry.set_password(&token) { + Ok(_) => { + // Verify it persists (fails in unsigned dev builds) + match entry.get_password() { + Ok(saved) if saved == token => { + log::info!("✅ Refresh token saved to keyring (production mode)"); + return Ok(()); + } + _ => { + log::info!("Keyring doesn't persist - using Tauri Store fallback (dev mode)"); + } + } + } + Err(e) => { + log::info!("Keyring failed: {} - using Tauri Store fallback", e); + } + } + + // Fallback to Tauri Store (dev mode without code signing) + let store = app_handle + .store(TOKENS_STORE_FILE) + .map_err(|e| format!("Failed to access tokens store: {}", e))?; + + store.set( + REFRESH_TOKEN_STORE_KEY, + serde_json::to_value(&token) + .map_err(|e| format!("Failed to serialize token: {}", e))?, + ); + + store + .save() + .map_err(|e| format!("Failed to save tokens store: {}", e))?; + + log::info!("✅ Refresh token saved to Tauri Store (fallback)"); + Ok(()) +} + +#[tauri::command] +pub async fn get_refresh_token(app_handle: AppHandle) -> Result, String> { + // Try keyring first (production) + let entry = get_refresh_token_keyring_entry()?; + match entry.get_password() { + Ok(token) => { + log::info!("✅ Refresh token retrieved from keyring"); + return Ok(Some(token)); + } + Err(keyring::Error::NoEntry) => { + log::debug!("No token in keyring, trying Tauri Store"); + } + Err(e) => { + log::warn!("Keyring error: {} - trying Tauri Store", e); + } + } + + // Fallback to Tauri Store (dev) + let store = app_handle + .store(TOKENS_STORE_FILE) + .map_err(|e| format!("Failed to access tokens store: {}", e))?; + + let token: Option = store + .get(REFRESH_TOKEN_STORE_KEY) + .and_then(|v| serde_json::from_value(v.clone()).ok()); + + if token.is_some() { + log::info!("✅ Refresh token retrieved from Tauri Store"); + } else { + log::info!("No refresh token found"); + } + + Ok(token) +} + +#[tauri::command] +pub async fn clear_refresh_token(app_handle: AppHandle) -> Result<(), String> { + log::info!("Clearing refresh token from all storage"); + + // Clear from keyring + let entry = get_refresh_token_keyring_entry()?; + match entry.delete_credential() { + Ok(_) => log::info!("Cleared from keyring"), + Err(keyring::Error::NoEntry) => log::debug!("Not in keyring"), + Err(e) => log::warn!("Keyring clear error: {}", e), + } + + // Clear from Tauri Store + let store = app_handle + .store(TOKENS_STORE_FILE) + .map_err(|e| format!("Failed to access tokens store: {}", e))?; + + store.delete(REFRESH_TOKEN_STORE_KEY); + + store + .save() + .map_err(|e| format!("Failed to save tokens store: {}", e))?; + + log::info!("✅ Refresh token cleared"); + Ok(()) +} + #[tauri::command] pub async fn save_user_info( app_handle: AppHandle, diff --git a/frontend/src-tauri/src/commands/mod.rs b/frontend/src-tauri/src/commands/mod.rs index 6e058a5be9..30904bbd90 100644 --- a/frontend/src-tauri/src/commands/mod.rs +++ b/frontend/src-tauri/src/commands/mod.rs @@ -14,11 +14,14 @@ pub use connection::{ }; pub use auth::{ clear_auth_token, + clear_refresh_token, clear_user_info, get_auth_token, + get_refresh_token, get_user_info, login, save_auth_token, + save_refresh_token, save_user_info, start_oauth_login, }; diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 61cbd6d435..ad49ea16e4 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -9,17 +9,20 @@ use commands::{ cleanup_backend, clear_auth_token, clear_opened_files, + clear_refresh_token, clear_user_info, is_default_pdf_handler, get_auth_token, get_backend_port, get_connection_config, get_opened_files, + get_refresh_token, get_user_info, is_first_launch, login, reset_setup_completion, save_auth_token, + save_refresh_token, save_user_info, set_connection_mode, set_as_default_pdf_handler, @@ -143,6 +146,9 @@ pub fn run() { save_auth_token, get_auth_token, clear_auth_token, + save_refresh_token, + get_refresh_token, + clear_refresh_token, save_user_info, get_user_info, clear_user_info, diff --git a/frontend/src/desktop/services/apiClientSetup.ts b/frontend/src/desktop/services/apiClientSetup.ts index a61f554dc5..d70bc2afa1 100644 --- a/frontend/src/desktop/services/apiClientSetup.ts +++ b/frontend/src/desktop/services/apiClientSetup.ts @@ -6,6 +6,7 @@ import { createBackendNotReadyError } from '@app/constants/backendErrors'; import { operationRouter } from '@app/services/operationRouter'; import { authService } from '@app/services/authService'; import { connectionModeService } from '@app/services/connectionModeService'; +import { STIRLING_SAAS_URL } from '@app/constants/connection'; import i18n from '@app/i18n'; const BACKEND_TOAST_COOLDOWN_MS = 4000; @@ -34,37 +35,40 @@ export function setupApiInterceptors(client: AxiosInstance): void { async (config: InternalAxiosRequestConfig) => { const extendedConfig = config as ExtendedRequestConfig; - // Get the operation name from config if provided - const operation = extendedConfig.operationName; + try { + // Get the appropriate base URL for this request + const baseUrl = await operationRouter.getBaseUrl(extendedConfig.url); - // Get the appropriate base URL for this operation - const baseUrl = await operationRouter.getBaseUrl(operation); - - // Build the full URL - if (extendedConfig.url && !extendedConfig.url.startsWith('http')) { - extendedConfig.url = `${baseUrl}${extendedConfig.url}`; - } - - localStorage.setItem('server_url', baseUrl); - - // Debug logging - console.debug(`[apiClientSetup] Request to: ${extendedConfig.url}`); - - // Add auth token for remote requests and enable credentials - const isRemote = await operationRouter.isSelfHostedMode(); - if (isRemote) { - // Self-hosted mode: enable credentials for session management - extendedConfig.withCredentials = true; - - const token = await authService.getAuthToken(); - if (token) { - extendedConfig.headers.Authorization = `Bearer ${token}`; - } else { - console.warn('[apiClientSetup] Self-hosted mode but no auth token available'); + // Build the full URL + if (extendedConfig.url && !extendedConfig.url.startsWith('http')) { + extendedConfig.url = `${baseUrl}${extendedConfig.url}`; } - } else { - // SaaS mode: disable credentials (security disabled on local backend) - extendedConfig.withCredentials = false; + + localStorage.setItem('server_url', baseUrl); + + // Debug logging + console.debug(`[apiClientSetup] Request to: ${extendedConfig.url}`); + + // Add auth token for remote requests and enable credentials + const isRemote = await operationRouter.isSelfHostedMode(); + if (isRemote) { + // Self-hosted mode: enable credentials for session management + extendedConfig.withCredentials = true; + + const token = await authService.getAuthToken(); + if (token) { + extendedConfig.headers.Authorization = `Bearer ${token}`; + } else { + console.warn('[apiClientSetup] Self-hosted mode but no auth token available'); + } + } else { + // SaaS mode: disable credentials (security disabled on local backend) + extendedConfig.withCredentials = false; + } + } catch (error) { + console.error('[apiClientSetup] Error in request interceptor:', error); + // Continue with request even if routing/auth logic fails + // This ensures requests aren't blocked by interceptor errors } // Backend readiness check (for local backend) @@ -108,23 +112,37 @@ export function setupApiInterceptors(client: AxiosInstance): void { } originalRequest._retry = true; + console.debug(`[apiClientSetup] 401 error, attempting token refresh for: ${originalRequest.url}`); + const isRemote = await operationRouter.isSelfHostedMode(); + let refreshed = false; + if (isRemote) { + // Self-hosted mode: use Spring Boot refresh endpoint const serverConfig = await connectionModeService.getServerConfig(); if (serverConfig) { - const refreshed = await authService.refreshToken(serverConfig.url); - if (refreshed) { - // Retry the original request with new token - const token = await authService.getAuthToken(); - if (token) { - originalRequest.headers.Authorization = `Bearer ${token}`; - } - return client(originalRequest); - } + refreshed = await authService.refreshToken(serverConfig.url); } + } else { + // SaaS mode: use Supabase refresh endpoint + refreshed = await authService.refreshSupabaseToken(STIRLING_SAAS_URL); } - // Refresh failed or not in remote mode - user needs to login again + if (refreshed) { + // Retry the original request with new token + const token = await authService.getAuthToken(); + console.debug(`[apiClientSetup] Token refreshed, retrying request to: ${originalRequest.url}`); + + if (token) { + originalRequest.headers.Authorization = `Bearer ${token}`; + } else { + console.error(`[apiClientSetup] No token available after successful refresh!`); + } + + return client.request(originalRequest); + } + + // Refresh failed - user needs to login again alert({ alertType: 'error', title: i18n.t('auth.sessionExpired', 'Session Expired'), diff --git a/frontend/src/desktop/services/authService.ts b/frontend/src/desktop/services/authService.ts index 41adf954e6..b3f3da9114 100644 --- a/frontend/src/desktop/services/authService.ts +++ b/frontend/src/desktop/services/authService.ts @@ -40,6 +40,7 @@ export class AuthService { private userInfo: UserInfo | null = null; private cachedToken: string | null = null; private authListeners = new Set<(status: AuthStatus, userInfo: UserInfo | null) => void>(); + private refreshPromise: Promise | null = null; static getInstance(): AuthService { if (!AuthService.instance) { @@ -51,15 +52,17 @@ export class AuthService { /** * Save token to all storage locations and notify listeners */ - private async saveTokenEverywhere(token: string): Promise { + private async saveTokenEverywhere(token: string, refreshToken?: string | null): Promise { // Validate token before caching if (!token || token.trim().length === 0) { console.warn('[Desktop AuthService] Attempted to save invalid/empty token'); throw new Error('Invalid token'); } + console.log(`[Desktop AuthService] Saving token (length: ${token.length})`); + + // Save access token to Tauri secure store (primary) try { - // Save to Tauri store await invoke('save_auth_token', { token }); console.log('[Desktop AuthService] ✅ Token saved to Tauri store'); } catch (error) { @@ -67,8 +70,8 @@ export class AuthService { // Don't throw - we can still use localStorage } + // Sync to localStorage for web layer (fallback) try { - // Sync to localStorage for web layer localStorage.setItem('stirling_jwt', token); console.log('[Desktop AuthService] ✅ Token saved to localStorage'); } catch (error) { @@ -79,6 +82,19 @@ export class AuthService { this.cachedToken = token; console.log('[Desktop AuthService] ✅ Token cached in memory'); + // Save refresh token if provided (keyring with Tauri Store fallback) + if (refreshToken) { + console.log('[Desktop AuthService] Saving refresh token to secure storage...'); + try { + await invoke('save_refresh_token', { token: refreshToken }); + console.log('[Desktop AuthService] ✅ Refresh token saved to secure storage'); + // Only remove from localStorage after successful save + localStorage.removeItem('stirling_refresh_token'); + } catch (error) { + console.error('[Desktop AuthService] ❌ Failed to save refresh token:', error); + } + } + // Notify other parts of the system window.dispatchEvent(new CustomEvent('jwt-available')); console.log('[Desktop AuthService] Dispatched jwt-available event'); @@ -112,6 +128,19 @@ export class AuthService { return localStorageToken; } + /** + * Get refresh token from secure storage (keyring or Tauri Store fallback) + */ + private async getRefreshToken(): Promise { + const token = await invoke('get_refresh_token'); + if (token) { + console.log('[Desktop AuthService] ✅ Refresh token retrieved from secure storage'); + } else { + console.log('[Desktop AuthService] No refresh token in secure storage'); + } + return token; + } + /** * Clear token from all storage locations */ @@ -120,20 +149,28 @@ export class AuthService { this.cachedToken = null; console.log('[Desktop AuthService] Cache invalidated'); - // Best effort: clear Tauri keyring + // Best effort: clear Tauri keyring (both access and refresh tokens) try { await invoke('clear_auth_token'); - console.log('[Desktop AuthService] Cleared Tauri keyring token'); + console.log('[Desktop AuthService] Cleared Tauri keyring access token'); } catch (error) { - console.warn('[Desktop AuthService] Failed to clear Tauri keyring token', error); + console.warn('[Desktop AuthService] Failed to clear Tauri keyring access token', error); + } + + try { + await invoke('clear_refresh_token'); + console.log('[Desktop AuthService] Cleared Tauri keyring refresh token'); + } catch (error) { + console.warn('[Desktop AuthService] Failed to clear Tauri keyring refresh token', error); } // Best effort: clear web storage try { localStorage.removeItem('stirling_jwt'); - console.log('[Desktop AuthService] Cleared localStorage token'); + localStorage.removeItem('stirling_refresh_token'); + console.log('[Desktop AuthService] Cleared localStorage tokens'); } catch (error) { - console.warn('[Desktop AuthService] Failed to clear localStorage token', error); + console.warn('[Desktop AuthService] Failed to clear localStorage tokens', error); } } @@ -469,6 +506,21 @@ export class AuthService { } async refreshToken(serverUrl: string): Promise { + // Prevent concurrent refresh attempts - reuse in-flight refresh + if (this.refreshPromise) { + console.log('[Desktop AuthService] Refresh already in progress, awaiting existing refresh'); + return this.refreshPromise; + } + + this.refreshPromise = this._doRefreshToken(serverUrl); + try { + return await this.refreshPromise; + } finally { + this.refreshPromise = null; + } + } + + private async _doRefreshToken(serverUrl: string): Promise { try { console.log('[Desktop AuthService] Refreshing auth token'); this.setAuthStatus('refreshing', this.userInfo); @@ -511,6 +563,68 @@ export class AuthService { } } + async refreshSupabaseToken(authServerUrl: string): Promise { + // Prevent concurrent refresh attempts - reuse in-flight refresh + if (this.refreshPromise) { + console.log('[Desktop AuthService] Refresh already in progress, awaiting existing refresh'); + return this.refreshPromise; + } + + this.refreshPromise = this._doRefreshSupabaseToken(authServerUrl); + try { + return await this.refreshPromise; + } finally { + this.refreshPromise = null; + } + } + + private async _doRefreshSupabaseToken(authServerUrl: string): Promise { + try { + console.log('[Desktop AuthService] Refreshing Supabase token'); + this.setAuthStatus('refreshing', this.userInfo); + + const refreshToken = await this.getRefreshToken(); + if (!refreshToken) { + console.error('[Desktop AuthService] No refresh token available'); + this.setAuthStatus('unauthenticated', null); + return false; + } + + // Call Supabase refresh endpoint + const response = await axios.post( + `${authServerUrl}/auth/v1/token?grant_type=refresh_token`, + { + refresh_token: refreshToken, + }, + { + headers: { + 'apikey': SUPABASE_KEY, + 'Content-Type': 'application/json', + }, + } + ); + + const { access_token, refresh_token: newRefreshToken } = response.data; + + // Save new tokens + await this.saveTokenEverywhere(access_token, newRefreshToken); + + const userInfo = await this.getUserInfo(); + this.setAuthStatus('authenticated', userInfo); + + console.log('[Desktop AuthService] Supabase token refreshed successfully'); + return true; + } catch (error) { + console.error('[Desktop AuthService] Supabase token refresh failed:', error); + this.setAuthStatus('unauthenticated', null); + + // Clear stored credentials on refresh failure + await this.logout(); + + return false; + } + } + async initializeAuthState(): Promise { console.log('[Desktop AuthService] Initializing auth state...'); // If we are on the login/setup screen, don't auto-restore a previous session; clear instead @@ -532,11 +646,7 @@ export class AuthService { const userInfo = await this.getUserInfo(); if (token && userInfo) { - console.log('[Desktop AuthService] Found token, syncing to all storage locations'); - - // Ensure token is in both Tauri store and localStorage - await this.saveTokenEverywhere(token); - + console.log('[Desktop AuthService] Found existing token and user info'); this.setAuthStatus('authenticated', userInfo); console.log('[Desktop AuthService] Auth state initialized as authenticated'); } else { @@ -576,9 +686,12 @@ export class AuthService { }); console.log('[Desktop AuthService] OAuth authentication successful, storing tokens'); + console.log('[Desktop AuthService] OAuth result - has access_token:', !!result.access_token); + console.log('[Desktop AuthService] OAuth result - has refresh_token:', !!result.refresh_token); + console.log('[Desktop AuthService] OAuth result - expires_in:', result.expires_in); - // Save token to all storage locations - await this.saveTokenEverywhere(result.access_token); + // Save token and refresh token to all storage locations + await this.saveTokenEverywhere(result.access_token, result.refresh_token); // Fetch user info from Supabase using the access token const userInfo = await this.fetchSupabaseUserInfo(authServerUrl, result.access_token); From 4f404a1ccf80b144a800066ce48adc3f91e14c0e Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 31 Jan 2026 20:59:25 +0000 Subject: [PATCH 4/6] Support multiple pipeline watch directories and configurable pipeline base path (#5545) ### Motivation - Allow operators to configure a pipeline base directory and multiple watched folders so the pipeline can monitor several directories and subdirectories concurrently. - Ensure scanning traverses subdirectories while skipping internal processing folders (e.g. `processing`) and preserve existing behavior for finished/output paths. - Expose the new options in the server `settings.yml.template` and the admin UI so paths can be edited from the web console. ### Description - Added new `pipelineDir` and `watchedFoldersDirs` fields to `ApplicationProperties.CustomPaths.Pipeline` and kept backward compatibility with `watchedFoldersDir` (app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java). - Resolved pipeline base and multiple watched folder paths in `RuntimePathConfig` and exposed `getPipelineWatchedFoldersPaths()` (app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java). - Updated `FileMonitor` to accept and register multiple root paths instead of a single root (app/common/src/main/java/stirling/software/common/util/FileMonitor.java). - Updated `PipelineDirectoryProcessor` to iterate all configured watched roots and to walk subdirectories while ignoring `processing` dirs (app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java). - Exposed the new settings in `settings.yml.template` and the admin UI, including a multi-line `Textarea` to edit `watchedFoldersDirs` (app/core/src/main/resources/settings.yml.template, frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx). - Adjusted unit test setup to account for list-based watched folders (app/common/src/test/java/stirling/software/common/util/FileMonitorTest.java). ### Testing - Ran formatting and build checks with `./gradlew spotlessApply` and `./gradlew build` using Java 21 via `JAVA_HOME=/root/.local/share/mise/installs/java/21.0.2 PATH=/root/.local/share/mise/installs/java/21.0.2/bin:$PATH ./gradlew ...`, but both runs failed due to Gradle plugin resolution being blocked in this environment (plugin portal/network 403), so full compilation/formatting could not complete. - Confirmed the code compiles locally was not possible here; unit test `FileMonitorTest` was updated to use the new API but was not executed due to the blocked build. - Changes were committed (`Support multiple pipeline watch directories`) and the repository diff contains the listed file modifications. ------ [Codex Task](https://chatgpt.com/codex/tasks/task_b_69741ecd17c883288d8085a63ccd66f4) --- .../configuration/RuntimePathConfig.java | 161 +++++++++++++++++- .../common/model/ApplicationProperties.java | 2 + .../software/common/util/FileMonitor.java | 42 +++-- .../software/common/util/FileMonitorTest.java | 4 +- .../pipeline/PipelineDirectoryProcessor.java | 65 ++++++- .../src/main/resources/settings.yml.template | 2 + .../api/AdminSettingsController.java | 55 ++++++ .../public/locales/en-GB/translation.toml | 8 + .../configSections/AdminGeneralSection.tsx | 148 ++++++++++++++-- 9 files changed, 452 insertions(+), 35 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java b/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java index 480e806111..49ed068e21 100644 --- a/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java +++ b/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java @@ -1,10 +1,14 @@ package stirling.software.common.configuration; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.springframework.context.annotation.Configuration; @@ -41,6 +45,7 @@ public class RuntimePathConfig { // Pipeline paths private final String pipelineWatchedFoldersPath; + private final List pipelineWatchedFoldersPaths; private final String pipelineFinishedFoldersPath; private final String pipelineDefaultWebUiConfigs; private final String pipelinePath; @@ -49,20 +54,27 @@ public class RuntimePathConfig { this.properties = properties; this.basePath = InstallationPathConfig.getPath(); - this.pipelinePath = Path.of(basePath, "pipeline").toString(); - String defaultWatchedFolders = Path.of(this.pipelinePath, "watchedFolders").toString(); - String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString(); - String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString(); - System system = properties.getSystem(); CustomPaths customPaths = system.getCustomPaths(); Pipeline pipeline = customPaths.getPipeline(); - this.pipelineWatchedFoldersPath = + this.pipelinePath = resolvePath( + Path.of(basePath, "pipeline").toString(), + pipeline != null ? pipeline.getPipelineDir() : null); + String defaultWatchedFolders = Path.of(this.pipelinePath, "watchedFolders").toString(); + String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString(); + String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString(); + + List watchedFoldersDirs = + sanitizePathList(pipeline != null ? pipeline.getWatchedFoldersDirs() : null); + this.pipelineWatchedFoldersPaths = + resolveWatchedFolderPaths( defaultWatchedFolders, + watchedFoldersDirs, pipeline != null ? pipeline.getWatchedFoldersDir() : null); + this.pipelineWatchedFoldersPath = this.pipelineWatchedFoldersPaths.get(0); this.pipelineFinishedFoldersPath = resolvePath( defaultFinishedFolders, @@ -72,6 +84,9 @@ public class RuntimePathConfig { defaultWebUIConfigs, pipeline != null ? pipeline.getWebUIConfigsDir() : null); + // Validate path conflicts after all paths are resolved + validatePipelinePaths(); + boolean isDocker = isRunningInDocker(); // Initialize Operation paths @@ -129,6 +144,140 @@ public class RuntimePathConfig { return StringUtils.isNotBlank(customPath) ? customPath : defaultPath; } + private List resolveWatchedFolderPaths( + String defaultPath, List watchedFoldersDirs, String legacyWatchedFolder) { + List rawPaths = new ArrayList<>(); + + // Collect paths from new config + if (watchedFoldersDirs != null && !watchedFoldersDirs.isEmpty()) { + rawPaths.addAll(watchedFoldersDirs); + } + // Fall back to legacy config + else if (StringUtils.isNotBlank(legacyWatchedFolder)) { + rawPaths.add(legacyWatchedFolder); + } + // Fall back to default + else { + rawPaths.add(defaultPath); + } + + // Validate, normalize, and deduplicate paths + List validatedPaths = validateAndNormalizePaths(rawPaths); + + // Ensure we have at least one valid path (critical for system to function) + if (validatedPaths.isEmpty()) { + log.warn( + "No valid watched folder paths configured, falling back to default: {}", + defaultPath); + validatedPaths.add(defaultPath); + } + + // Detect overlapping paths (warning only, not blocking) + detectOverlappingPaths(validatedPaths); + + return validatedPaths; + } + + private List sanitizePathList(List paths) { + if (paths == null || paths.isEmpty()) { + return Collections.emptyList(); + } + List sanitized = new ArrayList<>(); + for (String path : paths) { + if (StringUtils.isNotBlank(path)) { + sanitized.add(path.trim()); + } + } + return sanitized; + } + + private List validateAndNormalizePaths(List paths) { + Set normalizedPaths = new LinkedHashSet<>(); // Preserves order, prevents duplicates + + for (String pathStr : paths) { + if (StringUtils.isBlank(pathStr)) { + continue; + } + + try { + // Normalize to absolute path + Path path = Paths.get(pathStr.trim()).toAbsolutePath().normalize(); + String normalizedPath = path.toString(); + + // Check for duplicates + if (normalizedPaths.contains(normalizedPath)) { + log.debug("Skipping duplicate watched folder path: {}", pathStr); + continue; + } + + normalizedPaths.add(normalizedPath); + log.info("Registered watched folder path: {}", normalizedPath); + + } catch (InvalidPathException e) { + log.error( + "Invalid watched folder path '{}' - skipping: {}", pathStr, e.getMessage()); + } + } + + return new ArrayList<>(normalizedPaths); + } + + private void detectOverlappingPaths(List paths) { + for (int i = 0; i < paths.size(); i++) { + Path path1 = Paths.get(paths.get(i)); + for (int j = i + 1; j < paths.size(); j++) { + Path path2 = Paths.get(paths.get(j)); + + // Check if one path is a parent of the other + if (path1.startsWith(path2)) { + log.warn( + "Watched folder path '{}' is nested inside '{}' - this may cause duplicate processing", + path1, + path2); + } else if (path2.startsWith(path1)) { + log.warn( + "Watched folder path '{}' is nested inside '{}' - this may cause duplicate processing", + path2, + path1); + } + } + } + } + + private void validatePipelinePaths() { + try { + Path finishedPath = Paths.get(pipelineFinishedFoldersPath).toAbsolutePath().normalize(); + + for (String watchedPathStr : pipelineWatchedFoldersPaths) { + Path watchedPath = Paths.get(watchedPathStr).toAbsolutePath().normalize(); + + // Check if watched folder is same as finished folder + if (watchedPath.equals(finishedPath)) { + log.error( + "CRITICAL: Watched folder '{}' is the same as finished folder '{}' - this will cause processing loops!", + watchedPath, + finishedPath); + } + // Check if watched folder contains finished folder + else if (finishedPath.startsWith(watchedPath)) { + log.warn( + "Finished folder '{}' is nested inside watched folder '{}' - this may cause issues", + finishedPath, + watchedPath); + } + // Check if finished folder contains watched folder + else if (watchedPath.startsWith(finishedPath)) { + log.error( + "CRITICAL: Watched folder '{}' is nested inside finished folder '{}' - this will cause processing loops!", + watchedPath, + finishedPath); + } + } + } catch (Exception e) { + log.error("Error validating pipeline paths: {}", e.getMessage()); + } + } + private boolean isRunningInDocker() { return Files.exists(Path.of("/.dockerenv")); } diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index cabced160b..9ff046e040 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -459,7 +459,9 @@ public class ApplicationProperties { @Data public static class Pipeline { + private String pipelineDir; private String watchedFoldersDir; + private List watchedFoldersDirs = new ArrayList<>(); private String finishedFoldersDir; private String webUIConfigsDir; } diff --git a/app/common/src/main/java/stirling/software/common/util/FileMonitor.java b/app/common/src/main/java/stirling/software/common/util/FileMonitor.java index 3d1fe4f584..fac9bf6503 100644 --- a/app/common/src/main/java/stirling/software/common/util/FileMonitor.java +++ b/app/common/src/main/java/stirling/software/common/util/FileMonitor.java @@ -29,7 +29,7 @@ public class FileMonitor { private final ConcurrentHashMap.KeySetView readyForProcessingFiles; private final WatchService watchService; private final Predicate pathFilter; - private final Path rootDir; + private final List rootDirs; private Set stagingFiles; /** @@ -47,8 +47,28 @@ public class FileMonitor { this.pathFilter = pathFilter; this.readyForProcessingFiles = ConcurrentHashMap.newKeySet(); this.watchService = FileSystems.getDefault().newWatchService(); - log.info("Monitoring directory: {}", runtimePathConfig.getPipelineWatchedFoldersPath()); - this.rootDir = Path.of(runtimePathConfig.getPipelineWatchedFoldersPath()); + + List watchedFoldersDirs = runtimePathConfig.getPipelineWatchedFoldersPaths(); + List validRootDirs = new ArrayList<>(); + + for (String pathStr : watchedFoldersDirs) { + try { + Path path = Path.of(pathStr); + validRootDirs.add(path); + log.info("Monitoring directory: {}", path); + } catch (Exception e) { + log.error( + "Failed to initialize monitoring for path '{}': {}", + pathStr, + e.getMessage()); + } + } + + this.rootDirs = Collections.unmodifiableList(validRootDirs); + + if (this.rootDirs.isEmpty()) { + log.error("No valid directories to monitor - FileMonitor will not function"); + } } private boolean shouldNotProcess(Path path) { @@ -85,13 +105,15 @@ public class FileMonitor { readyForProcessingFiles.clear(); if (path2KeyMapping.isEmpty()) { - log.warn("not monitoring any directory, even the root directory itself: {}", rootDir); - if (Files.exists( - rootDir)) { // if the root directory exists, re-register the root directory - try { - recursivelyRegisterEntry(rootDir); - } catch (IOException e) { - log.error("unable to register monitoring", e); + log.warn("Not monitoring any directories; attempting to re-register root paths."); + for (Path rootDir : rootDirs) { + if (Files.exists( + rootDir)) { // if the root directory exists, re-register the root directory + try { + recursivelyRegisterEntry(rootDir); + } catch (IOException e) { + log.error("unable to register monitoring for {}", rootDir, e); + } } } } diff --git a/app/common/src/test/java/stirling/software/common/util/FileMonitorTest.java b/app/common/src/test/java/stirling/software/common/util/FileMonitorTest.java index cd137723fb..851c1c6cd1 100644 --- a/app/common/src/test/java/stirling/software/common/util/FileMonitorTest.java +++ b/app/common/src/test/java/stirling/software/common/util/FileMonitorTest.java @@ -9,6 +9,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileTime; import java.time.Instant; +import java.util.List; import java.util.function.Predicate; import org.junit.jupiter.api.BeforeEach; @@ -34,7 +35,8 @@ class FileMonitorTest { @BeforeEach void setUp() throws IOException { - when(runtimePathConfig.getPipelineWatchedFoldersPath()).thenReturn(tempDir.toString()); + when(runtimePathConfig.getPipelineWatchedFoldersPaths()) + .thenReturn(List.of(tempDir.toString())); // This mock is used in all tests except testPathFilter // We use lenient to avoid UnnecessaryStubbingException in that test diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java index 070bd4103e..83450088e7 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.controller.api.pipeline; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystemException; +import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -14,6 +15,7 @@ import java.time.LocalDate; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -41,14 +43,20 @@ import stirling.software.common.util.FileMonitor; @Slf4j public class PipelineDirectoryProcessor { + private static final int MAX_DIRECTORY_DEPTH = 50; // Prevent excessive recursion + private final ObjectMapper objectMapper; private final ApiDocService apiDocService; private final PipelineProcessor processor; private final FileMonitor fileMonitor; private final PostHogService postHogService; - private final String watchedFoldersDir; + private final List watchedFoldersDirs; private final String finishedFoldersDir; + // Track processed directories in current scan to prevent duplicates + private final ThreadLocal> processedDirsInScan = + ThreadLocal.withInitial(java.util.HashSet::new); + public PipelineDirectoryProcessor( ObjectMapper objectMapper, ApiDocService apiDocService, @@ -61,13 +69,26 @@ public class PipelineDirectoryProcessor { this.processor = processor; this.fileMonitor = fileMonitor; this.postHogService = postHogService; - this.watchedFoldersDir = runtimePathConfig.getPipelineWatchedFoldersPath(); + this.watchedFoldersDirs = runtimePathConfig.getPipelineWatchedFoldersPaths(); this.finishedFoldersDir = runtimePathConfig.getPipelineFinishedFoldersPath(); } @Scheduled(fixedRate = 60000) public void scanFolders() { - Path watchedFolderPath = Paths.get(watchedFoldersDir).toAbsolutePath(); + // Clear the processed directories set for this scan cycle + processedDirsInScan.get().clear(); + + try { + for (String watchedFoldersDir : watchedFoldersDirs) { + scanWatchedFolder(Paths.get(watchedFoldersDir).toAbsolutePath()); + } + } finally { + // Clean up ThreadLocal to prevent memory leaks + processedDirsInScan.remove(); + } + } + + private void scanWatchedFolder(Path watchedFolderPath) { if (!Files.exists(watchedFolderPath)) { try { Files.createDirectories(watchedFolderPath); @@ -78,16 +99,34 @@ public class PipelineDirectoryProcessor { } } + // Validate the path is a directory and readable + if (!Files.isDirectory(watchedFolderPath)) { + log.error("Path is not a directory: {}", watchedFolderPath); + return; + } + if (!Files.isReadable(watchedFolderPath)) { + log.error("Directory is not readable: {}", watchedFolderPath); + return; + } + try { + // Use FOLLOW_LINKS to follow symlinks, with max depth to prevent infinite loops Files.walkFileTree( watchedFolderPath, + EnumSet.of(FileVisitOption.FOLLOW_LINKS), + MAX_DIRECTORY_DEPTH, new SimpleFileVisitor<>() { @Override public FileVisitResult preVisitDirectory( Path dir, BasicFileAttributes attrs) { try { + String dirName = + dir.getFileName() != null + ? dir.getFileName().toString() + : ""; // Skip root directory and "processing" subdirectories - if (!dir.equals(watchedFolderPath) && !dir.endsWith("processing")) { + if (!dir.equals(watchedFolderPath) + && !"processing".equals(dirName)) { handleDirectory(dir); } } catch (Exception e) { @@ -98,8 +137,11 @@ public class PipelineDirectoryProcessor { @Override public FileVisitResult visitFileFailed(Path path, IOException exc) { - // Handle broken symlinks or inaccessible directories - log.error("Error accessing path: {}", path, exc); + // Handle broken symlinks, permission issues, or inaccessible + // directories + if (exc != null) { + log.debug("Cannot access path '{}': {}", path, exc.getMessage()); + } return FileVisitResult.CONTINUE; } }); @@ -109,6 +151,17 @@ public class PipelineDirectoryProcessor { } public void handleDirectory(Path dir) throws IOException { + // Normalize path to absolute to prevent duplicate processing from different path + // representations + Path normalizedDir = dir.toAbsolutePath().normalize(); + + // Check if we've already processed this directory in this scan cycle + java.util.Set processedDirs = processedDirsInScan.get(); + if (!processedDirs.add(normalizedDir)) { + log.debug("Directory already processed in this scan cycle: {}", normalizedDir); + return; + } + log.info("Handling directory: {}", dir); Path processingDir = createProcessingDirectory(dir); Optional jsonFileOptional = findJsonFile(dir); diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index 8a952fa970..8b1cbdeb83 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -203,7 +203,9 @@ system: name: postgres # set the name of your database. Should match the name of the database you create customPaths: pipeline: + pipelineDir: "" # Defaults to /pipeline watchedFoldersDir: "" # Defaults to /pipeline/watchedFolders + watchedFoldersDirs: [] # List of watched folder directories. Defaults to watchedFoldersDir or /pipeline/watchedFolders. finishedFoldersDir: "" # Defaults to /pipeline/finishedFolders operations: weasyprint: "" # Defaults to /opt/venv/bin/weasyprint diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java index 28a580c746..f92434fd98 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java @@ -186,6 +186,13 @@ public class AdminSettingsController { + HtmlUtils.htmlEscape(key))); } + // Validate pipeline path settings + String validationError = validatePipelinePathSetting(key, value); + if (validationError != null) { + return ResponseEntity.badRequest() + .body(Map.of("error", HtmlUtils.htmlEscape(validationError))); + } + log.info("Admin updating setting: {} = {}", key, value); GeneralUtils.saveKeyToSettings(key, value); @@ -642,6 +649,54 @@ public class AdminSettingsController { return true; } + private String validatePipelinePathSetting(String key, Object value) { + // Validate pipeline path settings + if (key.startsWith("system.customPaths.pipeline.watchedFoldersDirs") + && value instanceof java.util.List) { + @SuppressWarnings("unchecked") + java.util.List paths = (java.util.List) value; + + // Check for empty or all-blank paths + if (paths.isEmpty()) { + return null; // Empty is OK, will use default + } + + // Validate each path + java.util.Set normalizedPaths = new java.util.HashSet<>(); + for (String path : paths) { + if (path != null && !path.trim().isEmpty()) { + try { + java.nio.file.Path normalized = + java.nio.file.Paths.get(path.trim()).toAbsolutePath().normalize(); + String normalizedStr = normalized.toString(); + + // Check for duplicates + if (normalizedPaths.contains(normalizedStr)) { + return "Duplicate path detected: " + path; + } + normalizedPaths.add(normalizedStr); + } catch (java.nio.file.InvalidPathException e) { + return "Invalid path: " + path + " - " + e.getMessage(); + } + } + } + + // Check for overlapping paths + java.util.List pathList = new java.util.ArrayList<>(normalizedPaths); + for (int i = 0; i < pathList.size(); i++) { + java.nio.file.Path path1 = java.nio.file.Paths.get(pathList.get(i)); + for (int j = i + 1; j < pathList.size(); j++) { + java.nio.file.Path path2 = java.nio.file.Paths.get(pathList.get(j)); + if (path1.startsWith(path2) || path2.startsWith(path1)) { + return "Overlapping paths detected: " + path1 + " and " + path2; + } + } + } + } + + return null; // Valid + } + private Object getSettingByKey(String key) { if (key == null || key.trim().isEmpty()) { return null; diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index a34fe6cb10..2bb1b12f9a 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -4514,10 +4514,18 @@ description = "Configure custom file system paths for pipeline processing and ex [admin.settings.general.customPaths.pipeline] label = "Pipeline Directories" +[admin.settings.general.customPaths.pipeline.pipelineDir] +label = "Pipeline Directory" +description = "Base directory for pipeline resources (leave empty for default: /pipeline)" + [admin.settings.general.customPaths.pipeline.watchedFoldersDir] label = "Watched Folders Directory" description = "Directory where pipeline monitors for incoming PDFs (leave empty for default: /pipeline/watchedFolders)" +[admin.settings.general.customPaths.pipeline.watchedFoldersDirs] +label = "Watched Folders Directories" +description = "Directories where pipeline monitors for incoming PDFs (one per line or comma-separated; leave empty for default: /pipeline/watchedFolders)" + [admin.settings.general.customPaths.pipeline.finishedFoldersDir] label = "Finished Folders Directory" description = "Directory where processed PDFs are outputted (leave empty for default: /pipeline/finishedFolders)" diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx index 85bc61e1b3..3a50bd725c 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useRef, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; -import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, MultiSelect, Badge, SegmentedControl, Select } from '@mantine/core'; +import { TextInput, Textarea, Switch, Button, Stack, Paper, Text, Loader, Group, MultiSelect, Badge, SegmentedControl, Select } from '@mantine/core'; import { alert } from '@app/components/toast'; import RestartConfirmationModal from '@app/components/shared/config/RestartConfirmationModal'; import { useRestartServer } from '@app/components/shared/config/useRestartServer'; @@ -31,7 +31,9 @@ interface GeneralSettingsData { }; customPaths?: { pipeline?: { + pipelineDir?: string; watchedFoldersDir?: string; + watchedFoldersDirs?: string[]; finishedFoldersDir?: string; }; operations?: { @@ -61,6 +63,17 @@ export default function AdminGeneralSection() { .sort((a, b) => a.label.localeCompare(b.label)), [] ); + const parseWatchedFoldersInput = useCallback((value: string) => { + const paths = value + .split(/[\n,;]+/) + .map((entry) => entry.trim()) + .filter(Boolean); + + // Deduplicate paths (case-sensitive, exact match) + const uniquePaths = Array.from(new Set(paths)); + + return uniquePaths; + }, []); // Track original settings for dirty detection const [originalSettingsSnapshot, setOriginalSettingsSnapshot] = useState(''); @@ -91,17 +104,31 @@ export default function AdminGeneralSection() { ui.languages = Array.isArray(ui.languages) ? toUnderscoreLanguages(ui.languages) : []; + const pipelinePaths = system.customPaths?.pipeline || {}; + const watchedFoldersDirs = Array.isArray(pipelinePaths.watchedFoldersDirs) + ? pipelinePaths.watchedFoldersDirs + : []; + const normalizedWatchedFoldersDirs = + watchedFoldersDirs.length > 0 + ? watchedFoldersDirs + : (pipelinePaths.watchedFoldersDir ? [pipelinePaths.watchedFoldersDir] : []); + const result: any = { ui, system, - customPaths: system.customPaths || { + customPaths: { + ...(system.customPaths || {}), pipeline: { - watchedFoldersDir: '', - finishedFoldersDir: '' + ...pipelinePaths, + pipelineDir: pipelinePaths.pipelineDir || '', + watchedFoldersDir: pipelinePaths.watchedFoldersDir || '', + watchedFoldersDirs: normalizedWatchedFoldersDirs, + finishedFoldersDir: pipelinePaths.finishedFoldersDir || '' }, operations: { - weasyprint: '', - unoconvert: '' + ...(system.customPaths?.operations || {}), + weasyprint: system.customPaths?.operations?.weasyprint || '', + unoconvert: system.customPaths?.operations?.unoconvert || '' } }, customMetadata: premium.proFeatures?.customMetadata || { @@ -154,7 +181,9 @@ export default function AdminGeneralSection() { }; if (settings.customPaths) { + deltaSettings['system.customPaths.pipeline.pipelineDir'] = settings.customPaths?.pipeline?.pipelineDir; deltaSettings['system.customPaths.pipeline.watchedFoldersDir'] = settings.customPaths?.pipeline?.watchedFoldersDir; + deltaSettings['system.customPaths.pipeline.watchedFoldersDirs'] = settings.customPaths?.pipeline?.watchedFoldersDirs; deltaSettings['system.customPaths.pipeline.finishedFoldersDir'] = settings.customPaths?.pipeline?.finishedFoldersDir; deltaSettings['system.customPaths.operations.weasyprint'] = settings.customPaths?.operations?.weasyprint; deltaSettings['system.customPaths.operations.unoconvert'] = settings.customPaths?.operations?.unoconvert; @@ -171,6 +200,54 @@ export default function AdminGeneralSection() { () => toUnderscoreLanguages(settings.ui?.languages || []), [settings.ui?.languages] ); + const watchedFoldersInput = useMemo(() => ( + (settings.customPaths?.pipeline?.watchedFoldersDirs || []).join('\n') + ), [settings.customPaths?.pipeline?.watchedFoldersDirs]); + + const watchedFoldersValidation = useMemo(() => { + const paths = settings.customPaths?.pipeline?.watchedFoldersDirs || []; + const finishedPath = settings.customPaths?.pipeline?.finishedFoldersDir || ''; + const warnings: string[] = []; + + // Normalize paths for comparison (handle both Windows and Unix paths) + const normalizePath = (p: string) => p.replace(/\\/g, '/').replace(/\/+$/, ''); + + // Check for overlapping watched folders + if (paths.length >= 2) { + for (let i = 0; i < paths.length; i++) { + for (let j = i + 1; j < paths.length; j++) { + const path1 = normalizePath(paths[i]); + const path2 = normalizePath(paths[j]); + + if (path1 === path2) { + warnings.push(`Duplicate path detected: '${paths[i]}'`); + } else if (path1.startsWith(path2 + '/')) { + warnings.push(`'${paths[i]}' is nested inside '${paths[j]}' - may cause duplicate processing`); + } else if (path2.startsWith(path1 + '/')) { + warnings.push(`'${paths[j]}' is nested inside '${paths[i]}' - may cause duplicate processing`); + } + } + } + } + + // Check for conflicts with finished folder + if (finishedPath && paths.length > 0) { + const normalizedFinished = normalizePath(finishedPath); + for (const watchedPath of paths) { + const normalizedWatched = normalizePath(watchedPath); + + if (normalizedWatched === normalizedFinished) { + warnings.push(`CRITICAL: Watched folder '${watchedPath}' is the same as finished folder - will cause processing loops!`); + } else if (normalizedFinished.startsWith(normalizedWatched + '/')) { + warnings.push(`Finished folder is nested inside watched folder '${watchedPath}' - may cause issues`); + } else if (normalizedWatched.startsWith(normalizedFinished + '/')) { + warnings.push(`CRITICAL: Watched folder '${watchedPath}' is nested inside finished folder - will cause processing loops!`); + } + } + } + + return warnings.length > 0 ? warnings : null; + }, [settings.customPaths?.pipeline?.watchedFoldersDirs, settings.customPaths?.pipeline?.finishedFoldersDir]); // Filter default locale options based on available languages setting const defaultLocaleOptions = useMemo(() => { @@ -651,27 +728,74 @@ export default function AdminGeneralSection() { - {t('admin.settings.general.customPaths.pipeline.watchedFoldersDir.label', 'Watched Folders Directory')} - + {t('admin.settings.general.customPaths.pipeline.pipelineDir.label', 'Pipeline Directory')} + } - description={t('admin.settings.general.customPaths.pipeline.watchedFoldersDir.description', 'Directory where pipeline monitors for incoming PDFs (leave empty for default: /pipeline/watchedFolders)')} - value={settings.customPaths?.pipeline?.watchedFoldersDir || ''} + description={t('admin.settings.general.customPaths.pipeline.pipelineDir.description', 'Base directory for pipeline resources (leave empty for default: /pipeline)')} + value={settings.customPaths?.pipeline?.pipelineDir || ''} onChange={(e) => setSettings({ ...settings, customPaths: { ...settings.customPaths, pipeline: { ...settings.customPaths?.pipeline, - watchedFoldersDir: e.target.value + pipelineDir: e.target.value } } })} - placeholder="/pipeline/watchedFolders" + placeholder="/pipeline" disabled={!loginEnabled} /> +
+