diff --git a/OCR_Implementation_Plan.md b/OCR_Implementation_Plan.md
new file mode 100644
index 00000000000..503925993ea
--- /dev/null
+++ b/OCR_Implementation_Plan.md
@@ -0,0 +1,195 @@
+# OCR Implementation Plan for JabRef
+
+This document outlines the implementation plan for adding OCR (Optical Character Recognition) support to JabRef, focusing on improved handling of ancient documents and scanned PDFs. This plan demonstrates my understanding of JabRef's architecture and how OCR functionality can be integrated in a clean, modular way.
+
+## 1. OCR Service Interface Prototype
+
+### Architecture Overview
+
+Following JabRef's hexagonal architecture, I'll create a clean separation of concerns with:
+
+- **Domain core**: Define OCR operations and models
+- **Ports**: Interfaces that define boundaries between components
+- **Adapters**: Implementations that connect to specific OCR engines
+
+### Key Components
+
+```
+org.jabref.logic.ocr/
+ ├── OcrService.java # Core interface (port) defining OCR operations
+ ├── models/
+ │ ├── OcrResult.java # Domain model for OCR results
+ │ ├── OcrLanguage.java # Domain model for OCR language options
+ │ └── OcrEngineConfig.java # Domain model for engine configuration
+ ├── exception/
+ │ └── OcrProcessException.java # Domain-specific exceptions
+ ├── engines/ # Package for engine adapters
+ │ ├── OcrEngineAdapter.java # Base adapter interface
+ │ └── TesseractAdapter.java # Adapter for Tesseract (placeholder)
+ └── OcrManager.java # Facade coordinating OCR operations
+```
+
+### Implementation Approach
+
+I'll follow these principles to match JabRef's architecture:
+
+1. **Interface-first design**: Define clear interfaces before implementation
+2. **Adapter pattern**: Wrap OCR engines in adapters that implement common interface
+3. **Dependency inversion**: Core logic depends on abstractions, not concrete implementations
+4. **Domain-driven design**: Create proper domain models for OCR concepts
+
+### Integration Points
+
+The OCR service will integrate with JabRef through:
+
+- **Entry processing**: Attach to entry import workflow
+- **PDF handling**: Integrate with PDF utilities
+- **Search system**: Connect to Lucene indexer
+
+## 2. PDF Text Layer Proof-of-Concept
+
+### Architecture Overview
+
+This component will demonstrate how to add OCR-extracted text as a searchable layer to PDFs, following JabRef's existing patterns for PDF manipulation.
+
+### Key Components
+
+```
+org.jabref.logic.ocr.pdf/
+ ├── TextLayerAdder.java # Utility to add text layers to PDFs
+ ├── OcrPdfProcessor.java # Processor for PDF OCR operations
+ └── SearchableTextLayer.java # Model for searchable text layers
+```
+
+### Implementation Approach
+
+The proof-of-concept will:
+
+1. Use PDFBox in a similar way to existing JabRef PDF utilities
+2. Follow the same patterns as `XmpUtilWriter` for metadata operations
+3. Create a clean API for adding text layers to PDFs
+4. Demonstrate how OCR text can be indexed by Lucene
+
+### Integration Points
+
+- Connect with `IndexManager` for search indexing
+- Integrate with JabRef's PDF processing pipeline
+- Utilize existing PDF utilities where appropriate
+
+## 3. Preference Panel for OCR Configuration
+
+### Architecture Overview
+
+I'll create a preference panel that follows JabRef's existing UI patterns and preference management system.
+
+### Key Components
+
+```
+org.jabref.gui.preferences.ocr/
+ ├── OcrTab.java # UI component extending AbstractPreferenceTabView
+ ├── OcrTabViewModel.java # ViewModel for the OCR preferences
+ └── OcrPreferences.java # Preference model for OCR settings
+```
+
+### Implementation Approach
+
+The preference panel will:
+
+1. Follow MVVM pattern like other JabRef preference tabs
+2. Use JavaFX controls with property binding
+3. Implement validation for preference values
+4. Integrate with JabRef's preference persistence
+
+### Integration Points
+
+- Register in `PreferencesDialogViewModel`
+- Access through `GuiPreferences`
+- Coordinate with OCR service implementation
+
+## API Design
+
+The core OCR service interface will define these operations:
+
+```java
+public interface OcrService {
+ /**
+ * Process a PDF file using OCR to extract text
+ * @param pdfPath Path to the PDF file
+ * @return OCR result containing extracted text and metadata
+ */
+ OcrResult processPdf(Path pdfPath) throws OcrProcessException;
+
+ /**
+ * Process an image file using OCR to extract text
+ * @param imagePath Path to the image file
+ * @return OCR result containing extracted text and metadata
+ */
+ OcrResult processImage(Path imagePath) throws OcrProcessException;
+
+ /**
+ * Add OCR-extracted text as a searchable layer to a PDF file
+ * @param pdfPath Path to the source PDF file
+ * @param outputPath Path to save the modified PDF
+ * @param ocrResult OCR result containing extracted text to add
+ */
+ void addTextLayerToPdf(Path pdfPath, Path outputPath, OcrResult ocrResult) throws OcrProcessException;
+
+ /**
+ * Set the language for OCR processing
+ * @param language OCR language to use
+ */
+ void setLanguage(OcrLanguage language) throws OcrProcessException;
+
+ /**
+ * Get the name of the OCR engine
+ * @return Engine name
+ */
+ String getEngineName();
+
+ /**
+ * Check if the OCR engine is available
+ * @return true if the engine is ready to use
+ */
+ boolean isAvailable();
+}
+```
+
+The adapter base class will provide common functionality:
+
+```java
+public abstract class OcrEngineAdapter implements OcrService {
+ protected OcrLanguage currentLanguage;
+ protected OcrEngineConfig config;
+
+ // Common implementation details for OCR engines
+ // Engine-specific subclasses will override key methods
+}
+```
+
+## Implementation Strategy
+
+I'll implement this project in phases:
+
+1. **Phase 1**: Core interfaces and models
+2. **Phase 2**: Basic adapter implementation (placeholder)
+3. **Phase 3**: PDF text layer utility (demonstration)
+4. **Phase 4**: Preference panel integration
+
+This phased approach allows for early feedback and iterative improvement.
+
+## Coding Standards and Testing
+
+I'll follow JabRef's existing patterns for:
+
+- Code style and organization
+- JavaDoc documentation
+- Unit testing with JUnit 5
+- Separation of concerns
+
+## Next Steps
+
+1. Implement core interfaces and models
+2. Create basic adapter implementations
+3. Develop PDF text layer proof-of-concept
+4. Design and integrate preference panel
+5. Document and submit PR for review
\ No newline at end of file
diff --git a/src/main/java/org/jabref/gui/preferences/ocr/OcrTab.java b/src/main/java/org/jabref/gui/preferences/ocr/OcrTab.java
new file mode 100644
index 00000000000..cb6c53e330a
--- /dev/null
+++ b/src/main/java/org/jabref/gui/preferences/ocr/OcrTab.java
@@ -0,0 +1,119 @@
+package org.jabref.gui.preferences.ocr;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.TextField;
+import javafx.stage.DirectoryChooser;
+
+import org.jabref.gui.Globals;
+import org.jabref.gui.actions.ActionFactory;
+import org.jabref.gui.actions.StandardActions;
+import org.jabref.gui.help.HelpAction;
+import org.jabref.gui.preferences.AbstractPreferenceTabView;
+import org.jabref.gui.preferences.PreferencesTab;
+import org.jabref.gui.util.DirectoryDialogConfiguration;
+import org.jabref.gui.util.ViewModelListCellFactory;
+import org.jabref.logic.help.HelpFile;
+import org.jabref.logic.l10n.Localization;
+import org.jabref.logic.ocr.models.OcrEngineConfig;
+
+import com.airhacks.afterburner.views.ViewLoader;
+import de.saxsys.mvvmfx.utils.validation.visualization.ControlsFxVisualizer;
+
+/**
+ * Tab for OCR preferences in JabRef's preferences dialog.
+ *
+ * This class demonstrates how to create a preference tab using JabRef's
+ * UI framework and MVVM pattern.
+ */
+public class OcrTab extends AbstractPreferenceTabView implements PreferencesTab {
+
+ @FXML private CheckBox enableOcrCheckBox;
+ @FXML private ComboBox engineComboBox;
+ @FXML private ComboBox languageComboBox;
+ @FXML private CheckBox preprocessImagesCheckBox;
+ @FXML private ComboBox qualityPresetComboBox;
+ @FXML private TextField tesseractPathTextField;
+ @FXML private Button tesseractPathBrowseButton;
+ @FXML private Button helpButton;
+
+ private final ControlsFxVisualizer visualizer = new ControlsFxVisualizer();
+
+ /**
+ * Create a new OCR tab for JabRef preferences.
+ */
+ public OcrTab() {
+ ViewLoader.view(this)
+ .root(this)
+ .load();
+ }
+
+ /**
+ * Initialize the tab.
+ */
+ @FXML
+ public void initialize() {
+ this.viewModel = new OcrTabViewModel(preferences);
+
+ // Bind UI components to view model properties
+ enableOcrCheckBox.selectedProperty().bindBidirectional(viewModel.ocrEnabledProperty());
+
+ // Set up combo boxes with models
+ new ViewModelListCellFactory()
+ .withText(name -> name)
+ .install(engineComboBox);
+ engineComboBox.itemsProperty().bind(viewModel.availableEnginesProperty());
+ engineComboBox.valueProperty().bindBidirectional(viewModel.defaultOcrEngineProperty());
+ engineComboBox.disableProperty().bind(enableOcrCheckBox.selectedProperty().not());
+
+ new ViewModelListCellFactory()
+ .withText(name -> name)
+ .install(languageComboBox);
+ languageComboBox.itemsProperty().bind(viewModel.availableLanguagesProperty());
+ languageComboBox.valueProperty().bindBidirectional(viewModel.defaultLanguageProperty());
+ languageComboBox.disableProperty().bind(enableOcrCheckBox.selectedProperty().not());
+
+ preprocessImagesCheckBox.selectedProperty().bindBidirectional(viewModel.preprocessImagesProperty());
+ preprocessImagesCheckBox.disableProperty().bind(enableOcrCheckBox.selectedProperty().not());
+
+ new ViewModelListCellFactory()
+ .withText(OcrEngineConfig.QualityPreset::getDescription)
+ .install(qualityPresetComboBox);
+ qualityPresetComboBox.itemsProperty().bind(viewModel.availableQualityPresetsProperty());
+ qualityPresetComboBox.valueProperty().bindBidirectional(viewModel.qualityPresetProperty());
+ qualityPresetComboBox.disableProperty().bind(enableOcrCheckBox.selectedProperty().not());
+
+ tesseractPathTextField.textProperty().bindBidirectional(viewModel.tesseractPathProperty());
+ tesseractPathTextField.disableProperty().bind(enableOcrCheckBox.selectedProperty().not());
+ tesseractPathBrowseButton.disableProperty().bind(enableOcrCheckBox.selectedProperty().not());
+
+ // Setup validation
+ visualizer.initVisualization(viewModel.getTesseractPathValidationStatus(), tesseractPathTextField);
+
+ // Configure help button
+ ActionFactory actionFactory = new ActionFactory();
+ actionFactory.configureIconButton(StandardActions.HELP,
+ new HelpAction(HelpFile.IMPORT_USING_OCR, dialogService, preferences.getExternalApplicationsPreferences()),
+ helpButton);
+ }
+
+ /**
+ * Handle the browse button click for Tesseract path.
+ */
+ @FXML
+ private void onBrowseTesseractPath() {
+ DirectoryDialogConfiguration dirDialogConfiguration = new DirectoryDialogConfiguration.Builder()
+ .withInitialDirectory(Globals.prefs.get(Globals.WORKING_DIRECTORY))
+ .build();
+
+ dialogService.showDirectorySelectionDialog(dirDialogConfiguration)
+ .ifPresent(selectedDirectory -> viewModel.tesseractPathProperty().setValue(selectedDirectory.toString()));
+ }
+
+ @Override
+ public String getTabName() {
+ return Localization.lang("OCR");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jabref/gui/preferences/ocr/OcrTabViewModel.java b/src/main/java/org/jabref/gui/preferences/ocr/OcrTabViewModel.java
new file mode 100644
index 00000000000..964fb0fab09
--- /dev/null
+++ b/src/main/java/org/jabref/gui/preferences/ocr/OcrTabViewModel.java
@@ -0,0 +1,179 @@
+package org.jabref.gui.preferences.ocr;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ListProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleListProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.collections.FXCollections;
+
+import org.jabref.gui.preferences.GuiPreferences;
+import org.jabref.gui.preferences.PreferenceTabViewModel;
+import org.jabref.logic.l10n.Localization;
+import org.jabref.logic.ocr.models.OcrEngineConfig;
+
+import de.saxsys.mvvmfx.utils.validation.FunctionBasedValidator;
+import de.saxsys.mvvmfx.utils.validation.ValidationMessage;
+import de.saxsys.mvvmfx.utils.validation.ValidationStatus;
+import de.saxsys.mvvmfx.utils.validation.Validator;
+
+/**
+ * View model for the OCR tab in JabRef preferences.
+ *
+ * This class demonstrates how to implement a preference tab view model
+ * following JabRef's MVVM pattern.
+ */
+public class OcrTabViewModel implements PreferenceTabViewModel {
+
+ // Preference properties
+ private final BooleanProperty ocrEnabled;
+ private final StringProperty defaultOcrEngine;
+ private final StringProperty defaultLanguage;
+ private final BooleanProperty preprocessImages;
+ private final ObjectProperty qualityPreset;
+ private final StringProperty tesseractPath;
+
+ // Available options
+ private final ListProperty availableEngines;
+ private final ListProperty availableLanguages;
+ private final ListProperty availableQualityPresets;
+
+ // Validators
+ private final Validator tesseractPathValidator;
+
+ private final GuiPreferences preferences;
+
+ /**
+ * Create a new OCR tab view model.
+ *
+ * @param preferences GUI preferences
+ */
+ public OcrTabViewModel(GuiPreferences preferences) {
+ this.preferences = preferences;
+
+ // Initialize properties with default values
+ ocrEnabled = new SimpleBooleanProperty(false);
+ defaultOcrEngine = new SimpleStringProperty("Tesseract");
+ defaultLanguage = new SimpleStringProperty("eng");
+ preprocessImages = new SimpleBooleanProperty(true);
+ qualityPreset = new SimpleObjectProperty<>(OcrEngineConfig.QualityPreset.BALANCED);
+ tesseractPath = new SimpleStringProperty("");
+
+ // Initialize available options
+ availableEngines = new SimpleListProperty<>(FXCollections.observableArrayList(
+ "Tesseract", "Google Vision", "ABBYY Cloud OCR"
+ ));
+
+ availableLanguages = new SimpleListProperty<>(FXCollections.observableArrayList(
+ "eng", "deu", "fra", "spa", "ita", "lat", "grc", "san", "rus", "jpn", "chi_sim", "chi_tra"
+ ));
+
+ availableQualityPresets = new SimpleListProperty<>(FXCollections.observableArrayList(
+ OcrEngineConfig.QualityPreset.values()
+ ));
+
+ // Setup validators
+ tesseractPathValidator = new FunctionBasedValidator<>(
+ tesseractPath,
+ this::validateTesseractPath,
+ ValidationMessage.error(Localization.lang("The Tesseract path must be valid"))
+ );
+ }
+
+ /**
+ * Validate the Tesseract path.
+ *
+ * @param path Path to validate
+ * @return true if path is valid (either empty or existing directory)
+ */
+ private boolean validateTesseractPath(String path) {
+ if (path == null || path.trim().isEmpty()) {
+ return true; // Empty path is allowed (will use system default)
+ }
+
+ Path tesseractDir = Paths.get(path);
+ return Files.exists(tesseractDir) && Files.isDirectory(tesseractDir);
+ }
+
+ @Override
+ public void setValues() {
+ // In a real implementation, these values would be loaded from JabRef preferences
+ // For this demonstration, we just use the default values set in the constructor
+ }
+
+ @Override
+ public void storeSettings() {
+ // In a real implementation, these values would be stored in JabRef preferences
+ // For this demonstration, we just log the values
+ System.out.println("OCR settings stored:");
+ System.out.println("OCR enabled: " + ocrEnabled.get());
+ System.out.println("Default OCR engine: " + defaultOcrEngine.get());
+ System.out.println("Default language: " + defaultLanguage.get());
+ System.out.println("Preprocess images: " + preprocessImages.get());
+ System.out.println("Quality preset: " + qualityPreset.get());
+ System.out.println("Tesseract path: " + tesseractPath.get());
+ }
+
+ @Override
+ public boolean validateSettings() {
+ return tesseractPathValidator.getValidationStatus().isValid();
+ }
+
+ @Override
+ public List getRestartWarnings() {
+ // No restart needed for OCR settings
+ return new ArrayList<>();
+ }
+
+ // Property getters
+
+ public BooleanProperty ocrEnabledProperty() {
+ return ocrEnabled;
+ }
+
+ public StringProperty defaultOcrEngineProperty() {
+ return defaultOcrEngine;
+ }
+
+ public StringProperty defaultLanguageProperty() {
+ return defaultLanguage;
+ }
+
+ public BooleanProperty preprocessImagesProperty() {
+ return preprocessImages;
+ }
+
+ public ObjectProperty qualityPresetProperty() {
+ return qualityPreset;
+ }
+
+ public StringProperty tesseractPathProperty() {
+ return tesseractPath;
+ }
+
+ public ListProperty availableEnginesProperty() {
+ return availableEngines;
+ }
+
+ public ListProperty availableLanguagesProperty() {
+ return availableLanguages;
+ }
+
+ public ListProperty availableQualityPresetsProperty() {
+ return availableQualityPresets;
+ }
+
+ public ValidationStatus getTesseractPathValidationStatus() {
+ return tesseractPathValidator.getValidationStatus();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jabref/logic/ocr/OcrManager.java b/src/main/java/org/jabref/logic/ocr/OcrManager.java
new file mode 100644
index 00000000000..7c82f3f2f27
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ocr/OcrManager.java
@@ -0,0 +1,213 @@
+package org.jabref.logic.ocr;
+
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.jabref.logic.l10n.Localization;
+import org.jabref.logic.ocr.engines.OcrEngineAdapter;
+import org.jabref.logic.ocr.engines.TesseractAdapter;
+import org.jabref.logic.ocr.exception.OcrProcessException;
+import org.jabref.logic.ocr.models.OcrEngineConfig;
+import org.jabref.logic.ocr.models.OcrLanguage;
+import org.jabref.logic.ocr.models.OcrResult;
+import org.jabref.logic.search.IndexManager;
+import org.jabref.logic.util.TaskExecutor;
+import org.jabref.model.entry.BibEntry;
+import org.jabref.model.entry.LinkedFile;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manager class for OCR operations.
+ *
+ * This class coordinates OCR operations and integrates with JabRef's
+ * task execution and indexing systems. It follows JabRef's facade pattern
+ * to provide a simplified interface for OCR functionality.
+ */
+public class OcrManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(OcrManager.class);
+
+ private final Map ocrEngines = new HashMap<>();
+ private final TaskExecutor taskExecutor;
+ private final Optional indexManager;
+ private final String defaultEngineName;
+
+ /**
+ * Create a new OCR manager.
+ *
+ * @param tesseractPath Path to Tesseract installation
+ * @param taskExecutor Task executor for background processing
+ * @param indexManager Index manager for search indexing
+ */
+ public OcrManager(String tesseractPath, TaskExecutor taskExecutor, Optional indexManager) {
+ this.taskExecutor = taskExecutor;
+ this.indexManager = indexManager;
+
+ // Register available OCR engines
+ registerEngines(tesseractPath);
+
+ // Set default engine (first one registered)
+ this.defaultEngineName = ocrEngines.isEmpty() ? null : ocrEngines.keySet().iterator().next();
+ }
+
+ /**
+ * Register available OCR engines.
+ *
+ * @param tesseractPath Path to Tesseract installation
+ */
+ private void registerEngines(String tesseractPath) {
+ // Register Tesseract OCR engine
+ TesseractAdapter tesseractAdapter = new TesseractAdapter(tesseractPath);
+ ocrEngines.put(tesseractAdapter.getEngineName(), tesseractAdapter);
+
+ // Additional engines would be registered here
+ }
+
+ /**
+ * Get an OCR service by name.
+ *
+ * @param engineName Engine name
+ * @return OCR service
+ * @throws OcrProcessException if the engine is not available
+ */
+ public OcrService getOcrService(String engineName) throws OcrProcessException {
+ OcrService service = ocrEngines.get(engineName);
+
+ if (service == null) {
+ throw new OcrProcessException(
+ String.format("OCR engine '%s' not found. Available engines: %s",
+ engineName, ocrEngines.keySet()));
+ }
+
+ if (!service.isAvailable()) {
+ throw new OcrProcessException(
+ String.format("OCR engine '%s' is not available. Please check the installation.",
+ engineName));
+ }
+
+ return service;
+ }
+
+ /**
+ * Get the default OCR service.
+ *
+ * @return Default OCR service
+ * @throws OcrProcessException if no engine is available
+ */
+ public OcrService getDefaultOcrService() throws OcrProcessException {
+ if (defaultEngineName == null) {
+ throw new OcrProcessException("No OCR engine is available");
+ }
+
+ return getOcrService(defaultEngineName);
+ }
+
+ /**
+ * Process a PDF file with OCR.
+ *
+ * @param pdfPath Path to the PDF file
+ * @return OCR result
+ * @throws OcrProcessException if OCR processing fails
+ */
+ public OcrResult processPdf(Path pdfPath) throws OcrProcessException {
+ OcrService service = getDefaultOcrService();
+ return service.processPdf(pdfPath);
+ }
+
+ /**
+ * Process an image file with OCR.
+ *
+ * @param imagePath Path to the image file
+ * @return OCR result
+ * @throws OcrProcessException if OCR processing fails
+ */
+ public OcrResult processImage(Path imagePath) throws OcrProcessException {
+ OcrService service = getDefaultOcrService();
+ return service.processImage(imagePath);
+ }
+
+ /**
+ * Process a PDF file and add a searchable text layer.
+ *
+ * @param pdfPath Path to the PDF file
+ * @param outputPath Path to save the processed PDF
+ * @return OCR result
+ * @throws OcrProcessException if OCR processing fails
+ */
+ public OcrResult processPdfAndAddTextLayer(Path pdfPath, Path outputPath) throws OcrProcessException {
+ OcrService service = getDefaultOcrService();
+ OcrResult result = service.processPdf(pdfPath);
+ service.addTextLayerToPdf(pdfPath, outputPath, result);
+ return result;
+ }
+
+ /**
+ * Process PDF files for a BibEntry asynchronously.
+ *
+ * @param entry BibEntry to process
+ * @return true if processing was started
+ */
+ public boolean processPdfFilesForEntry(BibEntry entry) {
+ boolean processingStarted = false;
+
+ for (LinkedFile file : entry.getFiles()) {
+ if ("pdf".equalsIgnoreCase(file.getFileType().toString())) {
+ Path filePath = file.findIn(entry).orElse(null);
+
+ if (filePath != null) {
+ processingStarted = true;
+
+ // Process in background
+ taskExecutor.execute(() -> {
+ try {
+ // Process PDF
+ OcrResult result = processPdf(filePath);
+
+ // Index the result
+ indexManager.ifPresent(manager -> {
+ manager.addToIndex(entry, result.getExtractedText());
+ });
+
+ LOGGER.info("OCR completed for {}: {} characters extracted",
+ filePath, result.getExtractedText().length());
+
+ } catch (OcrProcessException e) {
+ LOGGER.error("OCR processing failed for {}: {}",
+ filePath, e.getMessage(), e);
+ }
+ });
+ }
+ }
+ }
+
+ return processingStarted;
+ }
+
+ /**
+ * Get names of available OCR engines.
+ *
+ * @return Map of engine names to availability status
+ */
+ public Map getAvailableEngines() {
+ Map engines = new HashMap<>();
+
+ for (OcrService service : ocrEngines.values()) {
+ engines.put(service.getEngineName(), service.isAvailable());
+ }
+
+ return engines;
+ }
+
+ /**
+ * Check if any OCR engine is available.
+ *
+ * @return true if at least one engine is available
+ */
+ public boolean isAnyEngineAvailable() {
+ return ocrEngines.values().stream().anyMatch(OcrService::isAvailable);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jabref/logic/ocr/OcrService.java b/src/main/java/org/jabref/logic/ocr/OcrService.java
new file mode 100644
index 00000000000..3b1078b844a
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ocr/OcrService.java
@@ -0,0 +1,83 @@
+package org.jabref.logic.ocr;
+
+import java.nio.file.Path;
+import java.util.Set;
+
+import org.jabref.logic.ocr.exception.OcrProcessException;
+import org.jabref.logic.ocr.models.OcrEngineConfig;
+import org.jabref.logic.ocr.models.OcrLanguage;
+import org.jabref.logic.ocr.models.OcrResult;
+
+/**
+ * Interface defining OCR (Optical Character Recognition) operations.
+ *
+ * This interface serves as a port in JabRef's hexagonal architecture, allowing
+ * different OCR engines to be plugged in via adapters.
+ */
+public interface OcrService {
+
+ /**
+ * Process a PDF file using OCR to extract text.
+ *
+ * @param pdfPath Path to the PDF file
+ * @return OCR result containing extracted text and metadata
+ * @throws OcrProcessException if OCR processing fails
+ */
+ OcrResult processPdf(Path pdfPath) throws OcrProcessException;
+
+ /**
+ * Process an image file using OCR to extract text.
+ *
+ * @param imagePath Path to the image file
+ * @return OCR result containing extracted text and metadata
+ * @throws OcrProcessException if OCR processing fails
+ */
+ OcrResult processImage(Path imagePath) throws OcrProcessException;
+
+ /**
+ * Add OCR-extracted text as a searchable layer to a PDF file.
+ *
+ * @param pdfPath Path to the source PDF file
+ * @param outputPath Path to save the modified PDF
+ * @param ocrResult OCR result containing extracted text to add
+ * @throws OcrProcessException if adding the text layer fails
+ */
+ void addTextLayerToPdf(Path pdfPath, Path outputPath, OcrResult ocrResult) throws OcrProcessException;
+
+ /**
+ * Set the language for OCR processing.
+ *
+ * @param language OCR language to use
+ * @throws OcrProcessException if the language is not supported or cannot be set
+ */
+ void setLanguage(OcrLanguage language) throws OcrProcessException;
+
+ /**
+ * Get supported languages for this OCR engine.
+ *
+ * @return Set of supported languages
+ */
+ Set getSupportedLanguages();
+
+ /**
+ * Get the name of the OCR engine.
+ *
+ * @return Engine name
+ */
+ String getEngineName();
+
+ /**
+ * Check if the OCR engine is available and properly configured.
+ *
+ * @return true if the engine is ready to use
+ */
+ boolean isAvailable();
+
+ /**
+ * Apply a specific configuration to the OCR engine.
+ *
+ * @param config Configuration to apply
+ * @throws OcrProcessException if the configuration cannot be applied
+ */
+ void applyConfig(OcrEngineConfig config) throws OcrProcessException;
+}
\ No newline at end of file
diff --git a/src/main/java/org/jabref/logic/ocr/engines/OcrEngineAdapter.java b/src/main/java/org/jabref/logic/ocr/engines/OcrEngineAdapter.java
new file mode 100644
index 00000000000..775dfb685e0
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ocr/engines/OcrEngineAdapter.java
@@ -0,0 +1,95 @@
+package org.jabref.logic.ocr.engines;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.jabref.logic.ocr.OcrService;
+import org.jabref.logic.ocr.exception.OcrProcessException;
+import org.jabref.logic.ocr.models.OcrEngineConfig;
+import org.jabref.logic.ocr.models.OcrLanguage;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base adapter class for OCR engines.
+ *
+ * This abstract class provides common functionality for OCR engine adapters,
+ * allowing engine-specific implementations to focus on their unique aspects.
+ */
+public abstract class OcrEngineAdapter implements OcrService {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(OcrEngineAdapter.class);
+
+ protected OcrLanguage currentLanguage;
+ protected OcrEngineConfig currentConfig;
+ protected final Set supportedLanguages = new HashSet<>();
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setLanguage(OcrLanguage language) throws OcrProcessException {
+ if (!supportedLanguages.contains(language)) {
+ throw new OcrProcessException(getEngineName(),
+ String.format("Language '%s' is not supported", language.getDisplayName()));
+ }
+
+ LOGGER.debug("Setting OCR language to: {}", language);
+ this.currentLanguage = language;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Set getSupportedLanguages() {
+ return new HashSet<>(supportedLanguages);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void applyConfig(OcrEngineConfig config) throws OcrProcessException {
+ if (!config.getEngineName().equals(getEngineName())) {
+ throw new OcrProcessException(getEngineName(),
+ String.format("Configuration is for engine '%s', not compatible with '%s'",
+ config.getEngineName(), getEngineName()));
+ }
+
+ LOGGER.debug("Applying configuration to {}: {}", getEngineName(), config);
+ this.currentConfig = config;
+ setLanguage(config.getLanguage());
+ }
+
+ /**
+ * Initialize supported languages.
+ *
+ * Subclasses should call this method to populate the supported languages.
+ *
+ * @param languages Set of supported languages
+ */
+ protected void initializeSupportedLanguages(Set languages) {
+ this.supportedLanguages.clear();
+ this.supportedLanguages.addAll(languages);
+ }
+
+ /**
+ * Add a supported language.
+ *
+ * @param language Language to add
+ */
+ protected void addSupportedLanguage(OcrLanguage language) {
+ this.supportedLanguages.add(language);
+ }
+
+ /**
+ * Get the current configuration.
+ *
+ * @return Current configuration or null if not configured
+ */
+ protected OcrEngineConfig getCurrentConfig() {
+ return currentConfig;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jabref/logic/ocr/engines/TesseractAdapter.java b/src/main/java/org/jabref/logic/ocr/engines/TesseractAdapter.java
new file mode 100644
index 00000000000..23fca674537
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ocr/engines/TesseractAdapter.java
@@ -0,0 +1,207 @@
+package org.jabref.logic.ocr.engines;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.jabref.logic.ocr.exception.OcrProcessException;
+import org.jabref.logic.ocr.models.OcrEngineConfig;
+import org.jabref.logic.ocr.models.OcrLanguage;
+import org.jabref.logic.ocr.models.OcrResult;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Adapter for Tesseract OCR engine.
+ *
+ * This class demonstrates how to implement an adapter for a specific OCR engine.
+ * For a real implementation, this would use the Tess4J library to interface with Tesseract.
+ */
+public class TesseractAdapter extends OcrEngineAdapter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TesseractAdapter.class);
+ private static final String ENGINE_NAME = "Tesseract";
+
+ // Path to Tesseract installation
+ private final String tesseractPath;
+
+ /**
+ * Create a new Tesseract adapter.
+ *
+ * @param tesseractPath Path to Tesseract installation (optional)
+ */
+ public TesseractAdapter(String tesseractPath) {
+ this.tesseractPath = tesseractPath;
+
+ // Initialize supported languages
+ initializeLanguages();
+
+ // Set default language (English)
+ try {
+ setLanguage(findLanguage("eng").orElseThrow(() ->
+ new OcrProcessException("Failed to find default language 'eng'")));
+ } catch (OcrProcessException e) {
+ LOGGER.error("Failed to set default language", e);
+ }
+ }
+
+ /**
+ * Initialize supported languages.
+ */
+ private void initializeLanguages() {
+ // Common languages
+ addSupportedLanguage(new OcrLanguage("eng", "English", false));
+ addSupportedLanguage(new OcrLanguage("deu", "German", false));
+ addSupportedLanguage(new OcrLanguage("fra", "French", false));
+ addSupportedLanguage(new OcrLanguage("spa", "Spanish", false));
+ addSupportedLanguage(new OcrLanguage("ita", "Italian", false));
+
+ // Ancient languages (relevant for ancient documents)
+ addSupportedLanguage(new OcrLanguage("grc", "Ancient Greek", true));
+ addSupportedLanguage(new OcrLanguage("lat", "Latin", true));
+ addSupportedLanguage(new OcrLanguage("san", "Sanskrit", true));
+ addSupportedLanguage(new OcrLanguage("cop", "Coptic", true));
+ }
+
+ /**
+ * Find a language by ISO code.
+ *
+ * @param isoCode ISO language code
+ * @return Language if found
+ */
+ private java.util.Optional findLanguage(String isoCode) {
+ return supportedLanguages.stream()
+ .filter(lang -> lang.getIsoCode().equals(isoCode))
+ .findFirst();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public OcrResult processPdf(Path pdfPath) throws OcrProcessException {
+ if (!isAvailable()) {
+ throw new OcrProcessException(ENGINE_NAME, "Tesseract is not available");
+ }
+
+ if (!Files.exists(pdfPath)) {
+ throw new OcrProcessException(ENGINE_NAME, "PDF file does not exist: " + pdfPath);
+ }
+
+ // In a real implementation, we would:
+ // 1. Use PDFBox to render PDF pages to images
+ // 2. Process each image with Tesseract
+ // 3. Collect results
+
+ // Placeholder implementation
+ LOGGER.info("Processing PDF with Tesseract: {}", pdfPath);
+ LOGGER.info("Using language: {}", currentLanguage.getDisplayName());
+
+ // Simulate OCR processing
+ String extractedText = "This is a placeholder OCR result for " + pdfPath.getFileName() +
+ " using language " + currentLanguage.getDisplayName() + ".";
+
+ // Create a result with sample confidence scores
+ Map pageConfidences = new HashMap<>();
+ pageConfidences.put(1, 90.5);
+
+ return new OcrResult.Builder()
+ .withExtractedText(extractedText)
+ .withAverageConfidence(90.5)
+ .withPageConfidenceMap(pageConfidences)
+ .withSourceFile(pdfPath)
+ .withEngineName(ENGINE_NAME)
+ .withLanguage(currentLanguage)
+ .build();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public OcrResult processImage(Path imagePath) throws OcrProcessException {
+ if (!isAvailable()) {
+ throw new OcrProcessException(ENGINE_NAME, "Tesseract is not available");
+ }
+
+ if (!Files.exists(imagePath)) {
+ throw new OcrProcessException(ENGINE_NAME, "Image file does not exist: " + imagePath);
+ }
+
+ // In a real implementation, we would:
+ // 1. Use Tess4J to process the image
+ // 2. Get text and confidence scores
+
+ // Placeholder implementation
+ LOGGER.info("Processing image with Tesseract: {}", imagePath);
+ LOGGER.info("Using language: {}", currentLanguage.getDisplayName());
+
+ // Simulate OCR processing
+ String extractedText = "This is a placeholder OCR result for image " + imagePath.getFileName() +
+ " using language " + currentLanguage.getDisplayName() + ".";
+
+ // Create result with sample confidence score
+ Map pageConfidences = new HashMap<>();
+ pageConfidences.put(1, 95.0);
+
+ return new OcrResult.Builder()
+ .withExtractedText(extractedText)
+ .withAverageConfidence(95.0)
+ .withPageConfidenceMap(pageConfidences)
+ .withSourceFile(imagePath)
+ .withEngineName(ENGINE_NAME)
+ .withLanguage(currentLanguage)
+ .build();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void addTextLayerToPdf(Path pdfPath, Path outputPath, OcrResult ocrResult) throws OcrProcessException {
+ if (!isAvailable()) {
+ throw new OcrProcessException(ENGINE_NAME, "Tesseract is not available");
+ }
+
+ if (!Files.exists(pdfPath)) {
+ throw new OcrProcessException(ENGINE_NAME, "PDF file does not exist: " + pdfPath);
+ }
+
+ // In a real implementation, we would:
+ // 1. Use PDFBox to add a text layer to the PDF
+ // 2. Save the result to outputPath
+
+ // Placeholder implementation
+ LOGGER.info("Adding text layer to PDF: {} -> {}", pdfPath, outputPath);
+
+ try {
+ // Just copy the file for now
+ Files.copy(pdfPath, outputPath);
+ } catch (IOException e) {
+ throw new OcrProcessException(ENGINE_NAME, "Failed to add text layer to PDF: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getEngineName() {
+ return ENGINE_NAME;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isAvailable() {
+ // In a real implementation, we would check if Tesseract is installed
+ // For now, just check if the path is set
+ return tesseractPath != null && !tesseractPath.isEmpty();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jabref/logic/ocr/exception/OcrProcessException.java b/src/main/java/org/jabref/logic/ocr/exception/OcrProcessException.java
new file mode 100644
index 00000000000..ecae5026493
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ocr/exception/OcrProcessException.java
@@ -0,0 +1,51 @@
+package org.jabref.logic.ocr.exception;
+
+import org.jabref.logic.JabRefException;
+
+/**
+ * Exception thrown when OCR processing fails.
+ *
+ * Follows JabRef's exception hierarchy by extending JabRefException.
+ */
+public class OcrProcessException extends JabRefException {
+
+ /**
+ * Create a new OCR process exception.
+ *
+ * @param message The error message
+ */
+ public OcrProcessException(String message) {
+ super(message);
+ }
+
+ /**
+ * Create a new OCR process exception with a cause.
+ *
+ * @param message The error message
+ * @param cause The underlying cause
+ */
+ public OcrProcessException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Create a new OCR process exception for a specific engine.
+ *
+ * @param engineName Name of the OCR engine
+ * @param message The error message
+ */
+ public OcrProcessException(String engineName, String message) {
+ super(String.format("OCR engine '%s': %s", engineName, message));
+ }
+
+ /**
+ * Create a new OCR process exception for a specific engine with a cause.
+ *
+ * @param engineName Name of the OCR engine
+ * @param message The error message
+ * @param cause The underlying cause
+ */
+ public OcrProcessException(String engineName, String message, Throwable cause) {
+ super(String.format("OCR engine '%s': %s", engineName, message), cause);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jabref/logic/ocr/models/OcrEngineConfig.java b/src/main/java/org/jabref/logic/ocr/models/OcrEngineConfig.java
new file mode 100644
index 00000000000..d405515d8ca
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ocr/models/OcrEngineConfig.java
@@ -0,0 +1,211 @@
+package org.jabref.logic.ocr.models;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Configuration options for OCR engines.
+ *
+ * This class follows JabRef's domain model pattern and provides a consistent
+ * way to configure different OCR engines through a common interface.
+ */
+public class OcrEngineConfig {
+ /**
+ * Quality presets for OCR processing.
+ */
+ public enum QualityPreset {
+ FAST("Optimize for speed (lower accuracy)"),
+ BALANCED("Balanced speed and accuracy"),
+ ACCURATE("Optimize for accuracy (slower processing)");
+
+ private final String description;
+
+ QualityPreset(String description) {
+ this.description = description;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+ }
+
+ private final String engineName;
+ private final OcrLanguage language;
+ private final Map engineSettings;
+ private final boolean preprocessImages;
+ private final int dpi;
+ private final QualityPreset qualityPreset;
+
+ private OcrEngineConfig(Builder builder) {
+ this.engineName = Objects.requireNonNull(builder.engineName);
+ this.language = Objects.requireNonNull(builder.language);
+ this.engineSettings = Collections.unmodifiableMap(new HashMap<>(builder.engineSettings));
+ this.preprocessImages = builder.preprocessImages;
+ this.dpi = builder.dpi;
+ this.qualityPreset = builder.qualityPreset;
+ }
+
+ /**
+ * Get the OCR engine name.
+ *
+ * @return Engine name
+ */
+ public String getEngineName() {
+ return engineName;
+ }
+
+ /**
+ * Get the OCR language.
+ *
+ * @return OCR language
+ */
+ public OcrLanguage getLanguage() {
+ return language;
+ }
+
+ /**
+ * Get engine-specific settings.
+ *
+ * @return Map of settings
+ */
+ public Map getEngineSettings() {
+ return engineSettings;
+ }
+
+ /**
+ * Get a specific engine setting.
+ *
+ * @param key Setting key
+ * @return Optional containing the setting value if present
+ */
+ public Optional getEngineSetting(String key) {
+ return Optional.ofNullable(engineSettings.get(key));
+ }
+
+ /**
+ * Check if image preprocessing is enabled.
+ *
+ * @return true if preprocessing is enabled
+ */
+ public boolean isPreprocessImages() {
+ return preprocessImages;
+ }
+
+ /**
+ * Get the DPI (dots per inch) setting for OCR.
+ *
+ * @return DPI value
+ */
+ public int getDpi() {
+ return dpi;
+ }
+
+ /**
+ * Get the quality preset.
+ *
+ * @return Quality preset
+ */
+ public QualityPreset getQualityPreset() {
+ return qualityPreset;
+ }
+
+ /**
+ * Builder for OcrEngineConfig.
+ */
+ public static class Builder {
+ private final String engineName;
+ private OcrLanguage language;
+ private final Map engineSettings = new HashMap<>();
+ private boolean preprocessImages = true;
+ private int dpi = 300;
+ private QualityPreset qualityPreset = QualityPreset.BALANCED;
+
+ /**
+ * Create a new builder.
+ *
+ * @param engineName OCR engine name
+ */
+ public Builder(String engineName) {
+ this.engineName = engineName;
+ }
+
+ /**
+ * Set the OCR language.
+ *
+ * @param language OCR language
+ * @return This builder
+ */
+ public Builder withLanguage(OcrLanguage language) {
+ this.language = language;
+ return this;
+ }
+
+ /**
+ * Add an engine-specific setting.
+ *
+ * @param key Setting key
+ * @param value Setting value
+ * @return This builder
+ */
+ public Builder withEngineSetting(String key, String value) {
+ this.engineSettings.put(key, value);
+ return this;
+ }
+
+ /**
+ * Add multiple engine settings.
+ *
+ * @param settings Map of settings
+ * @return This builder
+ */
+ public Builder withEngineSettings(Map settings) {
+ this.engineSettings.putAll(settings);
+ return this;
+ }
+
+ /**
+ * Set whether to preprocess images before OCR.
+ *
+ * @param preprocessImages true to enable preprocessing
+ * @return This builder
+ */
+ public Builder withPreprocessImages(boolean preprocessImages) {
+ this.preprocessImages = preprocessImages;
+ return this;
+ }
+
+ /**
+ * Set the DPI (dots per inch) for OCR.
+ *
+ * @param dpi DPI value
+ * @return This builder
+ */
+ public Builder withDpi(int dpi) {
+ this.dpi = dpi;
+ return this;
+ }
+
+ /**
+ * Set the quality preset.
+ *
+ * @param qualityPreset Quality preset
+ * @return This builder
+ */
+ public Builder withQualityPreset(QualityPreset qualityPreset) {
+ this.qualityPreset = qualityPreset;
+ return this;
+ }
+
+ /**
+ * Build the OcrEngineConfig.
+ *
+ * @return New OcrEngineConfig instance
+ */
+ public OcrEngineConfig build() {
+ return new OcrEngineConfig(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jabref/logic/ocr/models/OcrLanguage.java b/src/main/java/org/jabref/logic/ocr/models/OcrLanguage.java
new file mode 100644
index 00000000000..8dece2adfe6
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ocr/models/OcrLanguage.java
@@ -0,0 +1,99 @@
+package org.jabref.logic.ocr.models;
+
+import java.util.Objects;
+
+/**
+ * Represents a language for OCR processing.
+ *
+ * This class encapsulates language information with ISO code and display name.
+ * Follows JabRef's domain model pattern.
+ */
+public class OcrLanguage {
+ private final String isoCode;
+ private final String displayName;
+ private final boolean isAncient; // Special flag for ancient languages that might need special handling
+
+ /**
+ * Create a new OCR language.
+ *
+ * @param isoCode ISO 639-2 language code (e.g., "eng" for English)
+ * @param displayName Human-readable display name
+ * @param isAncient Whether this is an ancient language
+ */
+ public OcrLanguage(String isoCode, String displayName, boolean isAncient) {
+ this.isoCode = Objects.requireNonNull(isoCode);
+ this.displayName = Objects.requireNonNull(displayName);
+ this.isAncient = isAncient;
+ }
+
+ /**
+ * Create a new modern language for OCR.
+ *
+ * @param isoCode ISO 639-2 language code (e.g., "eng" for English)
+ * @param displayName Human-readable display name
+ * @return New OcrLanguage object
+ */
+ public static OcrLanguage createModernLanguage(String isoCode, String displayName) {
+ return new OcrLanguage(isoCode, displayName, false);
+ }
+
+ /**
+ * Create a new ancient language for OCR.
+ *
+ * @param isoCode ISO 639-2 language code (e.g., "grc" for Ancient Greek)
+ * @param displayName Human-readable display name
+ * @return New OcrLanguage object
+ */
+ public static OcrLanguage createAncientLanguage(String isoCode, String displayName) {
+ return new OcrLanguage(isoCode, displayName, true);
+ }
+
+ /**
+ * Get the ISO 639-2 language code.
+ *
+ * @return ISO language code
+ */
+ public String getIsoCode() {
+ return isoCode;
+ }
+
+ /**
+ * Get the human-readable display name.
+ *
+ * @return Display name
+ */
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ /**
+ * Check if this is an ancient language.
+ *
+ * @return true if this is an ancient language
+ */
+ public boolean isAncient() {
+ return isAncient;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ OcrLanguage that = (OcrLanguage) o;
+ return Objects.equals(isoCode, that.isoCode);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(isoCode);
+ }
+
+ @Override
+ public String toString() {
+ return displayName + " (" + isoCode + ")";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jabref/logic/ocr/models/OcrResult.java b/src/main/java/org/jabref/logic/ocr/models/OcrResult.java
new file mode 100644
index 00000000000..0c76eb14bbd
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ocr/models/OcrResult.java
@@ -0,0 +1,244 @@
+package org.jabref.logic.ocr.models;
+
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Domain model representing the result of an OCR processing operation.
+ *
+ * Contains the extracted text, confidence scores, and metadata.
+ * Follows JabRef's domain model patterns with immutability and builder pattern.
+ */
+public class OcrResult {
+ private final String extractedText;
+ private final double averageConfidence;
+ private final Map pageConfidenceMap;
+ private final Path sourceFile;
+ private final String engineName;
+ private final LocalDateTime processingTime;
+ private final Map metadata;
+ private final OcrLanguage language;
+
+ private OcrResult(Builder builder) {
+ this.extractedText = Objects.requireNonNull(builder.extractedText);
+ this.averageConfidence = builder.averageConfidence;
+ this.pageConfidenceMap = Collections.unmodifiableMap(new HashMap<>(builder.pageConfidenceMap));
+ this.sourceFile = builder.sourceFile;
+ this.engineName = builder.engineName;
+ this.processingTime = builder.processingTime != null ? builder.processingTime : LocalDateTime.now();
+ this.metadata = Collections.unmodifiableMap(new HashMap<>(builder.metadata));
+ this.language = builder.language;
+ }
+
+ /**
+ * Get the text extracted from the document.
+ *
+ * @return Extracted text
+ */
+ public String getExtractedText() {
+ return extractedText;
+ }
+
+ /**
+ * Get the average confidence score for the OCR result.
+ *
+ * @return Confidence score (0-100%)
+ */
+ public double getAverageConfidence() {
+ return averageConfidence;
+ }
+
+ /**
+ * Get confidence scores for individual pages.
+ *
+ * @return Map of page numbers to confidence scores
+ */
+ public Map getPageConfidenceMap() {
+ return pageConfidenceMap;
+ }
+
+ /**
+ * Get the source file that was processed.
+ *
+ * @return Optional containing the source file path if available
+ */
+ public Optional getSourceFile() {
+ return Optional.ofNullable(sourceFile);
+ }
+
+ /**
+ * Get the OCR engine name.
+ *
+ * @return Name of the OCR engine
+ */
+ public String getEngineName() {
+ return engineName;
+ }
+
+ /**
+ * Get the time when processing was completed.
+ *
+ * @return Processing completion time
+ */
+ public LocalDateTime getProcessingTime() {
+ return processingTime;
+ }
+
+ /**
+ * Get additional metadata from OCR processing.
+ *
+ * @return Map of metadata key-value pairs
+ */
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * Get the language used for OCR.
+ *
+ * @return Optional containing the language if available
+ */
+ public Optional getLanguage() {
+ return Optional.ofNullable(language);
+ }
+
+ /**
+ * Builder for OcrResult.
+ */
+ public static class Builder {
+ private String extractedText = "";
+ private double averageConfidence;
+ private final Map pageConfidenceMap = new HashMap<>();
+ private Path sourceFile;
+ private String engineName;
+ private LocalDateTime processingTime;
+ private final Map metadata = new HashMap<>();
+ private OcrLanguage language;
+
+ /**
+ * Set the extracted text.
+ *
+ * @param extractedText Text extracted by OCR
+ * @return This builder
+ */
+ public Builder withExtractedText(String extractedText) {
+ this.extractedText = extractedText;
+ return this;
+ }
+
+ /**
+ * Set the average confidence score.
+ *
+ * @param averageConfidence Confidence score (0-100%)
+ * @return This builder
+ */
+ public Builder withAverageConfidence(double averageConfidence) {
+ this.averageConfidence = averageConfidence;
+ return this;
+ }
+
+ /**
+ * Set confidence scores for individual pages.
+ *
+ * @param pageConfidenceMap Map of page numbers to confidence scores
+ * @return This builder
+ */
+ public Builder withPageConfidenceMap(Map pageConfidenceMap) {
+ this.pageConfidenceMap.putAll(pageConfidenceMap);
+ return this;
+ }
+
+ /**
+ * Add a confidence score for a specific page.
+ *
+ * @param pageNumber Page number (1-based)
+ * @param confidence Confidence score for the page
+ * @return This builder
+ */
+ public Builder withPageConfidence(int pageNumber, double confidence) {
+ this.pageConfidenceMap.put(pageNumber, confidence);
+ return this;
+ }
+
+ /**
+ * Set the source file.
+ *
+ * @param sourceFile Path to the source file
+ * @return This builder
+ */
+ public Builder withSourceFile(Path sourceFile) {
+ this.sourceFile = sourceFile;
+ return this;
+ }
+
+ /**
+ * Set the OCR engine name.
+ *
+ * @param engineName Name of the OCR engine
+ * @return This builder
+ */
+ public Builder withEngineName(String engineName) {
+ this.engineName = engineName;
+ return this;
+ }
+
+ /**
+ * Set the processing time.
+ *
+ * @param processingTime Time when processing was completed
+ * @return This builder
+ */
+ public Builder withProcessingTime(LocalDateTime processingTime) {
+ this.processingTime = processingTime;
+ return this;
+ }
+
+ /**
+ * Add a metadata entry.
+ *
+ * @param key Metadata key
+ * @param value Metadata value
+ * @return This builder
+ */
+ public Builder withMetadata(String key, String value) {
+ this.metadata.put(key, value);
+ return this;
+ }
+
+ /**
+ * Set multiple metadata entries.
+ *
+ * @param metadata Map of metadata entries
+ * @return This builder
+ */
+ public Builder withMetadata(Map metadata) {
+ this.metadata.putAll(metadata);
+ return this;
+ }
+
+ /**
+ * Set the language used for OCR.
+ *
+ * @param language OCR language
+ * @return This builder
+ */
+ public Builder withLanguage(OcrLanguage language) {
+ this.language = language;
+ return this;
+ }
+
+ /**
+ * Build the OcrResult.
+ *
+ * @return New OcrResult instance
+ */
+ public OcrResult build() {
+ return new OcrResult(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jabref/logic/ocr/pdf/OcrPdfProcessor.java b/src/main/java/org/jabref/logic/ocr/pdf/OcrPdfProcessor.java
new file mode 100644
index 00000000000..35fa16b130b
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ocr/pdf/OcrPdfProcessor.java
@@ -0,0 +1,162 @@
+package org.jabref.logic.ocr.pdf;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.jabref.logic.ocr.OcrService;
+import org.jabref.logic.ocr.exception.OcrProcessException;
+import org.jabref.logic.ocr.models.OcrResult;
+import org.jabref.logic.search.IndexManager;
+
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.text.PDFTextStripper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Processor for PDF OCR operations.
+ *
+ * This class coordinates the process of extracting text from PDFs using OCR,
+ * adding text layers, and indexing the results. It demonstrates how OCR would
+ * integrate with JabRef's PDF handling and search indexing.
+ */
+public class OcrPdfProcessor {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(OcrPdfProcessor.class);
+
+ private final OcrService ocrService;
+ private final IndexManager indexManager;
+
+ /**
+ * Create a new PDF OCR processor.
+ *
+ * @param ocrService OCR service to use
+ * @param indexManager Index manager for search indexing
+ */
+ public OcrPdfProcessor(OcrService ocrService, IndexManager indexManager) {
+ this.ocrService = ocrService;
+ this.indexManager = indexManager;
+ }
+
+ /**
+ * Process a PDF file with OCR and add a searchable text layer.
+ *
+ * @param pdfPath Path to the PDF file
+ * @param outputPath Path to save the processed PDF
+ * @return OCR result
+ * @throws OcrProcessException if processing fails
+ */
+ public OcrResult processAndAddTextLayer(Path pdfPath, Path outputPath) throws OcrProcessException {
+ // Check if PDF already has text
+ if (hasTextLayer(pdfPath)) {
+ LOGGER.info("PDF already has a text layer, no OCR needed: {}", pdfPath);
+ return new OcrResult.Builder()
+ .withExtractedText(extractTextFromPdf(pdfPath))
+ .withEngineName("Existing PDF Text")
+ .withSourceFile(pdfPath)
+ .build();
+ }
+
+ // Perform OCR
+ OcrResult result = ocrService.processPdf(pdfPath);
+
+ // Add text layer to PDF
+ TextLayerAdder.addTextLayer(pdfPath, outputPath, result);
+
+ return result;
+ }
+
+ /**
+ * Process a PDF and index the extracted text.
+ *
+ * @param pdfPath Path to the PDF file
+ * @param documentId Document ID for indexing
+ * @return OCR result
+ * @throws OcrProcessException if processing fails
+ */
+ public OcrResult processAndIndex(Path pdfPath, String documentId) throws OcrProcessException {
+ // Extract text from PDF (either existing or using OCR)
+ OcrResult result = hasTextLayer(pdfPath)
+ ? new OcrResult.Builder()
+ .withExtractedText(extractTextFromPdf(pdfPath))
+ .withEngineName("Existing PDF Text")
+ .withSourceFile(pdfPath)
+ .build()
+ : ocrService.processPdf(pdfPath);
+
+ // Index the extracted text
+ indexManager.addDocumentToIndex(documentId, result.getExtractedText());
+
+ return result;
+ }
+
+ /**
+ * Extract text from a PDF if it already has a text layer.
+ *
+ * @param pdfPath Path to the PDF file
+ * @return Extracted text
+ * @throws OcrProcessException if text extraction fails
+ */
+ private String extractTextFromPdf(Path pdfPath) throws OcrProcessException {
+ try (PDDocument document = PDDocument.load(pdfPath.toFile())) {
+ PDFTextStripper stripper = new PDFTextStripper();
+ return stripper.getText(document);
+ } catch (IOException e) {
+ throw new OcrProcessException("Failed to extract text from PDF: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Check if a PDF already has a text layer.
+ *
+ * @param pdfPath Path to the PDF file
+ * @return true if the PDF has a text layer
+ */
+ private boolean hasTextLayer(Path pdfPath) {
+ return TextLayerAdder.hasTextLayer(pdfPath);
+ }
+
+ /**
+ * Create a searchable text layer from OCR result.
+ *
+ * @param ocrResult OCR result
+ * @param pageCount Number of pages in the PDF
+ * @return Searchable text layer
+ */
+ public SearchableTextLayer createSearchableTextLayer(OcrResult ocrResult, int pageCount) {
+ Map pageTextMap = new HashMap<>();
+
+ // In a real implementation, we would have more sophisticated text distribution
+ String[] paragraphs = ocrResult.getExtractedText().split("\n\n");
+ int paragraphsPerPage = Math.max(1, (int) Math.ceil((double) paragraphs.length / pageCount));
+
+ StringBuilder currentPage = new StringBuilder();
+ int pageNumber = 1;
+ int paragraphCount = 0;
+
+ for (String paragraph : paragraphs) {
+ currentPage.append(paragraph).append("\n\n");
+ paragraphCount++;
+
+ if (paragraphCount >= paragraphsPerPage && pageNumber < pageCount) {
+ pageTextMap.put(pageNumber, currentPage.toString());
+ currentPage = new StringBuilder();
+ pageNumber++;
+ paragraphCount = 0;
+ }
+ }
+
+ // Add any remaining text
+ if (currentPage.length() > 0) {
+ pageTextMap.put(pageNumber, currentPage.toString());
+ }
+
+ return new SearchableTextLayer.Builder()
+ .withSourceText(ocrResult.getExtractedText())
+ .withPageTextMap(pageTextMap)
+ .withInvisible(true)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jabref/logic/ocr/pdf/SearchableTextLayer.java b/src/main/java/org/jabref/logic/ocr/pdf/SearchableTextLayer.java
new file mode 100644
index 00000000000..c718fb86aee
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ocr/pdf/SearchableTextLayer.java
@@ -0,0 +1,133 @@
+package org.jabref.logic.ocr.pdf;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Model class representing a searchable text layer for a PDF.
+ *
+ * This class stores the text content and position information
+ * needed to add a searchable layer to a PDF.
+ */
+public class SearchableTextLayer {
+
+ private final String sourceText;
+ private final Map pageTextMap;
+ private final boolean isInvisible;
+
+ /**
+ * Create a new searchable text layer.
+ *
+ * @param sourceText Source text from OCR
+ * @param pageTextMap Map of page numbers to page text
+ * @param isInvisible Whether the text should be invisible
+ */
+ private SearchableTextLayer(Builder builder) {
+ this.sourceText = Objects.requireNonNull(builder.sourceText);
+ this.pageTextMap = Collections.unmodifiableMap(new HashMap<>(builder.pageTextMap));
+ this.isInvisible = builder.isInvisible;
+ }
+
+ /**
+ * Get the original source text.
+ *
+ * @return Source text
+ */
+ public String getSourceText() {
+ return sourceText;
+ }
+
+ /**
+ * Get the text for a specific page.
+ *
+ * @param pageNumber Page number (1-based)
+ * @return Text for the page, or empty string if no text for that page
+ */
+ public String getPageText(int pageNumber) {
+ return pageTextMap.getOrDefault(pageNumber, "");
+ }
+
+ /**
+ * Get the page text map.
+ *
+ * @return Map of page numbers to page text
+ */
+ public Map getPageTextMap() {
+ return pageTextMap;
+ }
+
+ /**
+ * Check if the text layer should be invisible.
+ *
+ * @return true if the text should be invisible
+ */
+ public boolean isInvisible() {
+ return isInvisible;
+ }
+
+ /**
+ * Builder for SearchableTextLayer.
+ */
+ public static class Builder {
+ private String sourceText = "";
+ private final Map pageTextMap = new HashMap<>();
+ private boolean isInvisible = true;
+
+ /**
+ * Set the source text.
+ *
+ * @param sourceText Source text
+ * @return This builder
+ */
+ public Builder withSourceText(String sourceText) {
+ this.sourceText = sourceText;
+ return this;
+ }
+
+ /**
+ * Add text for a specific page.
+ *
+ * @param pageNumber Page number (1-based)
+ * @param text Text for the page
+ * @return This builder
+ */
+ public Builder withPageText(int pageNumber, String text) {
+ this.pageTextMap.put(pageNumber, text);
+ return this;
+ }
+
+ /**
+ * Set the page text map.
+ *
+ * @param pageTextMap Map of page numbers to page text
+ * @return This builder
+ */
+ public Builder withPageTextMap(Map pageTextMap) {
+ this.pageTextMap.clear();
+ this.pageTextMap.putAll(pageTextMap);
+ return this;
+ }
+
+ /**
+ * Set whether the text should be invisible.
+ *
+ * @param isInvisible true if the text should be invisible
+ * @return This builder
+ */
+ public Builder withInvisible(boolean isInvisible) {
+ this.isInvisible = isInvisible;
+ return this;
+ }
+
+ /**
+ * Build the searchable text layer.
+ *
+ * @return New SearchableTextLayer instance
+ */
+ public SearchableTextLayer build() {
+ return new SearchableTextLayer(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jabref/logic/ocr/pdf/TextLayerAdder.java b/src/main/java/org/jabref/logic/ocr/pdf/TextLayerAdder.java
new file mode 100644
index 00000000000..0310c8958f7
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ocr/pdf/TextLayerAdder.java
@@ -0,0 +1,199 @@
+package org.jabref.logic.ocr.pdf;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jabref.logic.ocr.exception.OcrProcessException;
+import org.jabref.logic.ocr.models.OcrResult;
+
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.PDPageContentStream;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.apache.pdfbox.pdmodel.font.PDFont;
+import org.apache.pdfbox.pdmodel.font.PDType1Font;
+import org.apache.pdfbox.pdmodel.font.encoding.WinAnsiEncoding;
+import org.apache.pdfbox.text.PDFTextStripper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility for adding searchable text layers to PDFs.
+ *
+ * This class demonstrates how to use PDFBox to add OCR-extracted text
+ * as a searchable layer to PDF documents.
+ */
+public class TextLayerAdder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TextLayerAdder.class);
+
+ private TextLayerAdder() {
+ // Utility class, no public constructor
+ }
+
+ /**
+ * Add a searchable text layer to a PDF.
+ *
+ * @param sourcePdf Path to source PDF
+ * @param outputPdf Path to save the output PDF
+ * @param ocrResult OCR result containing text to add
+ * @throws OcrProcessException if adding the text layer fails
+ */
+ public static void addTextLayer(Path sourcePdf, Path outputPdf, OcrResult ocrResult) throws OcrProcessException {
+ try (PDDocument document = PDDocument.load(sourcePdf.toFile())) {
+
+ // Split text into pages (in a real implementation, we would match text to page layout)
+ List pageTexts = splitTextIntoPages(ocrResult.getExtractedText(), document.getNumberOfPages());
+
+ // Process each page
+ for (int i = 0; i < document.getNumberOfPages(); i++) {
+ if (i < pageTexts.size()) {
+ addTextToPage(document, document.getPage(i), pageTexts.get(i));
+ }
+ }
+
+ // Save the document with the text layer
+ document.save(outputPdf.toFile());
+ LOGGER.info("Added searchable text layer to PDF: {}", outputPdf);
+
+ } catch (IOException e) {
+ throw new OcrProcessException("Failed to add text layer to PDF: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Add text to a PDF page as a searchable layer.
+ *
+ * In a real implementation, this would use PDFBox's more advanced features
+ * to properly position text according to the page layout.
+ *
+ * @param document PDF document
+ * @param page PDF page
+ * @param text Text to add
+ * @throws IOException if adding text fails
+ */
+ private static void addTextToPage(PDDocument document, PDPage page, String text) throws IOException {
+ PDRectangle pageRect = page.getMediaBox();
+ float fontSize = 1; // Very small to make it invisible
+
+ // Use a content stream to add text (in a real implementation, we would position text properly)
+ try (PDPageContentStream contentStream = new PDPageContentStream(
+ document, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
+
+ PDFont font = PDType1Font.HELVETICA;
+ contentStream.setFont(font, fontSize);
+ contentStream.beginText();
+
+ // Position text (in a real implementation, this would be more sophisticated)
+ contentStream.newLineAtOffset(0, 0);
+
+ // Set text rendering mode to invisible (3 = invisible)
+ contentStream.setRenderingMode(3);
+
+ // Add text, handling character encoding
+ addEncodedText(contentStream, text, font);
+
+ contentStream.endText();
+ }
+ }
+
+ /**
+ * Add text handling character encoding issues.
+ *
+ * This method breaks text into smaller chunks and handles characters
+ * that may not be supported by the font.
+ *
+ * @param contentStream PDF content stream
+ * @param text Text to add
+ * @param font PDF font
+ * @throws IOException if adding text fails
+ */
+ private static void addEncodedText(PDPageContentStream contentStream, String text, PDFont font) throws IOException {
+ // In a real implementation, we would handle character encoding more robustly
+ // For now, we'll just filter out characters that aren't in WinAnsiEncoding
+
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+ if (WinAnsiEncoding.INSTANCE.contains(c)) {
+ builder.append(c);
+ } else if (c == '\n') {
+ // Output current line and start a new one
+ contentStream.showText(builder.toString());
+ builder.setLength(0);
+ contentStream.newLine();
+ }
+ }
+
+ // Output any remaining text
+ if (builder.length() > 0) {
+ contentStream.showText(builder.toString());
+ }
+ }
+
+ /**
+ * Split text into pages.
+ *
+ * In a real implementation, this would use more sophisticated logic
+ * to match text to page layouts.
+ *
+ * @param text Full text
+ * @param numPages Number of pages
+ * @return List of text for each page
+ */
+ private static List splitTextIntoPages(String text, int numPages) {
+ List pages = new ArrayList<>();
+
+ // Simple approach: split by paragraphs and distribute
+ String[] paragraphs = text.split("\n\n");
+ int paragraphsPerPage = Math.max(1, (int) Math.ceil((double) paragraphs.length / numPages));
+
+ StringBuilder currentPage = new StringBuilder();
+ int paragraphCount = 0;
+
+ for (String paragraph : paragraphs) {
+ currentPage.append(paragraph).append("\n\n");
+ paragraphCount++;
+
+ if (paragraphCount >= paragraphsPerPage) {
+ pages.add(currentPage.toString());
+ currentPage = new StringBuilder();
+ paragraphCount = 0;
+ }
+ }
+
+ // Add any remaining text
+ if (currentPage.length() > 0) {
+ pages.add(currentPage.toString());
+ }
+
+ // If we didn't generate enough pages, add empty ones
+ while (pages.size() < numPages) {
+ pages.add("");
+ }
+
+ return pages;
+ }
+
+ /**
+ * Check if a PDF already has a text layer.
+ *
+ * @param pdfPath Path to PDF
+ * @return true if PDF has a text layer
+ */
+ public static boolean hasTextLayer(Path pdfPath) {
+ try (PDDocument document = PDDocument.load(pdfPath.toFile())) {
+ PDFTextStripper stripper = new PDFTextStripper();
+ String text = stripper.getText(document);
+
+ // If we get more than a threshold of characters, assume there's a text layer
+ return text.trim().length() > 50;
+
+ } catch (IOException e) {
+ LOGGER.error("Failed to check if PDF has text layer: {}", e.getMessage());
+ return false;
+ }
+ }
+}
\ No newline at end of file