Here’s everything you asked for, in one go:

Font embedding for Inter + Manrope (via jsPDF.addFileToVFS() / addFont()).

Composed SVG (normal + reverse).

High-res PNG/JPG variants (logo, reverse, text-only, icon, reverse icon).

Updated downloadBrandKit that builds the ZIP and drops in the Brand Kit PDF.

A PDF button wired to the same PDF engine (so clients can download the PDF directly too).

1) Add the font embed helper

Create src/services/export/embedFonts.ts:

// src/services/export/embedFonts.ts
import jsPDF from "jspdf";

/**
 * Register base64-encoded TTFs with jsPDF.
 * Add as many families/weights as you need.
 */
export function registerEmbeddedFonts(doc: jsPDF) {
  // ---- Inter Regular (400) ----
  // Replace the string with your real base64 (no data: prefix).
  // See the small Node script below to generate it.
  const INTER_REGULAR_BASE64 = "<<<BASE64_TTF_INTER_REGULAR>>>";

  // ---- Inter Bold (700) ----
  const INTER_BOLD_BASE64 = "<<<BASE64_TTF_INTER_BOLD>>>";

  // ---- Manrope Regular (400) ----
  const MANROPE_REGULAR_BASE64 = "<<<BASE64_TTF_MANROPE_REGULAR>>>";

  // ---- Manrope Bold (700) ----
  const MANROPE_BOLD_BASE64 = "<<<BASE64_TTF_MANROPE_BOLD>>>";

  // Register Inter
  if (INTER_REGULAR_BASE64 !== "<<<BASE64_TTF_INTER_REGULAR>>>") {
    doc.addFileToVFS("Inter-Regular.ttf", INTER_REGULAR_BASE64);
    doc.addFont("Inter-Regular.ttf", "Inter", "normal");
  }
  if (INTER_BOLD_BASE64 !== "<<<BASE64_TTF_INTER_BOLD>>>") {
    doc.addFileToVFS("Inter-Bold.ttf", INTER_BOLD_BASE64);
    doc.addFont("Inter-Bold.ttf", "Inter", "bold");
  }

  // Register Manrope
  if (MANROPE_REGULAR_BASE64 !== "<<<BASE64_TTF_MANROPE_REGULAR>>>") {
    doc.addFileToVFS("Manrope-Regular.ttf", MANROPE_REGULAR_BASE64);
    doc.addFont("Manrope-Regular.ttf", "Manrope", "normal");
  }
  if (MANROPE_BOLD_BASE64 !== "<<<BASE64_TTF_MANROPE_BOLD>>>") {
    doc.addFileToVFS("Manrope-Bold.ttf", MANROPE_BOLD_BASE64);
    doc.addFont("Manrope-Bold.ttf", "Manrope", "bold");
  }
}

/**
 * Pick a PDF-safe font name from your UI fontFamily string.
 * Falls back to Helvetica if the custom family wasn’t embedded.
 */
export function pdfFontFor(uiFontFamily: string): { name: string; weight: "normal" | "bold" } {
  const first = uiFontFamily.replace(/["']/g, "").split(",")[0].trim();
  const fam = /inter/i.test(first) ? "Inter"
           : /manrope/i.test(first) ? "Manrope"
           : "helvetica";
  return { name: fam, weight: "bold" };
}


Tiny Node script to get the base64 for each TTF (run once per font file):

# scripts/ttf2base64.mjs
import { readFileSync } from "node:fs";
const file = process.argv[2];
const b64 = readFileSync(file).toString("base64");
console.log(b64);


Usage:

node scripts/ttf2base64.mjs ./public/fonts/Inter-Regular.ttf > inter_regular.b64.txt


Paste the output strings into the placeholders in embedFonts.ts.

2) Add the composed SVG util

Create src/services/export/composeSvg.ts:

export type ComposeSvgOpts = {
  name: string;
  fontFamily: string;
  textSize: number;
  textColor: string;
  layout: "side-by-side" | "top-bottom";
  iconSize: number;
  pad?: number;
  iconSvgText: string;
  background?: string | null;
  textColorOverride?: string;
};

function parseViewBox(svg: string) {
  const m = svg.match(/viewBox="([\d.\-]+)\s+([\d.\-]+)\s+([\d.\-]+)\s+([\d.\-]+)"/i);
  if (!m) return { x: 0, y: 0, w: 1000, h: 1000 };
  return { x: +m[1], y: +m[2], w: +m[3], h: +m[4] };
}
function extractInner(svg: string) {
  const s = svg.indexOf(">") + 1, e = svg.lastIndexOf("</svg>");
  return s > 0 && e > s ? svg.slice(s, e) : svg;
}
function measure(text: string, fontFamily: string, px: number) {
  const c = document.createElement("canvas");
  const ctx = c.getContext("2d")!;
  ctx.font = `bold ${px}px ${fontFamily}, system-ui, sans-serif`;
  return ctx.measureText(text).width;
}
function esc(x: string) { return x.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&apos;'} as any)[c]); }

export function composeLogoSvg(opts: ComposeSvgOpts) {
  const { name, fontFamily, textSize, textColor, layout, iconSize, pad=48, iconSvgText, background=null, textColorOverride } = opts;
  const vb = parseViewBox(iconSvgText);
  const inner = extractInner(iconSvgText);
  const sx = iconSize / vb.w, sy = iconSize / vb.h;
  const tw = measure(name, fontFamily, textSize);
  const th = textSize;

  let w: number, h: number;
  if (layout === "side-by-side") { w = iconSize + pad + tw; h = Math.max(iconSize, th * 1.2); }
  else { w = Math.max(iconSize, tw); h = iconSize + pad + th * 1.2; }

  const bg = background ? `<rect x="0" y="0" width="${w}" height="${h}" fill="${background}"/>` : "";
  const icon =
    `<g transform="translate(${layout==='side-by-side'?0:(w-iconSize)/2},${layout==='side-by-side'?(h-iconSize)/2:0}) scale(${sx},${sy}) translate(${-vb.x},${-vb.y})">${inner}</g>`;
  const fill = textColorOverride ?? textColor;
  const text = layout === "side-by-side"
    ? `<text x="${iconSize+pad}" y="${(h+th*0.85)/2}" font-family="${fontFamily}, system-ui, sans-serif" font-weight="700" font-size="${textSize}" fill="${fill}">${esc(name)}</text>`
    : `<text x="${w/2}" y="${iconSize+pad+th*0.85}" text-anchor="middle" font-family="${fontFamily}, system-ui, sans-serif" font-weight="700" font-size="${textSize}" fill="${fill}">${esc(name)}</text>`;

  return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
  ${bg}
  ${icon}
  ${text}
</svg>`;
}

3) Add the raster logo export helpers

Create src/services/export/logoRaster.ts:

export type RenderOpts = {
  width: number;
  bg?: string | null;
  withIcon: boolean;
  withText: boolean;
  layout: "side-by-side" | "top-bottom";
  iconSize?: number;
  textSize?: number;
  textColor: string;
  fontFamily: string;
  name: string;
  svgUrl?: string | null;
  pad?: number;
};

export async function renderLogoPNG(opts: RenderOpts): Promise<Blob> {
  const { width, bg=null, withIcon, withText, layout, iconSize=300, textSize=160, textColor, fontFamily, name, svgUrl, pad=64 } = opts;
  const height = Math.round(width * (layout === "side-by-side" ? 0.5 : 0.75));
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d")!;
  canvas.width = width; canvas.height = height;
  if (bg) { ctx.fillStyle = bg; ctx.fillRect(0,0,width,height); } else { ctx.clearRect(0,0,width,height); }
  const cx = width / 2, cy = height / 2;

  let iconDrawn = false;
  if (withIcon && svgUrl) {
    const img = new Image(); img.crossOrigin = "anonymous";
    await new Promise<void>(res => { img.onload=()=>res(); img.onerror=()=>res(); img.src = svgUrl; });
    if (layout === "side-by-side") {
      const total = iconSize + (withText ? pad : 0);
      const x = withText ? cx - total/2 : cx - iconSize/2;
      ctx.drawImage(img, x, cy - iconSize/2, iconSize, iconSize);
    } else {
      ctx.drawImage(img, cx - iconSize/2, cy - (withText ? (iconSize/2 + pad/2) : iconSize/2), iconSize, iconSize);
    }
    iconDrawn = true;
  }

  if (withText) {
    ctx.fillStyle = textColor;
    ctx.font = `bold ${textSize}px ${fontFamily}`;
    ctx.textBaseline = "alphabetic";
    ctx.textAlign = layout === "side-by-side" ? "left" : "center";
    const m = ctx.measureText(name);
    if (layout === "side-by-side") {
      const x = iconDrawn ? cx + (pad/2) : cx - m.width/2;
      const y = cy + (iconDrawn ? iconSize/2 : textSize/2);
      ctx.fillText(name, x, y);
    } else {
      ctx.fillText(name, cx, cy + iconSize/2 + textSize/2);
    }
  }
  return await new Promise<Blob>(r => canvas.toBlob(b => r(b!), "image/png"));
}

export async function renderLogoJPG(opts: Omit<RenderOpts,"bg"> & { bg: string; quality?: number }): Promise<Blob> {
  const blobPng = await renderLogoPNG({ ...opts, bg: opts.bg });
  const url = URL.createObjectURL(blobPng);
  const img = new Image();
  await new Promise<void>(res => { img.onload=()=>res(); img.onerror=()=>res(); img.src=url; });
  const canvas = document.createElement("canvas");
  canvas.width = img.width; canvas.height = img.height;
  const ctx = canvas.getContext("2d")!; ctx.drawImage(img,0,0);
  URL.revokeObjectURL(url);
  return await new Promise<Blob>(r => canvas.toBlob(b => r(b!), "image/jpeg", opts.quality ?? 0.92));
}

4) Update your page: imports + single updated downloadBrandKit + PDF button

Open src/pages/BrandKit/BrandKitPage.tsx and add imports at the top:

import jsPDF from "jspdf";
import { registerEmbeddedFonts, pdfFontFor } from "@/services/export/embedFonts";
import { composeLogoSvg } from "@/services/export/composeSvg";
import { renderLogoPNG, renderLogoJPG } from "@/services/export/logoRaster";


Add this helper near the top-level component:

async function fetchIconSvgText(svgUrl: string | null): Promise<string | null> {
  if (!svgUrl) return null;
  try {
    const txt = await (await fetch(svgUrl)).text();
    return txt.includes("<svg") ? txt : null;
  } catch { return null; }
}

Replace your existing downloadBrandKit with this unified version
const downloadBrandKit = async () => {
  if (!name.trim()) { alert("Please enter a brand name first"); return; }
  setIsDownloadingKit(true);

  try {
    const zip = new JSZip();
    const brandFolderName = name.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase();

    // --- 1) brand-info.json
    const brandInfo = {
      brandName: name,
      createdAt: new Date().toISOString(),
      industry,
      styles: selectedStyles,
      font: { family: fontFamily, textSize, textColor: resolveTextColor() },
      palette: { ...palette },
      settings: { preset, layout, iconSize },
      generator: "IBrandBiz Brand Kit",
    };
    zip.file(`${brandFolderName}/brand-info.json`, JSON.stringify(brandInfo, null, 2));

    // --- 2) color-palette.txt
    const colorTxt = [
      `Primary:   ${palette.primary}`,
      `Secondary: ${palette.secondary}`,
      `Accent:    ${palette.accent}`,
      `Highlight: ${palette.highlight}`,
      `Neutral:   ${palette.neutral}`,
      `Surface:   ${palette.surface}`,
      `TextLight: ${palette.textLight}`,
      `TextDark:  ${palette.textDark}`,
    ].join("\n");
    zip.file(`${brandFolderName}/color-palette.txt`, colorTxt);

    // --- 3) Logo assets (PNG/JPG variants + raw icon SVG)
    const logosFolder = zip.folder(`${brandFolderName}/logos`)!;

    // raw icon SVG (if any)
    let iconSvgText: string | null = null;
    if (generatedSvgUrl) {
      try {
        iconSvgText = await fetchIconSvgText(generatedSvgUrl);
        if (iconSvgText) logosFolder.file(`icon.svg`, iconSvgText);
      } catch {}
    }

    // Composed vector SVGs (normal + reverse)
    if (iconSvgText) {
      const composed = composeLogoSvg({
        name,
        fontFamily,
        textSize,
        textColor: palette.textDark,
        layout,
        iconSize,
        iconSvgText,
        background: null,
      });
      logosFolder.file(`composed_logo.svg`, composed);

      const composedReverse = composeLogoSvg({
        name,
        fontFamily,
        textSize,
        textColor: palette.textDark,
        layout,
        iconSize,
        iconSvgText,
        background: palette.textDark,
        textColorOverride: palette.textLight,
      });
      logosFolder.file(`composed_logo_reverse.svg`, composedReverse);
    }

    // High-res rasters (logo, reverse, JPG, text-only, icon, reverse icon)
    const widths = [1000, 2000, 3000];
    for (const w of widths) {
      const scale = w / 1200;
      // PNG logo (transparent)
      logosFolder.file(
        `png_logo_${w}px.png`,
        await renderLogoPNG({
          width: w, bg: null, withIcon: true, withText: true,
          layout, iconSize: Math.round(iconSize * scale), textSize: Math.round(textSize * scale),
          textColor: palette.textDark, fontFamily, name, svgUrl: generatedSvgUrl, pad: 72,
        })
      );
      // PNG reverse logo (on dark)
      logosFolder.file(
        `png_logo_reverse_${w}px.png`,
        await renderLogoPNG({
          width: w, bg: palette.textDark, withIcon: true, withText: true,
          layout, iconSize: Math.round(iconSize * scale), textSize: Math.round(textSize * scale),
          textColor: palette.textLight, fontFamily, name, svgUrl: generatedSvgUrl, pad: 72,
        })
      );
      // JPG (flattened on surface)
      logosFolder.file(
        `jpg_logo_${w}px.jpg`,
        await renderLogoJPG({
          width: w, bg: palette.surface, withIcon: true, withText: true,
          layout, iconSize: Math.round(iconSize * scale), textSize: Math.round(textSize * scale),
          textColor: palette.textDark, fontFamily, name, svgUrl: generatedSvgUrl, pad: 72,
        })
      );
      // Text-only (transparent)
      logosFolder.file(
        `text_logo_${w}px.png`,
        await renderLogoPNG({
          width: w, bg: null, withIcon: false, withText: true,
          layout, iconSize: 0, textSize: Math.round(textSize * scale),
          textColor: palette.textDark, fontFamily, name, svgUrl: null, pad: 72,
        })
      );
    }
    for (const w of [512, 1024, 2048]) {
      // icon only
      logosFolder.file(
        `png_icon_${w}px.png`,
        await renderLogoPNG({
          width: w, bg: null, withIcon: true, withText: false,
          layout: "top-bottom", iconSize: Math.round(w * 0.66), textSize: 0,
          textColor: palette.textDark, fontFamily, name, svgUrl: generatedSvgUrl, pad: 0,
        })
      );
      // reverse icon
      logosFolder.file(
        `png_icon_reverse_${w}px.png`,
        await renderLogoPNG({
          width: w, bg: palette.textDark, withIcon: true, withText: false,
          layout: "top-bottom", iconSize: Math.round(w * 0.66), textSize: 0,
          textColor: palette.textLight, fontFamily, name, svgUrl: generatedSvgUrl, pad: 0,
        })
      );
    }

    // --- 4) Brand Kit PDF (with embedded font if provided)
    const pdfDoc = new jsPDF({ unit: "pt", format: "a4" });
    registerEmbeddedFonts(pdfDoc); // registers any base64s you filled in
    const pdfFont = pdfFontFor(fontFamily);

    // simple header/footer helpers
    const header = (title: string, subtitle?: string) => {
      pdfDoc.setFillColor("#0b1220");
      pdfDoc.rect(0, 0, pdfDoc.internal.pageSize.getWidth(), 48, "F");
      pdfDoc.setFont(pdfFont.name, "bold"); pdfDoc.setTextColor("#ffffff"); pdfDoc.setFontSize(16);
      pdfDoc.text(title, 40, 30);
      if (subtitle) { pdfDoc.setFont(pdfFont.name, "normal"); pdfDoc.text(subtitle, pdfDoc.internal.pageSize.getWidth()-40, 30, { align: "right" }); }
      pdfDoc.setTextColor("#111111");
    };
    const footer = (pg: number) => {
      const h = pdfDoc.internal.pageSize.getHeight();
      pdfDoc.setDrawColor("#e5e7eb");
      pdfDoc.line(40, h - 52, pdfDoc.internal.pageSize.getWidth() - 40, h - 52);
      pdfDoc.setFont(pdfFont.name, "normal"); pdfDoc.setFontSize(10); pdfDoc.setTextColor("#6b7280");
      pdfDoc.text(String(pg), pdfDoc.internal.pageSize.getWidth() - 40, h - 30, { align: "right" });
      pdfDoc.setTextColor("#111111");
    };

    // (A) Cover
    header("Brand Guidelines", "Make Your First Impression Your Best!");
    pdfDoc.setFont(pdfFont.name, "bold"); pdfDoc.setFontSize(28);
    const mid = pdfDoc.internal.pageSize.getWidth()/2;
    const y0 = 220;
    pdfDoc.text(name || "Your Brand", mid, y0 + 210, { align: "center" });
    pdfDoc.setFont(pdfFont.name, "normal"); pdfDoc.setFontSize(12).setTextColor("#6b7280");
    pdfDoc.text(industry || "Brand Kit", mid, y0 + 235, { align: "center" });
    footer(1);

    // (B) Palette page
    pdfDoc.addPage();
    header("03. Palette");
    pdfDoc.setFont(pdfFont.name, "bold").setFontSize(22).setTextColor("#111827");
    pdfDoc.text("Color Palette", 40, 88);
    const swatch = (x: number, y: number, hex: string, label: string) => {
      pdfDoc.setFillColor(hex); pdfDoc.rect(x, y, 36, 24, "F");
      pdfDoc.setFont(pdfFont.name, "normal").setFontSize(11).setTextColor("#111827");
      pdfDoc.text(`${label}: ${hex.toUpperCase()}`, x + 44, y + 16);
    };
    let y = 140, x = 40, col = 280;
    swatch(x, y, palette.primary, "Primary");
    swatch(x, y+36, palette.secondary, "Secondary");
    swatch(x, y+72, palette.accent, "Accent");
    swatch(x, y+108, palette.highlight, "Highlight");
    swatch(col, y, palette.neutral, "Neutral");
    swatch(col, y+36, palette.surface, "Surface");
    swatch(col, y+72, palette.textLight, "Text (Light)");
    swatch(col, y+108, palette.textDark, "Text (Dark)");
    footer(2);

    // (C) Typography page
    pdfDoc.addPage();
    header("04. Typography");
    pdfDoc.setFont(pdfFont.name, "bold").setFontSize(22).setTextColor("#111827");
    pdfDoc.text("Typography", 40, 88);
    pdfDoc.setFont(pdfFont.name, "bold").setFontSize(28).setTextColor("#111827");
    pdfDoc.text("Aa Bb Cc 123", 40, 176);
    pdfDoc.setFont(pdfFont.name, "normal").setFontSize(12).setTextColor("#6b7280");
    pdfDoc.text(`Font: ${fontFamily.split(",")[0]}`, 40, 196);
    footer(3);

    // (D) Logo & Icon page (drops composed normal SVG as PNG for preview)
    pdfDoc.addPage();
    header("05. Logo & Icon");
    pdfDoc.setFont(pdfFont.name, "bold").setFontSize(22).setTextColor("#111827");
    pdfDoc.text("Logo & Icon", 40, 88);
    // preview: draw current composed PNG
    // (Reuse your on-screen renderer for convenience)
    const previewBlob = await renderLogoPNG({
      width: 1200, bg: "#ffffff", withIcon: !!generatedSvgUrl, withText: true,
      layout, iconSize, textSize, textColor: palette.textDark, fontFamily, name, svgUrl: generatedSvgUrl, pad: 64,
    });
    const previewUrl = URL.createObjectURL(previewBlob);
    pdfDoc.addImage(previewUrl, "PNG", 40, 150, 400, 200, undefined, "FAST");
    URL.revokeObjectURL(previewUrl);
    footer(4);

    // Save PDF into ZIP
    const pdfArrayBuf = pdfDoc.output("arraybuffer");
    zip.file(`${brandFolderName}/${brandFolderName}-brand-kit.pdf`, pdfArrayBuf);

    // --- 5) Build and download the ZIP
    const zipBlob = await zip.generateAsync({ type: "blob" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(zipBlob);
    a.download = `${brandFolderName}-brand-kit.zip`;
    a.click();
    URL.revokeObjectURL(a.href);
  } catch (e) {
    console.error(e);
    alert("Failed to create brand kit. Please try again.");
  } finally {
    setIsDownloadingKit(false);
  }
};

Add a PDF-only button alongside your existing actions

Right next to “Download Brand Kit”:

const handleDownloadPdfOnly = async () => {
  const doc = new jsPDF({ unit: "pt", format: "a4" });
  registerEmbeddedFonts(doc);
  const { name: pdfFontName } = pdfFontFor(fontFamily);

  // Simple one-pager using current preview (you already have a full 12-pager above inside ZIP).
  // If you want this to run the same 12-page flow, lift that code into a shared function.
  doc.setFont(pdfFontName, "bold").setFontSize(20);
  doc.text("Brand Kit (Quick PDF)", 48, 64);

  const preview = await renderLogoPNG({
    width: 1200, bg: "#ffffff", withIcon: !!generatedSvgUrl, withText: true,
    layout, iconSize, textSize, textColor: palette.textDark, fontFamily, name, svgUrl: generatedSvgUrl, pad: 64,
  });
  const url = URL.createObjectURL(preview);
  doc.addImage(url, "PNG", 48, 90, 400, 220, undefined, "FAST");
  URL.revokeObjectURL(url);

  const safe = name.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase();
  doc.save(`${safe}-brand-kit.pdf`);
};

// …in the JSX actions row:
<button
  onClick={handleDownloadPdfOnly}
  className="flex items-center gap-2 px-4 py-2 rounded-lg bg-neutral-900 hover:bg-neutral-800 text-white transition-colors"
>
  Download Brand Kit PDF
</button>

What you get

A single click ZIP that includes:

brand-info.json, color-palette.txt

RAW icon.svg

Composed vector: composed_logo.svg, composed_logo_reverse.svg

High-res raster outputs (PNG logo/reverse/text-only, JPG logo, PNG icon/reverse) in multiple sizes

12-page Brand Kit PDF (with embedded Inter/Manrope when you paste the base64s)

A separate PDF button for quick client preview/downloader.