Fix #41 -- Drag-and-drop ticket editor

Undo/redo

Useful toolbox

Font selection

Add text content

Use hex for colors

JS-side dump and load

Save

Load layout, proper undo/redo

First steps to Python rendering

More PDF rendering

Copy and paste

Buttons for keyboard actions

Splash Screen

Block unbeforeunload in dirty state

Remove debugging output

Preview

Upload new PDFs via the editor

Fix bugs during PDF reload, link in settings form

New default ticket

Add OpenSans BI

Custom fonts, fix tests
This commit is contained in:
Raphael Michel
2017-05-05 12:19:16 +02:00
parent c98b0aac90
commit 0d3f5e0c32
27 changed files with 90428 additions and 165 deletions

View File

@@ -1,9 +1,56 @@
from django.dispatch import receiver
from django.dispatch import Signal, receiver
from django.template.loader import get_template
from django.urls import resolve
from pretix.base.signals import register_ticket_outputs
from pretix.control.signals import html_head
@receiver(register_ticket_outputs, dispatch_uid="output_pdf")
def register_ticket_outputs(sender, **kwargs):
from .ticketoutput import PdfTicketOutput
return PdfTicketOutput
@receiver(html_head, dispatch_uid="ticketoutputpdf_html_head")
def html_head_presale(sender, request=None, **kwargs):
url = resolve(request.path_info)
if url.namespace == 'plugins:ticketoutputpdf':
template = get_template('pretixplugins/ticketoutputpdf/control_head.html')
return template.render({
'request': request
})
else:
return ""
register_fonts = Signal()
"""
Return a dictionaries of the following structure. Paths should be relative to static root.
{
"font name": {
"regular": {
"truetype": "….ttf",
"woff": "",
"woff2": ""
},
"bold": {
...
},
"italic": {
...
},
"bolditalic": {
...
}
}
}
"""
def get_fonts():
f = {}
for recv, value in register_fonts.send(0):
f.update(value)
return f

View File

@@ -0,0 +1,41 @@
#fabric-container {
position: absolute;
top: 0;
left: 0;
}
#editor-canvas-area {
position: relative;
min-height: 500px;
}
body {
overflow-y: scroll;
}
#toolbox .control-group {
margin-bottom: 5px;
}
#toolbox .position, #toolbox .squaresize, #toolbox[data-type] .pdf-info, #toolbox .text, #toolbox .object-buttons {
display: none;
}
#toolbox[data-type] .position, #toolbox[data-type=barcodearea] .squaresize, #toolbox[data-type=text] .text, #toolbox[data-type=textarea] .text,
#toolbox[data-type] .object-buttons {
display: block;
}
#loading-container {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: white;
width: 100%;
text-align: center;
}
#loading-container > div {
max-width: 600px;
margin: auto;
}
#loading-upload {
display: none;
}
.preload-font {
visibility: hidden;
}

View File

@@ -0,0 +1,708 @@
/*globals $, gettext, fabric, PDFJS*/
fabric.Barcodearea = fabric.util.createClass(fabric.Rect, {
type: 'barcodearea',
initialize: function (text, options) {
options || (options = {});
this.callSuper('initialize', text, options);
this.set('label', options.label || '');
},
toObject: function () {
return fabric.util.object.extend(this.callSuper('toObject'), {});
},
_render: function (ctx) {
this.callSuper('_render', ctx);
ctx.font = '16px Helvetica';
ctx.fillStyle = '#fff';
ctx.fillText(gettext('QR Code'), -this.width / 2, -this.height / 2 + 20);
},
});
fabric.Barcodearea.fromObject = function (object, callback, forceAsync) {
return fabric.Object._fromObject('Barcodearea', object, callback, forceAsync);
};
fabric.Textarea = fabric.util.createClass(fabric.Textbox, {
type: 'textarea',
initialize: function (text, options) {
options || (options = {});
this.callSuper('initialize', text, options);
this.set('content', options.content || '');
},
toObject: function(propertiesToInclude) {
return this.callSuper('toObject', ['content'].concat(propertiesToInclude));
}
});
fabric.Textarea.fromObject = function (object, callback, forceAsync) {
return fabric.Object._fromObject('Textarea', object, callback, forceAsync, 'text');
};
var editor = {
$pdfcv: null,
$fcv: null,
$cva: null,
$fabric: null,
objects: [],
history: [],
clipboard: [],
pdf_page: null,
pdf_scale: 1,
pdf_viewport: null,
_history_pos: 0,
_history_modification_in_progress: false,
dirty: false,
pdf_url: null,
uploaded_file_id: null,
_window_loaded: false,
_fabric_loaded: false,
_px2mm: function (v) {
return v / editor.pdf_scale / 72 * editor.pdf_page.userUnit * 25.4;
},
_mm2px: function (v) {
return v * editor.pdf_scale * 72 / editor.pdf_page.userUnit / 25.4;
},
_px2pt: function (v) {
return v / editor.pdf_scale * editor.pdf_page.userUnit;
},
_pt2px: function (v) {
return v * editor.pdf_scale / editor.pdf_page.userUnit;
},
dump: function (objs) {
var d = [];
objs = objs || editor.fabric.getObjects();
for (var i in objs) {
var o = objs[i];
if (o.type === "textarea") {
var col = (new fabric.Color(o.getFill()))._source;
d.push({
type: "textarea",
left: editor._px2mm(o.left).toFixed(2),
bottom: editor._px2mm(editor.pdf_viewport.height - o.height - o.top).toFixed(2),
fontsize: editor._px2pt(o.getFontSize()).toFixed(1),
color: col,
//lineheight: o.lineHeight,
fontfamily: o.fontFamily,
bold: o.fontWeight === 'bold',
italic: o.fontStyle === 'italic',
width: editor._px2mm(o.width).toFixed(2),
content: o.content,
text: o.text,
align: o.textAlign,
});
} else if (o.type === "barcodearea") {
d.push({
type: "barcodearea",
left: editor._px2mm(o.left).toFixed(2),
bottom: editor._px2mm(editor.pdf_viewport.height - o.height * o.scaleY - o.top).toFixed(2),
size: editor._px2mm(o.height * o.scaleY).toFixed(2)
});
}
}
return d;
},
_add_from_data: function (d) {
if (d.type === "barcodearea") {
o = editor._add_qrcode();
o.scaleToHeight(editor._mm2px(d.size));
} else if (d.type === "textarea" || o.type === "text") {
o = editor._add_text();
o.setColor('rgb(' + d.color[0] + ',' + d.color[1] + ',' + d.color[2] + ')');
o.setFontSize(editor._pt2px(d.fontsize));
//o.setLineHeight(d.lineheight);
o.setFontFamily(d.fontfamily);
o.setFontWeight(d.bold ? 'bold' : 'normal');
o.setFontStyle(d.italic ? 'italic' : 'normal');
o.setWidth(editor._mm2px(d.width));
o.content = d.content;
o.setTextAlign(d.align);
if (d.content === "other") {
o.setText(d.text);
} else {
o.setText(editor.text_samples[d.content]);
}
}
var new_top = editor.pdf_viewport.height - editor._mm2px(d.bottom) - (o.height * o.scaleY);
o.set('left', editor._mm2px(d.left));
o.set('top', new_top);
o.setCoords();
return o;
},
load: function(data) {
editor.fabric.clear();
for (var i in data) {
var d = data[i], o;
editor._add_from_data(d);
}
editor.fabric.renderAll();
editor._update_toolbox_values();
},
text_samples: {
"secret": "tdmruoekvkpbv1o2mv8xccvqcikvr58u",
"order": "A1B2C",
"item": gettext("Sample product"),
"variation": gettext("Sample variation"),
"itemvar": gettext("Sample product sample variation"),
"price": gettext("123.45 EUR"),
"attendee_name": gettext("John Doe"),
"event_name": gettext("Sample event name"),
"event_date": gettext("May 31st, 2017"),
"event_begin_time": gettext("20:00"),
"event_location": gettext("Random City")
},
_load_pdf: function (dump) {
// TODO: Loading indicators
var url = editor.pdf_url;
// TODO: Handle cross-origin issues if static files are on a different origin
PDFJS.workerSrc = editor.$pdfcv.attr("data-worker-url");
// Asynchronous download of PDF
var loadingTask = PDFJS.getDocument(url);
loadingTask.promise.then(function (pdf) {
console.log('PDF loaded');
// Fetch the first page
var pageNumber = 1;
pdf.getPage(pageNumber).then(function (page) {
console.log('Page loaded');
var canvas = document.getElementById('pdf-canvas');
var scale = editor.$cva.width() / page.getViewport(1.0).width;
var viewport = page.getViewport(scale);
// Prepare canvas using PDF page dimensions
var context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
canvas.height = viewport.height;
canvas.width = viewport.width;
editor.pdf_page = page;
editor.pdf_scale = scale;
editor.pdf_viewport = viewport;
// Render PDF page into canvas context
var renderContext = {
canvasContext: context,
viewport: viewport
};
var renderTask = page.render(renderContext);
renderTask.then(function () {
console.log('Page rendered');
editor._init_fabric(dump);
});
});
}, function (reason) {
var msg = gettext('The PDF background file could not be loaded for the following reason:');
editor._error(msg + ' ' + reason);
});
},
_init_fabric: function (dump) {
editor.$fcv.get(0).width = editor.$pdfcv.get(0).width;
editor.$fcv.get(0).height = editor.$pdfcv.get(0).height;
editor.fabric = new fabric.Canvas('fabric-canvas');
editor.fabric.on('object:modified', editor._create_savepoint);
editor.fabric.on('object:added', editor._create_savepoint);
editor.fabric.on('selection:cleared', editor._update_toolbox);
editor.fabric.on('selection:created', editor._update_toolbox);
editor.fabric.on('object:selected', editor._update_toolbox);
editor.fabric.on('object:moving', editor._update_toolbox_values);
editor.fabric.on('object:modified', editor._update_toolbox_values);
editor.fabric.on('object:scaling', editor._update_toolbox_values);
editor._update_toolbox();
$("#toolbox-content-other").hide();
$(".add-buttons button").prop('disabled', false);
if (dump) {
editor.load(dump);
} else {
var data = $.trim($("#editor-data").text());
if (data) {
editor.load(JSON.parse(data));
}
}
editor.history = [];
editor._create_savepoint();
editor.dirty = !!dump;
if ($("#loading-upload").is(":visible")) {
$("#loading-container, #loading-upload").hide();
}
editor._fabric_loaded = true;
console.log("Fabric loaded");
if (editor._window_loaded) {
editor._ready();
}
},
_window_load_event: function () {
editor._window_loaded = true;
console.log("Window loaded");
if (editor._fabric_loaded) {
editor._ready();
}
},
_ready: function () {
$("#editor-loading").hide();
$("#editor-start").removeClass("sr-only");
$("#editor-start").click(function () {
$("#loading-container").hide();
$("#loading-initial").remove();
});
},
_update_toolbox_values: function () {
var o = editor.fabric.getActiveObject();
if (!o) {
o = editor.fabric.getActiveGroup();
if (!o) {
return;
}
}
$("#toolbox-position-x").val(editor._px2mm(o.left).toFixed(2));
$("#toolbox-position-y").val(editor._px2mm(editor.pdf_viewport.height - o.height * o.scaleY - o.top).toFixed(2));
if (o.type === "barcodearea") {
$("#toolbox-squaresize").val(editor._px2mm(o.height * o.scaleY).toFixed(2));
} else if (o.type === "text" || o.type === "textarea") {
var col = (new fabric.Color(o.getFill()))._source;
$("#toolbox-col").val("#" + ((1 << 24) + (col[0] << 16) + (col[1] << 8) + col[2]).toString(16).slice(1));
$("#toolbox-fontsize").val(editor._px2pt(o.fontSize).toFixed(1));
//$("#toolbox-lineheight").val(o.lineHeight);
$("#toolbox-fontfamily").val(o.fontFamily);
$("#toolbox").find("button[data-action=bold]").toggleClass('active', o.fontWeight === 'bold');
$("#toolbox").find("button[data-action=italic]").toggleClass('active', o.fontStyle === 'italic');
$("#toolbox").find("button[data-action=left]").toggleClass('active', o.textAlign === 'left');
$("#toolbox").find("button[data-action=center]").toggleClass('active', o.textAlign === 'center');
$("#toolbox").find("button[data-action=right]").toggleClass('active', o.textAlign === 'right');
$("#toolbox-textwidth").val(editor._px2mm(o.width).toFixed(2));
if (o.type === "textarea") {
$("#toolbox-content").val(o.content);
$("#toolbox-content-other").toggle($("#toolbox-content").val() === "other");
if (o.content === "other") {
$("#toolbox-content-other").val(o.text);
} else {
$("#toolbox-content-other").val("");
}
}
}
},
_update_values_from_toolbox: function () {
var o = editor.fabric.getActiveObject();
if (!o) {
o = editor.fabric.getActiveGroup();
if (!o) {
return;
}
}
var new_top = editor.pdf_viewport.height - editor._mm2px($("#toolbox-position-y").val()) - o.height * o.scaleY;
o.set('left', editor._mm2px($("#toolbox-position-x").val()));
o.set('top', new_top);
if (o.type === "barcodearea") {
var new_h = editor._mm2px($("#toolbox-squaresize").val());
new_top += o.height * o.scaleY - new_h;
o.setHeight(new_h);
o.setWidth(new_h);
o.setScaleX(1);
o.setScaleY(1);
o.set('top', new_top)
} else if (o.type === "textarea" || o.type === "text") {
o.setColor($("#toolbox-col").val());
o.setFontSize(editor._pt2px($("#toolbox-fontsize").val()));
//o.setLineHeight($("#toolbox-lineheight").val());
o.setFontFamily($("#toolbox-fontfamily").val());
o.setFontWeight($("#toolbox").find("button[data-action=bold]").is('.active') ? 'bold' : 'normal');
o.setFontStyle($("#toolbox").find("button[data-action=italic]").is('.active') ? 'italic' : 'normal');
var align = $("#toolbox-align").find(".active").attr("data-action");
if (align) {
o.setTextAlign(align);
}
o.setWidth(editor._mm2px($("#toolbox-textwidth").val()));
$("#toolbox-content-other").toggle($("#toolbox-content").val() === "other");
o.content = $("#toolbox-content").val();
if ($("#toolbox-content").val() === "other") {
o.setText($("#toolbox-content-other").val());
} else {
o.setText(editor.text_samples[$("#toolbox-content").val()]);
}
}
o.setCoords();
editor.fabric.renderAll();
},
_update_toolbox: function () {
if (editor.fabric.getActiveGroup()) {
$("#toolbox").attr("data-type", "group");
$("#toolbox-heading").text(gettext("Group of objects"));
var g = editor.fabric.getActiveGroup();
} else if (editor.fabric.getActiveObject()) {
var o = editor.fabric.getActiveObject();
$("#toolbox").attr("data-type", o.type);
if (o.type === "textarea" || o.type === "text") {
$("#toolbox-heading").text(gettext("Text object"));
} else if (o.type === "barcodearea") {
$("#toolbox-heading").text(gettext("Barcode area"));
} else {
$("#toolbox-heading").text(gettext("Object"));
}
} else {
$("#toolbox").removeAttr("data-type");
$("#toolbox-heading").text(gettext("Ticket design"));
$("#pdf-info-width").val(editor._px2mm(editor.pdf_viewport.width).toFixed(2));
$("#pdf-info-height").val(editor._px2mm(editor.pdf_viewport.height).toFixed(2));
}
editor._update_toolbox_values();
},
_error: function (msg) {
editor.$cva.before("<div class='alert alert-danger'>" + msg + "</div>");
},
_add_text: function () {
var text = new fabric.Textarea(editor.text_samples['item'], {
left: 100,
top: 100,
width: editor._mm2px(50),
lockRotation: true,
fontFamily: 'Open Sans',
lineHeight: 1,
content: 'item',
editable: false,
fontSize: editor._pt2px(13)
});
text.setControlsVisibility({
'tr': false,
'tl': false,
'mt': false,
'br': false,
'bl': false,
'mb': false,
'mr': true,
'ml': true,
'mtr': false
});
editor.fabric.add(text);
editor._create_savepoint();
return text;
},
_add_qrcode: function () {
var rect = new fabric.Barcodearea({
left: 100,
top: 100,
width: 100,
height: 100,
lockRotation: true,
lockUniScaling: true,
fill: '#666',
});
rect.setControlsVisibility({'mtr': false});
editor.fabric.add(rect);
editor._create_savepoint();
return rect;
},
_cut: function () {
editor._history_modification_in_progress = true;
var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup();
if (thing.type === "group") {
editor.clipboard = editor.dump(thing._objects);
thing.forEachObject(function (o) {
o.remove();
});
thing.remove();
} else {
editor.clipboard = editor.dump([thing]);
thing.remove();
}
editor.fabric.discardActiveGroup();
editor.fabric.discardActiveObject();
editor._history_modification_in_progress = false;
editor._create_savepoint();
},
_copy: function () {
editor._history_modification_in_progress = true;
var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup();
if (thing.type === "group") {
editor.clipboard = editor.dump(thing._objects);
} else {
editor.clipboard = editor.dump([thing]);
}
editor._history_modification_in_progress = false;
editor._create_savepoint();
},
_paste: function () {
if (editor.clipboard.length < 1) {
return;
}
editor._history_modification_in_progress = true;
var objs = [];
for (var i in editor.clipboard) {
objs.push(editor._add_from_data(editor.clipboard[i]));
}
editor.fabric.discardActiveObject();
editor.fabric.discardActiveGroup();
if (editor.clipboard.length > 1) {
var group = new fabric.Group(objs, {
originX: 'left',
originY: 'top',
left: 100,
top: 100,
});
group.setCoords();
editor.fabric.setActiveGroup(group);
} else {
editor.fabric.setActiveObject(objs[0]);
}
editor._history_modification_in_progress = false;
editor._create_savepoint();
},
_delete: function () {
var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup();
if (thing.type === "group") {
thing.forEachObject(function (o) {
o.remove();
});
thing.remove();
editor.fabric.discardActiveGroup();
} else {
thing.remove();
editor.fabric.discardActiveObject();
}
editor._create_savepoint();
},
_on_keydown: function (e) {
var step = e.shiftKey ? editor._mm2px(10) : editor._mm2px(1);
var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup();
switch (e.keyCode) {
case 38: /* Up arrow */
thing.set('top', thing.get('top') - step);
thing.setCoords();
editor._create_savepoint();
break;
case 40: /* Down arrow */
thing.set('top', thing.get('top') + step);
thing.setCoords();
editor._create_savepoint();
break;
case 37: /* Left arrow */
thing.set('left', thing.get('left') - step);
thing.setCoords();
editor._create_savepoint();
break;
case 39: /* Right arrow */
thing.set('left', thing.get('left') + step);
thing.setCoords();
editor._create_savepoint();
break;
case 46: /* Delete */
editor._delete();
break;
case 89: /* Y */
if (e.ctrlKey) {
editor._redo();
}
break;
case 90: /* Z */
if (e.ctrlKey) {
editor._undo();
}
break;
case 88: /* X */
if (e.ctrlKey) {
editor._cut();
}
break;
case 86: /* V */
if (e.ctrlKey) {
editor._paste();
}
break;
case 67: /* C */
if (e.ctrlKey) {
editor._copy();
}
break;
default:
return;
}
e.preventDefault();
editor.fabric.renderAll();
editor._update_toolbox_values();
},
_create_savepoint: function () {
if (editor._history_modification_in_progress) {
return;
}
var state = editor.dump();
if (editor._history_pos > 0) {
editor.history.splice(-1 * editor._history_pos, editor._history_pos);
editor._history_pos = 0;
}
editor.history.push(state);
editor.dirty = true;
},
_undo: function undo() {
if (editor._history_pos < editor.history.length - 1) {
editor._history_modification_in_progress = true;
editor._history_pos += 1;
editor.fabric.clear().renderAll();
editor.load(editor.history[editor.history.length - 1 - editor._history_pos]);
editor._history_modification_in_progress = false;
editor.dirty = true;
}
},
_redo: function redo() {
if (editor._history_pos > 0) {
editor._history_modification_in_progress = true;
editor._history_pos -= 1;
editor.load(editor.history[editor.history.length - 1 - editor._history_pos]);
editor._history_modification_in_progress = false;
editor.dirty = true;
}
},
_save: function () {
$("#editor-save").prop('disabled', true).prepend('<span class="fa fa-cog fa-spin"></span>');
var dump = editor.dump();
$.post(window.location.href, {
'data': JSON.stringify(dump),
'csrfmiddlewaretoken': $("input[name=csrfmiddlewaretoken]").val(),
'background': editor.uploaded_file_id,
}, function (data) {
if (data.status === 'ok') {
$("#editor-save span").remove();
$("#editor-save").prop('disabled', false);
editor.dirty = false;
editor.uploaded_file_id = null;
} else {
alert(gettext('Saving failed.'));
}
}, 'json');
},
_preview: function () {
$("#preview-form input[name=data]").val(JSON.stringify(editor.dump()));
$("#preview-form input[name=background]").val(editor.uploaded_file_id);
$("#preview-form").get(0).submit();
},
_replace_pdf_file: function (url) {
editor.pdf_url = url;
d = editor.dump();
editor.fabric.dispose();
editor._load_pdf(d);
},
init: function () {
editor.$pdfcv = $("#pdf-canvas");
editor.pdf_url = editor.$pdfcv.attr("data-pdf-url");
editor.$fcv = $("#fabric-canvas");
editor.$cva = $("#editor-canvas-area");
editor._load_pdf();
$("#editor-add-qrcode").click(editor._add_qrcode);
$("#editor-add-text").click(editor._add_text);
editor.$cva.get(0).tabIndex = 1000;
editor.$cva.on("keydown", editor._on_keydown);
$("#editor-save").on("click", editor._save);
$("#editor-preview").on("click", editor._preview);
window.onbeforeunload = function () {
if (editor.dirty) {
return gettext("Do you really want to leave the editor without saving your changes?");
}
};
$('#fileupload').fileupload({
url: location.href,
dataType: 'json',
done: function (e, data) {
if (data.result.status === "ok") {
editor.uploaded_file_id = data.result.id;
editor._replace_pdf_file(data.result.url);
} else {
alert(data.result.error || gettext("Error while uploading your PDF file, please try again."));
$("#loading-container, #loading-upload").hide();
}
$("#fileupload").prop('disabled', false);
$(".fileinput-button").removeClass("disabled");
},
add: function (e, data) {
data.formData = {
'csrfmiddlewaretoken': $("input[name=csrfmiddlewaretoken]").val()
};
$("#loading-container, #loading-upload").show();
$("#loading-upload .progress").show();
$('#loading-upload .progress-bar').css('width', 0);
$("#fileupload").prop('disabled', true);
$(".fileinput-button").addClass("disabled");
data.process().done(function () {
data.submit();
});
},
progressall: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
$('#loading-upload .progress-bar').css('width', progress + '%');
}
}).prop('disabled', !$.support.fileInput).parent().addClass($.support.fileInput ? undefined : 'disabled');
$("#toolbox input[type=number], #toolbox textarea, #toolbox input[type=text]").bind('change keydown keyup' +
' input', editor._update_values_from_toolbox);
$("#toolbox input[type=number], #toolbox textarea, #toolbox input[type=text], #toolbox input[type=radio]").bind('change', editor._create_savepoint);
$("#toolbox label.btn").bind('click change', editor._update_values_from_toolbox);
$("#toolbox select").bind('change', editor._update_values_from_toolbox);
$("#toolbox select").bind('change', editor._create_savepoint);
$("#toolbox button.toggling").bind('click change', function () {
if ($(this).is(".option")) {
$(this).addClass("active");
$(this).parent().siblings().find("button").removeClass("active");
} else {
$(this).toggleClass("active");
}
editor._update_values_from_toolbox();
editor._create_savepoint();
});
$("#toolbox .colorpickerfield").bind('changeColor', editor._update_values_from_toolbox);
$("#toolbox-copy").bind('click', editor._copy);
$("#toolbox-cut").bind('click', editor._cut);
$("#toolbox-delete").bind('click', editor._delete);
$("#toolbox-paste").bind('click', editor._paste);
$("#toolbox-undo").bind('click', editor._undo);
$("#toolbox-redo").bind('click', editor._redo);
}
};
$(function () {
editor.init();
});
$(window).bind('load', editor._window_load_event);

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
{% load staticfiles %}
{% load compress %}
{% compress css %}
<link type="text/css" rel="stylesheet" href="{% static "pretixplugins/ticketoutputpdf/editor.css" %}">
{% endcompress %}
<link type="text/css" rel="stylesheet" href="{% url "plugins:ticketoutputpdf:css" organizer=request.organizer.slug event=request.event.slug %}">

View File

@@ -0,0 +1,22 @@
{% load i18n %}
<div class="form-group">
<label class="col-md-3 control-label">
{% trans "Ticket design" %}
</label>
<div class="col-md-9">
<p>
{% blocktrans trimmed %}
You can customize the ticket design with our PDF ticket editor. There, you can upload a PDF file used
as a background for the tickets and then place various texts and QR codes on the background at the
positions of your choice. The editor is easy to use thanks to its drag-and-drop user interface, but it
requires a modern browser and a decent internet connection.
{% endblocktrans %}
</p>
<p>
<a class="btn btn-default" target="_blank"
href="{% url "plugins:ticketoutputpdf:editor" organizer=request.organizer.slug event=request.event.slug %}">
{% trans "Open the PDF editor in a new tab" %}
</a>
</p>
</div>
</div>

View File

@@ -0,0 +1,323 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% block title %}{% trans "PDF Ticket Editor" %}{% endblock %}
{% block content %}
<h1>{% trans "PDF Ticket Editor" %}</h1>
<script type="application/json" id="editor-data">
{{ layout|safe }}
</script>
<div class="row">
<div class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
<div class="pull-right">
<div class="btn-group">
<button type="button" class="btn btn-default btn-xs" id="toolbox-paste"
title="{% trans "Paste" %}">
<span class="fa fa-paste"></span>
</button>
<button type="button" class="btn btn-default btn-xs" id="toolbox-undo"
title="{% trans "Undo" %}">
<span class="fa fa-undo"></span>
</button>
<button type="button" class="btn btn-default btn-xs" id="toolbox-redo"
title="{% trans "Redo" %}">
<span class="fa fa-repeat"></span>
</button>
</div>
</div>
{% trans "Editor" %}
</div>
<div class="panel-body">
<div id="editor-canvas-area">
<canvas id="pdf-canvas"
{% if request.event.settings.ticketoutput_pdf_background %}
data-pdf-url="{{ request.event.settings.ticketoutput_pdf_background.url }}"
{% else %}
data-pdf-url="{% static "pretixpresale/pdf/ticket_default_a4.pdf" %}"
{% endif %}
data-worker-url="{% static "pretixplugins/ticketoutputpdf/pdf.worker.js" %}">
</canvas>
<div id="fabric-container">
<canvas id="fabric-canvas">
</canvas>
</div>
<div id="loading-container">
<div id="loading-upload">
<span class="fa fa-cog big-rotating-icon"></span>
<p>
{% trans "Uploading new PDF background…" %}
</p>
<div class="progress">
<div class="progress-bar" style="width: 0%;">
</div>
</div>
</div>
<div id="loading-initial">
<h2>{% trans "Welcome to the PDF ticket editor!" %}</h2>
<p>
{% blocktrans trimmed %}
This editor allows you to create a design for the PDF tickets of your event.
You can upload a background PDF and then use this tool to place texts and
a QR code on the ticket.
{% endblocktrans %}
</p>
<p>&nbsp;</p>
<p>
<span class="fa fa-eye fa-2x"></span>
</p>
<p>
{% blocktrans trimmed %}
Please note that the editor can only provide a rough preview. Some details,
for example in text rendering, might look slightly different in the final
tickets. You can use the "Preview" button on the right for a more precise
preview.
{% endblocktrans %}
</p>
<p>&nbsp;</p>
<p>
<span class="fa fa-chrome fa-2x"></span>
<span class="fa fa-firefox fa-2x"></span>
<span class="fa fa-opera fa-2x"></span>
</p>
<p>
{% blocktrans trimmed %}
The editor is tested with recent versions of Google Chome, Mozilla Firefox
and Opera. Other browsers, especially Internet Explorer or Microsoft Edge, might
have problems displaying your background PDF or loading the correct fonts.
{% endblocktrans %}
</p>
<noscript>
<div class="alert alert-danger">
{% blocktrans trimmed %}
The editor requires JavaScript to work. Please enable JavaScript in your
browser to continue.
{% endblocktrans %}
</div>
</noscript>
<p>&nbsp;</p>
<p>
<em id="editor-loading">
<span class="fa fa-cog fa-spin"></span>
{% trans "Loading…" %}
</em>
<button id="editor-start" class="btn btn-primary sr-only">
{% trans "Start editing" %}
</button>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3" id="editor-toolbox-area">
<div class="panel panel-default" id="toolbox">
<div class="panel-heading">
<div class="pull-right object-buttons">
<div class="btn-group">
<button type="button" class="btn btn-default btn-xs" id="toolbox-cut"
title="{% trans "Cut" %}">
<span class="fa fa-cut"></span>
</button>
<button type="button" class="btn btn-default btn-xs" id="toolbox-copy"
title="{% trans "Copy" %}">
<span class="fa fa-copy"></span>
</button>
<button type="button" class="btn btn-danger btn-xs" id="toolbox-delete"
title="{% trans "Delete" %}">
<span class="fa fa-trash"></span>
</button>
</div>
</div>
<span id="toolbox-heading">
{% trans "Loading…" %}
</span>
</div>
<div class="panel-body" id="toolbox-body">
<div class="row control-group pdf-info">
<div class="col-sm-6">
<label>{% trans "Width (mm)" %}</label><br>
<input type="number" id="pdf-info-width" class="input-block-level form-control" disabled>
</div>
<div class="col-sm-6">
<label>{% trans "Height (mm)" %}</label><br>
<input type="number" id="pdf-info-height" class="input-block-level form-control" disabled>
</div>
</div>
<div class="row control-group pdf-info">
<div class="col-sm-12">
<label>{% trans "Background PDF" %}</label><br>
<span class="btn btn-default fileinput-button">
<i class="fa fa-upload"></i>
<span>{% trans "Upload new background" %}</span>
<input id="fileupload" type="file" name="background">
</span>
</div>
</div>
<div class="row control-group position">
<div class="col-sm-6">
<label>{% trans "x (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-position-x">
</div>
<div class="col-sm-6">
<label>{% trans "y (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-position-y">
</div>
</div>
<div class="row control-group squaresize">
<div class="col-sm-12">
<label>{% trans "Size (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-squaresize">
</div>
</div>
<div class="row control-group squaresize">
<div class="col-sm-12">
<p>
{% blocktrans trimmed %}
The final QR code will be slightly smaller because some whitespace is required
for proper scanning.
{% endblocktrans %}
</p>
</div>
</div>
<div class="row control-group text">
<div class="col-sm-6">
<label>{% trans "Font size (pt)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.1"
id="toolbox-fontsize">
</div>
<div class="col-sm-6">
<label>&nbsp;</label><br>
<div class="btn-group btn-group-justified" role="group">
<div class="btn-group" role="group">
<button type="button" class="btn btn-default toggling" data-action="bold">
<span class="fa fa-bold"></span>
</button>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default toggling" data-action="italic">
<span class="fa fa-italic"></span>
</button>
</div>
</div>
</div>
</div>
<div class="row control-group text">
<div class="col-sm-6">
<label>{% trans "Text color" %}</label><br>
<input type="text" value="#000000" class="input-block-level form-control colorpickerfield"
id="toolbox-col">
</div>
<div class="col-sm-6">
<label>&nbsp;</label><br>
<div class="btn-group btn-group-justified" id="toolbox-align">
<div class="btn-group" role="group">
<button type="button" class="btn btn-default option toggling" data-action="left">
<span class="fa fa-align-left"></span>
</button>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default option toggling" data-action="center">
<span class="fa fa-align-center"></span>
</button>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default option toggling" data-action="right">
<span class="fa fa-align-right"></span>
</button>
</div>
</div>
</div>
</div>
<div class="row control-group text">
<div class="col-sm-12">
<label>{% trans "Font" %}</label><br>
<select class="input-block-level form-control" id="toolbox-fontfamily">
<option>Open Sans</option>
{% for family in fonts.keys %}
<option>{{ family }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row control-group text">
<div class="col-sm-12">
<label>{% trans "Width (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-textwidth">
</div>
</div>
<div class="row control-group text">
<div class="col-sm-12">
<label>{% trans "Text content" %}</label><br>
<select class="input-block-level form-control" id="toolbox-content">
<option value="secret">{% trans "Ticket code (barcode content)" %}</option>
<option value="order">{% trans "Order code" %}</option>
<option value="item">{% trans "Product name" %}</option>
<option value="variation">{% trans "Variation name" %}</option>
<option value="itemvar">{% trans "Product name and variation" %}</option>
<option value="price">{% trans "Price" %}</option>
<option value="attendee_name">{% trans "Attendee name" %}</option>
<option value="event_name">{% trans "Event name" %}</option>
<option value="event_date">{% trans "Event date" %}</option>
<option value="event_begin_time">{% trans "Event begin time" %}</option>
<option value="event_location">{% trans "Event location" %}</option>
<option value="other">{% trans "Other…" %}</option>
</select>
<textarea type="text" value="" class="input-block-level form-control"
id="toolbox-content-other"></textarea>
</div>
</div>
</div>
</div>
<div class="editor-toolbox-text panel panel-default">
<div class="panel-heading">
{% trans "Add a new object" %}
</div>
<div class="panel-body add-buttons">
<button class="btn btn-default btn-block" id="editor-add-text" disabled>
<span class="fa fa-font"></span>
{% trans "Text" %}
</button>
<button class="btn btn-default btn-block" id="editor-add-qrcode" disabled>
<span class="fa fa-qrcode"></span>
{% trans "QR code area" %}
</button>
</div>
</div>
<form method="post" action="" id="preview-form" target="_blank">
<div class="form-group submit-group">
{% csrf_token %}
<input type="hidden" value="" name="data">
<input type="hidden" value="" name="background">
<input type="hidden" value="true" name="preview">
<button type="submit" class="btn btn-default btn-lg" id="editor-preview">
{% trans "Preview" %}
</button>
<button type="submit" class="btn btn-primary btn-save" id="editor-save">
{% trans "Save" %}
</button>
</div>
</form>
</div>
</div>
<script type="text/javascript" src="{% static "pretixplugins/ticketoutputpdf/pdf.js" %}"></script>
<script type="text/javascript" src="{% static "pretixplugins/ticketoutputpdf/fabric.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixplugins/ticketoutputpdf/editor.js" %}"></script>
{% for family, styles in fonts.items %}
{% for style, formats in styles.items %}
<span class="preload-font" data-family="{{ family }}" data-style="{{ style }}">
giItT1WQy@!-/#
</span>
{% endfor %}
{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,36 @@
{% load staticfiles %}
{% for family, styles in fonts.items %}
{% for style, formats in styles.items %}
@font-face {
font-family: '{{ family }}';
{% if style == "italic" or style == "bolditalic" %}
font-style: italic;
{% else %}
font-style: normal;
{% endif %}
{% if style == "bold" or style == "bolditalic" %}
font-weight: bold;
{% else %}
font-weight: normal;
{% endif %}
src: {% if "woff2" in formats %}url('{% static formats.woff2 %}') format('woff2'),{% endif %}
{% if "woff" in formats %}url('{% static formats.woff %}') format('woff'),{% endif %}
{% if "truetype" in formats %}url('{% static formats.truetype %}') format('truetype'){% endif %};
}
.preload-font[data-family="{{family}}"][data-style="{{style}}"] {
font-family: '{{ family }}';
{% if style == "italic" or style == "bolditalic" %}
font-style: italic;
{% else %}
font-style: normal;
{% endif %}
{% if style == "bold" or style == "bolditalic" %}
font-weight: bold;
{% else %}
font-weight: normal;
{% endif %}
}
{% endfor %}
{% endfor %}

View File

@@ -1,17 +1,31 @@
import copy
import logging
from collections import OrderedDict
import uuid
from io import BytesIO
from django import forms
from django.contrib.staticfiles import finders
from django.core.files import File
from django.core.files.storage import default_storage
from django.http import HttpRequest
from django.template.loader import get_template
from django.utils.formats import localize
from django.utils.translation import ugettext_lazy as _
from reportlab.graphics import renderPDF
from reportlab.graphics.barcode.qr import QrCodeWidget
from reportlab.graphics.shapes import Drawing
from reportlab.lib.colors import Color
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.pdfmetrics import getAscentDescent
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph
from pretix.base.models import Order
from pretix.base.models import Order, OrderPosition
from pretix.base.ticketoutput import BaseTicketOutput
from pretix.control.forms import ExtFileField
from pretix.plugins.ticketoutputpdf.signals import get_fonts
logger = logging.getLogger('pretix.plugins.ticketoutputpdf')
@@ -21,71 +35,97 @@ class PdfTicketOutput(BaseTicketOutput):
verbose_name = _('PDF output')
download_button_text = _('PDF')
def _draw_page(self, p, op, order):
from reportlab.graphics.shapes import Drawing
from reportlab.lib import units
from reportlab.graphics.barcode.qr import QrCodeWidget
from reportlab.graphics import renderPDF
def __init__(self, event, override_layout=None, override_background=None):
self.override_layout = override_layout
self.override_background = override_background
super().__init__(event)
event_s = self.settings.get('event_s', default=22, as_type=float)
if event_s:
p.setFont("Helvetica", event_s)
event_x = self.settings.get('event_x', default=15, as_type=float)
event_y = self.settings.get('event_y', default=235, as_type=float)
p.drawString(event_x * units.mm, event_y * units.mm, str(self.event.name))
def _register_fonts(self):
pdfmetrics.registerFont(TTFont('Open Sans', finders.find('fonts/OpenSans-Regular.ttf')))
pdfmetrics.registerFont(TTFont('Open Sans I', finders.find('fonts/OpenSans-Italic.ttf')))
pdfmetrics.registerFont(TTFont('Open Sans B', finders.find('fonts/OpenSans-Bold.ttf')))
pdfmetrics.registerFont(TTFont('Open Sans B I', finders.find('fonts/OpenSans-BoldItalic.ttf')))
order_s = self.settings.get('order_s', default=17, as_type=float)
if order_s:
p.setFont("Helvetica", order_s)
order_x = self.settings.get('order_x', default=15, as_type=float)
order_y = self.settings.get('order_y', default=220, as_type=float)
p.drawString(order_x * units.mm, order_y * units.mm, _('Order code: {code}').format(code=order.code))
for family, styles in get_fonts().items():
print(family, finders.find(styles['regular']['truetype']))
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
name_s = self.settings.get('name_s', default=17, as_type=float)
if name_s:
p.setFont("Helvetica", name_s)
name_x = self.settings.get('name_x', default=15, as_type=float)
name_y = self.settings.get('name_y', default=210, as_type=float)
item = str(op.item.name)
if op.variation:
item += " " + str(op.variation)
p.drawString(name_x * units.mm, name_y * units.mm, item)
def _draw_barcodearea(self, canvas: Canvas, op: OrderPosition, o: dict):
reqs = float(o['size']) * mm
qrw = QrCodeWidget(op.secret, barLevel='H', barHeight=reqs, barWidth=reqs)
d = Drawing(reqs, reqs)
d.add(qrw)
qr_x = float(o['left']) * mm
qr_y = float(o['bottom']) * mm
renderPDF.draw(d, canvas, qr_x, qr_y)
price_s = self.settings.get('price_s', default=17, as_type=float)
if price_s:
p.setFont("Helvetica", price_s)
price_x = self.settings.get('price_x', default=15, as_type=float)
price_y = self.settings.get('price_y', default=200, as_type=float)
p.drawString(price_x * units.mm, price_y * units.mm, "%s %s" % (str(op.price), self.event.currency))
def _get_text_content(self, op: OrderPosition, order: Order, o: dict):
if o['content'] == 'other':
return o['text'].replace("\n", "<br/>\n")
elif o['content'] == 'order':
return order.code
elif o['content'] == 'item':
return str(op.item)
elif o['content'] == 'secret':
return op.secret
elif o['content'] == 'variation':
return str(op.variation) if op.variation else ''
elif o['content'] == 'itemvar':
return '{} - {}'.format(op.item, op.variation) if op.variation else str(op.item)
elif o['content'] == 'price':
return '{} {}'.format(order.event.currency, localize(op.price))
elif o['content'] == 'attendee_name':
return op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '')
elif o['content'] == 'event_name':
return str(order.event)
elif o['content'] == 'event_location':
return str(order.event.location)
elif o['content'] == 'event_date':
return order.event.get_date_from_display(show_times=False)
elif o['content'] == 'event_begin_time':
return order.event.get_date_from_display(show_times=False)
return ''
qr_s = self.settings.get('qr_s', default=80, as_type=float)
if qr_s:
reqs = qr_s * units.mm
qrw = QrCodeWidget(op.secret, barLevel='H')
b = qrw.getBounds()
w = b[2] - b[0]
h = b[3] - b[1]
d = Drawing(reqs, reqs, transform=[reqs / w, 0, 0, reqs / h, 0, 0])
d.add(qrw)
qr_x = self.settings.get('qr_x', default=10, as_type=float)
qr_y = self.settings.get('qr_y', default=120, as_type=float)
renderPDF.draw(d, p, qr_x * units.mm, qr_y * units.mm)
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
font = o['fontfamily']
if o['bold']:
font += ' B'
if o['italic']:
font += ' I'
code_s = self.settings.get('code_s', default=11, as_type=float)
if code_s:
p.setFont("Helvetica", code_s)
code_x = self.settings.get('code_x', default=15, as_type=float)
code_y = self.settings.get('code_y', default=120, as_type=float)
p.drawString(code_x * units.mm, code_y * units.mm, op.secret)
align_map = {
'left': TA_LEFT,
'center': TA_CENTER,
'right': TA_RIGHT
}
style = ParagraphStyle(
name=uuid.uuid4().hex,
fontName=font,
fontSize=float(o['fontsize']),
leading=float(o['fontsize']),
autoLeading="max",
textColor=Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255),
alignment=align_map[o['align']]
)
attendee_s = self.settings.get('attendee_s', default=0, as_type=float)
if attendee_s and op.attendee_name:
p.setFont("Helvetica", attendee_s)
attendee_x = self.settings.get('attendee_x', default=15, as_type=float)
attendee_y = self.settings.get('attendee_y', default=90, as_type=float)
p.drawString(attendee_x * units.mm, attendee_y * units.mm, op.attendee_name)
p = Paragraph(self._get_text_content(op, order, o), style=style)
p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
# p_size = p.wrap(float(o['width']) * mm, 1000 * mm)
ad = getAscentDescent(font, float(o['fontsize']))
p.drawOn(canvas, float(o['left']) * mm, float(o['bottom']) * mm - ad[1])
p.showPage()
def _draw_page(self, canvas: Canvas, op: OrderPosition, order: Order):
objs = self.override_layout or self.settings.get('layout', as_type=list) or self._legacy_layout()
for o in objs:
if o['type'] == "barcodearea":
self._draw_barcodearea(canvas, op, o)
elif o['type'] == "textarea":
self._draw_textarea(canvas, op, order, o)
canvas.showPage()
def generate_order(self, order: Order):
buffer = BytesIO()
@@ -113,97 +153,198 @@ class PdfTicketOutput(BaseTicketOutput):
from reportlab.pdfgen import canvas
from reportlab.lib import pagesizes
pagesize = self.settings.get('pagesize', default='A4')
if hasattr(pagesizes, pagesize):
pagesize = getattr(pagesizes, pagesize)
else:
pagesize = pagesizes.A4
orientation = self.settings.get('orientation', default='portrait')
if hasattr(pagesizes, orientation):
pagesize = getattr(pagesizes, orientation)(pagesize)
# Doesn't matter as we'll overpaint it over a background later
pagesize = pagesizes.A4
self._register_fonts()
return canvas.Canvas(buffer, pagesize=pagesize)
def _render_with_background(self, buffer):
def _render_with_background(self, buffer, title=_('Ticket')):
from PyPDF2 import PdfFileWriter, PdfFileReader
buffer.seek(0)
new_pdf = PdfFileReader(buffer)
output = PdfFileWriter()
bg_file = self.settings.get('background', as_type=File)
if isinstance(bg_file, File):
if self.override_background:
bgf = default_storage.open(self.override_background.name, "rb")
elif isinstance(bg_file, File):
bgf = default_storage.open(bg_file.name, "rb")
else:
bgf = open(finders.find('pretixpresale/pdf/ticket_default_a4.pdf'), "rb")
bg_pdf = PdfFileReader(bgf)
for page in new_pdf.pages:
bg_page = copy.copy(bg_pdf.getPage(0))
bg_page.mergePage(page)
output.addPage(bg_page)
output.addMetadata({
'/Title': str(title),
'/Creator': 'pretix',
})
outbuffer = BytesIO()
output.write(outbuffer)
outbuffer.seek(0)
return outbuffer
@property
def settings_form_fields(self) -> dict:
return OrderedDict(
list(super().settings_form_fields.items()) + [
('paper_size',
forms.ChoiceField(
label=_('Paper size'),
choices=(
('A4', 'A4'),
('A5', 'A5'),
('B4', 'B4'),
('B5', 'B5'),
('letter', 'Letter'),
('legal', 'Legal'),
),
required=False
)),
('orientation',
forms.ChoiceField(
label=_('Paper orientation'),
choices=(
('portrait', _('Portrait')),
('landscape', _('Landscape')),
),
required=False
)),
('background',
ExtFileField(
label=_('Background PDF'),
ext_whitelist=(".pdf", ),
required=False
)),
('qr_x', forms.FloatField(label=_('QR-Code x position (mm)'), required=False)),
('qr_y', forms.FloatField(label=_('QR-Code y position (mm)'), required=False)),
('qr_s', forms.FloatField(label=_('QR-Code size (mm)'), required=False)),
('code_x', forms.FloatField(label=_('Ticket code x position (mm)'), required=False)),
('code_y', forms.FloatField(label=_('Ticket code y position (mm)'), required=False)),
('code_s', forms.FloatField(label=_('Ticket code size (mm)'), required=False,
help_text=_('Visible by default, set this to 0 to hide the element.'))),
('order_x', forms.FloatField(label=_('Order x position (mm)'), required=False)),
('order_y', forms.FloatField(label=_('Order y position (mm)'), required=False)),
('order_s', forms.FloatField(label=_('Order size (mm)'), required=False,
help_text=_('Visible by default, set this to 0 to hide the element.'))),
('name_x', forms.FloatField(label=_('Product name x position (mm)'), required=False)),
('name_y', forms.FloatField(label=_('Product name y position (mm)'), required=False)),
('name_s', forms.FloatField(label=_('Product name size (mm)'), required=False,
help_text=_('Visible by default, set this to 0 to hide the element.'))),
('price_x', forms.FloatField(label=_('Price x position (mm)'), required=False)),
('price_y', forms.FloatField(label=_('Price y position (mm)'), required=False)),
('price_s', forms.FloatField(label=_('Price size (mm)'), required=False,
help_text=_('Visible by default, set this to 0 to hide the element.'))),
('event_x', forms.FloatField(label=_('Event name x position (mm)'), required=False)),
('event_y', forms.FloatField(label=_('Event name y position (mm)'), required=False)),
('event_s', forms.FloatField(label=_('Event name size (mm)'), required=False,
help_text=_('Visible by default, set this to 0 to hide the element.'))),
('attendee_x', forms.FloatField(label=_('Attendee name x position (mm)'), required=False)),
('attendee_y', forms.FloatField(label=_('Attendee name y position (mm)'), required=False)),
('attendee_s', forms.FloatField(label=_('Attendee name size (mm)'), required=False,
help_text=_('Invisible by default, set this to a number greater than 0 '
'to show.')))
]
)
def settings_content_render(self, request: HttpRequest) -> str:
"""
When the event's administrator visits the event configuration
page, this method is called. It may return HTML containing additional information
that is displayed below the form fields configured in ``settings_form_fields``.
"""
template = get_template('pretixplugins/ticketoutputpdf/form.html')
return template.render({
'request': request
})
def _legacy_layout(self):
if self.settings.get('ticketoutput_pdf_background'):
return self._migrate_from_old_settings()
else:
return self._default_layout()
def _default_layout(self):
return [
{"type": "textarea", "left": "17.50", "bottom": "274.60", "fontsize": "16.0", "color": [0, 0, 0, 1],
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "175.00", "content": "event_name",
"text": "Sample event name", "align": "left"},
{"type": "textarea", "left": "17.50", "bottom": "262.90", "fontsize": "13.0", "color": [0, 0, 0, 1],
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "110.00", "content": "itemvar",
"text": "Sample product sample variation", "align": "left"},
{"type": "textarea", "left": "17.50", "bottom": "252.50", "fontsize": "13.0", "color": [0, 0, 0, 1],
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "110.00", "content": "attendee_name",
"text": "John Doe", "align": "left"},
{"type": "textarea", "left": "17.50", "bottom": "242.10", "fontsize": "13.0", "color": [0, 0, 0, 1],
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "110.00", "content": "event_date",
"text": "May 31st, 2017", "align": "left"},
{"type": "textarea", "left": "17.50", "bottom": "234.30", "fontsize": "13.0", "color": [0, 0, 0, 1],
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "110.00", "content": "event_location",
"text": "Random City", "align": "left"},
{"type": "textarea", "left": "17.50", "bottom": "194.50", "fontsize": "13.0", "color": [0, 0, 0, 1],
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "30.00", "content": "order",
"text": "A1B2C", "align": "left"},
{"type": "textarea", "left": "52.50", "bottom": "194.50", "fontsize": "13.0", "color": [0, 0, 0, 1],
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "45.00", "content": "price",
"text": "123.45 EUR", "align": "right"},
{"type": "textarea", "left": "102.50", "bottom": "194.50", "fontsize": "13.0", "color": [0, 0, 0, 1],
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "90.00", "content": "secret",
"text": "tdmruoekvkpbv1o2mv8xccvqcikvr58u", "align": "left"},
{"type": "barcodearea", "left": "130.40", "bottom": "204.50", "size": "64.00"}
]
def _migrate_from_old_settings(self):
l = []
event_s = self.settings.get('event_s', default=22, as_type=float)
if event_s:
l.append({
'type': 'textarea',
'fontfamily': 'Helvetica',
'left': self.settings.get('event_x', default=15, as_type=float),
'bottom': self.settings.get('event_y', default=235, as_type=float),
'fontsize': event_s,
'color': [0, 0, 0, 1],
'bold': False,
'italic': False,
'width': 150,
'content': 'event_name',
'text': 'Sample event',
'align': 'left'
})
order_s = self.settings.get('order_s', default=17, as_type=float)
if order_s:
l.append({
'type': 'textarea',
'fontfamily': 'Helvetica',
'left': self.settings.get('order_x', default=15, as_type=float),
'bottom': self.settings.get('order_y', default=220, as_type=float),
'fontsize': order_s,
'color': [0, 0, 0, 1],
'bold': False,
'italic': False,
'width': 150,
'content': 'order',
'text': 'AB1C2',
'align': 'left'
})
name_s = self.settings.get('name_s', default=17, as_type=float)
if name_s:
l.append({
'type': 'textarea',
'fontfamily': 'Helvetica',
'left': self.settings.get('name_x', default=15, as_type=float),
'bottom': self.settings.get('name_y', default=210, as_type=float),
'fontsize': name_s,
'color': [0, 0, 0, 1],
'bold': False,
'italic': False,
'width': 150,
'content': 'itemvar',
'text': 'Sample Producs - XS',
'align': 'left'
})
price_s = self.settings.get('price_s', default=17, as_type=float)
if price_s:
l.append({
'type': 'textarea',
'fontfamily': 'Helvetica',
'left': self.settings.get('price_x', default=15, as_type=float),
'bottom': self.settings.get('price_y', default=200, as_type=float),
'fontsize': price_s,
'color': [0, 0, 0, 1],
'bold': False,
'italic': False,
'width': 150,
'content': 'price',
'text': 'EUR 12,34',
'align': 'left'
})
qr_s = self.settings.get('qr_s', default=80, as_type=float)
if qr_s:
l.append({
'type': 'barcodearea',
'left': self.settings.get('qr_x', default=10, as_type=float),
'bottom': self.settings.get('qr_y', default=120, as_type=float),
'size': qr_s,
})
code_s = self.settings.get('code_s', default=11, as_type=float)
if code_s:
l.append({
'type': 'textarea',
'fontfamily': 'Helvetica',
'left': self.settings.get('code_x', default=15, as_type=float),
'bottom': self.settings.get('code_y', default=120, as_type=float),
'fontsize': code_s,
'color': [0, 0, 0, 1],
'bold': False,
'italic': False,
'width': 150,
'content': 'secret',
'text': 'asdsdgjfgbgkjdastjrxfdg',
'align': 'left'
})
attendee_s = self.settings.get('attendee_s', default=0, as_type=float)
if attendee_s:
l.append({
'type': 'textarea',
'fontfamily': 'Helvetica',
'left': self.settings.get('attendee_x', default=15, as_type=float),
'bottom': self.settings.get('attendee_y', default=90, as_type=float),
'fontsize': attendee_s,
'color': [0, 0, 0, 1],
'bold': False,
'italic': False,
'width': 150,
'content': 'attendee_name',
'text': 'John Doe',
'align': 'left'
})
return l

View File

@@ -0,0 +1,11 @@
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/editor/$', views.EditorView.as_view(),
name='editor'),
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/editor/webfonts.css',
views.FontsCSSView.as_view(),
name='css'),
]

View File

@@ -0,0 +1,138 @@
import json
import logging
import mimetypes
from datetime import timedelta
from django.core.files import File
from django.core.files.storage import default_storage
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView
from pretix.base.i18n import language
from pretix.base.models import CachedFile
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views import ChartContainingView
from pretix.helpers.database import rolledback_transaction
from pretix.plugins.ticketoutputpdf.signals import get_fonts
from .ticketoutput import PdfTicketOutput
logger = logging.getLogger(__name__)
class EditorView(EventPermissionRequiredMixin, ChartContainingView, TemplateView):
template_name = 'pretixplugins/ticketoutputpdf/index.html'
permission = 'can_change_settings'
accepted_formats = (
'application/pdf',
)
maxfilesize = 1024 * 1024 * 10
minfilesize = 10
def process_upload(self):
f = self.request.FILES.get('background')
error = False
if f.size > self.maxfilesize:
error = _('The uploaded PDF file is to large.')
if f.size < self.minfilesize:
error = _('The uploaded PDF file is to small.')
if mimetypes.guess_type(f.name)[0] not in self.accepted_formats:
error = _('Please only upload PDF files.')
# if there was an error, add error message to response_data and return
if error:
return error, None
return None, f
def post(self, request, *args, **kwargs):
if "background" in request.FILES:
error, fileobj = self.process_upload()
if error:
return JsonResponse({
"status": "error",
"error": error
})
c = CachedFile()
c.expires = now() + timedelta(days=7)
c.date = now()
c.filename = 'background.pdf'
c.type = 'application/pdf'
c.file = fileobj
c.save()
c.refresh_from_db()
return JsonResponse({
"status": "ok",
"id": c.id,
"url": c.file.url
})
cf = None
if request.POST.get("background", "").strip():
try:
cf = CachedFile.objects.get(id=request.POST.get("background"))
except CachedFile.DoesNotExist:
pass
if "preview" in request.POST:
with rolledback_transaction(), language(request.event.settings.locale):
item = request.event.items.create(name=_("Sample product"), default_price=42.23)
from pretix.base.models import Order
order = request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
email='sample@pretix.eu',
expires=now(), code="PREVIEW1234", total=119)
p = order.positions.create(item=item, attendee_name=_("John Doe"), price=item.default_price)
prov = PdfTicketOutput(request.event,
override_layout=json.loads(request.POST.get("data")),
override_background=cf.file if cf else None)
fname, mimet, data = prov.generate(p)
resp = HttpResponse(data, content_type=mimet)
ftype = fname.split(".")[-1]
resp['Content-Security-Policy'] = "style-src 'unsafe-inline'; object-src 'self'"
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
return resp
elif "data" in request.POST:
if cf:
fexisting = request.event.settings.get('ticketoutput_pdf_layout', as_type=File)
if fexisting:
try:
default_storage.delete(fexisting.name)
except OSError: # pragma: no cover
logger.error('Deleting file %s failed.' % fexisting.name)
# Create new file
nonce = get_random_string(length=8)
fname = '%s-%s/%s/%s.%s.%s' % (
'event', 'settings', self.request.event.pk, 'ticketoutput_pdf_layout', nonce, 'pdf'
)
newname = default_storage.save(fname, cf.file)
request.event.settings.set('ticketoutput_pdf_background', 'file://' + newname)
request.event.settings.set('ticketoutput_pdf_layout', request.POST.get("data"))
return JsonResponse({'status': 'ok'})
return HttpResponseBadRequest()
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
prov = PdfTicketOutput(self.request.event)
ctx['fonts'] = get_fonts()
ctx['layout'] = json.dumps(
self.request.event.settings.get('ticketoutput_pdf_layout', as_type=list)
or prov._default_layout()
)
return ctx
class FontsCSSView(TemplateView):
content_type = 'text/css'
template_name = 'pretixplugins/ticketoutputpdf/webfonts.css'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['fonts'] = get_fonts()
return ctx