1
mirror of https://github.com/xitanggg/open-resume synced 2024-11-03 09:19:21 +01:00

Add Noto Sans SC language and make it load conditionally

This commit is contained in:
Xitang 2023-08-01 00:45:17 -07:00 committed by Xitang Zhao
parent 521caf98d0
commit 962f6d4f55
22 changed files with 334 additions and 161 deletions

View File

@ -1 +1 @@
public/fonts/fonts.css public/fonts/*.css

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,2 @@
@font-face {font-family: "NotoSansSC"; src: url("/fonts/NotoSansSC-Regular.ttf");}
@font-face {font-family: "NotoSansSC"; src: url("/fonts/NotoSansSC-Bold.ttf"); font-weight: bold;}

View File

@ -1,4 +1,5 @@
/* Adding a new font family needs to keep "public\fonts\fonts.ts" in sync */ /* Adding a new English font family needs to keep "public\fonts\fonts.ts" in sync */
/* Sans Serif Fonts */
@font-face {font-family: "Roboto"; src: url("/fonts/Roboto-Regular.ttf");} @font-face {font-family: "Roboto"; src: url("/fonts/Roboto-Regular.ttf");}
@font-face {font-family: "Roboto"; src: url("/fonts/Roboto-Bold.ttf"); font-weight: bold;} @font-face {font-family: "Roboto"; src: url("/fonts/Roboto-Bold.ttf"); font-weight: bold;}
@font-face {font-family: "Lato"; src: url("/fonts/Lato-Regular.ttf");} @font-face {font-family: "Lato"; src: url("/fonts/Lato-Regular.ttf");}
@ -10,13 +11,14 @@
@font-face {font-family: "Raleway"; src: url("/fonts/Raleway-Regular.ttf");} @font-face {font-family: "Raleway"; src: url("/fonts/Raleway-Regular.ttf");}
@font-face {font-family: "Raleway"; src: url("/fonts/Raleway-Bold.ttf"); font-weight: bold;} @font-face {font-family: "Raleway"; src: url("/fonts/Raleway-Bold.ttf"); font-weight: bold;}
/* Serif Fonts */
@font-face {font-family: "Caladea"; src: url("/fonts/Caladea-Regular.ttf");}
@font-face {font-family: "Caladea"; src: url("/fonts/Caladea-Bold.ttf"); font-weight: bold;}
@font-face {font-family: "Lora"; src: url("/fonts/Lora-Regular.ttf");}
@font-face {font-family: "Lora"; src: url("/fonts/Lora-Bold.ttf"); font-weight: bold;}
@font-face {font-family: "RobotoSlab"; src: url("/fonts/RobotoSlab-Regular.ttf");} @font-face {font-family: "RobotoSlab"; src: url("/fonts/RobotoSlab-Regular.ttf");}
@font-face {font-family: "RobotoSlab"; src: url("/fonts/RobotoSlab-Bold.ttf"); font-weight: bold;} @font-face {font-family: "RobotoSlab"; src: url("/fonts/RobotoSlab-Bold.ttf"); font-weight: bold;}
@font-face {font-family: "PlayfairDisplay"; src: url("/fonts/PlayfairDisplay-Regular.ttf");} @font-face {font-family: "PlayfairDisplay"; src: url("/fonts/PlayfairDisplay-Regular.ttf");}
@font-face {font-family: "PlayfairDisplay"; src: url("/fonts/PlayfairDisplay-Bold.ttf"); font-weight: bold;} @font-face {font-family: "PlayfairDisplay"; src: url("/fonts/PlayfairDisplay-Bold.ttf"); font-weight: bold;}
@font-face {font-family: "Merriweather"; src: url("/fonts/Merriweather-Regular.ttf");} @font-face {font-family: "Merriweather"; src: url("/fonts/Merriweather-Regular.ttf");}
@font-face {font-family: "Merriweather"; src: url("/fonts/Merriweather-Bold.ttf"); font-weight: bold;} @font-face {font-family: "Merriweather"; src: url("/fonts/Merriweather-Bold.ttf"); font-weight: bold;}
@font-face {font-family: "Lora"; src: url("/fonts/Lora-Regular.ttf");}
@font-face {font-family: "Lora"; src: url("/fonts/Lora-Bold.ttf"); font-weight: bold;}
@font-face {font-family: "Caladea"; src: url("/fonts/Caladea-Regular.ttf");}
@font-face {font-family: "Caladea"; src: url("/fonts/Caladea-Bold.ttf"); font-weight: bold;}

View File

@ -1,50 +0,0 @@
/**
* Adding a new font family needs to keep "public\fonts\fonts.css" in sync
*
* The followings are handled automatically once the font family name is added to the variable arrays
* 1. It would register font family for React PDF at "components\Resume\ResumePDF\styles.ts"
* 2. It would load font family for React PDF Iframe at "components\Resume\ResumeIFrame.tsx"
* 3. It would add font family selection to Resume Settings at "components\ResumeForm\ThemeForm.tsx"
*
* One caveat while adding a new google font is that sometimes the font doesn't work with React PDF:
* https://github.com/diegomura/react-pdf/issues/915. The solution is to re-export the font with fontforge.
* ./fontforge -lang=ff -c 'Open($1); Generate($2); Close();' old_font.ttf new_font.ttf
* (Note that some fonts might still not work after export.)
*/
export const SANS_SERI_FONT_FAMILIES = [
"FangZheng",
"Roboto",
"Lato",
"Montserrat",
"OpenSans",
"Raleway",
] as const;
export const SERI_FONT_FAMILIES = [
"Caladea",
"Lora",
"RobotoSlab",
"PlayfairDisplay",
"Merriweather",
] as const;
export const FONT_FAMILIES = [
...SANS_SERI_FONT_FAMILIES,
...SERI_FONT_FAMILIES,
];
export type FontFamily = (typeof FONT_FAMILIES)[number];
export const FONT_FAMILY_TO_STANDARD_SIZE_IN_PT: Record<FontFamily, number> = {
FangZheng: 11,
Roboto: 11,
Lato: 11,
Montserrat: 10,
OpenSans: 10,
Raleway: 10,
Caladea: 11,
Lora: 11,
RobotoSlab: 10,
PlayfairDisplay: 10,
Merriweather: 10,
};

View File

@ -8,7 +8,7 @@ import {
import { usePDF } from "@react-pdf/renderer"; import { usePDF } from "@react-pdf/renderer";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
const ResumeControlBarComponent = ({ const ResumeControlBar = ({
scale, scale,
setScale, setScale,
documentSize, documentSize,
@ -71,9 +71,11 @@ const ResumeControlBarComponent = ({
); );
}; };
// Make ResumeControlBar dynamic to make nextjs happy /**
export const ResumeControlBar = dynamic( * Load ResumeControlBar client side since it uses usePDF, which is a web specific API
() => Promise.resolve(ResumeControlBarComponent), */
export const ResumeControlBarCSR = dynamic(
() => Promise.resolve(ResumeControlBar),
{ {
ssr: false, ssr: false,
} }

View File

@ -1,47 +1,58 @@
"use client"; "use client";
import { FONT_FAMILIES } from "public/fonts/fonts"; import { useMemo } from "react";
import Frame from "react-frame-component"; import Frame from "react-frame-component";
import { import {
A4_HEIGHT_PX, A4_HEIGHT_PX,
A4_WIDTH_PX, A4_WIDTH_PX,
A4_WIDTH_PT,
LETTER_HEIGHT_PX, LETTER_HEIGHT_PX,
LETTER_WIDTH_PT,
LETTER_WIDTH_PX, LETTER_WIDTH_PX,
LETTER_WIDTH_PT,
} from "lib/constants"; } from "lib/constants";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { getAllFontFamiliesToLoad } from "components/fonts/lib";
const IFRAME_INITIAL_CONTENT_FONT_FAMILIES_PRELOAD_LINKS = FONT_FAMILIES.map( const getIframeInitialContent = (isA4: boolean) => {
( const width = isA4 ? A4_WIDTH_PT : LETTER_WIDTH_PT;
font const allFontFamilies = getAllFontFamiliesToLoad();
) => `<link rel="preload" as="font" href="/fonts/${font}-Regular.ttf" type="font/ttf" crossorigin="anonymous">
<link rel="preload" as="font" href="/fonts/${font}-Bold.ttf" type="font/ttf" crossorigin="anonymous">`
).join("");
const IFRAME_INITIAL_CONTENT_FONT_FAMILIES_FONT_FACE = FONT_FAMILIES.map( const allFontFamiliesPreloadLinks = allFontFamilies
( .map(
font (
) => `@font-face {font-family: "${font}"; src: url("/fonts/${font}-Regular.ttf");} font
@font-face {font-family: "${font}"; src: url("/fonts/${font}-Bold.ttf"); font-weight: bold;}` ) => `<link rel="preload" as="font" href="/fonts/${font}-Regular.ttf" type="font/ttf" crossorigin="anonymous">
).join(""); <link rel="preload" as="font" href="/fonts/${font}-Bold.ttf" type="font/ttf" crossorigin="anonymous">`
)
.join("");
const IFRAME_INITIAL_CONTENT = `<!DOCTYPE html> const allFontFamiliesFontFaces = allFontFamilies
<html> .map(
<head> (
${IFRAME_INITIAL_CONTENT_FONT_FAMILIES_PRELOAD_LINKS} font
<style> ) => `@font-face {font-family: "${font}"; src: url("/fonts/${font}-Regular.ttf");}
${IFRAME_INITIAL_CONTENT_FONT_FAMILIES_FONT_FACE} @font-face {font-family: "${font}"; src: url("/fonts/${font}-Bold.ttf"); font-weight: bold;}`
</style> )
</head> .join("");
<body style='overflow: hidden; width: ${LETTER_WIDTH_PT}pt; margin: 0; padding: 0; -webkit-text-size-adjust:none;'>
<div></div> return `<!DOCTYPE html>
</body> <html>
</html>`; <head>
${allFontFamiliesPreloadLinks}
<style>
${allFontFamiliesFontFaces}
</style>
</head>
<body style='overflow: hidden; width: ${width}pt; margin: 0; padding: 0; -webkit-text-size-adjust:none;'>
<div></div>
</body>
</html>`;
};
/** /**
* IFrame is used here for style isolation, since react pdf uses pt unit. * Iframe is used here for style isolation, since react pdf uses pt unit.
* It creates a sandbox document body that uses letter/A4 size as width. * It creates a sandbox document body that uses letter/A4 pt size as width.
*/ */
const ResumeIFrameComponent = ({ const ResumeIframe = ({
documentSize, documentSize,
scale, scale,
children, children,
@ -52,6 +63,12 @@ const ResumeIFrameComponent = ({
children: React.ReactNode; children: React.ReactNode;
enablePDFViewer?: boolean; enablePDFViewer?: boolean;
}) => { }) => {
const isA4 = documentSize === "A4";
const iframeInitialContent = useMemo(
() => getIframeInitialContent(isA4),
[isA4]
);
if (enablePDFViewer) { if (enablePDFViewer) {
return ( return (
<DynamicPDFViewer className="h-full w-full"> <DynamicPDFViewer className="h-full w-full">
@ -59,8 +76,8 @@ const ResumeIFrameComponent = ({
</DynamicPDFViewer> </DynamicPDFViewer>
); );
} }
const width = documentSize === "A4" ? A4_WIDTH_PX : LETTER_WIDTH_PX; const width = isA4 ? A4_WIDTH_PX : LETTER_WIDTH_PX;
const height = documentSize === "A4" ? A4_HEIGHT_PX : LETTER_HEIGHT_PX; const height = isA4 ? A4_HEIGHT_PX : LETTER_HEIGHT_PX;
return ( return (
<div <div
@ -82,7 +99,9 @@ const ResumeIFrameComponent = ({
> >
<Frame <Frame
style={{ width: "100%", height: "100%" }} style={{ width: "100%", height: "100%" }}
initialContent={IFRAME_INITIAL_CONTENT} initialContent={iframeInitialContent}
// key is used to force component to re-mount when document size changes
key={isA4 ? "A4" : "LETTER"}
> >
{children} {children}
</Frame> </Frame>
@ -91,13 +110,12 @@ const ResumeIFrameComponent = ({
); );
}; };
// Iframe can't be server side rendered, so we use dynamic import to load it only on client side /**
export const ResumeIFrame = dynamic( * Load iframe client side since iframe can't be SSR
() => Promise.resolve(ResumeIFrameComponent), */
{ export const ResumeIframeCSR = dynamic(() => Promise.resolve(ResumeIframe), {
ssr: false, ssr: false,
} });
);
// PDFViewer is only used for debugging. Its size is quite large, so we make it dynamic import // PDFViewer is only used for debugging. Its size is quite large, so we make it dynamic import
const DynamicPDFViewer = dynamic( const DynamicPDFViewer = dynamic(

View File

@ -1,23 +1,4 @@
import { StyleSheet, Font } from "@react-pdf/renderer"; import { StyleSheet } from "@react-pdf/renderer";
import { FONT_FAMILIES } from "public/fonts/fonts";
FONT_FAMILIES.forEach((fontFamily) => {
Font.register({
family: fontFamily,
fonts: [
{
src: `fonts/${fontFamily}-Regular.ttf`,
},
{
src: `fonts/${fontFamily}-Bold.ttf`,
fontWeight: "bold",
},
],
});
});
// Disable hyphenation https://github.com/diegomura/react-pdf/issues/311#issuecomment-548301604
Font.registerHyphenationCallback((word) => [word]);
// Tailwindcss Spacing Design System: https://tailwindcss.com/docs/theme#spacing // Tailwindcss Spacing Design System: https://tailwindcss.com/docs/theme#spacing
// It is converted from rem to pt (1rem = 12pt) since https://react-pdf.org/styling only accepts pt unit // It is converted from rem to pt (1rem = 12pt) since https://react-pdf.org/styling only accepts pt unit

View File

@ -1,9 +1,9 @@
"use client"; "use client";
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import { ResumeIFrame } from "components/Resume/ResumeIFrame"; import { ResumeIframeCSR } from "components/Resume/ResumeIframe";
import { ResumePDF } from "components/Resume/ResumePDF"; import { ResumePDF } from "components/Resume/ResumePDF";
import { import {
ResumeControlBar, ResumeControlBarCSR,
ResumeControlBarBorder, ResumeControlBarBorder,
} from "components/Resume/ResumeControlBar"; } from "components/Resume/ResumeControlBar";
import { FlexboxSpacer } from "components/FlexboxSpacer"; import { FlexboxSpacer } from "components/FlexboxSpacer";
@ -11,8 +11,13 @@ import { useAppSelector } from "lib/redux/hooks";
import { selectResume } from "lib/redux/resumeSlice"; import { selectResume } from "lib/redux/resumeSlice";
import { selectSettings } from "lib/redux/settingsSlice"; import { selectSettings } from "lib/redux/settingsSlice";
import { DEBUG_RESUME_PDF_FLAG } from "lib/constants"; import { DEBUG_RESUME_PDF_FLAG } from "lib/constants";
import {
useRegisterReactPDFFont,
useRegisterReactPDFHyphenationCallback,
} from "components/fonts/hooks";
import { NonEnglishFontsCSSLoader } from "components/fonts/NonEnglishFontsCSSLoader";
const Resume = () => { export const Resume = () => {
const [scale, setScale] = useState(0.8); const [scale, setScale] = useState(0.8);
const resume = useAppSelector(selectResume); const resume = useAppSelector(selectResume);
const settings = useAppSelector(selectSettings); const settings = useAppSelector(selectSettings);
@ -21,34 +26,38 @@ const Resume = () => {
[resume, settings] [resume, settings]
); );
useRegisterReactPDFFont();
useRegisterReactPDFHyphenationCallback(settings.fontFamily);
return ( return (
<div className="relative flex justify-center md:justify-start"> <>
<FlexboxSpacer maxWidth={50} className="hidden md:block" /> <NonEnglishFontsCSSLoader />
<div className="relative"> <div className="relative flex justify-center md:justify-start">
<section className="h-[calc(100vh-var(--top-nav-bar-height)-var(--resume-control-bar-height))] overflow-hidden md:p-[var(--resume-padding)]"> <FlexboxSpacer maxWidth={50} className="hidden md:block" />
<ResumeIFrame <div className="relative">
documentSize={settings.documentSize} <section className="h-[calc(100vh-var(--top-nav-bar-height)-var(--resume-control-bar-height))] overflow-hidden md:p-[var(--resume-padding)]">
<ResumeIframeCSR
documentSize={settings.documentSize}
scale={scale}
enablePDFViewer={DEBUG_RESUME_PDF_FLAG}
>
<ResumePDF
resume={resume}
settings={settings}
isPDF={DEBUG_RESUME_PDF_FLAG}
/>
</ResumeIframeCSR>
</section>
<ResumeControlBarCSR
scale={scale} scale={scale}
enablePDFViewer={DEBUG_RESUME_PDF_FLAG} setScale={setScale}
> documentSize={settings.documentSize}
<ResumePDF document={document}
resume={resume} fileName={resume.profile.name + " - Resume"}
settings={settings} />
isPDF={DEBUG_RESUME_PDF_FLAG} </div>
/> <ResumeControlBarBorder />
</ResumeIFrame>
</section>
<ResumeControlBar
scale={scale}
setScale={setScale}
documentSize={settings.documentSize}
document={document}
fileName={resume.profile.name + " - Resume"}
/>
</div> </div>
<ResumeControlBarBorder /> </>
</div>
); );
}; };
export default Resume;

View File

@ -2,9 +2,11 @@ import type { GeneralSetting } from "lib/redux/settingsSlice";
import { PX_PER_PT } from "lib/constants"; import { PX_PER_PT } from "lib/constants";
import { import {
FONT_FAMILY_TO_STANDARD_SIZE_IN_PT, FONT_FAMILY_TO_STANDARD_SIZE_IN_PT,
FONT_FAMILIES, FONT_FAMILY_TO_DISPLAY_NAME,
type FontFamily, type FontFamily,
} from "public/fonts/fonts"; } from "components/fonts/constants";
import { getAllFontFamiliesToLoad } from "components/fonts/lib";
import dynamic from "next/dynamic";
const Selection = ({ const Selection = ({
selectedColor, selectedColor,
@ -45,7 +47,7 @@ const SelectionsWrapper = ({ children }: { children: React.ReactNode }) => {
return <div className="mt-2 flex flex-wrap gap-3">{children}</div>; return <div className="mt-2 flex flex-wrap gap-3">{children}</div>;
}; };
export const FontFamilySelections = ({ const FontFamilySelections = ({
selectedFontFamily, selectedFontFamily,
themeColor, themeColor,
handleSettingsChange, handleSettingsChange,
@ -54,9 +56,10 @@ export const FontFamilySelections = ({
themeColor: string; themeColor: string;
handleSettingsChange: (field: GeneralSetting, value: string) => void; handleSettingsChange: (field: GeneralSetting, value: string) => void;
}) => { }) => {
const allFontFamilies = getAllFontFamiliesToLoad();
return ( return (
<SelectionsWrapper> <SelectionsWrapper>
{FONT_FAMILIES.map((fontFamily, idx) => { {allFontFamilies.map((fontFamily, idx) => {
const isSelected = selectedFontFamily === fontFamily; const isSelected = selectedFontFamily === fontFamily;
const standardSizePt = FONT_FAMILY_TO_STANDARD_SIZE_IN_PT[fontFamily]; const standardSizePt = FONT_FAMILY_TO_STANDARD_SIZE_IN_PT[fontFamily];
return ( return (
@ -70,7 +73,7 @@ export const FontFamilySelections = ({
}} }}
onClick={() => handleSettingsChange("fontFamily", fontFamily)} onClick={() => handleSettingsChange("fontFamily", fontFamily)}
> >
{fontFamily.split(/(?=[A-Z])/).join(" ")} {FONT_FAMILY_TO_DISPLAY_NAME[fontFamily]}
</Selection> </Selection>
); );
})} })}
@ -78,6 +81,17 @@ export const FontFamilySelections = ({
); );
}; };
/**
* Load FontFamilySelections client side since it calls getAllFontFamiliesToLoad,
* which uses navigator object that is only available on client side
*/
export const FontFamilySelectionsCSR = dynamic(
() => Promise.resolve(FontFamilySelections),
{
ssr: false,
}
);
export const FontSizeSelections = ({ export const FontSizeSelections = ({
selectedFontSize, selectedFontSize,
fontFamily, fontFamily,

View File

@ -4,7 +4,7 @@ import { THEME_COLORS } from "components/ResumeForm/ThemeForm/constants";
import { InlineInput } from "components/ResumeForm/ThemeForm/InlineInput"; import { InlineInput } from "components/ResumeForm/ThemeForm/InlineInput";
import { import {
DocumentSizeSelections, DocumentSizeSelections,
FontFamilySelections, FontFamilySelectionsCSR,
FontSizeSelections, FontSizeSelections,
} from "components/ResumeForm/ThemeForm/Selection"; } from "components/ResumeForm/ThemeForm/Selection";
import { import {
@ -14,7 +14,7 @@ import {
type GeneralSetting, type GeneralSetting,
} from "lib/redux/settingsSlice"; } from "lib/redux/settingsSlice";
import { useAppDispatch, useAppSelector } from "lib/redux/hooks"; import { useAppDispatch, useAppSelector } from "lib/redux/hooks";
import type { FontFamily } from "public/fonts/fonts"; import type { FontFamily } from "components/fonts/constants";
import { Cog6ToothIcon } from "@heroicons/react/24/outline"; import { Cog6ToothIcon } from "@heroicons/react/24/outline";
export const ThemeForm = () => { export const ThemeForm = () => {
@ -65,7 +65,7 @@ export const ThemeForm = () => {
</div> </div>
<div> <div>
<InputGroupWrapper label="Font Family" /> <InputGroupWrapper label="Font Family" />
<FontFamilySelections <FontFamilySelectionsCSR
selectedFontFamily={fontFamily} selectedFontFamily={fontFamily}
themeColor={themeColor} themeColor={themeColor}
handleSettingsChange={handleSettingsChange} handleSettingsChange={handleSettingsChange}

View File

@ -0,0 +1,7 @@
import "public/fonts/fonts-zh.css";
/**
* Empty component. Main purpose is to load fonts-zh.css
*/
const FontsZh = () => <></>;
export default FontsZh;

View File

@ -0,0 +1,24 @@
import { useState, useEffect } from "react";
import dynamic from "next/dynamic";
import { getAllFontFamiliesToLoad } from "components/fonts/lib";
const FontsZh = dynamic(() => import("components/fonts/FontsZh"), {
ssr: false,
});
/**
* Empty component to load non-english fonts CSS conditionally
*
* Reference: https://prawira.medium.com/react-conditional-import-conditional-css-import-110cc58e0da6
*/
export const NonEnglishFontsCSSLoader = () => {
const [shouldLoadFontsZh, setShouldLoadFontsZh] = useState(false);
useEffect(() => {
if (getAllFontFamiliesToLoad().includes("NotoSansSC")) {
setShouldLoadFontsZh(true);
}
}, []);
return <>{shouldLoadFontsZh && <FontsZh />}</>;
};

View File

@ -0,0 +1,94 @@
/**
* Adding a new font family involves 3 steps:
* Step 1. Add it to one of the below FONT_FAMILIES variable array:
* English fonts -> SANS_SERIF_ENGLISH_FONT_FAMILIES or SERIF_ENGLISH_FONT_FAMILIES
* Non-English fonts -> NON_ENGLISH_FONT_FAMILIES
* Once the font is added, it would take care of
* a. Registering font family for React PDF at "components/fonts/hooks.tsx"
* b. Loading font family for React PDF iframe at "components/Resume/ResumeIframe.tsx"
* c. Adding font family selection to Resume Settings at "components/ResumeForm/ThemeForm/Selection.tsx"
* Step 2. To load css correctly for the Resume Form:
* English fonts -> add it to the "public\fonts\fonts.css" file
* Non-English fonts -> create/update "public\fonts\fonts-<language>.css" and update "components/fonts/NonEnglishFontsCSSLoader.tsx"
* Step 3. Update FONT_FAMILY_TO_STANDARD_SIZE_IN_PT and FONT_FAMILY_TO_DISPLAY_NAME accordingly
*
* IMPORTANT NOTE:
* One major problem with adding a new font family is that most font family doesn't work with
* React PDF out of box. The texts would appear fine in the PDF, but copying and pasting them
* would result in different texts. See issues: https://github.com/diegomura/react-pdf/issues/915
* and https://github.com/diegomura/react-pdf/issues/629
*
* A solution to this problem is to import and re-export the font with a font editor, e.g. fontforge or birdfont.
*
* If using fontforge, the following command can be used to export the font:
* ./fontforge -lang=ff -c 'Open($1); Generate($2); Close();' old_font.ttf new_font.ttf
* Note that fontforge doesn't work on non-english fonts: https://github.com/fontforge/fontforge/issues/1534
* Also, some fonts might still not work after re-export.
*/
const SANS_SERIF_ENGLISH_FONT_FAMILIES = [
"Roboto",
"Lato",
"Montserrat",
"OpenSans",
"Raleway",
] as const;
const SERIF_ENGLISH_FONT_FAMILIES = [
"Caladea",
"Lora",
"RobotoSlab",
"PlayfairDisplay",
"Merriweather",
] as const;
export const ENGLISH_FONT_FAMILIES = [
...SANS_SERIF_ENGLISH_FONT_FAMILIES,
...SERIF_ENGLISH_FONT_FAMILIES,
];
type EnglishFontFamily = (typeof ENGLISH_FONT_FAMILIES)[number];
export const NON_ENGLISH_FONT_FAMILIES = ["NotoSansSC"] as const;
type NonEnglishFontFamily = (typeof NON_ENGLISH_FONT_FAMILIES)[number];
export const NON_ENGLISH_FONT_FAMILIES_TO_LANGUAGE: Record<
NonEnglishFontFamily,
string[]
> = {
NotoSansSC: ["zh", "zh-CN", "zh-TW"],
};
export type FontFamily = EnglishFontFamily | NonEnglishFontFamily;
export const FONT_FAMILY_TO_STANDARD_SIZE_IN_PT: Record<FontFamily, number> = {
// Sans Serif Fonts
Roboto: 11,
Lato: 11,
Montserrat: 10,
OpenSans: 10,
Raleway: 10,
// Serif Fonts
Caladea: 11,
Lora: 11,
RobotoSlab: 10,
PlayfairDisplay: 10,
Merriweather: 10,
// Non-English Fonts
NotoSansSC: 11,
};
export const FONT_FAMILY_TO_DISPLAY_NAME: Record<FontFamily, string> = {
// Sans Serif Fonts
Roboto: "Roboto",
Lato: "Lato",
Montserrat: "Montserrat",
OpenSans: "Open Sans",
Raleway: "Raleway",
// Serif Fonts
Caladea: "Caladea",
Lora: "Lora",
RobotoSlab: "Roboto Slab",
PlayfairDisplay: "Playfair Display",
Merriweather: "Merriweather",
// Non-English Fonts
NotoSansSC: "思源黑体(简体)",
};

View File

@ -0,0 +1,47 @@
import { useEffect } from "react";
import { Font } from "@react-pdf/renderer";
import { ENGLISH_FONT_FAMILIES } from "components/fonts/constants";
import { getAllFontFamiliesToLoad } from "components/fonts/lib";
/**
* Register all fonts to React PDF so it can render fonts correctly in PDF
*/
export const useRegisterReactPDFFont = () => {
useEffect(() => {
const allFontFamilies = getAllFontFamiliesToLoad();
allFontFamilies.forEach((fontFamily) => {
Font.register({
family: fontFamily,
fonts: [
{
src: `fonts/${fontFamily}-Regular.ttf`,
},
{
src: `fonts/${fontFamily}-Bold.ttf`,
fontWeight: "bold",
},
],
});
});
}, []);
};
export const useRegisterReactPDFHyphenationCallback = (fontFamily: string) => {
useEffect(() => {
if (ENGLISH_FONT_FAMILIES.includes(fontFamily as any)) {
// Disable hyphenation for English Font Family so the word wraps each line
// https://github.com/diegomura/react-pdf/issues/311#issuecomment-548301604
Font.registerHyphenationCallback((word) => [word]);
} else {
// React PDF doesn't understand how to wrap non-english word on line break
// A workaround is to add an empty character after each word
// Reference https://github.com/diegomura/react-pdf/issues/1568
Font.registerHyphenationCallback((word) =>
word
.split("")
.map((char) => [char, ""])
.flat()
);
}
}, [fontFamily]);
};

View File

@ -0,0 +1,24 @@
"use client";
import {
ENGLISH_FONT_FAMILIES,
NON_ENGLISH_FONT_FAMILIES,
NON_ENGLISH_FONT_FAMILIES_TO_LANGUAGE,
} from "components/fonts/constants";
/**
* getPreferredNonEnglishFontFamilies returns non-english font families that are included in
* user's preferred languages. This is to avoid loading fonts/languages that users won't use.
*/
export const getPreferredNonEnglishFontFamilies = () => {
return NON_ENGLISH_FONT_FAMILIES.filter((fontFamily) => {
const fontLanguages = NON_ENGLISH_FONT_FAMILIES_TO_LANGUAGE[fontFamily];
const userPreferredLanguages = navigator.languages ?? [navigator.language];
return userPreferredLanguages.some((preferredLanguage) =>
fontLanguages.includes(preferredLanguage)
);
});
};
export const getAllFontFamiliesToLoad = () => {
return [...ENGLISH_FONT_FAMILIES, ...getPreferredNonEnglishFontFamilies()];
};

View File

@ -1,10 +1,9 @@
"use client"; "use client";
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { ResumePDF } from "components/Resume/ResumePDF"; import { ResumePDF } from "components/Resume/ResumePDF";
import type { Resume } from "lib/redux/types";
import { initialResumeState } from "lib/redux/resumeSlice"; import { initialResumeState } from "lib/redux/resumeSlice";
import { initialSettings } from "lib/redux/settingsSlice"; import { initialSettings } from "lib/redux/settingsSlice";
import { ResumeIFrame } from "components/Resume/ResumeIFrame"; import { ResumeIframeCSR } from "components/Resume/ResumeIframe";
import { START_HOME_RESUME, END_HOME_RESUME } from "home/constants"; import { START_HOME_RESUME, END_HOME_RESUME } from "home/constants";
import { makeObjectCharIterator } from "lib/make-object-char-iterator"; import { makeObjectCharIterator } from "lib/make-object-char-iterator";
import { useTailwindBreakpoints } from "lib/hooks/useTailwindBreakpoints"; import { useTailwindBreakpoints } from "lib/hooks/useTailwindBreakpoints";
@ -61,7 +60,7 @@ export const AutoTypingResume = () => {
return ( return (
<> <>
<ResumeIFrame documentSize="Letter" scale={isLg ? 0.7 : 0.5}> <ResumeIframeCSR documentSize="Letter" scale={isLg ? 0.7 : 0.5}>
<ResumePDF <ResumePDF
resume={resume} resume={resume}
settings={{ settings={{
@ -78,7 +77,7 @@ export const AutoTypingResume = () => {
}, },
}} }}
/> />
</ResumeIFrame> </ResumeIframeCSR>
</> </>
); );
}; };

View File

@ -2,7 +2,7 @@
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { store } from "lib/redux/store"; import { store } from "lib/redux/store";
import { ResumeForm } from "components/ResumeForm"; import { ResumeForm } from "components/ResumeForm";
import Resume from "components/Resume"; import { Resume } from "components/Resume";
export default function Create() { export default function Create() {
return ( return (