Skip to content

Commit cef988d

Browse files
authored
feat(PDF): New sample for using a custom font (#3895)
1 parent 5c66046 commit cef988d

File tree

7 files changed

+460
-0
lines changed

7 files changed

+460
-0
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<section class="container">
2+
<p class="description">
3+
This sample ships with <a href="https://fonts.google.com/noto/specimen/Noto+Sans" target="_blank">Noto Sans</a>,
4+
which covers Latin, Cyrillic, and Greek characters. For <strong>Japanese, Chinese, or Korean</strong> support,
5+
download <a href="https://fonts.google.com/noto/specimen/Noto+Sans+JP" target="_blank">Noto Sans JP</a>
6+
(or another CJK font) and upload the <code>.ttf</code> file below.
7+
</p>
8+
9+
@if (builtInFontLoading()) {
10+
<p class="status loading">Loading built-in Noto Sans font…</p>
11+
}
12+
13+
<div class="font-upload">
14+
<label for="fontFile">Upload a custom font (.ttf):</label>
15+
<input
16+
id="fontFile"
17+
type="file"
18+
accept=".ttf,.otf,.woff"
19+
(change)="onFontFileSelected($event)"
20+
/>
21+
@if (uploadedFontName()) {
22+
<span class="file-name">{{ uploadedFontName() }}</span>
23+
}
24+
</div>
25+
26+
<div class="export-buttons">
27+
<button
28+
(click)="exportWithBuiltInFont()"
29+
[disabled]="!canExportBuiltIn()"
30+
>
31+
Export with Noto Sans
32+
</button>
33+
<button
34+
(click)="exportWithUploadedFont()"
35+
[disabled]="!canExportUploaded()"
36+
>
37+
Export with Uploaded Font
38+
</button>
39+
<button
40+
(click)="exportWithDefaultFont()"
41+
[disabled]="isExporting()"
42+
>
43+
Export with Default Font
44+
</button>
45+
</div>
46+
47+
@if (exportStatus()) {
48+
<p class="status">{{ exportStatus() }}</p>
49+
}
50+
51+
<igx-grid #grid [data]="data()" [autoGenerate]="false" height="320px">
52+
<igx-column field="Name" header="Name / Име / 名前"></igx-column>
53+
<igx-column field="City" header="City / Град / 都市"></igx-column>
54+
<igx-column field="Product" header="Product / Продукт / 製品"></igx-column>
55+
<igx-column field="Amount" header="Amount" dataType="number"></igx-column>
56+
</igx-grid>
57+
</section>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
.container {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 12px;
5+
padding: 16px;
6+
}
7+
8+
.description {
9+
color: #555;
10+
max-width: 800px;
11+
12+
a {
13+
color: #0078d4;
14+
text-decoration: none;
15+
16+
&:hover {
17+
text-decoration: underline;
18+
}
19+
}
20+
}
21+
22+
.font-upload {
23+
display: flex;
24+
align-items: center;
25+
gap: 8px;
26+
27+
input[type="file"] {
28+
// Hide the default input
29+
position: absolute;
30+
opacity: 0;
31+
width: 0.1px;
32+
height: 0.1px;
33+
}
34+
35+
label {
36+
font-weight: 500;
37+
padding: 10px 20px;
38+
background-color: #fff;
39+
border: 2px dashed #0078d4;
40+
border-radius: 6px;
41+
cursor: pointer;
42+
color: #0078d4;
43+
transition: all 0.3s ease;
44+
display: inline-block;
45+
46+
&:hover {
47+
background-color: #0078d4;
48+
color: white;
49+
border-style: solid;
50+
transform: translateY(-1px);
51+
box-shadow: 0 2px 8px rgba(0, 120, 212, 0.3);
52+
}
53+
54+
&:active {
55+
transform: translateY(0);
56+
}
57+
}
58+
59+
.file-name {
60+
color: #28a745;
61+
font-size: 0.9em;
62+
font-weight: 500;
63+
padding: 6px 12px;
64+
background-color: #e8f5e9;
65+
border-radius: 4px;
66+
border: 1px solid #28a745;
67+
}
68+
}
69+
70+
.export-buttons {
71+
display: flex;
72+
flex-wrap: wrap;
73+
gap: 12px;
74+
75+
button {
76+
padding: 8px 16px;
77+
border: 1px solid #ccc;
78+
border-radius: 4px;
79+
cursor: pointer;
80+
font-size: 14px;
81+
82+
&:not(:disabled):hover {
83+
background-color: #e8e8e8;
84+
}
85+
86+
&:disabled {
87+
opacity: 0.5;
88+
cursor: not-allowed;
89+
}
90+
91+
&:first-child {
92+
background-color: #0078d4;
93+
color: white;
94+
border-color: #0078d4;
95+
96+
&:not(:disabled):hover {
97+
background-color: #106ebe;
98+
}
99+
}
100+
}
101+
}
102+
103+
.status {
104+
padding: 8px 12px;
105+
background-color: #f0f0f0;
106+
border-radius: 4px;
107+
font-size: 0.9em;
108+
109+
&.loading {
110+
color: #0078d4;
111+
font-style: italic;
112+
}
113+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { ChangeDetectionStrategy, Component, signal, computed, inject, viewChild, OnInit } from '@angular/core';
2+
import { IgxColumnComponent, IgxPdfExporterService, IgxPdfExporterOptions } from 'igniteui-angular/grids/core';
3+
import { IgxGridComponent } from 'igniteui-angular/grids/grid';
4+
5+
/**
6+
* Demonstrates PDF export with a custom Unicode font.
7+
*
8+
* The sample ships with Noto Sans (Latin/Cyrillic/Greek) loaded from
9+
* assets/fonts/noto-sans.json. Users can also upload their own .ttf font —
10+
* for example Noto Sans CJK for Japanese/Chinese/Korean support.
11+
*
12+
* All Noto fonts are licensed under the SIL Open Font License 1.1
13+
* (see assets/fonts/OFL.txt).
14+
*/
15+
@Component({
16+
selector: 'app-export-pdf-custom-font',
17+
templateUrl: './export-pdf-custom-font.component.html',
18+
styleUrls: ['./export-pdf-custom-font.component.scss'],
19+
changeDetection: ChangeDetectionStrategy.OnPush,
20+
imports: [IgxGridComponent, IgxColumnComponent]
21+
})
22+
export class ExportPdfCustomFontComponent implements OnInit {
23+
private pdfExporter = inject(IgxPdfExporterService);
24+
25+
protected readonly grid = viewChild.required<IgxGridComponent>('grid');
26+
27+
protected readonly isExporting = signal(false);
28+
protected readonly builtInFontLoaded = signal(false);
29+
protected readonly builtInFontLoading = signal(false);
30+
protected readonly uploadedFontName = signal('');
31+
protected readonly exportStatus = signal('');
32+
33+
// Built-in Noto Sans (Latin / Cyrillic / Greek)
34+
private builtInFontData: string | null = null;
35+
private builtInBoldFontData: string | null = null;
36+
37+
// User-uploaded font (e.g. Noto Sans CJK for Japanese)
38+
private uploadedFontData = signal<string | null>(null);
39+
40+
protected readonly canExportBuiltIn = computed(() => this.builtInFontLoaded() && !this.isExporting());
41+
protected readonly canExportUploaded = computed(() => !!this.uploadedFontData() && !this.isExporting());
42+
43+
protected readonly data = signal([
44+
{ Name: 'Александър Иванов', City: 'София', Product: '商品A', Amount: 1500 },
45+
{ Name: '田中太郎', City: '東京', Product: '商品B', Amount: 2300 },
46+
{ Name: 'Élise Müller', City: 'München', Product: '商品D', Amount: 3200 },
47+
{ Name: '王小明', City: '北京', Product: '商品E', Amount: 1750 },
48+
{ Name: 'Ирина Петрова', City: 'Санкт-Петербург', Product: '製品 F', Amount: 2890 }
49+
]);
50+
51+
public ngOnInit(): void {
52+
this.loadBuiltInFont();
53+
}
54+
55+
/** Loads the built-in Noto Sans font from application assets. */
56+
private async loadBuiltInFont(): Promise<void> {
57+
this.builtInFontLoading.set(true);
58+
this.exportStatus.set('Loading built-in Noto Sans font…');
59+
60+
try {
61+
const response = await fetch('assets/fonts/noto-sans.json');
62+
if (!response.ok) {
63+
throw new Error(`HTTP ${response.status}`);
64+
}
65+
const fontJson: { normal: string; bold: string } = await response.json();
66+
67+
this.builtInFontData = fontJson.normal;
68+
this.builtInBoldFontData = fontJson.bold;
69+
this.builtInFontLoaded.set(true);
70+
this.exportStatus.set('Noto Sans loaded — ready to export. Upload a CJK font for Japanese/Chinese/Korean support.');
71+
} catch (error) {
72+
console.error('Failed to load built-in font:', error);
73+
this.exportStatus.set('Failed to load built-in Noto Sans font.');
74+
} finally {
75+
this.builtInFontLoading.set(false);
76+
}
77+
}
78+
79+
/** Handles the user uploading a custom .ttf font file. */
80+
protected onFontFileSelected(event: Event): void {
81+
const input = event.target as HTMLInputElement;
82+
if (!input.files?.[0]) {
83+
return;
84+
}
85+
const file = input.files[0];
86+
this.uploadedFontName.set(file.name);
87+
this.exportStatus.set(`Reading "${file.name}"…`);
88+
89+
this.readFontFile(file).then(base64 => {
90+
this.uploadedFontData.set(base64);
91+
this.exportStatus.set(`"${file.name}" loaded — you can now export with the uploaded font.`);
92+
});
93+
}
94+
95+
/** Export using the built-in Noto Sans font. */
96+
protected exportWithBuiltInFont(): void {
97+
if (!this.builtInFontData) {
98+
return;
99+
}
100+
101+
this.isExporting.set(true);
102+
this.exportStatus.set('Exporting PDF with Noto Sans…');
103+
104+
const options = new IgxPdfExporterOptions('NotoSansExport');
105+
options.customFont = {
106+
name: 'NotoSans',
107+
data: this.builtInFontData
108+
};
109+
110+
if (this.builtInBoldFontData) {
111+
options.customFont.bold = {
112+
name: 'NotoSans-Bold',
113+
data: this.builtInBoldFontData
114+
};
115+
}
116+
117+
this.pdfExporter.exportEnded.subscribe({
118+
next: () => {
119+
this.isExporting.set(false);
120+
this.exportStatus.set(
121+
'PDF exported with Noto Sans. Note: CJK characters (Japanese, Chinese, Korean) require a CJK font.'
122+
);
123+
}
124+
});
125+
126+
this.pdfExporter.export(this.grid(), options);
127+
}
128+
129+
/** Export using the user-uploaded font file. */
130+
protected exportWithUploadedFont(): void {
131+
const fontData = this.uploadedFontData();
132+
if (!fontData) {
133+
return;
134+
}
135+
136+
this.isExporting.set(true);
137+
this.exportStatus.set('Exporting PDF with uploaded font…');
138+
139+
const options = new IgxPdfExporterOptions('CustomFontExport');
140+
options.customFont = {
141+
name: 'CustomFont',
142+
data: fontData
143+
};
144+
145+
this.pdfExporter.exportEnded.subscribe({
146+
next: () => {
147+
this.isExporting.set(false);
148+
this.exportStatus.set('PDF exported successfully with the uploaded font!');
149+
}
150+
});
151+
152+
this.pdfExporter.export(this.grid(), options);
153+
}
154+
155+
/** Export with the default PDF font (Helvetica). */
156+
protected exportWithDefaultFont(): void {
157+
this.isExporting.set(true);
158+
this.exportStatus.set('Exporting PDF with default font (Helvetica)…');
159+
160+
const options = new IgxPdfExporterOptions('DefaultFontExport');
161+
162+
this.pdfExporter.exportEnded.subscribe({
163+
next: () => {
164+
this.isExporting.set(false);
165+
this.exportStatus.set(
166+
'PDF exported — non-Latin characters may not render correctly with the default Helvetica font.'
167+
);
168+
}
169+
});
170+
171+
this.pdfExporter.export(this.grid(), options);
172+
}
173+
174+
private readFontFile(file: File): Promise<string> {
175+
return new Promise((resolve, reject) => {
176+
const reader = new FileReader();
177+
reader.onload = () => {
178+
const result = reader.result as string;
179+
const base64 = result.includes(',') ? result.split(',')[1] : result;
180+
resolve(base64);
181+
};
182+
reader.onerror = () => reject(reader.error);
183+
reader.readAsDataURL(file);
184+
});
185+
}
186+
}

src/app/services/services-routes-data.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const servicesRoutesData = {
66
'export-excel-sample-1': { displayName: 'Excel Export Grid', parentName: 'Excel Export' },
77
'export-excel-tree-grid-sample': { displayName: 'Excel Export TreeGrid', parentName: 'Excel Export' },
88
'export-pdf': { displayName: 'PDF Export Raw Data', parentName: 'PDF Export' },
9+
'export-pdf-custom-font': { displayName: 'PDF Export with Custom Font', parentName: 'PDF Export' },
910
'localization-sample-1': { displayName: 'Localize one component', parentName: 'Localization' },
1011
'localization-sample-2': { displayName: 'Localize All', parentName: 'Localization' },
1112
'localization-sample-3': { displayName: 'Localize partially', parentName: 'Localization' },

src/app/services/services.routes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { servicesRoutesData } from './services-routes-data';
1414
import { TransactionBaseComponent } from './transaction/transaction-base/transaction-base.component';
1515
import { PdfExportComponent } from './export-pdf/pdf-export.component';
1616
import { LocalizationAllResourcesComponent } from './localization-samples/localization-all-resources/localization-all-resources.component';
17+
import { ExportPdfCustomFontComponent } from './export-pdf-custom-font/export-pdf-custom-font.component';
1718
// tslint:enable:max-line-length
1819

1920
export const ServicesRoutes: Routes = [
@@ -47,6 +48,11 @@ export const ServicesRoutes: Routes = [
4748
data: servicesRoutesData['export-pdf'],
4849
path: 'export-pdf'
4950
},
51+
{
52+
component: ExportPdfCustomFontComponent,
53+
data: servicesRoutesData['export-pdf-custom-font'],
54+
path: 'export-pdf-custom-font'
55+
},
5056
{
5157
component: TreeGridExcelExportSample1Component,
5258
data: servicesRoutesData['export-excel-tree-grid-sample'],

0 commit comments

Comments
 (0)