Skip to content

PDF export: keep line breaks in the text layer's paragraph text - #1257

Open
narain1 wants to merge 1 commit into
storytold:mainfrom
narain1:fix/pdf-text-layer-line-breaks
Open

narain1 wants to merge 1 commit into
storytold:mainfrom
narain1:fix/pdf-text-layer-line-breaks

Conversation

@narain1

@narain1 narain1 commented Oct 24, 2026 •

Copy link
Copy Markdown

Problem

A paragraph with a line break (Shift+Enter) has its characters scrambled in the PDF text layer. Text after a break is read from one position too early, so search and copy give wrong text. In a table cell I saw BETA come out as ETAG.

Cause

text_layer and master_text_pieces in crates/pdf/src/lib.rs build each paragraph's string with p.runs.iter().map(|r| r.text.as_str()).collect::<String>(), then cut the laid-out run's character range out of it. A RunKind::Break run has empty text but occupies one character in the layout, because Run::content() returns "\u{b}" for it. The string is one character short per break, so every later range is shifted.

Fix

Build the string with Paragraph::text(), which is a sum of Run::content() and so counts a break as one character like the layout does. This covers both sites: the slide text layer, and the notes and handout master text.

Tests

crates/pdf/src/tests_line_breaks.rs adds two regression tests:

  • line_breaks_preserve_actual_pdf_text_and_native_character_ranges exports a slide for several paragraphs with breaks (leading, consecutive, trailing, and accented or Greek text with combining marks). It checks that the layout's character ranges index the paragraph text and that the extracted PDF text equals the source with the breaks removed.
  • line_breaks_preserve_master_pieces_and_notes_handout_pdf_text checks master_text_pieces and the exported text on notes and handout pages, where the master header text has breaks.

Both fail on main and pass with this change. cargo fmt --check is clean and the 17 tests in deckcraft-pdf pass.

Overlap with open PRs

#1192 and #1197 rewrite the same two lines for a different bug (slide-number fields, #1153): #1192 switches them to r.content() and #1197 to a precomputed shown string. Either also fixes this line-break bug, and I checked that applying only #1192's two content() hunks makes these tests pass. Neither adds a test for breaks.

I opened this so the regression tests land. If #1192 or #1197 merges first, I'll rebase this down to just the tests, or close it if you'd rather add them to that PR. Happy to go either way.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed PDF text export for paragraphs containing Shift+Enter line breaks. Characters after a line break now remain aligned with their source text.
    • Improved consistency and ordering of extracted text across PDF exports, including Notes and Handouts. Exported text no longer includes line-break characters, and slide and header text appears once.

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 81a07db and 83cc4c5.

Files selected for processing: 1
  • ROADMAP.md
Files skipped from review as they are similar to previous changes: 1
  • ROADMAP.md

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Walkthrough

PDF export now uses Paragraph::text() when extracting master text pieces and text layers. Regression tests cover line breaks, character ranges, source-model immutability, and Notes and Handouts output.

Changes: PDF text layer

Layer / File(s) Summary
Paragraph text extraction and regression coverage
crates/pdf/src/lib.rs, crates/pdf/src/tests_line_breaks.rs, ROADMAP.md
master_text_pieces and text_layer now obtain paragraph text through Paragraph::text(). Tests cover line breaks, character ranges, source-model immutability, and exported Notes and Handouts text. The roadmap records the PDF export change.

Priority: Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

Merge Risk: Minimal · up to 83cc4

The text-layer change preserves alignment across paragraph line breaks. No issue requiring a fix before merge was identified.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: preserving paragraph line breaks in the PDF text layer during export.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 2 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

The PDF text layer cut each laid-out run's characters out of a paragraph
string built from run.text. A Shift+Enter break run has empty text but
occupies one character in the layout, so every character after a break
was read from one position too early and the searchable text came out
scrambled (BETA became ETAG).

Build the string with Paragraph::text(), which counts a break as one
character like the layout does, at both sites: the slide text layer and
master_text_pieces (notes and handout masters).

Adds two regression tests, which fail without the change.
@narain1
narain1 force-pushed the fix/pdf-text-layer-line-breaks branch from 81a07db to 83cc4c5 Compare October 24, 2026 15:28
@Harrison-Booth

Copy link
Copy Markdown
Contributor

Keeping line breaks aligned in PDF text improves search and copy results for exported presentations. I checked this change against #1470: the vector PDF work is separate from this text-layer line-break fix. If you would rather not, I can open a PR that credits you, keeps your commits and shows a git range-diff against this one, so you'll still get credit and have visible work.

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

ReviewerA — source-read COMMENT (not an approval)

I read the head of the diff: PDF export keeps line breaks in the text layer's paragraph text. The two places that joined a paragraph's runs (runs.iter().map(|r| r.text).collect::<String>()) now call p.text(), so a line break inside a paragraph (Shift+Enter) counts as one character in the hidden text layer and offsets stay aligned with the drawn glyphs; the ROADMAP records it and a test module tests_line_breaks is added. A text layer whose offsets drift from the drawn text breaks search and selection in the PDF, so using the model's own text accessor is the correct fix. p.text() must give a newline or a vertical tab as the character the exporter expects, and that choice should be stated in the test. I have not run it, and no exact-head CI result exists for this head. Nothing was run.

I ran nothing and have no exact-head CI result for this PR, so this is not an approval. Where a diff was long I read its code hunks rather than every generated or documentation line.

Status: waiting_author.

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