Skip to main content
Glama

Measure Text Dimensions

text-measure

Calculate text dimensions for PDF layout planning. Measure width and height based on font, size, and content to ensure proper text fitting and multi-line formatting.

Instructions

Measure text width and height before rendering.

Returns exact dimensions based on font, font size, and text content. Use this to:

  • Calculate text width to set proper container sizes

  • Determine if text will fit in a given space

  • Plan multi-line layouts by specifying width constraint

Width is measured as single-line natural width. Height accounts for text wrapping when width is specified.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
itemsYesArray of text items to measure
fontNoFont specification (default: auto). Built-ins: Helvetica, Times-Roman, Courier. Use a path or URL for custom fonts.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
fontYesFont used for measurements
measurementsYes

Implementation Reference

  • Main tool handler: processes input items, sets up temporary PDF doc and fonts (handling emoji), measures width/height per item using helpers, returns structured measurements.
    async function handler(args: Input): Promise<CallToolResult> {
      const { items, font } = args;
    
      try {
        // Create a temporary PDFDocument for measurements (not saved)
        const doc = new PDFDocument({ size: 'LETTER' });
    
        // Setup fonts
        const contentText = items.map((i) => i.text).join(' ');
        const containsEmoji = hasEmoji(contentText);
        const emojiAvailable = containsEmoji ? registerEmojiFont() : false;
        const fonts = await setupFonts(doc, font);
        const { regular: regularFont, bold: boldFont } = fonts;
    
        // Measure each item
        const measurements: z.infer<typeof measurementResultSchema>[] = [];
    
        for (const item of items) {
          const fontSize = item.fontSize ?? DEFAULT_TEXT_FONT_SIZE;
          const fontName = item.bold ? boldFont : regularFont;
    
          // Measure natural width (single line)
          const width = measureTextWidth(doc, item.text, fontSize, fontName, emojiAvailable);
    
          // Measure height (accounts for wrapping if width specified)
          const height = measureTextHeight(doc, item.text, fontSize, fontName, emojiAvailable, {
            width: item.width,
            lineGap: item.lineGap,
          });
    
          measurements.push({
            text: item.text,
            width: Math.round(width * 100) / 100, // Round to 2 decimal places
            height: Math.round(height * 100) / 100,
            fontSize,
            font: fontName,
          });
        }
    
        // Close doc (not saving, just cleaning up)
        doc.end();
    
        const result: Output = {
          measurements,
          font: regularFont,
        };
    
        return {
          content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
          structuredContent: result,
        };
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        throw new McpError(ErrorCode.InternalError, `Error measuring text: ${message}`, {
          stack: error instanceof Error ? error.stack : undefined,
        });
      }
    }
  • Zod schemas defining input (text items with optional fontSize, bold, width, lineGap), output (measurements array with width/height), and intermediate schemas.
    const textItemSchema = z.object({
      text: z.string().describe('Text content to measure'),
      fontSize: z.number().optional().describe('Font size in points (default: 12)'),
      bold: z.boolean().optional().describe('Use bold font weight (default: false)'),
      width: z.number().optional().describe('Constrain width for height calculation (enables text wrapping)'),
      lineGap: z.number().optional().describe('Extra spacing between lines in points (default: 0)'),
    });
    
    const inputSchema = z.object({
      items: z.array(textItemSchema).describe('Array of text items to measure'),
      font: z.string().optional().describe('Font specification (default: auto). Built-ins: Helvetica, Times-Roman, Courier. Use a path or URL for custom fonts.'),
    });
    
    const measurementResultSchema = z.object({
      text: z.string(),
      width: z.number().describe('Natural text width in points (single line)'),
      height: z.number().describe('Text height in points (accounts for wrapping if width specified)'),
      fontSize: z.number(),
      font: z.string(),
    });
    
    const outputSchema = z.object({
      measurements: z.array(measurementResultSchema),
      font: z.string().describe('Font used for measurements'),
    });
  • ToolModule registration: defines name 'text-measure', references config (with schemas/title/desc) and handler function.
      return {
        name: 'text-measure',
        config,
        handler,
      } satisfies ToolModule;
    }
  • Helper: measures text height, accounts for wrapping if width provided, handles emoji by custom line counting, uses PDFKit metrics.
    export function measureTextHeight(doc: PDFKit.PDFDocument, text: string, fontSize: number, fontName: string, emojiAvailable: boolean, options: PDFTextOptions = {}): number {
      if (!text || text.trim() === '') {
        return 0;
      }
    
      // Save current state (using type assertion for internal PDFKit properties)
      const pdfDoc = doc as unknown as { _font?: { name: string }; _fontSize?: number };
      const savedFont = pdfDoc._font?.name;
      const savedFontSize = pdfDoc._fontSize;
    
      // Set font for measurements
      doc.fontSize(fontSize).font(fontName);
    
      // Calculate available width
      const availableWidth = options.width || doc.page.width - doc.page.margins.left - doc.page.margins.right;
      const effectiveWidth = availableWidth - (options.indent || 0);
    
      // Get actual line height from PDFKit (matches PDFKit's internal calculation)
      // currentLineHeight(true) = font's natural height with built-in gap
      // + lineGap = any extra spacing user requested
      const lineHeight = doc.currentLineHeight(true) + (options.lineGap ?? 0);
    
      let height: number;
    
      if (!emojiAvailable || !hasEmoji(text)) {
        // Use PDFKit's heightOfString directly
        height = doc.heightOfString(text, {
          width: effectiveWidth,
          lineGap: options.lineGap,
        });
      } else {
        // Complex case: manually calculate with emoji segments
        const lineCount = measureLinesWithEmoji(doc, text, fontSize, effectiveWidth);
        height = lineCount * lineHeight;
      }
    
      // Restore font state
      if (savedFont) {
        doc.font(savedFont);
      }
      if (savedFontSize) {
        doc.fontSize(savedFontSize);
      }
    
      return height;
    }
  • Helper: measures natural single-line text width, handles emoji by summing segment widths using PDFKit and canvas metrics.
    export function measureTextWidth(doc: PDFKit.PDFDocument, text: string, fontSize: number, fontName: string, emojiAvailable: boolean): number {
      if (!text || text.trim() === '') {
        return 0;
      }
    
      // Save current state
      const pdfDoc = doc as unknown as { _font?: { name: string }; _fontSize?: number };
      const savedFont = pdfDoc._font?.name;
      const savedFontSize = pdfDoc._fontSize;
    
      // Set font for measurements
      doc.fontSize(fontSize).font(fontName);
    
      let width: number;
    
      if (!emojiAvailable || !hasEmoji(text)) {
        // Use PDFKit's widthOfString directly
        width = doc.widthOfString(text);
      } else {
        // Complex case: measure text and emoji segments
        const segments = splitTextAndEmoji(text);
        width = 0;
        for (const segment of segments) {
          if (segment.type === 'emoji') {
            const emojiMetrics = measureEmoji(segment.content, fontSize);
            width += emojiMetrics.width;
          } else {
            width += doc.widthOfString(segment.content);
          }
        }
      }
    
      // Restore font state
      if (savedFont) {
        doc.font(savedFont);
      }
      if (savedFontSize) {
        doc.fontSize(savedFontSize);
      }
    
      return width;
    }
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 by explaining key behaviors: 'Returns exact dimensions', 'Width is measured as single-line natural width', and 'Height accounts for text wrapping when width is specified'. It doesn't mention performance characteristics or error conditions, but covers the core functionality adequately.

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?

The description is well-structured and front-loaded with the core purpose. Every sentence earns its place: the opening statement defines the tool, bullet points provide usage guidelines, and the final sentence clarifies measurement behavior. No wasted words.

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

Completeness5/5

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

Given the tool's moderate complexity, 100% schema description coverage, and the presence of an output schema, the description is complete enough. It explains what the tool does, when to use it, and key behavioral aspects without needing to detail return values (handled by output schema).

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 100%, so the baseline is 3. The description adds some context about 'font, font size, and text content' and mentions 'width constraint' for multi-line layouts, but doesn't provide additional parameter semantics beyond what's already documented in the schema.

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 the tool's purpose with specific verbs ('measure text width and height before rendering') and resources ('text dimensions based on font, font size, and text content'). It distinguishes itself from sibling tools by focusing on measurement rather than document/image creation or layout generation.

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?

The description provides explicit usage scenarios with bullet points: 'Calculate text width to set proper container sizes', 'Determine if text will fit in a given space', and 'Plan multi-line layouts by specifying width constraint'. These give clear guidance on when to use this tool versus alternatives.

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