pdf_extractor.cc (28935B)
1 /* 2 This file is part of libextractor. 3 Copyright (C) 2002, 2003, 2009, 2026 Vidyut Samanta and Christian Grothoff 4 5 libextractor is free software; you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as published 7 by the Free Software Foundation; either version 3, or (at your 8 option) any later version. 9 10 libextractor is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with libextractor; see the file COPYING. If not, write to the 17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 18 Boston, MA 02110-1301, USA. 19 */ 20 /** 21 * @file plugins/pdf_extractor.cc 22 * @brief plugin to support PDF files 23 * @author Vidyut Samanta 24 * @author Christian Grothoff 25 */ 26 #include "platform.h" 27 #include "extractor.h" 28 #include "pagelayout.h" 29 #include <poppler/cpp/poppler-document.h> 30 #include <poppler/cpp/poppler-global.h> 31 #include <poppler/cpp/poppler-page.h> 32 #include <algorithm> 33 #include <cmath> 34 #include <ctime> 35 #include <string> 36 #include <utility> 37 #include <vector> 38 39 40 /** 41 * Sanity bound on the size of a PDF we are willing to buffer 42 * in memory (1 GB). libpoppler needs the whole document, and 43 * its raw-data loader takes an `int` length. 44 */ 45 #define MAX_PDF_SIZE (1024LL * 1024LL * 1024LL) 46 47 48 /** 49 * Entry in the mapping from poppler accessors to LE types. 50 */ 51 struct Matches 52 { 53 /** 54 * Accessor on the poppler document returning the value. 55 */ 56 poppler::ustring (poppler::document::*get) () const; 57 58 /** 59 * Corresponding meta data type in LE. 60 */ 61 enum EXTRACTOR_MetaType type; 62 }; 63 64 65 /** 66 * Map from poppler document info accessors to LE types. 67 * 68 * Note that we deliberately map "Creator" to 69 * #EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE: we believe that 70 * Adobe's "Creator" is not a person nor an organisation, but 71 * just a piece of software. 72 */ 73 static const struct Matches tmap[] = { 74 { &poppler::document::get_title, EXTRACTOR_METATYPE_TITLE }, 75 { &poppler::document::get_subject, EXTRACTOR_METATYPE_SUBJECT }, 76 { &poppler::document::get_keywords, EXTRACTOR_METATYPE_KEYWORDS }, 77 { &poppler::document::get_author, EXTRACTOR_METATYPE_AUTHOR_NAME }, 78 { &poppler::document::get_creator, EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE }, 79 { &poppler::document::get_producer, EXTRACTOR_METATYPE_PRODUCED_BY_SOFTWARE }, 80 { NULL, EXTRACTOR_METATYPE_RESERVED } 81 }; 82 83 84 /** 85 * Silence libpoppler: we do not want parsing diagnostics on 86 * stderr of the plugin child process. 87 * 88 * @param msg the message (ignored) 89 * @param cls closure (ignored) 90 */ 91 static void 92 quiet_error (const std::string &msg, 93 void *cls) 94 { 95 (void) msg; 96 (void) cls; 97 } 98 99 100 /** 101 * Hand a UTF-8 string to the meta data processor, after 102 * stripping trailing whitespace. Empty values are skipped. 103 * 104 * @param ec extraction context 105 * @param type meta data type to use 106 * @param val UTF-8 bytes (need not be 0-terminated) 107 * @return 0 to continue extracting, 1 if @a ec asked us to stop 108 */ 109 static int 110 add_utf8 (struct EXTRACTOR_ExtractContext *ec, 111 enum EXTRACTOR_MetaType type, 112 std::vector<char> val) 113 { 114 size_t len = val.size (); 115 116 /* 117 * Avoid outputting trailing whitespace. Note that ISO-8859 118 * NBSP (0xA0) becomes 0xC2 0xA0 in UTF-8 and is intentionally 119 * not stripped here. 120 */ 121 while ((0 < len) && 122 ((' ' == val[len - 1]) || 123 ('\r' == val[len - 1]) || 124 ('\n' == val[len - 1]) || 125 ('\t' == val[len - 1]) || 126 ('\v' == val[len - 1]) || 127 ('\f' == val[len - 1]))) 128 len--; 129 if (0 == len) 130 return 0; 131 std::string s (val.data (), len); 132 if (0 != ec->proc (ec->cls, 133 "pdf", 134 type, 135 EXTRACTOR_METAFORMAT_UTF8, 136 "text/plain", 137 s.c_str (), 138 s.size () + 1)) 139 return 1; 140 return 0; 141 } 142 143 144 /** 145 * Hand a 0-terminated C string to the meta data processor. 146 * 147 * @param ec extraction context 148 * @param type meta data type to use 149 * @param s the string 150 * @return 0 to continue extracting, 1 if @a ec asked us to stop 151 */ 152 static int 153 add_str (struct EXTRACTOR_ExtractContext *ec, 154 enum EXTRACTOR_MetaType type, 155 const char *s) 156 { 157 if ((NULL == s) || ('\0' == s[0])) 158 return 0; 159 if (0 != ec->proc (ec->cls, 160 "pdf", 161 type, 162 EXTRACTOR_METAFORMAT_UTF8, 163 "text/plain", 164 s, 165 strlen (s) + 1)) 166 return 1; 167 return 0; 168 } 169 170 171 /** 172 * Report a date (given as a `time_t`) in ISO-8601 / UTC. 173 * 174 * @param ec extraction context 175 * @param type meta data type to use 176 * @param t the time, `(time_t) -1` or 0 if absent 177 * @return 0 to continue extracting, 1 if @a ec asked us to stop 178 */ 179 static int 180 add_date (struct EXTRACTOR_ExtractContext *ec, 181 enum EXTRACTOR_MetaType type, 182 time_t t) 183 { 184 char buf[32]; 185 struct tm tv; 186 187 if (((time_t) -1 == t) || (0 == t)) 188 return 0; 189 if (NULL == gmtime_r (&t, &tv)) 190 return 0; 191 if (0 == strftime (buf, sizeof (buf), "%Y-%m-%d %H:%M:%S", &tv)) 192 return 0; 193 return add_str (ec, type, buf); 194 } 195 196 197 /** 198 * Read the entire input into @a buf. 199 * 200 * @param ec extraction context 201 * @param[out] buf buffer to fill with the file contents 202 * @return 0 on success, -1 on error 203 */ 204 static int 205 read_all (struct EXTRACTOR_ExtractContext *ec, 206 std::vector<char> &buf) 207 { 208 uint64_t size; 209 210 size = ec->get_size (ec->cls); 211 if ((UINT64_MAX == size) || 212 (0 == size) || 213 (size > MAX_PDF_SIZE)) 214 return -1; 215 if (0 != ec->seek (ec->cls, 0, SEEK_SET)) 216 return -1; 217 buf.reserve ((size_t) size); 218 while (buf.size () < size) 219 { 220 void *data; 221 ssize_t got; 222 223 got = ec->read (ec->cls, 224 &data, 225 (size_t) (size - buf.size ())); 226 if ((got <= 0) || (NULL == data)) 227 break; 228 buf.insert (buf.end (), 229 static_cast<char *> (data), 230 static_cast<char *> (data) + got); 231 } 232 if (buf.empty ()) 233 return -1; 234 return 0; 235 } 236 237 238 /* ****************** full text extraction ********************** */ 239 240 /** 241 * Maximum number of pages we extract text from. 242 */ 243 #define PDF_FT_MAX_PAGES 5000 244 245 /** 246 * Maximum number of text boxes (words) we look at per page. 247 */ 248 #define PDF_FT_MAX_WORDS 100000 249 250 /** 251 * Maximum number of lines we collect over the whole document. 252 */ 253 #define PDF_FT_MAX_LINES 500000 254 255 /** 256 * Wall-clock budget for the full text extraction, in seconds. 257 */ 258 #define PDF_FT_DEADLINE 60 259 260 /** 261 * Relative height of the top and bottom margin bands in which lines 262 * separated from the body are treated as page furniture that never 263 * splits into columns. 264 */ 265 #define PDF_FT_BAND 0.12 266 267 268 /** 269 * A word (poppler text box) on a page. 270 */ 271 struct Word 272 { 273 /** 274 * Bounding box, in points, origin top left. 275 */ 276 double x0; 277 double y0; 278 double x1; 279 double y1; 280 281 /** 282 * UTF-8 text. 283 */ 284 std::string text; 285 286 /** 287 * 1 if the word is followed by a space. 288 */ 289 bool space_after; 290 291 /** 292 * 1 if the word consists of digits only. 293 */ 294 bool numeric; 295 }; 296 297 298 /** 299 * A line of words (in reading order once built). 300 */ 301 struct Line 302 { 303 /** 304 * Indices into the word array, sorted left to right. 305 */ 306 std::vector<size_t> words; 307 308 /** 309 * Vertical extent of the line's first word (for grouping). 310 */ 311 double core_y0; 312 double core_y1; 313 314 /** 315 * Baseline (median bottom of the words). 316 */ 317 double base; 318 319 /** 320 * Horizontal extent. 321 */ 322 double x0; 323 double x1; 324 325 /** 326 * Median word height. 327 */ 328 double size; 329 }; 330 331 332 /** 333 * Median of a vector of doubles. 334 * 335 * @param v values (reordered), not empty 336 * @return the median 337 */ 338 static double 339 median (std::vector<double> &v) 340 { 341 size_t mid = v.size () / 2; 342 343 std::nth_element (v.begin (), 344 v.begin () + mid, 345 v.end ()); 346 return v[mid]; 347 } 348 349 350 /** 351 * Current monotonic time in seconds. 352 * 353 * @return seconds 354 */ 355 static double 356 now_s () 357 { 358 struct timespec ts; 359 360 if (0 != clock_gettime (CLOCK_MONOTONIC, &ts)) 361 return 0; 362 return (double) ts.tv_sec + ts.tv_nsec / 1e9; 363 } 364 365 366 /** 367 * Append the text of @a w to @a out, expanding the Latin ligature 368 * characters U+FB00..U+FB06. 369 * 370 * @param out string to append to 371 * @param w text to append 372 */ 373 static void 374 append_expanded (std::string &out, 375 const std::string &w) 376 { 377 static const char *const ligs[] = { 378 "ff", "fi", "fl", "ffi", "ffl", "st", "st" 379 }; 380 size_t i = 0; 381 382 while (i < w.size ()) 383 { 384 if ( (i + 2 < w.size ()) && 385 ((unsigned char) w[i] == 0xEF) && 386 ((unsigned char) w[i + 1] == 0xAC) && 387 ((unsigned char) w[i + 2] <= 0x86) && 388 ((unsigned char) w[i + 2] >= 0x80) ) 389 { 390 out += ligs[(unsigned char) w[i + 2] - 0x80]; 391 i += 3; 392 continue; 393 } 394 out += w[i]; 395 i++; 396 } 397 } 398 399 400 /** 401 * Replace a Latin letter followed by a combining accent by the 402 * precomposed character (poppler reports TeX's accents that way). 403 * 404 * @param s UTF-8 text, modified in place 405 */ 406 static void 407 compose_accents (std::string &s) 408 { 409 std::string out; 410 size_t i = 0; 411 412 out.reserve (s.size ()); 413 while (i < s.size ()) 414 { 415 unsigned char c = (unsigned char) s[i]; 416 uint32_t base = 0; 417 size_t blen = 0; 418 419 if ( (c < 0x80) && 420 isalpha (c) ) 421 { 422 base = c; 423 blen = 1; 424 } 425 else if ( (0xC4 == c) && 426 (i + 1 < s.size ()) && 427 (0xB1 == (unsigned char) s[i + 1]) ) 428 { 429 base = 0x131; 430 blen = 2; 431 } 432 if ( (0 != base) && 433 (i + blen + 1 < s.size ()) ) 434 { 435 unsigned char m0 = (unsigned char) s[i + blen]; 436 unsigned char m1 = (unsigned char) s[i + blen + 1]; 437 438 if ( ( (0xCC == m0) || (0xCD == m0) ) && 439 ( (m1 & 0xC0) == 0x80) ) 440 { 441 uint32_t mark = ((uint32_t) (m0 & 0x1F) << 6) | (m1 & 0x3F); 442 uint32_t comp = EXTRACTOR_pagelayout_compose_ (base, 443 mark); 444 445 if (0 != comp) 446 { 447 /* all precomposed results are below U+0800 */ 448 out += (char) (0xC0 | (comp >> 6)); 449 out += (char) (0x80 | (comp & 0x3F)); 450 i += blen + 2; 451 continue; 452 } 453 } 454 } 455 out += s[i]; 456 i++; 457 } 458 s.swap (out); 459 } 460 461 462 /** 463 * Drop columns of line numbers (or similar pure numbers) in the left 464 * and right page margins, i.e. numbers that are outside of all other 465 * text on the page and aligned with each other. 466 * 467 * @param words words of the page, filtered in place 468 */ 469 static void 470 drop_marginal_numbers (std::vector<Word> &words) 471 { 472 double tmin = HUGE_VAL; 473 double tmax = -HUGE_VAL; 474 std::vector<double> hs; 475 double h; 476 477 for (const Word &w : words) 478 if (! w.numeric) 479 { 480 tmin = std::min (tmin, w.x0); 481 tmax = std::max (tmax, w.x1); 482 hs.push_back (w.y1 - w.y0); 483 } 484 if (hs.empty ()) 485 return; 486 h = median (hs); 487 for (int side = 0; side < 2; side++) 488 { 489 std::vector<size_t> cand; 490 double lo = HUGE_VAL; 491 double hi = -HUGE_VAL; 492 493 for (size_t i = 0; i < words.size (); i++) 494 { 495 const Word &w = words[i]; 496 497 if (! w.numeric) 498 continue; 499 if ( (0 == side) && 500 (w.x1 < tmin - 0.3 * h) ) 501 { 502 cand.push_back (i); 503 lo = std::min (lo, w.x1); 504 hi = std::max (hi, w.x1); 505 } 506 if ( (1 == side) && 507 (w.x0 > tmax + 0.3 * h) ) 508 { 509 cand.push_back (i); 510 lo = std::min (lo, w.x0); 511 hi = std::max (hi, w.x0); 512 } 513 } 514 if ( (cand.size () < 5) || 515 (hi - lo > 4 * h) ) 516 continue; 517 for (size_t i : cand) 518 words[i].text.clear (); 519 } 520 words.erase (std::remove_if (words.begin (), 521 words.end (), 522 [] (const Word &w) { 523 return w.text.empty (); 524 }), 525 words.end ()); 526 } 527 528 529 /** 530 * Group the words of a page into lines by their vertical position. 531 * 532 * @param words words of the page 533 * @param[out] lines resulting lines, sorted top to bottom, each with 534 * its words sorted left to right 535 */ 536 static void 537 group_lines (const std::vector<Word> &words, 538 std::vector<Line> &lines) 539 { 540 std::vector<size_t> order (words.size ()); 541 542 for (size_t i = 0; i < words.size (); i++) 543 order[i] = i; 544 std::sort (order.begin (), 545 order.end (), 546 [&words] (size_t a, size_t b) { 547 double ca = words[a].y0 + words[a].y1; 548 double cb = words[b].y0 + words[b].y1; 549 550 if (ca != cb) 551 return ca < cb; 552 return words[a].x0 < words[b].x0; 553 }); 554 for (size_t i : order) 555 { 556 const Word &w = words[i]; 557 double wh = w.y1 - w.y0; 558 bool placed = false; 559 560 /* check the last two lines (a superscript may have opened one) */ 561 for (size_t k = 0; (k < 2) && (k < lines.size ()); k++) 562 { 563 Line &l = lines[lines.size () - 1 - k]; 564 double lh = l.core_y1 - l.core_y0; 565 double ov = std::min (w.y1, l.core_y1) - std::max (w.y0, l.core_y0); 566 567 if (ov >= 0.5 * std::min (wh, lh)) 568 { 569 l.words.push_back (i); 570 placed = true; 571 break; 572 } 573 } 574 if (placed) 575 continue; 576 lines.emplace_back (); 577 lines.back ().words.push_back (i); 578 lines.back ().core_y0 = w.y0; 579 lines.back ().core_y1 = w.y1; 580 } 581 for (Line &l : lines) 582 { 583 std::vector<double> bases; 584 std::vector<double> hs; 585 586 std::sort (l.words.begin (), 587 l.words.end (), 588 [&words] (size_t a, size_t b) { 589 return words[a].x0 < words[b].x0; 590 }); 591 l.x0 = HUGE_VAL; 592 l.x1 = -HUGE_VAL; 593 for (size_t i : l.words) 594 { 595 bases.push_back (words[i].y1); 596 hs.push_back (words[i].y1 - words[i].y0); 597 l.x0 = std::min (l.x0, words[i].x0); 598 l.x1 = std::max (l.x1, words[i].x1); 599 } 600 l.base = median (bases); 601 l.size = median (hs); 602 } 603 std::stable_sort (lines.begin (), 604 lines.end (), 605 [] (const Line &a, const Line &b) { 606 return a.base < b.base; 607 }); 608 } 609 610 611 /** 612 * Compute the horizontal coverage of a line: its words, with gaps of 613 * less than half the line height closed. 614 * 615 * @param words words of the page 616 * @param l the line 617 * @param[out] iv resulting intervals, left to right 618 */ 619 static void 620 line_coverage (const std::vector<Word> &words, 621 const Line &l, 622 std::vector<std::pair<double, double> > &iv) 623 { 624 iv.clear (); 625 for (size_t i : l.words) 626 { 627 const Word &w = words[i]; 628 629 if ( (! iv.empty ()) && 630 (w.x0 - iv.back ().second < 0.5 * l.size) ) 631 iv.back ().second = std::max (iv.back ().second, w.x1); 632 else 633 iv.push_back (std::make_pair (w.x0, w.x1)); 634 } 635 } 636 637 638 /** 639 * Look for a vertical gutter separating two text columns. 640 * 641 * @param words words of the page 642 * @param lines lines of the page 643 * @param furniture which lines are page furniture (ignored) 644 * @param[out] g0 left end of the gutter 645 * @param[out] g1 right end of the gutter 646 * @return true if a gutter was found 647 */ 648 static bool 649 find_gutter (const std::vector<Word> &words, 650 const std::vector<Line> &lines, 651 const std::vector<bool> &furniture, 652 double &g0, 653 double &g1) 654 { 655 double left = HUGE_VAL; 656 double right = -HUGE_VAL; 657 std::vector<double> sizes; 658 std::vector<std::pair<double, double> > iv; 659 size_t nbins; 660 size_t body = 0; 661 double bw; 662 double sz; 663 664 for (size_t i = 0; i < lines.size (); i++) 665 { 666 if (furniture[i]) 667 continue; 668 left = std::min (left, lines[i].x0); 669 right = std::max (right, lines[i].x1); 670 sizes.push_back (lines[i].size); 671 body++; 672 } 673 if (body < 6) 674 return false; 675 sz = median (sizes); 676 if ( (! (right - left > 10 * sz)) || 677 (! (sz > 0)) ) 678 return false; 679 nbins = 1000; 680 bw = (right - left) / nbins; 681 std::vector<long> cnt (nbins + 1, 0); 682 683 /* difference array: +1 where an interval starts, -1 after its end */ 684 for (size_t i = 0; i < lines.size (); i++) 685 { 686 if (furniture[i]) 687 continue; 688 line_coverage (words, lines[i], iv); 689 for (const auto &p : iv) 690 { 691 double f0 = std::max (0.0, (p.first - left) / bw); 692 double f1 = std::max (0.0, (p.second - left) / bw); 693 size_t b0 = (size_t) std::min (f0, (double) nbins); 694 size_t b1 = (size_t) std::min (f1 + 1, (double) nbins); 695 696 cnt[b0]++; 697 cnt[b1]--; 698 } 699 } 700 for (size_t b = 1; b < nbins; b++) 701 cnt[b] += cnt[b - 1]; 702 /* widest run of rarely covered bins in the middle half */ 703 long thr = (long) (body / 5); 704 size_t best_s = 0; 705 size_t best_len = 0; 706 size_t run_s = 0; 707 size_t run_len = 0; 708 709 for (size_t b = nbins / 4; b < 3 * nbins / 4; b++) 710 { 711 if (cnt[b] <= thr) 712 { 713 if (0 == run_len) 714 run_s = b; 715 run_len++; 716 if (run_len > best_len) 717 { 718 best_len = run_len; 719 best_s = run_s; 720 } 721 } 722 else 723 { 724 run_len = 0; 725 } 726 } 727 if ( (0 == best_len) || 728 (best_len * bw < std::max (4.0, 0.3 * sz)) ) 729 return false; 730 g0 = left + best_s * bw; 731 g1 = left + (best_s + best_len) * bw; 732 /* both sides must hold a good share of the text */ 733 size_t nl = 0; 734 size_t nr = 0; 735 736 for (size_t i = 0; i < lines.size (); i++) 737 { 738 bool l = false; 739 bool r = false; 740 741 if (furniture[i]) 742 continue; 743 for (size_t w : lines[i].words) 744 { 745 if (words[w].x1 <= g0 + bw) 746 l = true; 747 if (words[w].x0 >= g1 - bw) 748 r = true; 749 } 750 if (l) 751 nl++; 752 if (r) 753 nr++; 754 } 755 return (10 * nl >= 3 * body) && (10 * nr >= 3 * body); 756 } 757 758 759 /** 760 * Build the text of a line from its words and add it to @a page. 761 * 762 * @param words words of the page 763 * @param widx indices of the words of the line, left to right 764 * @param page_top top of the page 765 * @param page_h height of the page 766 * @param page page to add to 767 * @param geom geometry array of @a page to append to 768 * @param total_bytes running total of text bytes, updated 769 * @return 0 on success, -1 to stop (limit reached or no memory) 770 */ 771 static int 772 emit_line (const std::vector<Word> &words, 773 const std::vector<size_t> &widx, 774 double page_top, 775 double page_h, 776 struct EXTRACTOR_FullTextPage *page, 777 std::vector<struct EXTRACTOR_PageLineGeom> &geom, 778 size_t &total_bytes) 779 { 780 std::string text; 781 std::vector<double> bases; 782 std::vector<double> hs; 783 struct EXTRACTOR_PageLineGeom g; 784 double y; 785 786 if (widx.empty ()) 787 return 0; 788 g.x0 = HUGE_VAL; 789 g.x1 = -HUGE_VAL; 790 for (size_t k = 0; k < widx.size (); k++) 791 { 792 const Word &w = words[widx[k]]; 793 794 bases.push_back (w.y1); 795 hs.push_back (w.y1 - w.y0); 796 g.x0 = std::min (g.x0, w.x0); 797 g.x1 = std::max (g.x1, w.x1); 798 } 799 g.y = median (bases); 800 g.size = median (hs); 801 for (size_t k = 0; k < widx.size (); k++) 802 { 803 const Word &w = words[widx[k]]; 804 805 if (k > 0) 806 { 807 const Word &p = words[widx[k - 1]]; 808 809 if ( (p.space_after) || 810 (w.x0 - p.x1 > 0.15 * g.size) ) 811 text += ' '; 812 } 813 append_expanded (text, w.text); 814 } 815 compose_accents (text); 816 if (text.empty ()) 817 return 0; 818 if (total_bytes + text.size () >= EXTRACTOR_FULLTEXT_MAX) 819 return -1; 820 y = (g.y - page_top) / page_h; 821 if (! (y >= 0)) 822 y = 0; 823 if (y > 1) 824 y = 1; 825 if (0 != EXTRACTOR_fulltext_page_add_ (page, 826 text.data (), 827 text.size (), 828 y, 829 0)) 830 return -1; 831 geom.push_back (g); 832 total_bytes += text.size (); 833 return 0; 834 } 835 836 837 /** 838 * Collect the lines of page @a pg in reading order. 839 * 840 * @param pg the page 841 * @param page page to fill 842 * @param geom geometry of the lines to fill 843 * @param total_bytes running total of text bytes, updated 844 * @return 0 on success, -1 to stop 845 */ 846 static int 847 collect_page (poppler::page *pg, 848 struct EXTRACTOR_FullTextPage *page, 849 std::vector<struct EXTRACTOR_PageLineGeom> &geom, 850 size_t &total_bytes) 851 { 852 poppler::rectf rect = pg->page_rect (); 853 double top = rect.top (); 854 double ph = rect.height (); 855 std::vector<poppler::text_box> boxes; 856 std::vector<Word> words; 857 std::vector<Line> lines; 858 859 if ( (! (ph > 0)) || 860 (! std::isfinite (ph)) || 861 (! std::isfinite (top)) ) 862 return 0; 863 boxes = pg->text_list (); 864 for (const poppler::text_box &b : boxes) 865 { 866 poppler::rectf r = b.bbox (); 867 std::vector<char> u = b.text ().to_utf8 (); 868 Word w; 869 870 if (words.size () >= PDF_FT_MAX_WORDS) 871 break; 872 if ( (0 != b.rotation ()) || 873 (u.empty ()) ) 874 continue; 875 w.x0 = r.left (); 876 w.x1 = r.right (); 877 w.y0 = r.top (); 878 w.y1 = r.bottom (); 879 if ( (! std::isfinite (w.x0)) || 880 (! std::isfinite (w.x1)) || 881 (! std::isfinite (w.y0)) || 882 (! std::isfinite (w.y1)) || 883 (! (w.y1 > w.y0)) || 884 (w.x1 < w.x0) || 885 (w.y1 - w.y0 > ph) ) 886 continue; 887 w.text.assign (u.data (), u.size ()); 888 w.space_after = b.has_space_after (); 889 w.numeric = (w.text.size () <= 4); 890 for (char c : w.text) 891 if (! isdigit ((unsigned char) c)) 892 w.numeric = false; 893 words.push_back (std::move (w)); 894 } 895 boxes.clear (); 896 if (words.empty ()) 897 return 0; 898 drop_marginal_numbers (words); 899 group_lines (words, lines); 900 if (lines.empty ()) 901 return 0; 902 903 /* page furniture: lines in the top/bottom band set apart from 904 the rest by a clear vertical gap */ 905 std::vector<bool> furniture (lines.size (), false); 906 { 907 std::vector<double> gaps; 908 double spacing; 909 910 for (size_t i = 1; i < lines.size (); i++) 911 { 912 double d = lines[i].base - lines[i - 1].base; 913 914 if ( (d > 0.3 * lines[i].size) && 915 (d < 3 * lines[i].size) ) 916 gaps.push_back (d); 917 } 918 spacing = gaps.empty () ? 0 : median (gaps); 919 for (size_t i = 0; i + 1 < lines.size (); i++) 920 { 921 if ( (lines[i].base - top) / ph > PDF_FT_BAND) 922 break; 923 if (lines[i + 1].base - lines[i].base <= 1.3 * spacing) 924 break; 925 furniture[i] = true; 926 } 927 for (size_t i = lines.size () - 1; i > 0; i--) 928 { 929 if ( (lines[i].base - top) / ph < 1.0 - PDF_FT_BAND) 930 break; 931 if (lines[i].base - lines[i - 1].base <= 1.3 * spacing) 932 break; 933 furniture[i] = true; 934 } 935 } 936 937 double g0 = 0; 938 double g1 = 0; 939 bool cols = find_gutter (words, lines, furniture, g0, g1); 940 std::vector<std::vector<size_t> > left_col; 941 std::vector<std::vector<size_t> > right_col; 942 std::vector<std::pair<double, double> > iv; 943 944 for (size_t i = 0; i <= lines.size (); i++) 945 { 946 bool spanning = true; 947 948 if ( (i < lines.size ()) && 949 cols && 950 (! furniture[i]) ) 951 { 952 line_coverage (words, lines[i], iv); 953 spanning = false; 954 for (const auto &p : iv) 955 if ( (p.first < g1) && 956 (p.second > g0) ) 957 spanning = true; 958 } 959 if (spanning) 960 { 961 /* flush the column region above */ 962 for (const auto &l : left_col) 963 if (0 != emit_line (words, l, top, ph, page, geom, total_bytes)) 964 return -1; 965 for (const auto &l : right_col) 966 if (0 != emit_line (words, l, top, ph, page, geom, total_bytes)) 967 return -1; 968 left_col.clear (); 969 right_col.clear (); 970 if (i == lines.size ()) 971 break; 972 if (0 != emit_line (words, lines[i].words, top, ph, page, geom, 973 total_bytes)) 974 return -1; 975 continue; 976 } 977 std::vector<size_t> l; 978 std::vector<size_t> r; 979 980 for (size_t w : lines[i].words) 981 { 982 if (words[w].x1 <= g1) 983 l.push_back (w); 984 else 985 r.push_back (w); 986 } 987 if (! l.empty ()) 988 left_col.push_back (std::move (l)); 989 if (! r.empty ()) 990 right_col.push_back (std::move (r)); 991 } 992 return 0; 993 } 994 995 996 /** 997 * Extract the full text of @a doc and emit it. 998 * 999 * @param ec extraction context 1000 * @param doc the document 1001 * @return 0 to continue, 1 if the callback asked us to stop 1002 */ 1003 static int 1004 pdf_fulltext (struct EXTRACTOR_ExtractContext *ec, 1005 poppler::document *doc) 1006 { 1007 std::vector<struct EXTRACTOR_FullTextPage> pages; 1008 std::vector<std::vector<struct EXTRACTOR_PageLineGeom> > geoms; 1009 std::vector<struct EXTRACTOR_PageLineGeom *> gptrs; 1010 size_t total_bytes = 0; 1011 size_t total_lines = 0; 1012 double deadline = now_s () + PDF_FT_DEADLINE; 1013 int npages = doc->pages (); 1014 int ret = 0; 1015 1016 if (npages > PDF_FT_MAX_PAGES) 1017 npages = PDF_FT_MAX_PAGES; 1018 try 1019 { 1020 pages.reserve (npages); 1021 geoms.reserve (npages); 1022 for (int i = 0; i < npages; i++) 1023 { 1024 poppler::page *pg; 1025 struct EXTRACTOR_FullTextPage fp; 1026 int r; 1027 1028 if (now_s () > deadline) 1029 break; 1030 /* keep the host from presuming us hung */ 1031 ec->seek (ec->cls, 0, SEEK_CUR); 1032 pg = doc->create_page (i); 1033 if (NULL == pg) 1034 continue; 1035 memset (&fp, 0, sizeof (fp)); 1036 pages.push_back (fp); 1037 geoms.emplace_back (); 1038 r = collect_page (pg, 1039 &pages.back (), 1040 geoms.back (), 1041 total_bytes); 1042 delete pg; 1043 total_lines += pages.back ().num_lines; 1044 if ( (0 != r) || 1045 (total_lines > PDF_FT_MAX_LINES) ) 1046 break; 1047 } 1048 } 1049 catch (...) 1050 { 1051 /* out of memory: go with what we have */ 1052 } 1053 /* we may have failed half-way; keep geometry and lines in sync */ 1054 while (pages.size () > geoms.size ()) 1055 { 1056 EXTRACTOR_fulltext_page_clear_ (&pages.back ()); 1057 pages.pop_back (); 1058 } 1059 for (size_t i = 0; i < pages.size (); i++) 1060 while (pages[i].num_lines > geoms[i].size ()) 1061 { 1062 pages[i].num_lines--; 1063 free (pages[i].lines[pages[i].num_lines].text); 1064 } 1065 if (! pages.empty ()) 1066 { 1067 struct EXTRACTOR_FullText ft; 1068 1069 for (size_t i = 0; i < geoms.size (); i++) 1070 gptrs.push_back (geoms[i].data ()); 1071 EXTRACTOR_fulltext_filter_margins_ (pages.data (), 1072 (unsigned int) pages.size ()); 1073 EXTRACTOR_pagelayout_paragraphs_ (pages.data (), 1074 gptrs.data (), 1075 (unsigned int) pages.size (), 1076 0.02); 1077 EXTRACTOR_fulltext_init_ (&ft); 1078 EXTRACTOR_fulltext_append_pages_ (&ft, 1079 pages.data (), 1080 (unsigned int) pages.size ()); 1081 ret = EXTRACTOR_fulltext_emit_ (&ft, 1082 ec, 1083 "pdf"); 1084 EXTRACTOR_fulltext_clear_ (&ft); 1085 } 1086 for (size_t i = 0; i < pages.size (); i++) 1087 EXTRACTOR_fulltext_page_clear_ (&pages[i]); 1088 return (0 != ret) ? 1 : 0; 1089 } 1090 1091 1092 /** 1093 * Main entry method for the PDF extraction plugin. 1094 * 1095 * @param ec extraction context provided to the plugin 1096 */ 1097 extern "C" void 1098 EXTRACTOR_pdf_extract_method (struct EXTRACTOR_ExtractContext *ec) 1099 { 1100 void *hdr; 1101 std::vector<char> buf; 1102 poppler::document *doc; 1103 int major; 1104 int minor; 1105 char ver[32]; 1106 char pages[16]; 1107 1108 if (4 != ec->read (ec->cls, &hdr, 4)) 1109 return; 1110 if (0 != memcmp ("%PDF", hdr, 4)) 1111 return; 1112 if (0 != read_all (ec, buf)) 1113 return; 1114 1115 poppler::set_debug_error_function (&quiet_error, NULL); 1116 doc = poppler::document::load_from_raw_data (buf.data (), 1117 (int) buf.size ()); 1118 if (NULL == doc) 1119 return; 1120 /* An encrypted document we cannot open exposes no usable meta data. */ 1121 if (doc->is_locked ()) 1122 { 1123 delete doc; 1124 return; 1125 } 1126 1127 if (0 != add_str (ec, 1128 EXTRACTOR_METATYPE_MIMETYPE, 1129 "application/pdf")) 1130 goto CLEANUP; 1131 for (unsigned int i = 0; NULL != tmap[i].get; i++) 1132 if (0 != add_utf8 (ec, 1133 tmap[i].type, 1134 (doc->*tmap[i].get)().to_utf8 ())) 1135 goto CLEANUP; 1136 doc->get_pdf_version (&major, &minor); 1137 snprintf (ver, sizeof (ver), "PDF %d.%d", major, minor); 1138 if (0 != add_str (ec, EXTRACTOR_METATYPE_FORMAT, ver)) 1139 goto CLEANUP; 1140 snprintf (pages, sizeof (pages), "%d", doc->pages ()); 1141 if (0 != add_str (ec, EXTRACTOR_METATYPE_PAGE_COUNT, pages)) 1142 goto CLEANUP; 1143 if (0 != add_date (ec, 1144 EXTRACTOR_METATYPE_CREATION_DATE, 1145 doc->get_creation_date_t ())) 1146 goto CLEANUP; 1147 if (0 != add_date (ec, 1148 EXTRACTOR_METATYPE_MODIFICATION_DATE, 1149 doc->get_modification_date_t ())) 1150 goto CLEANUP; 1151 if (EXTRACTOR_fulltext_requested_ (ec->config)) 1152 (void) pdf_fulltext (ec, doc); 1153 CLEANUP: 1154 delete doc; 1155 } 1156 1157 1158 /* end of pdf_extractor.cc */