This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 8548ae052d5f camel-docling: honour the OCR headers in the CLI command 
(#29595)
8548ae052d5f is described below

commit 8548ae052d5f9f8e39668e41304371515f3e1cf5
Author: WMK666666 <[email protected]>
AuthorDate: Thu Nov 12 20:03:51 2026 +0800

    camel-docling: honour the OCR headers in the CLI command (#29595)
    
    camel-docling: honour the OCR headers in the CLI command and the serve 
request
    
    Rebased on main now that #29591 and #29593 are in.
    
    The only conflict was the end of the camel-docling section of the 4.24
    upgrade guide, where #29591 appended its apiKeyHeader entry: both entries
    are kept, the apiKeyHeader one first.
    
    DoclingProducer.java also gained the bearer-token client from #29593; it
    merged cleanly with the OCR header resolution, which sits in
    resolveOcrSettings and applyConfigurationToOptions.
    
    Content is otherwise the one approved in 80d7804: the two headers are
    resolved once per message into OcrSettings on the thread that owns the
    exchange, so no batch worker touches the exchange, and a language header
    does not turn OCR back on when the endpoint disabled it.
---
 .../camel/component/docling/DoclingProducer.java   | 166 +++++++++++++----
 .../component/docling/DoclingOcrHeadersTest.java   | 196 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  12 ++
 3 files changed, 339 insertions(+), 35 deletions(-)

diff --git 
a/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingProducer.java
 
b/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingProducer.java
index a68eb13156a4..430581c93030 100644
--- 
a/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingProducer.java
+++ 
b/components/camel-ai/camel-docling/src/main/java/org/apache/camel/component/docling/DoclingProducer.java
@@ -335,7 +335,7 @@ public class DoclingProducer extends DefaultProducer {
     private void processConvertToJSON(Exchange exchange) throws Exception {
         String inputPath = getInputPath(exchange);
         if (configuration.isUseDoclingServe()) {
-            ConvertDocumentRequest request = buildConvertRequest(inputPath, 
"json");
+            ConvertDocumentRequest request = buildConvertRequest(inputPath, 
"json", resolveOcrSettings(exchange));
             exchange.getIn().setBody(convertToDoclingDocument(request, 
exchange));
         } else {
             String result = executeDoclingCommand(inputPath, "json", exchange);
@@ -357,7 +357,7 @@ public class DoclingProducer extends DefaultProducer {
     private void processExtractStructuredData(Exchange exchange) throws 
Exception {
         String inputPath = getInputPath(exchange);
         if (configuration.isUseDoclingServe()) {
-            ConvertDocumentRequest request = 
buildStructuredDataRequest(inputPath);
+            ConvertDocumentRequest request = 
buildStructuredDataRequest(inputPath, resolveOcrSettings(exchange));
             exchange.getIn().setBody(convertToDoclingDocument(request, 
exchange));
         } else {
             String result = executeDoclingCommand(inputPath, "json", exchange);
@@ -382,7 +382,7 @@ public class DoclingProducer extends DefaultProducer {
         }
 
         // Start async conversion
-        ConvertDocumentRequest request = buildConvertRequest(inputPath, 
outputFormat);
+        ConvertDocumentRequest request = buildConvertRequest(inputPath, 
outputFormat, resolveOcrSettings(exchange));
         CompletableFuture<ConvertDocumentResponse> asyncResult
                 = 
doclingServeApi.convertSourceAsync(request).toCompletableFuture();
 
@@ -524,7 +524,7 @@ public class DoclingProducer extends DefaultProducer {
 
         if (configuration.isUseDoclingServe()) {
             // Use docling-serve API for metadata extraction
-            metadata = extractMetadataUsingApi(inputPath);
+            metadata = extractMetadataUsingApi(inputPath, exchange);
         } else {
             // Use CLI for metadata extraction
             metadata = extractMetadataUsingCLI(inputPath, exchange);
@@ -665,11 +665,11 @@ public class DoclingProducer extends DefaultProducer {
         }
     }
 
-    private DocumentMetadata extractMetadataUsingApi(String inputPath) throws 
IOException {
+    private DocumentMetadata extractMetadataUsingApi(String inputPath, 
Exchange exchange) throws IOException {
         LOG.debug("Extracting metadata using docling-java: {}", inputPath);
 
         // Convert the document to JSON format to get structured data 
including metadata
-        ConvertDocumentRequest request = buildConvertRequest(inputPath, 
"json");
+        ConvertDocumentRequest request = buildConvertRequest(inputPath, 
"json", resolveOcrSettings(exchange));
         ConvertDocumentResponse response;
         try {
             response = doclingServeApi.convertSource(request);
@@ -876,9 +876,14 @@ public class DoclingProducer extends DefaultProducer {
             useAsync = asyncModeHeader;
         }
 
+        // The OCR headers are read here, on the thread that owns the 
exchange: the workers below run in parallel and
+        // an exchange must not be shared between threads.
+        OcrSettings ocrSettings = resolveOcrSettings(exchange);
+
         // Process batch
         BatchProcessingResults results = convertDocumentsBatch(
-                documentPaths, outputFormat, batchSize, parallelism, 
failOnFirstError, useAsync, batchTimeout);
+                documentPaths, outputFormat, batchSize, parallelism, 
failOnFirstError, useAsync, batchTimeout,
+                ocrSettings);
 
         // Check if we should split results into individual exchanges
         boolean splitResults = configuration.isSplitBatchResults();
@@ -912,7 +917,7 @@ public class DoclingProducer extends DefaultProducer {
 
     private BatchProcessingResults convertDocumentsBatch(
             List<String> inputSources, String outputFormat, int batchSize, int 
parallelism,
-            boolean failOnFirstError, boolean useAsync, long batchTimeout) {
+            boolean failOnFirstError, boolean useAsync, long batchTimeout, 
OcrSettings ocrSettings) {
 
         LOG.info(
                 "Starting batch conversion of {} documents with batchSize={}, 
parallelism={}, failOnFirstError={}, timeout={}ms",
@@ -964,9 +969,9 @@ public class DoclingProducer extends DefaultProducer {
 
                             String converted;
                             if (useAsync) {
-                                converted = 
convertDocumentAsyncAndWait(inputSource, outputFormat);
+                                converted = 
convertDocumentAsyncAndWait(inputSource, outputFormat, ocrSettings);
                             } else {
-                                converted = convertDocumentSync(inputSource, 
outputFormat);
+                                converted = convertDocumentSync(inputSource, 
outputFormat, ocrSettings);
                             }
 
                             result.setResult(converted);
@@ -1185,12 +1190,13 @@ public class DoclingProducer extends DefaultProducer {
             }
         }
 
+        OcrSettings ocrSettings = resolveOcrSettings(exchange);
         if (useAsync) {
             LOG.debug("Using async mode for conversion");
-            return convertDocumentAsyncAndWait(inputPath, outputFormat);
+            return convertDocumentAsyncAndWait(inputPath, outputFormat, 
ocrSettings);
         } else {
             LOG.debug("Using sync mode for conversion");
-            return convertDocumentSync(inputPath, outputFormat);
+            return convertDocumentSync(inputPath, outputFormat, ocrSettings);
         }
     }
 
@@ -1282,9 +1288,13 @@ public class DoclingProducer extends DefaultProducer {
             useAsync = asyncModeHeader;
         }
 
+        // The OCR headers are read here, on the thread that owns the 
exchange: the workers below run in parallel and
+        // an exchange must not be shared between threads.
+        OcrSettings ocrSettings = resolveOcrSettings(exchange);
+
         // Process batch using structured data extraction
         BatchProcessingResults results = convertStructuredDataBatch(
-                documentPaths, batchSize, parallelism, failOnFirstError, 
useAsync, batchTimeout);
+                documentPaths, batchSize, parallelism, failOnFirstError, 
useAsync, batchTimeout, ocrSettings);
 
         // Check if we should split results
         boolean splitResults = configuration.isSplitBatchResults();
@@ -1313,7 +1323,7 @@ public class DoclingProducer extends DefaultProducer {
 
     private BatchProcessingResults convertStructuredDataBatch(
             List<String> inputSources, int batchSize, int parallelism,
-            boolean failOnFirstError, boolean useAsync, long batchTimeout) {
+            boolean failOnFirstError, boolean useAsync, long batchTimeout, 
OcrSettings ocrSettings) {
 
         LOG.info(
                 "Starting batch structured data extraction of {} documents 
with batchSize={}, parallelism={}, failOnFirstError={}, timeout={}ms",
@@ -1363,7 +1373,7 @@ public class DoclingProducer extends DefaultProducer {
                             LOG.debug("Extracting structured data from 
document {} (index {}): {}", documentId,
                                     currentIndex, inputSource);
 
-                            ConvertDocumentRequest request = 
buildStructuredDataRequest(inputSource);
+                            ConvertDocumentRequest request = 
buildStructuredDataRequest(inputSource, ocrSettings);
                             String converted;
                             if (useAsync) {
                                 converted = 
convertDocumentAsyncAndWait(request);
@@ -1458,10 +1468,11 @@ public class DoclingProducer extends DefaultProducer {
         return results;
     }
 
-    private String convertDocumentSync(String inputSource, String 
outputFormat) throws IOException {
+    private String convertDocumentSync(String inputSource, String 
outputFormat, OcrSettings ocrSettings)
+            throws IOException {
         LOG.debug("Converting document using docling-java (sync): {}", 
inputSource);
 
-        ConvertDocumentRequest request = buildConvertRequest(inputSource, 
outputFormat);
+        ConvertDocumentRequest request = buildConvertRequest(inputSource, 
outputFormat, ocrSettings);
 
         try {
             ConvertDocumentResponse response = 
doclingServeApi.convertSource(request);
@@ -1482,10 +1493,11 @@ public class DoclingProducer extends DefaultProducer {
         }
     }
 
-    private String convertDocumentAsyncAndWait(String inputSource, String 
outputFormat) throws IOException {
+    private String convertDocumentAsyncAndWait(String inputSource, String 
outputFormat, OcrSettings ocrSettings)
+            throws IOException {
         LOG.debug("Converting document using docling-java (async): {}", 
inputSource);
 
-        ConvertDocumentRequest request = buildConvertRequest(inputSource, 
outputFormat);
+        ConvertDocumentRequest request = buildConvertRequest(inputSource, 
outputFormat, ocrSettings);
 
         try {
             CompletionStage<ConvertDocumentResponse> asyncResult = 
doclingServeApi.convertSourceAsync(request);
@@ -1532,7 +1544,9 @@ public class DoclingProducer extends DefaultProducer {
         }
     }
 
-    private ConvertDocumentRequest buildConvertRequest(String inputSource, 
String outputFormat) throws IOException {
+    // package-private so DoclingOcrHeadersTest can assert the OCR options of 
the request without reflection
+    ConvertDocumentRequest buildConvertRequest(String inputSource, String 
outputFormat, OcrSettings ocrSettings)
+            throws IOException {
         ConvertDocumentRequest.Builder requestBuilder = 
ConvertDocumentRequest.builder();
 
         addSourceToRequest(requestBuilder, inputSource);
@@ -1542,13 +1556,14 @@ public class DoclingProducer extends DefaultProducer {
         if (outputFormat != null && !outputFormat.isEmpty()) {
             optionsBuilder.toFormat(mapToOutputFormat(outputFormat));
         }
-        applyConfigurationToOptions(optionsBuilder);
+        applyConfigurationToOptions(optionsBuilder, ocrSettings);
         requestBuilder.options(optionsBuilder.build());
 
         return requestBuilder.build();
     }
 
-    private ConvertDocumentRequest buildStructuredDataRequest(String 
inputSource) throws IOException {
+    private ConvertDocumentRequest buildStructuredDataRequest(String 
inputSource, OcrSettings ocrSettings)
+            throws IOException {
         ConvertDocumentRequest.Builder requestBuilder = 
ConvertDocumentRequest.builder();
 
         addSourceToRequest(requestBuilder, inputSource);
@@ -1561,7 +1576,7 @@ public class DoclingProducer extends DefaultProducer {
                 .doTableStructure(true);
 
         // Apply user configuration (can enable additional enrichment features)
-        applyConfigurationToOptions(optionsBuilder);
+        applyConfigurationToOptions(optionsBuilder, ocrSettings);
         requestBuilder.options(optionsBuilder.build());
 
         return requestBuilder.build();
@@ -1600,18 +1615,58 @@ public class DoclingProducer extends DefaultProducer {
         }
     }
 
-    private void applyConfigurationToOptions(ConvertDocumentOptions.Builder 
optionsBuilder) {
+    /**
+     * Resolves the OCR decisions of one message from the endpoint 
configuration and the two per-message headers.
+     * <p>
+     * The headers are read here, on the calling thread, and not where the 
request is built: a batch reads them once and
+     * hands the result down to its workers, which run in parallel and must 
not share the exchange. Package-private so
+     * that DoclingOcrHeadersTest can resolve them the way the producer does.
+     */
+    OcrSettings resolveOcrSettings(Exchange exchange) {
         // Send doOcr only when explicitly configured via the doOcr property,
         // or when enableOCR has been explicitly disabled. When both are at 
their
         // defaults (doOcr=null, enableOCR=true), let the server use its own 
defaults
         // to preserve backward compatibility.
-        if (configuration.getDoOcr() != null) {
-            optionsBuilder.doOcr(configuration.getDoOcr());
-            if (configuration.getDoOcr() && configuration.getOcrLanguage() != 
null) {
-                optionsBuilder.ocrLang(configuration.getOcrLanguage());
+        Boolean doOcr = configuration.getDoOcr();
+        String ocrLanguage = configuration.getOcrLanguage();
+        if (doOcr == null && !configuration.isEnableOCR()) {
+            doOcr = Boolean.FALSE;
+        }
+
+        // The per-message headers win over the endpoint configuration, as 
they do on the CLI path: a scanned
+        // attachment in an otherwise text-based route, or a foreign-language 
one, can then be OCRed without a
+        // second endpoint.
+        if (exchange != null) {
+            Boolean headerEnableOcr = 
exchange.getIn().getHeader(DoclingHeaders.ENABLE_OCR, Boolean.class);
+            if (headerEnableOcr != null) {
+                doOcr = headerEnableOcr;
+                if (!headerEnableOcr) {
+                    // an explicit "no OCR" must not carry a language along
+                    ocrLanguage = null;
+                }
+            }
+
+            String headerOcrLanguage = 
exchange.getIn().getHeader(DoclingHeaders.OCR_LANGUAGE, String.class);
+            if (ObjectHelper.isNotEmpty(headerOcrLanguage)) {
+                ocrLanguage = headerOcrLanguage;
+                if (doOcr == null) {
+                    // asking for a language is asking for OCR
+                    doOcr = Boolean.TRUE;
+                }
+            }
+        }
+
+        return new OcrSettings(doOcr, ocrLanguage);
+    }
+
+    private void applyConfigurationToOptions(ConvertDocumentOptions.Builder 
optionsBuilder, OcrSettings ocrSettings) {
+        Boolean doOcr = ocrSettings.getDoOcr();
+        String ocrLanguage = ocrSettings.getOcrLanguage();
+        if (doOcr != null) {
+            optionsBuilder.doOcr(doOcr);
+            if (doOcr && ObjectHelper.isNotEmpty(ocrLanguage)) {
+                optionsBuilder.ocrLang(ocrLanguage);
             }
-        } else if (!configuration.isEnableOCR()) {
-            optionsBuilder.doOcr(false);
         }
 
         if (configuration.getForceOcr() != null) {
@@ -2152,7 +2207,7 @@ public class DoclingProducer extends DefaultProducer {
         addOutputFormatArguments(command, outputFormat);
 
         // OCR configuration
-        addOcrArguments(command);
+        addOcrArguments(command, exchange);
 
         // Layout information
         addLayoutArguments(command);
@@ -2287,12 +2342,26 @@ public class DoclingProducer extends DefaultProducer {
         }
     }
 
-    private void addOcrArguments(List<String> command) {
-        if (!configuration.isEnableOCR()) {
+    /**
+     * Adds the OCR arguments, honouring the per-message headers when they are 
set: a scanned attachment in an otherwise
+     * text-based route, or a foreign-language one, can then be OCRed without 
a second endpoint.
+     */
+    private void addOcrArguments(List<String> command, Exchange exchange) {
+        Boolean enableOcr = 
exchange.getIn().getHeader(DoclingHeaders.ENABLE_OCR, Boolean.class);
+        if (enableOcr == null) {
+            enableOcr = configuration.isEnableOCR();
+        }
+
+        String ocrLanguage = 
exchange.getIn().getHeader(DoclingHeaders.OCR_LANGUAGE, String.class);
+        if (ObjectHelper.isEmpty(ocrLanguage)) {
+            ocrLanguage = configuration.getOcrLanguage();
+        }
+
+        if (!enableOcr) {
             command.add("--no-ocr");
-        } else if (configuration.getOcrLanguage() != null) {
+        } else if (ObjectHelper.isNotEmpty(ocrLanguage)) {
             command.add("--ocr-lang");
-            command.add(configuration.getOcrLanguage());
+            command.add(ocrLanguage);
         }
     }
 
@@ -2355,4 +2424,31 @@ public class DoclingProducer extends DefaultProducer {
         }
     }
 
+    /**
+     * The OCR decisions of one message, resolved from the endpoint 
configuration and the two OCR headers by
+     * {@link #resolveOcrSettings(Exchange)}.
+     * <p>
+     * A {@code null} {@code doOcr} means "not decided here, leave it to the 
docling-serve defaults". The CLI path
+     * reaches the same outcome through {@link #addOcrArguments(List, 
Exchange)}, which renders it as {@code --no-ocr} /
+     * {@code --ocr-lang}.
+     */
+    static final class OcrSettings {
+
+        private final Boolean doOcr;
+        private final String ocrLanguage;
+
+        OcrSettings(Boolean doOcr, String ocrLanguage) {
+            this.doOcr = doOcr;
+            this.ocrLanguage = ocrLanguage;
+        }
+
+        Boolean getDoOcr() {
+            return doOcr;
+        }
+
+        String getOcrLanguage() {
+            return ocrLanguage;
+        }
+    }
+
 }
diff --git 
a/components/camel-ai/camel-docling/src/test/java/org/apache/camel/component/docling/DoclingOcrHeadersTest.java
 
b/components/camel-ai/camel-docling/src/test/java/org/apache/camel/component/docling/DoclingOcrHeadersTest.java
new file mode 100644
index 000000000000..f323f1f43317
--- /dev/null
+++ 
b/components/camel-ai/camel-docling/src/test/java/org/apache/camel/component/docling/DoclingOcrHeadersTest.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.docling;
+
+import java.util.List;
+
+import ai.docling.serve.api.convert.request.options.ConvertDocumentOptions;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests the OCR of a single message, on both paths.
+ * <p>
+ * The component documentation has a "Using headers to control processing" 
section that sets
+ * {@code CamelDoclingEnableOCR} and {@code CamelDoclingOCRLanguage} per 
message, next to {@code CamelDoclingOperation},
+ * which is honoured. The two OCR headers were declared with {@code @Metadata} 
and documented but never read, so the
+ * example had no effect: every message was OCRed the way the endpoint was 
configured. Both paths now read them, the
+ * docling CLI command and the docling-serve request.
+ */
+class DoclingOcrHeadersTest extends CamelTestSupport {
+
+    private static final String SOURCE = "https://example.com/report.pdf";;
+    private static final String DEFAULT = 
"docling:convert?operation=CONVERT_TO_MARKDOWN";
+    private static final String OCR_OFF = 
"docling:convert?operation=CONVERT_TO_MARKDOWN&enableOCR=false&ocrLanguage=de";
+    private static final String OCR_ON_IN_FRENCH
+            = 
"docling:convert?operation=CONVERT_TO_MARKDOWN&doOcr=true&ocrLanguage=fr";
+
+    // --- the docling CLI command
+
+    @Test
+    void withoutHeadersTheEndpointConfigurationIsUsed() throws Exception {
+        List<String> command = commandFor(DEFAULT, null, null);
+
+        assertThat(command).contains("--ocr-lang", 
"en").doesNotContain("--no-ocr");
+    }
+
+    @Test
+    void enableOcrHeaderTurnsOcrOffForTheMessage() throws Exception {
+        // the endpoint has OCR on, this one message must not be OCRed, and no 
language may leak into the command
+        List<String> command = commandFor(DEFAULT, false, null);
+
+        assertThat(command).contains("--no-ocr").doesNotContain("--ocr-lang");
+    }
+
+    @Test
+    void ocrLanguageHeaderSelectsTheLanguageForTheMessage() throws Exception {
+        List<String> command = commandFor(DEFAULT, null, "es");
+
+        assertThat(command).contains("--ocr-lang", "es");
+    }
+
+    @Test
+    void headersWinOverTheEndpointConfiguration() throws Exception {
+        // the endpoint has OCR off and German, the message asks for Spanish 
OCR
+        List<String> command = commandFor(OCR_OFF, true, "es");
+
+        assertThat(command).contains("--ocr-lang", 
"es").doesNotContain("--no-ocr");
+    }
+
+    // --- the docling-serve request
+
+    @Test
+    void withoutHeadersTheServeRequestKeepsTheServerDefaults() throws 
Exception {
+        ConvertDocumentOptions options = serveOptions(DEFAULT, null, null);
+
+        assertThat(options.getDoOcr()).isNull();
+        assertThat(options.getOcrLang()).isNullOrEmpty();
+    }
+
+    @Test
+    void enableOcrHeaderTurnsOcrOffForTheServeRequest() throws Exception {
+        ConvertDocumentOptions options = serveOptions(DEFAULT, false, null);
+
+        assertThat(options.getDoOcr()).isFalse();
+        assertThat(options.getOcrLang()).isNullOrEmpty();
+    }
+
+    @Test
+    void ocrLanguageHeaderSelectsTheLanguageOfTheServeRequest() throws 
Exception {
+        ConvertDocumentOptions options = serveOptions(DEFAULT, null, "es");
+
+        assertThat(options.getDoOcr()).isTrue();
+        assertThat(options.getOcrLang()).containsExactly("es");
+    }
+
+    @Test
+    void theServeRequestFallsBackToTheLanguageOfTheEndpoint() throws Exception 
{
+        // the endpoint has OCR off and German, the message only asks for OCR
+        ConvertDocumentOptions options = serveOptions(OCR_OFF, true, null);
+
+        assertThat(options.getDoOcr()).isTrue();
+        assertThat(options.getOcrLang()).containsExactly("de");
+    }
+
+    @Test
+    void theDoOcrOptionOfTheEndpointIsStillSent() throws Exception {
+        ConvertDocumentOptions options = serveOptions(OCR_ON_IN_FRENCH, null, 
null);
+
+        assertThat(options.getDoOcr()).isTrue();
+        assertThat(options.getOcrLang()).containsExactly("fr");
+    }
+
+    @Test
+    void aLanguageHeaderDoesNotTurnOnOcrThatTheEndpointDisabledOnTheCli() 
throws Exception {
+        // the endpoint has OCR off, the message only names a language: OCR 
must stay off, as the upgrade guide says
+        List<String> command = commandFor(OCR_OFF, null, "es");
+
+        assertThat(command).contains("--no-ocr").doesNotContain("--ocr-lang");
+    }
+
+    @Test
+    void 
aLanguageHeaderDoesNotTurnOnOcrThatTheEndpointDisabledInTheServeRequest() 
throws Exception {
+        ConvertDocumentOptions options = serveOptions(OCR_OFF, null, "es");
+
+        assertThat(options.getDoOcr()).isFalse();
+        assertThat(options.getOcrLang()).isNullOrEmpty();
+    }
+
+    // --- the language is docling's own identifier, so it is passed on as it 
is
+
+    @Test
+    void doclingLanguageIdentifiersReachTheCli() throws Exception {
+        // docling also takes BCP-47 tags with a delimiter, script names and 
Tesseract combinations
+        assertThat(commandFor(DEFAULT, null, 
"iso:zh-Hant")).contains("--ocr-lang", "iso:zh-Hant");
+        assertThat(commandFor(DEFAULT, null, 
"script/Latin")).contains("--ocr-lang", "script/Latin");
+        assertThat(commandFor(DEFAULT, null, 
"eng+deu")).contains("--ocr-lang", "eng+deu");
+    }
+
+    @Test
+    void doclingLanguageIdentifiersReachTheServeRequest() throws Exception {
+        assertThat(serveOptions(DEFAULT, null, 
"iso:zh-Hant").getOcrLang()).containsExactly("iso:zh-Hant");
+    }
+
+    @Test
+    void aLanguageListWithASpaceReachesTheCli() throws Exception {
+        assertThat(commandFor(DEFAULT, null, "en, de")).contains("--ocr-lang", 
"en, de");
+    }
+
+    private List<String> commandFor(String endpointUri, Boolean enableOcr, 
String ocrLanguage) throws Exception {
+        DoclingEndpoint endpoint = context.getEndpoint(endpointUri, 
DoclingEndpoint.class);
+        DoclingProducer producer = (DoclingProducer) endpoint.createProducer();
+
+        return producer.buildDoclingCommand("input.pdf", "markdown", 
exchangeFor(endpoint, enableOcr, ocrLanguage),
+                "/tmp/managed");
+    }
+
+    private ConvertDocumentOptions serveOptions(String endpointUri, Boolean 
enableOcr, String ocrLanguage)
+            throws Exception {
+        DoclingEndpoint endpoint = context.getEndpoint(endpointUri, 
DoclingEndpoint.class);
+        DoclingProducer producer = (DoclingProducer) endpoint.createProducer();
+
+        Exchange exchange = exchangeFor(endpoint, enableOcr, ocrLanguage);
+
+        return producer.buildConvertRequest(SOURCE, "markdown", 
producer.resolveOcrSettings(exchange)).getOptions();
+    }
+
+    private Exchange exchangeFor(DoclingEndpoint endpoint, Boolean enableOcr, 
String ocrLanguage) {
+        Exchange exchange = endpoint.createExchange();
+        if (enableOcr != null) {
+            exchange.getIn().setHeader(DoclingHeaders.ENABLE_OCR, enableOcr);
+        }
+        if (ocrLanguage != null) {
+            exchange.getIn().setHeader(DoclingHeaders.OCR_LANGUAGE, 
ocrLanguage);
+        }
+
+        return exchange;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:noop").to("mock:noop");
+            }
+        };
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index d8a069d1ace4..e80edda3d353 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -8186,3 +8186,15 @@ The `apiKeyHeader` option never had any effect: the name 
of the header that carr
 docling-serve client, which always sends it as `X-Api-Key`, and the producer 
never read the option. It is now
 deprecated, with a `deprecationNote` that says so. Remove `apiKeyHeader` from 
the endpoint URI or the configuration:
 with `authenticationScheme=API_KEY` and `authenticationToken` set, the key is 
still sent, as `X-Api-Key`.
+
+=== camel-docling - the CamelDoclingEnableOCR and CamelDoclingOCRLanguage 
headers are read
+
+The `CamelDoclingEnableOCR` and `CamelDoclingOCRLanguage` headers were 
declared and documented as per-message
+overrides of `enableOCR` and `ocrLanguage`, but the component never read them: 
the message was OCRed the way the
+endpoint was configured. They now override those two settings for the message, 
on the docling CLI path and on the
+docling-serve path, as `CamelDoclingUseAsyncMode` already overrode 
`useAsyncMode`. A header that turns OCR off does
+not pass a language on; a language header alone turns OCR on, unless the 
endpoint has disabled it with
+`enableOCR=false`, which keeps winning over the header.
+
+The language itself is passed to docling untouched, in both forms: it is 
docling's own identifier (language codes such
+as `en,de`, BCP-47 tags such as `iso:zh-Hant`, or `script/Latin`), and docling 
validates it.

Reply via email to