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.