Skip to content

Add API to supply pre-computed figures/tables and areas to ignore - #1375

Draft
lfoppiano wants to merge 13 commits into
masterfrom
feature/add-ignore-areas-api
Draft

Add API to supply pre-computed figures/tables and areas to ignore #1375
lfoppiano wants to merge 13 commits into
masterfrom
feature/add-ignore-areas-api

Conversation

@lfoppiano

Copy link
Copy Markdown
Member

TBC

@lfoppiano
lfoppiano marked this pull request as draft March 3, 2026 08:04
@lfoppiano
lfoppiano force-pushed the feature/add-ignore-areas-api branch 2 times, most recently from 275a28e to 63f0189 Compare March 3, 2026 08:05
@lfoppiano
lfoppiano force-pushed the feature/add-ignore-areas-api branch from 20d703a to 647249a Compare June 14, 2026 05:41
}
}
// Check if we're switching blocks
if (tok.getBlockPtr() != token.getBlockPtr()) {
graphicVector = false;
graphicBitmap = false;

double pageHeight = block.getPage().getHeight();
Introduce a Typed Areas API allowing clients to declare rectangular PDF
regions with a type (figure, table, ignore, paratext) for specialized
processing. Layout tokens are filtered by these areas: figure/table
regions are routed to the dedicated parsers (tables grouped per area),
while ignore/paratext regions are excluded from body processing.

- New layout types TypedArea and AreaType
- Document.filterLayoutTokensByTypedAreas categorises tokens and recomputes
  block pointers; excluded tokens are dropped from body pieces
- typedAreas threaded through GrobidAnalysisConfig and the REST endpoints
  (header, header-funding, fulltext, fulltext-asset, references) via a
  parseTypedAreas JSON helper
- Documentation (Typed-Areas-API.md, Grobid-service.md, index.md)

Squashed from feature/add-ignore-areas-api and rebased onto master,
integrating with the concurrent debug-labelling feature.
Signed-off-by: Luca Foppiano <luca@foppiano.org>
@lfoppiano
lfoppiano force-pushed the feature/add-ignore-areas-api branch from 5607763 to f6f974a Compare July 21, 2026 15:40
Signed-off-by: Luca Foppiano <luca@foppiano.org>
Signed-off-by: Luca Foppiano <luca@foppiano.org>
Copilot AI review requested due to automatic review settings August 2, 2026 18:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new typedAreas API parameter to allow callers to supply pre-identified page regions (figures/tables/ignored/paratext) and have GROBID filter tokens accordingly and run specialized figure/table extraction on those regions, with corresponding documentation and unit tests.

Changes:

  • Add typedAreas support to REST endpoints and propagate it into GrobidAnalysisConfig.
  • Implement token filtering + typed-area figure/table processing in the parsing pipeline and ensure TEI serialization doesn’t drop typed-area assets.
  • Add new AreaType / TypedArea core types, update table completeness behavior, and add unit tests + docs.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
grobid-service/src/main/java/org/grobid/service/process/GrobidRestProcessFiles.java Thread typedAreas into service processing config for header/fulltext flows.
grobid-service/src/main/java/org/grobid/service/GrobidRestService.java Accept and parse typedAreas multipart field and pass parsed areas to processing.
grobid-core/src/main/java/org/grobid/core/layout/TypedArea.java New value object for page regions with geometry + type and intersection helpers.
grobid-core/src/main/java/org/grobid/core/layout/AreaType.java New enum defining supported region types.
grobid-core/src/main/java/org/grobid/core/engines/config/GrobidAnalysisConfig.java Add typed-areas field + builder hook to carry areas through the pipeline.
grobid-core/src/main/java/org/grobid/core/engines/HeaderParser.java Apply typed-area token filtering prior to header processing.
grobid-core/src/main/java/org/grobid/core/engines/FullTextParser.java Apply typed-area filtering, run typed-area figure/table extraction, and preserve/merge results through annex processing.
grobid-core/src/main/java/org/grobid/core/document/Document.java Add storage for typed areas, excluded tokens, and filtering/splitting utilities.
grobid-core/src/main/java/org/grobid/core/document/TEIFormatter.java Ensure typed-area figures/tables still serialize even when annex text is absent.
grobid-core/src/main/java/org/grobid/core/data/Table.java Mark tables coming from typed areas as TEI-complete even without head/caption.
grobid-core/src/test/java/org/grobid/core/layout/TypedAreaTest.java Unit tests for TypedArea construction, parsing, containment, equals/hash.
grobid-core/src/test/java/org/grobid/core/layout/AreaTypeTest.java Unit tests for AreaType parsing and values.
grobid-core/src/test/java/org/grobid/core/data/TableTest.java Unit tests for TEI completeness behavior when sourced from typed areas.
doc/Typed-Areas-API.md New user-facing documentation for the typed areas request format and behavior.
doc/Grobid-service.md Document typedAreas in service endpoint parameter tables + add a “Typed Areas” section.
doc/index.md Link to the new Typed Areas API documentation page.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +4032 to +4044
features.relativeDocumentPosition = featureFactory
.linearScaling(nn, fulltextLength, NBBINS_POSITION);

features.relativePagePositionChar = featureFactory
.linearScaling(mm, 0, NBBINS_POSITION);

double pageHeight = 1.0;
if (token.getPage() >= 0 && doc.getPages() != null && token.getPage() < doc.getPages().size()) {
Page page = doc.getPages().get(token.getPage());
if (page != null) {
pageHeight = page.getHeight();
}
}
Comment on lines +517 to +522
} else {
LOGGER.warn("typedAreas should be a JSON array, but received: " + typedAreasJson);
}
} catch (Exception e) {
LOGGER.error("Failed to parse typed areas JSON: " + typedAreasJson, e);
}
Comment thread doc/Grobid-service.md
| | | | `includeRawCitations` | optional | `includeRawCitations` is a boolean value, `0` (default. do not include raw reference string in the result) or `1` (include raw reference string in the result). |
| | | | `debugMode` | optional | If `1` or `true`, replaces the response with a `text/plain` dump of the raw CRF/sequence-labelling output of each model invoked during processing. See [Debug raw labelling output](#debug-raw-labelling-output). |
| | | | `models` | optional | Comma-separated list of model names to include in the debug response (e.g. `segmentation,reference-segmenter,citation`). Only meaningful when `debugMode` is enabled. The pipeline still runs in full; this only filters the response. Unknown model names yield `400`. |
| | | | `typedAreas` | optional | JSON array specifying areas with coordinates and types for specialized processing (see [Typed Areas](#typed-areas) below) |
Comment thread doc/Grobid-service.md
| `y` | number | Yes | Y-coordinate of the upper-left corner of the area |
| `width` | number | Yes | Width of the area |
| `height` | number | Yes | Height of the area |
| `type` | string | Yes | Area type: `"figure"`, `"table"`, or `"ignore"` |
Comment thread doc/Typed-Areas-API.md
Comment on lines +17 to +20
- `POST /api/processHeaderDocument` - Header extraction with typed areas
- `POST /api/processFulltextDocument` - Full document processing with typed areas
- `POST /api/processReferences` - Reference extraction with typed areas

Figures embedded as PDF Form XObjects routinely carry the text of the page they
were exported from, clipped away and invisible on screen but fully present in
the font stream. Every font-stream extractor picks it up: whole paragraphs and
section headings are emitted two or three times, and no downstream model can
tell the duplicates from the real body. It affects 60% of a 2,595-document
materials-science corpus, so it is not an edge case.

pdfalto gains -discardClippedText (dropping glyphs whose glyph box lies entirely
outside the current clip path); GROBID passes it whenever
pdf.pdfalto.discardClippedText is set, default true. The lin-64 binary is
rebuilt from pdfalto fix/discard-clipped-text (691527a).

Measured effect on plain GROBID, normalised similarity against JATS body text:

  Materials Science (2,595 docs)   0.7593 -> 0.9017
  Bioinformatics    (1,943 docs)   0.8926 -> 0.8963

The materials-science corpus is where figures are most often embedded as PDF,
and there the correction is worth +0.14 similarity on its own -- an order of
magnitude more than any modelling change we have measured on that corpus.

Only the lin-64 binary is rebuilt here; mac-64, mac_arm-64 and lin_arm-64 still
carry the unpatched 0.6.2 build and must be rebuilt on those platforms before
this can go upstream.
parseTypedAreas logged and carried on. A payload that failed to parse, or that
was not a JSON array, produced an empty area list; an entry with an unknown
type or a missing field was skipped. Either way the request returned a perfectly
ordinary 200 whose content had silently fallen back towards plain GROBID --
partly, or entirely -- with nothing in the response to say so. For batch
experiments that is the worst possible failure mode: it is invisible, and it
quietly contaminates a whole run.

Now strict. An unparseable payload, a non-array payload, or any single unusable
entry aborts with HTTP 400 and a message naming the offending indices and
reasons (first five, then a count). An absent or empty parameter is still not an
error -- it just means no masking. The success path logs accepted counts by type
as before.

Verified against a running service:
  not JSON                  -> 400 typedAreas is not valid JSON: ...
  JSON object, not array    -> 400 typedAreas must be a JSON array, received: ...
  type: "banana"            -> 400 rejected 1 of 1 entries: [0] Unknown area type: banana
  missing "type"            -> 400 rejected 1 of 1 entries: [0] missing required 'type' field
  missing "y"               -> 400 rejected 1 of 1 entries: [0] missing required 'y' field
  valid area / no parameter -> 200
…nnex

processTypedAreas parked every figure and table built from a detector region in
doc.annexFigures/annexTables, and the merge after annex processing appended them
all back into the annex lists. The annex was only ever a holding pen, but the
TEI writer takes it literally: on the materials-science corpus 94% of a masked
run's figures were serialised inside <back>, against 0.7% for plain GROBID on
the same PDFs. Wrong for every downstream consumer of the TEI, and it made the
"the serialisation is unchanged" claim untrue.

Each typed-area figure and table is now routed by where it actually sits.
A detector region carries page coordinates but no position in the token stream,
and a region that is pure artwork captures no tokens at all, so the decision is
geometric: annexStartPosition() takes the page and y of the first token of the
ANNEX segment, and anything at or beyond that point goes to the annex, anything
before it to the body. No annex, or no usable bounding box, means body -- the
body is where a figure belongs unless we can show otherwise.

One subtlety worth recording: toTEI() is handed the *local* bodyFigures /
bodyTables lists, not doc.getFigures() / doc.getTables(). Updating only the
Document dropped 151 of 174 figures from the output on a 20-document pilot while
appearing to fix the placement. The locals have to be updated too.

Pilot, 20 materials-science PDFs, grouped masks:

                              figures  captioned  in <back>
  before                          174        156  152 (87.4%)
  after                           174        156    1 ( 0.6%)
  plain GROBID (same PDFs)        169        167    1 ( 0.6%)

The figure and caption sets are identical before and after in all 20 documents,
so this relocates figures and loses none. The single remaining annex figure is
in a document with genuine back matter.

Does not deduplicate typed against native figures -- still a separate problem.
spotlessApply on the typed-area validation added in a354506 -- line wrapping
only, no semantic change and no import changes. `./gradlew build -x test`, the
command CI runs, now passes.
Splits the two failures apart, which a354506 had lumped together.

A payload that cannot be read as a list of areas at all -- not JSON, or JSON
that is not an array -- now behaves exactly as if the parameter had been
omitted: the request proceeds unmasked and returns 200, with the reason logged
at ERROR. The caller plainly did not supply a mask list, and failing the whole
request buys nothing.

A payload that *is* a list but carries an unusable entry still aborts with
HTTP 400. That is the case worth failing on: the caller did supply masks, and
applying a subset of them is never what they wanted -- it used to return an
ordinary 200 having quietly dropped boxes, which is invisible and contaminates
a batch run.

Verified against a running service, same PDF throughout:

  not JSON                  -> 200, 14 figures  (= no-parameter case)
  JSON object, not an array -> 200, 14 figures  (= no-parameter case)
  empty / omitted           -> 200, 14 figures
  array, unknown type       -> 400 rejected 1 of 1 entries: [0] Unknown area type: banana
  array, missing 'type'     -> 400 rejected 1 of 1 entries: [0] missing required 'type' field
  valid single area         -> 200, 15 figures
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants