Skip to content

PDF → XLSX: each ruled table on its own sheet (#267) - #499

Merged
KnifeLemon merged 10 commits into
KnifeLemon:mainfrom
cre8ive-marketing:feature/267-pdf-to-excel
Oct 23, 2026
Merged

KnifeLemon merged 10 commits into
KnifeLemon:mainfrom
cre8ive-marketing:feature/267-pdf-to-excel

Conversation

@cre8ive-marketing

Copy link
Copy Markdown
Collaborator

Closes #267. This follows the plan agreed there and the points you listed.

What it does

PDF → XLSX with a new built-in converter, Pdf/PdfTablesConverter.cs. It adds no library:

  • Reading: the PDF is read the way PDF → DOCX reads it (PdfDocumentReader, ruled tables with merged cells, column widths and row heights).
  • Writing: the tables go out through the existing XlsxWriter.
  • Other formats: through XLSX, the tables reach ODS and CSV as well.
Point from #267 How
Numbers as numbers Cells that read as numbers become real values, with a number format that shows them as the PDF did:
• 1,234.50 → #,##0.00
• (120) → #,##0;(#,##0)
• $9.99 / € £ ¥ ₩ ₹ → the symbol in the format
• 12.5% → 0.0%
Year-first dates (2026-10-23, 2026/1/5, the Korean 2026. 10. 23.) become dates with a matching format.
These stay text: a leading zero (codes, phone numbers), more than 15 digits, a decimal comma (1.234,50, which can't be told from thousands), and day- or month-first dates.
Merged cells and column widths Spans become merges, and every covered cell keeps the borders. Column widths come from the detected table (points ÷ 5.25 per character), and row heights are kept. Cells also get thin borders, shading, vertical alignment, bold when all their text is bold, and wrapping.
Layout One sheet per table ("Table 1", "Table 2" …). A table that goes on at the top of the next page, right after one that ended its page, with the same columns (widths within 5 %) joins it on one sheet, and a header row repeated on the new page is left out.
Text outside tables Left out. The XLSX target in the preset editor now has a hint that says so.
Nothing found A PDF with no ruled table, or a scan (no text), fails with error.pdf_no_tables instead of writing an empty workbook.
Borderless tables Out of scope, as in PDF → DOCX.

I added no new preset, because the existing XLSX preset (to-xlsx) now works for PDFs. Putting it on the PDF donut is your call. Doing so would need a settings migration, and #429 and #489 are already queued for schema numbers.

Tests (PdfTablesTests)

  • A 4 × 3 ruled table with a paragraph above it:
    • the amounts, the accounting negative, the percentage, the dates and the currency come out as values
    • 007 stays text
    • the merged "Total" cell becomes a merge
    • the paragraph isn't on the sheet
    • column widths follow the PDF
  • A table split over two pages with its header repeated becomes one sheet without the second header. A different table on the second page becomes its own sheet.
  • A PDF without a table fails with error.pdf_no_tables.
  • Number and date parsing, including the text-stays-text cases.
  • docs/ENGINES.md: a "PDF → XLSX" section and a row in the engine table.
  • 3 new strings in all 14 language files (the XLSX hint, the error, and the engine description on the Engines page).
  • dotnet format --verify-no-changes is clean. Full dotnet test (after merging current main, OCR included): Platform 54, Core 511, CLI 27, Setup 149, Engines 1640 and App 915 pass, none fail. The App run was repeated after adding the engine description; the first run also hit the main-window test flake that Tests: run the main-window tests one at a time (fixes main CI) #498 fixes.

PdfTablesConverter reads the PDF the way PDF -> DOCX does
(PdfDocumentReader, ruled tables with merged cells, column widths and
row heights) and writes its tables with the existing XlsxWriter.

- One sheet per table; a table going on at the top of the next page with
  the same columns joins the one before it, without the repeated header.
- Numbers, percentages, currency, accounting negatives and year-first
  dates become real values in a number format that shows them as the PDF
  did; codes with a leading zero, long digit strings, decimal commas and
  day/month-first dates stay text.
- Thin borders, shading, vertical alignment, bold headers, wrapping;
  merged areas keep their borders.
- No ruled table (or a scan) fails with error.pdf_no_tables.
- The XLSX target's hint in the preset editor says text outside tables
  is left out. No new preset: the existing XLSX one works for PDFs.
PresetEditorViewModel: kept OCR's ShowOcr / ShowPageSize and its
ShowPdfCompression rule next to the XLSX tables note.
@KnifeLemon KnifeLemon added the feature New features label Oct 23, 2026

@KnifeLemon KnifeLemon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks @cre8ive-marketing, this hits the points from #267 well. I merged it with current main and ran dotnet format --verify-no-changes, the build, the PDF tables / PDF reader / XLSX engine tests (98 passed), Core (510) and the preset editor, localization and Engines page App tests (136). All 14 language files have the 3 new keys.

I also made a few PDFs in code with PdfBuilder (not committed) and read the XLSX back:

  • A Korean table with a vertical merge, 1,234,567, ₩5,000, 2026. 10. 23., 2026-01-05, 12.5% and -3% gave the right values and formats, and the merges came out as A2:A3 and A5:C5.
  • 1.234,50, 23.10.2026 and 0012 stayed text.
  • A table that goes on over a page without a repeated header became one sheet.
  • A PDF with only text aligned by spaces failed with error.pdf_no_tables.

Two things need fixing before it goes in:

1. Other formats now reach XLSX and CSV through a PDF. pdf → xlsx is an ordinary edge, so the planner chains it. With every engine available:

  • jpg → xlsx is now pdfsharp: jpg → pdf then pdf-tables. PNG, WebP, HEIC, PSD, TIFF and the other images work the same way.
  • From XLSX the route goes on to CSV, ODS, JSON, YAML, XML, ICS, VCF, GeoJSON, KML, GPX and SHP.

A picture in a PDF never has ruled tables, so these routes always fail, and the XLSX / CSV presets would show up on images. Please mark the edge DirectOnly, as PDF → PPTX does. If you want ODS and CSV from a PDF, add direct pdf → ods / pdf → csv edges that write the same workbook. A planner test (jpg → xlsx has no route; pdf → csv is one step) would keep it that way.

2. Two different tables can be joined. I made a PDF with a table at the top of page 1, then two paragraphs, then a different table with the same column widths at the top of page 2. Both ended up on one sheet. LastOnPage / FirstOnPage only look at the other tables, so "ended its page" isn't checked. Please also require that no text comes after the first table on its page, or before the second one on the next page (the empty page-break paragraph doesn't count). A test with text between the two tables would cover it.

Also: the CI build job was cancelled at the Explorer menu step (12:42), so it needs a re-run. We'll adjust the Korean strings to the app's usual tone ourselves, so leave those as they are. Putting the XLSX preset on the PDF donut is our call, as you said. We'll decide that separately and add the migration ourselves if we do.

To get this into the next release, we'll push the remaining changes to your branch ourselves today if you haven't by then. Have a look at the commits when they land.

To get this into the next release, we'll push the remaining changes to your branch ourselves today (maintainer edits are on). Have a look at the commits when they land, and tell us if anything looks off.

…feLemon#499 review)

1. pdf -> xlsx, ods and csv are DirectOnly: an image made into a PDF has
   no ruled tables, so jpg -> pdf -> xlsx (and on to CSV, ODS, JSON, ...)
   always failed and showed spreadsheet presets on pictures. The workbook
   is written with SpreadsheetConverter.Write, as the spreadsheet engine
   does (CSV: one file per sheet). Planner test: no image -> spreadsheet
   route; pdf -> xlsx / ods / csv is one step.
2. A table only goes on over the next page when nothing, table or text,
   comes after it on its page or before the next one (the empty page
   break paragraph doesn't count). Test with text between two tables.
@cre8ive-marketing

Copy link
Copy Markdown
Collaborator Author

Thanks @KnifeLemon, and for the extra PDFs. Both points are in 430e221:

  1. Routes from images. The edges are DirectOnly now, like PDF → PPTX. I added direct pdf → ods and pdf → csv edges that write the same workbook. To keep that in one place, the write switch from SpreadsheetConverter moved into SpreadsheetConverter.Write(book, step), which both engines call (CSV still gets one file per sheet). A planner test checks that jpg → xlsx and png → csv have no route, and that pdf → xlsx, ods and csv are each one step through pdf-tables. Another test writes CSV and ODS from a PDF.
  2. Joining across text. The pages are now walked as a sequence of tables and text. A paragraph only counts when it has text, so the empty page-break paragraph doesn't. A table goes on to the next page only when nothing comes after it on its page and nothing comes before the next one on the new page. A new test has a table, two paragraphs, and then a different table with the same widths at the top of page 2; they end up on two sheets.

docs/ENGINES.md covers both. The PDF tables / spreadsheet / route engine tests (172) and the Engines page / availability App tests (18) pass, and dotnet format --verify-no-changes is clean. Pushing re-runs the CI build job that was cancelled.

KnifeLemon and others added 5 commits October 24, 2026 00:27
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…even with every engine

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	src/Filee.App/Assets/i18n/ar.json
#	src/Filee.App/Assets/i18n/en.json
#	src/Filee.App/Assets/i18n/es.json
#	src/Filee.App/Assets/i18n/fr.json
#	src/Filee.App/Assets/i18n/hi.json
#	src/Filee.App/Assets/i18n/ja.json
#	src/Filee.App/Assets/i18n/ko.json
#	src/Filee.App/Assets/i18n/ne.json
#	src/Filee.App/Assets/i18n/pl.json
#	src/Filee.App/Assets/i18n/pt-BR.json
#	src/Filee.App/Assets/i18n/ru.json
#	src/Filee.App/Assets/i18n/th.json
#	src/Filee.App/Assets/i18n/zh-CN.json
#	src/Filee.App/Assets/i18n/zh-TW.json
@KnifeLemon
KnifeLemon merged commit e3c6536 into KnifeLemon:main Oct 23, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pdf 转excel

2 participants