Character Encoding Detection

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Baseline date: 2026-10-12. This page was written against Apache Tika 4.1.0 (the latest release on Maven Central on that date) with the 3.3.x differences noted, Java 21 and later for the examples (Tika 4.x needs Java 17), Spring Boot 4.x on Spring Framework 7 and the Docker image apache/tika:4.1.0. A text file is only bytes until somebody decides how to turn those bytes into characters. Tika’s parsers for plain text, HTML and other text-based formats have to make that decision for files that arrive without a reliable label, and a wrong decision silently corrupts every character outside ASCII: the index, the search result and the RAG chunk all carry the damage forward. This page explains the problem, the EncodingDetector extension point, the 4.x detection chain and how its parts cooperate, how to read text with a detected charset, how to configure or replace the chain and how to score the quality of a decoding yourself. The running example is legacy-catalogue.txt, a price list that the Bookshelf intake service received from a publisher whose systems still write windows-1252.

The detection of the media type (is this a PDF or a text file?) is a separate step, described in Media Types & Detection. Encoding detection starts only after the media type is known to be a text-based one. The first look at Tika.detect and Tika.parseToString is in Getting Started with the Tika Facade. The module map, including the three encoding-detector artifacts, is in Introduction & Architecture, and the dependencies are in Setup & Installation. For the way Java itself treats charsets and files see Java I/O and Files.

All examples on this page were compiled and run against Tika 4.1.0 (tika-core, tika-parsers-standard-package as a pom dependency, tika-serialization for TikaLoader, and, for the 3.x chain only, tika-encoding-detector-universal and tika-encoding-detector-icu4j). The sample files were generated for this page and are small: a five-line legacy-catalogue.txt (234 bytes), two short HTML pages and a Cyrillic text file. Detection on such short inputs is harder than on real documents, which makes them a fair stress test. The class and key names in the tables were checked against the 4.1.0 sources and the 4.1.x documentation; anything not run is marked.

Why charset detection matters

Computers store bytes; people read characters. An encoding (or charset) is the table that maps one to the other. ASCII fixes 128 characters in 7 bits. Everything else, from the accented letters of Spanish and French to the Euro sign, Cyrillic and Chinese, has been encoded in dozens of incompatible ways: the single-byte code pages of DOS and Windows (windows-1252, windows-1251, IBM850), the ISO-8859 family, the Mac and EBCDIC variants, the multi-byte East Asian encodings (Shift_JIS, GB18030, Big5) and finally Unicode, usually as UTF-8 and sometimes as UTF-16 or UTF-32.

A plain .txt file carries no label. The same byte 0xE1 is the letter á in windows-1252, с in windows-1251 and the second half of a broken sequence in UTF-8. Decoding with the wrong table gives mojibake (Japanese for "character transformation"): the file is readable as bytes, but the text is garbage such as Catálogo or Cat?logo. Four situations produce it in an intake service like Bookshelf:

  • a legacy export (a 1990s accounting system, a Windows-only tool) written in a code page nobody documented;

  • a web page whose <meta charset> tag says one thing while the server, or the file, did another;

  • an e-mail or upload whose Content-Type header names a charset that the sender’s software did not really use;

  • a file that has been converted twice, once correctly and once not.

Tika has to decide the charset itself for every text-based file, because the content handler it feeds expects characters, not bytes. The decision is made by a chain of encoding detectors, and the result is both used for decoding and recorded in the Metadata so that you can see what happened.

Charset detection applies to text-based formats such as plain text and HTML. Binary container formats (PDF, DOCX, images) store their text in their own internal encodings and do not go through this chain.

The Bookshelf sample: legacy-catalogue.txt

The publisher’s price list is a five-line text file saved in windows-1252 with Windows line endings. It was generated for this page, with only characters that exist in windows-1252 and that make the file invalid UTF-8:

text = ("Catálogo de libros — edición 1998\r\n"
        "Precio de «La sombra del viento»: 12,50 €\r\n"
        "Éditions Gallimard: « Le Petit Prince » – 8,90 €\r\n"
        "Autor: José Núñez Peña, Zaragoza. Reseña: ¡excelente!\r\n"
        "Stock: 14 ejemplares, niños y niñas (ñ, ü, ç, œ).\r\n")
with open("legacy-catalogue.txt", "wb") as f:
    f.write(text.encode("cp1252"))

The first bytes show why guessing is hard:

00000000: 4361 74e1 6c6f 676f 2064 6520 6c69 6272  Cat.logo de libr
00000010: 6f73 2097 2065 6469 6369 f36e 2031 3939  os . edici.n 199
00000020: 380d 0a50 7265 6369 6f20 6465 20ab 4c61  8..Precio de .La

E1 is á, 97 is the em dash, F3 is ó, AB is « and 80 (on a later line) is the Euro sign. None of these bytes forms a valid UTF-8 sequence, and 80 and 97 are control characters in ISO-8859-1, which is why labelling such files "Latin-1" is a common but wrong shortcut. Windows-1252 is the superset in which they are printable.

The first Java experiment decodes the same bytes with two wrong guesses and then asks Tika:

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import org.apache.tika.detect.AutoDetectReader;
import org.apache.tika.detect.DefaultEncodingDetector;
import org.apache.tika.detect.EncodingResult;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;

public class EncodingTour {

    public static void main(String[] args) throws Exception {
        Path file = Path.of(args[0]);
        byte[] raw = Files.readAllBytes(file);

        // 1. What goes wrong when you guess
        System.out.println("UTF-8 guess : " + secondLine(new String(raw, StandardCharsets.UTF_8)));

        // 2. The default chain, asked directly
        Metadata metadata = new Metadata();
        DefaultEncodingDetector detector = new DefaultEncodingDetector();
        try (TikaInputStream tis = TikaInputStream.get(file)) {
            List<EncodingResult> results = detector.detect(tis, metadata, new ParseContext());
            for (EncodingResult r : results) {
                System.out.println("candidate   : " + r + " label=" + r.getLabel()
                        + " decodeAs=" + r.getDecodeAs());
            }
        }
        System.out.println("tk:detected-encoding        = "
                + metadata.get(TikaCoreProperties.DETECTED_ENCODING));
        System.out.println("tk:encoding-detector        = "
                + metadata.get(TikaCoreProperties.ENCODING_DETECTOR));
        System.out.println("tk:encoding-detection-trace = "
                + metadata.get(TikaCoreProperties.ENCODING_DETECTION_TRACE));

        // 3. Read the text with the detected charset
        try (AutoDetectReader reader = new AutoDetectReader(Files.newInputStream(file))) {
            System.out.println("AutoDetectReader charset = " + reader.getCharset());
            System.out.println(reader.readLine());
        }
    }

    private static String secondLine(String s) {
        return s.lines().skip(1).findFirst().orElse("");
    }
}

Output on Tika 4.1.0 (the question marks are the Unicode replacement character that the terminal shows for bytes that are not valid UTF-8):

UTF-8 guess : Precio de ?La sombra del viento?: 12,50 ?
candidate   : windows-1252@1.00[STATISTICAL] label=windows-1252 decodeAs=windows-1252
tk:detected-encoding        = windows-1252
tk:encoding-detector        = MojibusterEncodingDetector
tk:encoding-detection-trace = MojibusterEncodingDetector->windows-1252[STATISTICAL](1.00)
AutoDetectReader charset = windows-1252
Catálogo de libros — edición 1998

Reading the output: decoding as UTF-8 destroys every non-ASCII byte; the default chain returns a single candidate, windows-1252, with confidence 1.00 and the result type STATISTICAL; the metadata records the winner, the detector that proposed it and a trace of all detectors that took part. The rest of this page explains each of those words.

The EncodingDetector SPI

All detectors implement org.apache.tika.detect.EncodingDetector in tika-core. In 4.x the interface is:

public interface EncodingDetector extends Serializable, SelfConfiguring {

    List<EncodingResult> detect(TikaInputStream tis, Metadata metadata,
                                ParseContext parseContext) throws IOException;
}

The contract, taken from the interface Javadoc:

  • It returns a ranked list of candidates, best first, or an empty list when the encoding cannot be determined. It never returns null.

  • The stream may be null when only metadata is available (for example, a charset passed in an HTTP header).

  • When a stream is given it supports mark and reset. The detector must mark before reading, reset before returning and must not close the stream.

  • The Metadata is input only. A detector reads hints from it and does not modify it. The composite detector writes the result keys, see Where the result is recorded.

EncodingResult and ResultType

Each candidate is an EncodingResult:

Member Meaning

getCharset()

The detected java.nio.charset.Charset. This is the charset to report (in Content-Type).

getDecodeAs()

The charset to decode with. It widens a subset to its superset when one exists, for example GBK to GB18030 or EUC-KR to x-windows-949, so that extension characters do not become mojibake. Otherwise it equals getCharset().

getConfidence()

A float from 0.0 to 1.0. It is meaningful for ranking STATISTICAL candidates. For DECLARATIVE and STRUCTURAL results it is conventionally 1.0 and carries no extra information.

getLabel()

The detector’s own label, which may be finer than the Java name: IBM420-ltr and IBM420-rtl both map to Java’s IBM420, and windows-874 maps to x-windows-874.

getResultType()

The kind of evidence, see below.

ResultType Evidence

DECLARATIVE

The document or its envelope states the encoding: a byte-order mark, an HTML <meta charset> tag, a Content-Type charset parameter. It records the author’s intent, not the truth.

STRUCTURAL

The byte grammar proves it: ISO-2022 escape sequences, a valid UTF-8 multi-byte sequence, a UTF-16 or UTF-32 pattern. Not a guess.

STATISTICAL

A probabilistic model’s inference. The confidence is meaningful for ranking.

The three types matter because the default chain does not simply take a declaration at its word; see JunkFilterEncodingDetector: the arbiter.

MetaEncodingDetector

MetaEncodingDetector is a marker sub-interface of EncodingDetector. A detector that implements it does not look at bytes first; it receives the candidates of all the other detectors and picks among them. Only one meta detector is supported in a chain. If a second is found, CompositeEncodingDetector logs a warning and ignores it. In the default chain the meta detector is JunkFilterEncodingDetector.

The 3.x contract

The 3.x interface is much smaller, and this is the form the book Tika in Action (chapter 4) describes for 1.x:

// Tika 3.x (the 3.3.2 Javadoc shows this signature); NOT the 4.x API
public interface EncodingDetector extends Serializable {
    Charset detect(InputStream input, Metadata metadata) throws IOException;
}

It returns a single Charset or null, with no confidence and no result type, and a composite takes the first non-null answer. Custom detectors written for 3.x must be rewritten for 4.x: the stream becomes a TikaInputStream, a ParseContext is added and the return type becomes a list of EncodingResult.

The 4.x detection chain

DefaultEncodingDetector assembles the chain through the Java service-provider interface (META-INF/services/org.apache.tika.detect.EncodingDetector). The 4.x chain has five members. The diagram shows the flow in the default (collect-all) mode that the next section explains.

flowchart TD IN["Bytes of a text-based file + Metadata hints"] --> BOM["BOMDetector: first 4 bytes"] IN --> META["MetadataCharsetDetector: Content-Type charset, hint, Content-Encoding"] IN --> HTML["HtmlEncodingDetector: meta charset tag"] IN --> MOJI["MojibusterEncodingDetector: UTF-32, UTF-16, UTF-8 gate, ISO-2022, Naive Bayes"] BOM -- "DECLARATIVE" --> CTX["EncodingDetectorContext: all candidates"] META -- "DECLARATIVE" --> CTX HTML -- "DECLARATIVE" --> CTX MOJI -- "STRUCTURAL or STATISTICAL" --> CTX CTX --> JUNK["JunkFilterEncodingDetector: pick by decode quality"] JUNK --> OUT["Ranked List of EncodingResult"] OUT --> MD["Metadata: tk:detected-encoding, tk:encoding-detector, tk:encoding-detection-trace"] OUT --> DEC["Decoder: Charset used by the parser"]
# Name in JSON Module Default Role

1

bom-detector (BOMDetector)

tika-core

yes

Looks at the first four bytes. A DECLARATIVE candidate for a UTF-8, UTF-16 or UTF-32 byte-order mark.

2

metadata-charset-detector (MetadataCharsetDetector)

tika-core

yes

Reads declared hints from Metadata without touching the stream. DECLARATIVE.

3

html-encoding-detector (HtmlEncodingDetector)

tika-encoding-detector-html

yes

A candidate from an HTML <meta charset> or http-equiv tag, found with a lenient regular expression and resolved through a curated table of WHATWG label aliases. DECLARATIVE.

4

mojibuster-encoding-detector (MojibusterEncodingDetector)

tika-encoding-detector-mojibuster

yes

Structural detectors for UTF-32, UTF-16 and ISO-2022, a UTF-8 grammar gate, HTML stripping and a 34-class Naive Bayes classifier over byte bigrams. STRUCTURAL or STATISTICAL.

5

junk-filter-encoding-detector (JunkFilterEncodingDetector)

tika-ml-junkdetect

yes, last

The MetaEncodingDetector that arbitrates the candidates by decode quality.

 — 

override-encoding-detector (OverrideEncodingDetector)

tika-core

no

Always returns the configured charset (charset, default UTF-8).

 — 

standard-html-encoding-detector (StandardHtmlEncodingDetector)

tika-encoding-detector-html

no

Spec-strict WHATWG prescan, opt in.

 — 

universal-encoding-detector (UniversalEncodingDetector)

tika-encoding-detector-universal

no

The juniversalchardet state-machine prober of 3.x. Not bundled and not auto-discovered.

 — 

icu4j-encoding-detector (Icu4jEncodingDetector)

tika-encoding-detector-icu4j

no

A vendored copy of ICU4J’s CharsetDetector. Not bundled and not auto-discovered.

tika-parsers-standard-package pulls in the three modules that make up the default chain, which was confirmed with mvn dependency:tree on 4.1.0: tika-encoding-detector-html, tika-encoding-detector-mojibuster and tika-ml-junkdetect. With a hand-picked set of modules (only tika-core and a few parsers) the chain shrinks to the detectors that are on the classpath: with tika-core alone you have the BOM and metadata detectors, which cannot decide a file that carries no declaration. Add the modules you need, or the whole standard package.

Collect-all or first-match-wins

How the members cooperate depends on one question: is a MetaEncodingDetector in the chain?

Mode Behaviour

Collect-all (a meta detector is present, the default)

Every base detector runs and contributes its candidates to an EncodingDetectorContext, which the meta detector then arbitrates. The registration order of the base detectors does not matter, and a declaration does not automatically win.

First-match-wins (no meta detector)

The detectors run in order and the first non-empty answer is the result. Order matters, and every declaration (BOM, header, <meta> tag) is authoritative when present.

The 4.x default is collect-all on purpose: the Tika documentation states that the trade-off is "occasional overrides of correct declarations on short probes for resilience against lying declarations and wrong-codec decodings". Section Configuring the chain shows how to choose the other mode.

The detectors one by one

BOMDetector

A byte-order mark is a few bytes at the start of a file that announce its Unicode encoding. BOMDetector reads the first four bytes (it needs at least two) and compares them with this table:

Leading bytes Charset

EF BB BF

UTF-8

FF FE 00 00

UTF-32LE

00 00 FE FF

UTF-32BE

FF FE

UTF-16LE

FE FF

UTF-16BE

The result is DECLARATIVE with confidence 1.0. The HTML detectors deliberately ignore BOMs, so BOMDetector is the only source of BOM evidence, which lets the arbiter decide when a BOM and a <meta> tag disagree. A UTF-8 BOM is optional and many files lack it; a file that has one is not necessarily valid UTF-8 afterwards, so the BOM is evidence, not a guarantee. AutoDetectReader drops a leading U+FEFF character after decoding (the code comments refer to TIKA-240) so that the BOM does not show up as a stray character in the text.

Run on a file that starts with EF BB BF, the default chain reported (4.1.0):

tk:encoding-detector        = BOMDetector
tk:encoding-detection-trace = BOMDetector->UTF-8[DECLARATIVE], MojibusterEncodingDetector->UTF-8[STRUCTURAL]
Content-Type                = text/plain; charset=UTF-8

Both detectors agreed on UTF-8, and the trace credits the BOM detector, the first to propose the winning charset.

MetadataCharsetDetector

This detector looks at the Metadata before any byte is read and checks three keys in this order:

  1. the charset parameter of Content-Type (for example text/html; charset=windows-1251),

  2. the charset parameter of the tk:content-type-hint property, a content type that a source claimed for the bytes (an HTML <meta> tag, or the UTF-8 flag of a ZIP entry). It is a hint, not a verdict, and is consulted only when this detector is in the active chain,

  3. a bare charset label in Content-Encoding, which parsers such as RFC822Parser set for mail bodies.

It normalises labels with the WHATWG rules. In particular ISO-8859-1 and US-ASCII are normalised to windows-1252, because browsers and the HTML standard treat them as aliases in practice. The result is DECLARATIVE.

This is how you pass the information that your caller already has. An upload handler that knows the HTTP Content-Type header, or a mail parser that has read a charset= parameter, should put it into the metadata rather than hope the detector re-derives it:

import java.nio.file.Path;

import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.BodyContentHandler;

public class HintedParse {

    public static void main(String[] args) throws Exception {
        Metadata metadata = new Metadata();
        // What the publisher's FTP gateway claimed. Both labels end up as windows-1252.
        metadata.set("Content-Type", "text/plain; charset=ISO-8859-1");
        metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, "legacy-catalogue.txt");

        AutoDetectParser parser = new AutoDetectParser();
        BodyContentHandler handler = new BodyContentHandler(-1);
        try (TikaInputStream tis = TikaInputStream.get(Path.of("legacy-catalogue.txt"))) {
            parser.parse(tis, handler, metadata, new ParseContext());
        }
        System.out.println(metadata.get(TikaCoreProperties.ENCODING_DETECTOR) + " | "
                + metadata.get("Content-Type"));
        System.out.println(metadata.get(TikaCoreProperties.ENCODING_DETECTION_TRACE));
    }
}

Output on Tika 4.1.0:

MetadataCharsetDetector | text/plain; charset=windows-1252
MetadataCharsetDetector->windows-1252[DECLARATIVE], MojibusterEncodingDetector->windows-1252[STATISTICAL](1.00)

The header said ISO-8859-1, Tika normalised it to windows-1252, the statistical detector independently agreed, and the declaration is credited. Note what the header would have been worth if it had been wrong: in the default chain the arbiter can still override it, as the next section shows.

HtmlEncodingDetector and StandardHtmlEncodingDetector

HTML declares its charset inside the document, in <meta charset="windows-1252"> or in the older <meta http-equiv="Content-Type" content="text/html; charset=windows-1252">. HtmlEncodingDetector finds it with a fast, lenient regular expression over the first markLimit bytes (8,192 by default, see TIKA-2485 for why scripts before the tag are a problem), and maps the label through a curated subset of the WHATWG alias table (TikaHtmlCharsetAliases).

StandardHtmlEncodingDetector implements the full WHATWG pre-scan algorithm. Use it when leniency produces false matches, for example when a charset declaration appears inside an HTML comment that the regular expression should have skipped. It is not in the default chain.

MojibusterEncodingDetector

Mojibuster is the statistical heart of the 4.x chain. Its layers run in order, and the first confident structural answer ends the run; otherwise the bytes fall through to the classifier:

  1. UTF-32 structure. Every group of four bytes is read as a 32-bit value, in both byte orders, and checked for validity (at most U+10FFFF, no surrogates). Valid code points occupy about 0.004 percent of the 32-bit space, so bytes that are not UTF-32 almost always fail within the first eight bytes.

  2. UTF-16 specialist. A small classifier looks at the distribution of bytes at even versus odd positions, the property that separates UTF-16 from the legacy single-byte and multi-byte encodings. UTF-16 lives outside the main classifier because byte-pair features cannot tell UTF-16 CJK text from legacy CJK encodings.

  3. UTF-8 grammar gate. The bytes are walked for valid lead and continuation sequences. A valid grammar with at least one complete multi-byte sequence emits UTF-8 as a STRUCTURAL candidate; a violated grammar removes UTF-8 from the classifier’s output; pure ASCII or a truncated sequence is ambiguous and emits nothing.

  4. ISO-2022-JP, -KR and -CN. These encodings are 7-bit and escape-based, so they contain no high bytes and are invisible to the classifier. When the bytes are pure ASCII the pipeline looks for the designation escape and verifies it by decoding: the result must contain real CJK text at a near-zero replacement rate.

  5. HTML stripping. If the bytes look like HTML or XML, a byte-level state machine removes tags, scripts, styles and comments but keeps text-bearing attribute values (alt, title, placeholder, aria-label), so the classifier sees the content and not the markup.

  6. Naive Bayes. A 34-class classifier over byte bigrams: CJK multi-byte (Big5-HKSCS, EUC-JP, GB18030, Shift_JIS, x-EUC-TW, x-windows-949), EBCDIC (IBM420, IBM424, IBM500, IBM1047), DOS (IBM850, IBM852, IBM855, IBM866), Cyrillic (KOI8-R, KOI8-U), the Windows single-byte code pages 1250 to 1258 and 874, ISO-8859-2, -3 and -16, the Mac variants and UTF-8.

The classifier has several safeguards that explain behaviour you will see on small files:

Safeguard Effect

Fewer than two bytes

windows-1252 at confidence 0.1, "the WHATWG default"; the detector never returns an empty result.

Pure-ASCII probe

ISO-2022 detection first, otherwise windows-1252: bigram statistics cannot tell Latin code pages apart on ASCII.

Latin-sibling rewrite

On low-evidence probes (fewer than five high bytes), if the best candidate is a non-1252 member of the Latin family and the probe decodes identically under windows-1252, the result is rewritten to windows-1252.

Margin-gated emission

At most five candidates, and a runner-up only if its score is within a margin of the best. The confidence is the linear distance within that margin, not a softmax.

CJK decode-failure veto

A legacy CJK charset chosen on bytes that do not validate under it (a decoding failure rate above roughly 2.5 percent) is dropped. If it was the only pick, windows-1252 wins.

Run on 4.1.0, the small cases show these safeguards:

file                          bytes of interest             result
short.txt  ("Hi")             pure ASCII, 2 bytes           windows-1252@0.10 [STATISTICAL]
cafe.txt   ("Caf" + E9)       one high byte                 windows-1252@1.00 [STATISTICAL]
u8.txt     (UTF-8, no BOM)    valid multi-byte sequences    UTF-8@0.95 [STRUCTURAL]
u16.txt    (UTF-16LE, no BOM) even/odd byte structure       UTF-16LE@0.95 [STRUCTURAL] (label UTF-16-LE)
cyr.txt    (windows-1251)     Cyrillic letters              windows-1251@1.00 [STATISTICAL]

The model sources and the training procedure are described in the design document. The same document states plainly that no accuracy figures are published, because the last full evaluation predates a requantization of the model and no test asserts an accuracy floor; it gives only a qualitative comparison, which is that Mojibuster roughly doubles the strict accuracy of ICU4J and roughly triples that of juniversalchardet, at an order of magnitude less latency than ICU4J. Treat that as the project’s own claim, which this page did not reproduce. The design document also names EvalCharsetDetectors as the tool for measuring the shipped model on your own corpus.

JunkFilterEncodingDetector: the arbiter

JunkFilterEncodingDetector is a MetaEncodingDetector backed by JunkDetector, a script-aware scorer that tells clean text from mojibake, wrong-codec decodings and corrupted bytes. Its presence switches the composite into collect-all mode. It arbitrates over all candidate types, DECLARATIVE included, because real-world declarations are unreliable. The outline of its decision:

  1. One unique candidate: it abstains and the composite’s default order stands.

  2. HTML bytes are stripped of markup first, so that the score reflects the body text; if no tags are found the raw probe is used.

  3. Every distinct candidate charset decodes the bytes, in insertion order, for a deterministic result.

  4. If a DECLARATIVE candidate decodes to the same string as another candidate, it wins at once. When the decodings are identical, text quality cannot separate the two, and honouring the declaration makes sure that bytes which differ in other charsets (0xA4 is a Euro sign in ISO-8859-15 and a currency sign in windows-1252) decode as the author intended.

  5. If all decodings are identical and none is declarative, it abstains.

  6. Otherwise a pairwise tournament compares the candidates by text quality, and the higher score wins.

  7. Two demote gates run afterwards. A CJK-family gate demotes a CJK champion if the best non-CJK candidate beats it by a margin on the characters where the candidates differ; a Latin-letter gate demotes a DOS or Mac code page (IBM850, x-MacRoman) to windows-1252 if windows-1252 decodes clearly more cased high-byte letters. Neither can promote a candidate.

Because declarations are only one input, a lying <meta> tag does not defeat the default chain. The second sample is reviews-es.html's cousin, a Spanish review page whose bytes are windows-1252 but whose <meta> tag says UTF-8:

// lying.html: <meta charset="utf-8"> in the head, but the bytes are windows-1252
AutoDetectParser parser = new AutoDetectParser();
Metadata metadata = new Metadata();
BodyContentHandler handler = new BodyContentHandler(-1);
try (TikaInputStream tis = TikaInputStream.get(Path.of("lying.html"))) {
    parser.parse(tis, handler, metadata, new ParseContext());
}
System.out.println(metadata.get(TikaCoreProperties.ENCODING_DETECTION_TRACE));
System.out.println(metadata.get("Content-Type"));
System.out.println(handler.toString().strip());

Output on Tika 4.1.0:

HtmlEncodingDetector->UTF-8[DECLARATIVE], MojibusterEncodingDetector->windows-1252[STATISTICAL](1.00) [junk-filter-selected]
text/html; charset=windows-1252
La reseña: «excelente» — niños, ñandú, canción, árbol, pingüino.

The two detectors disagreed, the arbiter decoded both ways, found that UTF-8 produced replacement characters and windows-1252 produced clean Spanish, and chose windows-1252 (junk-filter-selected in the trace). With an honest declaration the outcome is the same, and the declaration is credited:

HtmlEncodingDetector->windows-1252[DECLARATIVE], MojibusterEncodingDetector->windows-1252[STATISTICAL](1.00)

Where the result is recorded

The composite detector records its decision in the Metadata of the document, and the parsers add the decoded charset to the Content-Type they report. All keys use the 4.x tk: namespace; the 3.x names were in the X-TIKA: namespace, which LegacyKeyMigrationFilter can restore (the rename is explained in Metadata). The key names below were read from TikaCoreProperties in the 4.1.0 sources and observed in the runs above.

Key (4.x) Content

tk:detected-encoding

The charset name an EncodingDetector produced. Different from Content-Encoding, which is what a parser chose to use. If detection returns nothing and a parser falls back to a default, the two differ, and that is why both exist.

tk:encoding-detector

The simple class name of the detector whose result was used. In collect-all mode the base detector that first proposed the winning charset is credited.

tk:encoding-detection-trace

A diagnostic string: every detector that took part with its charset, result type and (for statistical results) confidence, then the arbitration outcome. Example: JunkFilterEncodingDetector adds a suffix such as [junk-filter-selected].

tk:decoded-charset

Set only when a superset was used for decoding (getDecodeAs() differed from getCharset()), for example GB18030 instead of GB2312.

Content-Type

The media type with a charset= parameter, for example text/plain; charset=windows-1252.

Content-Encoding

The charset the parser used.

The full parse of legacy-catalogue.txt with AutoDetectParser produced (Tika 4.1.0, keys in map order):

tk:encoding-detection-trace = MojibusterEncodingDetector->windows-1252[STATISTICAL](1.00)
tk:detected-encoding        = windows-1252
tk:content-type-magic-detected = text/plain
tk:encoding-detector        = MojibusterEncodingDetector
Content-Encoding            = windows-1252
tk:parsed-by                = org.apache.tika.parser.DefaultParser
Content-Type                = text/plain; charset=windows-1252
When a detector is used directly, as EncodingTour did with detect(...), the composite writes these keys to the Metadata you pass. A first-match-wins chain writes tk:detected-encoding and tk:encoding-detector but, in 4.1.0, no trace (the trace was null in the runs). OverrideEncodingDetector writes nothing, because it bypasses the composite.

Log the three tk: keys with every ingested text file. When a user reports garbled text in the search index, the trace tells you which detector decided and what the others said, which is usually enough to see why.

AutoDetectReader

Not every use of detection is a full parse. org.apache.tika.detect.AutoDetectReader is a BufferedReader that detects the charset and decodes in one step, which is what you need to read a text file of unknown encoding in your own code:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import org.apache.tika.detect.AutoDetectReader;
import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;

public class ReadLegacyCatalogue {

    public static void main(String[] args) throws IOException, TikaException {
        Metadata metadata = new Metadata();   // may carry hints such as Content-Type
        try (AutoDetectReader reader =
                     new AutoDetectReader(Files.newInputStream(Path.of("legacy-catalogue.txt")), metadata)) {
            System.out.println("charset: " + reader.getCharset());
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        }
    }
}

Output:

charset: windows-1252
Catálogo de libros — edición 1998
Precio de «La sombra del viento»: 12,50 €
Éditions Gallimard: « Le Petit Prince » – 8,90 €
Autor: José Núñez Peña, Zaragoza. Reseña: ¡excelente!
Stock: 14 ejemplares, niños y niñas (ñ, ü, ç, œ).

The class offers these constructors (all in tika-core):

Constructor Use

AutoDetectReader(InputStream)

Default chain, empty metadata.

AutoDetectReader(InputStream, Metadata)

Default chain; hints from the metadata are honoured and the detection keys are written to it.

AutoDetectReader(InputStream, Metadata, EncodingDetector)

Your own detector or chain, for example one loaded with TikaLoader.

AutoDetectReader(InputStream, Metadata, ServiceLoader)

A detector built from a custom service loader.

Other members: getCharset() returns the charset in use, and asInputSource() gives a SAX InputSource with the encoding set, for feeding an XML or HTML parser.

Two behaviours in the 4.1.0 source are worth knowing:

  • Fallback chain. If the detector returns nothing, the reader consults the charset parameter of Content-Type and then Content-Encoding. If those are missing too, it falls back to ISO-8859-1 and records tk:detected-encoding with the detector name AutoDetectReader-default-fallback. The code comments say this keeps the 3.x default, and it differs from the windows-1252 that the 4.x chain itself uses for ASCII-only input. In practice the default chain does not return an empty result, so you would see the fallback only with a custom chain.

  • Superset decoding. The reader decodes with getDecodeAs() and records tk:decoded-charset when it differs from the detected charset.

Always use try-with-resources: the reader owns the stream.

Choosing the detector for one parse

A parser that needs a charset asks the ParseContext first. The base class of the text-based parsers (AbstractEncodingDetectorParser) reads parseContext.get(EncodingDetector.class) and falls back to the detector it was created with. That makes a per-parse override a single line, which is useful when the intake service knows the encoding of a particular supplier:

import java.nio.charset.Charset;
import java.nio.file.Path;

import org.apache.tika.detect.EncodingDetector;
import org.apache.tika.detect.OverrideEncodingDetector;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.BodyContentHandler;

public class PerSupplierEncoding {

    public static void main(String[] args) throws Exception {
        ParseContext context = new ParseContext();
        // Supplier "Gallimard Legacy" is documented to send windows-1252.
        context.set(EncodingDetector.class, new OverrideEncodingDetector(Charset.forName("windows-1252")));

        AutoDetectParser parser = new AutoDetectParser();
        Metadata metadata = new Metadata();
        BodyContentHandler handler = new BodyContentHandler(-1);
        try (TikaInputStream tis = TikaInputStream.get(Path.of("legacy-catalogue.txt"))) {
            parser.parse(tis, handler, metadata, context);
        }
        System.out.println(metadata.get("Content-Type"));
        System.out.println(handler.toString().lines().skip(1).findFirst().orElse(""));
    }
}

Output with windows-1252 and, to show the risk, with UTF-8:

text/plain; charset=windows-1252
Precio de «La sombra del viento»: 12,50 €

text/plain; charset=UTF-8
Precio de ?La sombra del viento?: 12,50 ?

An override replaces detection completely: nothing checks whether the bytes make sense in the charset you forced. Use it only where the encoding is a fact of your pipeline, and consider keeping the detector but checking its answer against the expected one (see Text quality scoring).

Configuring the chain

Detectors are components of the 4.x JSON configuration, under the top-level encoding-detectors array of tika-config.json. The names in the table of the chain are the JSON names. Load the configuration with TikaLoader.load(path) from tika-serialization (see Configuration in this section for the full file layout) and ask it for the encoding detector:

import java.nio.file.Path;

import org.apache.tika.config.loader.TikaLoader;
import org.apache.tika.detect.EncodingDetector;
import org.apache.tika.detect.EncodingResult;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;

public class ConfiguredChain {

    public static void main(String[] args) throws Exception {
        TikaLoader loader = TikaLoader.load(Path.of("tika-config.json"));

        // 1. Only the encoding detectors
        EncodingDetector detector = loader.loadEncodingDetectors();
        Metadata metadata = new Metadata();
        try (TikaInputStream tis = TikaInputStream.get(Path.of("legacy-catalogue.txt"))) {
            for (EncodingResult r : detector.detect(tis, metadata, new ParseContext())) {
                System.out.println(r);
            }
        }

        // 2. A whole AutoDetectParser that uses the configured chain
        Parser parser = loader.loadAutoDetectParser();
    }
}

Both loadEncodingDetectors() and loadAutoDetectParser() exist in the 4.1.0 TikaLoader. The chain defined in the encoding-detectors array was honoured by loadAutoDetectParser() in the runs for this page (see the first-match-wins example below, where a lying declaration was obeyed), which is how the encoding settings reach the parsers.

Exclude a detector

default-encoding-detector is the SPI-discovered chain, and exclude drops members of it:

{
  "encoding-detectors": [
    {
      "default-encoding-detector": {
        "exclude": ["html-encoding-detector"]
      }
    }
  ]
}

Run on the HTML sample, the chain then consisted of Mojibuster only (MojibusterEncodingDetector->windows-1252), the <meta> tag being ignored. Do not put default-encoding-detector and explicit detectors in the same list: the loader wraps everything in an outer composite that has no meta detector at its top level, so the collect-all arbitration is lost silently and the explicit detectors are never reached (the 4.1.x documentation warns about this).

Specify the chain explicitly

An explicit ordered list replaces the discovered chain. End it with junk-filter-encoding-detector to keep collect-all arbitration; omit it for first-match-wins. This is also the only way to set an option of an individual detector, such as the HTML detector’s read limit:

{
  "encoding-detectors": [
    {"html-encoding-detector": {"markLimit": 131072}},
    {"mojibuster-encoding-detector": {}},
    {"junk-filter-encoding-detector": {}}
  ]
}

markLimit is the number of bytes the HTML detector scans for the <meta> tag. The default is 8,192. Raise it if your pages put large <script> blocks before the tag. Mojibuster reads a larger probe of its own, so with the default chain the HTML limit matters mainly for very large preambles. Loaded with TikaLoader on 4.1.0, this configuration produced the expected three-detector chain (the HTML candidate and the Mojibuster candidate both appeared in the trace); the effect of a raised markLimit itself was not measured.

The spec-strict HTML detector

{
  "encoding-detectors": [
    {"standard-html-encoding-detector": {}},
    {"mojibuster-encoding-detector": {}},
    {"junk-filter-encoding-detector": {}}
  ]
}

First-match-wins: declarations are authoritative

When you can trust the declarations (a controlled upstream that labels its files correctly) and want predictable, cheaper behaviour, drop the meta detector and list the detectors in priority order:

{
  "encoding-detectors": [
    {"bom-detector": {}},
    {"metadata-charset-detector": {}},
    {"standard-html-encoding-detector": {}},
    {"mojibuster-encoding-detector": {}}
  ]
}

The classifier then runs only when no declaration fires. The cost shows with the lying page from the previous section. Parsing lying.html with a TikaLoader-built parser using this chain gave:

parser chain -> UTF-8 by StandardHtmlEncodingDetector | text/html; charset=UTF-8
La rese?a: ?excelente? ? ni?os, ?and?, canci?n, ?rbol, ping?ino.

The false declaration won and the text is corrupted, which is exactly the trade-off the documentation describes: "lying declarations propagate unfiltered". Choose this mode only for sources you control.

A fixed charset for a whole corpus

For a corpus whose encoding you already know, configure the override detector, which always returns its charset (default UTF-8):

{
  "encoding-detectors": [
    {"override-encoding-detector": {"charset": "windows-1252"}}
  ]
}

On 4.1.0 this chain returned windows-1252 as DECLARATIVE for the HTML sample regardless of content.

Restoring the 3.x chain: universal and ICU4J

Tika 3.x used a first-match-wins chain of the HTML detector, juniversalchardet (UniversalEncodingDetector) and ICU4J (Icu4jEncodingDetector). Both of the latter are still published in 4.1.0 (tika-encoding-detector-universal and tika-encoding-detector-icu4j; the ICU4J module is a vendored copy of ICU4J’s CharsetDetector), but they are neither bundled in the tika-app or tika-server-standard packages nor auto-discovered. You have to do two things, and either one alone is not enough:

  1. Put the jars on the classpath (for tika-app and tika-server, for example with -Dtika.extras.dir, see the configuration page; in a Maven project add the two dependencies).

  2. Configure the chain explicitly. An explicit chain with no meta detector is first-match-wins:

<dependency>
  <groupId>org.apache.tika</groupId>
  <artifactId>tika-encoding-detector-universal</artifactId>
  <version>4.1.0</version>
</dependency>
<dependency>
  <groupId>org.apache.tika</groupId>
  <artifactId>tika-encoding-detector-icu4j</artifactId>
  <version>4.1.0</version>
</dependency>
{
  "encoding-detectors": [
    {"html-encoding-detector": {}},
    {"universal-encoding-detector": {}},
    {"icu4j-encoding-detector": {}}
  ]
}

Run on 4.1.0 with both jars added, for legacy-catalogue.txt the chain answered windows-1252, credited to UniversalEncodingDetector, at a confidence of 0.50 (against 1.00 for Mojibuster); for the Cyrillic file it answered windows-1251, also at 0.50. With the jars on the classpath but no configuration, the default chain was unchanged and Mojibuster still decided, confirming that these two are config-only. Whether to go back to the 3.x chain is a question for your corpus, not for this page: measure both on a labelled sample of your files. The only published comparison is the project’s design document, quoted above.

The 3.x equivalent of the exclude example used tika-config.xml with an <encodingDetectors> element and the TikaConfig class, which is removed in 4.x. The 4.x conversion tool, --convert-config-xml-to-json, migrates only the parsers section (see the migration page of the 4.1.x documentation); recreate the encoding-detectors array by hand.

Text quality scoring

The arbiter’s engine is available as a public API. org.apache.tika.quality.TextQualityDetector (in tika-core) has an implementation in the tika-ml-junkdetect module, and you can use it yourself to flag mojibake after the fact, to check a forced charset or to decide whether OCR output deserves a second pass.

Method Meaning

score(String)

A TextQualityScore for one string: getZScore() is the combiner’s logit (above 0 is clean), getPClean() is its sigmoid, getCiLow() and getCiHigh() give an interval that is wide for short texts, getDominantScript() names the script that carried the score and isUnknown() is true when no script of the input has a model.

compare(String, String, String, String)

Compares two decodings of the same bytes, each with a label. The result (TextQualityComparison) has winner(), delta(), both scores and both labels.

How the score is built (from the 4.1.x documentation): every code-point bigram is bucketed into its Unicode script and scored against that script’s own table (trained on Wikipedia and MADLAD-400), then document-level features (block transitions, control bytes, script transitions, replacement characters, alternation) are combined by one linear model. A script without a trained table is neutral, not junk; the bundled model knows 72 scripts.

The 4.1.x documentation says to load the detector with ServiceLoader.load(TextQualityDetector.class). On the classpath of 4.1.0 that call failed with ServiceConfigurationError ("Unable to get public no-arg constructor"), because JunkDetector exposes a static provider() method (the module-system form) and has no public no-arg constructor. Use JunkDetector.loadFromClasspath() as below, or JunkDetector.provider(). The class is thread-safe and immutable after construction according to the documentation: load it once.
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;

import org.apache.tika.ml.junkdetect.JunkDetector;
import org.apache.tika.quality.TextQualityComparison;
import org.apache.tika.quality.TextQualityDetector;
import org.apache.tika.quality.TextQualityScore;

public class DecodingQuality {

    public static void main(String[] args) throws Exception {
        byte[] raw = Files.readAllBytes(Path.of(args[0]));
        TextQualityDetector detector = JunkDetector.loadFromClasspath();   // load once, share

        for (String name : new String[] {"windows-1252", "windows-1251", "UTF-8", "ISO-8859-5", "IBM850"}) {
            TextQualityScore s = detector.score(new String(raw, Charset.forName(name)));
            System.out.printf(java.util.Locale.ROOT, "%-13s z=%6.2f pClean=%.3f script=%s unknown=%s%n",
                    name, s.getZScore(), s.getPClean(), s.getDominantScript(), s.isUnknown());
        }

        TextQualityComparison c = detector.compare(
                "windows-1252", new String(raw, Charset.forName("windows-1252")),
                "windows-1251", new String(raw, Charset.forName("windows-1251")));
        System.out.println("winner=" + c.winner() + " delta=" + c.delta());
    }
}

Output for the Cyrillic file (a windows-1251 sentence of Russian book titles) on 4.1.0:

windows-1252  z= -7.00 pClean=0.001 script=LATIN unknown=false
windows-1251  z=  2.12 pClean=0.892 script=CYRILLIC unknown=false
UTF-8         z= -1.00 pClean=0.269 script=NONE unknown=false
ISO-8859-5    z= -3.58 pClean=0.027 script=CYRILLIC unknown=false
IBM850        z=-11.90 pClean=0.000 script=LATIN unknown=false
winner=windows-1251 delta=9.1193285

A delta of more than 3 is a high-confidence verdict (the documentation’s guidance is: below 0.5 very uncertain, 0.5 to 1.0 weak, 1.0 to 3.0 useful, above 3.0 high). Now the same code on legacy-catalogue.txt:

windows-1252  z=  0.79 pClean=0.689 script=LATIN unknown=false
windows-1251  z=  1.06 pClean=0.742 script=LATIN unknown=false
UTF-8         z= -0.72 pClean=0.328 script=LATIN unknown=false
ISO-8859-5    z=  0.18 pClean=0.545 script=LATIN unknown=false
IBM850        z= -0.43 pClean=0.394 script=LATIN unknown=false
winner=windows-1251 delta=0.2628771

This is the honest limit of text-quality scoring, and a good lesson about the examples in this section: the scorer rates windows-1251 slightly above the correct windows-1252 for this Spanish and French text. Both decodings of the Latin letters in a mostly ASCII file look plausible to a model that pools many languages (the documentation lists this limit: close charsets in the same script "dilute the signal"), and the delta of 0.26 is below the 0.5 line that the guidance calls "very uncertain". The scorer correctly separated the large differences (UTF-8 mojibake at -0.72, IBM850 at -0.43) but not the small ones. The detector chain does not rely on it for this: Mojibuster’s classifier and the Latin-sibling rules had already named windows-1252 for the file. Use the score for the big question (is this decoding garbage?), apply the delta rules before trusting a winner and never use a score alone as the charset decision between Latin siblings.

Thresholds suggested by the documentation: below -2 is a reasonable trigger for re-decoding or re-OCR, below -5 is almost certainly garbled, a delta above 1.0 is a usable tiebreak and a delta below 0.5 should make you abstain. Under about 50 UTF-8 bytes use getCiLow() instead of the raw score, and below roughly 15 bytes the score is unreliable. The scorer does not detect a wrong language (an English file labelled French); use the language detector for that, described in Language Detection & Translation.

Writing your own EncodingDetector

The SPI is a one-method interface, so a company-specific rule is short. This detector recognises the Bookshelf publisher’s convention, a first line #CODEPAGE=1252, and otherwise abstains so the rest of the chain runs:

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;

import org.apache.tika.detect.EncodingDetector;
import org.apache.tika.detect.EncodingResult;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;

public class PublisherCodepageDetector implements EncodingDetector {

    private static final int PROBE = 64;

    @Override
    public List<EncodingResult> detect(TikaInputStream tis, Metadata metadata, ParseContext context)
            throws IOException {
        if (tis == null) {
            return Collections.emptyList();
        }
        tis.mark(PROBE);
        byte[] head = new byte[PROBE];
        int n;
        try {
            n = tis.read(head);
        } finally {
            tis.reset();                       // the contract: reset, never close
        }
        if (n < 15) {
            return Collections.emptyList();
        }
        String first = new String(head, 0, n, StandardCharsets.US_ASCII);
        if (first.startsWith("#CODEPAGE=1252")) {
            return List.of(new EncodingResult(Charset.forName("windows-1252"), 1.0f,
                    "windows-1252", EncodingResult.ResultType.DECLARATIVE));
        }
        return Collections.emptyList();
    }
}

Register it like any other component (a service file or the 4.x @TikaComponent annotation; see Extending Tika in this section for the details). Mark a self-declared charset DECLARATIVE, as above, so that the arbiter treats it as a declaration. This sketch was compiled against the 4.1.0 signature but not run as part of a chain.

Pitfalls, performance and security

  • Short files are guesses. Under a handful of non-ASCII bytes there is little evidence. The 4.x chain deliberately prefers windows-1252 for ASCII-only and low-evidence input. If you already know the encoding, pass it as a hint or override it.

  • ISO-8859-1 labels become windows-1252. The metadata detector normalises the two, as the HTML standard does. If you need strict Latin-1 behaviour (bytes 80 to 9F as controls) you must not rely on the default chain.

  • Detection is not validation. A file in a charset that no detector knows is decoded with the closest candidate and contains replacement characters. Check for U+FFFD in the output of important documents.

  • The label and the decoder can differ. Use getDecodeAs() for decoding and getCharset() for reporting. AutoDetectReader does this for you; code that calls a detector directly must.

  • Do not mix default-encoding-detector and explicit entries in one encoding-detectors list (see above).

  • Order matters only in first-match-wins. Putting the arbiter in a chain makes the order of the others irrelevant, and leaving it out makes the order the whole policy.

  • The HTML limit. A <meta> tag after more than markLimit bytes of preamble is not seen by the HTML detector, although the default chain’s classifier still reads the bytes.

  • Detectors read a probe. They mark the stream, read some bytes and reset it (the contract requires it); the size of Mojibuster’s probe is not documented in the pages consulted, so this page gives no number. Detection costs are small next to parsing. The project’s own comparison, as noted above, claims an order of magnitude less latency than ICU4J for Mojibuster; it was not benchmarked for this page.

  • Spooling. The EncodingDetector contract guarantees that the stream supports mark and reset; see the spooling page of the 4.1.x documentation for when a TikaInputStream copies its source to disk.

  • Security. The detected charset is an input to text decoding, not a trust signal. Never use a charset from an upload header to choose a file-system path or a database collation, and never pass a charset name from a client to Charset.forName without catching UnsupportedCharsetException and IllegalCharsetNameException. Process isolation and resource limits for hostile files are a separate topic, covered in the security guidance of the Tika documentation.

  • Decoding and indexing. Normalise text to Unicode (UTF-8 in the index) right after parsing and store the detected charset as a field; re-detecting later from decoded text is not possible.

1.x to 4.x at a glance

Topic 1.x (book, chapter 4) and 2.x/3.x 4.x

Interface

Charset detect(InputStream, Metadata)

List<EncodingResult> detect(TikaInputStream, Metadata, ParseContext) with EncodingResult, ResultType and a MetaEncodingDetector role

Default chain

HTML meta detector, juniversalchardet, ICU4J (first match wins)

BOM, metadata, HTML, Mojibuster, junk-filter arbiter (collect-all)

Declared charset

Authoritative when found

One input among several; the arbiter can override it when the bytes contradict it

Composite

CompositeEncodingDetector, first non-null

The same class, with a meta-detector mode

Reading text

AutoDetectReader

AutoDetectReader (same API; default fallback ISO-8859-1 kept for the final fallback)

Configuration

tika-config.xml <encodingDetectors>, TikaConfig

tika-config.json encoding-detectors, TikaLoader.loadEncodingDetectors()

Result keys

X-TIKA: namespace (names not re-verified here)

tk:detected-encoding, tk:encoding-detector, tk:encoding-detection-trace, tk:decoded-charset

Universal and ICU4J

Bundled

Separate modules, config-only

Quality scoring

Not available

TextQualityDetector, JunkDetector (tika-ml-junkdetect)

What the documentation does not provide

  • No accuracy figures for the shipped Mojibuster model (the project says none are published), so this page has none either.

  • No guidance on the size of the probe that Mojibuster reads.

  • The ServiceLoader snippet of the junk-detection page does not work on the classpath of 4.1.0 as written, see the caution above.

  • The literal 3.x metadata key names for the detection keys were not re-checked against a running 3.x; only the constants DETECTED_ENCODING and ENCODING_DETECTOR of the 3.3.2 Javadoc were seen.

Checklist

  • Pass what the caller knows (HTTP Content-Type, resource name) in the Metadata and let MetadataCharsetDetector use it.

  • Keep the default collect-all chain unless the source is controlled; choose first-match-wins only then.

  • Log tk:detected-encoding, tk:encoding-detector and tk:encoding-detection-trace for every text file.

  • Use getDecodeAs() when you decode a result yourself, or AutoDetectReader to do it for you.

  • Check important results for replacement characters, or score the decoding with JunkDetector, and trust a compare winner only when its delta is above 1.0.

  • To change the chain, write the whole encoding-detectors array, end it with junk-filter-encoding-detector, and do not mix it with default-encoding-detector.

  • Restore the 3.x chain only with both the jars and an explicit configuration, and only after measuring it on your files.