Source code for stoqlib.gui.utils.printing

# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4

##
## Copyright (C) 2005-2013 Async Open Source
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU Lesser General Public License
## as published by the Free Software Foundation; either version 2
## of the License, or (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., or visit: http://www.gnu.org/.
##
##
## Author(s): Stoq Team <stoq-devel@async.com.br>
##

import logging
import os
import platform
import tempfile
import threading

import gio
import gtk
import pango

from stoqlib.gui.base.dialogs import get_current_toplevel
from stoqlib.gui.events import PrintReportEvent
from stoqlib.lib.message import warning
from stoqlib.lib.osutils import get_application_dir
from stoqlib.lib.parameters import sysparam
from stoqlib.lib.template import render_template_string
from stoqlib.lib.threadutils import (schedule_in_main_thread,
                                     terminate_thread)
from stoqlib.lib.translation import stoqlib_gettext
from stoqlib.reporting.report import HTMLReport
from stoqlib.reporting.labelreport import LabelReport


_ = stoqlib_gettext
_system = platform.system()
log = logging.Logger(__name__)


# https://github.com/Kozea/WeasyPrint/issues/130
# http://pythonhosted.org/cairocffi/cffi_api.html#converting-pycairo-wrappers-to-cairocffi
def _UNSAFE_pycairo_context_to_cairocffi(pycairo_context):
    import cairocffi
    # Sanity check. Continuing with another type would probably segfault.
    if not isinstance(pycairo_context, gtk.gdk.CairoContext):
        raise TypeError('Expected a cairo.Context, got %r' % pycairo_context)

    # On CPython, id() gives the memory address of a Python object.
    # pycairo implements Context as a C struct:
    #     typedef struct {
    #         PyObject_HEAD
    #         cairo_t *ctx;
    #         PyObject *base;
    #     } PycairoContext;
    # Still on CPython, object.__basicsize__ is the size of PyObject_HEAD,
    # ie. the offset to the ctx field.
    # ffi.cast() converts the integer address to a cairo_t** pointer.
    # [0] dereferences that pointer, ie. read the ctx field.
    # The result is a cairo_t* pointer that cairocffi can use.
    return cairocffi.Context._from_pointer(
        cairocffi.ffi.cast('cairo_t **',
                           id(pycairo_context) + object.__basicsize__)[0],
        incref=True)


[docs]class PrintOperation(gtk.PrintOperation): def __init__(self, report): gtk.PrintOperation.__init__(self) self.connect("begin-print", self._on_operation_begin_print) self.connect("draw-page", self._on_operation_draw_page) self.connect("done", self._on_operation_done) self.connect("paginate", self._on_operation_paginate) self.connect("status-changed", self._on_operation_status_changed) self._in_nested_main_loop = False self._threaded = False self._printing_complete = False self._report = report self._rendering_thread = None self.set_job_name(self._report.title) self.set_show_progress(True) self.set_track_print_status(True) # Public API
[docs] def set_threaded(self): self._threaded = True self.set_allow_async(True)
[docs] def run(self): gtk.PrintOperation.run(self, gtk.PRINT_OPERATION_ACTION_PRINT_DIALOG, parent=get_current_toplevel()) # GtkPrintOperation.run() is not blocking by default, as the rendering # is threaded we need to wait for the operation to finish before we can # return from here, since currently the rendering depends on state that # might be released just after exiting this function. if self._threaded: self._in_nested_main_loop = True # Before creating a nested main loop, we need to process everything # that was pending on the main one as even the PrintOperation may # be waiting at this point. while gtk.events_pending(): gtk.main_iteration() gtk.main() self._in_nested_main_loop = False
[docs] def begin_print(self): """This is called before printing is done. It can be used to fetch print settings that the user selected in the dialog """
[docs] def render(self): """Renders the actual page. This might run in a separate thread, no glib/gtk+ calls are allowed here, they needs to be done in render_done() which is called when this is finished. """ raise NotImplementedError
[docs] def render_done(self): """Rendering of the printed page is done. This should call self.set_n_pages() """ raise NotImplementedError
[docs] def draw_page(self, cr, page_no): """Draws a page :param cr: a cairo context :param int page_no: the page to draw """ raise NotImplementedError
[docs] def done(self): """Called when rendering and drawing is complete, can be used to free resources created during printing. """
# Private API def _threaded_render(self): self.render() schedule_in_main_thread(self._threaded_render_done) def _threaded_render_done(self): if self.get_status() == gtk.PRINT_STATUS_FINISHED_ABORTED: return self.render_done() self._printing_complete = True def _is_rendering_finished(self): return self.get_status() in [ gtk.PRINT_STATUS_SENDING_DATA, gtk.PRINT_STATUS_FINISHED, gtk.PRINT_STATUS_FINISHED_ABORTED] # Callbacks def _on_operation_status_changed(self, operation): if (self._in_nested_main_loop and self._is_rendering_finished()): gtk.main_quit() if self.get_status() == gtk.PRINT_STATUS_FINISHED_ABORTED: terminate_thread(self._rendering_thread) def _on_operation_begin_print(self, operation, context): self.begin_print() if self._threaded: self._rendering_thread = threading.Thread(target=self._threaded_render) self._rendering_thread.start() else: self.render() self.render_done() self._printing_complete = True def _on_operation_paginate(self, operation, context): return self._printing_complete def _on_operation_draw_page(self, operation, context, page_no): cr = context.get_cairo_context() self.draw_page(cr, page_no) def _on_operation_done(self, operation, context): self.done()
[docs]class PrintOperationPoppler(PrintOperation): def __init__(self, report): PrintOperation.__init__(self, report) self._report.save() uri = gio.File(path=self._report.filename).get_uri() import poppler self._document = poppler.document_new_from_file(uri, password="") self.set_embed_page_setup(True)
[docs] def render(self): # FIXME: This is an specific fix for boleto printing in landscape # orientation. We should find a better fix for it or simply remove # PrintOperationPoppler when migrating the last reports using # reportlab to weasyprint if getattr(self._report, 'print_as_landscape', False): default_page_setup = gtk.PageSetup() default_page_setup.set_orientation(gtk.PAGE_ORIENTATION_LANDSCAPE) self.set_default_page_setup(default_page_setup)
[docs] def render_done(self): self.set_n_pages(self._document.get_n_pages())
[docs] def draw_page(self, cr, page_no): page = self._document.get_page(page_no) page.render_for_printing(cr)
[docs] def done(self): if not os.path.isfile(self._report.filename): return os.unlink(self._report.filename)
[docs]class PrintOperationWEasyPrint(PrintOperation): PRINT_CSS_TEMPLATE = """ @page { size: ${ page_width }mm ${ page_height }mm; font-family: "${ font_family }"; } body { font-family: "${ font_family }"; font-size: ${ font_size }pt; } """ page_setup_name = 'page_setup.ini' print_settings_name = 'print_settings.ini' def __init__(self, report): PrintOperation.__init__(self, report) self._load_settings() self.connect('create-custom-widget', self._on_operation_create_custom_widget) self.set_embed_page_setup(True) self.set_use_full_page(True) self.set_custom_tab_label(_('Stoq')) def _load_settings(self): self.config_dir = get_application_dir('stoq') settings = gtk.PrintSettings() filename = os.path.join(self.config_dir, self.print_settings_name) if os.path.exists(filename): settings.load_file(filename) self.set_print_settings(settings) default_page_setup = gtk.PageSetup() default_page_setup.set_orientation(gtk.PAGE_ORIENTATION_PORTRAIT) filename = os.path.join(self.config_dir, self.page_setup_name) if os.path.exists(filename): default_page_setup.load_file(filename) self.set_default_page_setup(default_page_setup)
[docs] def begin_print(self): self._fetch_settings()
[docs] def render(self): self._document = self._report.render( stylesheet=self.print_css)
[docs] def render_done(self): self.set_n_pages(len(self._document.pages))
[docs] def draw_page(self, cr, page_no): import weasyprint weasyprint_version = tuple(map(int, weasyprint.__version__.split('.'))) if weasyprint_version >= (0, 18): cr = _UNSAFE_pycairo_context_to_cairocffi(cr) # 0.75 is here because its also in weasyprint render_pdf() self._document.pages[page_no].paint(cr, scale=0.75)
# Private def _fetch_settings(self): font_name = self.font_button.get_font_name() settings = self.get_print_settings() settings.set('stoq-font-name', font_name) settings.to_file(os.path.join(self.config_dir, self.print_settings_name)) page_setup = self.get_default_page_setup() page_setup.to_file(os.path.join(self.config_dir, self.page_setup_name)) orientation = page_setup.get_orientation() paper_size = page_setup.get_paper_size() width = paper_size.get_width(gtk.UNIT_MM) height = paper_size.get_height(gtk.UNIT_MM) if orientation in (gtk.PAGE_ORIENTATION_LANDSCAPE, gtk.PAGE_ORIENTATION_REVERSE_LANDSCAPE): width, height = height, width descr = pango.FontDescription(font_name) # CSS expects fonts in pt, get_font_size() is scaled, # for screen display pango.SCALE should be used, it looks # okay for printed media again, since we're multiplying # with 0.75 at the easyprint level as well. At some point # we should probably align them. font_size = descr.get_size() / pango.SCALE self.print_css = render_template_string( self.PRINT_CSS_TEMPLATE, page_width=width, page_height=height, font_family=descr.get_family(), font_size=font_size) def _create_custom_tab(self): # TODO: Improve this code (maybe a slave) box = gtk.VBox() table = gtk.Table() table.set_row_spacings(6) table.set_col_spacings(6) table.set_border_width(6) table.attach(gtk.Label(_('Font:')), 0, 1, 0, 1, yoptions=0, xoptions=0) settings = self.get_print_settings() font_name = settings.get('stoq-font-name') self.font_button = gtk.FontButton(font_name) table.attach(self.font_button, 1, 2, 0, 1, xoptions=0, yoptions=0) box.pack_start(table, False, False) box.show_all() return box # Callbacks def _on_operation_create_custom_widget(self, operation): return self._create_custom_tab()
[docs]def describe_search_filters_for_reports(filters, **kwargs): filter_strings = [] for filter in filters: description = filter.get_description() if description: filter_strings.append(description) kwargs['filter_strings'] = filter_strings return kwargs