Skip to main content
Glama

Create PDF Layout

pdf-layout

Create PDFs with precise positioning using Yoga flexbox layout. Design dashboards, slides, certificates, and flyers with exact placement control.

Instructions

Create a PDF with precise positioning using Yoga flexbox layout.

Best for: Dashboards, slides, certificates, flyers, and designs requiring exact placement.

All items are positioned absolutely on specific pages. Use the "page" property to target different pages (e.g., page: 2 for multi-slide presentations). Pages are created as needed.

Use groups for flexbox containers - they support direction, gap, justify, alignItems, and alignment properties for sophisticated layouts.

Default margins: 0 (full canvas access for precise positioning).

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
filenameNoOptional logical filename (metadata only). Storage uses UUID. Defaults to "document.pdf".
titleNoDocument title metadata
authorNoDocument author metadata
fontNoFont strategy (default: auto). Built-ins: Helvetica, Times-Roman, Courier. Use a path or URL for Unicode.
layoutNoLayout configuration for overflow handling
pageSetupNoPage configuration including size, margins, and background color.
contentYes

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • Main execution logic: processes layout input, groups content by page, computes Yoga flexbox positions, renders text/images/shapes per page, generates PDF, stores file, returns URI.
    async function handler(args: Input, extra: StorageExtra): Promise<CallToolResult> {
      const { storageContext } = extra;
      const { storageDir, baseUrl, transport } = storageContext;
      const { filename = 'document.pdf', title, author, font, layout, pageSetup, content } = args;
      const overflowBehavior = layout?.overflow ?? 'auto';
    
      try {
        // Create PDF document with shared utilities
        const contentText = JSON.stringify(content);
        const setup = await createPDFDocument(
          {
            title,
            author,
            subject: filename,
            pageSize: pageSetup?.size as PageSizePreset | [number, number] | undefined,
            margins: pageSetup?.margins ?? { top: 0, bottom: 0, left: 0, right: 0 },
            backgroundColor: pageSetup?.backgroundColor,
          },
          font,
          contentText
        );
    
        const { doc, pdfPromise, fonts, emojiAvailable, warnings } = setup;
        const { regular: regularFont, bold: boldFont } = fonts;
    
        // Validate text content against font
        validateContentText(content as ContentItem[], regularFont, boldFont, warnings);
    
        // Get page dimensions and margins
        const pageWidth = doc.page.width;
        const pageHeight = doc.page.height;
        const margins = {
          top: doc.page.margins.top,
          right: doc.page.margins.right,
          bottom: doc.page.margins.bottom,
          left: doc.page.margins.left,
        };
    
        // Height measurer for Yoga layout
        const measureHeight = (item: LayoutContent, availableWidth: number): number => {
          if (item.type === 'text' || item.type === 'heading') {
            if (!item.text) return 0;
            const fontSize = item.type === 'heading' ? ((item.fontSize as number) ?? DEFAULT_HEADING_FONT_SIZE) : ((item.fontSize as number) ?? DEFAULT_TEXT_FONT_SIZE);
            const fontName = item.type === 'heading' ? (item.bold !== false ? boldFont : regularFont) : item.bold ? boldFont : regularFont;
            let height = measureTextHeight(doc, item.text as string, fontSize, fontName, emojiAvailable, {
              width: availableWidth,
              indent: item.indent as number | undefined,
              lineGap: item.lineGap as number | undefined,
            });
            const moveDown = item.moveDown as number | undefined;
            if (moveDown !== undefined && moveDown > 0) {
              doc.fontSize(fontSize).font(fontName);
              height += moveDown * doc.currentLineHeight();
            }
            return height;
          }
          if (item.type === 'image') {
            const dimensions = resolveImageDimensions(item.imagePath as string, item.width as number | undefined, item.height as number | undefined);
            return dimensions.height;
          }
          if (item.type === 'rect') {
            return item.height as number;
          }
          if (item.type === 'circle') {
            return (item.radius as number) * 2;
          }
          if (item.type === 'line') {
            return Math.abs((item.y2 as number) - (item.y1 as number));
          }
          return 0;
        };
    
        // Render a base content item at computed position
        function renderBaseItem(item: BaseContentItem, computedX?: number, computedY?: number, computedWidth?: number) {
          switch (item.type) {
            case 'text': {
              const fontSize = item.fontSize ?? DEFAULT_TEXT_FONT_SIZE;
              const fnt = item.bold ? boldFont : regularFont;
              if (item.color) doc.fillColor(item.color);
    
              const options = extractTextOptions(item);
              if (computedX !== undefined) options.x = computedX;
              if (computedY !== undefined) options.y = computedY;
              if (computedWidth !== undefined) options.width = computedWidth;
    
              renderTextWithEmoji(doc, item.text ?? '', fontSize, fnt, emojiAvailable, options);
              if (item.color) doc.fillColor('black');
              break;
            }
            case 'heading': {
              const fontSize = item.fontSize ?? DEFAULT_HEADING_FONT_SIZE;
              const fnt = item.bold !== false ? boldFont : regularFont;
              if (item.color) doc.fillColor(item.color);
    
              const options = extractTextOptions(item);
              if (computedX !== undefined) options.x = computedX;
              if (computedY !== undefined) options.y = computedY;
              if (computedWidth !== undefined) options.width = computedWidth;
    
              renderTextWithEmoji(doc, item.text ?? '', fontSize, fnt, emojiAvailable, options);
              if (item.color) doc.fillColor('black');
              break;
            }
            case 'image': {
              const dimensions = resolveImageDimensions(item.imagePath, item.width as number | undefined, item.height as number | undefined);
              const opts = { width: dimensions.width, height: dimensions.height };
    
              const imgX = computedX ?? item.left;
              const imgY = computedY ?? item.top;
    
              if (imgX !== undefined && imgY !== undefined) {
                doc.image(item.imagePath, imgX, imgY, opts);
              } else {
                doc.image(item.imagePath, opts);
              }
              break;
            }
            case 'rect': {
              const rectX = computedX ?? item.left;
              const rectY = computedY ?? item.top;
              const rectWidth = computedWidth ?? item.width;
    
              doc.rect(rectX, rectY, rectWidth, item.height);
              if (item.fillColor && item.strokeColor) {
                if (item.lineWidth) doc.lineWidth(item.lineWidth);
                doc.fillAndStroke(item.fillColor, item.strokeColor);
              } else if (item.fillColor) {
                doc.fill(item.fillColor);
              } else if (item.strokeColor) {
                if (item.lineWidth) doc.lineWidth(item.lineWidth);
                doc.stroke(item.strokeColor);
              }
              doc.fillColor('black');
              break;
            }
            case 'circle': {
              const circleX = computedX ?? item.left;
              const circleY = computedY ?? item.top;
    
              doc.circle(circleX, circleY, item.radius);
              if (item.fillColor && item.strokeColor) {
                if (item.lineWidth) doc.lineWidth(item.lineWidth);
                doc.fillAndStroke(item.fillColor, item.strokeColor);
              } else if (item.fillColor) {
                doc.fill(item.fillColor);
              } else if (item.strokeColor) {
                if (item.lineWidth) doc.lineWidth(item.lineWidth);
                doc.stroke(item.strokeColor);
              }
              doc.fillColor('black');
              break;
            }
            case 'line': {
              if (item.lineWidth) doc.lineWidth(item.lineWidth);
              doc
                .moveTo(item.x1, item.y1)
                .lineTo(item.x2, item.y2)
                .stroke(item.strokeColor || 'black');
              break;
            }
          }
        }
    
        // Render a group with its visual properties
        function renderGroupVisuals(group: GroupItem, layoutNode: LayoutNode) {
          if (group.background) {
            doc.rect(layoutNode.x, layoutNode.y, layoutNode.width, layoutNode.height).fill(group.background);
            doc.fillColor('black');
          }
    
          if (group.border) {
            doc.lineWidth(group.border.width);
            doc.rect(layoutNode.x, layoutNode.y, layoutNode.width, layoutNode.height).stroke(group.border.color);
          }
        }
    
        // Render a layout node tree
        function renderLayoutNode(node: LayoutNode) {
          const item = node.content as ContentItem;
    
          if (item.type === 'group') {
            const group = item as GroupItem;
            renderGroupVisuals(group, node);
    
            if (node.children) {
              for (const childNode of node.children) {
                renderLayoutNode(childNode);
              }
            }
          } else {
            renderBaseItem(item as BaseContentItem, node.x, node.y, node.width);
          }
        }
    
        // Helper to get page number for an item
        function getItemPage(item: ContentItem): number {
          if ('page' in item && typeof item.page === 'number') return item.page;
          return 1;
        }
    
        // Group content by page number
        function groupContentByPage(items: ContentItem[]): Map<number, ContentItem[]> {
          const pageGroups = new Map<number, ContentItem[]>();
          for (const item of items) {
            const pageNum = getItemPage(item);
            const existing = pageGroups.get(pageNum) ?? [];
            existing.push(item);
            pageGroups.set(pageNum, existing);
          }
          return pageGroups;
        }
    
        // Group content by page
        const pageGroups = groupContentByPage(content as ContentItem[]);
        const maxPage = Math.max(...pageGroups.keys(), 1);
    
        // Width measurer for row layouts with space-between
        const measureWidth = createWidthMeasurer(doc, regularFont, boldFont, emojiAvailable);
    
        // Calculate layout and render per page
        for (let pageNum = 1; pageNum <= maxPage; pageNum++) {
          if (pageNum > 1) {
            doc.addPage();
          }
    
          // Get content for this page
          const pageContent = pageGroups.get(pageNum) ?? [];
          if (pageContent.length === 0) continue;
    
          // Convert to LayoutContent for Yoga
          const layoutContent: LayoutContent[] = pageContent.map((item) => {
            const layoutItem = item as unknown as LayoutContent;
            // Root items in pdf-layout default to absolute IF they have explicit coordinates
            // Items without coordinates (pure flex layouts) remain relative
            // Users can override with explicit position property
            if (layoutItem.position !== undefined) {
              return layoutItem; // Explicit position, use as-is
            }
            // Default: absolute if coordinates exist, relative otherwise
            const hasCoordinates = layoutItem.left !== undefined && layoutItem.top !== undefined;
            return {
              ...layoutItem,
              position: hasCoordinates ? 'absolute' : 'relative',
            };
          });
    
          // Calculate layout for THIS page only
          const layoutNodes = await calculateLayout(layoutContent, pageWidth, pageHeight, measureHeight, margins, measureWidth);
    
          // Check for content overflow when warn mode is enabled
          if (overflowBehavior === 'warn') {
            const getMaxBottom = (node: LayoutNode): number => {
              let maxBottom = node.y + node.height;
              if (node.children) {
                for (const child of node.children) {
                  maxBottom = Math.max(maxBottom, getMaxBottom(child));
                }
              }
              return maxBottom;
            };
    
            for (const node of layoutNodes) {
              const bottom = getMaxBottom(node);
              if (bottom > pageHeight) {
                warnings.push(`Page ${pageNum}: Content exceeds page height by ${Math.ceil(bottom - pageHeight)}px. Consider reducing font sizes or removing content.`);
                break;
              }
            }
          }
    
          // Render items for this page
          for (const node of layoutNodes) {
            renderLayoutNode(node);
          }
        }
    
        doc.end();
        const pdfBuffer = await pdfPromise;
    
        // Write file
        const { storedName } = await writeFile(pdfBuffer, filename, { storageDir });
    
        // Generate URI
        const fileUri = getFileUri(storedName, transport, {
          storageDir,
          ...(baseUrl && { baseUrl }),
          endpoint: '/files',
        });
    
        const result: Output = {
          operationSummary: `Created PDF layout: ${filename}`,
          itemsProcessed: 1,
          itemsChanged: 1,
          completedAt: new Date().toISOString(),
          documentId: storedName,
          filename,
          uri: fileUri,
          sizeBytes: pdfBuffer.length,
          pageCount: setup.actualPageCount,
          margins: setup.doc.page.margins as Margins,
          ...(warnings.length > 0 && { warnings }),
        };
    
        return {
          content: [{ type: 'text' as const, text: JSON.stringify(result) }],
          structuredContent: { result },
        };
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        throw new McpError(ErrorCode.InternalError, `Error creating PDF layout: ${message}`, {
          stack: error instanceof Error ? error.stack : undefined,
        });
      }
    }
  • Tool configuration including inputSchema (with layout and pageSetup), outputSchema extending pdfOutputSchema with margins.
    const config = {
      title: 'Create PDF Layout',
      description: `Create a PDF with precise positioning using Yoga flexbox layout.
    
    Best for: Dashboards, slides, certificates, flyers, and designs requiring exact placement.
    
    All items are positioned absolutely on specific pages. Use the "page" property to target different pages (e.g., page: 2 for multi-slide presentations). Pages are created as needed.
    
    Use groups for flexbox containers - they support direction, gap, justify, alignItems, and alignment properties for sophisticated layouts.
    
    Default margins: 0 (full canvas access for precise positioning).`,
      inputSchema,
      outputSchema: z.object({
        result: pdfOutputSchema.extend({
          margins: z.object({
            top: z.number(),
            bottom: z.number(),
            left: z.number(),
            right: z.number(),
          }),
        }),
      }),
    } as const;
  • Tool module registration: defines name 'pdf-layout', references config and handler.
    return {
      name: 'pdf-layout',
      config,
      handler,
    } satisfies ToolModule;
  • Exports the pdf-layout tool factory (createTool) as pdfLayout for use in MCP server.
    export { default as pdfLayout } from './pdf-layout.ts';
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden and does well: explains pages are created as needed, default margins (0 for full canvas), positioning strategies (absolute vs relative), and group functionality for flexbox containers. Could improve by mentioning output format or error handling, but covers key behavioral aspects for a creation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Perfectly structured with front-loaded purpose, usage guidelines, key features (page targeting, groups, default margins) in 5 focused sentences. Zero wasted words - every sentence provides essential information about when and how to use the tool effectively.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (7 parameters, nested objects) and no annotations, the description does well covering purpose, use cases, key behaviors, and flexbox capabilities. With an output schema present, it doesn't need to explain return values. Could mention error conditions or performance considerations, but provides solid foundation for agent understanding.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is high (86%), so baseline is 3. The description adds some value by explaining groups support 'direction, gap, justify, alignItems, and alignment properties for sophisticated layouts' and mentions 'page' property for multi-page targeting. However, it doesn't significantly enhance understanding beyond the well-documented schema parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'Create a PDF with precise positioning using Yoga flexbox layout' - specific verb (create) + resource (PDF) + method (Yoga flexbox). It distinguishes from siblings by emphasizing precise positioning and flexbox layout, unlike pdf-document (general), pdf-image (image-focused), pdf-resume (resume-specific), and text-measure (measurement only).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states 'Best for: Dashboards, slides, certificates, flyers, and designs requiring exact placement' - clear when-to-use guidance. Implicitly distinguishes from siblings by focusing on layout-intensive designs rather than general documents (pdf-document), image handling (pdf-image), or resume generation (pdf-resume).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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