Add PDF custom fonts

This commit is contained in:
Sean Engelhardt 2019-01-22 11:59:16 +01:00
parent 209105efc3
commit e680ca38da
16 changed files with 248 additions and 38 deletions

View File

@ -51,8 +51,6 @@
"po2json": "^1.0.0-alpha", "po2json": "^1.0.0-alpha",
"rxjs": "^6.3.3", "rxjs": "^6.3.3",
"tinymce": "^4.9.0", "tinymce": "^4.9.0",
"typeface-fira-sans": "^0.0.54",
"typeface-fira-sans-condensed": "^0.0.54",
"uuid": "^3.3.2", "uuid": "^3.3.2",
"zone.js": "^0.8.26" "zone.js": "^0.8.26"
}, },

View File

@ -51,12 +51,12 @@ export class ConfigService extends OpenSlidesComponent {
* *
* @param key The config value to get from. * @param key The config value to get from.
*/ */
public instant(key: string): any { public instant<T>(key: string): T {
const values = this.DS.filter<Config>('core/config', value => value.key === key); const values = this.DS.filter<Config>('core/config', value => value.key === key);
if (values.length > 1) { if (values.length > 1) {
throw new Error('More keys found then expected'); throw new Error('More keys found then expected');
} else if (values.length === 1) { } else if (values.length === 1) {
return values[0].value; return values[0].value as T;
} else { } else {
return; return;
} }

View File

@ -53,6 +53,7 @@ export class HttpService {
* @param data optional, if sending a data body is required * @param data optional, if sending a data body is required
* @param queryParams optional queryparams to append to the path * @param queryParams optional queryparams to append to the path
* @param customHeader optional custom HTTP header of required * @param customHeader optional custom HTTP header of required
* @param responseType optional response type, default set to json (i.e 'arraybuffer')
* @returns a promise containing a generic * @returns a promise containing a generic
*/ */
private async send<T>( private async send<T>(
@ -60,21 +61,26 @@ export class HttpService {
method: HTTPMethod, method: HTTPMethod,
data?: any, data?: any,
queryParams?: QueryParams, queryParams?: QueryParams,
customHeader?: HttpHeaders customHeader?: HttpHeaders,
responseType?: string
): Promise<T> { ): Promise<T> {
// end early, if we are in history mode // end early, if we are in history mode
if (this.OSStatus.isInHistoryMode && method !== HTTPMethod.GET) { if (this.OSStatus.isInHistoryMode && method !== HTTPMethod.GET) {
throw this.handleError('You cannot make changes while in history mode'); throw this.handleError('You cannot make changes while in history mode');
} }
if (!path.endsWith('/')) { // there is a current bug with the responseType.
path += '/'; // https://github.com/angular/angular/issues/18586
// castting it to 'json' allows the usage of the current array
if (!responseType) {
responseType = 'json';
} }
const url = path + formatQueryParams(queryParams); const url = path + formatQueryParams(queryParams);
const options = { const options = {
body: data, body: data,
headers: customHeader ? customHeader : this.defaultHeaders headers: customHeader ? customHeader : this.defaultHeaders,
responseType: responseType as 'json'
}; };
try { try {
@ -149,10 +155,17 @@ export class HttpService {
* @param data An optional payload for the request. * @param data An optional payload for the request.
* @param queryParams Optional params appended to the path as the query part of the url. * @param queryParams Optional params appended to the path as the query part of the url.
* @param header optional HTTP header if required * @param header optional HTTP header if required
* @param responseType option expected response type by the request (i.e 'arraybuffer')
* @returns A promise holding a generic * @returns A promise holding a generic
*/ */
public async get<T>(path: string, data?: any, queryParams?: QueryParams, header?: HttpHeaders): Promise<T> { public async get<T>(
return await this.send<T>(path, HTTPMethod.GET, data, queryParams, header); path: string,
data?: any,
queryParams?: QueryParams,
header?: HttpHeaders,
responseType?: string
): Promise<T> {
return await this.send<T>(path, HTTPMethod.GET, data, queryParams, header, responseType);
} }
/** /**

View File

@ -1,11 +1,12 @@
import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import * as pdfMake from 'pdfmake/build/pdfmake'; import pdfMake from 'pdfmake/build/pdfmake';
import * as pdfFonts from 'pdfmake/build/vfs_fonts';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ConfigService } from './config.service'; import { ConfigService } from './config.service';
import { HttpService } from './http.service';
/** /**
* TODO: Images and fonts * TODO: Images and fonts
@ -31,10 +32,84 @@ export class PdfDocumentService {
* *
* @param translate translations * @param translate translations
* @param configService read config values * @param configService read config values
* @param mediaManageService to read out font files as media data
*/ */
public constructor(private translate: TranslateService, private configService: ConfigService) { public constructor(
// It should be possible to add own fonts here private translate: TranslateService,
pdfMake.vfs = pdfFonts.pdfMake.vfs; private configService: ConfigService,
private httpService: HttpService
) {}
/**
* Define the pdfmake virtual file system for fonts
*
* @returns the vfs-object
*/
private async getVfs(): Promise<object> {
const fontPathList: string[] = Array.from(
// create a list without redundancies
new Set(
this.configService
.instant<string[]>('fonts_available')
.map(available => this.configService.instant<any>(available))
.map(font => font.path || `/${font.default}`)
)
);
const promises = fontPathList.map(fontPath => {
return this.convertUrlToBase64(fontPath).then(base64 => {
return {
[fontPath.split('/').pop()]: base64.split(',')[1]
};
});
});
const fontDataUrls = await Promise.all(promises);
let vfs = {};
fontDataUrls.map(entry => {
vfs = {
...vfs,
...entry
};
});
return vfs;
}
/**
* Converts a given blob to base64
*
* @param file File as blob
* @returns a promise to the base64 as string
*/
private async convertUrlToBase64(url: string): Promise<string> {
const headers = new HttpHeaders();
const file = await this.httpService.get<ArrayBuffer>(url, {}, {}, headers, 'arraybuffer');
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(new Blob([file]));
reader.onload = () => {
const resultStr: string = reader.result as string;
resolve(resultStr);
};
reader.onerror = error => {
reject(error);
};
}) as Promise<string>;
}
/**
* Returns the name of a font from the value of the given
* config variable.
*
* @param fontType the config variable of the font (font_regular, font_italic)
* @returns the name of the selected font
*/
private getFontName(fontType: string): string {
const font = this.configService.instant<any>(fontType);
return (font.path || `/${font.default}`).split('/').pop();
} }
/** /**
@ -43,16 +118,25 @@ export class PdfDocumentService {
* @param documentContent the content of the pdf as object * @param documentContent the content of the pdf as object
* @returns the pdf document definition ready to export * @returns the pdf document definition ready to export
*/ */
private getStandardPaper(documentContent: object, metadata?: object): object { private async getStandardPaper(documentContent: object, metadata?: object): Promise<object> {
const standardFontSize = this.configService.instant('general_export_pdf_fontsize'); // define the fonts
pdfMake.fonts = {
PdfFont: {
normal: this.getFontName('font_regular'),
bold: this.getFontName('font_bold'),
italics: this.getFontName('font_italic'),
bolditalics: this.getFontName('font_bold_italic')
}
};
pdfMake.vfs = await this.getVfs();
return { return {
pageSize: 'A4', pageSize: 'A4',
pageMargins: [75, 90, 75, 75], pageMargins: [75, 90, 75, 75],
defaultStyle: { defaultStyle: {
// TODO add fonts to vfs font: 'PdfFont',
// font: 'PdfFont', fontSize: this.configService.instant('general_export_pdf_fontsize')
fontSize: standardFontSize
}, },
header: this.getHeader(), header: this.getHeader(),
// TODO: option for no footer, wherever this can be defined // TODO: option for no footer, wherever this can be defined
@ -73,8 +157,8 @@ export class PdfDocumentService {
*/ */
private getHeader(): object { private getHeader(): object {
// check for the required logos // check for the required logos
let logoHeaderLeftUrl = this.configService.instant('logo_pdf_header_L').path; let logoHeaderLeftUrl = this.configService.instant<any>('logo_pdf_header_L').path;
let logoHeaderRightUrl = this.configService.instant('logo_pdf_header_R').path; let logoHeaderRightUrl = this.configService.instant<any>('logo_pdf_header_R').path;
let text; let text;
const columns = []; const columns = [];
@ -151,8 +235,8 @@ export class PdfDocumentService {
let logoContainerWidth: string; let logoContainerWidth: string;
let pageNumberPosition: string; let pageNumberPosition: string;
let logoConteinerSize: Array<number>; let logoConteinerSize: Array<number>;
let logoFooterLeftUrl = this.configService.instant('logo_pdf_footer_L').path; let logoFooterLeftUrl = this.configService.instant<any>('logo_pdf_footer_L').path;
let logoFooterRightUrl = this.configService.instant('logo_pdf_footer_R').path; let logoFooterRightUrl = this.configService.instant<any>('logo_pdf_footer_R').path;
// if there is a single logo, give it a lot of space // if there is a single logo, give it a lot of space
if (logoFooterLeftUrl && logoFooterRightUrl) { if (logoFooterLeftUrl && logoFooterRightUrl) {
@ -229,9 +313,9 @@ export class PdfDocumentService {
* @param docDefinition the structure of the PDF document * @param docDefinition the structure of the PDF document
*/ */
public download(docDefinition: object, filename: string, metadata?: object): void { public download(docDefinition: object, filename: string, metadata?: object): void {
pdfMake this.getStandardPaper(docDefinition, metadata).then(doc => {
.createPdf(this.getStandardPaper(docDefinition, metadata)) pdfMake.createPdf(doc).getBlob(blob => saveAs(blob, `${filename}.pdf`, { autoBOM: true }));
.getBlob(blob => saveAs(blob, `${filename}.pdf`, { autoBOM: true })); });
} }
/** /**

View File

@ -58,7 +58,7 @@ export class MediaManageService {
* @param action determines the action * @param action determines the action
*/ */
public async setAs(file: ViewMediafile, action: string): Promise<void> { public async setAs(file: ViewMediafile, action: string): Promise<void> {
const restPath = `rest/core/config/${action}`; const restPath = `rest/core/config/${action}/`;
const config = this.getMediaConfig(action); const config = this.getMediaConfig(action);
const path = config.path !== file.downloadUrl ? file.downloadUrl : ''; const path = config.path !== file.downloadUrl ? file.downloadUrl : '';

View File

@ -322,7 +322,7 @@ export class MotionPdfService {
changes.sort((a, b) => a.getLineFrom() - b.getLineFrom()); changes.sort((a, b) => a.getLineFrom() - b.getLineFrom());
// get the line length from the config // get the line length from the config
const lineLength = this.configService.instant('motions_line_length'); const lineLength = this.configService.instant<number>('motions_line_length');
motionText = this.motionRepo.formatMotion(motion.id, crMode, changes, lineLength); motionText = this.motionRepo.formatMotion(motion.id, crMode, changes, lineLength);
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,22 @@
/** Define font variables */
/** Default Font */
$font: 'OSFont';
$font-regular: url('assets/fonts/fira-sans-latin-400.woff') format('woff');
$font-weight-regular: 400;
$font-italic: url('assets/fonts/fira-sans-latin-400italic.woff') format('woff');
$font-weight-italic: 400;
$font-medium: url('assets/fonts/fira-sans-latin-500.woff') format('woff');
$font-weight-medium: 500;
$font-medium-italic: url('assets/fonts/fira-sans-latin-500italic.woff') format('woff');
$font-weight-medium-italic: 500;
/** Condensed Font */
$font-condensed: 'OSFont Condensed';
$font-condensed-regular: url('assets/fonts/fira-sans-condensed-latin-400.woff') format('woff');
$font-weight-condensed-regular: 400;

View File

@ -0,0 +1,46 @@
@import './font-variables.scss';
/** Regular */
@font-face {
font-family: $font;
font-style: normal;
font-display: swap;
font-weight: $font-weight-regular;
src: $font-regular;
}
/** Italic */
@font-face {
font-family: $font;
font-style: italic;
font-display: swap;
font-weight: $font-weight-italic;
src: $font-italic;
}
/** Medium */
@font-face {
font-family: $font;
font-style: normal;
font-display: swap;
font-weight: $font-weight-medium;
src: $font-medium;
}
/** Medium Italic */
@font-face {
font-family: $font;
font-style: italic;
font-display: swap;
font-weight: $font-weight-medium-italic;
src: $font-medium-italic;
}
/** Condensed Regular */
@font-face {
font-family: $font-condensed;
font-style: normal;
font-display: swap;
font-weight: $font-weight-condensed-regular;
src: $font-condensed-regular;
}

View File

@ -4,8 +4,10 @@
/** Import brand theme and (new) component themes */ /** Import brand theme and (new) component themes */
@import './assets/styles/openslides-theme.scss'; @import './assets/styles/openslides-theme.scss';
@import './app/site/site.component.scss-theme'; @import './app/site/site.component.scss-theme';
@import '../node_modules/typeface-fira-sans/index.css';
@import '../node_modules/typeface-fira-sans-condensed/index.css'; /** fonts */
@import './assets/styles/fonts.scss';
@mixin openslides-components-theme($theme) { @mixin openslides-components-theme($theme) {
@include os-site-theme($theme); @include os-site-theme($theme);
/** More components are added here */ /** More components are added here */
@ -17,10 +19,10 @@
@import '~angular-tree-component/dist/angular-tree-component.css'; @import '~angular-tree-component/dist/angular-tree-component.css';
* { * {
font-family: Fira Sans, Roboto, Arial, Helvetica, sans-serif; font-family: OSFont, Fira Sans, Roboto, Arial, Helvetica, sans-serif;
} }
.mat-toolbar h2 { .mat-toolbar h2 {
font-family: Fira Sans, Roboto, Arial, Helvetica, sans-serif !important; font-family: OSFont, Fira Sans, Roboto, Arial, Helvetica, sans-serif !important;
} }
mat-icon { mat-icon {
@ -40,7 +42,7 @@ h1,
h2, h2,
h3, h3,
.title-font { .title-font {
font-family: Fira Sans Condensed, Roboto-condensed, Arial, Helvetica, sans-serif; font-family: OSFont Condensed, Fira Sans Condensed, Roboto-condensed, Arial, Helvetica, sans-serif;
} }
h1 { h1 {

View File

@ -362,7 +362,7 @@ def get_config_variables():
name="font_regular", name="font_regular",
default_value={ default_value={
"display_name": "Font regular", "display_name": "Font regular",
"default": "static/fonts/Roboto-Regular.woff", "default": "assets/fonts/fira-sans-latin-400.woff",
"path": "", "path": "",
}, },
input_type="static", input_type="static",
@ -375,7 +375,7 @@ def get_config_variables():
name="font_italic", name="font_italic",
default_value={ default_value={
"display_name": "Font italic", "display_name": "Font italic",
"default": "static/fonts/Roboto-Medium.woff", "default": "assets/fonts/fira-sans-latin-400italic.woff",
"path": "", "path": "",
}, },
input_type="static", input_type="static",
@ -388,7 +388,7 @@ def get_config_variables():
name="font_bold", name="font_bold",
default_value={ default_value={
"display_name": "Font bold", "display_name": "Font bold",
"default": "static/fonts/Roboto-Condensed-Regular.woff", "default": "assets/fonts/fira-sans-latin-500.woff",
"path": "", "path": "",
}, },
input_type="static", input_type="static",
@ -401,7 +401,7 @@ def get_config_variables():
name="font_bold_italic", name="font_bold_italic",
default_value={ default_value={
"display_name": "Font bold italic", "display_name": "Font bold italic",
"default": "static/fonts/Roboto-Condensed-Light.woff", "default": "assets/fonts/fira-sans-latin-500italic.woff",
"path": "", "path": "",
}, },
input_type="static", input_type="static",

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
from openslides.core.config import config
def change_font_default_path(apps, schema_editor):
"""
Writes the new default font path into the database.
"""
ConfigStore = apps.get_model("core", "ConfigStore")
try:
font_regular = ConfigStore.objects.get(key="font_regular")
font_italic = ConfigStore.objects.get(key="font_italic")
font_bold = ConfigStore.objects.get(key="font_bold")
font_bold_italic = ConfigStore.objects.get(key="font_bold_italic")
except ConfigStore.DoesNotExist:
return # The key is not in the database, nothing to change here
default_font_regular = config.config_variables["font_regular"].default_value
default_font_italic = config.config_variables["font_italic"].default_value
default_font_bold = config.config_variables["font_bold"].default_value
default_font_bold_italic = config.config_variables["font_bold_italic"].default_value
font_regular.value = default_font_regular
font_italic.value = default_font_italic
font_bold.value = default_font_bold
font_bold_italic.value = default_font_bold_italic
font_regular.save()
font_italic.save()
font_bold.save()
font_bold_italic.save()
class Migration(migrations.Migration):
dependencies = [("core", "0013_auto_20190119_1641")]
operations = [
migrations.RunPython(change_font_default_path),
]