From 546c4e65f64e2e350e5d73cd73c9b902914f4f62 Mon Sep 17 00:00:00 2001 From: Thomas Junk Date: Fri, 19 Aug 2016 14:10:30 +0200 Subject: [PATCH 1/2] PDFMake implemented --- README.rst | 1 + bower.json | 4 + openslides/core/static/js/core/site.js | 397 ++++++++++++++++++ openslides/motions/static/js/motions/site.js | 235 ++++++++++- .../templates/motions/motion-detail.html | 4 + openslides/motions/urls.py | 2 + openslides/motions/views.py | 147 ++++++- 7 files changed, 788 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 11f8ef01f..48e733349 100644 --- a/README.rst +++ b/README.rst @@ -199,6 +199,7 @@ OpenSlides uses the following projects or parts of them: * `angular-gettext `_, License: MIT * `angular-loading-bar `_, License: MIT * `angular-messages `_, License: MIT + * `pdfmake `_, License: MIT * `angular-pdf `_, License: MIT * `angular-sanitize `_, License: MIT * `angular-scroll-glue `_, License: MIT diff --git a/bower.json b/bower.json index 58777efe6..58e85c011 100644 --- a/bower.json +++ b/bower.json @@ -29,12 +29,16 @@ "ng-dialog": "~0.5.6", "ng-file-upload": "~11.2.3", "ngBootbox": "~0.1.3", + "pdfmake": "~0.1.17", "open-sans-fontface": "https://github.com/OpenSlides/open-sans.git#1.4.2.post1", "roboto-condensed": "~0.3.0", "tinymce-dist": "4.3.12", "tinymce-i18n": "OpenSlides/tinymce-i18n#a186ad61e0aa30fdf657e88f405f966d790f0805" }, "overrides": { + "pdfmake-dist": { + "main": "build/pdfmake.min.js" + }, "pdfjs-dist": { "main": "build/pdf.combined.js" }, diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js index 07bf1811e..789fc43db 100644 --- a/openslides/core/static/js/core/site.js +++ b/openslides/core/static/js/core/site.js @@ -18,6 +18,403 @@ angular.module('OpenSlidesApp.core.site', [ 'ui.tinymce', 'luegg.directives', ]) +.factory('PdfMakeDocumentProvider', function() { + /** + * Provides the global Document + * @constructor + * @param {object} contentProvider - Object with on method `getContent`, which returns an array for content + * @param {string} defaultFont - Default font for the document + */ + var createInstance = function(contentProvider, defaultFont) { + /** + * Generates header for PDF + * @constructor + */ + var header = function() { + var date = new Date(); + return { + // alignment: 'center', + color: '#555', + fontSize: 10, + margin: [80, 50, 80, 0], //margin: [left, top, right, bottom] + columns: ['OpenSlides | Presentation and assembly system', { + fontSize: 6, + text: 'Stand: ' + date.toLocaleDateString() + " " + date.toLocaleTimeString(), + alignment: 'right' + }] + }; + }, + /** + * Generates footer line + * @function + * @param {object} currentPage - An object representing the current page + * @param {number} pageCount - number for pages + */ + footer = function(currentPage, pageCount) { + return { + alignment: 'center', + fontSize: 8, + color: '#555', + text: "Seite: " + currentPage.toString() + }; + }, + /** + * Generates the document(definition) for pdfMake + * @function + */ + getDocument = function() { + var content = contentProvider.getContent(); + return { + pageSize: 'A4', + pageMargins: [80, 90, 80, 60], + defaultStyle: { + font: defaultFont + }, + fontSize: 8, + header: header, + footer: footer, + content: content, + }; + }; + return { + getDocument: getDocument + }; + }; + return { + createInstance: createInstance + }; + }) +.factory('PdfMakeConverter', function() { + /** + * Converter component for HTML->JSON for pdfMake + * @constructor + * @param {object} images - Key-Value structure representing image.src/BASE64 of images + * @param {object} fonts - Key-Value structure representing fonts (detailed description below) + * @param {object} pdfMake - the converter component enhances pdfMake + */ + var createInstance = function(images, fonts, pdfMake) { + var slice = Function.prototype.call.bind([].slice), + map = Function.prototype.call.bind([].map), + /** + * Adds a custom font to pdfMake.vfs + * @function + * @param {object} fontFiles - object with Files to add to pdfMake.vfs + * { + * normal: $Filename + * bold: $Filename + * italics: $Filename + * bolditalics: $Filename + * } + */ + addFontToVfs = function(fontFiles) { + Object.keys(fontFiles).forEach(function(name) { + var file = fontFiles[name]; + pdfMake.vfs[file.name] = file.content; + }); + }, + /** + * Adds custom fonts to pdfMake + * @function + * @param {object} fontInfo - Font configuration from Backend + * { + * $FontName : { + * normal: $Filename + * bold: $Filename + * italics: $Filename + * bolditalics: $Filename + * } + * } + */ + registerFont = function(fontInfo) { + Object.keys(fontInfo).forEach(function(name) { + var font = fontInfo[name]; + addFontToVfs(font); + pdfMake.fonts = pdfMake.fonts || {}; + pdfMake.fonts[name] = Object.keys(font).reduce(function(fontDefinition, style) { + fontDefinition[style] = font[style].name; + return fontDefinition; + }, {}); + }); + }, + /** + * Convertes HTML for use with pdfMake + * @function + * @param {object} html - html + */ + convertHTML = function(html) { + var elementStyles = { + "b": ["font-weight:bold"], + "strong": ["font-weight:bold"], + "u": ["text-decoration:underline"], + "em": ["font-style:italic"], + "i": ["font-style:italic"], + "h1": ["font-size:30"], + "h2": ["font-size:28"], + "h3": ["font-size:26"], + "h4": ["font-size:24"], + "h5": ["font-size:22"], + "h6": ["font-size:20"], + "a": ["color:blue", "text-decoration:underline"] + }, + /** + * Parses Children of the current paragraph + * @function + * @param {object} converted - + * @param {object} element - + * @param {object} currentParagraph - + * @param {object} styles - + */ + parseChildren = function(converted, element, currentParagraph, styles) { + var elements = []; + var children = element.childNodes; + if (children.length !== 0) { + _.forEach(children, function(child) { + currentParagraph = ParseElement(elements, child, currentParagraph, styles); + }); + } + if (elements.length !== 0) { + _.forEach(elements, function(el) { + converted.push(el); + }); + } + return currentParagraph; + }, + /** + * Extracts the style from an object + * @function + * @param {object} o - the current object + * @param {object} styles - an array with styles + */ + ComputeStyle = function(o, styles) { + styles.forEach(function(singleStyle) { + var styleDefinition = singleStyle.trim().toLowerCase().split(":"); + var style = styleDefinition[0]; + var value = styleDefinition[1]; + if (styleDefinition.length == 2) { + switch (style) { + case "padding-left": + o.margin = [parseInt(value), 0, 0, 0]; + break; + case "font-size": + o.fontSize = parseInt(value); + break; + case "text-align": + switch (value) { + case "right": + case "center": + case "justify": + o.alignment = value; + break; + } + break; + case "font-weight": + switch (value) { + case "bold": + o.bold = true; + break; + } + break; + case "text-decoration": + switch (value) { + case "underline": + o.decoration = "underline"; + break; + case "line-through": + o.decoration = "lineThrough"; + break; + } + break; + case "font-style": + switch (value) { + case "italic": + o.italics = true; + break; + } + break; + case "color": + o.color = value; + break; + case "background-color": + o.background = value; + break; + } + } + }); + }, + /** + * Parses a single HTML element + * @function + * @param {object} alreadyConverted - + * @param {object} element - + * @param {object} currentParagraph - + * @param {object} styles - + */ + ParseElement = function(alreadyConverted, element, currentParagraph, styles) { + styles = styles || []; + if (element.getAttribute) { + var nodeStyle = element.getAttribute("style"); + if (nodeStyle) { + nodeStyle.split(";").forEach(function(nodeStyle) { + var tmp = nodeStyle.replace(/\s/g, ''); + styles.push(tmp); + }); + } + } + var nodeName = element.nodeName.toLowerCase(); + switch (nodeName) { + case "h1": + case "h2": + case "h3": + case "h4": + case "h5": + case "h6": + currentParagraph = create("text"); + /* falls through */ + case "a": + parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName])); + alreadyConverted.push(currentParagraph); + break; + case "b": + case "strong": + case "u": + case "em": + case "i": + parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName])); + break; + case "table": + var t = create("table", { + widths: [], + body: [] + }); + var border = element.getAttribute("border"); + var isBorder = false; + if (border) + if (parseInt(border) == 1) isBorder = true; + if (!isBorder) t.layout = 'noBorders'; + parseChildren(t.table.body, element, currentParagraph, styles); + var widths = element.getAttribute("widths"); + if (!widths) { + if (t.table.body.length !== 0) { + if (t.table.body[0].length !== 0) + for (var k = 0; k < t.table.body[0].length; k++) + t.table.widths.push("*"); + } + } else { + var w = widths.split(","); + for (var ko = 0; ko < w.length; ko++) t.table.widths.push(w[ko]); + } + alreadyConverted.push(t); + break; + case "tbody": + parseChildren(alreadyConverted, element, currentParagraph, styles); + break; + case "tr": + var row = []; + parseChildren(row, element, currentParagraph, styles); + alreadyConverted.push(row); + break; + case "td": + currentParagraph = create("text"); + var st = create("stack"); + st.stack.push(currentParagraph); + var rspan = element.getAttribute("rowspan"); + if (rspan) + st.rowSpan = parseInt(rspan); + var cspan = element.getAttribute("colspan"); + if (cspan) + st.colSpan = parseInt(cspan); + parseChildren(st.stack, element, currentParagraph, styles); + alreadyConverted.push(st); + break; + case "span": + parseChildren(alreadyConverted, element, currentParagraph, styles); + break; + case "br": + currentParagraph = create("text"); + alreadyConverted.push(currentParagraph); + break; + case "li": + case "div": + case "p": + currentParagraph = create("text"); + var stack = create("stack"); + stack.stack.push(currentParagraph); + ComputeStyle(stack, styles); + parseChildren(stack.stack, element, currentParagraph); + alreadyConverted.push(stack); + break; + case "img": + alreadyConverted.push({ + image: BaseMap[element.getAttribute("src")], + width: parseInt(element.getAttribute("width")), + height: parseInt(element.getAttribute("height")) + }); + break; + case "ul": + var u = create("ul"); + parseChildren(u.ul, element, currentParagraph, styles); + alreadyConverted.push(u); + break; + case "ol": + var o = create("ol"); + parseChildren(o.ol, element, currentParagraph, styles); + alreadyConverted.push(o); + break; + default: + var temporary = create("text", element.textContent.replace(/\n/g, "")); + if (styles) + ComputeStyle(temporary, styles); + currentParagraph.text.push(temporary); + break; + } + return currentParagraph; + }, + /** + * Parses HTML + * @function + * @param {string} converted - + * @param {object} htmlText - + */ + ParseHtml = function(converted, htmlText) { + var html = $(htmlText.replace(/\t/g, "").replace(/\n/g, "")); + var emptyParagraph = create("text"); + slice(html).forEach(function(element) { + ParseElement(converted, element, emptyParagraph); + }); + }, + content = []; + ParseHtml(content, html); + return content; + }, + BaseMap = images, + /** + * Creates containerelements for pdfMake + * e.g create("text":"MyText") result in { text: "MyText" } + * or complex objects create("stack", [{text:"MyText"}, {text:"MyText2"}]) + *for units / paragraphs of text + * + * @function + * @param {string} name - name of the attribute holding content + * @param {object} content - the actual content (maybe empty) + */ + create = function(name, content) { + var o = {}; + content = content || []; + o[name] = content; + return o; + }; + fonts.forEach(function(fontInfo) { + registerFont(fontInfo); + }); + return { + convertHTML: convertHTML, + createElement: create + }; + }; + return { + createInstance: createInstance + }; +}) // Provider to register entries for the main menu. .provider('mainMenu', [ diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index 98fccd042..a9928c9b2 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -4,6 +4,207 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions']) +.factory('MotionContentProvider', ['gettextCatalog', function(gettextCatalog) { + /** + * Provides the content as JS objects for Motions in pdfMake context + * @constructor + */ + var createInstance = function(converter) { + /** + * Text of motion + * @function + * @param {object} motion - Current motion + * @param {object} $scope - Current $scope + */ + var textContent = function(motion, $scope) { + return converter.convertHTML(motion.getText($scope.version)); + }, + /** + * Generate text of reason + * @function + * @param {object} motion - Current motion + * @param {object} $scope - Current $scope + */ + reasonContent = function(motion, $scope) { + return converter.convertHTML(motion.getReason($scope.version)); + }, + /** + * Generate header text of motion + * @function + * @param {object} motion - Current motion + * @param {object} $scope - Current $scope + */ + motionHeader = function(motion, $scope) { + var header = converter.createElement("text", gettextCatalog.getString("Motion") + " " + motion.identifier + ": " + motion.getTitle($scope.version)); + header.bold = true; + header.fontSize = 26; + return header; + }, + /** + * Generate text of signment + * @function + * @param {object} motion - Current motion + * @param {object} $scope - Current $scope + * @param {object} User - Current user + */ + signment = function(motion, $scope, User) { + var label = converter.createElement("text", gettextCatalog.getString('Submitter') + ':\nStatus:'); + label.width = "30%"; + label.bold = true; + var signment = converter.createElement("stack", [label]); + signment.margin = [10, 20, 0, 10]; + signment.lineHeight = 2.5; + return signment; + }, + /** + * Generates polls + * @function + * @param {object} motion - Current motion + * @param {object} $scope - Current $scope + */ + polls = function(motion, $scope) { + if (!motion.polls.length) return {}; + var pollLabel = converter.createElement("text", gettextCatalog.getString('Voting result') + ":"), + results = function() { + return motion.polls.map(function(poll, index) { + var id = index + 1, + yes = poll.yes, + yesRelative = (poll.yes) * 100 / (poll.votescast), + no = poll.no, + noRelative = (poll.no) * 100 / (poll.votescast), + abstain = poll.abstain, + abstainRelative = (poll.abstain) * 100 / (poll.votescast), + valid = poll.votesvalid, + validRelative = (poll.votesvalid) * 100 / (poll.votescast), + number = { + text: id + ".", + width: "5%" + }, + headerText = { + text: "Abstimmung", + width: "15%" + }, + /** + * Generates a part (consisting of different columns) of the polls + * + * Example Ja 100 ( 90% ) + * + * @function + * @param {string} name - E.g. "Ja" + * @param {number} value - E.g.100 + * @param {number} relValue - E.g. 90 + */ + createPart = function(name, value, relValue) { + var indexColumn = converter.createElement("text"); + var nameColumn = converter.createElement("text", "" + name); + var valueColumn = converter.createElement("text", "" + value); + var relColumn = converter.createElement("text", "(" + "" + relValue + "%)"); + valueColumn.width = "40%"; + indexColumn.width = "5%"; + valueColumn.width = "5%"; + valueColumn.alignment = "right"; + relColumn.margin = [5, 0, 0, 0]; + return [indexColumn, nameColumn, valueColumn, relColumn]; + }, + yesPart = converter.createElement("columns", createPart(gettextCatalog.getString("Yes"), yes, yesRelative)), + noPart = converter.createElement("columns", createPart(gettextCatalog.getString("No"), no, noRelative)), + abstainPart = converter.createElement("columns", createPart(gettextCatalog.getString("Abstain"), abstain, abstainRelative)), + totalPart = converter.createElement("columns", createPart(gettextCatalog.getString("Valid votes"), valid, validRelative)), + heading = converter.createElement("columns", [number, headerText]), + pollResult = converter.createElement("stack", [ + heading, yesPart, noPart, abstainPart, totalPart + ]); + + return pollResult; + }, {}); + }; + pollLabel.width = '35%'; + pollLabel.bold = true; + var result = converter.createElement("columns", [pollLabel, results()]); + result.margin = [10, 0, 0, 10]; + result.lineHeight = 1; + return result; + }, + /** + * Generates title section for motion + * @function + * @param {object} motion - Current motion + * @param {object} $scope - Current $scope + */ + titleSection = function(motion, $scope) { + var title = converter.createElement("text", motion.getTitle($scope.version)); + title.bold = true; + title.fontSize = 14; + title.margin = [0, 0, 0, 10]; + return title; + }, + /** + * Generates reason section for polls + * @function + * @param {object} motion - Current motion + * @param {object} $scope - Current $scope + */ + reason = function(motion, $scope) { + var r = converter.createElement("text", gettextCatalog.getString("Reason") + ":"); + r.bold = true; + r.fontSize = 14; + r.margin = [0, 30, 0, 10]; + return r; + }, + /** + * Generates content as a pdfmake consumable + * @function + * @param {object} motion - Current motion + * @param {object} $scope - Current $scope + * @param {object} User - Current user + */ + getContent = function(motion, $scope, User) { + return [ + motionHeader(motion, $scope), + signment(motion, $scope, User), + polls(motion, $scope), + titleSection(motion, $scope), + textContent(motion, $scope), + reason(motion, $scope), + reasonContent(motion, $scope) + ]; + }; + return { + getContent: getContent + }; + }; + return { + createInstance: createInstance + }; +}]) +.factory('SingleMotionContentProvider', function() { + /** + * Generates a content provider + * @constructor + * @param {object} motionContentProvider - Generates pdfMake structure from motion + * @param {object} $scope - Current $scope + * @param {object} User - Current User + */ + var createInstance = function(motionContentProvider, motion, $scope, User) { + /** + * Returns Content for single motion + * @function + * @param {object} motion - Current motion + * @param {object} $scope - Current $scope + * @param {object} User - Current User + */ + var getContent = function() { + return motionContentProvider.getContent(motion, $scope, User); + }; + return { + getContent: getContent + }; + }; + return { + createInstance: createInstance + }; +}) + .config([ 'mainMenuProvider', 'gettext', @@ -573,7 +774,14 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions']) 'User', 'Workflow', 'motion', - function($scope, $http, ngDialog, MotionForm, Motion, Category, Mediafile, Tag, User, Workflow, motion) { + 'SingleMotionContentProvider', + 'MotionContentProvider', + 'PdfMakeConverter', + 'PdfMakeDocumentProvider', + function($scope, $http, ngDialog, MotionForm, + Motion, Category, Mediafile, Tag, + User, Workflow, motion, + SingleMotionContentProvider, MotionContentProvider, PdfMakeConverter, PdfMakeDocumentProvider) { Motion.bindOne(motion.id, $scope, 'motion'); Category.bindAll({}, $scope, 'categories'); Mediafile.bindAll({}, $scope, 'mediafiles'); @@ -584,6 +792,31 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions']) $scope.version = motion.active_version; $scope.isCollapsed = true; + $scope.makePDF = function(){ + var content = motion.getText($scope.version) + motion.getReason($scope.version), + slice = Function.prototype.call.bind([].slice), + map = Function.prototype.call.bind([].map), + image_sources = map($(content).find("img"), function(element) { + return element.getAttribute("src"); + }); + + $http.post('/motions/encode_media/', JSON.stringify(image_sources)).success(function(data) { + /** + * Converter for use with pdfMake + * @constructor + * @param {object} images - An object to resolve the BASE64 encoded images { "$src":$BASE64DATA } + * @param {object} fonts - An object representing the available custom fonts + * @param {object} pdfMake - pdfMake object for enhancement with custom fonts + */ + + var converter = PdfMakeConverter.createInstance(data.images, data.fonts, pdfMake), + motionContentProvider = MotionContentProvider.createInstance(converter), + contentProvider = SingleMotionContentProvider.createInstance(motionContentProvider, motion, $scope, User), + documentProvider = PdfMakeDocumentProvider.createInstance(contentProvider, data.defaultFont); + pdfMake.createPdf(documentProvider.getDocument()).open(); + }); + }; + // open edit dialog $scope.openDialog = function (motion) { ngDialog.open(MotionForm.getDialog(motion)); diff --git a/openslides/motions/static/templates/motions/motion-detail.html b/openslides/motions/static/templates/motions/motion-detail.html index 067a47b94..2061abd98 100644 --- a/openslides/motions/static/templates/motions/motion-detail.html +++ b/openslides/motions/static/templates/motions/motion-detail.html @@ -9,6 +9,10 @@ PDF + + + PDFmake + diff --git a/openslides/motions/urls.py b/openslides/motions/urls.py index 9b37d60d8..f0d89ef51 100644 --- a/openslides/motions/urls.py +++ b/openslides/motions/urls.py @@ -14,4 +14,6 @@ urlpatterns = [ url(r'^poll/(?P\d+)/print/$', views.MotionPollPDF.as_view(), name='motionpoll_pdf'), + + url(r'^encode_media/', views.encode_media, name="media_encoding") ] diff --git a/openslides/motions/views.py b/openslides/motions/views.py index 703558528..e34285a89 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -1,10 +1,16 @@ +import base64 +import json +import os + +from django.conf import settings from django.db import transaction -from django.http import Http404 +from django.http import Http404, JsonResponse from django.utils.text import slugify from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_noop from reportlab.platypus import SimpleDocTemplate from rest_framework import status +from rest_framework.decorators import api_view from openslides.core.config import config from openslides.utils.rest_api import ( @@ -466,3 +472,142 @@ class MotionPDFView(SingleObjectMixin, PDFView): motions_to_pdf(pdf, motions) else: motion_to_pdf(pdf, self.get_object()) + + +@api_view(["POST"]) +def encode_media(request): + """ + Encode_image is used in the context of PDF-Generation + Takes an array of IMG.src - Paths + Retrieves the according images + Encodes the images + Add configured fonts + Puts it into a key-value structure + + { + "images": { + "media/file/ubuntu.png":"$ENCODED_IMAGE" + }, + "fonts": [{ + $FontName : { + normal: $Filename + bold: $Filename + italics: $Filename + bolditalics: $Filename + } + }], + "default_font": "$DEFAULTFONT" + } + + :param request: + :return: JsonResponse of the resulting dictionary + + Calling e.g. + $.ajax({ type: "POST", url: "/motions/encode_images/", + data: JSON.stringify(["$FILEPATH"]), + success: function(data){ console.log(data); }, + dataType: 'application/json' }); + """ + body_unicode = request.body.decode('utf-8') + file_paths = json.loads(body_unicode) + images = {file_path: encode_image_from(file_path) for file_path in file_paths} + fonts = encoded_fonts() + default_font = get_default_font() + return JsonResponse({ + "images": images, + "fonts": fonts, + "defaultFont": default_font + }) + + +def get_default_font(): + """ + For development purposes this is hard coded + :return: the name of the default Font + """ + return "OpenSans" + + +def encoded_fonts(): + """ + Generate font encoding for pdfMake + :return: list of Font Encodings + """ + + fonts = get_configured_fonts() + + enc_fonts = [encode_font(name, files) for name, files in fonts.items()] + + return enc_fonts + + +def get_configured_fonts(): + """ + For development purposes, the current font definition is hard coded + The form is { + $FontName : { + normal: $Filename + bold: $Filename + italics: $Filename + bolditalics: $Filename + } + } + This structure is required according to PDFMake specs. + :return: + """ + + fonts = { + "OpenSans": { + "normal": 'OpenSans-Regular.ttf', + "bold": 'OpenSans-Bold.ttf', + "italics": 'OpenSans-Italic.ttf', + "bolditalics": 'OpenSans-BoldItalic.ttf' + } + } + return fonts + + +def encode_font(fontName, font_files): + """ + Responsible to encode a single font + :param fontName: name of the font + :param font_files: files for different weighs + :return: dictionary with encoded font + """ + + encoded_files = {type: encode_font_from(file_path) for type, file_path in font_files.items()} + return {fontName: encoded_files} + + +def encode_font_from(file_path): + """ + Returns the BASE64 encoded version of an image-file for a given path + :param file_path: + :return: dictionary with the string representation (content) and the name of the file + for the pdfMake.vfs structure + """ + path = os.path.join(settings.SITE_ROOT, 'static/fonts', os.path.basename(file_path)) + try: + with open(path, "rb") as file: + string_representation = "{}".format(base64.b64encode(file.read()).decode()) + except: + return "" + else: + return {"content": string_representation, "name": file_path} + + +def encode_image_from(file_path): + """ + Returns the BASE64 encoded version of an image-file for a given path + :param file_path: + :return: + """ + path = os.path.join(settings.MEDIA_ROOT, 'file', os.path.basename(file_path)) + try: + with open(path, "rb") as file: + string_representation = "data:image/{};base64,{}".format(os.path.splitext(file_path)[1][1:], + base64.b64encode(file.read()).decode()) + except: + return "" + else: + return string_representation From 92a541215f6d438d7d11eb231ce39b33c5bd163a Mon Sep 17 00:00:00 2001 From: Thomas Junk Date: Fri, 19 Aug 2016 14:44:58 +0200 Subject: [PATCH 2/2] pdfmake incl. fixes --- CHANGELOG | 1 + openslides/core/static/js/core/site.js | 9 +- openslides/core/urls.py | 6 +- openslides/core/views.py | 143 +++++++++++++++++ openslides/motions/static/js/motions/site.js | 15 +- .../templates/motions/motion-detail.html | 6 +- openslides/motions/urls.py | 2 - openslides/motions/views.py | 147 +----------------- 8 files changed, 169 insertions(+), 160 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 118e79ecf..7258b1961 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -22,6 +22,7 @@ Core: Motions: - Added origin field. - Added button to sort and number all motions in a category. +- Introduced pdfMake for clientside generation of PDFs. Users: - Added field is_committee and new default group Committees. diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js index 789fc43db..33af57eb2 100644 --- a/openslides/core/static/js/core/site.js +++ b/openslides/core/static/js/core/site.js @@ -37,8 +37,15 @@ angular.module('OpenSlidesApp.core.site', [ color: '#555', fontSize: 10, margin: [80, 50, 80, 0], //margin: [left, top, right, bottom] - columns: ['OpenSlides | Presentation and assembly system', { + columns: [ + { + text: 'OpenSlides | Presentation and assembly system', + fontSize:10, + width: '70%' + }, + { fontSize: 6, + width: '30%', text: 'Stand: ' + date.toLocaleDateString() + " " + date.toLocaleTimeString(), alignment: 'right' }] diff --git a/openslides/core/urls.py b/openslides/core/urls.py index 76ea25daa..3dbc1b614 100644 --- a/openslides/core/urls.py +++ b/openslides/core/urls.py @@ -19,6 +19,10 @@ urlpatterns = [ views.SearchView.as_view(), name='core_search'), + url(r'^core/encode_media/$', + views.MediaEncoder.as_view(), + name="core_mediaencoding"), + url(r'^angular_js/(?Psite|projector)/$', views.AppsJsView.as_view(), name='core_apps_js'), @@ -26,8 +30,8 @@ urlpatterns = [ # View for the projectors are handelt by angular. url(r'^projector.*$', views.ProjectorView.as_view()), - # Main entry point for all angular pages. # Has to be the last entry in the urls.py url(r'^.*$', views.IndexView.as_view()), + ] diff --git a/openslides/core/views.py b/openslides/core/views.py index ac6ee5412..7c38e257d 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -1,3 +1,6 @@ +import base64 +import json +import os import re import uuid from collections import OrderedDict @@ -606,3 +609,143 @@ class SearchView(utils_views.APIView): return super().get_context_data( elements=search(unquote(query)), **context) + + +class MediaEncoder(utils_views.APIView): + """ + MediaEncoder is a class based view to prepare encoded media for pdfMake + """ + http_method_names = ['post'] + + def post(self, request, *args, **kwargs): + """ + Encode_image is used in the context of PDF-Generation + Takes an array of IMG.src - Paths + Retrieves the according images + Encodes the images to BASE64 + Add configured fonts + Puts it into a key-value structure + + { + "images": { + "media/file/ubuntu.png":"$ENCODED_IMAGE" + }, + "fonts": [{ + $FontName : { + normal: $Filename + bold: $Filename + italics: $Filename + bolditalics: $Filename + } + }], + "default_font": "$DEFAULTFONT" + } + + :param request: + :return: Response of the resulting dictionary + + Calling e.g. + $.ajax({ type: "POST", url: "/motions/encode_images/", + data: JSON.stringify(["$FILEPATH"]), + success: function(data){ console.log(data); }, + dataType: 'application/json' }); + """ + body_unicode = request.body.decode('utf-8') + file_paths = json.loads(body_unicode) + images = {file_path: self.encode_image_from(file_path) for file_path in file_paths} + fonts = self.encoded_fonts() + default_font = self.get_default_font() + return Response({ + "images": images, + "fonts": fonts, + "defaultFont": default_font + }) + + def get_default_font(self): + """ + Returns the default font for pdfMake. + + Note: For development purposes this is hard coded. + + :return: the name of the default Font + """ + return 'OpenSans' + + def encoded_fonts(self): + """ + Generate font encoding for pdfMake + :return: list of Font Encodings + """ + fonts = self.get_configured_fonts() + enc_fonts = [self.encode_font(name, files) for name, files in fonts.items()] + return enc_fonts + + def get_configured_fonts(self): + """ + Returns the configured fonts + + Note: For development purposes, the current font definition is hard coded + + The form is { + $FontName : { + normal: $Filename + bold: $Filename + italics: $Filename + bolditalics: $Filename + } + } + This structure is required according to PDFMake specs. + :return: + """ + fonts = { + 'OpenSans': { + 'normal': 'OpenSans-Regular.ttf', + 'bold': 'OpenSans-Bold.ttf', + 'italics': 'OpenSans-Italic.ttf', + 'bolditalics': 'OpenSans-BoldItalic.ttf' + } + } + return fonts + + def encode_font(self, font_name, font_files): + """ + Responsible to encode a single font + :param fontName: name of the font + :param font_files: files for different weighs + :return: dictionary with encoded font + """ + encoded_files = {type: self.encode_font_from(file_path) for type, file_path in font_files.items()} + return {font_name: encoded_files} + + def encode_font_from(self, file_path): + """ + Returns the BASE64 encoded version of an image-file for a given path + :param file_path: + :return: dictionary with the string representation (content) and the name of the file + for the pdfMake.vfs structure + """ + path = os.path.join(settings.SITE_ROOT, 'static/fonts', os.path.basename(file_path)) + try: + with open(path, "rb") as file: + string_representation = "{}".format(base64.b64encode(file.read()).decode()) + except: + return "" + else: + return {"content": string_representation, "name": file_path} + + def encode_image_from(self, file_path): + """ + Returns the BASE64 encoded version of an image-file for a given path + :param file_path: + :return: + """ + path = os.path.join(settings.MEDIA_ROOT, 'file', os.path.basename(file_path)) + try: + with open(path, "rb") as file: + string_representation = "data:image/{};base64,{}".format(os.path.splitext(file_path)[1][1:], + base64.b64encode(file.read()).decode()) + except Exception: + # If any error occurs ignore it and return an empty string + return "" + else: + return string_representation diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index a9928c9b2..c8ae39fd6 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -49,9 +49,11 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions']) */ signment = function(motion, $scope, User) { var label = converter.createElement("text", gettextCatalog.getString('Submitter') + ':\nStatus:'); + var state = converter.createElement("text", User.get(motion.submitters_id[0]).full_name + '\n'+gettextCatalog.getString(motion.state.name)); + state.width = "70%"; label.width = "30%"; label.bold = true; - var signment = converter.createElement("stack", [label]); + var signment = converter.createElement("columns", [label, state]); signment.margin = [10, 20, 0, 10]; signment.lineHeight = 2.5; return signment; @@ -778,10 +780,11 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions']) 'MotionContentProvider', 'PdfMakeConverter', 'PdfMakeDocumentProvider', + 'gettextCatalog', function($scope, $http, ngDialog, MotionForm, Motion, Category, Mediafile, Tag, User, Workflow, motion, - SingleMotionContentProvider, MotionContentProvider, PdfMakeConverter, PdfMakeDocumentProvider) { + SingleMotionContentProvider, MotionContentProvider, PdfMakeConverter, PdfMakeDocumentProvider, gettextCatalog) { Motion.bindOne(motion.id, $scope, 'motion'); Category.bindAll({}, $scope, 'categories'); Mediafile.bindAll({}, $scope, 'mediafiles'); @@ -794,13 +797,14 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions']) $scope.makePDF = function(){ var content = motion.getText($scope.version) + motion.getReason($scope.version), + id = motion.identifier, slice = Function.prototype.call.bind([].slice), map = Function.prototype.call.bind([].map), image_sources = map($(content).find("img"), function(element) { return element.getAttribute("src"); }); - $http.post('/motions/encode_media/', JSON.stringify(image_sources)).success(function(data) { + $http.post('/core/encode_media/', JSON.stringify(image_sources)).success(function(data) { /** * Converter for use with pdfMake * @constructor @@ -812,8 +816,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions']) var converter = PdfMakeConverter.createInstance(data.images, data.fonts, pdfMake), motionContentProvider = MotionContentProvider.createInstance(converter), contentProvider = SingleMotionContentProvider.createInstance(motionContentProvider, motion, $scope, User), - documentProvider = PdfMakeDocumentProvider.createInstance(contentProvider, data.defaultFont); - pdfMake.createPdf(documentProvider.getDocument()).open(); + documentProvider = PdfMakeDocumentProvider.createInstance(contentProvider, data.defaultFont), + filename = gettextCatalog.getString("Motion") + " " + id + ".pdf"; + pdfMake.createPdf(documentProvider.getDocument()).download(filename); }); }; diff --git a/openslides/motions/static/templates/motions/motion-detail.html b/openslides/motions/static/templates/motions/motion-detail.html index 2061abd98..db5bb7e37 100644 --- a/openslides/motions/static/templates/motions/motion-detail.html +++ b/openslides/motions/static/templates/motions/motion-detail.html @@ -5,13 +5,9 @@ All motions - - - PDF - - PDFmake + PDF diff --git a/openslides/motions/urls.py b/openslides/motions/urls.py index f0d89ef51..9b37d60d8 100644 --- a/openslides/motions/urls.py +++ b/openslides/motions/urls.py @@ -14,6 +14,4 @@ urlpatterns = [ url(r'^poll/(?P\d+)/print/$', views.MotionPollPDF.as_view(), name='motionpoll_pdf'), - - url(r'^encode_media/', views.encode_media, name="media_encoding") ] diff --git a/openslides/motions/views.py b/openslides/motions/views.py index e34285a89..703558528 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -1,16 +1,10 @@ -import base64 -import json -import os - -from django.conf import settings from django.db import transaction -from django.http import Http404, JsonResponse +from django.http import Http404 from django.utils.text import slugify from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_noop from reportlab.platypus import SimpleDocTemplate from rest_framework import status -from rest_framework.decorators import api_view from openslides.core.config import config from openslides.utils.rest_api import ( @@ -472,142 +466,3 @@ class MotionPDFView(SingleObjectMixin, PDFView): motions_to_pdf(pdf, motions) else: motion_to_pdf(pdf, self.get_object()) - - -@api_view(["POST"]) -def encode_media(request): - """ - Encode_image is used in the context of PDF-Generation - Takes an array of IMG.src - Paths - Retrieves the according images - Encodes the images - Add configured fonts - Puts it into a key-value structure - - { - "images": { - "media/file/ubuntu.png":"$ENCODED_IMAGE" - }, - "fonts": [{ - $FontName : { - normal: $Filename - bold: $Filename - italics: $Filename - bolditalics: $Filename - } - }], - "default_font": "$DEFAULTFONT" - } - - :param request: - :return: JsonResponse of the resulting dictionary - - Calling e.g. - $.ajax({ type: "POST", url: "/motions/encode_images/", - data: JSON.stringify(["$FILEPATH"]), - success: function(data){ console.log(data); }, - dataType: 'application/json' }); - """ - body_unicode = request.body.decode('utf-8') - file_paths = json.loads(body_unicode) - images = {file_path: encode_image_from(file_path) for file_path in file_paths} - fonts = encoded_fonts() - default_font = get_default_font() - return JsonResponse({ - "images": images, - "fonts": fonts, - "defaultFont": default_font - }) - - -def get_default_font(): - """ - For development purposes this is hard coded - :return: the name of the default Font - """ - return "OpenSans" - - -def encoded_fonts(): - """ - Generate font encoding for pdfMake - :return: list of Font Encodings - """ - - fonts = get_configured_fonts() - - enc_fonts = [encode_font(name, files) for name, files in fonts.items()] - - return enc_fonts - - -def get_configured_fonts(): - """ - For development purposes, the current font definition is hard coded - The form is { - $FontName : { - normal: $Filename - bold: $Filename - italics: $Filename - bolditalics: $Filename - } - } - This structure is required according to PDFMake specs. - :return: - """ - - fonts = { - "OpenSans": { - "normal": 'OpenSans-Regular.ttf', - "bold": 'OpenSans-Bold.ttf', - "italics": 'OpenSans-Italic.ttf', - "bolditalics": 'OpenSans-BoldItalic.ttf' - } - } - return fonts - - -def encode_font(fontName, font_files): - """ - Responsible to encode a single font - :param fontName: name of the font - :param font_files: files for different weighs - :return: dictionary with encoded font - """ - - encoded_files = {type: encode_font_from(file_path) for type, file_path in font_files.items()} - return {fontName: encoded_files} - - -def encode_font_from(file_path): - """ - Returns the BASE64 encoded version of an image-file for a given path - :param file_path: - :return: dictionary with the string representation (content) and the name of the file - for the pdfMake.vfs structure - """ - path = os.path.join(settings.SITE_ROOT, 'static/fonts', os.path.basename(file_path)) - try: - with open(path, "rb") as file: - string_representation = "{}".format(base64.b64encode(file.read()).decode()) - except: - return "" - else: - return {"content": string_representation, "name": file_path} - - -def encode_image_from(file_path): - """ - Returns the BASE64 encoded version of an image-file for a given path - :param file_path: - :return: - """ - path = os.path.join(settings.MEDIA_ROOT, 'file', os.path.basename(file_path)) - try: - with open(path, "rb") as file: - string_representation = "data:image/{};base64,{}".format(os.path.splitext(file_path)[1][1:], - base64.b64encode(file.read()).decode()) - except: - return "" - else: - return string_representation