Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add font-weight and font-style support #535

Merged
merged 7 commits into from
Mar 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 45 additions & 8 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ declare module '@react-pdf/renderer' {
type SourceObject =
| string
| { data: Buffer; format: 'png' | 'jpg' }
| { uri: string; method: HTTPMethod, body: any, headers: any }
| { uri: string; method: HTTPMethod; body: any; headers: any };

interface BaseImageProps extends NodeProps {
debug?: boolean;
Expand Down Expand Up @@ -216,11 +216,46 @@ declare module '@react-pdf/renderer' {
*/
class PDFDownloadLink extends React.Component<PDFDownloadLinkProps> {}

type FontStyle = 'normal' | 'italic' | 'oblique';

type FontWeight =
| number
| 'thin'
| 'ultralight'
| 'light'
| 'normal'
| 'medium'
| 'semibold'
| 'bold'
| 'ultrabold'
| 'heavy';

interface FontSource {
src: string;
fontFamily: string;
fontStyle: FontStyle;
fontWeight: number;
data: any;
loading: boolean;
options: any;
}

interface FontInstance {
family: string;
sources: FontSource[];
}

interface EmojiSource {
url: string;
format: string;
}

interface FontDescriptor {
family: string;
fontStyle: FontStyle;
fontWeight: FontWeight;
}

interface RegisteredFont {
src: string;
loaded: boolean;
Expand All @@ -235,20 +270,22 @@ declare module '@react-pdf/renderer' {
) => string[];

const Font: {
register: (
src: string,
options: { family: string; [key: string]: any },
) => void;
register: (options: {
family: string;
src: string;
[key: string]: any;
}) => void;
getEmojiSource: () => EmojiSource;
getRegisteredFonts: () => string[];
getRegisteredFonts: () => FontInstance[];
getRegisteredFontFamilies: () => string[];
registerEmojiSource: (emojiSource: EmojiSource) => void;
registerHyphenationCallback: (
hyphenationCallback: HyphenationCallback,
) => void;
getHyphenationCallback: () => HyphenationCallback;
getFont: (fontFamily: string) => RegisteredFont | undefined;
getFont: (fontDescriptor: FontDescriptor) => RegisteredFont | undefined;
load: (
fontFamily: string,
fontDescriptor: FontDescriptor,
document: React.ReactElement<DocumentProps>,
) => Promise<void>;
clear: () => void;
Expand Down
2 changes: 1 addition & 1 deletion src/elements/Document.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class Document {
const node = listToExplore.shift();

if (node.style && node.style.fontFamily) {
promises.push(Font.load(node.style.fontFamily, this.root.instance));
promises.push(Font.load(node.style, this.root.instance));
}

if (node.children) {
Expand Down
12 changes: 12 additions & 0 deletions src/font/emoji.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
let emojiSource;

export const registerEmojiSource = ({ url, format = 'png' }) => {
emojiSource = { url, format };
};

export const getEmojiSource = () => emojiSource;

export default {
registerEmojiSource,
getEmojiSource,
};
114 changes: 114 additions & 0 deletions src/font/font.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import isUrl from 'is-url';
import fontkit from '@react-pdf/fontkit';
import fetch from 'cross-fetch';

import { processFontWeight } from '../stylesheet/transformFontWeight';

const fetchFont = async (src, options) => {
const response = await fetch(src, options);

const buffer = await (response.buffer
? response.buffer()
: response.arrayBuffer());

return buffer.constructor.name === 'Buffer' ? buffer : Buffer.from(buffer);
};

const throwInvalidUrl = src => {
throw new Error(
`Invalid font url: ${src}. If you use relative url please replace it with absolute one (ex. /font.ttf -> http://localhost:3000/font.ttf)`,
);
};

class FontSource {
constructor(src, fontFamily, fontStyle, fontWeight, options) {
this.src = src;
this.fontFamily = fontFamily;
this.fontStyle = fontStyle || 'normal';
this.fontWeight = processFontWeight(fontWeight) || 400;

this.data = null;
this.loading = false;
this.options = options;
}

async load() {
if (isUrl(this.src)) {
const { headers, body, method = 'GET' } = this.options;
const data = await fetchFont(this.src, { method, body, headers });
this.data = fontkit.create(data);
} else {
if (BROWSER) throwInvalidUrl(this.src); // Can't load a non-url font in browser

this.data = await new Promise((resolve, reject) =>
fontkit.open(this.src, (err, data) =>
err ? reject(err) : resolve(data),
),
);
}

this.loading = false;
}
}

class Font {
static create(family) {
return new Font(family);
}

constructor(family) {
this.family = family;
this.sources = [];
}

register({ src, fontWeight, fontStyle, ...options }) {
this.sources.push(
new FontSource(src, this.fontFamily, fontStyle, fontWeight, options),
);
}

resolve(descriptor) {
const { fontWeight = 400, fontStyle = 'normal' } = descriptor;
const styleSources = this.sources.filter(s => s.fontStyle === fontStyle);

// Weight resolution. https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Fallback_weights
const exactFit = styleSources.find(s => s.fontWeight === fontWeight);

if (exactFit) return exactFit;

let res;

if (fontWeight >= 400 && fontWeight <= 500) {
const leftOffset = styleSources.filter(s => s.fontWeight <= fontWeight);
const rightOffset = styleSources.filter(s => s.fontWeight > 500);
const fit = styleSources.filter(
s => s.fontWeight >= fontWeight && s.fontWeight < 500,
);

res = fit[0] || leftOffset[leftOffset.length - 1] || rightOffset[0];
}

const lt = styleSources.filter(s => s.fontWeight < fontWeight);
const gt = styleSources.filter(s => s.fontWeight > fontWeight);

if (fontWeight < 400) {
res = lt[lt.length - 1] || gt[0];
}

if (fontWeight > 500) {
res = gt[0] || lt[lt.length - 1];
}

if (!res) {
throw new Error(
`Could not resolve font for ${
this.fontFamily
}, fontWeight ${fontWeight}`,
);
}

return res;
}
}

export default Font;
12 changes: 12 additions & 0 deletions src/font/hyphenation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
let hyphenationCallback;

export const registerHyphenationCallback = callback => {
hyphenationCallback = callback;
};

export const getHyphenationCallback = () => hyphenationCallback;

export default {
registerHyphenationCallback,
getHyphenationCallback,
};
126 changes: 52 additions & 74 deletions src/font/index.js
Original file line number Diff line number Diff line change
@@ -1,97 +1,76 @@
import isUrl from 'is-url';
import fetch from 'cross-fetch';
import fontkit from '@react-pdf/fontkit';

import font from './font';
import emoji from './emoji';
import standardFonts from './standard';
import hyphenation from './hyphenation';
import warning from '../utils/warning';

let fonts = {};
let emojiSource;
let hyphenationCallback;

const fetchFont = async (src, options) => {
const response = await fetch(src, options);
const register = (src, data) => {
if (typeof src === 'object') {
data = src;
} else {
warning(
false,
'Font.register will not longer accept the font source as first argument. Please move it into the data object. For more info refer to https://react-pdf.org/fonts',
);

const buffer = await (response.buffer
? response.buffer()
: response.arrayBuffer());
data.src = src;
}

return buffer.constructor.name === 'Buffer' ? buffer : Buffer.from(buffer);
};
const { family } = data;

const register = (src, { family, ...otherOptions }) => {
fonts[family] = {
src,
loaded: false,
loading: false,
data: null,
...otherOptions,
};
};
if (!fonts[family]) {
fonts[family] = font.create(family);
}

const registerHyphenationCallback = callback => {
hyphenationCallback = callback;
// Bulk loading
if (data.fonts) {
for (let i = 0; i < data.fonts.length; i++) {
fonts[family].register({ family, ...data.fonts[i] });
}
} else {
fonts[family].register(data);
}
};

const registerEmojiSource = ({ url, format = 'png' }) => {
emojiSource = { url, format };
};
const getRegisteredFonts = () => fonts;

const getRegisteredFonts = () => Object.keys(fonts);
const getRegisteredFontFamilies = () => Object.keys(fonts);

const getFont = family => fonts[family];
const getFont = descriptor => {
const { fontFamily } = descriptor;
const isStandard = standardFonts.includes(fontFamily);

const getEmojiSource = () => emojiSource;
if (isStandard) return null;

const getHyphenationCallback = () => hyphenationCallback;
if (!fonts[fontFamily]) {
throw new Error(
`Font family not registered: ${fontFamily}. Please register it calling Font.register() method.`,
);
}

const load = async function(fontFamily, doc) {
const font = getFont(fontFamily);
return fonts[fontFamily].resolve(descriptor);
};

// We cache the font to avoid fetching it many times
if (font && !font.data && !font.loading) {
font.loading = true;

if (isUrl(font.src)) {
const { src, headers, body, method = 'GET' } = font;
const data = await fetchFont(src, { headers, method, body });
font.data = fontkit.create(data);
} else {
if (BROWSER) {
throw new Error(
`Invalid font url: ${
font.src
}. If you use relative url please replace it with absolute one (ex. /font.ttf -> http://localhost:3000/font.ttf)`,
);
}

font.data = await new Promise((resolve, reject) =>
fontkit.open(font.src, (err, data) =>
err ? reject(err) : resolve(data),
),
);
}
}
const load = async function(descriptor, doc) {
const { fontFamily } = descriptor;
const isStandard = standardFonts.includes(fontFamily);

// If the font wasn't added to the document yet (aka. loaded), we add it.
// This prevents calling `registerFont` many times for the same font.
// Fonts loaded state will be reset after the document is closed.
if (font && !font.loaded) {
font.loaded = true;
font.loading = false;
doc.registerFont(fontFamily, font.data);
}
if (isStandard) return;

if (!font && !standardFonts.includes(fontFamily)) {
throw new Error(
`Font family not registered: ${fontFamily}. Please register it calling Font.register() method.`,
);
const font = getFont(descriptor);

// We cache the font to avoid fetching it many times
if (!font.data && !font.loading) {
await font.load();
}
};

const reset = function() {
for (const font in fonts) {
if (fonts.hasOwnProperty(font)) {
fonts[font].loaded = false;
fonts[font].data = null;
}
}
};
Expand All @@ -102,13 +81,12 @@ const clear = function() {

export default {
register,
getEmojiSource,
getRegisteredFonts,
registerEmojiSource,
registerHyphenationCallback,
getHyphenationCallback,
getRegisteredFontFamilies,
getFont,
load,
clear,
reset,
...emoji,
...hyphenation,
};