pagelayout.c (14696B)
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/pagelayout.c 22 * @brief paragraph detection from line geometry for paged formats 23 * @author Christian Grothoff 24 */ 25 #include "platform.h" 26 #include "pagelayout.h" 27 #include <math.h> 28 29 /** 30 * Maximum number of left margins (text columns, indentation levels 31 * of lists, ...) we track per page. 32 */ 33 #define MAX_MARGINS 8 34 35 /** 36 * Lines whose left end is within this many font sizes of a margin 37 * start "at" that margin. 38 */ 39 #define AT_MARGIN 0.3 40 41 /** 42 * Minimum indentation of a paragraph's first line, in font sizes. 43 */ 44 #define MIN_INDENT 0.5 45 46 /** 47 * Maximum indentation of a paragraph's first line, in font sizes. 48 */ 49 #define MAX_INDENT 4.0 50 51 52 /** 53 * Per-margin statistics. 54 */ 55 struct Margin 56 { 57 /** 58 * Horizontal position of the margin. 59 */ 60 double x; 61 62 /** 63 * Typical right end of the lines starting at this margin. 64 */ 65 double right; 66 67 /** 68 * 1 if most lines at this margin end at @e right (justified text). 69 */ 70 int justified; 71 }; 72 73 74 /** 75 * Layout statistics of one page, computed over the non-skipped lines. 76 */ 77 struct PageStats 78 { 79 /** 80 * Left margins. 81 */ 82 struct Margin margins[MAX_MARGINS]; 83 84 /** 85 * Number of entries in @e margins. 86 */ 87 unsigned int num_margins; 88 89 /** 90 * Median font size. 91 */ 92 double size; 93 94 /** 95 * Typical baseline distance of consecutive lines. 96 */ 97 double spacing; 98 99 /** 100 * Relative uncertainty of the right ends. 101 */ 102 double tol; 103 }; 104 105 106 /** 107 * Compare two doubles for qsort(). 108 * 109 * @param a first value 110 * @param b second value 111 * @return comparison result 112 */ 113 static int 114 cmp_double (const void *a, 115 const void *b) 116 { 117 double x = *(const double *) a; 118 double y = *(const double *) b; 119 120 if (x < y) 121 return -1; 122 if (x > y) 123 return 1; 124 return 0; 125 } 126 127 128 /** 129 * Return the value at quantile @a q of the @a n values in @a v (which 130 * are sorted in place). 131 * 132 * @param v values, @a n > 0 133 * @param n number of values 134 * @param q quantile in [0,1] 135 * @return the quantile 136 */ 137 static double 138 quantile (double *v, 139 size_t n, 140 double q) 141 { 142 size_t i; 143 144 qsort (v, 145 n, 146 sizeof (double), 147 &cmp_double); 148 i = (size_t) (q * (double) (n - 1) + 0.5); 149 if (i >= n) 150 i = n - 1; 151 return v[i]; 152 } 153 154 155 /** 156 * Check whether a geometry value is usable. 157 * 158 * @param d value to check 159 * @return 1 if finite 160 */ 161 static int 162 ok (double d) 163 { 164 return isfinite (d) ? 1 : 0; 165 } 166 167 168 /** 169 * Find the margin a line starting at @a x belongs to: the closest 170 * margin to the left within #MAX_INDENT font sizes (or just right of 171 * it within #AT_MARGIN). 172 * 173 * @param st page statistics 174 * @param x left end of the line 175 * @return margin index, -1 if none 176 */ 177 static int 178 find_margin (const struct PageStats *st, 179 double x) 180 { 181 int best = -1; 182 double bestd = 0; 183 unsigned int i; 184 185 for (i = 0; i < st->num_margins; i++) 186 { 187 double d = x - st->margins[i].x; 188 189 if ( (d < -AT_MARGIN * st->size) || 190 (d > MAX_INDENT * st->size) ) 191 continue; 192 if ( (-1 == best) || 193 (fabs (d) < bestd) ) 194 { 195 best = (int) i; 196 bestd = fabs (d); 197 } 198 } 199 return best; 200 } 201 202 203 /** 204 * Check whether a line starting at @a x starts at a left margin. 205 * 206 * @param st page statistics 207 * @param x left end of the line 208 * @return 1 if at a margin 209 */ 210 static int 211 at_margin (const struct PageStats *st, 212 double x) 213 { 214 unsigned int i; 215 216 for (i = 0; i < st->num_margins; i++) 217 if (fabs (x - st->margins[i].x) <= AT_MARGIN * st->size) 218 return 1; 219 return 0; 220 } 221 222 223 /** 224 * Check whether line @a b (with page statistics @a st) is the indented 225 * first line of a paragraph, given that it follows line @a a. 226 * 227 * @param st statistics of the page of @a b 228 * @param a previous line 229 * @param b line to check 230 * @return 1 if @a b is indented 231 */ 232 static int 233 is_indented (const struct PageStats *st, 234 const struct EXTRACTOR_PageLineGeom *a, 235 const struct EXTRACTOR_PageLineGeom *b) 236 { 237 unsigned int i; 238 239 if (at_margin (st, 240 b->x0)) 241 return 0; 242 /* a block of lines that are all indented alike (a quote, a list) */ 243 if (fabs (a->x0 - b->x0) < AT_MARGIN * st->size) 244 return 0; 245 for (i = 0; i < st->num_margins; i++) 246 { 247 double d = b->x0 - st->margins[i].x; 248 249 if ( (d >= MIN_INDENT * st->size) && 250 (d <= MAX_INDENT * st->size) ) 251 return 1; 252 } 253 return 0; 254 } 255 256 257 /** 258 * Check whether line @a a ends well before the right margin of a 259 * justified block. 260 * 261 * @param st statistics of the page of @a a 262 * @param a line to check 263 * @return 1 if @a a is short 264 */ 265 static int 266 is_short (const struct PageStats *st, 267 const struct EXTRACTOR_PageLineGeom *a) 268 { 269 int m; 270 const struct Margin *mg; 271 double width; 272 double slack; 273 274 m = find_margin (st, 275 a->x0); 276 if (-1 == m) 277 return 0; 278 mg = &st->margins[m]; 279 if (! mg->justified) 280 return 0; 281 width = mg->right - mg->x; 282 if (width <= 0) 283 return 0; 284 slack = (0.05 + 2 * st->tol) * width; 285 if (slack < 2 * st->size) 286 slack = 2 * st->size; 287 return (a->x1 < mg->right - slack) ? 1 : 0; 288 } 289 290 291 /** 292 * Check whether the font size changes noticeably from @a a to @a b. 293 * 294 * @param a first line 295 * @param b second line 296 * @return 1 on a size change 297 */ 298 static int 299 size_change (const struct EXTRACTOR_PageLineGeom *a, 300 const struct EXTRACTOR_PageLineGeom *b) 301 { 302 double mx = (a->size > b->size) ? a->size : b->size; 303 304 return (fabs (a->size - b->size) > 0.15 * mx) ? 1 : 0; 305 } 306 307 308 /** 309 * Compute the layout statistics of page @a page. 310 * 311 * @param page the page 312 * @param g geometry of its lines 313 * @param tol relative uncertainty of right ends 314 * @param[out] st statistics to fill 315 * @return 0 on success, -1 on allocation failure or if the page has 316 * no usable lines 317 */ 318 static int 319 page_stats (const struct EXTRACTOR_FullTextPage *page, 320 const struct EXTRACTOR_PageLineGeom *g, 321 double tol, 322 struct PageStats *st) 323 { 324 double *v; 325 double *xs; 326 size_t n; 327 size_t k; 328 unsigned int i; 329 unsigned int prev; 330 int have_prev; 331 332 memset (st, 0, sizeof (*st)); 333 st->tol = tol; 334 if (0 == page->num_lines) 335 return -1; 336 v = malloc (page->num_lines * sizeof (double)); 337 xs = malloc (page->num_lines * sizeof (double)); 338 if ( (NULL == v) || 339 (NULL == xs) ) 340 { 341 free (v); 342 free (xs); 343 return -1; 344 } 345 /* median font size */ 346 n = 0; 347 for (i = 0; i < page->num_lines; i++) 348 if ( (! page->lines[i].skip) && 349 (g[i].size > 0) ) 350 v[n++] = g[i].size; 351 if (0 == n) 352 { 353 free (v); 354 free (xs); 355 return -1; 356 } 357 st->size = quantile (v, n, 0.5); 358 /* typical line spacing */ 359 n = 0; 360 have_prev = 0; 361 prev = 0; 362 for (i = 0; i < page->num_lines; i++) 363 { 364 double dy; 365 366 if (page->lines[i].skip) 367 continue; 368 if (have_prev) 369 { 370 dy = g[i].y - g[prev].y; 371 if ( (dy > 0.3 * st->size) && 372 (dy < 3 * st->size) ) 373 v[n++] = dy; 374 } 375 prev = i; 376 have_prev = 1; 377 } 378 st->spacing = (n > 0) ? quantile (v, n, 0.5) : 1.2 * st->size; 379 /* left margins: clusters of line starts */ 380 n = 0; 381 for (i = 0; i < page->num_lines; i++) 382 if (! page->lines[i].skip) 383 xs[n++] = g[i].x0; 384 qsort (xs, 385 n, 386 sizeof (double), 387 &cmp_double); 388 k = 0; 389 while (k < n) 390 { 391 size_t e = k + 1; 392 size_t cnt; 393 394 while ( (e < n) && 395 (xs[e] - xs[e - 1] <= AT_MARGIN * st->size) ) 396 e++; 397 cnt = e - k; 398 if ( ( (cnt >= 2) && 399 (cnt * 100 >= n * 15) ) || 400 ( (n < 4) && 401 (0 == k) ) ) 402 { 403 if (st->num_margins < MAX_MARGINS) 404 { 405 st->margins[st->num_margins].x = xs[k]; 406 st->num_margins++; 407 } 408 else 409 { 410 /* replace the smallest cluster? keep it simple: stop */ 411 break; 412 } 413 } 414 k = e; 415 } 416 /* right ends per margin */ 417 for (i = 0; i < st->num_margins; i++) 418 { 419 struct Margin *mg = &st->margins[i]; 420 unsigned int j; 421 size_t at; 422 size_t near; 423 double slack; 424 425 n = 0; 426 for (j = 0; j < page->num_lines; j++) 427 if ( (! page->lines[j].skip) && 428 ((int) i == find_margin (st, g[j].x0)) ) 429 v[n++] = g[j].x1; 430 if (n < 3) 431 continue; 432 mg->right = quantile (v, n, 0.75); 433 slack = tol * (mg->right - mg->x); 434 if (slack < 0.5 * st->size) 435 slack = 0.5 * st->size; 436 near = 0; 437 for (at = 0; at < n; at++) 438 if (fabs (v[at] - mg->right) <= slack) 439 near++; 440 mg->justified = (2 * near >= n) ? 1 : 0; 441 } 442 free (v); 443 free (xs); 444 return 0; 445 } 446 447 448 /** 449 * Decide whether line @a a ends a paragraph, given that line @a b (on 450 * the same page) follows it. 451 * 452 * @param st page statistics 453 * @param a the line 454 * @param b the next line 455 * @return 1 if @a a ends a paragraph 456 */ 457 static int 458 ends_paragraph (const struct PageStats *st, 459 const struct EXTRACTOR_PageLineGeom *a, 460 const struct EXTRACTOR_PageLineGeom *b) 461 { 462 double dy = b->y - a->y; 463 double big; 464 double lim; 465 466 big = (a->size > b->size) ? a->size : b->size; 467 lim = 1.15 * big; 468 if (lim < st->spacing) 469 lim = st->spacing; 470 if ( (dy > 0) && 471 (dy > 1.3 * lim) ) 472 return 1; 473 if (size_change (a, b)) 474 return 1; 475 if (is_indented (st, a, b)) 476 return 1; 477 if (is_short (st, a)) 478 return 1; 479 return 0; 480 } 481 482 483 void 484 EXTRACTOR_pagelayout_paragraphs_ (struct EXTRACTOR_FullTextPage *pages, 485 struct EXTRACTOR_PageLineGeom *const *geoms, 486 unsigned int num_pages, 487 double width_tolerance) 488 { 489 struct PageStats *stats; 490 unsigned int p; 491 int have_last; 492 unsigned int last_p = 0; 493 unsigned int last_i = 0; 494 495 if (0 == num_pages) 496 return; 497 stats = calloc (num_pages, 498 sizeof (*stats)); 499 if (NULL == stats) 500 return; 501 have_last = 0; 502 for (p = 0; p < num_pages; p++) 503 { 504 struct EXTRACTOR_FullTextPage *page = &pages[p]; 505 const struct EXTRACTOR_PageLineGeom *g = geoms[p]; 506 unsigned int i; 507 int have_prev; 508 unsigned int prev = 0; 509 510 for (i = 0; i < page->num_lines; i++) 511 { 512 page->lines[i].par_end = 0; 513 if ( (! ok (g[i].x0)) || 514 (! ok (g[i].x1)) || 515 (! ok (g[i].y)) || 516 (! ok (g[i].size)) ) 517 page->lines[i].skip = 1; 518 } 519 if (0 != page_stats (page, 520 g, 521 width_tolerance, 522 &stats[p])) 523 continue; 524 have_prev = 0; 525 for (i = 0; i < page->num_lines; i++) 526 { 527 if (page->lines[i].skip) 528 continue; 529 if (have_prev) 530 { 531 page->lines[prev].par_end = ends_paragraph (&stats[p], 532 &g[prev], 533 &g[i]); 534 } 535 else if (have_last) 536 { 537 /* first line of this page, previous page's last line */ 538 const struct EXTRACTOR_PageLineGeom *a = &geoms[last_p][last_i]; 539 540 if ( (is_short (&stats[last_p], a)) || 541 (size_change (a, &g[i])) || 542 (is_indented (&stats[p], a, &g[i])) ) 543 pages[last_p].lines[last_i].par_end = 1; 544 } 545 prev = i; 546 have_prev = 1; 547 } 548 if (have_prev) 549 { 550 have_last = 1; 551 last_p = p; 552 last_i = prev; 553 } 554 } 555 if (have_last) 556 pages[last_p].lines[last_i].par_end = 1; 557 free (stats); 558 } 559 560 561 /** 562 * Precomposed characters for base letter + combining accent. 563 */ 564 struct Composition 565 { 566 /** 567 * Combining accent. 568 */ 569 uint16_t accent; 570 571 /** 572 * Base letters (ASCII). 573 */ 574 const char *bases; 575 576 /** 577 * Precomposed characters, same order as @e bases. 578 */ 579 uint16_t composed[26]; 580 }; 581 582 583 /** 584 * Table of precomposed characters. 585 */ 586 static const struct Composition compositions[] = { 587 { 0x0300, "AEIOUaeiou", 588 { 0xC0, 0xC8, 0xCC, 0xD2, 0xD9, 0xE0, 0xE8, 0xEC, 0xF2, 0xF9 } }, 589 { 0x0301, "AEIOUYaeiouyCcNnSsZzLlRr", 590 { 0xC1, 0xC9, 0xCD, 0xD3, 0xDA, 0xDD, 0xE1, 0xE9, 0xED, 0xF3, 0xFA, 591 0xFD, 0x106, 0x107, 0x143, 0x144, 0x15A, 0x15B, 0x179, 0x17A, 592 0x139, 0x13A, 0x154, 0x155 } }, 593 { 0x0302, "AEIOUaeiou", 594 { 0xC2, 0xCA, 0xCE, 0xD4, 0xDB, 0xE2, 0xEA, 0xEE, 0xF4, 0xFB } }, 595 { 0x0303, "ANOano", 596 { 0xC3, 0xD1, 0xD5, 0xE3, 0xF1, 0xF5 } }, 597 { 0x0304, "AaEeIiOoUu", 598 { 0x100, 0x101, 0x112, 0x113, 0x12A, 0x12B, 0x14C, 0x14D, 0x16A, 599 0x16B } }, 600 { 0x0306, "AaGgUu", 601 { 0x102, 0x103, 0x11E, 0x11F, 0x16C, 0x16D } }, 602 { 0x0307, "ZzIEeCcGg", 603 { 0x17B, 0x17C, 0x130, 0x116, 0x117, 0x10A, 0x10B, 0x120, 0x121 } }, 604 { 0x0308, "AEIOUaeiouyY", 605 { 0xC4, 0xCB, 0xCF, 0xD6, 0xDC, 0xE4, 0xEB, 0xEF, 0xF6, 0xFC, 0xFF, 606 0x178 } }, 607 { 0x030A, "AaUu", 608 { 0xC5, 0xE5, 0x16E, 0x16F } }, 609 { 0x030B, "OoUu", 610 { 0x150, 0x151, 0x170, 0x171 } }, 611 { 0x030C, "CcDdEeNnRrSsTtZz", 612 { 0x10C, 0x10D, 0x10E, 0x10F, 0x11A, 0x11B, 0x147, 0x148, 0x158, 613 0x159, 0x160, 0x161, 0x164, 0x165, 0x17D, 0x17E } }, 614 { 0x0327, "CcSsTtKkLlNnRr", 615 { 0xC7, 0xE7, 0x15E, 0x15F, 0x162, 0x163, 0x136, 0x137, 0x13B, 0x13C, 616 0x145, 0x146, 0x156, 0x157 } }, 617 { 0x0328, "AaEeIiUu", 618 { 0x104, 0x105, 0x118, 0x119, 0x12E, 0x12F, 0x172, 0x173 } }, 619 { 0x0337, "Ll", 620 { 0x141, 0x142 } }, 621 { 0, NULL, { 0 } } 622 }; 623 624 625 uint32_t 626 EXTRACTOR_pagelayout_compose_ (uint32_t base, 627 uint32_t accent) 628 { 629 unsigned int i; 630 const char *pos; 631 632 if (0x0131 == base) 633 base = 'i'; 634 if (0x0237 == base) 635 base = 'j'; 636 if ( (base >= 0x80) || 637 (! isalpha ((int) base)) ) 638 return 0; 639 for (i = 0; NULL != compositions[i].bases; i++) 640 { 641 if (compositions[i].accent != accent) 642 continue; 643 pos = strchr (compositions[i].bases, 644 (int) base); 645 if (NULL == pos) 646 return 0; 647 return compositions[i].composed[pos - compositions[i].bases]; 648 } 649 return 0; 650 } 651 652 653 /* end of pagelayout.c */