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
| Name | Required | Description | Default |
|---|---|---|---|
| items | Yes | Array of text items to measure | |
| font | No | Font specification (default: auto). Built-ins: Helvetica, Times-Roman, Courier. Use a path or URL for custom fonts. |
Implementation Reference
- src/mcp/tools/text-measure.ts:65-122 (handler)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, }); } }
- src/mcp/tools/text-measure.ts:21-46 (schema)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'), });
- src/mcp/tools/text-measure.ts:124-129 (registration)ToolModule registration: defines name 'text-measure', references config (with schemas/title/desc) and handler function.return { name: 'text-measure', config, handler, } satisfies ToolModule; }
- src/lib/content-measure.ts:26-71 (helper)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; }
- src/lib/content-measure.ts:122-163 (helper)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; }