Skip to main content
Glama

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.

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;
    }

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