This guide shows how to search text in PDF on iOS across every page of a document with RadaeePDF, and how to reuse the same text objects to extract page text and highlight matches.
Applies to: RadaeePDF Classic and Master SDK for iOS. All snippets use selectors declared in PDFViewer/PDFLib/PDFObjc.h, which are the same in both editions.
Short answer: For each page, get an RDPDFPage with [doc page:i] and load its text with [page objsStart:false]. Then call [page find:key :matchCase :wholeWord]. The returned RDPDFFinder gives you the match count (count) and the first character index of each match (objsIndex:). Run the loop on a background queue, the way the demo’s RDExtendedSearch class does. The same character indexes work with objsString:: to extract text and with objsCharRect:: to get rectangles for highlighting.
How the text API is organized
Text search and text extraction in RadaeePDF both work on a page’s text objects. You call objsStart: once per page. After that, every character on the page has a 0-based index from 0 to objsCount - 1, and the other calls use those indexes:
-(void)objsStart:(bool)rtolloads the page’s text objects. Passtrueonly for right-to-left text. The demo passesGLOBAL.g_sel_rtol. The header notes that this needs a standard license.-(int)objsCountreturns the number of characters on the page.-(NSString *)objsString:(int)from :(int)toreturns the text for a range of character indexes.-(void)objsCharRect:(int)index :(PDF_RECT *)rectreturns a character’s box in PDF page coordinates.-(RDPDFFinder *)find:(NSString *)key :(bool)match_case :(bool)whole_wordstarts a find session on the page. It returnsnilwhen there are no matches.-(RDPDFFinder *)find2:(NSString *)key :(bool)match_case :(bool)whole_word :(bool)skip_blanksdoes the same but treats line breaks as blanks. Use it when a phrase can wrap across lines.
RDPDFFinder has three methods: count, objsIndex: (the first character of match n) and objsEnd: (the end character of match n).
The KB sample Search in whole pdf file (iOS) uses the older names PDFDoc, PDFPage, PDFFinder and an argument-less objsStart. In the current Classic and Master repositories, these classes are RDPDFDoc, RDPDFPage and RDPDFFinder, and objsStart: takes a bool.
Search text in PDF on iOS on a background queue
On a long document, searching every page can take noticeable time, so keep it off the main thread. This is how the demo’s RDExtendedSearch works: it loops over the pages inside dispatch_async(dispatch_get_global_queue(...)) and checks a stop flag on every page so the search can be cancelled. The class below does the same thing in a self-contained way.
Objective-C (Classic and Master)
#import "PDFObjc.h"
@interface DocSearcher : NSObject
@property (atomic) BOOL cancelled;
- (void)search:(RDPDFDoc *)doc
key:(NSString *)key
matchCase:(bool)matchCase
wholeWord:(bool)wholeWord
completion:(void (^)(NSArray<NSDictionary *> *hits))completion;
@end
@implementation DocSearcher
- (void)search:(RDPDFDoc *)doc
key:(NSString *)key
matchCase:(bool)matchCase
wholeWord:(bool)wholeWord
completion:(void (^)(NSArray<NSDictionary *> *hits))completion
{
self.cancelled = NO;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
NSMutableArray<NSDictionary *> *hits = [NSMutableArray array];
int pageCount = [doc pageCount];
for (int pageno = 0; pageno < pageCount && !self.cancelled; pageno++) {
RDPDFPage *page = [doc page:pageno];
if (!page) break;
[page objsStart:false];
RDPDFFinder *finder = [page find:key :matchCase :wholeWord];
int count = [finder count]; // 0 when finder is nil
int chars = [page objsCount];
for (int i = 0; i < count; i++) {
int first = [finder objsIndex:i];
// Context snippet around the hit, like RDExtendedSearch builds it
int from = MAX(first - 25, 0);
int to = MIN(first + (int)key.length + 25, chars - 1);
from = [page objsAlignWord:from :-1];
to = [page objsAlignWord:to :1] + 1;
NSString *context = [page objsString:from :to];
[hits addObject:@{ @"page": @(pageno),
@"char": @(first),
@"context": context ?: @"" }];
}
page = nil;
}
dispatch_async(dispatch_get_main_queue(), ^{
completion(hits);
});
});
}
@end
Set cancelled = YES when the user changes the query or closes the document, and keep the RDPDFDoc open until the completion block has run.
Reusing the demo’s RDExtendedSearch
If you embed the demo’s PDFReader sources, you can call the singleton directly. It reads the case, whole-word and right-to-left options from GLOBAL.g_case_sensitive, GLOBAL.g_match_whole_word and GLOBAL.g_sel_rtol. Each result is an RDSearchResult with stringResult and a 1-based page. Both blocks are called on the background queue, which is why SearchResultViewController hops to the main queue:
#import "RDExtendedSearch.h"
[[RDExtendedSearch sharedInstance] searchText:@"invoice" inDoc:doc progress:^(NSMutableArray *occurrences, NSMutableArray *total) {
dispatch_async(dispatch_get_main_queue(), ^{
for (RDSearchResult *r in occurrences) {
NSLog(@"page %d: %@", r.page, r.stringResult);
}
});
} finish:^{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"search finished");
});
}];
// To stop a running search:
[[RDExtendedSearch sharedInstance] clearSearch];
Extract the full text of a page
To get a page’s full text, call objsStart: and then read the range from 0 to objsCount. This is what the Cordova plugin’s extractTextFromPage: does on iOS. To extract a whole document, run the same calls in a loop on a background queue:
- (NSString *)textOfDocument:(RDPDFDoc *)doc
{
NSMutableString *all = [NSMutableString string];
int pageCount = [doc pageCount];
for (int pageno = 0; pageno < pageCount; pageno++) {
RDPDFPage *page = [doc page:pageno];
[page objsStart:false];
int count = [page objsCount];
if (count > 0) {
NSString *text = [page objsString:0 :count];
if (text) [all appendFormat:@"%@\n", text];
}
page = nil;
}
return all;
}
Get match rectangles for highlighting
A match can span several characters and even wrap onto a second line. The viewer’s RDVFinder gets the box of each character with objsCharRect:: and merges characters that share a line into a single rectangle. You can do the same to draw your own overlays:
// One rectangle per line covered by the match, in PDF page coordinates.
static NSArray<NSValue *> *MatchRects(RDPDFPage *page, int first, int length)
{
NSMutableArray<NSValue *> *rects = [NSMutableArray array];
PDF_RECT word, r;
[page objsCharRect:first :&word];
for (int i = first + 1; i < first + length; i++) {
[page objsCharRect:i :&r];
float gap = (r.bottom - r.top) / 2;
if (word.top == r.top && word.bottom == r.bottom &&
word.right + gap > r.left && word.left - gap < r.right) {
if (word.left > r.left) word.left = r.left;
if (word.right < r.right) word.right = r.right;
} else {
[rects addObject:[NSValue valueWithBytes:&word objCType:@encode(PDF_RECT)]];
word = r;
}
}
[rects addObject:[NSValue valueWithBytes:&word objCType:@encode(PDF_RECT)]];
return rects;
}
Call this right after find: while the page’s text objects are still loaded, with first = [finder objsIndex:i] and length = key.length. That is the same end the viewer uses. PDF coordinates have the y axis pointing up, so the viewer maps a rectangle’s bottom to the top of the on-screen box. If you place overlays yourself at a zoom factor scale, use x = rect.left * scale and y = ([doc pageHeight:pageno] - rect.bottom) * scale.
To make the highlight a permanent annotation in the file instead of a temporary overlay, call [page addAnnotMarkup:first :first + length - 1 :0 :0xFFFFFF00] after objsStart:. Type 0 is Highlight, and the color is the ARGB value the demo uses by default for highlights. Then save the document. The header says this needs a professional or premium license.
If you build your own viewer on the demo’s PDFLayoutView, you get on-screen highlighting without any extra code. vFindStart::: starts a search, vFind: moves to the next (1) or previous (-1) match and paints it, and vFindEnd clears the highlight. PDFReaderCtrl uses these calls internally.
Swift equivalent
The repositories ship Objective-C sources only. Once you add PDFObjc.h to your bridging header, Swift imports the unlabeled selectors with _ parameters, int as Int32 and unannotated object returns as optionals:
func search(_ doc: RDPDFDoc, key: String,
completion: @escaping ([(page: Int32, char: Int32)]) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
var hits: [(page: Int32, char: Int32)] = []
for pageno in 0..<doc.pageCount() {
guard let page = doc.page(pageno) else { break }
page.objsStart(false)
guard let finder = page.find(key, false, false) else { continue }
for i in 0..<finder.count() {
hits.append((page: pageno, char: finder.objsIndex(i)))
}
}
DispatchQueue.main.async { completion(hits) }
}
}
In Swift, check the result of find for nil with guard let. In Objective-C, sending count to nil simply returns 0.
Related questions
How do I make the search case-sensitive or whole-word only?
Pass true as the second or third argument of find:::, which are match_case and whole_word. In the demo viewer, these options come from GLOBAL.g_case_sensitive and GLOBAL.g_match_whole_word, and both RDExtendedSearch and PDFLayoutView read them.
Why does a phrase that wraps onto the next line not match?
find::: matches the characters exactly as they appear on the page, including the line break. Use find2:::: with skip_blanks set to true, which treats line breaks as blank characters.
Do I need to call objsStart before every find?
Call it once per RDPDFPage object, before find:, objsCount, objsString:: or objsCharRect::. The RDPDFFinder header notes that objsIndex: expects objsStart to have been called on the page.
Can I search a document that is open in PDFReaderCtrl?
Yes. Get the document with [reader getDoc] and run the background loop on it. To jump to a result, call [reader PDFGoto:pageno] with the 0-based page number.
Related on radaeepdf.com
- RadaeePDF SDK for iOS
- Open a PDF from a URL on iOS without a full download
- Export and import PDF annotations on iOS
- Download the free trial
Where to go next
- PDFObjc.h (Master iOS) and PDFObjc.h (Classic iOS):
RDPDFPageandRDPDFFinderdeclarations - RDExtendedSearch.m: background search used by the demo
- RDVFinder.m: how the viewer builds match rectangles
- PDFLayoutView.h:
vFindStart,vFindandvFindEnd - RadaeePDFPlugin.m (Cordova):
extractTextFromPage:on iOS - KB: Search in whole pdf file (iOS)