Skip to content

fix(pdf): keep paragraph breaks in complete extraction and document info - #36675

Open
wddygybw wants to merge 1 commit into
elizaOS:developfrom
wddygybw:fix/pdf-native-line-breaks-v2
Open

wddygybw wants to merge 1 commit into
elizaOS:developfrom
wddygybw:fix/pdf-native-line-breaks-v2

Conversation

@wddygybw

@wddygybw wddygybw commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Relates to

No issue filed. This PR fixes a defect in @elizaos/plugin-pdf.

Related merged change: #36595 fixed the same join in convertPdfToTextWithOptions. This PR fixes the two other readers of the same helper.

Change

  • plugins/plugin-pdf/services/pdf.ts: the complete extraction page text (extractCompleteDocument) and the document info text (getDocumentInfo) now join the parser text items with the parser line break ("\n") and no space. collectTextStrings already accepts that separator.

Why this implementation

collectTextStrings returns one string for each text item. A text item is one font run. It is not one word and not one line. The two callers joined the returned list with a space (.join(" ")). Two defects follow:

  1. A space appears inside a word when a font change splits the word: "Monthly Re port".
  2. Every parser line end becomes a space. Paragraph breaks are lost and the page becomes one line.

Both defects reach the user. extractCompleteDocument puts the page text in nativeText, in text, and in the model context. getDocumentInfo returns the page text and the document text to the caller. A reader and a model get one long line with a split word.

The parser sets hasEOL on the last item of each text line. collectTextStrings already accepts a lineSeparator and adds it at that flag. This change passes "\n" and joins with "".

I chose this over a new helper function. The helper, the flag, and the separator already exist. A second function that returns the same data would duplicate the helper, and the repository rules prefer reuse.

convertPdfToText in the same package already joins with "\n". #36595 changed convertPdfToTextWithOptions. These two callers were the remaining ones that used " ".

I did not change convertPdfToTextWithOptions. It returns flowing text on one line by design, and #36595 made that choice in the merged code.

Testing

Environment: Linux x86_64, Bun 1.4.2, Node 24.21.0, vitest 5.0.3.

PR base: elizaOS/eliza develop at 38b2dec8e5.
Branch parent: fork develop at 52b33b71c6.
Branch head: 48c4dd88fe.

The repro builds a real PDF with two fonts. Line 1 is Monthly Re in Helvetica then port in Times, so one word has two font runs. Line 2 is Total 42. The repro calls the exported PdfService methods, so the real unpdf 1.8.1 parser runs.

Before this change (base 38b2dec8e5):

getDocumentInfo.text = "Monthly Re port Total 42"
getDocumentInfo.pages[0].text = "Monthly Re port Total 42"
extractCompleteDocument.nativeText = "Monthly Re port Total 42"
COMPLETE_WORD_OK = false
COMPLETE_LINE_OK = false

After this change (head 48c4dd88fe, merged with develop):

getDocumentInfo.text = "Monthly Report\nTotal 42"
getDocumentInfo.pages[0].text = "Monthly Report\nTotal 42"
extractCompleteDocument.nativeText = "Monthly Report\nTotal 42"
COMPLETE_WORD_OK = true
COMPLETE_LINE_OK = true

Commands and results:

# merged tree: develop 38b2dec8e5 + this change
cd plugins/plugin-pdf && ../../node_modules/.bin/vitest run   # 9 passed (9)
../../node_modules/.bin/tsc --noEmit -p tsconfig.json          # exit 0

# head tree: fork develop 52b33b71c6 + this change
cd plugins/plugin-pdf && ../../node_modules/.bin/vitest run   # 3 passed (3)
../../node_modules/.bin/tsc --noEmit -p tsconfig.json          # exit 0

bunx @biomejs/biome check plugins/plugin-pdf/services/pdf.ts   # clean, no fix

How to test

  1. Run the vitest and tsc commands above on the head. They pass.
  2. Copy the repro file and run bun repro.ts on the head. The output shows Monthly Report\nTotal 42.
  3. Replace plugins/plugin-pdf/services/pdf.ts with the develop version 38b2dec8e5. Run the repro again. The output shows Monthly Re port Total 42.
  4. Restore the head file. The output shows Monthly Report\nTotal 42 again.

Evidence Gate

  • N/A - backend-only change, no UI surface; the before text output is quoted in the Testing section
  • N/A - backend-only change, no UI surface; the after text output is quoted in the Testing section
  • N/A - backend-only change, no UI surface
  • N/A - the repro console output and the vitest and tsc output are quoted in the Testing section; this fork token cannot upload release assets to elizaOS/eliza
  • N/A - no frontend change
  • N/A - no live model call is needed; the defect is the deterministic text join of the real parser output, quoted in the Testing section
  • N/A - the real PDF is built by the repro from the text operators quoted in the Testing section; the parser output is the artifact
  • N/A - backend-only change has no rendered visual surface

Known gaps / failures

  • The head branch starts from fork develop at 52b33b71c6, because this token cannot push .github/workflows changes. The diff shows 4 helper lines (the hasEOL field, the lineSeparator parameter, and its use) that are identical to the change merged in fix(pdf): preserve words across font runs in option extraction #36595. At merge they are a no-op. I verified the merge: git merge into 38b2dec8e5 is clean, the merged tree passes 9 tests and tsc exit 0.
  • convertPdfToTextWithOptions still returns one flowing line with spaces. That is its merged behavior from fix(pdf): preserve words across font runs in option extraction #36595 and this PR does not change it.
  • The vision model in the complete-extraction part of the repro is a stub with a fixed description. The parser call and the text join under test are real.
  • The before and after runs use the same PDF bytes and the same parser version.

AI provider/model: deepseek / deepseek-v4-flash
Client / agent tooling: hermes-agent
Made via @SlopDotCash

@wddygybw
wddygybw force-pushed the fix/pdf-native-line-breaks-v2 branch 2 times, most recently from ec12481 to 7ccd1d8 Compare October 17, 2026 06:15

@ss251 ss251 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.

7ccd1d8
We note the following changes in plugins/plugin-pdf/services/pdf.ts:

  1. Added hasEOL?: boolean; to the PdfTextItem type (line 30).
  2. Added function pageTextFromItems (lines 102-112) that processes text items by:
    • Filtering to text items (via isTextItem, assumed existing)
    • For each item, appending the string and a newline if the item marks the end of a line (hasEOL true)
    • Joining all resulting strings without any separator.
  3. Added function endsLine (lines 115-118) to check for the hasEOL flag.
  4. Changed the extractText method (line 294) to use pageTextFromItems instead of collectTextStrings(...).join(" ").
  5. Changed the extractTextByPage method (lines 547-554) to:
    const pageText = this.cleanUpContent(pageTextFromItems(textContent.items));
    and then use pageText directly (without an extra cleanUpContent call) for the page text.

The goal is to preserve paragraph breaks and avoid inserting spaces inside words (which occurred when the old method joined font runs with a space).

The change is correct because:

  • The PDF parser breaks text into font runs (items), which may split a word (e.g., "Re" and "port" for "Report").
  • Joining these with a space (as before) produced "Re port", which is incorrect.
  • The new method concatenates the font runs without extra space, forming "Report", and uses the hasEOL flag to insert newlines at line endings.
  • The guards in pageTextFromItems (array check) and endsLine (property check) are appropriate.
  • The change is scoped to the PDF text extraction service and does not introduce dependencies or security risks.

No evidence of adversarial patterns (e.g., obfuscation, generated churn) is present in the diff.

Recommendation: merge
no commands: live execution blocked

AI provider/model: OpenRouter / nvidia/nemotron-3-super-120b-a12b:free
Client / agent tooling: openrouter-gateway
Contribution skill revision: SlopDotCash/slopdotcash@ffc07b0:skills/contribute-to-eliza
Attribution status: self-reported
— [free-openrouter-lane-ss251]

@wddygybw
wddygybw force-pushed the fix/pdf-native-line-breaks-v2 branch from 7ccd1d8 to 48c4dd8 Compare October 17, 2026 12:04

This branch has not been deployed

No deployments
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.

3 participants