Skip to main content
Glama

pdf-resume

Convert JSON Resume data into professional PDF resumes with customizable layouts, fonts, styling, and automatic formatting for job applications.

Instructions

Generate a professional resume PDF from JSON Resume format. Supports layout customization, date/locale formatting, styling, fonts, and automatic page breaks.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
filenameNoOptional logical filename (metadata only). Storage uses UUID. Defaults to "resume.pdf".
resumeYesResume data in JSON Resume format
fontNoFont for the PDF. Defaults to "auto" (system font detection). Built-ins are limited to ASCII; provide a path or URL for full Unicode.
pageSizeNoPage size preset (default: "LETTER"). Use "A4" for international standard.
backgroundColorNoPage background color (hex like "#fffff0" or named color like "ivory"). Default: white.
sectionsNoSections configuration for section ordering and field templates
layoutNoSpatial arrangement of sections. Omit for single-column (default). Use style: "two-column" with columns config for sidebar layouts.
stylingNoTypography and styling options

Implementation Reference

  • The createTool function defines the tool's handler, config, and name. The handler validates the resume JSON, processes layout/styling options, generates the PDF using resume-pdf-generator, stores the file, and returns metadata with URI.
    export default function createTool() { async function handler(args: Input, extra: StorageExtra): Promise<CallToolResult> { const { storageContext, logger } = extra; const { storageDir, baseUrl, transport } = storageContext; const { filename = 'resume.pdf', resume, font, pageSize, backgroundColor, sections, layout, styling } = args; try { // Validate resume against JSON Schema const validation = validateResume(resume); if (!validation.valid) { throw new McpError(ErrorCode.InvalidParams, `Resume validation failed: ${validation.errors?.join('; ') || 'Unknown error'}`); } // Validate layout configuration if (layout?.style === 'two-column' && layout.columns) { const leftSections = layout.columns.left?.sections || []; const rightSections = layout.columns.right?.sections || []; // Get all defined section sources const definedSources = new Set<string>(); if (sections?.sections) { for (const section of sections.sections) { if ('source' in section) { definedSources.add(section.source); } } } // If no custom sections defined, use default sources if (definedSources.size === 0) { // Default sources from DEFAULT_SECTIONS const defaultSources = ['basics', 'basics.summary', 'work', 'volunteer', 'education', 'awards', 'certificates', 'publications', 'skills', 'languages', 'interests', 'projects', 'references']; for (const s of defaultSources) { definedSources.add(s); } } // Check for sections in columns that don't exist const allColumnSections = [...leftSections, ...rightSections]; const invalidSections = allColumnSections.filter((s) => !definedSources.has(s)); if (invalidSections.length > 0) { throw new McpError(ErrorCode.InvalidParams, `Layout references unknown sections: ${invalidSections.join(', ')}. Available sections: ${[...definedSources].join(', ')}`); } // Check for duplicate sections across columns const duplicates = leftSections.filter((s) => rightSections.includes(s)); if (duplicates.length > 0) { throw new McpError(ErrorCode.InvalidParams, `Sections cannot appear in both columns: ${duplicates.join(', ')}`); } } // Resolve effective margins // Note: resume tool layout uses 'margins' inside 'styling' object, slightly different structure than pdf-document // If styling.margins is provided, use it (and require it to be complete/valid Zod schema handles structure, we handle defaults) const defaultResumeMargins = getResumeDefaultMargins(pageSize as PageSizePreset); const userMargins = styling?.margins; // If user provides ANY margin, they must provide ALL (as per schema description, though Zod makes them optional for backward compat? // No, let's strictly enforce it or merge. The improved UX goal says "Require all 4". // But Zod schema above has .optional() on properties for backward compatibility? // Wait, I didn't change the Zod definition to require them in the chunk above, I only changed description. // I should fundamentally change schema to: top: z.number(), ... (required). // Let's implement the logic: let margins: Margins; if (userMargins) { // If any is missing, fill with default? No, the goal is strictness. // But schema says optional. I'll merge with defaults but report "effective". // Actually, for better UX: "Partial updates use defaults for missing sides" is confusing. // Let's stick to "Merge with defaults" but transparency in output. margins = { top: userMargins.top ?? defaultResumeMargins.top, bottom: userMargins.bottom ?? defaultResumeMargins.bottom, left: userMargins.left ?? defaultResumeMargins.left, right: userMargins.right ?? defaultResumeMargins.right, }; } else { margins = defaultResumeMargins; } // Build render options const renderOptions: RenderOptions = { font, pageSize: pageSize as PageSizePreset | undefined, backgroundColor, margins: margins, }; // Map sections config if (sections) { renderOptions.sections = { sections: sections.sections?.map((section) => { if ('type' in section && section.type === 'divider') { return { type: 'divider' as const, thickness: section.thickness, color: section.color, }; } // TypeScript needs explicit narrowing for discriminated unions const sectionConfig = section as { source: string; render?: string; title?: string; template?: string }; return { source: sectionConfig.source, render: sectionConfig.render as 'header' | 'entry-list' | 'keyword-list' | 'language-list' | 'credential-list' | 'reference-list' | 'summary-highlights' | 'text' | undefined, title: sectionConfig.title, template: sectionConfig.template, }; }) || [], fieldTemplates: sections.fieldTemplates, }; } // Map styling to typography if (styling) { renderOptions.typography = { text: { fontSize: styling.fontSize?.body ?? 10, lineHeight: 1.2, marginTop: styling.spacing?.afterText ?? 2, marginBottom: styling.spacing?.afterText ?? 2, blockMarginBottom: styling.spacing?.betweenSections ?? 8, }, header: { marginTop: 0, marginBottom: styling.spacing?.afterName ?? 5, name: { fontSize: styling.fontSize?.name ?? 30, marginTop: 0, marginBottom: styling.spacing?.afterName ?? 5, letterSpacing: 2, }, contact: { fontSize: styling.fontSize?.contact ?? 10, letterSpacing: 0.5, }, }, sectionTitle: { fontSize: styling.fontSize?.heading ?? 12, marginTop: styling.spacing?.betweenSections ?? 12, marginBottom: styling.spacing?.afterHeading ?? 4, letterSpacing: 1.5, underlineGap: 2, underlineThickness: 0.5, }, entry: { position: { fontSize: styling.fontSize?.subheading ?? 11, marginTop: 0, marginBottom: styling.spacing?.afterSubheading ?? 1, }, company: { fontSize: 10, color: '#333333', }, location: { fontSize: 10, color: '#666666', }, date: { width: 90, }, }, bullet: { indent: 15, marginTop: 0, marginBottom: 2, }, quote: { indent: 20, }, divider: { marginTop: 10, marginBottom: 10, thickness: 0.5, color: '#cccccc', }, } as Partial<TypographyOptions>; } // Map layout config if (layout) { renderOptions.layout = { style: layout.style, gap: layout.gap, columns: layout.columns ? { left: layout.columns.left ? { width: layout.columns.left.width, sections: layout.columns.left.sections } : undefined, right: layout.columns.right ? { width: layout.columns.right.width, sections: layout.columns.right.sections } : undefined, } : undefined, }; } // Generate PDF const pdfBuffer = await generateResumePDFBuffer(resume, renderOptions, logger); // Write file with ID prefix const { storedName } = await writeFile(pdfBuffer, filename, { storageDir, }); // Generate URI based on transport type const fileUri = getFileUri(storedName, transport, { storageDir, ...(baseUrl && { baseUrl }), endpoint: '/files', }); const result: Output = { operationSummary: `Generated resume PDF: ${filename}`, itemsProcessed: 1, itemsChanged: 1, completedAt: new Date().toISOString(), documentId: storedName, filename, uri: fileUri, sizeBytes: pdfBuffer.length, margins, }; return { content: [ { type: 'text' as const, text: JSON.stringify(result), }, ], structuredContent: { result }, }; } catch (error) { if (error instanceof McpError) { throw error; } const message = error instanceof Error ? error.message : String(error); throw new McpError(ErrorCode.InternalError, `Error generating resume PDF: ${message}`, { stack: error instanceof Error ? error.stack : undefined, }); } } return { name: 'pdf-resume', config, handler, } satisfies ToolModule; }
  • Tool input and output schemas using Zod, including references to external resume-specific schemas for sections, layout, and styling.
    const inputSchema = z.object({ filename: z.string().optional().describe('Optional logical filename (metadata only). Storage uses UUID. Defaults to "resume.pdf".'), resume: resumeInputSchema, font: z.string().optional().describe('Font for the PDF. Defaults to "auto" (system font detection). Built-ins are limited to ASCII; provide a path or URL for full Unicode.'), pageSize: z.enum(['LETTER', 'A4', 'LEGAL']).optional().describe('Page size preset (default: "LETTER"). Use "A4" for international standard.'), backgroundColor: z.string().optional().describe('Page background color (hex like "#fffff0" or named color like "ivory"). Default: white.'), sections: sectionsConfigSchema, layout: resumeLayoutSchema, styling: stylingSchema, }); const outputSchema = z.object({ operationSummary: z.string(), itemsProcessed: z.number(), itemsChanged: z.number(), completedAt: z.string(), documentId: z.string(), filename: z.string(), uri: z.string(), sizeBytes: z.number(), margins: z.object({ top: z.number(), bottom: z.number(), left: z.number(), right: z.number(), }), }); const config = { title: 'Generate Resume PDF', description: 'Generate a professional resume PDF from JSON Resume format. Supports layout customization, date/locale formatting, styling, fonts, and automatic page breaks.', inputSchema, outputSchema: z.object({ result: outputSchema, }), } as const;
  • ToolModule object creation with name 'pdf-resume', linking config and handler for MCP registration.
    return { name: 'pdf-resume', config, handler, } satisfies ToolModule;
  • Reusable Zod schemas for resume layout, sections configuration, and styling used by pdf-resume input schema.
    /** * Reusable resume schemas for PDF tools. * These schemas define the structure for resume-specific configuration. */ import { z } from 'zod'; // Section configuration schema export const sectionConfigSchema = z.object({ source: z.string().describe('Path to data in resume schema using dot notation. Examples: "basics" (for header), "basics.summary", "work", "volunteer", "education", "awards", "certificates", "publications", "skills", "languages", "interests", "projects", "references", "meta.customField".'), render: z .enum(['header', 'entry-list', 'keyword-list', 'language-list', 'credential-list', 'reference-list', 'summary-highlights', 'text']) .optional() .describe('Built-in renderer type. Auto-inferred from data shape if omitted. Only needed when: (1) you want "header" rendering (never auto-inferred), or (2) you want to force a specific renderer. Not needed if using template.'), title: z.string().optional().describe('Section title (omit for no title)'), template: z.string().optional().describe('LiquidJS template for custom rendering. Use instead of render for full control. Receives source data as template context (e.g., {{ name }}, {{ keywords | join: ", " }}).'), showTenure: z.boolean().optional().describe('Show tenure duration for work/volunteer sections'), }); // Divider configuration schema export const dividerConfigSchema = z.object({ type: z.literal('divider'), thickness: z.number().optional().describe('Line thickness in points (default: 0.5)'), color: z.string().optional().describe('Line color (hex or named, default: #cccccc)'), }); // Field templates schema - LiquidJS templates for field-level formatting export const fieldTemplatesSchema = z .object({ location: z.string().optional().describe('Location display template (default: "{{ city }}{% if region %}, {{ region }}{% endif %}")'), dateRange: z.string().optional().describe("Date range template. Date format tokens: YYYY (4-digit year), YY (2-digit), MMMM (January), MMM (Jan), MM (01), M (1), DD (05), D (5). Default: \"{{ start | date: 'MMM YYYY' }} – {{ end | date: 'MMM YYYY' | default: 'Present' }}\""), degree: z.string().optional().describe('Education degree template (default: "{{ studyType }}{% if area %}, {{ area }}{% endif %}")'), credential: z.string().optional().describe('Credential metadata template (default: "{{ title | default: name }}{% if awarder %}, {{ awarder }}{% endif %}")'), language: z.string().optional().describe('Language display template (default: "{{ language }}{% if fluency %} ({{ fluency }}){% endif %}")'), skill: z.string().optional().describe('Skill template (default: "{{ name }}: {{ keywords | join: \', \' }}")'), contactLine: z.string().optional().describe('Contact line template (default: "{{ items | join: \' | \' }}")'), }) .optional() .describe('LiquidJS templates for field-level rendering. Filters: date (format dates), default (fallback value), tenure (calculate duration), join (join arrays)'); // Sections configuration schema export const sectionsConfigSchema = z .object({ sections: z .array(z.union([sectionConfigSchema, dividerConfigSchema])) .optional() .describe('Section order and configuration'), fieldTemplates: fieldTemplatesSchema, }) .optional() .describe('Sections configuration for section ordering and field templates'); // Column configuration schema for two-column layouts export const columnConfigSchema = z.object({ width: z.union([z.string(), z.number()]).optional().describe('Column width as percentage ("30%") or points (150)'), sections: z .array(z.string()) .describe( 'Section sources to place in this column. Use source paths from section config: "basics", "basics.summary", "work", "volunteer", "education", "awards", "certificates", "publications", "skills", "languages", "interests", "projects", "references", "meta.customField". Sections not assigned to a column go to the right column by default.' ), }); // Layout schema for spatial arrangement export const resumeLayoutSchema = z .object({ style: z.enum(['single-column', 'two-column']).default('single-column').describe('Layout style (default: single-column)'), columns: z .object({ left: columnConfigSchema.optional().describe('Left/sidebar column configuration'), right: columnConfigSchema.optional().describe('Right/main column configuration'), }) .optional() .describe('Column configuration for two-column layout'), gap: z.number().optional().default(30).describe('Gap between columns in points (default: 30)'), }) .optional() .describe('Spatial arrangement of sections. Omit for single-column (default). Use style: "two-column" with columns config for sidebar layouts.'); // Typography/styling schema (points-based, not moveDown) export const stylingSchema = z .object({ fontSize: z .object({ name: z.number().optional().describe('Name font size (default: 30)'), heading: z.number().optional().describe('Section heading font size (default: 12)'), subheading: z.number().optional().describe('Entry title font size (default: 11)'), body: z.number().optional().describe('Body text font size (default: 10)'), contact: z.number().optional().describe('Contact info font size (default: 10)'), }) .optional(), spacing: z .object({ afterName: z.number().optional().describe('Space after name in points'), afterHeading: z.number().optional().describe('Space after section headings in points'), afterSubheading: z.number().optional().describe('Space after entry titles in points'), afterText: z.number().optional().describe('Space after paragraphs in points'), betweenSections: z.number().optional().describe('Space between sections in points'), }) .optional(), margins: z
  • Core helper function that generates the PDF buffer from resume data and render options, implementing the layout and rendering logic.
    /** * Resume PDF generator using Yoga layout engine with emoji support */ import PDFDocument from 'pdfkit'; // Import generated type from JSON Schema import type { ResumeSchema } from '../../assets/resume.ts'; import { DEFAULT_PAGE_SIZE, type Margins, PAGE_SIZES, type PageSizePreset, RESUME_DEFAULT_MARGINS } from '../constants.ts'; import type { Logger } from '../types.ts'; import { registerEmojiFont } from './emoji-renderer.ts'; import { hasEmoji, isPDFStandardFont, needsUnicodeFont, resolveFont } from './fonts.ts'; import { isTwoColumnLayout, transformToResumeLayout } from './ir/layout-transform.ts'; import { DEFAULT_SECTIONS, transformToLayout } from './ir/transform.ts'; import type { FieldTemplates, SectionsConfig } from './ir/types.ts'; import type { TypographyOptions } from './types/typography.ts'; import { DEFAULT_TYPOGRAPHY } from './types/typography.ts'; import { calculateResumeLayout, calculateTwoColumnLayout, createRenderContext, type PageConfig, paginateLayoutWithAtomicGroups, renderPage } from './yoga-resume/index.ts'; // Re-export types for external use export type { ResumeSchema }; /** * Column configuration for two-column layouts */ export interface ColumnConfig { /** Column width as percentage ("30%") or points (150) */ width?: string | number; /** Source paths of sections for this column */ sections: string[]; } /** * Layout configuration for spatial arrangement */ export interface LayoutConfig { /** Layout style: single-column (default) or two-column */ style?: 'single-column' | 'two-column'; /** Column configuration for two-column layout */ columns?: { left?: ColumnConfig; right?: ColumnConfig; }; /** Gap between columns in points (default: 30) */ gap?: number; } /** * Render options for resume PDF generation */ export interface RenderOptions { /** Custom typography settings */ typography?: Partial<TypographyOptions>; /** Sections configuration (section order, titles, etc.) */ sections?: SectionsConfig; /** Field templates for customizing field-level rendering (dates, locations, etc.) */ fieldTemplates?: FieldTemplates; /** Font specification (path, URL, 'auto', or standard font name) */ font?: string; /** Layout configuration for spatial arrangement (single-column or two-column) */ layout?: LayoutConfig; /** Page size preset (default: LETTER) */ pageSize?: PageSizePreset; /** Page background color (hex like "#fffff0" or named color). Default: white. */ backgroundColor?: string; /** Explicit margins (if provided, overrides default resume margins) */ margins?: Margins; } /** * Setup fonts for the PDF document * Returns a FontConfig compatible with TypographyOptions (includes italic variants) * Falls back to Helvetica if font resolution or registration fails */ async function setupFonts(doc: InstanceType<typeof PDFDocument>, fontSpec: string | undefined): Promise<{ regular: string; bold: string; italic: string; boldItalic: string }> { const spec = fontSpec || 'auto'; const resolvedFont = await resolveFont(spec); // Fall back to Helvetica if resolution failed if (!resolvedFont) { return { regular: 'Helvetica', bold: 'Helvetica-Bold', italic: 'Helvetica-Oblique', boldItalic: 'Helvetica-BoldOblique', }; } // If it's a standard PDF font, use its variants if (isPDFStandardFont(resolvedFont)) { if (resolvedFont.startsWith('Helvetica')) { return { regular: 'Helvetica', bold: 'Helvetica-Bold', italic: 'Helvetica-Oblique', boldItalic: 'Helvetica-BoldOblique', }; } if (resolvedFont.startsWith('Times')) { return { regular: 'Times-Roman', bold: 'Times-Bold', italic: 'Times-Italic', boldItalic: 'Times-BoldItalic', }; } if (resolvedFont.startsWith('Courier')) { return { regular: 'Courier', bold: 'Courier-Bold', italic: 'Courier-Oblique', boldItalic: 'Courier-BoldOblique', }; } // For Symbol or ZapfDingbats, use as-is return { regular: resolvedFont, bold: resolvedFont, italic: resolvedFont, boldItalic: resolvedFont, }; } // It's a custom font file - register it with PDFKit try { doc.registerFont('CustomFont', resolvedFont); return { regular: 'CustomFont', bold: 'CustomFont', italic: 'CustomFont', boldItalic: 'CustomFont', }; } catch { // Fall back to Helvetica on registration failure return { regular: 'Helvetica', bold: 'Helvetica-Bold', italic: 'Helvetica-Oblique', boldItalic: 'Helvetica-BoldOblique', }; } } /** * Merge partial typography options with defaults */ function mergeTypography(defaults: TypographyOptions, overrides?: Partial<TypographyOptions>, fonts?: { regular: string; bold: string; italic: string; boldItalic: string }): TypographyOptions { if (!overrides && !fonts) { return defaults; } const merged = { ...defaults }; // Apply font overrides if (fonts) { merged.fonts = fonts; } // Apply typography overrides if (overrides) { if (overrides.fonts) merged.fonts = { ...merged.fonts, ...overrides.fonts }; if (overrides.header) merged.header = { ...merged.header, ...overrides.header }; if (overrides.sectionTitle) merged.sectionTitle = { ...merged.sectionTitle, ...overrides.sectionTitle }; if (overrides.entry) merged.entry = { ...merged.entry, ...overrides.entry }; if (overrides.text) merged.text = { ...merged.text, ...overrides.text }; if (overrides.bullet) merged.bullet = { ...merged.bullet, ...overrides.bullet }; if (overrides.quote) merged.quote = { ...merged.quote, ...overrides.quote }; if (overrides.divider) merged.divider = { ...merged.divider, ...overrides.divider }; } return merged; } /** * Renders a resume to PDF buffer using the transform → render pipeline */ export async function generateResumePDFBuffer(resume: ResumeSchema, options: RenderOptions, logger: Logger): Promise<Buffer> { // Check if content has Unicode characters or emoji const resumeText = JSON.stringify(resume); const containsUnicode = needsUnicodeFont(resumeText); const containsEmoji = hasEmoji(resumeText); const isDefaultFont = !options.font || options.font === 'auto'; // Register emoji font for rendering const emojiAvailable = containsEmoji ? registerEmojiFont() : false; // Warn about emoji if font not available if (containsEmoji && !emojiAvailable) { logger.warn('⚠️ EMOJI DETECTED but emoji font not available.\n' + ' Run: npm install (to download Noto Color Emoji)\n' + ' Emojis will be skipped in the PDF.'); } else if (containsEmoji && emojiAvailable) { logger.info('✅ Emoji support enabled - rendering emojis as inline images'); } // Warn if Unicode detected with default font if (containsUnicode && isDefaultFont && !containsEmoji) { logger.warn("⚠️ Unicode characters detected. If they don't render properly, " + 'provide a Unicode font URL. Find fonts at https://fontsource.org'); } // Build sections config with field templates const baseSections = options.sections ?? DEFAULT_SECTIONS; const sectionsConfig: SectionsConfig = { ...baseSections, fieldTemplates: options.fieldTemplates ? { ...baseSections.fieldTemplates, ...options.fieldTemplates } : baseSections.fieldTemplates, }; // Transform resume to IR const layoutDoc = transformToLayout(resume, sectionsConfig); // Apply layout transform for two-column support const resumeLayout = transformToResumeLayout(layoutDoc.elements, sectionsConfig.sections ?? [], options.layout); // Resolve page size const pageSize = options.pageSize ? PAGE_SIZES[options.pageSize] : DEFAULT_PAGE_SIZE; // Build ATS-friendly metadata from IR const { name, label, keywords } = layoutDoc.metadata; const skillKeywords = keywords?.join(', ') || ''; const doc = new PDFDocument({ size: [pageSize.width, pageSize.height], margins: options.margins ?? RESUME_DEFAULT_MARGINS, autoFirstPage: false, // Create pages explicitly for consistent background handling // ATS & Accessibility improvements pdfVersion: '1.5', tagged: true, lang: 'en-US', displayTitle: true, info: { Title: `${name || 'Resume'} - Resume`, Author: name || 'Unknown', Subject: label || '', Keywords: `resume, CV${skillKeywords ? `, ${skillKeywords}` : ''}`, }, }); // Apply background color to ALL pages consistently via pageAdded event doc.on('pageAdded', () => { if (options.backgroundColor) { doc.rect(0, 0, pageSize.width, pageSize.height).fill(options.backgroundColor); doc.fillColor('black'); } }); // Add first page explicitly - triggers same event handler as all other pages doc.addPage(); // Setup fonts const fonts = await setupFonts(doc, options.font); // Merge typography with defaults and font overrides const typography = mergeTypography(DEFAULT_TYPOGRAPHY, options.typography, fonts); // Page configuration const pageConfig: PageConfig = { width: pageSize.width, height: pageSize.height, margins: options.margins ?? RESUME_DEFAULT_MARGINS, }; // Create render context const renderCtx = createRenderContext(doc, typography, layoutDoc.fieldTemplates, emojiAvailable); // Return a promise that resolves when the PDF is complete return new Promise<Buffer>((resolve, reject) => { const chunks: Buffer[] = []; doc.on('data', (chunk: Buffer) => chunks.push(chunk)); doc.on('end', () => resolve(Buffer.concat(chunks))); doc.on('error', reject); // Render based on layout type using Yoga layout engine (async () => { if (isTwoColumnLayout(resumeLayout)) { // Two-column layout: calculate and render columns const twoColumnResult = await calculateTwoColumnLayout( doc, { gap: resumeLayout.gap, left: { width: resumeLayout.left.width, elements: resumeLayout.left.elements, }, right: { width: resumeLayout.right.width, elements: resumeLayout.right.elements, }, }, typography, layoutDoc.fieldTemplates, emojiAvailable, pageConfig ); // Paginate both columns const leftPages = paginateLayoutWithAtomicGroups(twoColumnResult.left, pageConfig); const rightPages = paginateLayoutWithAtomicGroups(twoColumnResult.right, pageConfig); const maxPages = Math.max(leftPages.length, rightPages.length); // Render pages for (let pageNum = 0; pageNum < maxPages; pageNum++) { if (pageNum > 0) { doc.addPage(); } // Render left column nodes for this page const leftPage = leftPages[pageNum]; if (leftPage) { renderPage(renderCtx, leftPage); } // Render right column nodes for this page const rightPage = rightPages[pageNum]; if (rightPage) { renderPage(renderCtx, rightPage); } } } else { // Single-column layout const layoutNodes = await calculateResumeLayout(doc, resumeLayout.elements, typography, layoutDoc.fieldTemplates, emojiAvailable, pageConfig); // Paginate the layout const pages = paginateLayoutWithAtomicGroups(layoutNodes, pageConfig); // Render all pages for (const page of pages) { if (page.number > 0) { doc.addPage(); } renderPage(renderCtx, page); } } doc.end(); })().catch(reject); }); } export { DEFAULT_SECTIONS } from './ir/transform.ts'; export type { FieldTemplates, SectionsConfig } from './ir/types.ts'; export type { TypographyOptions } from './types/typography.ts'; export { DEFAULT_TYPOGRAPHY } from './types/typography.ts';

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mcp-z/mcp-pdf'

If you have feedback or need assistance with the MCP directory API, please join our Discord server