diff --git a/CHANGELOG b/CHANGELOG
index c09196ef0..7f2089666 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -112,6 +112,7 @@ Core:
- Set default of projector resolution to 1220x915 [#2549].
- Preparations for the SAML plugin; Fixed caching of main views [#3535].
- Removed unnecessary OPTIONS request in config [#3541].
+- Added possibility to upload custom fonts for projector and pdf [#3568].
Mediafiles:
- Fixed reloading of PDF on page change [#3274].
diff --git a/gulpfile.js b/gulpfile.js
index 4f87b0666..12432640a 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -80,9 +80,9 @@ gulp.task('pdf-worker', function () {
gulp.task('pdf-worker-libs', function () {
return gulp.src([
path.join('bower_components', 'pdfmake', 'build', 'pdfmake.min.js'),
- path.join('bower_components', 'pdfmake', 'build', 'vfs_fonts.js'),
])
- .pipe(concat('pdf-worker-libs.js'))
+ .pipe(gulpif(argv.production, uglify()))
+ .pipe(rename('pdf-worker-libs.js'))
.pipe(gulp.dest(path.join(output_directory, 'js', 'workers')));
});
diff --git a/openslides/core/config.py b/openslides/core/config.py
index 3d29ebf6e..ab1d6921c 100644
--- a/openslides/core/config.py
+++ b/openslides/core/config.py
@@ -19,7 +19,7 @@ INPUT_TYPE_MAPPING = {
'colorpicker': str,
'datetimepicker': int,
'majorityMethod': str,
- 'logo': dict,
+ 'static': dict,
'translations': list,
}
@@ -125,9 +125,9 @@ class ConfigHandler:
valuecopy[id] = commentsfield
value = valuecopy
- if config_variable.input_type == 'logo':
+ if config_variable.input_type == 'static':
if not isinstance(value, dict):
- raise ConfigError(_('logo has to be a dict.'))
+ raise ConfigError(_('This has to be a dict.'))
whitelist = (
'path',
'display_name',
diff --git a/openslides/core/config_variables.py b/openslides/core/config_variables.py
index 8d97ef8f2..843ed6b2b 100644
--- a/openslides/core/config_variables.py
+++ b/openslides/core/config_variables.py
@@ -259,7 +259,7 @@ def get_config_variables():
default_value={
'display_name': 'Projector logo',
'path': ''},
- input_type='logo',
+ input_type='static',
weight=301,
group='Logo',
hidden=True)
@@ -269,7 +269,7 @@ def get_config_variables():
default_value={
'display_name': 'Projector header image',
'path': ''},
- input_type='logo',
+ input_type='static',
weight=302,
group='Logo',
hidden=True)
@@ -279,7 +279,7 @@ def get_config_variables():
default_value={
'display_name': 'Web interface header logo',
'path': ''},
- input_type='logo',
+ input_type='static',
weight=303,
group='Logo',
hidden=True)
@@ -290,7 +290,7 @@ def get_config_variables():
default_value={
'display_name': 'PDF header logo',
'path': ''},
- input_type='logo',
+ input_type='static',
weight=310,
group='Logo',
hidden=True)
@@ -300,7 +300,7 @@ def get_config_variables():
default_value={
'display_name': 'PDF footer logo',
'path': ''},
- input_type='logo',
+ input_type='static',
weight=311,
group='Logo',
hidden=True)
@@ -310,11 +310,67 @@ def get_config_variables():
default_value={
'display_name': 'PDF ballot paper logo',
'path': ''},
- input_type='logo',
+ input_type='static',
weight=312,
group='Logo',
hidden=True)
+ # Fonts
+ yield ConfigVariable(
+ name='fonts_available',
+ default_value=[
+ 'font_regular',
+ 'font_italic',
+ 'font_bold',
+ 'font_bold_italic'],
+ weight=320,
+ group='Font',
+ hidden=True)
+
+ yield ConfigVariable(
+ name='font_regular',
+ default_value={
+ 'display_name': 'Font regular',
+ 'default': 'static/fonts/Roboto-Regular.woff',
+ 'path': ''},
+ input_type='static',
+ weight=321,
+ group='Font',
+ hidden=True)
+
+ yield ConfigVariable(
+ name='font_italic',
+ default_value={
+ 'display_name': 'Font italic',
+ 'default': 'static/fonts/Roboto-Medium.woff',
+ 'path': ''},
+ input_type='static',
+ weight=321,
+ group='Font',
+ hidden=True)
+
+ yield ConfigVariable(
+ name='font_bold',
+ default_value={
+ 'display_name': 'Font bold',
+ 'default': 'static/fonts/Roboto-Condensed-Regular.woff',
+ 'path': ''},
+ input_type='static',
+ weight=321,
+ group='Font',
+ hidden=True)
+
+ yield ConfigVariable(
+ name='font_bold_italic',
+ default_value={
+ 'display_name': 'Font bold italic',
+ 'default': 'static/fonts/Roboto-Condensed-Light.woff',
+ 'path': ''},
+ input_type='static',
+ weight=321,
+ group='Font',
+ hidden=True)
+
# Custom translations
yield ConfigVariable(
name='translations',
diff --git a/openslides/core/migrations/0007_auto_20180130_1400.py b/openslides/core/migrations/0007_auto_20180130_1400.py
new file mode 100644
index 000000000..f685beed4
--- /dev/null
+++ b/openslides/core/migrations/0007_auto_20180130_1400.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.8 on 2018-01-30 13:00
+from __future__ import unicode_literals
+
+from django.contrib.auth.models import Permission
+from django.db import migrations
+
+
+def delete_old_logo_permission(apps, schema_editor):
+ """
+ Deletes the old 'can_manage_logo' permission which is replaced with
+ 'can_manage_logos_and_fonts'. If this is a fresh database, no permission
+ will be deleted, in fact the old permission does not exist. Django creates
+ the permission after all migration and the old one is not generated.
+ If this is an old database, the new permission will be created and the old
+ one deleted. Also it will be assigned to the groups, which had the old permission.
+ """
+ perm = Permission.objects.filter(codename='can_manage_logos')
+
+ if len(perm):
+ perm = perm.get()
+ # Save content_type for manual creation of new permissions.
+ content_type = perm.content_type
+
+ # Save groups. list() is necessary to evaluate the database query right now.
+ groups = list(perm.group_set.all())
+
+ # Delete permission
+ perm.delete()
+
+ # Create new permission
+ perm = Permission.objects.create(
+ codename='can_manage_logos_and_fonts',
+ name='Can manage logos and fonts',
+ content_type=content_type)
+
+ for group in groups:
+ group.permissions.add(perm)
+ group.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0006_auto_20180123_0903'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='configstore',
+ options={
+ 'default_permissions': (),
+ 'permissions': (
+ ('can_manage_config', 'Can manage configuration'),
+ ('can_manage_logos_and_fonts', 'Can manage logos and fonts')
+ )
+ },
+ ),
+ migrations.RunPython(
+ delete_old_logo_permission
+ ),
+ ]
diff --git a/openslides/core/models.py b/openslides/core/models.py
index abb0c09c1..a186962a1 100644
--- a/openslides/core/models.py
+++ b/openslides/core/models.py
@@ -281,7 +281,7 @@ class ConfigStore(RESTModelMixin, models.Model):
default_permissions = ()
permissions = (
('can_manage_config', 'Can manage configuration'),
- ('can_manage_logos', 'Can manage logos'))
+ ('can_manage_logos_and_fonts', 'Can manage logos and fonts'))
@classmethod
def get_collection_string(cls):
diff --git a/openslides/core/static/css/_fonts.scss b/openslides/core/static/css/_fonts.scss
index 57c7f95cf..12ab0c028 100644
--- a/openslides/core/static/css/_fonts.scss
+++ b/openslides/core/static/css/_fonts.scss
@@ -3,22 +3,22 @@
*/
@font-face {
- font-family: $font;
- src: $font-src;
- font-weight: 400;
- font-style: normal;
+ font-family: $font;
+ src: $font-src;
+ font-weight: 400;
+ font-style: normal;
}
@font-face {
- font-family: $font-medium;
- src: $font-medium-src;
- font-weight: 400;
- font-style: normal;
+ font-family: $font-medium;
+ src: $font-medium-src;
+ font-weight: 400;
+ font-style: normal;
}
@font-face {
- font-family: $font-condensed;
- src: $font-condensed-src;
- font-weight: 100;
- font-style: normal;
+ font-family: $font-condensed;
+ src: $font-condensed-src;
+ font-weight: 100;
+ font-style: normal;
}
@font-face {
font-family: $font-condensed-light;
diff --git a/openslides/core/static/css/_variables.scss b/openslides/core/static/css/_variables.scss
index f52882b66..8894c57e4 100644
--- a/openslides/core/static/css/_variables.scss
+++ b/openslides/core/static/css/_variables.scss
@@ -1,9 +1,10 @@
/** Fonts **/
-$font: 'Roboto';
+/* Note: The font naming has to be consistent to the projector.html */
+$font: 'OSFont';
$font-src: url('../fonts/Roboto-Regular.woff') format('woff');
-$font-medium: 'Roboto Medium';
+$font-medium: 'OSFont Medium';
$font-medium-src: url('../fonts/Roboto-Medium.woff') format('woff');
-$font-condensed: 'Roboto Condensed';
+$font-condensed: 'OSFont Condensed';
$font-condensed-src: url('../fonts/Roboto-Condensed-Regular.woff') format('woff');
-$font-condensed-light: 'Roboto Condensed Light';
+$font-condensed-light: 'OSFont Condensed Light';
$font-condensed-light-src: url('../fonts/Roboto-Condensed-Light.woff') format('woff');
diff --git a/openslides/core/static/css/projector.scss b/openslides/core/static/css/projector.scss
index 3b555410a..82fc04756 100644
--- a/openslides/core/static/css/projector.scss
+++ b/openslides/core/static/css/projector.scss
@@ -2,7 +2,6 @@
/* General */
@import "variables";
-@import "fonts";
@import "helper";
@import "ui-override";
diff --git a/openslides/core/static/css/site.scss b/openslides/core/static/css/site.scss
index b0f101633..824a03fc9 100644
--- a/openslides/core/static/css/site.scss
+++ b/openslides/core/static/css/site.scss
@@ -15,4 +15,3 @@
@import "../../../motions/static/css/motions/site";
@import "../../../users/static/css/users/site";
@import "../../../mediafiles/static/css/mediafiles/site";
-
diff --git a/openslides/core/static/js/core/base.js b/openslides/core/static/js/core/base.js
index b9046c86d..b8ef87cb8 100644
--- a/openslides/core/static/js/core/base.js
+++ b/openslides/core/static/js/core/base.js
@@ -826,36 +826,78 @@ angular.module('OpenSlidesApp.core', [
getAll: function () {
var self = this;
return _.map(this.getKeys(), function (key) {
- return self.getFromKey(key);
+ return self.get(key);
});
},
- getFromKey: function (key) {
+ get: function (key) {
var config = Config.get(key);
if (config) {
config.value.key = key;
return config.value;
}
},
- isMediafileUsedAsLogo: function (mediafile) {
- return _.find(this.getAll(), function (logoPlaceholder) {
- return logoPlaceholder.path === mediafile.mediafileUrl;
- });
- },
- canMediafileBeUsedAsLogo: function (mediafile) {
- return mediafile.is_image;
- },
- setMediafile: function (key, mediafile) {
+ set: function (key, path) {
var config = Config.get(key);
- if (!mediafile || mediafile.canBeUsedAsLogo()) {
- config.value.path = mediafile ? mediafile.mediafileUrl : '';
+ if (config) {
+ config.value.path = path;// ? mediafile.mediafileUrl : '';
Config.save(key);
}
},
- getLogosForMediafile: function (mediafile) {
- return _.filter(this.getAll(), function (logoPlaceholder) {
- return logoPlaceholder.path === mediafile.mediafileUrl;
+ };
+ }
+])
+
+.factory('Fonts', [
+ 'Config',
+ 'gettext',
+ function (Config, gettext) {
+ var extensionFormatMap = {
+ 'ttf': 'truetype',
+ 'woff': 'woff',
+ };
+
+ return {
+ getKeys: function () {
+ return Config.get('fonts_available').value;
+ },
+ getAll: function () {
+ var self = this;
+ return _.map(this.getKeys(), function (key) {
+ return self.get(key);
});
},
+ get: function (key) {
+ var config = Config.get(key);
+ if (config) {
+ config.value.key = key;
+ return config.value;
+ }
+ },
+ getUrl: function (key) {
+ var font = this.get(key);
+ if (font) {
+ var path = font.path;
+ if (!path) {
+ return font.default;
+ }
+ return path;
+ }
+ },
+ getForCss: function (key) {
+ var url = this.getUrl(key);
+ if (url) {
+ var ext = _.last(url.split('.'));
+ return "url('" + url + "') format('" +
+ extensionFormatMap[ext] + "')";
+ }
+ },
+ set: function (key, path) {
+ var config = Config.get(key);
+ if (config) {
+ config.value.path = path;
+ Config.save(key);
+ }
+ },
};
}
])
diff --git a/openslides/core/static/js/core/pdf-worker.js b/openslides/core/static/js/core/pdf-worker.js
index 074035ed1..449645e82 100644
--- a/openslides/core/static/js/core/pdf-worker.js
+++ b/openslides/core/static/js/core/pdf-worker.js
@@ -10,20 +10,18 @@ var document = {
};
var window = this;
-// PdfMake and Fonts
+// PdfMake
importScripts('/static/js/workers/pdf-worker-libs.js');
// Set default font family.
-// To use custom ttf font files you have to replace the vfs_fonts.js file.
-// See https://github.com/pdfmake/pdfmake/wiki/Custom-Fonts---client-side
-// "PdfFont" is used as generic name in core/pdf.js. Adjust the four
-// font style names only.
+// "PdfFont" and "OSFont-*" are generic names used here and in core/pdf.js. The
+// suffix after "OSFont-" has to be the same as the config value.
pdfMake.fonts = {
PdfFont: {
- normal: 'Roboto-Regular.ttf',
- bold: 'Roboto-Medium.ttf',
- italics: 'Roboto-Italic.ttf',
- bolditalics: 'Roboto-Italic.ttf'
+ normal: 'OSFont-regular.ttf',
+ bold: 'OSFont-bold.ttf',
+ italics: 'OSFont-italic.ttf',
+ bolditalics: 'OSFont-bold_italic.ttf'
}
};
@@ -106,8 +104,9 @@ var replaceFooter = function (doc) {
// Create PDF on message and return the base64 decoded document
self.addEventListener('message', function(e) {
var data = JSON.parse(e.data);
- var doc = data.pdfDocument;
+ pdfMake.vfs = data.vfs; // Set custom fonts.
+ var doc = data.pdfDocument;
replaceFooter(doc);
replacePlaceholder(doc.content);
diff --git a/openslides/core/static/js/core/pdf.js b/openslides/core/static/js/core/pdf.js
index 234df7ed2..03deb869c 100644
--- a/openslides/core/static/js/core/pdf.js
+++ b/openslides/core/static/js/core/pdf.js
@@ -1061,13 +1061,99 @@ angular.module('OpenSlidesApp.core.pdf', [])
}
])
+// Creates the virtual filesystem for PdfMake.
+.factory('PdfVfs', [
+ '$q',
+ '$http',
+ 'Fonts',
+ 'Config',
+ function ($q, $http, Fonts, Config) {
+ var urlCache = {}; // Caches the get request. Maps urls to base64 data ready to use.
+
+ var loadFont = function (url) {
+ return $q(function (resolve, reject) {
+ // Get font
+ return $http.get(url, {responseType: 'blob'}).then(function (success) {
+ // Convert to base64
+ var reader = new FileReader();
+ reader.readAsDataURL(success.data);
+ reader.onloadend = function() {
+ resolve(reader.result.split(',')[1]);
+ };
+ }, function (error) {
+ reject(error);
+ });
+ });
+ };
+
+ /*
+ * Returns a map from urls to arrays of font types used by PdfMake.
+ * E.g. if the font "regular" and bold" have the urls "fonts/myFont.ttf",
+ * the map fould be "fonts/myFont.ttf": ["OSFont-regular.ttf", "OSFont-bold.ttf"]
+ */
+ var getUrlMapping = function () {
+ var urlMap = {};
+ var fonts = ['regular', 'italic', 'bold', 'bold_italic'];
+ _.forEach(fonts, function (font) {
+ var url = Fonts.getUrl('font_' + font);
+ if (!urlMap[url]) {
+ urlMap[url] = [];
+ }
+ urlMap[url].push('OSFont-' + font + '.ttf');
+ });
+ return urlMap;
+ };
+
+ /*
+ * Create the virtual filesystem needed by PdfMake for the fonts. Gets the url
+ * mapping and loads all fonts via get requests or the urlCache.
+ */
+ var getVfs = function () {
+ return $q(function (resolve, reject) {
+ var vfs = {};
+ var urls = getUrlMapping();
+ var promises = _.chain(urls)
+ .map(function (filenames, url) {
+ if (urlCache[url]) {
+ // Just save the cache data into vfs.
+ _.forEach(filenames, function (filename) {
+ vfs[filename] = urlCache[url];
+ });
+ return false; // No promise here, it was all cached.
+ } else {
+ // Not in the cache, get the font and save the data into vfs.
+ return loadFont(url).then(function (data) {
+ urlCache[url] = data;
+ _.forEach(filenames, function (filename) {
+ vfs[filename] = data;
+ });
+ });
+ }
+ })
+ .filter(function (promise) {
+ return promise;
+ })
+ .value();
+ $q.all(promises).then(function () {
+ resolve(vfs);
+ });
+ });
+ };
+
+ return {
+ get: getVfs,
+ };
+ }
+])
+
.factory('PdfCreate', [
'$timeout',
'$q',
'gettextCatalog',
'FileSaver',
+ 'PdfVfs',
'Messaging',
- function ($timeout, $q, gettextCatalog, FileSaver, Messaging) {
+ function ($timeout, $q, gettextCatalog, FileSaver, PdfVfs, Messaging) {
var filenameMessageMap = {};
var b64toBlob = function(b64Data) {
var byteCharacters = atob(b64Data);
@@ -1105,40 +1191,46 @@ angular.module('OpenSlidesApp.core.pdf', [])
return {
getBase64FromDocument: function (pdfDocument) {
return $q(function (resolve, reject) {
- var pdfWorker = new Worker('/static/js/workers/pdf-worker.js');
- pdfWorker.addEventListener('message', function (event) {
- resolve(event.data);
+ PdfVfs.get().then(function (vfs) {
+ var pdfWorker = new Worker('/static/js/workers/pdf-worker.js');
+ pdfWorker.addEventListener('message', function (event) {
+ resolve(event.data);
+ });
+ pdfWorker.addEventListener('error', function (event) {
+ reject(event);
+ });
+ pdfWorker.postMessage(JSON.stringify({
+ pdfDocument: pdfDocument,
+ vfs: vfs,
+ }));
});
- pdfWorker.addEventListener('error', function (event) {
- reject(event);
- });
- pdfWorker.postMessage(JSON.stringify({
- pdfDocument: pdfDocument
- }));
});
},
// Struckture of pdfDocuments: { filname1: doc, filename2: doc, ...}
getBase64FromMultipleDocuments: function (pdfDocuments) {
return $q(function (resolve, reject) {
- var pdfWorker = new Worker('/static/js/workers/pdf-worker.js');
- var resultCount = 0;
- var base64Map = {}; // Maps filename to base64
- pdfWorker.addEventListener('message', function (event) {
- resultCount++;
- var data = JSON.parse(event.data);
- base64Map[data.filename] = data.base64;
- if (resultCount === _.keys(pdfDocuments).length) {
- resolve(base64Map);
- }
- });
- pdfWorker.addEventListener('error', function (event) {
- reject(event);
- });
- _.forEach(pdfDocuments, function (doc, filename) {
- pdfWorker.postMessage(JSON.stringify({
- filename: filename,
- pdfDocument: doc
- }));
+ PdfVfs.get().then(function (vfs) {
+ var pdfWorker = new Worker('/static/js/workers/pdf-worker.js');
+ var resultCount = 0;
+ var base64Map = {}; // Maps filename to base64
+ pdfWorker.addEventListener('message', function (event) {
+ resultCount++;
+ var data = JSON.parse(event.data);
+ base64Map[data.filename] = data.base64;
+ if (resultCount === _.keys(pdfDocuments).length) {
+ resolve(base64Map);
+ }
+ });
+ pdfWorker.addEventListener('error', function (event) {
+ reject(event);
+ });
+ _.forEach(pdfDocuments, function (doc, filename) {
+ pdfWorker.postMessage(JSON.stringify({
+ filename: filename,
+ pdfDocument: doc,
+ vfs: vfs,
+ }));
+ });
});
});
},
diff --git a/openslides/core/static/js/core/projector.js b/openslides/core/static/js/core/projector.js
index b31fcbb7c..71b59baa7 100644
--- a/openslides/core/static/js/core/projector.js
+++ b/openslides/core/static/js/core/projector.js
@@ -78,12 +78,13 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
}
])
-.controller('LanguageCtrl', [
+.controller('LanguageAndFontCtrl', [
'$scope',
'Languages',
'Config',
'ProjectorID',
- function ($scope, Languages, Config, ProjectorID) {
+ 'Fonts',
+ function ($scope, Languages, Config, ProjectorID, Fonts) {
// for the dynamic title
$scope.projectorId = ProjectorID();
@@ -98,6 +99,18 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
}
Languages.setCurrentLanguage($scope.selectedLanguage);
});
+
+ $scope.$watch(function () {
+ return Config.lastModified('font_regular') +
+ Config.lastModified('font_italic') +
+ Config.lastModified('font_bold') +
+ Config.lastModified('font_bold_italic');
+ }, function () {
+ $scope.font = Fonts.getForCss('font_regular');
+ $scope.font_medium = Fonts.getForCss('font_italic');
+ $scope.font_condensed = Fonts.getForCss('font_bold');
+ $scope.font_condensed_light = Fonts.getForCss('font_bold_italic');
+ });
}
])
diff --git a/openslides/core/static/templates/projector-container.html b/openslides/core/static/templates/projector-container.html
index 78296c5e3..1b83c8532 100644
--- a/openslides/core/static/templates/projector-container.html
+++ b/openslides/core/static/templates/projector-container.html
@@ -1,5 +1,5 @@
-
+