diff --git a/apps/live/src/lib/pdf/node-renderers.tsx b/apps/live/src/lib/pdf/node-renderers.tsx index 003d21f552a..7abd063ddd1 100644 --- a/apps/live/src/lib/pdf/node-renderers.tsx +++ b/apps/live/src/lib/pdf/node-renderers.tsx @@ -88,6 +88,11 @@ const getFlexAlignStyle = (textAlign: string | null | undefined): Style => { return {}; }; +const getRtlStyle = (dir: string | null | undefined): Style => { + if (dir !== "rtl") return {}; + return { fontFamily: "Vazirmatn" }; +}; + export const nodeRenderers: NodeRendererRegistry = { doc: (_node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => ( {children} @@ -98,16 +103,20 @@ export const nodeRenderers: NodeRendererRegistry = { paragraph: (node: TipTapNode, children: ReactElement[], ctx: PDFRenderContext): ReactElement => { const textAlign = node.attrs?.textAlign as string | null; + const dir = node.attrs?.dir as string | null | undefined; + // For RTL paragraphs with no explicit alignment, default to right-aligned + const effectiveTextAlign = textAlign ?? (dir === "rtl" ? "right" : null); const background = node.attrs?.backgroundColor as string | undefined; - const alignStyle = getTextAlignStyle(textAlign); - const flexStyle = getFlexAlignStyle(textAlign); + const alignStyle = getTextAlignStyle(effectiveTextAlign); + const flexStyle = getFlexAlignStyle(effectiveTextAlign); + const rtlStyle = getRtlStyle(dir); const resolvedBgColor = background && background !== "default" ? resolveColorForPdf(background, "background") : null; const bgStyle = resolvedBgColor ? { backgroundColor: resolvedBgColor } : {}; return ( - {children} + {children} ); }, @@ -117,12 +126,16 @@ export const nodeRenderers: NodeRendererRegistry = { const styleKey = `heading${level}` as keyof typeof pdfStyles; const style = pdfStyles[styleKey] || pdfStyles.heading1; const textAlign = node.attrs?.textAlign as string | null; - const alignStyle = getTextAlignStyle(textAlign); - const flexStyle = getFlexAlignStyle(textAlign); + const dir = node.attrs?.dir as string | null | undefined; + // For RTL headings with no explicit alignment, default to right-aligned + const effectiveTextAlign = textAlign ?? (dir === "rtl" ? "right" : null); + const alignStyle = getTextAlignStyle(effectiveTextAlign); + const flexStyle = getFlexAlignStyle(effectiveTextAlign); + const rtlStyle = getRtlStyle(dir); return ( - {children} + {children} ); }, diff --git a/apps/live/src/lib/pdf/plane-pdf-exporter.tsx b/apps/live/src/lib/pdf/plane-pdf-exporter.tsx index f6c6b599c90..743eb62ed2b 100644 --- a/apps/live/src/lib/pdf/plane-pdf-exporter.tsx +++ b/apps/live/src/lib/pdf/plane-pdf-exporter.tsx @@ -6,6 +6,7 @@ import { createRequire } from "module"; import path from "path"; +import { fileURLToPath } from "url"; import { Document, Font, Page, pdf, Text } from "@react-pdf/renderer"; import { createKeyGenerator, renderNode } from "./node-renderers"; import { pdfStyles } from "./styles"; @@ -50,6 +51,26 @@ Font.register({ ], }); +// Resolve Vazirmatn font files relative to the compiled bundle (dist/start.js), +// so the path is stable regardless of where the process is started from. +// Place the woff files at apps/live/fonts/vazirmatn/ before starting the server. +// Download from: https://github.com/rastikerdar/vazirmatn/releases +const vazirmatnFontDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../fonts/vazirmatn"); + +Font.register({ + family: "Vazirmatn", + fonts: [ + { + src: path.join(vazirmatnFontDir, "vazirmatn-regular.woff"), + fontWeight: "normal", + }, + { + src: path.join(vazirmatnFontDir, "vazirmatn-bold.woff"), + fontWeight: "bold", + }, + ], +}); + export const createPdfDocument = (doc: TipTapDocument, options: PDFExportOptions = {}) => { const { title, author, subject, pageSize = "A4", pageOrientation = "portrait", metadata, noAssets } = options; diff --git a/apps/web/core/components/editor/pdf/document.tsx b/apps/web/core/components/editor/pdf/document.tsx index 1c439bfae14..e516ae1fbc6 100644 --- a/apps/web/core/components/editor/pdf/document.tsx +++ b/apps/web/core/components/editor/pdf/document.tsx @@ -17,6 +17,11 @@ import interSemibold from "@/app/assets/fonts/inter/semibold.ttf?url"; import interThin from "@/app/assets/fonts/inter/thin.ttf?url"; import interUltraBold from "@/app/assets/fonts/inter/ultrabold.ttf?url"; import interUltraLight from "@/app/assets/fonts/inter/ultralight.ttf?url"; +// Vazirmatn — Persian/Arabic font for RTL content. +// Place font files at apps/web/app/assets/fonts/vazirmatn/ before building. +// Download from: https://github.com/rastikerdar/vazirmatn/releases +import vazirmatnBold from "@/app/assets/fonts/vazirmatn/bold.ttf?url"; +import vazirmatnRegular from "@/app/assets/fonts/vazirmatn/regular.ttf?url"; // constants import { EDITOR_PDF_DOCUMENT_STYLESHEET } from "@/constants/editor"; @@ -44,6 +49,14 @@ Font.register({ ], }); +Font.register({ + family: "Vazirmatn", + fonts: [ + { src: vazirmatnRegular, fontWeight: "normal" }, + { src: vazirmatnBold, fontWeight: "bold" }, + ], +}); + type Props = { content: string; pageFormat: PageProps["size"]; diff --git a/apps/web/core/constants/editor.ts b/apps/web/core/constants/editor.ts index 5cd8b929cda..de446b52ea4 100644 --- a/apps/web/core/constants/editor.ts +++ b/apps/web/core/constants/editor.ts @@ -236,6 +236,12 @@ const EDITOR_PDF_FONT_FAMILY_STYLES: Styles = { ".courier-bold": { fontFamily: "Courier-Bold", }, + // RTL content (Persian, Arabic, Hebrew, etc.) — use a font that carries + // the required Unicode shaping tables so letters connect correctly. + "[dir='rtl']": { + fontFamily: "Vazirmatn", + textAlign: "right", + }, }; const EDITOR_PDF_TYPOGRAPHY_STYLES: Styles = {