libextractor

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

fulltext.c (21121B)


      1 /*
      2      This file is part of libextractor.
      3      Copyright (C) 2026 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/fulltext.c
     22  * @brief helpers shared by the plugins implementing the "fulltext" option
     23  * @author Christian Grothoff
     24  */
     25 #include "platform.h"
     26 #include "fulltext.h"
     27 #include <strings.h>
     28 
     29 
     30 const char *
     31 EXTRACTOR_config_find_ (const char *config,
     32                         const char *key)
     33 {
     34   const char *pos;
     35   size_t klen;
     36 
     37   if ( (NULL == config) ||
     38        (NULL == key) )
     39     return NULL;
     40   klen = strlen (key);
     41   pos = config;
     42   while (1)
     43   {
     44     if ( (0 == strncmp (pos,
     45                         key,
     46                         klen)) &&
     47          ( ('\0' == pos[klen]) ||
     48            (',' == pos[klen]) ||
     49            ('=' == pos[klen]) ||
     50            (':' == pos[klen]) ) )
     51       return &pos[klen];
     52     pos = strchr (pos, ',');
     53     if (NULL == pos)
     54       return NULL;
     55     pos++;
     56   }
     57 }
     58 
     59 
     60 int
     61 EXTRACTOR_fulltext_requested_ (const char *config)
     62 {
     63   return (NULL != EXTRACTOR_config_find_ (config,
     64                                           "fulltext")) ? 1 : 0;
     65 }
     66 
     67 
     68 void
     69 EXTRACTOR_fulltext_init_ (struct EXTRACTOR_FullText *ft)
     70 {
     71   memset (ft, 0, sizeof (*ft));
     72 }
     73 
     74 
     75 void
     76 EXTRACTOR_fulltext_set_context_ (struct EXTRACTOR_FullText *ft,
     77                                  struct EXTRACTOR_ExtractContext *ec)
     78 {
     79   ft->ec = ec;
     80 }
     81 
     82 
     83 /**
     84  * Number of units of work between two keep-alives; the plugin side
     85  * of the IPC rate-limits them by time anyway, so this only bounds
     86  * the cost of asking.
     87  */
     88 #define TICKS_PER_ALIVE 4096
     89 
     90 
     91 void
     92 EXTRACTOR_fulltext_tick_ (struct EXTRACTOR_FullText *ft)
     93 {
     94   if (NULL == ft->ec)
     95     return;
     96   if (++ft->ticks < TICKS_PER_ALIVE)
     97     return;
     98   ft->ticks = 0;
     99   (void) ft->ec->seek (ft->ec->cls,
    100                        0,
    101                        SEEK_CUR);
    102 }
    103 
    104 
    105 /**
    106  * Check whether @a cp is whitespace for our purposes.
    107  *
    108  * @param cp code point to check
    109  * @return 1 if @a cp is whitespace
    110  */
    111 static int
    112 is_space_cp (uint32_t cp)
    113 {
    114   switch (cp)
    115   {
    116   case ' ':
    117   case '\t':
    118   case '\n':
    119   case '\r':
    120   case '\f':
    121   case '\v':
    122   case 0x85:
    123   case 0xA0:
    124   case 0x1680:
    125   case 0x2028:
    126   case 0x2029:
    127   case 0x202F:
    128   case 0x205F:
    129   case 0x3000:
    130     return 1;
    131   default:
    132     return ( (cp >= 0x2000) && (cp <= 0x200A) ) ? 1 : 0;
    133   }
    134 }
    135 
    136 
    137 /**
    138  * Check whether @a cp must be dropped silently: control characters,
    139  * soft hyphens, zero-width characters and byte order marks.
    140  *
    141  * @param cp code point to check
    142  * @return 1 if @a cp is to be dropped
    143  */
    144 static int
    145 is_dropped_cp (uint32_t cp)
    146 {
    147   if ( (cp < 0x20) ||
    148        ( (cp >= 0x7F) && (cp < 0xA0) ) )
    149     return 1;
    150   switch (cp)
    151   {
    152   case 0xAD:
    153   case 0x200B:
    154   case 0x200C:
    155   case 0x200D:
    156   case 0x2060:
    157   case 0xFEFF:
    158     return 1;
    159   default:
    160     return 0;
    161   }
    162 }
    163 
    164 
    165 /**
    166  * Make room for @a extra more bytes plus the terminating 0.
    167  *
    168  * @param ft buffer to grow
    169  * @param extra number of bytes we want to append
    170  * @return 0 on success, -1 if the buffer is full
    171  */
    172 static int
    173 reserve (struct EXTRACTOR_FullText *ft,
    174          size_t extra)
    175 {
    176   size_t need;
    177   size_t ncap;
    178   char *nbuf;
    179 
    180   if (ft->full)
    181     return -1;
    182   need = ft->len + extra + 1;
    183   if (need > EXTRACTOR_FULLTEXT_MAX)
    184   {
    185     ft->full = 1;
    186     return -1;
    187   }
    188   if (need <= ft->cap)
    189     return 0;
    190   ncap = (0 == ft->cap) ? 4096 : ft->cap;
    191   while (ncap < need)
    192     ncap *= 2;
    193   if (ncap > EXTRACTOR_FULLTEXT_MAX)
    194     ncap = EXTRACTOR_FULLTEXT_MAX;
    195   nbuf = realloc (ft->buf,
    196                   ncap);
    197   if (NULL == nbuf)
    198   {
    199     ft->full = 1;
    200     return -1;
    201   }
    202   ft->buf = nbuf;
    203   ft->cap = ncap;
    204   return 0;
    205 }
    206 
    207 
    208 void
    209 EXTRACTOR_fulltext_append_cp_ (struct EXTRACTOR_FullText *ft,
    210                                uint32_t cp)
    211 {
    212   char enc[4];
    213   size_t elen;
    214   const char *sep;
    215   size_t slen;
    216 
    217   if (ft->full)
    218     return;
    219   EXTRACTOR_fulltext_tick_ (ft);
    220   if (is_space_cp (cp))
    221   {
    222     EXTRACTOR_fulltext_space_ (ft);
    223     return;
    224   }
    225   if (is_dropped_cp (cp))
    226     return;
    227   if ( (cp > 0x10FFFF) ||
    228        ( (cp >= 0xD800) && (cp <= 0xDFFF) ) )
    229     cp = 0xFFFD;
    230   if (cp < 0x80)
    231   {
    232     enc[0] = (char) cp;
    233     elen = 1;
    234   }
    235   else if (cp < 0x800)
    236   {
    237     enc[0] = (char) (0xC0 | (cp >> 6));
    238     enc[1] = (char) (0x80 | (cp & 0x3F));
    239     elen = 2;
    240   }
    241   else if (cp < 0x10000)
    242   {
    243     enc[0] = (char) (0xE0 | (cp >> 12));
    244     enc[1] = (char) (0x80 | ((cp >> 6) & 0x3F));
    245     enc[2] = (char) (0x80 | (cp & 0x3F));
    246     elen = 3;
    247   }
    248   else
    249   {
    250     enc[0] = (char) (0xF0 | (cp >> 18));
    251     enc[1] = (char) (0x80 | ((cp >> 12) & 0x3F));
    252     enc[2] = (char) (0x80 | ((cp >> 6) & 0x3F));
    253     enc[3] = (char) (0x80 | (cp & 0x3F));
    254     elen = 4;
    255   }
    256   sep = "";
    257   slen = 0;
    258   if (ft->len > 0)
    259   {
    260     if (ft->pending_par)
    261     {
    262       sep = "\n\n";
    263       slen = 2;
    264     }
    265     else if (ft->pending_space)
    266     {
    267       sep = " ";
    268       slen = 1;
    269     }
    270   }
    271   if (0 != reserve (ft,
    272                     slen + elen))
    273     return;
    274   memcpy (&ft->buf[ft->len],
    275           sep,
    276           slen);
    277   ft->len += slen;
    278   memcpy (&ft->buf[ft->len],
    279           enc,
    280           elen);
    281   ft->len += elen;
    282   ft->buf[ft->len] = '\0';
    283   ft->pending_par = 0;
    284   ft->pending_space = 0;
    285 }
    286 
    287 
    288 /**
    289  * Decode one UTF-8 sequence.
    290  *
    291  * @param s input
    292  * @param len number of bytes available at @a s, at least 1
    293  * @param[out] cp decoded code point, U+FFFD if invalid
    294  * @return number of bytes consumed (at least 1)
    295  */
    296 static size_t
    297 decode_utf8 (const unsigned char *s,
    298              size_t len,
    299              uint32_t *cp)
    300 {
    301   uint32_t c;
    302   size_t n;
    303   size_t i;
    304   uint32_t min;
    305 
    306   c = s[0];
    307   if (c < 0x80)
    308   {
    309     *cp = c;
    310     return 1;
    311   }
    312   if ( (c & 0xE0) == 0xC0)
    313   {
    314     n = 2;
    315     c &= 0x1F;
    316     min = 0x80;
    317   }
    318   else if ( (c & 0xF0) == 0xE0)
    319   {
    320     n = 3;
    321     c &= 0x0F;
    322     min = 0x800;
    323   }
    324   else if ( (c & 0xF8) == 0xF0)
    325   {
    326     n = 4;
    327     c &= 0x07;
    328     min = 0x10000;
    329   }
    330   else
    331   {
    332     *cp = 0xFFFD;
    333     return 1;
    334   }
    335   if (n > len)
    336   {
    337     *cp = 0xFFFD;
    338     return 1;
    339   }
    340   for (i = 1; i < n; i++)
    341   {
    342     if ( (s[i] & 0xC0) != 0x80)
    343     {
    344       *cp = 0xFFFD;
    345       return 1;
    346     }
    347     c = (c << 6) | (s[i] & 0x3F);
    348   }
    349   if ( (c < min) ||
    350        (c > 0x10FFFF) ||
    351        ( (c >= 0xD800) && (c <= 0xDFFF) ) )
    352   {
    353     *cp = 0xFFFD;
    354     return 1;
    355   }
    356   *cp = c;
    357   return n;
    358 }
    359 
    360 
    361 void
    362 EXTRACTOR_fulltext_append_ (struct EXTRACTOR_FullText *ft,
    363                             const char *text,
    364                             size_t len)
    365 {
    366   const unsigned char *s = (const unsigned char *) text;
    367   size_t off;
    368   uint32_t cp;
    369 
    370   off = 0;
    371   while ( (off < len) &&
    372           (! ft->full) )
    373   {
    374     off += decode_utf8 (&s[off],
    375                         len - off,
    376                         &cp);
    377     EXTRACTOR_fulltext_append_cp_ (ft,
    378                                    cp);
    379   }
    380 }
    381 
    382 
    383 void
    384 EXTRACTOR_fulltext_append_str_ (struct EXTRACTOR_FullText *ft,
    385                                 const char *text)
    386 {
    387   if (NULL == text)
    388     return;
    389   EXTRACTOR_fulltext_append_ (ft,
    390                               text,
    391                               strlen (text));
    392 }
    393 
    394 
    395 void
    396 EXTRACTOR_fulltext_space_ (struct EXTRACTOR_FullText *ft)
    397 {
    398   if (ft->len > 0)
    399     ft->pending_space = 1;
    400 }
    401 
    402 
    403 void
    404 EXTRACTOR_fulltext_paragraph_ (struct EXTRACTOR_FullText *ft)
    405 {
    406   if (ft->len > 0)
    407     ft->pending_par = 1;
    408 }
    409 
    410 
    411 int
    412 EXTRACTOR_fulltext_emit_ (const struct EXTRACTOR_FullText *ft,
    413                           struct EXTRACTOR_ExtractContext *ec,
    414                           const char *plugin_name)
    415 {
    416   if (0 == ft->len)
    417     return 0;
    418   return ec->proc (ec->cls,
    419                    plugin_name,
    420                    EXTRACTOR_METATYPE_FULL_TEXT,
    421                    EXTRACTOR_METAFORMAT_UTF8,
    422                    "text/plain",
    423                    ft->buf,
    424                    ft->len + 1);
    425 }
    426 
    427 
    428 void
    429 EXTRACTOR_fulltext_clear_ (struct EXTRACTOR_FullText *ft)
    430 {
    431   free (ft->buf);
    432   memset (ft, 0, sizeof (*ft));
    433 }
    434 
    435 
    436 /* ------------------------------------------------------------------ */
    437 /* Page furniture                                                      */
    438 /* ------------------------------------------------------------------ */
    439 
    440 /**
    441  * Words that may introduce a page number ("Page 3", "Seite 3", ...).
    442  */
    443 static const char *const page_words[] = {
    444   "page", "p", "pg", "pp", "seite", "s", "pagina", "página", "pag",
    445   "страница", "стр", "sida", "side", "sivu", "strona", "str", NULL
    446 };
    447 
    448 /**
    449  * Words that may join page number and page count ("3 of 17").
    450  */
    451 static const char *const of_words[] = {
    452   "of", "von", "de", "sur", "di", "van", "av", "af", "из", "z", "/", NULL
    453 };
    454 
    455 
    456 /**
    457  * Check whether @a tok (of @a len bytes) is in @a list, ignoring ASCII
    458  * case and a trailing '.'.
    459  *
    460  * @param tok token to check
    461  * @param len length of @a tok
    462  * @param list NULL-terminated word list
    463  * @return 1 if found
    464  */
    465 static int
    466 in_list (const char *tok,
    467          size_t len,
    468          const char *const *list)
    469 {
    470   unsigned int i;
    471 
    472   if ( (len > 1) &&
    473        ('.' == tok[len - 1]) )
    474     len--;
    475   for (i = 0; NULL != list[i]; i++)
    476     if ( (strlen (list[i]) == len) &&
    477          (0 == strncasecmp (tok,
    478                             list[i],
    479                             len)) )
    480       return 1;
    481   return 0;
    482 }
    483 
    484 
    485 /**
    486  * Check whether @a tok is a number: 1-5 decimal digits, or a roman
    487  * numeral of at most 8 characters.
    488  *
    489  * @param tok token to check
    490  * @param len length of @a tok
    491  * @return 1 if @a tok is a number
    492  */
    493 static int
    494 is_number (const char *tok,
    495            size_t len)
    496 {
    497   size_t i;
    498   int digits = 1;
    499   int roman = 1;
    500 
    501   if ( (0 == len) ||
    502        (len > 8) )
    503     return 0;
    504   for (i = 0; i < len; i++)
    505   {
    506     char c = tok[i];
    507 
    508     if (! isdigit ((unsigned char) c))
    509       digits = 0;
    510     if (NULL == strchr ("ivxlcdmIVXLCDM", c))
    511       roman = 0;
    512   }
    513   if (digits && (len <= 5))
    514     return 1;
    515   return roman;
    516 }
    517 
    518 
    519 /**
    520  * Check whether @a c is a decoration character around page numbers.
    521  *
    522  * @param c character to check
    523  * @return 1 if @a c is decoration
    524  */
    525 static int
    526 is_decoration (char c)
    527 {
    528   return (NULL != strchr (" \t\r\n-.,:;[]()|*~<>_=#", c)) ? 1 : 0;
    529 }
    530 
    531 
    532 int
    533 EXTRACTOR_fulltext_is_page_number_ (const char *line,
    534                                     size_t len)
    535 {
    536   const char *tok[4];
    537   size_t tlen[4];
    538   unsigned int ntok;
    539   size_t pos;
    540   size_t start;
    541   size_t end;
    542 
    543   /* strip ASCII decorations and UTF-8 en/em dashes at both ends */
    544   start = 0;
    545   end = len;
    546   while (start < end)
    547   {
    548     if (is_decoration (line[start]))
    549       start++;
    550     else if ( (start + 3 <= end) &&
    551               (0 == memcmp (&line[start], "\xE2\x80", 2)) &&
    552               ( ((unsigned char) line[start + 2] >= 0x93) &&
    553                 ((unsigned char) line[start + 2] <= 0x94) ) )
    554       start += 3;
    555     else
    556       break;
    557   }
    558   while (end > start)
    559   {
    560     if (is_decoration (line[end - 1]))
    561       end--;
    562     else if ( (end >= start + 3) &&
    563               (0 == memcmp (&line[end - 3], "\xE2\x80", 2)) &&
    564               ( ((unsigned char) line[end - 1] >= 0x93) &&
    565                 ((unsigned char) line[end - 1] <= 0x94) ) )
    566       end -= 3;
    567     else
    568       break;
    569   }
    570   if (start == end)
    571     return 0;
    572   /* tokenize on spaces, and split '/' into its own token */
    573   ntok = 0;
    574   pos = start;
    575   while (pos < end)
    576   {
    577     size_t ts;
    578 
    579     while ( (pos < end) &&
    580             ( (' ' == line[pos]) || ('\t' == line[pos]) ) )
    581       pos++;
    582     if (pos == end)
    583       break;
    584     if (ntok == 4)
    585       return 0;
    586     ts = pos;
    587     if ('/' == line[pos])
    588       pos++;
    589     else
    590       while ( (pos < end) &&
    591               (' ' != line[pos]) &&
    592               ('\t' != line[pos]) &&
    593               ('/' != line[pos]) )
    594         pos++;
    595     tok[ntok] = &line[ts];
    596     tlen[ntok] = pos - ts;
    597     ntok++;
    598   }
    599   /* "N" | "WORD N" | "N OF M" | "WORD N OF M" */
    600   pos = 0;
    601   if ( (ntok > 1) &&
    602        in_list (tok[0], tlen[0], page_words) )
    603     pos = 1;
    604   if ( (pos >= ntok) ||
    605        (! is_number (tok[pos], tlen[pos])) )
    606     return 0;
    607   pos++;
    608   if (pos == ntok)
    609     return 1;
    610   if ( (pos + 2 == ntok) &&
    611        in_list (tok[pos], tlen[pos], of_words) &&
    612        is_number (tok[pos + 1], tlen[pos + 1]) )
    613     return 1;
    614   return 0;
    615 }
    616 
    617 
    618 int
    619 EXTRACTOR_fulltext_page_add_ (struct EXTRACTOR_FullTextPage *page,
    620                               const char *text,
    621                               size_t len,
    622                               double y,
    623                               int par_end)
    624 {
    625   struct EXTRACTOR_FullTextLine *l;
    626 
    627   if (page->num_lines == page->cap_lines)
    628   {
    629     unsigned int ncap;
    630     struct EXTRACTOR_FullTextLine *nl;
    631 
    632     ncap = (0 == page->cap_lines) ? 32 : 2 * page->cap_lines;
    633     if (ncap > 1024 * 1024)
    634       return -1;
    635     nl = realloc (page->lines,
    636                   ncap * sizeof (*nl));
    637     if (NULL == nl)
    638       return -1;
    639     page->lines = nl;
    640     page->cap_lines = ncap;
    641   }
    642   l = &page->lines[page->num_lines];
    643   l->text = malloc (len + 1);
    644   if (NULL == l->text)
    645     return -1;
    646   memcpy (l->text,
    647           text,
    648           len);
    649   l->text[len] = '\0';
    650   l->y = y;
    651   l->par_end = par_end;
    652   l->skip = 0;
    653   page->num_lines++;
    654   return 0;
    655 }
    656 
    657 
    658 void
    659 EXTRACTOR_fulltext_page_clear_ (struct EXTRACTOR_FullTextPage *page)
    660 {
    661   unsigned int i;
    662 
    663   for (i = 0; i < page->num_lines; i++)
    664     free (page->lines[i].text);
    665   free (page->lines);
    666   memset (page, 0, sizeof (*page));
    667 }
    668 
    669 
    670 /**
    671  * Maximum number of lines at the top and at the bottom of a page we
    672  * consider as page furniture.
    673  */
    674 #define MARGIN_LINES 3
    675 
    676 /**
    677  * Relative height of the top and bottom margin bands.
    678  */
    679 #define MARGIN_BAND 0.12
    680 
    681 /**
    682  * Lines longer than this are never page furniture.
    683  */
    684 #define MARGIN_MAX_LEN 200
    685 
    686 
    687 /**
    688  * A margin line candidate for running header/footer detection.
    689  */
    690 struct Candidate
    691 {
    692   /**
    693    * Normalized text: lower-case ASCII, digits and whitespace removed.
    694    */
    695   char *key;
    696 
    697   /**
    698    * The line this candidate refers to.
    699    */
    700   struct EXTRACTOR_FullTextLine *line;
    701 
    702   /**
    703    * Page number of the line.
    704    */
    705   unsigned int page;
    706 
    707   /**
    708    * 0 for the top band, 1 for the bottom band.
    709    */
    710   int band;
    711 };
    712 
    713 
    714 /**
    715  * Order candidates by band, then key, then page.
    716  *
    717  * @param a first candidate
    718  * @param b second candidate
    719  * @return comparison result
    720  */
    721 static int
    722 cmp_candidate (const void *a,
    723                const void *b)
    724 {
    725   const struct Candidate *ca = a;
    726   const struct Candidate *cb = b;
    727   int r;
    728 
    729   if (ca->band != cb->band)
    730     return ca->band - cb->band;
    731   r = strcmp (ca->key,
    732               cb->key);
    733   if (0 != r)
    734     return r;
    735   if (ca->page != cb->page)
    736     return (ca->page < cb->page) ? -1 : 1;
    737   return 0;
    738 }
    739 
    740 
    741 /**
    742  * Normalize @a text for running header comparison.
    743  *
    744  * @param text line text
    745  * @return normalized copy, NULL on allocation failure
    746  */
    747 static char *
    748 normalize_key (const char *text)
    749 {
    750   size_t len = strlen (text);
    751   char *key;
    752   size_t i;
    753   size_t o;
    754 
    755   key = malloc (len + 1);
    756   if (NULL == key)
    757     return NULL;
    758   o = 0;
    759   for (i = 0; i < len; i++)
    760   {
    761     unsigned char c = (unsigned char) text[i];
    762 
    763     if (isdigit (c) || isspace (c))
    764       continue;
    765     key[o++] = (char) tolower (c);
    766   }
    767   key[o] = '\0';
    768   return key;
    769 }
    770 
    771 
    772 /**
    773  * Check whether line @a i of @a page is in the margin band @a band.
    774  *
    775  * @param page page to check
    776  * @param i line index
    777  * @param band 0 for top, 1 for bottom
    778  * @return 1 if in the band
    779  */
    780 static int
    781 in_band (const struct EXTRACTOR_FullTextPage *page,
    782          unsigned int i,
    783          int band)
    784 {
    785   const struct EXTRACTOR_FullTextLine *l = &page->lines[i];
    786 
    787   if (strlen (l->text) > MARGIN_MAX_LEN)
    788     return 0;
    789   if (l->y < 0)
    790   {
    791     /* position unknown: only the first/last lines qualify */
    792     return (0 == band) ? (i < 2) : (i + 2 >= page->num_lines);
    793   }
    794   return (0 == band) ? (l->y < MARGIN_BAND) : (l->y > 1.0 - MARGIN_BAND);
    795 }
    796 
    797 
    798 void
    799 EXTRACTOR_fulltext_filter_margins_ (struct EXTRACTOR_FullTextPage *pages,
    800                                     unsigned int num_pages)
    801 {
    802   struct Candidate *cands;
    803   size_t ncands;
    804   size_t i;
    805   unsigned int p;
    806 
    807   if (0 == num_pages)
    808     return;
    809   /* pass 1: page numbers */
    810   for (p = 0; p < num_pages; p++)
    811   {
    812     struct EXTRACTOR_FullTextPage *page = &pages[p];
    813     unsigned int j;
    814 
    815     /* do not strip the only line of a page */
    816     if (page->num_lines < 2)
    817       continue;
    818     for (j = 0; (j < MARGIN_LINES) && (j < page->num_lines); j++)
    819       if (in_band (page, j, 0) &&
    820           EXTRACTOR_fulltext_is_page_number_ (page->lines[j].text,
    821                                               strlen (page->lines[j].text)))
    822         page->lines[j].skip = 1;
    823     for (j = 0; (j < MARGIN_LINES) && (j < page->num_lines); j++)
    824     {
    825       unsigned int k = page->num_lines - 1 - j;
    826 
    827       if (in_band (page, k, 1) &&
    828           EXTRACTOR_fulltext_is_page_number_ (page->lines[k].text,
    829                                               strlen (page->lines[k].text)))
    830         page->lines[k].skip = 1;
    831     }
    832   }
    833   if (num_pages < 3)
    834     return;
    835   /* pass 2: running headers and footers */
    836   if (num_pages > 1000000)
    837     return;
    838   cands = calloc ((size_t) num_pages * 2 * MARGIN_LINES,
    839                   sizeof (*cands));
    840   if (NULL == cands)
    841     return;
    842   ncands = 0;
    843   for (p = 0; p < num_pages; p++)
    844   {
    845     struct EXTRACTOR_FullTextPage *page = &pages[p];
    846     unsigned int j;
    847     int band;
    848 
    849     if (page->num_lines < 2)
    850       continue;
    851     for (band = 0; band < 2; band++)
    852       for (j = 0; (j < MARGIN_LINES) && (j < page->num_lines); j++)
    853       {
    854         unsigned int k = (0 == band) ? j : page->num_lines - 1 - j;
    855         char *key;
    856 
    857         if ( (page->lines[k].skip) ||
    858              (! in_band (page, k, band)) )
    859           continue;
    860         key = normalize_key (page->lines[k].text);
    861         if (NULL == key)
    862           continue;
    863         if ('\0' == key[0])
    864         {
    865           free (key);
    866           continue;
    867         }
    868         cands[ncands].key = key;
    869         cands[ncands].line = &page->lines[k];
    870         cands[ncands].page = p;
    871         cands[ncands].band = band;
    872         ncands++;
    873       }
    874   }
    875   qsort (cands,
    876          ncands,
    877          sizeof (*cands),
    878          &cmp_candidate);
    879   i = 0;
    880   while (i < ncands)
    881   {
    882     size_t j;
    883     unsigned int distinct_pages;
    884 
    885     distinct_pages = 1;
    886     for (j = i + 1; j < ncands; j++)
    887     {
    888       if ( (cands[j].band != cands[i].band) ||
    889            (0 != strcmp (cands[j].key, cands[i].key)) )
    890         break;
    891       if (cands[j].page != cands[j - 1].page)
    892         distinct_pages++;
    893     }
    894     if ( (distinct_pages >= 2) &&
    895          (2 * distinct_pages >= num_pages) )
    896     {
    897       size_t k;
    898 
    899       for (k = i; k < j; k++)
    900         cands[k].line->skip = 1;
    901     }
    902     i = j;
    903   }
    904   for (i = 0; i < ncands; i++)
    905     free (cands[i].key);
    906   free (cands);
    907 }
    908 
    909 
    910 /**
    911  * Check whether the hyphen at the end of @a ft is part of a compound
    912  * word: the word it ends already contains a hyphen, or the word
    913  * @a next starts with does.
    914  *
    915  * @param ft text so far, ending in '-'
    916  * @param next text of the next line
    917  * @return 1 if the hyphen is to be kept
    918  */
    919 static int
    920 is_compound (const struct EXTRACTOR_FullText *ft,
    921              const char *next)
    922 {
    923   size_t i;
    924 
    925   for (i = ft->len - 1; i > 0; i--)
    926   {
    927     char c = ft->buf[i - 1];
    928 
    929     if (' ' == c)
    930       break;
    931     if ('-' == c)
    932       return 1;
    933   }
    934   for (i = 0; ('\0' != next[i]) && (' ' != next[i]); i++)
    935     if ('-' == next[i])
    936       return 1;
    937   return 0;
    938 }
    939 
    940 
    941 void
    942 EXTRACTOR_fulltext_append_pages_ (struct EXTRACTOR_FullText *ft,
    943                                   const struct EXTRACTOR_FullTextPage *pages,
    944                                   unsigned int num_pages)
    945 {
    946   unsigned int p;
    947   unsigned int j;
    948 
    949   for (p = 0; p < num_pages; p++)
    950     for (j = 0; j < pages[p].num_lines; j++)
    951     {
    952       const struct EXTRACTOR_FullTextLine *l = &pages[p].lines[j];
    953 
    954       if (l->skip)
    955         continue;
    956       /* re-join "hyphen-\nated" words; a compound broken at one of its
    957          own hyphens ("register-\nto-register") keeps the hyphen */
    958       if ( (ft->pending_space) &&
    959            (! ft->pending_par) &&
    960            (ft->len >= 2) &&
    961            ('-' == ft->buf[ft->len - 1]) &&
    962            isalpha ((unsigned char) ft->buf[ft->len - 2]) &&
    963            islower ((unsigned char) l->text[0]) )
    964       {
    965         if (! is_compound (ft,
    966                            l->text))
    967         {
    968           ft->len--;
    969           ft->buf[ft->len] = '\0';
    970         }
    971         ft->pending_space = 0;
    972       }
    973       EXTRACTOR_fulltext_append_str_ (ft,
    974                                       l->text);
    975       if (l->par_end)
    976         EXTRACTOR_fulltext_paragraph_ (ft);
    977       else
    978         EXTRACTOR_fulltext_space_ (ft);
    979     }
    980 }
    981 
    982 
    983 /* end of fulltext.c */