Search for text in PDFs on Android

To search text inside a document, create an instance of TextSearch, passing in the loaded PdfDocument via its constructor. The simplest way to retrieve search results is by calling TextSearch#performSearch(String), which will return a list of found SearchResult objects. If a search has no results, this method will return an empty list:

val textSearch = TextSearch(document, configuration)
val query = "Weather reports"
val searchResults : List<SearchResult> = textSearch.performSearch(query)
final TextSearch textSearch = new TextSearch(document, configuration);
final String query = "Weather reports";
final List<SearchResult> searchResults = textSearch.performSearch(query);

ℹ️ Note: #performSearch is a blocking call and should only be used on a background thread.

To keep the main thread operational and to avoid ANR dialogs while performing a search, you can use the non-blocking #performSearchAsync method, which will return a Flowable<SearchResult>. The flowable emits search results as soon as they are available — you can already use them during the ongoing search, e.g. to populate your search UI:

// Perform an async search on a computation thread and update the UI on the main thread.
val searchSubscription = search.performSearchAsync(query)
    .subscribeOn(Schedulers.computation())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { nextResult ->
        // This will be called once for every `SearchResult` object.
        // Put your search result handling here.
    }
// Perform an async search on a computation thread and update the UI on the main thread.
final Disposable searchSubscription = textSearch.performSearchAsync(query)
    .subscribeOn(Schedulers.computation())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(nextResult -> {
        // This will be called once for every `SearchResult` object.
        // Put your search result handling here.
    })

💡 Tip: You can use Flowable#toList to collect all search results if you want to handle them at once.

Specify search options

Both methods — #performSearch and #performSearchAsync — offer overloaded versions that take SearchOptions for specifying search options. Use this to limit search results or search result snippet length:

val options = SearchOptions.Builder()
    .maxSearchResults(100)
    .snippetLength(40)
    .build()

val results = textSearch.performSearch(query, options)
final SearchOptions options = new SearchOptions.Builder()
    .maxSearchResults(100)
    .snippetLength(40)
    .build();

final List<SearchResult> results = textSearch.performSearch(query, options);

ℹ️ Note: Calling #performSearch(String) without options will use default options.

Compare options

SearchOptions.Builder#compareOptions() can be used to tweak how the text search matches its results. A combination of the following options is possible.

Compare Option Description
CASE_INSENSITIVE Text search ignores case when this option is used.
DIACRITIC_INSENSITIVE Text search ignores diacritic when this option is used.
SMART_SEARCH Enables smart search. Smart search ignores white spaces and hyphens. It also resolves typographic quotes and apostrophes. For example, when searching for standard quotes or apostrophes, typographic variants such as “”«»„“ are also matched.
REGULAR_EXPRESSION Enables regular expression search. When this option is used, the SMART_SEARCH option is ignored.

Nutrient uses a combination of CASE_INSENSITIVE, DIACRITIC_INSENSITIVE, and SMART_SEARCH compare options by default.

ℹ️ Note: TextSearch uses regular expression capabilities available in Java. Refer to the Pattern documentation for the supported regular expression syntax.

Using search results

Every SearchResult consists of the pageNumber, a textBlock containing the PDF coordinates on the page, and an optional snippet containing a preview text usable for showing text around search matches in the user interface. For example, if you would like to zoom to a search result, you can do the following:

// The search result consists of at least one text rect. This will create a union of all of its rects.
val searchResultRect: RectF = PdfUtils.createPdfRectUnion(searchResult.textBlock.pageRects)
// This will also switch to the correct page if it is not active.
fragment.zoomTo(searchResultRect, searchResult.pageNumber, 200)
// The search result consists of at least one text rect. This will create a union of all of its rects.
final RectF searchResultRect = PdfUtils.createPdfRectUnion(searchResult.textBlock.pageRects);
// This will also switch to the correct page if it is not active.
fragment.zoomTo(searchResultRect, searchResult.pageNumber, 200);

SearchResult#snippet can be used to present an upfront preview of the search result — for example, inside a search result list. The snippet consists of both the text and the rangeInSnippet, the latter of which gives the position of the actual result inside the snippet:

// Use the synthetic property for accessing a view from the layout.
import kotlinx.android.synthetic.main.activity_main.previewText

/**
 * Sets the search result snippet to a TextView highlighting the actual search term inside the result.
 */
private fun showSearchResultText(result: SearchResult) {
    // If you deactivated snippet extraction, it may be `null`.
    val snippet = result.snippet ?: return

    // This is the location of the search term inside the snippet.
    val highlightStart = snippet.rangeInSnippet.startPosition
    val highlightEnd = snippet.rangeInSnippet.endPosition

    val previewText = SpannableString(snippet.text)
    val resultHighlight = BackgroundColorSpan(Color.YELLOW)
    previewText.setSpan(resultHighlight, highlightStart, highlightEnd, 0)
}
// This has to be inflated from your layout before using it.
private TextView previewText;

/**
 * Sets the search result snippet to a TextView highlighting the actual search term inside the result.
 */
private void showSearchResultText(@NonNull final SearchResult result) {
    // If you deactivated snippet extraction, it may be `null`.
    if (result.snippet == null) return;

    // This is the location of the search term inside the snippet.
    final int highlightStart = result.snippet.rangeInSnippet.getStartPosition();
    final int highlightEnd = result.snippet.rangeInSnippet.getEndPosition();

    final SpannableString previewText = new SpannableString(result.snippet.text);
    final BackgroundColorSpan resultHighlight = new BackgroundColorSpan(Color.YELLOW);
    previewText.setSpan(resultHighlight, highlightStart, highlightEnd, 0);
}

ℹ️ Note: The SearchResult#snippet may be null if you deactivated snippet extraction by setting the SearchOptions.Builder#snippetLength to 0.

Highlighting search results

To display search results on the PdfFragment, create a SearchResultHighlighter and register it with your fragment. Once you have a list of search results, you can start highlighting them by calling #setSearchResults on the highlighter:

lateinit var highlighter : SearchResultHighlighter

/** Holds your PdfFragment. You don't need to define this when using PdfActivity. */
val fragment: PdfFragment

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // The context is used to retrieve the highlighter color from the theme.
    highlighter = SearchResultHighlighter(context)
    fragment.addDrawableProvider(highlighter)
}

/**
 * Called by your application once a search is finished.
 * The highlighter will immediately start highlighting the given results.
 */
fun onSearchFinished(searchResults : List<SearchResult>) = highlighter.setSearchResults(searchResults)
private SearchResultHighlighter highlighter;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // This is possible in all `PdfActivity` subclasses.
    // If you aren't using `PdfActivity`, replace this with your own code.
    final PdfFragment fragment = getPdfFragment();

    // The context is used to retrieve the highlighter color from the theme.
    highlighter = new SearchResultHighlighter(context);
    fragment.addDrawableProvider(highlighter);

    // ...
}

/**
 * Called by your application once a search is finished.
 */
private void onSearchFinished(@NonNull final List<SearchResult> searchResults) {
    // The highlighter will immediately start highlighting the given results.
    highlighter.setSearchResults(searchResults);
}

Emphasizing the selected result

You can emphasize a single, selected search result, which will reveal itself with a pop-out animation and receive an additional border. Note that you can only select a search result that was previously set on the highlighter using #setSearchResults(List<SearchResult>). Setting a search result that was not previously added will raise an exception:

/**
 * Called by your application, e.g. if the user taps a search result inside a list.
 */
fun onSearchResultSelected(selectedResult : SearchResult) =
    highlighter.setSelectedSearchResult(selectedResult)
/**
 * Called by your application, e.g. if the user taps a search result inside a list.
 */
private void onSearchResultSelected(@NonNull final SearchResult selectedResult) {
    highlighter.setSelectedSearchResult(selectedResult);
}

ℹ️ Note: SearchResultHighlighter#setSelectedSearchResult also allows null as an argument, which will then clear any previously selected result.

Highlighter customization

The SearchResultHighlighter uses predefined colors and styles. If you want to change the appearance, you can apply a custom style to the pspdf__searchResultHighlighterStyle attribute of your activity theme:

<!-- This is the theme you set to your activity inside AndroidManifest.xml -->
<style name="MyApp_Theme" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="pspdf__searchResultHighlighterStyle">@style/MyApp_Theme_SearchResultHighlighter</item>
</style>

<!-- This is a custom style for search result highlights. -->
<style name="MyApp_Theme_SearchResultHighlighter" parent="PSPDFKit.SearchResultHighlighter">
    <item name="pspdf__searchResultBackgroundColor">#FF009999</item>
    <item name="pspdf__searchResultBorderColor">#FF888888</item>
    <item name="pspdf__searchResultBorderWidth">2px</item>
    <item name="pspdf__searchResultPadding">2dp</item>
    <item name="pspdf__searchResultAnnotationPadding">3dp</item>
    <item name="pspdf__searchResultAnimationPadding">8dp</item>
    <item name="pspdf__searchResultCornerRadiusToHeightRatio">0.1</item>
    <item name="pspdf__searchResultCornerRadiusMin">2dp</item>
    <item name="pspdf__searchResultCornerRadiusMax">10dp</item>
</style>

💡 Tip: Take a look at DarkThemeExample, which explains how to apply custom themes, including a custom search result highlighter style.

Below is a complete list of the style attributes you can define for the pspdf__searchResultHighlighterStyle.

Attribute Description
pspdf__searchResultBackgroundColor Background color of the highlighted search result.
pspdf__searchResultBorderColor Border color of the highlighted search result.
pspdf__searchResultBorderWidth Border width of the highlighted search result.
pspdf__searchResultPadding Padding of the highlighted search result used when highlighting document text.
pspdf__searchResultAnnotationPadding Padding of the highlighted search result used when highlighting annotations.
pspdf__searchResultAnimationPadding Padding of the highlighted search result used when pop-out animating the currently selected search result.
pspdf__searchResultCornerRadiusToHeightRatio Ratio between corner radius of the search result rectangle and its height.
pspdf__searchResultCornerRadiusMin Minimal corner radius of the highlighted search result.
pspdf__searchResultCornerRadiusMax Maximal corner radius of the highlighted search result.