Visual Layout Builder

PDF Template & UI Builder

Build multi-page PDF templates visually. Add headings, text blocks, images/logos, custom tables, divider lines, X/Y positioning, and dynamic {{placeholders}}.

100% Client-Side — Visual PDF layout & form engine
Layout & Element Controls Page 1 of 1
Add Element to Page:
Live Multi-Page PDF Preview
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript code for encoding multi-page PDF layout schemas, generating shareable URL hashes, and rendering PDF documents offline using pdf-lib.

// Standalone Multi-Page PDF Builder & Link Hash Engine (Vanilla JS)
function encodeTemplateSchema(schema) {
  const jsonStr = JSON.stringify(schema);
  return btoa(encodeURIComponent(jsonStr)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}

function decodeTemplateSchema(base64Url) {
  let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
  while (base64.length % 4 !== 0) base64 += '=';
  return JSON.parse(decodeURIComponent(atob(base64)));
}

async function renderMultiPagePdf(schema, formValues) {
  const { PDFDocument, rgb, StandardFonts } = PDFLib;
  const pdfDoc = await PDFDocument.create();
  const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
  const boldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold);

  for (const pgSchema of schema.pages) {
    const page = pdfDoc.addPage([595.28, 841.89]); // A4

    for (const el of pgSchema.elements) {
      let text = (el.content || '').replace(/\{\{(\w+)\}\}/g, (_, key) => formValues[key] || `[\${key}]`);

      if (el.type === 'heading') {
        page.drawText(text, { x: el.x || 50, y: el.y || 750, size: el.fontSize || 18, font: boldFont, color: rgb(0.9, 0.15, 0) });
      } else if (el.type === 'text') {
        const lines = text.split('\n');
        let curY = el.y || 700;
        lines.forEach(line => {
          page.drawText(line, { x: el.x || 50, y: curY, size: el.fontSize || 11, font, color: rgb(0.15, 0.15, 0.15) });
          curY -= (el.fontSize || 11) + 4;
        });
      } else if (el.type === 'line') {
        page.drawLine({ start: { x: el.x1 || 50, y: el.y1 || 700 }, end: { x: el.x2 || 545, y: el.y2 || 700 }, thickness: 1, color: rgb(0.8, 0.8, 0.8) });
      } else if (el.type === 'table') {
        let curY = el.y || 600;
        const startX = el.x || 50;
        const cols = el.columns || ["Col 1", "Col 2"];
        const colW = el.colWidths || Array(cols.length).fill(100);

        let curX = startX;
        cols.forEach((colHeader, cIdx) => {
          page.drawText(colHeader, { x: curX, y: curY, size: 10, font: boldFont, color: rgb(0,0,0) });
          curX += colW[cIdx] || 100;
        });
        curY -= 14;
        page.drawLine({ start: { x: startX, y: curY }, end: { x: startX + colW.reduce((a,b)=>a+b,0), y: curY }, thickness: 1, color: rgb(0.7,0.7,0.7) });
        curY -= 16;

        (el.rows || []).forEach(row => {
          curX = startX;
          row.forEach((cell, cIdx) => {
            let cellText = cell.replace(/\{\{(\w+)\}\}/g, (_, key) => formValues[key] || `[\${key}]`);
            page.drawText(cellText, { x: curX, y: curY, size: 10, font, color: rgb(0.2,0.2,0.2) });
            curX += colW[cIdx] || 100;
          });
          curY -= 16;
        });
      }
    }
  }

  return await pdfDoc.save();
}