Turn Resumes into Visual Profile Cards for Your Recruitment Platform

6 min read Image Generation

Resumes Are Text. Hiring Is Visual.

Recruiters don’t read resumes linearly. They scan. Name, current title, years of experience, key skills — that’s the first-pass filter. A wall of text makes this harder than it needs to be.

Recruitment platforms solve this with profile cards — compact visual summaries that show the essentials at a glance. Photo, name, title, top skills, maybe a location. The kind of thing you can line up side by side in a comparison view or drop into a shortlist email.

Building these cards usually means either a frontend component (which can’t be shared as an image) or a headless browser rendering HTML to PNG (slow, inconsistent, and a pain to maintain). Neither option scales well when you need to generate cards for hundreds of candidates at once — for a shortlist PDF, a batch email to a hiring manager, or a printable overview for an interview panel.

The Pipeline: Parse, Then Render

This is a two-step pipeline. First, extract structured data from the resume. Then, generate a visual card from that data.

The Document Extraction API handles step one. The Image Generation API handles step two. Two API calls, back to back — raw PDF in, polished profile card out.

Step 1: Extract Candidate Data from the Resume

Define a schema describing the fields you want from the resume, and send it to the Document Extraction.

import { IterationLayer } from "iterationlayer";

const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const parsedResume = await client.extract({
  files: [
    { type: "url", name: "resume.pdf", url: "https://cdn.example.com/resumes/candidate-42.pdf" },
  ],
  schema: [
    {
      name: "full_name",
      type: "TEXT",
      description: "The candidate's full name",
      is_required: true,
    },
    {
      name: "current_title",
      type: "TEXT",
      description: "The candidate's most recent job title",
      is_required: true,
    },
    {
      name: "location",
      type: "TEXT",
      description: "The candidate's city and country",
    },
    {
      name: "years_of_experience",
      type: "INTEGER",
      description: "Total years of professional experience based on work history",
    },
    {
      name: "top_skills",
      type: "ARRAY",
      description: "The candidate's top 5 most relevant technical skills",
      item_schema: {
        fields: [
          { name: "skill", type: "TEXT", description: "Skill name" },
        ],
      },
    },
    {
      name: "summary",
      type: "TEXT",
      description: "A one-sentence professional summary of the candidate",
    },
  ],
});

The parser returns structured JSON with confidence scores on every field. A typical response looks like this:

{
  "success": true,
  "data": {
    "fields": {
      "full_name": { "value": "Sarah Chen", "confidence": 0.98 },
      "current_title": { "value": "Senior Frontend Engineer", "confidence": 0.95 },
      "location": { "value": "Berlin, Germany", "confidence": 0.92 },
      "years_of_experience": { "value": 8, "confidence": 0.88 },
      "top_skills": {
        "value": [
          { "skill": "React" },
          { "skill": "TypeScript" },
          { "skill": "Node.js" },
          { "skill": "GraphQL" },
          { "skill": "AWS" }
        ],
        "confidence": 0.91
      },
      "summary": { "value": "Full-stack engineer with 8 years of experience building high-traffic web applications", "confidence": 0.85 }
    }
  }
}

No regex. No template matching per resume format. The parser handles PDFs from every applicant tracking system, every resume builder, every Word template. The schema tells it what to look for — it figures out where to find it.

Step 2: Generate the Profile Card

Now take that structured data and render it into a visual card.

const { fields } = parsedResume.data;
const skillsText = fields.top_skills.value.map((item) => item.skill).join("  |  ");

const profileCard = await client.generateImage({
  dimensions: { width: 800, height: 400 },
  output_format: "png",
  fonts: [
    {
      name: "Inter",
      weight: "Bold",
      style: "normal",
      file: { type: "url", name: "Inter-Bold.woff2", url: "https://cdn.example.com/fonts/Inter-Bold.woff2" },
    },
    {
      name: "Inter",
      weight: "Regular",
      style: "normal",
      file: { type: "url", name: "Inter-Regular.woff2", url: "https://cdn.example.com/fonts/Inter-Regular.woff2" },
    },
  ],
  layers: [
    {
      index: 0,
      type: "solid-color-background",
      hex_color: "#FFFFFF",
    },
    {
      index: 1,
      type: "rectangle",
      position: { x: 0, y: 0 },
      dimensions: { width: 800, height: 6 },
      hex_color: "#4F46E5",
    },
    {
      index: 2,
      type: "rectangle",
      position: { x: 40, y: 40 },
      dimensions: { width: 120, height: 120 },
      hex_color: "#E5E7EB",
    },
    {
      index: 3,
      type: "text",
      text: `**${fields.full_name.value}**`,
      position: { x: 190, y: 40 },
      dimensions: { width: 570, height: 44 },
      font_name: "Inter",
      font_weight: "Bold",
      font_size_in_px: 32,
      text_color: "#111827",
      text_align: "left",
      vertical_align: "top",
    },
    {
      index: 4,
      type: "text",
      text: fields.current_title.value,
      position: { x: 190, y: 88 },
      dimensions: { width: 570, height: 30 },
      font_name: "Inter",
      font_weight: "Regular",
      font_size_in_px: 18,
      text_color: "#6B7280",
      text_align: "left",
      vertical_align: "top",
    },
    {
      index: 5,
      type: "text",
      text: `${fields.location.value}  ·  ${fields.years_of_experience.value} years experience`,
      position: { x: 190, y: 122 },
      dimensions: { width: 570, height: 26 },
      font_name: "Inter",
      font_weight: "Regular",
      font_size_in_px: 14,
      text_color: "#9CA3AF",
      text_align: "left",
      vertical_align: "top",
    },
    {
      index: 6,
      type: "rectangle",
      position: { x: 40, y: 190 },
      dimensions: { width: 720, height: 1 },
      hex_color: "#E5E7EB",
    },
    {
      index: 7,
      type: "text",
      text: fields.summary.value,
      position: { x: 40, y: 210 },
      dimensions: { width: 720, height: 60 },
      font_name: "Inter",
      font_weight: "Regular",
      font_size_in_px: 14,
      text_color: "#374151",
      text_align: "left",
      vertical_align: "top",
      is_splitting_lines: true,
    },
    {
      index: 8,
      type: "text",
      text: "**Skills**",
      position: { x: 40, y: 290 },
      dimensions: { width: 720, height: 28 },
      font_name: "Inter",
      font_weight: "Bold",
      font_size_in_px: 13,
      text_color: "#6B7280",
      text_align: "left",
      vertical_align: "top",
    },
    {
      index: 9,
      type: "text",
      text: skillsText,
      position: { x: 40, y: 318 },
      dimensions: { width: 720, height: 28 },
      font_name: "Inter",
      font_weight: "Regular",
      font_size_in_px: 15,
      text_color: "#111827",
      text_align: "left",
      vertical_align: "top",
    },
  ],
});

const profileCardBuffer = Buffer.from(profileCard.data.buffer, "base64");

The result is a clean, branded profile card — name, title, location, experience, summary, and skills. All rendered as a PNG that can be embedded in emails, exported to PDF, or displayed in a dashboard.

Adding a Candidate Photo

When the candidate provides a headshot — or your platform captures one during onboarding — you can replace the placeholder circle with their actual photo:

// Replace the gray circle layer with:
{
  index: 2,
  type: "static-image",
  file: { type: "url", name: "candidate-42.jpg", url: "https://cdn.example.com/photos/candidate-42.jpg" },
  position: { x: 40, y: 40 },
  dimensions: { width: 120, height: 120 },
  should_use_smart_cropping: true,
}

The should_use_smart_cropping flag detects the face in the photo and crops around it. No manual focus coordinates needed — upload any headshot and the API frames the face correctly within the 120x120 container.

Batch Processing a Candidate Pipeline

When a hiring manager needs a visual overview of 30 shortlisted candidates, you generate all 30 cards from their parsed resumes:

const candidates = await database.getShortlistedCandidates(jobId);

const profileCards = await Promise.all(
  candidates.map(async (candidate) => {
    const skillsText = candidate.top_skills.join("  |  ");

    const result = await client.generateImage({
      dimensions: { width: 800, height: 400 },
      output_format: "png",
      fonts: [/* same font config */],
      layers: [/* same template, candidate data swapped in */],
    });

    return { candidateId: candidate.id, buffer: Buffer.from(result.data.buffer, "base64") };
  })
);

Thirty API calls in parallel. Each card renders in under a second. The hiring manager gets a visual shortlist — side by side, easy to compare, easy to share.

The Ingest-Render Pipeline

This pattern — parse a document, then render a visual from the extracted data — applies far beyond recruitment. Insurance claims to summary cards. Medical records to patient overviews. Financial reports to dashboard snapshots.

The principle is the same: unstructured input goes into the Document Extraction, structured data comes out, and the Image Generation API turns that data into something visual.

For recruitment platforms, the pipeline turns a stack of PDFs into a gallery of comparable profile cards. No manual data entry. No custom rendering infrastructure. Two API calls per candidate.

Get Started

Check the docs for the full Document Extraction schema reference and Image Generation layer types. The TypeScript and Python SDKs handle authentication and response parsing for both APIs.

Sign up for a free account — no credit card required. Parse a resume, generate a card, and see the pipeline end to end.

Start building in minutes

Free trial included. No credit card required.