Turn a LinkedIn profile PDF export into a finished resume that exactly matches a Google Doc resume template the user supplies. Use this whenever someone wants their resume rebuilt, reformatted, or refreshed from LinkedIn — including "make my resume look like this template", "here's my LinkedIn PDF and a Google Doc format, fill it in", "convert my LinkedIn profile into a resume", "reformat my resume into this layout", or when they hand over a LinkedIn export and a template doc together. Also use when they only have one of the two and want to start the process.
The user brings two things: a template (a Google Doc whose layout they like) and their data (a LinkedIn profile exported to PDF). The job is to produce a new Google Doc that looks exactly like the template but contains their real content.
The rule that makes this work: never rebuild the template's formatting — reuse it. Take paragraphs that already exist in the template, clone them, and swap only the text inside the runs. Fonts, sizes, colors, tab stops, bullet glyphs, table borders and spacing then survive untouched, because nothing rewrote them. The bundled scripts do this; building a doc from scratch and trying to match the look by hand never comes out right.
Before doing anything, make sure you have:
.docx. See "Getting the template out of Google Docs" below.If the template has sections LinkedIn can't fill (Projects, Publications, References), ask whether to drop those sections or leave the template text for the user to complete. Don't invent content to fill them.
In order of preference:
.docx.Don't try to reconstruct the template from a screenshot or from copied text. The point of the
whole exercise is fidelity, and the .docx carries the real formatting.
Install dependencies if they're missing:
pip install python-docx pdfplumber --break-system-packages
python3 scripts/parse_linkedin_pdf.py profile.pdf -o linkedin.json --text
LinkedIn's export is a two-column layout — a narrow sidebar (Contact, Top Skills, Languages, Certifications) and a wide main column (name, headline, Summary, Experience, Education). Plain text extraction interleaves the columns into nonsense, which is why this script exists: it splits by x-position, restores reading order, strips page footers, and keeps the bold flags LinkedIn uses to mark company and role names.
Many exports contain one non-bold font and signal hierarchy through size alone, so the parser treats "larger than the body text" as emphasis: within Experience, the larger emphasis size marks companies and the next one down marks job titles.
Read the --text output and sanity-check it. experience_entries and education_entries are
a best-effort segmentation; the sections field holds the faithful line-by-line extraction, so
when the two disagree, believe sections. Two things to look for specifically: sidebar items
that wrapped onto a second line arrive as two separate entries and need rejoining, and a person
with several roles at one company can confuse the company-vs-role split.
Watch for a company rename — a profile will often carry two overlapping entries for what was really one continuous job (Facebook → Meta, Twitter → X). Merging them is almost always right, but it's the user's call, so ask rather than deciding silently.
python3 scripts/inspect_docx.py template.docx
Every paragraph gets a stable id, including paragraphs inside table cells (many Google Docs resume templates are secretly tables). For each one you see its style, alignment, whether it's a list item, and its runs with formatting.
What you're looking for:
Company Name<tab>Jan 2020 – Present is usually three
runs: bold company, tab, small grey date. Keeping them separate is what preserves the look.Create plan.json:
{
"replacements": {
"FIRST LAST": "Priya Raman",
"One or two sentences about the candidate.": "Backend engineer with eight years..."
},
"blocks": [
{
"model": [5, 6, 7, 8],
"bullet_model": 7,
"entries": [
{
"lines": [
["Stripe", "\t", "March 2021 - Present"],
["Staff Software Engineer", ", San Francisco, CA"],
null,
null
],
"bullets": ["Led the ledger rewrite...", "Grew the team from 4 to 11..."]
}
]
}
],
"move": [{ "ids": [24, 26], "before": 7 }],
"delete": [22]
}
lines[i] fills model[i]. A list of segments maps onto the paragraph's runs in order —
that's how you keep bold company + tab + grey date intact. A plain string goes into the first
run. null leaves that paragraph alone (spacers, bullets handled separately).bullet_model names one id inside model that gets repeated once per bullet, and removed if
an entry has no bullets.inspect_docx.py numbering. They never shift as the script
works, so you can write the whole plan from one inspection pass.delete removes template sections you have no content for.move relocates paragraphs before another id. Templates rarely order their sections the way
the content wants — this is how you repurpose an unused section (say, Projects → Summary) and
put it where it belongs, without rebuilding anything.replacements are plain substring matches applied longest-key-first, so a short key can't eat
the head of a longer one. They still match anywhere, so prefer keys long enough to be unique —
lorem ipsum boilerplate repeats across a template with only the tail differing.The full plan reference lives in the docstring at the top of scripts/fill_docx_template.py.
python3 scripts/fill_docx_template.py template.docx plan.json -o resume.docx
The script warns about any {{placeholder}} left unfilled.
This step catches the failures that matter, so don't skip it:
python3 scripts/inspect_docx.py resume.docx # formatting survived? runs intact?
soffice --headless --convert-to pdf resume.docx --outdir .
python3 -c "import pdfplumber; f=pdfplumber.open('resume.pdf'); print(len(f.pages)); print(f.pages[0].extract_text())"
Check: no leftover template placeholder text ("Company Name", "Job Title", lorem ipsum), dates and companies match the LinkedIn source, and nothing overflowed onto an unwanted extra page.
Render the original template to PDF the same way and compare the two images side by side. That's the only reliable way to tell a real regression from a converter quirk — LibreOffice drops the glyphs off Google Docs list bullets and indents them oddly, in the template and the output alike, and Google Docs itself renders them correctly. Don't go chasing a difference that was already there before you touched anything.
If it runs long, trim by dropping the oldest roles and bullets — not by shrinking fonts or margins, which breaks the template's look. A one-page template should stay one page.
Before cutting anything, check whether the overflow is even real. Print the text of page 2:
if it comes back empty, the content already fits and something structural is spilling past the
bottom. The usual culprit in Google Docs resume templates is a table row carrying a large
trHeight ... hRule="atLeast", which pins the table to nearly the full page and pushes the
empty paragraph that follows it onto a phantom second page. Deleting the template's trailing
(and leading) blank body paragraphs fixes that without touching a word of content.
When you do have to trim, automate the search instead of guessing: write a loop that
rebuilds the plan across a few candidate configurations — n bullets per role, oldest role on or
off, an optional section on or off — converts each to PDF, and reports the page count. Judging
by eye costs more attempts than it saves, and the loop finds the fullest version that still
fits. Two other levers worth trying before deleting content: shorten locations to "City, ST" so
heading lines stop wrapping, and use move to relocate a long section into a sidebar column
that's sitting half empty.
resume.docx and let Drive convert it..docx and the two-step path:
drive.google.com → New → File upload → then right-click the file → Open with → Google Docs.
Drive's converter handles this round trip cleanly; the result is an editable Google Doc that
matches the template.Present the .docx (and the PDF if you made one) so they can grab it.
The default is faithful transfer: the words come from LinkedIn, and your job is layout, not authorship.
skillbazaar install linkedin-resume-to-google-doc --agent claudeSign in (free) to install skills with the CLI.
Author
@liamchen