libextractor

GNU libextractor
Log | Files | Refs | Submodules | README | LICENSE

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 */