PDF editor: New text element implementation (#4246)

* draft

* almost working

* Widgth adjustment

* Fix crash on empty text

* Change default layouts

* Fix editor bugs

* Update src/pretix/control/templates/pretixcontrol/pdf/index.html

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Show deprecated text on old text

* lockScalingFlip

* Regroup editor controls

* Update src/pretix/static/pretixcontrol/js/ui/main.js

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/static/pretixcontrol/js/ui/main.js

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/static/pretixcontrol/js/ui/main.js

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/static/pretixcontrol/js/ui/editor.js

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Increase default height even further

* Add a small version warning

* Update src/pretix/control/templates/pretixcontrol/pdf/index.html

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/control/templates/pretixcontrol/pdf/index.html

Co-authored-by: Richard Schreiber <schreiber@rami.io>

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2024-08-07 11:26:47 +02:00
committed by GitHub
parent a682eab18e
commit 022f44ad00
7 changed files with 775 additions and 263 deletions

View File

@@ -956,7 +956,7 @@ class Renderer:
) )
canvas.restoreState() canvas.restoreState()
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict): def _text_paragraph(self, op: OrderPosition, order: Order, o: dict, legacy_lineheight=False, override_fontsize=None):
font = o['fontfamily'] font = o['fontfamily']
# Since pdfmetrics.registerFont is global, we want to make sure that no one tries to sneak in a font, they # Since pdfmetrics.registerFont is global, we want to make sure that no one tries to sneak in a font, they
@@ -970,12 +970,13 @@ class Renderer:
if o['italic']: if o['italic']:
font += ' I' font += ' I'
fontsize = override_fontsize if override_fontsize is not None else float(o['fontsize'])
try: try:
ad = getAscentDescent(font, float(o['fontsize'])) ad = getAscentDescent(font, fontsize)
except KeyError: # font not known, fall back except KeyError: # font not known, fall back
logger.warning(f'Use of unknown font "{font}"') logger.warning(f'Use of unknown font "{font}"')
font = 'Open Sans' font = 'Open Sans'
ad = getAscentDescent(font, float(o['fontsize'])) ad = getAscentDescent(font, fontsize)
align_map = { align_map = {
'left': TA_LEFT, 'left': TA_LEFT,
@@ -985,16 +986,17 @@ class Renderer:
# lineheight display differs from browser canvas. This calc is just empirical values to get # lineheight display differs from browser canvas. This calc is just empirical values to get
# reportlab render similarly to browser canvas. # reportlab render similarly to browser canvas.
# for backwards compatability use „uncorrected“ lineheight of 1.0 instead of 1.15 # for backwards compatability use „uncorrected“ lineheight of 1.0 instead of 1.15
lineheight = float(o['lineheight']) * 1.15 if 'lineheight' in o else 1.0 lineheight = float(o['lineheight']) * 1.15 if not legacy_lineheight or 'lineheight' in o else 1.0
style = ParagraphStyle( style = ParagraphStyle(
name=uuid.uuid4().hex, name=uuid.uuid4().hex,
fontName=font, fontName=font,
fontSize=float(o['fontsize']), fontSize=fontsize,
leading=lineheight * float(o['fontsize']), leading=lineheight * fontsize,
# for backwards compatability use autoLeading if no lineheight is given # for backwards compatability use autoLeading if no lineheight is given
autoLeading='off' if 'lineheight' in o else 'max', autoLeading='off' if not legacy_lineheight or 'lineheight' in o else 'max',
textColor=Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255), textColor=Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255),
alignment=align_map[o['align']] alignment=align_map[o['align']],
splitLongWords=o.get('splitlongwords', True),
) )
# add an almost-invisible space &hairsp; after hyphens as word-wrap in ReportLab only works on space chars # add an almost-invisible space &hairsp; after hyphens as word-wrap in ReportLab only works on space chars
text = conditional_escape( text = conditional_escape(
@@ -1013,6 +1015,41 @@ class Renderer:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text))) logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
p = Paragraph(text, style=style) p = Paragraph(text, style=style)
return p, ad, lineheight
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
fontsize = float(o['fontsize'])
height = float(o['height']) * mm
width = float(o['width']) * mm
while True:
p, ad, lineheight = self._text_paragraph(op, order, o, override_fontsize=fontsize)
w, h = p.wrapOn(canvas, width, 1000 * mm)
widths = p.getActualLineWidths0()
if not widths:
break
actual_w = max(widths)
if not o.get('autoresize', False) or (h <= height and actual_w <= width) or fontsize <= 1.0:
break
if h > height: # we can do larger steps for height
fontsize -= max(1.0, fontsize * .1)
else:
fontsize -= max(.25, fontsize * .025)
canvas.saveState()
# The ascent/descent offsets here are not really proven to be correct, they're just empirical values to get
# reportlab render similarly to browser canvas.
canvas.translate(float(o['left']) * mm, float(o['bottom']) * mm + height)
canvas.rotate(o.get('rotation', 0) * -1)
if o.get('verticalalign', 'top') == 'top':
p.drawOn(canvas, 0, - h)
elif o.get('verticalalign', 'top') == 'middle':
p.drawOn(canvas, 0, (-height - h) / 2)
elif o.get('verticalalign', 'top') == 'bottom':
p.drawOn(canvas, 0, -height)
canvas.restoreState()
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
p, ad, lineheight = self._text_paragraph(op, order, o, legacy_lineheight=True)
w, h = p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm) w, h = p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
# p_size = p.wrap(float(o['width']) * mm, 1000 * mm) # p_size = p.wrap(float(o['width']) * mm, 1000 * mm)
canvas.saveState() canvas.saveState()
@@ -1051,6 +1088,8 @@ class Renderer:
self._draw_barcodearea(canvas, op, order, o) self._draw_barcodearea(canvas, op, order, o)
elif o['type'] == "imagearea": elif o['type'] == "imagearea":
self._draw_imagearea(canvas, op, order, o) self._draw_imagearea(canvas, op, order, o)
elif o['type'] == "textcontainer":
self._draw_textcontainer(canvas, op, order, o)
elif o['type'] == "textarea": elif o['type'] == "textarea":
self._draw_textarea(canvas, op, order, o) self._draw_textarea(canvas, op, order, o)
elif o['type'] == "poweredby": elif o['type'] == "poweredby":

View File

@@ -177,7 +177,7 @@
{% if name %} {% if name %}
<div class="row control-group pdf-info"> <div class="row control-group pdf-info">
<div class="col-sm-12"> <div class="col-sm-12">
<label>{% trans "Layout name" %}</label><br> <label for="pdf-info-name">{% trans "Layout name" %}</label><br>
<input type="text" id="pdf-info-name" class="input-block-level form-control" name="name" value="{{ name }}"> <input type="text" id="pdf-info-name" class="input-block-level form-control" name="name" value="{{ name }}">
</div> </div>
</div> </div>
@@ -185,11 +185,11 @@
<div class="row control-group pdf-info"> <div class="row control-group pdf-info">
<hr/> <hr/>
<div class="col-sm-6"> <div class="col-sm-6">
<label>{% trans "Width (mm)" %}</label><br> <label for="pdf-info-width">{% trans "Width (mm)" %}</label><br>
<input type="number" id="pdf-info-width" class="input-block-level form-control"> <input type="number" id="pdf-info-width" class="input-block-level form-control">
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<label>{% trans "Height (mm)" %}</label><br> <label for="pdf-info-height">{% trans "Height (mm)" %}</label><br>
<input type="number" id="pdf-info-height" class="input-block-level form-control"> <input type="number" id="pdf-info-height" class="input-block-level form-control">
</div> </div>
</div> </div>
@@ -227,7 +227,7 @@
<div class="row control-group pdf-info"> <div class="row control-group pdf-info">
<hr/> <hr/>
<div class="col-sm-12"> <div class="col-sm-12">
<label>{% trans "Preferred language" %}</label><br> <label for="pdf-info-locale">{% trans "Preferred language" %}</label><br>
<select class="form-control" id="pdf-info-locale"> <select class="form-control" id="pdf-info-locale">
<option value="">{% trans "Order locale" %}</option> <option value="">{% trans "Order locale" %}</option>
{% for l in locales %} {% for l in locales %}
@@ -238,7 +238,7 @@
</div> </div>
<div class="row control-group poweredby"> <div class="row control-group poweredby">
<div class="col-sm-12"> <div class="col-sm-12">
<label>{% trans "Style" %}</label><br> <label for="toolbox-poweredby-style">{% trans "Style" %}</label><br>
<select class="input-block-level form-control" id="toolbox-poweredby-style"> <select class="input-block-level form-control" id="toolbox-poweredby-style">
<option value="dark">{% trans "Dark" %}</option> <option value="dark">{% trans "Dark" %}</option>
<option value="white">{% trans "Light" %}</option> <option value="white">{% trans "Light" %}</option>
@@ -247,7 +247,7 @@
</div> </div>
<div class="row control-group imagecontent"> <div class="row control-group imagecontent">
<div class="col-sm-12"> <div class="col-sm-12">
<label>{% trans "Image content" %}</label><br> <label for="toolbox-imagecontent">{% trans "Image content" %}</label><br>
<select class="input-block-level form-control" id="toolbox-imagecontent"> <select class="input-block-level form-control" id="toolbox-imagecontent">
<option value="">{% trans "Empty" %}</option> <option value="">{% trans "Empty" %}</option>
{% for varname, var in images.items %} {% for varname, var in images.items %}
@@ -258,7 +258,7 @@
</div> </div>
<div class="row control-group text textcontent"> <div class="row control-group text textcontent">
<div class="col-sm-12"> <div class="col-sm-12">
<label>{% trans "Content" %}</label><br> <label for="toolbox-content">{% trans "Content" %}</label><br>
<select class="input-block-level form-control" id="toolbox-content"> <select class="input-block-level form-control" id="toolbox-content">
{% for varname, var in variables.items %} {% for varname, var in variables.items %}
{% if not var.hidden %} {% if not var.hidden %}
@@ -293,31 +293,31 @@
<div class="row control-group position"> <div class="row control-group position">
<hr/> <hr/>
<div class="col-sm-6"> <div class="col-sm-6">
<label>{% trans "x (mm)" %}</label><br> <label for="toolbox-position-x">{% trans "x (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01" <input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-position-x"> id="toolbox-position-x">
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<label>{% trans "y (mm)" %}</label><br> <label for="toolbox-position-y">{% trans "y (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01" <input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-position-y"> id="toolbox-position-y">
</div> </div>
</div> </div>
<div class="row control-group rectsize"> <div class="row control-group rectsize">
<div class="col-sm-6"> <div class="col-sm-6">
<label>{% trans "Width (mm)" %}</label><br> <label for="toolbox-width">{% trans "Width (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01" <input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-width"> id="toolbox-width">
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<label>{% trans "Height (mm)" %}</label><br> <label for="toolbox-height">{% trans "Height (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01" <input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-height"> id="toolbox-height">
</div> </div>
</div> </div>
<div class="row control-group squaresize poweredby"> <div class="row control-group squaresize poweredby">
<div class="col-sm-12"> <div class="col-sm-12">
<label>{% trans "Size (mm)" %}</label><br> <label for="toolbox-squaresize">{% trans "Size (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01" <input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-squaresize"> id="toolbox-squaresize">
</div> </div>
@@ -335,13 +335,13 @@
</div> </div>
</div> </div>
<div class="row control-group text"> <div class="row control-group text">
<div class="col-sm-6"> <div class="col-sm-6 textarea">
<label>{% trans "Width (mm)" %}</label><br> <label for="toolbox-textwidth">{% trans "Width (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01" <input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-textwidth"> id="toolbox-textwidth">
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<label>{% trans "Rotation (°)" %}</label><br> <label for="toolbox-textrotation">{% trans "Rotation (°)" %}</label><br>
<input type="number" value="0" class="input-block-level form-control" step="0.1" <input type="number" value="0" class="input-block-level form-control" step="0.1"
id="toolbox-textrotation"> id="toolbox-textrotation">
</div> </div>
@@ -349,7 +349,7 @@
<div class="row control-group text"> <div class="row control-group text">
<hr/> <hr/>
<div class="col-sm-12"> <div class="col-sm-12">
<label>{% trans "Font" %}</label><br> <label for="toolbox-fontfamily">{% trans "Font" %}</label><br>
<select class="input-block-level form-control" id="toolbox-fontfamily"> <select class="input-block-level form-control" id="toolbox-fontfamily">
<option>Open Sans</option> <option>Open Sans</option>
{% for family in fonts.keys %} {% for family in fonts.keys %}
@@ -360,43 +360,50 @@
</div> </div>
<div class="row control-group text"> <div class="row control-group text">
<div class="col-sm-6"> <div class="col-sm-6">
<label>{% trans "Font size (pt)" %}</label><br> <label for="toolbox-fontsize">{% trans "Font size (pt)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.1" <input type="number" value="13" class="input-block-level form-control" step="0.1"
id="toolbox-fontsize"> id="toolbox-fontsize">
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<label>{% trans "Line height" %}</label><br> <label for="toolbox-lineheight">{% trans "Line height" %}</label><br>
<input type="number" value="1" class="input-block-level form-control" step="0.1" <input type="number" value="1" class="input-block-level form-control" step="0.1"
id="toolbox-lineheight"> id="toolbox-lineheight">
</div> </div>
</div> </div>
<div class="row control-group text"> <div class="row control-group text">
<div class="col-sm-6"> <div class="col-sm-6">
<label>{% trans "Text color" %}</label><br> <label for="toolbox-col">{% trans "Text color" %}</label><br>
<input type="text" value="#000000" class="input-block-level form-control colorpickerfield" <div class="input-group">
id="toolbox-col"> <input type="text" value="#000000" class="input-block-level form-control colorpickerfield"
id="toolbox-col">
<span class="input-group-addon contrast-icon">
</span>
</div>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<label>&nbsp;</label><br> <label>&nbsp;</label><br>
<div class="btn-group btn-group-justified" role="group"> <div class="btn-group btn-group-justified" role="group">
<div class="btn-group" role="group"> <div class="btn-group text" role="group">
<button type="button" class="btn btn-default toggling" data-action="bold"> <button type="button" class="btn btn-default toggling" data-action="bold">
<span class="fa fa-bold"></span> <span class="fa fa-bold"></span>
</button> </button>
</div> </div>
<div class="btn-group" role="group"> <div class="btn-group text" role="group">
<button type="button" class="btn btn-default toggling" data-action="italic"> <button type="button" class="btn btn-default toggling" data-action="italic">
<span class="fa fa-italic"></span> <span class="fa fa-italic"></span>
</button> </button>
</div> </div>
<div class="btn-group" role="group"> <div class="btn-group textarea" role="group">
<button type="button" class="btn btn-default toggling" data-action="downward" <button type="button" class="btn btn-default toggling" data-action="downward"
data-toggle="tooltip" title="{% trans "Flow multiple lines downward from specified position" %}"> data-toggle="tooltip" title="{% trans "Flow multiple lines downward from specified position" %}">
<span class="fa fa-caret-square-o-down"></span> <span class="fa fa-caret-square-o-down"></span>
</button> </button>
</div> </div>
</div> </div>
<label>&nbsp;</label><br> </div>
</div>
<div class="row control-group text">
<div class="col-sm-6">
<div class="btn-group btn-group-justified" id="toolbox-align"> <div class="btn-group btn-group-justified" id="toolbox-align">
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button type="button" class="btn btn-default option toggling" data-action="left"> <button type="button" class="btn btn-default option toggling" data-action="left">
@@ -415,6 +422,45 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-sm-6">
<div class="textcontainer">
<div class="btn-group btn-group-justified" id="toolbox-verticalalign">
<div class="btn-group" role="group">
<button type="button" class="btn btn-default option toggling" data-action="top">
<span class="fa fa-toggle-down"></span>
</button>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default option toggling" data-action="middle">
<span class="fa fa-sort"></span>
</button>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default option toggling" data-action="bottom">
<span class="fa fa-toggle-up"></span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="row control-group textcontainer">
<div class="col-sm-6">
<div class="btn-group btn-group-justified">
<div class="btn-group textcontainer" role="group">
<button type="button" class="btn btn-default toggling" data-action="autoresize"
data-toggle="tooltip" title="{% trans "Automatically reduce font size to fit content" %}">
<span class="fa fa-text-height"></span>
</button>
</div>
<div class="btn-group textcontainer" role="group">
<button type="button" class="btn btn-default toggling" data-action="splitlongwords"
data-toggle="tooltip" title="{% trans "Allow long words to be split (preview is not accurate)" %}">
<span class="fa fa-scissors fa-rotate-90"></span>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -423,9 +469,13 @@
{% trans "Add a new object" %} {% trans "Add a new object" %}
</div> </div>
<div class="panel-body add-buttons"> <div class="panel-body add-buttons">
<button class="btn btn-default btn-block" id="editor-add-textcontainer" disabled>
<span class="fa fa-font"></span>
{% trans "Text box" %}
</button>
<button class="btn btn-default btn-block" id="editor-add-text" disabled> <button class="btn btn-default btn-block" id="editor-add-text" disabled>
<span class="fa fa-font"></span> <span class="fa fa-font"></span>
{% trans "Text" %} {% trans "Text (deprecated)" %}
</button> </button>
<button class="btn btn-default btn-block" id="editor-add-qrcode" data-content="secret" disabled> <button class="btn btn-default btn-block" id="editor-add-qrcode" data-content="secret" disabled>
<span class="fa fa-qrcode"></span> <span class="fa fa-qrcode"></span>
@@ -456,7 +506,6 @@
</button> </button>
</div> </div>
</div> </div>
<form method="post" action="" id="preview-form" target="_blank"> <form method="post" action="" id="preview-form" target="_blank">
<div class="form-group submit-group"> <div class="form-group submit-group">
{% csrf_token %} {% csrf_token %}
@@ -472,6 +521,13 @@
</button> </button>
</div> </div>
</form> </form>
<p>&nbsp;</p>
<div class="alert alert-info" id="version-notice">
{% blocktrans trimmed with print_version="2.18" scan_version="1.22" %}
This layout uses new features. If you print from your device, make sure you use pretixPRINT version
{{ print_version }} (or newer) or pretixSCAN Desktop version {{ scan_version }} (or newer).
{% endblocktrans %}
</div>
</div> </div>
</div> </div>
<script type="text/plain" id="schema-url">{% static "schema/pdf-layout.schema.json" %}</script> <script type="text/plain" id="schema-url">{% static "schema/pdf-layout.schema.json" %}</script>

View File

@@ -30,11 +30,11 @@ def _simple_template(w, h):
company_size = name_size - 2 company_size = name_size - 2
return [ return [
{ {
"type": "textarea", "type": "textcontainer",
"page": 1, "page": 1,
"locale": "", "locale": "",
"left": "5.00", "left": "5.00",
"bottom": "%.2f" % (((h - company_size * 1.5 - name_size) / 2 + company_size * 1.5) / mm), "bottom": "%.2f" % (h / mm / 2 + 2),
"fontsize": name_size, "fontsize": name_size,
"lineheight": "1", "lineheight": "1",
"color": [0, 0, 0, 1], "color": [0, 0, 0, 1],
@@ -42,19 +42,22 @@ def _simple_template(w, h):
"bold": True, "bold": True,
"italic": False, "italic": False,
"width": "%.2f" % (w / mm - 10), "width": "%.2f" % (w / mm - 10),
"downward": False, "height": "%.2f" % (h / mm / 2 - 7),
"content": "attendee_name", "content": "attendee_name",
"text": "John Doe", "text": "Dr John Doe",
"text_i18n": {}, "text_i18n": {},
"rotation": 0, "rotation": 0,
"align": "center", "align": "center",
"verticalalign": "bottom",
"autoresize": True,
"splitlongwords": False,
}, },
{ {
"type": "textarea", "type": "textcontainer",
"page": 1, "page": 1,
"locale": "", "locale": "",
"left": "5.00", "left": "5.00",
"bottom": "%.2f" % ((((h - company_size * 1.5 - name_size) / 2) + company_size) / mm), "bottom": "5.00",
"fontsize": company_size, "fontsize": company_size,
"lineheight": "1", "lineheight": "1",
"color": [0, 0, 0, 1], "color": [0, 0, 0, 1],
@@ -62,12 +65,15 @@ def _simple_template(w, h):
"bold": False, "bold": False,
"italic": False, "italic": False,
"width": "%.2f" % (w / mm - 10), "width": "%.2f" % (w / mm - 10),
"downward": True, "height": "%.2f" % (h / mm / 2 - 7),
"content": "attendee_company", "content": "attendee_company",
"text": "Sample company", "text": "Sample company",
"text_i18n": {}, "text_i18n": {},
"rotation": 0, "rotation": 0,
"align": "center", "align": "center",
"verticalalign": "top",
"autoresize": True,
"splitlongwords": False,
}, },
] ]
@@ -94,15 +100,17 @@ TEMPLATES = {
"layout": _simple_template(*pagesizes.portrait(pagesizes.A7)), "layout": _simple_template(*pagesizes.portrait(pagesizes.A7)),
}, },
"82x203butterfly": { "82x203butterfly": {
"label": format_lazy(_("{width} x {height} mm butterfly badge"), width=82, height=203), "label": format_lazy(
_("{width} x {height} mm butterfly badge"), width=82, height=203
),
"pagesize": (82 * mm, 203 * mm), "pagesize": (82 * mm, 203 * mm),
"layout": [ "layout": [
{ {
"type": "textarea", "type": "textcontainer",
"page": 1, "page": 1,
"locale": "", "locale": "",
"left": "5.00", "left": "5.00",
"bottom": "152.55", "bottom": "153.00",
"fontsize": "20.0", "fontsize": "20.0",
"lineheight": "1", "lineheight": "1",
"color": [0, 0, 0, 1], "color": [0, 0, 0, 1],
@@ -110,19 +118,22 @@ TEMPLATES = {
"bold": True, "bold": True,
"italic": False, "italic": False,
"width": "72.00", "width": "72.00",
"downward": False, "height": "20.00",
"content": "attendee_name", "content": "attendee_name",
"text": "John Doe", "text": "Dr John Doe",
"text_i18n": {}, "text_i18n": {},
"rotation": 0, "rotation": 0,
"align": "center", "align": "center",
"verticalalign": "bottom",
"autoresize": True,
"splitlongwords": False,
}, },
{ {
"type": "textarea", "type": "textcontainer",
"page": 1, "page": 1,
"locale": "", "locale": "",
"left": "5.00", "left": "5.00",
"bottom": "144.55", "bottom": "132.10",
"fontsize": "18.0", "fontsize": "18.0",
"lineheight": "1", "lineheight": "1",
"color": [0, 0, 0, 1], "color": [0, 0, 0, 1],
@@ -130,19 +141,22 @@ TEMPLATES = {
"bold": False, "bold": False,
"italic": False, "italic": False,
"width": "72.00", "width": "72.00",
"downward": False, "height": "20.00",
"content": "attendee_company", "content": "attendee_company",
"text": "Sample company", "text": "Sample company",
"text_i18n": {}, "text_i18n": {},
"rotation": 0, "rotation": 0,
"align": "center", "align": "center",
"verticalalign": "top",
"autoresize": True,
"splitlongwords": False,
}, },
{ {
"type": "textarea", "type": "textcontainer",
"page": 1, "page": 1,
"locale": "", "locale": "",
"left": "77.10", "left": "76.97",
"bottom": "34.68", "bottom": "10.86",
"fontsize": "20.0", "fontsize": "20.0",
"lineheight": "1", "lineheight": "1",
"color": [0, 0, 0, 1], "color": [0, 0, 0, 1],
@@ -150,19 +164,22 @@ TEMPLATES = {
"bold": True, "bold": True,
"italic": False, "italic": False,
"width": "72.00", "width": "72.00",
"downward": False, "height": "20.00",
"content": "attendee_name", "content": "attendee_name",
"text": "John Doe", "text": "Dr John Doe",
"text_i18n": {}, "text_i18n": {},
"rotation": 180, "rotation": -180,
"align": "center", "align": "center",
"verticalalign": "bottom",
"autoresize": True,
"splitlongwords": False,
}, },
{ {
"type": "textarea", "type": "textcontainer",
"page": 1, "page": 1,
"locale": "", "locale": "",
"left": "77.06", "left": "77.07",
"bottom": "44.28", "bottom": "31.76",
"fontsize": "18.0", "fontsize": "18.0",
"lineheight": "1", "lineheight": "1",
"color": [0, 0, 0, 1], "color": [0, 0, 0, 1],
@@ -170,12 +187,15 @@ TEMPLATES = {
"bold": False, "bold": False,
"italic": False, "italic": False,
"width": "72.00", "width": "72.00",
"downward": False, "height": "20.00",
"content": "attendee_company", "content": "attendee_company",
"text": "Sample company", "text": "Sample company",
"text_i18n": {}, "text_i18n": {},
"rotation": 180, "rotation": -180,
"align": "center", "align": "center",
"verticalalign": "top",
"autoresize": True,
"splitlongwords": False,
}, },
], ],
}, },
@@ -235,7 +255,9 @@ TEMPLATES = {
"layout": _simple_template(40 * mm, 40 * mm), "layout": _simple_template(40 * mm, 40 * mm),
}, },
"88.9x33.87": { "88.9x33.87": {
"label": format_lazy(_("{width} x {height} mm label"), width=88.9, height=33.87), "label": format_lazy(
_("{width} x {height} mm label"), width=88.9, height=33.87
),
"pagesize": (88.9 * mm, 33.87 * mm), "pagesize": (88.9 * mm, 33.87 * mm),
"layout": _simple_template(88.9 * mm, 33.87 * mm), "layout": _simple_template(88.9 * mm, 33.87 * mm),
}, },

View File

@@ -27,191 +27,279 @@ from django.utils.translation import gettext_lazy as _
from pretix.base.models import LoggedModel from pretix.base.models import LoggedModel
DEFAULT_TICKET_LAYOUT = '''[{ DEFAULT_TICKET_LAYOUT = '''[
"type":"textarea", {
"left":"17.50", "type": "barcodearea",
"bottom":"274.60", "page": 1,
"fontsize":"16.0", "left": "130.40",
"color":[ "bottom": "204.50",
0, "size": "64.00",
0, "content": "secret",
0, "text": "",
1 "text_i18n": {},
], "nowhitespace": false
"fontfamily":"Open Sans", },
"bold":false, {
"italic":false, "type": "poweredby",
"width":"175.00", "page": 1,
"content":"event_name", "left": "88.72",
"text":"Sample event name", "bottom": "10.00",
"align":"left" "size": "20.00",
}, "content": "dark"
{ },
"type":"textarea", {
"left":"17.50", "type": "textcontainer",
"bottom":"262.90", "page": 1,
"fontsize":"13.0", "locale": "",
"color":[ "left": "16.35",
0, "bottom": "272.09",
0, "fontsize": "14.0",
0, "lineheight": "1",
1 "color": [
], 0,
"fontfamily":"Open Sans", 0,
"bold":false, 0,
"italic":false, 1
"width":"110.00", ],
"content":"itemvar", "fontfamily": "Open Sans",
"text":"Sample product sample variation", "bold": false,
"align":"left" "italic": false,
}, "width": "177.07",
{ "height": "11.80",
"type":"textarea", "content": "event_name",
"left":"17.50", "text": "Sample event name",
"bottom":"252.50", "text_i18n": {},
"fontsize":"13.0", "rotation": 0,
"color":[ "align": "left",
0, "verticalalign": "middle",
0, "autoresize": true,
0, "splitlongwords": true
1 },
], {
"fontfamily":"Open Sans", "type": "textcontainer",
"bold":false, "page": 1,
"italic":false, "locale": "",
"width":"110.00", "left": "16.35",
"content":"attendee_name", "bottom": "261.77",
"text":"John Doe", "fontsize": "13.0",
"align":"left" "lineheight": "1",
}, "color": [
{ 0,
"type":"textarea", 0,
"left":"17.50", 0,
"bottom":"242.10", 1
"fontsize":"13.0", ],
"color":[ "fontfamily": "Open Sans",
0, "bold": false,
0, "italic": false,
0, "width": "113.03",
1 "height": "7.83",
], "content": "itemvar",
"fontfamily":"Open Sans", "text": "Sample product sample variation",
"bold":false, "text_i18n": {},
"italic":false, "rotation": 0,
"width":"110.00", "align": "left",
"content":"event_begin", "verticalalign": "middle",
"text":"2016-05-31 20:00", "autoresize": true,
"align":"left" "splitlongwords": true
}, },
{ {
"type":"textarea", "type": "textcontainer",
"left":"17.50", "page": 1,
"bottom":"231.70", "locale": "",
"fontsize":"13.0", "left": "16.35",
"color":[ "bottom": "251.30",
0, "fontsize": "13.0",
0, "lineheight": "1",
0, "color": [
1 0,
], 0,
"fontfamily":"Open Sans", 0,
"bold":false, 1
"italic":false, ],
"width":"110.00", "fontfamily": "Open Sans",
"content":"seat", "bold": false,
"text":"Ground floor, Row 3, Seat 4", "italic": false,
"align":"left" "width": "113.03",
}, "height": "7.83",
{ "content": "attendee_name",
"type":"textarea", "text": "Dr John Doe",
"left":"17.50", "text_i18n": {},
"bottom":"204.80", "rotation": 0,
"fontsize":"13.0", "align": "left",
"color":[ "verticalalign": "middle",
0, "autoresize": true,
0, "splitlongwords": true
0, },
1 {
], "type": "textcontainer",
"fontfamily":"Open Sans", "page": 1,
"bold":false, "locale": "",
"italic":false, "left": "16.35",
"width":"110.00", "bottom": "240.30",
"content":"event_location", "fontsize": "13.0",
"text":"Random City", "lineheight": "1",
"align":"left" "color": [
}, 0,
{ 0,
"type":"textarea", 0,
"left":"17.50", 1
"bottom":"194.50", ],
"fontsize":"13.0", "fontfamily": "Open Sans",
"color":[ "bold": false,
0, "italic": false,
0, "width": "113.03",
0, "height": "7.83",
1 "content": "event_begin",
], "text": "2017-05-31 20:00",
"fontfamily":"Open Sans", "text_i18n": {},
"bold":false, "rotation": 0,
"italic":false, "align": "left",
"width":"30.00", "verticalalign": "middle",
"content":"order", "autoresize": true,
"text":"A1B2C", "splitlongwords": true
"align":"left" },
}, {
{ "type": "textcontainer",
"type":"textarea", "page": 1,
"left":"52.50", "locale": "",
"bottom":"194.50", "left": "16.35",
"fontsize":"13.0", "bottom": "231.30",
"color":[ "fontsize": "13.0",
0, "lineheight": "1",
0, "color": [
0, 0,
1 0,
], 0,
"fontfamily":"Open Sans", 1
"bold":false, ],
"italic":false, "fontfamily": "Open Sans",
"width":"45.00", "bold": false,
"content":"price", "italic": false,
"text":"123.45 EUR", "width": "113.03",
"align":"right" "height": "7.83",
}, "content": "seat",
{ "text": "Ground floor, Row 3, Seat 4",
"type":"textarea", "text_i18n": {},
"left":"102.50", "rotation": 0,
"bottom":"194.50", "align": "left",
"fontsize":"13.0", "verticalalign": "middle",
"color":[ "autoresize": true,
0, "splitlongwords": true
0, },
0, {
1 "type": "textcontainer",
], "page": 1,
"fontfamily":"Open Sans", "locale": "",
"bold":false, "left": "16.35",
"italic":false, "bottom": "203.43",
"width":"90.00", "fontsize": "13.0",
"content":"secret", "lineheight": "1",
"text":"tdmruoekvkpbv1o2mv8xccvqcikvr58u", "color": [
"align":"left" 0,
}, 0,
{ 0,
"type":"barcodearea", 1
"left":"130.40", ],
"bottom":"204.50", "fontfamily": "Open Sans",
"size":"64.00", "bold": false,
"content":"secret" "italic": false,
}, "width": "113.03",
{ "height": "25.70",
"type":"poweredby", "content": "event_location",
"left":"88.72", "text": "Random City",
"bottom":"10.00", "text_i18n": {},
"size":"20.00", "rotation": 0,
"content":"dark" "align": "left",
}]''' "verticalalign": "bottom",
"autoresize": true,
"splitlongwords": true
},
{
"type": "textcontainer",
"page": 1,
"locale": "",
"left": "101.50",
"bottom": "193.33",
"fontsize": "13.0",
"lineheight": "1",
"color": [
0,
0,
0,
1
],
"fontfamily": "Open Sans",
"bold": false,
"italic": false,
"width": "91.93",
"height": "7.83",
"content": "secret",
"text": "tdmruoekvkpbv1o2mv8xccvqcikvr58u",
"text_i18n": {},
"rotation": 0,
"align": "left",
"verticalalign": "middle",
"autoresize": true,
"splitlongwords": true
},
{
"type": "textcontainer",
"page": 1,
"locale": "",
"left": "51.50",
"bottom": "193.33",
"fontsize": "13.0",
"lineheight": "1",
"color": [
0,
0,
0,
1
],
"fontfamily": "Open Sans",
"bold": false,
"italic": false,
"width": "47.38",
"height": "7.83",
"content": "price",
"text": "123.45 EUR",
"text_i18n": {},
"rotation": 0,
"align": "right",
"verticalalign": "middle",
"autoresize": true,
"splitlongwords": true
},
{
"type": "textcontainer",
"page": 1,
"locale": "",
"left": "16.50",
"bottom": "193.33",
"fontsize": "13.0",
"lineheight": "1",
"color": [
0,
0,
0,
1
],
"fontfamily": "Open Sans",
"bold": false,
"italic": false,
"width": "32.32",
"height": "7.83",
"content": "order",
"text": "A1B2C",
"text_i18n": {},
"rotation": 0,
"align": "left",
"verticalalign": "middle",
"autoresize": true,
"splitlongwords": true
}
]'''
def bg_name(instance, filename: str) -> str: def bg_name(instance, filename: str) -> str:

View File

@@ -47,6 +47,105 @@ fabric.Imagearea = fabric.util.createClass(fabric.Rect, {
fabric.Imagearea.fromObject = function (object, callback, forceAsync) { fabric.Imagearea.fromObject = function (object, callback, forceAsync) {
return fabric.Object._fromObject('Imagearea', object, callback, forceAsync); return fabric.Object._fromObject('Imagearea', object, callback, forceAsync);
}; };
fabric.Textcontainer = fabric.util.createClass(fabric.Rect, {
type: 'textcontainer',
initialize: function (text, options) {
options || (options = {});
this.callSuper('initialize', options);
//this.textbox = new fabric.Textbox(text, JSON.parse(JSON.stringify(options)));
this.set('content', options.content || '');
this.cacheProperties.push(
"width", "height", "fontSize", "lineHeight", "fill", "fontFamily", "fontWeight",
"fontStyle", "text", "textAlign", "splitLongWords", "splitByGrapheme", "verticalAlign",
"autoResize",
)
},
toObject: function (propertiesToInclude) {
return this.callSuper('toObject', ['content'].concat(propertiesToInclude));
},
_internalTextbox: function () {
var fontSize = parseFloat(this.fontSize);
var text = (this.text || "").replace("-", "-\u200B");
while (true) {
var tmptext = new fabric.Textbox(text, {
_wordJoiners: /[ \t\r\u200B]/u,
left: 0,
top: 0,
originY: 'top',
originX: 'left',
width: this.width,
height: this.height,
fontSize: fontSize,
lineHeight: this.lineHeight,
fill: this.fill,
fontFamily: this.fontFamily,
fontWeight: this.fontWeight,
fontStyle: this.fontStyle,
text: text,
textAlign: this.textAlign,
splitByGrapheme: this.splitLongWords
})
tmptext.setCoords();
var lineHeights = 0;
for (var i = 0, len = tmptext._textLines.length; i < len; i++) {
var heightOfLine = tmptext.getHeightOfLine(i);
lineHeights += heightOfLine;
}
if (!this.autoResize || (lineHeights <= this.height && tmptext.width <= this.width) || fontSize <= 1.0) {
return {textbox: tmptext, height: lineHeights, width: tmptext.width}
}
if (lineHeights > this.height) { // we can do larger steps for height
fontSize = fontSize - Math.max(1.0, fontSize * .1)
} else {
fontSize = fontSize - Math.max(.25, fontSize * .025)
}
}
},
_render: function (ctx) {
var h = this.height, w = this.width;
/*
var x = -this.width / 2,
y = -this.height / 2;
ctx.fillStyle = '#ccc';
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + w, y);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x, y + h);
ctx.lineTo(x, y);
ctx.closePath();
ctx.fill();
*/
var { textbox, height, width } = this._internalTextbox();
ctx.save();
if (this.verticalAlign === "top") {
ctx.translate(0, - (h - height) / 2);
} else if (this.verticalAlign === "bottom") {
ctx.translate(0, (h - height) / 2);
}
// it is entirely unclear to me why overflow is always rendered centered, so we manually readjust
if (this.textAlign === "left" && width > w) {
ctx.translate(-(w - width) / 2, 0);
} else if (this.verticalAlign === "right" && width > w) {
ctx.translate((w - width) / 2, 0);
}
textbox._render(ctx);
ctx.restore();
},
});
fabric.Textcontainer.fromObject = function (object, callback, forceAsync) {
return fabric.Object._fromObject('Textcontainer', object, callback, forceAsync);
};
fabric.Barcodearea = fabric.util.createClass(fabric.Rect, { fabric.Barcodearea = fabric.util.createClass(fabric.Rect, {
type: 'barcodearea', type: 'barcodearea',
@@ -150,9 +249,10 @@ var editor = {
top += o.group.top + o.group.height / 2; top += o.group.top + o.group.height / 2;
left += o.group.left + o.group.width / 2; left += o.group.left + o.group.width / 2;
} }
var col, bottom;
if (o.type === "textarea") { if (o.type === "textarea") {
var col = (new fabric.Color(o.fill))._source; col = (new fabric.Color(o.fill))._source;
var bottom = editor.pdf_viewport.height - o.height - top; bottom = editor.pdf_viewport.height - o.height - top;
if (o.downward) { if (o.downward) {
bottom = editor.pdf_viewport.height - top; bottom = editor.pdf_viewport.height - top;
} }
@@ -176,6 +276,32 @@ var editor = {
rotation: o.angle, rotation: o.angle,
align: o.textAlign, align: o.textAlign,
}); });
} else if (o.type === "textcontainer") {
col = (new fabric.Color(o.fill))._source;
bottom = editor.pdf_viewport.height - o.height - top;
d.push({
type: "textcontainer",
page: editor.pdf_page_number,
locale: $("#pdf-info-locale").val(),
left: editor._px2mm(left).toFixed(2),
bottom: editor._px2mm(bottom).toFixed(2),
fontsize: editor._px2pt(o.fontSize).toFixed(1),
lineheight: o.lineHeight,
color: col,
fontfamily: o.fontFamily,
bold: o.fontWeight === 'bold',
italic: o.fontStyle === 'italic',
width: editor._px2mm(o.width).toFixed(2),
height: editor._px2mm(o.height).toFixed(2),
content: o.content,
text: o.text,
text_i18n: o.text_i18n || {},
rotation: o.angle,
align: o.textAlign || 'left',
verticalalign: o.verticalAlign || 'middle',
autoresize: o.autoResize || false,
splitlongwords: o.splitLongWords || false,
});
} else if (o.type === "imagearea") { } else if (o.type === "imagearea") {
d.push({ d.push({
type: "imagearea", type: "imagearea",
@@ -239,6 +365,36 @@ var editor = {
o = editor._add_poweredby(d.content); o = editor._add_poweredby(d.content);
o.content = d.content; o.content = d.content;
o.scaleToHeight(editor._mm2px(d.size)); o.scaleToHeight(editor._mm2px(d.size));
} else if (d.type === "textcontainer") {
o = editor._add_textcontainer();
o.set('fill', 'rgb(' + d.color[0] + ',' + d.color[1] + ',' + d.color[2] + ')');
o.set('fontSize', editor._pt2px(d.fontsize));
o.set('lineHeight', d.lineheight || 1);
o.set('fontFamily', d.fontfamily);
o.set('fontWeight', d.bold ? 'bold' : 'normal');
o.set('fontStyle', d.italic ? 'italic' : 'normal');
o.content = d.content;
o.set('textAlign', d.align);
o.set('verticalAlign', d.verticalalign);
o.set('autoResize', d.autoresize);
o.set('splitLongWords', d.splitlongwords);
if (d.rotation) {
o.rotate(d.rotation);
}
if (d.content === "other") {
o.set('text', d.text);
} else if (d.content === "other_i18n") {
o.text_i18n = d.text_i18n
o.set('text', d.text_i18n[Object.keys(d.text_i18n)[0]]);
} else if (d.content) {
o.set('text', editor._get_text_sample(d.content));
}
o.set('width', editor._mm2px(d.width)); // needs to be after setText
o.set('height', editor._mm2px(d.height)); // needs to be after setText
if (d.locale) {
// The data format allows to set the locale per text field but we currently only expose a global field
$("#pdf-info-locale").val(d.locale);
}
} else if (d.type === "textarea" || o.type === "text") { } else if (d.type === "textarea" || o.type === "text") {
o = editor._add_text(); o = editor._add_text();
o.set('fill', 'rgb(' + d.color[0] + ',' + d.color[1] + ',' + d.color[2] + ')'); o.set('fill', 'rgb(' + d.color[0] + ',' + d.color[1] + ',' + d.color[2] + ')');
@@ -455,6 +611,11 @@ var editor = {
}, },
_update_toolbox_values: function () { _update_toolbox_values: function () {
$("#version-notice").toggle(
editor._other_page_objects.some((o) => o.type === "textcontainer") ||
editor.fabric.getObjects().some((o) => o.type === "textcontainer")
);
var o = editor.fabric.getActiveObject(); var o = editor.fabric.getActiveObject();
if (!o) { if (!o) {
return; return;
@@ -484,6 +645,35 @@ var editor = {
} else if (o.type === "poweredby") { } else if (o.type === "poweredby") {
$("#toolbox-squaresize").val(editor._px2mm(o.height * o.scaleY).toFixed(2)); $("#toolbox-squaresize").val(editor._px2mm(o.height * o.scaleY).toFixed(2));
$("#toolbox-poweredby-style").val(o.content); $("#toolbox-poweredby-style").val(o.content);
} else if (o.type === "textcontainer") {
var col = (new fabric.Color(o.fill))._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 || 1);
$("#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=autoresize]").toggleClass('active', o.autoResize || false)
$("#toolbox").find("button[data-action=splitlongwords]").toggleClass('active', o.splitLongWords || false)
$("#toolbox").find("button[data-action=left]").toggleClass('active', o.textAlign === 'left' || !o.textAlign);
$("#toolbox").find("button[data-action=center]").toggleClass('active', o.textAlign === 'center');
$("#toolbox").find("button[data-action=right]").toggleClass('active', o.textAlign === 'right');
$("#toolbox").find("button[data-action=top]").toggleClass('active', o.verticalAlign === 'top' || !o.verticalAlign);
$("#toolbox").find("button[data-action=middle]").toggleClass('active', o.verticalAlign === 'middle');
$("#toolbox").find("button[data-action=bottom]").toggleClass('active', o.verticalAlign === 'bottom');
if (o.scaleY !== 1 || o.scaleX !== 1) {
o.set({
height: o.height * o.scaleY,
width: o.width * o.scaleX,
scaleX: 1,
scaleY: 1
});
}
$("#toolbox-height").val(editor._px2mm(o.height).toFixed(2));
$("#toolbox-width").val(editor._px2mm(o.width).toFixed(2));
$("#toolbox-textrotation").val((o.angle || 0.0).toFixed(1));
} else if (o.type === "text" || o.type === "textarea") { } else if (o.type === "text" || o.type === "textarea") {
var col = (new fabric.Color(o.fill))._source; var col = (new fabric.Color(o.fill))._source;
$("#toolbox-col").val("#" + ((1 << 24) + (col[0] << 16) + (col[1] << 8) + col[2]).toString(16).slice(1)); $("#toolbox-col").val("#" + ((1 << 24) + (col[0] << 16) + (col[1] << 8) + col[2]).toString(16).slice(1));
@@ -500,7 +690,7 @@ var editor = {
$("#toolbox-textrotation").val((o.angle || 0.0).toFixed(1)); $("#toolbox-textrotation").val((o.angle || 0.0).toFixed(1));
} }
if (o.type === "textarea" || o.type === "barcodearea") { if (o.type === "textarea" || o.type === "barcodearea" || o.type === "textcontainer") {
if (!o.content && o.type == "barcodearea") { if (!o.content && o.type == "barcodearea") {
o.content = "secret"; o.content = "secret";
} }
@@ -598,6 +788,50 @@ var editor = {
editor.fabric.discardActiveObject(); editor.fabric.discardActiveObject();
editor.fabric.setActiveObject(newo); editor.fabric.setActiveObject(newo);
} }
} else if (o.type === "textcontainer") {
o.set('fill', $("#toolbox-col").val());
o.set('fontSize', editor._pt2px($("#toolbox-fontsize").val()));
o.set('lineHeight', $("#toolbox-lineheight").val() || 1);
o.set('fontFamily', $("#toolbox-fontfamily").val());
o.set('fontWeight', $("#toolbox").find("button[data-action=bold]").is('.active') ? 'bold' : 'normal');
o.set('fontStyle', $("#toolbox").find("button[data-action=italic]").is('.active') ? 'italic' : 'normal');
var align = $("#toolbox-align").find(".active").attr("data-action");
if (align) {
o.set('textAlign', align);
}
var verticalAlign = $("#toolbox-verticalalign").find(".active").attr("data-action");
if (verticalAlign) {
o.set('verticalAlign', verticalAlign);
}
o.set('autoResize', $("#toolbox").find("button[data-action=autoresize]").is('.active'));
o.set('splitLongWords', $("#toolbox").find("button[data-action=splitlongwords]").is('.active'));
// todo: verticalalign
o.rotate(parseFloat($("#toolbox-textrotation").val()));
$("#toolbox-content-other").toggle($("#toolbox-content").val() === "other");
$("#toolbox-content-other-i18n").toggle($("#toolbox-content").val() === "other_i18n");
$("#toolbox-content-other-help").toggle($("#toolbox-content").val() === "other" || $("#toolbox-content").val() === "other_i18n");
o.content = $("#toolbox-content").val();
if ($("#toolbox-content").val() === "other") {
if (e.target.id === "toolbox-content") {
// user used dropdown to switch content-type, update value with value from i18n textarea
$("#toolbox-content-other").val($("#toolbox-content-other-i18n textarea").val());
}
o.set('text', $("#toolbox-content-other").val());
} else if ($("#toolbox-content").val() === "other_i18n") {
if (e.target.id === "toolbox-content") {
// user used dropdown to switch content-type, update value with value from "other" textarea
$("#toolbox-content-other-i18n textarea").val($("#toolbox-content-other").val());
}
o.text_i18n = {}
$("#toolbox-content-other-i18n textarea").each(function () {
o.text_i18n[$(this).attr("lang")] = $(this).val();
});
o.set('text', $("#toolbox-content-other-i18n textarea").first().val());
} else {
o.set('text', editor._get_text_sample($("#toolbox-content").val()));
}
o.set('width', editor._mm2px($("#toolbox-width").val()));
o.set('height', editor._mm2px($("#toolbox-height").val()));
} else if (o.type === "textarea" || o.type === "text") { } else if (o.type === "textarea" || o.type === "text") {
o.set('fill', $("#toolbox-col").val()); o.set('fill', $("#toolbox-col").val());
o.set('fontSize', editor._pt2px($("#toolbox-fontsize").val())); o.set('fontSize', editor._pt2px($("#toolbox-fontsize").val()));
@@ -658,7 +892,9 @@ var editor = {
var o = selected[0]; var o = selected[0];
$("#toolbox").attr("data-type", o.type); $("#toolbox").attr("data-type", o.type);
if (o.type === "textarea" || o.type === "text") { if (o.type === "textarea" || o.type === "text") {
$("#toolbox-heading").text(gettext("Text object")); $("#toolbox-heading").text(gettext("Text object (deprecated)"));
} else if (o.type === "textarea" || o.type === "text" || o.type === "textcontainer") {
$("#toolbox-heading").text(gettext("Text box"));
} else if (o.type === "barcodearea") { } else if (o.type === "barcodearea") {
$("#toolbox-heading").text(gettext("Barcode area")); $("#toolbox-heading").text(gettext("Barcode area"));
} else if (o.type === "imagearea") { } else if (o.type === "imagearea") {
@@ -727,6 +963,44 @@ var editor = {
return rect; return rect;
}, },
_add_textcontainer: function () {
var rect = new fabric.Textcontainer(editor._get_text_sample('event_name'), {
left: 100,
top: 100,
width: 300,
height: 29,
lockRotation: false,
fill: '#000',
content: 'event_name',
text: editor._get_text_sample('event_name'),
fontFamily: 'Open Sans',
fontStyle: 'normal',
lineHeight: 1,
editable: false,
fontSize: editor._pt2px(13),
verticalAlign: 'middle',
textAlign: 'left',
splitLongWords: true,
autoResize: true,
lockScalingFlip: true,
});
rect.setControlsVisibility({
'tr': true,
'tl': true,
'mt': true,
'br': true,
'bl': true,
'mb': true,
'mr': true,
'ml': true,
'mtr': true
});
editor.fabric.add(rect);
editor._create_savepoint();
$("#version-notice").show();
return rect;
},
_add_imagearea: function () { _add_imagearea: function () {
var rect = new fabric.Imagearea({ var rect = new fabric.Imagearea({
left: 100, left: 100,
@@ -1063,6 +1337,7 @@ var editor = {
editor._load_pdf(); editor._load_pdf();
$("#editor-add-qrcode, #editor-add-qrcode-lead, #editor-add-qrcode-other").click(editor._add_qrcode); $("#editor-add-qrcode, #editor-add-qrcode-lead, #editor-add-qrcode-other").click(editor._add_qrcode);
$("#editor-add-image").click(editor._add_imagearea); $("#editor-add-image").click(editor._add_imagearea);
$("#editor-add-textcontainer").click(editor._add_textcontainer);
$("#editor-add-text").click(editor._add_text); $("#editor-add-text").click(editor._add_text);
$("#editor-add-poweredby").click(function() {editor._add_poweredby("dark")}); $("#editor-add-poweredby").click(function() {editor._add_poweredby("dark")});
editor.$cva.get(0).tabIndex = 1000; editor.$cva.get(0).tabIndex = 1000;
@@ -1144,6 +1419,7 @@ var editor = {
editor._update_save_button(); editor._update_save_button();
}); });
$("#pdf-info-width, #pdf-info-height").bind('change input', editor._paper_size_warning); $("#pdf-info-width, #pdf-info-height").bind('change input', editor._paper_size_warning);
editor._update_toolbox_values();
$.getJSON($("#schema-url").text(), function (data) { $.getJSON($("#schema-url").text(), function (data) {
editor.schema = data; editor.schema = data;

View File

@@ -298,29 +298,45 @@ var form_handlers = function (el) {
} }
} }
}).not(".no-contrast").on('changeColor create', function (e) { }).not(".no-contrast").on('changeColor create', function (e) {
if (e.type == 'changeColor' && !e.value) {
return;
}
var rgb = $(this).colorpicker('color').toRGB(); var rgb = $(this).colorpicker('color').toRGB();
var c = contrast([255,255,255], [rgb.r, rgb.g, rgb.b]); var c = contrast([255,255,255], [rgb.r, rgb.g, rgb.b]);
var mark = 'times'; var mark = 'times';
if ($(this).parent().find(".contrast-state").length === 0) { var $icon = $(this).parent().find(".contrast-icon");
if ($icon.length === 0 && $(this).parent().find(".contrast-state").length === 0) {
$(this).parent().append("<div class='help-block contrast-state'></div>"); $(this).parent().append("<div class='help-block contrast-state'></div>");
} }
var $note = $(this).parent().find(".contrast-state"); var $note = $(this).parent().find(".contrast-state");
if ($(this).val() === "") { if ($(this).val() === "") {
$note.remove(); $note.remove();
} }
var icon, text, cls;
if (c > 7) { if (c > 7) {
$note.html("<span class='fa fa-fw fa-check-circle'></span>") icon = "fa-check-circle";
.append(gettext('Your color has great contrast and is very easy to read!')); text = gettext('Your color has great contrast and is very easy to read!');
$note.addClass("text-success").removeClass("text-warning").removeClass("text-danger"); cls = "text-success";
} else if (c > 2.5) { } else if (c > 2.5) {
$note.html("<span class='fa fa-fw fa-info-circle'></span>") icon = "fa-info-circle";
.append(gettext('Your color has decent contrast and is probably good-enough to read!')); text = gettext('Your color has decent contrast and is probably good-enough to read!');
$note.removeClass("text-success").removeClass("text-warning").removeClass("text-danger"); cls = "";
} else { } else {
$note.html("<span class='fa fa-fw fa-warning'></span>") icon = "fa-warning";
.append(gettext('Your color has bad contrast for text on white background, please choose a darker ' + text = gettext('Your color has bad contrast for text on white background, please choose a darker shade.');
'shade.')); cls = "text-danger";
$note.addClass("text-danger").removeClass("text-success").removeClass("text-warning"); }
if ($icon.length === 0) {
$note.html("<span class='fa fa-fw " + icon + "'></span>")
.append(text);
$note.removeClass("text-success").removeClass("text-danger").addClass(cls);
} else {
$icon.html("<span class='fa fa-fw " + icon + " " + cls + "'></span>")
$icon.attr("title", text);
$icon.tooltip('destroy');
window.setTimeout(function() {
$icon.tooltip({"title": text});
}, 250);
} }
}); });

View File

@@ -19,6 +19,8 @@ body {
#toolbox .poweredby, #toolbox .poweredby,
#toolbox[data-type] .pdf-info, #toolbox[data-type] .pdf-info,
#toolbox .text, #toolbox .text,
#toolbox .textarea,
#toolbox .textcontainer,
#toolbox .imagecontent, #toolbox .imagecontent,
#toolbox .object-buttons { #toolbox .object-buttons {
display: none; display: none;
@@ -30,10 +32,23 @@ body {
#toolbox[data-type=imagearea] .imagecontent, #toolbox[data-type=imagearea] .imagecontent,
#toolbox[data-type=poweredby] .poweredby, #toolbox[data-type=poweredby] .poweredby,
#toolbox[data-type=text] .text, #toolbox[data-type=text] .text,
#toolbox[data-type=text] .textarea,
#toolbox[data-type=textcontainer] .text,
#toolbox[data-type=textcontainer] .textcontainer,
#toolbox[data-type=textcontainer] .rectsize,
#toolbox[data-type=textarea] .text, #toolbox[data-type=textarea] .text,
#toolbox[data-type=textarea] .textarea,
#toolbox[data-type] .object-buttons { #toolbox[data-type] .object-buttons {
display: block; display: block;
} }
#toolbox[data-type=text] .btn-group-justified > .btn-group.text,
#toolbox[data-type=text] .btn-group-justified > .btn-group.textarea,
#toolbox[data-type=textcontainer] .btn-group-justified > .btn-group.text,
#toolbox[data-type=textcontainer] .btn-group-justified > .btn-group.textcontainer,
#toolbox[data-type=textarea] .btn-group-justified > .btn-group.text,
#toolbox[data-type=textarea] .btn-group-justified > .btn-group.textarea {
display: table-cell;
}
#loading-container { #loading-container {
position: absolute; position: absolute;
top: 0; top: 0;