Export: auto-bolded metric bullets extract one word per line in stream-order parsers (pypdf/Drive)
評価
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 初心者へのやさしさ
- 25/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- python, typescript
- 領域
- frontend
調査の方向性
Start with Layout.drawRunWords in src/lib/pdf/render-ats-pdf.ts (around line 1809), which issues one drawText per word chunk, and check how drawHeaderRuns and wrapRuns feed it. Reproduce by exporting a bullet with auto-bolded metrics and running pypdf's extract_text on it. Done means mixed-weight lines write real spaces, a stream-order extraction returns no single-word lines, and wrapping and page fit are unchanged. Open PR #1196 already targets this, so check it first.
索引モデルが issue の本文から書いたものです。
説明
Problem
Any bullet that contains an auto-bolded metric (autoBoldMetrics, #425) is exported so that stream-order PDF text extractors (pypdf, and Google Drive's text extraction) read it as one word per line:
•
Built
the
org
to
18
engineers
...
Layout-aware extractors (poppler pdftotext) put the line back together correctly, so the export looks fine in most viewers and passes our own round-trip. But ATS parsers in the pypdf family will see a shredded bullet, and those are exactly the bullets that carry the numbers.
Bullets with no metric are unaffected because they take the plain drawText path. Achievement headers that use the emphasis sentinels (Patent ·, Exit ·, …) are affected the same way, through drawHeaderRuns.
Root cause
Layout.drawRunWords (src/lib/pdf/render-ats-pdf.ts, ~L1809) issues one page.drawText per word chunk and advances x by the measured space width. No U+0020 is ever written into the content stream, so the gap between words exists only as positioning. Stream-order extractors treat each separate text-show as its own line.
Repro (synthetic)
- Export any résumé with a bullet like
Grew the team to 18 engineers and cut p95 latency 6x by rewriting the cache. python3 -c "import pypdf,sys; print(pypdf.PdfReader(sys.argv[1]).pages[0].extract_text())" out.pdf- That bullet prints one word per line;
pdftotext out.pdf -prints it correctly.
On a real 2-page résumé: 197 single-word lines before the fix.
Interim fix (validated locally, not committed)
Merge consecutive same-weight chunks into one drawn string with real spaces, and keep the per-run x from wrapRuns:
const segments: Array<{ str: string; bold: boolean; x: number }> = [];
let x = startX;
for (let w = 0; w < words.length; w++) {
if (w > 0) { segments[segments.length - 1].str += " "; x += space; }
for (const chunk of words[w]) {
const last = segments[segments.length - 1];
if (last && last.bold === chunk.bold) last.str += chunk.str;
else segments.push({ str: chunk.str, bold: chunk.bold, x });
x += chunk.width;
}
}
for (const seg of segments) this.page.drawText(seg.str, { x: seg.x, y: this.y - size, size, font: seg.bold ? this.fonts.bold : this.fonts.regular, color });
Liberation Sans has the same space advance in Regular and Bold (278/1000 em), so the drawn width still matches what wrapRuns measured and line breaks are unchanged.
Result on the same résumé: 197 → 28 single-word lines. The ones left are bold/regular boundaries (5.5% / to / 7.9%), where words stay whole but each weight switch still starts a new text object.
Acceptance criteria
- Mixed-weight lines write real spaces between words into the content stream
- A pypdf-based extraction of an exported bullet with metrics returns the bullet as words on one line. Ideally that includes bold boundaries, e.g. a single BT/ET block with an inline
Tfswitch, if pdf-lib allows it; otherwise document the residual. - Regression test: export a bullet with auto-bolded metrics, extract it with a stream-order extractor, and assert there are no single-word lines
- No change to measured wrapping (
wrapRuns) or page fit
- 主要言語
- TypeScript
- スター
- 11
- フォーク
- 4
- PR マージ指標
- 30日以内にマージされた PR はありません
環境構築
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
offlinecv/OfflineCV のほかの issue
-
fix(heuristics): unit number before a Saint-named city drops "St." from the location対応中かも @gaal-agent が 33 日前に担当しました。 オープンbug gaal ux:parsing
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
-
Migrate Gaal implement/review/revise to the local gaal watch; keep mechanical workflows on Actionsオープンarchitecture chore github_actions needs-human ready-for-agent
難易度 5/5 1週間以上 初心者へのやさしさ 8/100
-
bug duplicate improvement
難易度 3/5 半日 初心者へのやさしさ 66/100
-
Return legs from /download/ to /: Fix now lands on the missing field, and a bfcache miss keeps the résumé再び着手できるかも @s-annam が 34 日前に担当しましたが、オープン中のプルリクエストはありません。 オープンimprovement ready-for-agent ux:flow
-
/download/ entry replaces ExportDialog: readable full-page PDF preview beside the PDF/Markdown/report options, inline soft gate再び着手できるかも @s-annam が 34 日前に担当しましたが、オープン中のプルリクエストはありません。 オープンfeature ready-for-agent ux:edit-export ux:flow
offlinecv/OfflineCV の issue をすべて見る
似ている issue
-
bug from-app needs-triage
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
nightly-labs/openbot#7447 ·
メンテナーはふだん 1 日以内に返信
-
Team:ResponseOps technical debt
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
elastic/kibana#304569 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
security
難易度 2/5 1〜3時間 初心者へのやさしさ 64/100
IBM/ibmi-mcp-server#256 ·
メンテナーはふだん 7 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
egonSchiele/agency-lang#1444 ·
メンテナーはふだん 1 日以内に返信
-
e2e-failure ready-to-code
難易度 1/5 1時間未満 初心者へのやさしさ 84/100
redhat-developer/rhdh-plugin-export-overlays#4746 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信