Skip to content
All articles

Will a two-column resume break in an ATS?

The Editors · · 10 min read

Two resume pages standing side by side on a dark background, one of them under a magnifying glass, with a small figure looking up at them

Sometimes a two-column resume breaks in an ATS, and whether yours does comes down to the order a PDF text extractor writes your page out in.

Nothing in a resume parser looks at your layout and decides to reject the file for using two columns. The extractor’s only job is turning a PDF into text, and the one decision it makes about your layout is what order to write that text out in.

That order goes wrong in two different ways, and which one you hit depends on whether the library sorts text by position before writing it out, a setting the person who exported your resume never sees. Leave it off and the order follows however the PDF was painted onto the page, a property of the software that built the file, not of how the resume looks. Turn it on and two columns whose rows sit at the same height on the page get spliced into one line, mid-sentence.

A single column removes the condition that makes both failure modes possible.

Will a two-column resume get rejected by an ATS?

No. Nothing in the extraction step inspects your resume’s layout and decides to throw the file out because it uses two columns.

We read the source of Apache PDFBox looking for the step that would do it. PDFTextStripper walks the PDF’s content stream and writes out the characters it finds, in whatever order it lands on, and there is no step in it that evaluates your layout and passes or fails it. What decides the order is TextPositionComparator. Getting that order wrong doesn’t reject a file. It produces text a human reader, or a keyword search, can misread.

Does a two-column resume come out scrambled?

Sometimes. How a two-column resume comes out depends on one setting, sortByPosition.

PDFTextStripper defaults sortByPosition to false. With it off, characters come out in whatever order the PDF’s content stream painted them, which is a property of the software that generated the file, not of how the resume looks on the page. Two tools can lay out a page that looks identical and paint it in a different order, so identical-looking resumes can extract differently, and there’s no way to tell which you have without opening the file’s internals.

Turn sortByPosition on and the risk changes shape instead of disappearing. One branch of TextPositionComparator fires whenever two characters’ vertical spans overlap, or sit within 0.1 of each other, and it resolves that case with Float.compare(x1, x2), a plain left-to-right comparison across the full width of the page. Nothing in that comparison groups characters by which side of the page they started on. A sidebar bullet and a body line sitting at the same height get sorted onto one output line together, in reading order, with the gap between your columns gone. A skill from the sidebar can end up inside a job title.

Is a two-column resume safer if the columns don’t line up?

No. A two-column resume interleaves whether or not its two columns line up. The branch of TextPositionComparator that fires on a shared height is not the only branch, and the branch that catches everything else doesn’t check horizontal position at all.

The branch that splices a sidebar bullet into a body line only fires when two characters share a vertical band: an overlapping Y span, or a difference under 0.1. Everything that doesn’t share a band falls to the else branch, and that branch compares pos1YBottom against pos2YBottom and returns -1 when the first character sits higher on the page. No X coordinate enters that comparison at all. It’s strict top-to-bottom, down the full page, regardless of which column either character came from.

So a sidebar and a body column that never share a height still interleave, just differently. Instead of two lines splicing into one, the extractor alternates between them line by line as it walks down the page, because neither branch groups characters by which column they’re in. We found no branch anywhere in the comparator that clusters on horizontal position. The shared Y band decides how the mixing looks, not whether it happens.

Why can’t a resume parser just read one column at a time?

Because PDFTextStripper has no geometric column detection at all. It doesn’t measure gaps, look for whitespace gutters, or guess where one column ends and another begins. The one feature it has for separating columns is PDF article beads, and beads are optional metadata a PDF’s author has to add on purpose. The javadoc on the field that holds them says what that means in practice: “Most PDFs won’t have any beads, so charactersByArticle will contain a single entry.” A single entry is the case where no column separation happened at all, and the ordering algorithm decides your whole page on its own.

Bead separation is switched on by default (shouldSeparateByBeads = true), so the library is trying. On most PDFs it has nothing to act on. PDFBox’s actual answer to columns is a different class, PDFTextStripperByArea, and it works by refusing to guess. The caller has to name the rectangles themselves.

Whether the PDF your export tool produces carries beads at all is a separate question, and we did not answer it. It would take opening real Canva, Word and LaTeX output and checking, not reading source.

How do you check whether your two-column resume parses correctly?

You mostly can’t check a two-column resume by looking at the page. Three things decide the outcome and none of them are visible in a rendered PDF: whether the file carries beads, what order the tool that made it painted the text in, and whether your columns’ rows share a Y band.

Turning sorting on doesn’t fully settle this either, because TextPositionComparator isn’t guaranteed to produce one consistent ordering. A comment at the sort call site in PDFTextStripper, not in the comparator’s own file, notes that the comparator is not transitive. So the library calls textList.sort(comparator) inside a try block, catches the IllegalArgumentException the JDK throws when a non-transitive comparator breaks its sort, and falls back to IterativeMergeSort. An ambiguous ordering can come out differently depending on which sort actually ran.

A free checker doesn’t necessarily close this gap. We read the code of one, the open-source open-resume, and it inherits the gap. It groups text into lines from PDF.js’s hasEOL flag with no geometric reconstruction of its own, which is paint order again, so it’s exposed to the same column mechanism. And its results table has a structural blind spot. An empty field can mean “not on your resume” or “on your resume but unextractable,” and the table has no way to tell you which.

Do all applicant tracking systems handle resume columns the same way?

No, because there’s no single thing called an ATS, and the vendors who describe their own extraction don’t agree on a method. We read two of those descriptions. Greenhouse’s Talent Matching - Data Processing FAQ describes a series of fine-tuned LLM models, each trained for a specific extraction task, with the parsed fields “sent for external processing to third-party services (OpenAI).” Indeed holds a patent, US11962708B2, describing computer vision and OCR instead, with Google’s Cloud Vision API named as an example of the service invoked. A patent describes an invention, not necessarily what ships. Neither description looks anything like a PDF text extractor.

The claim that ATS platforms run Apache Tika under the hood circulates constantly, and we could not find a primary source behind it. No vendor documentation we could find ties Workday, Greenhouse or Lever to Tika, and every trail we followed led back to another resume tool’s blog rather than to the vendor.

What we can speak to is Tika and PDFBox specifically, at their defaults, because we can read their source. But “Tika and PDFBox” isn’t two independent data points here. Tika’s PDF handling, AbstractPDF2XHTML, extends PDFTextStripper directly, and its PDFParserConfig carries the identical sortByPosition default of false. That’s one library’s behaviour, surfaced through two names. Switching from one to the other on a two-column file won’t get you a different outcome.

What we tested, and what we did not

We did not test the two-column parsing behaviour described above against Workday, Greenhouse or Lever. Every vendor sandbox we checked is either paid-tier only or gated behind a partner agreement, so a live ATS wasn’t a door available to us. Everything above we read directly from the published source of PDFBox and Apache Tika, at their default settings. What we claim is scoped to exactly that: what two of the extractors resumes commonly pass through do, by default, according to their own published code, which is narrower than a claim about Workday specifically. It’s also scoped to PDF specifically. PDF versus Word is a different question, with a failure mode on each side.

Do you have to use a single-column resume?

Not strictly, but there’s no way to confirm from outside the file that a two-column resume is safe, so in practice, yes.

With one column, every character on a line sits in the same Y band as its line-mates, so the branch that fires on a shared height sorts them left to right, correctly, because there’s nothing else on that band to splice in. Separate lines don’t share a band with each other, so the else branch orders them top to bottom. That’s ordinary reading order, and the comparator produces it by construction, not by chance.

A second column reintroduces exactly the condition this whole mechanism depends on. Characters from two different places on the page might end up sharing a Y band, or might not, depending on a font size and a line height you don’t control once the file leaves your hands. HoneCV’s PDF output is single column because that’s the layout the extractor’s own sort order handles correctly.