1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2007       Zsolt Foldvari
5# Copyright (C) 2008-2009  Brian G. Matherly
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20#
21
22"""Printing interface based on Gtk.Print* classes.
23"""
24
25#------------------------------------------------------------------------
26#
27# Python modules
28#
29#------------------------------------------------------------------------
30from math import radians
31import logging
32
33#-------------------------------------------------------------------------
34#
35# GTK modules
36#
37#-------------------------------------------------------------------------
38import cairo
39try: # the Gramps-Connect server has no DISPLAY
40    from gi.repository import GObject
41    from gi.repository import Gtk
42except:
43    pass
44
45#------------------------------------------------------------------------
46#
47# Gramps modules
48#
49#------------------------------------------------------------------------
50from gramps.gen.plug.docgen import PAPER_PORTRAIT
51import gramps.plugins.lib.libcairodoc as libcairodoc
52from gramps.gen.const import GRAMPS_LOCALE as glocale
53_ = glocale.translation.gettext
54
55#------------------------------------------------------------------------
56#
57# Set up logging
58#
59#------------------------------------------------------------------------
60LOG = logging.getLogger(".GtkPrint")
61
62#------------------------------------------------------------------------
63#
64# Constants
65#
66#------------------------------------------------------------------------
67
68# printer settings (might be needed to align for different platforms)
69PRINTER_DPI = 72.0
70PRINTER_SCALE = 1.0
71
72# the print settings to remember between print sessions
73PRINT_SETTINGS = None
74
75# minimum spacing around a page in print preview
76MARGIN = 6
77
78# zoom modes in print preview
79(ZOOM_BEST_FIT,
80 ZOOM_FIT_WIDTH,
81 ZOOM_FREE,) = list(range(3))
82
83#------------------------------------------------------------------------
84#
85# Converter functions
86#
87#------------------------------------------------------------------------
88
89def paperstyle_to_pagesetup(paper_style):
90    """Convert a PaperStyle instance into a Gtk.PageSetup instance.
91
92    @param paper_style: Gramps paper style object to convert
93    @param type: PaperStyle
94    @return: page_setup
95    @rtype: Gtk.PageSetup
96    """
97    # paper size names according to 'PWG Candidate Standard 5101.1-2002'
98    # ftp://ftp.pwg.org/pub/pwg/candidates/cs-pwgmsn10-20020226-5101.1.pdf
99    gramps_to_gtk = {
100        "Letter": "na_letter",
101        "Legal": "na_legal",
102        "A0": "iso_a0",
103        "A1": "iso_a1",
104        "A2": "iso_a2",
105        "A3": "iso_a3",
106        "A4": "iso_a4",
107        "A5": "iso_a5",
108        "B0": "iso_b0",
109        "B1": "iso_b1",
110        "B2": "iso_b2",
111        "B3": "iso_b3",
112        "B4": "iso_b4",
113        "B5": "iso_b5",
114        "B6": "iso_b6",
115        "B": "na_ledger",
116        "C": "na_c",
117        "D": "na_d",
118        "E": "na_e",
119    }
120
121    # First set the paper size
122    gramps_paper_size = paper_style.get_size()
123    gramps_paper_name = gramps_paper_size.get_name()
124
125    # All sizes not included in the translation table (even if a standard size)
126    # are handled as custom format, because we are not intelligent enough.
127    if gramps_paper_name in gramps_to_gtk:
128        paper_size = Gtk.PaperSize.new(name=gramps_to_gtk[gramps_paper_name])
129        LOG.debug("Selected paper size: %s", gramps_to_gtk[gramps_paper_name])
130    else:
131        if paper_style.get_orientation() == PAPER_PORTRAIT:
132            paper_width = gramps_paper_size.get_width() * 10
133            paper_height = gramps_paper_size.get_height() * 10
134        else:
135            paper_width = gramps_paper_size.get_height() * 10
136            paper_height = gramps_paper_size.get_width() * 10
137        paper_size = Gtk.PaperSize.new_custom("custom", "Custom Size",
138                                              paper_width, paper_height,
139                                              Gtk.Unit.MM)
140        LOG.debug("Selected paper size: (%f,%f)", paper_width, paper_height)
141
142    page_setup = Gtk.PageSetup()
143    page_setup.set_paper_size(paper_size)
144
145    # Set paper orientation
146    if paper_style.get_orientation() == PAPER_PORTRAIT:
147        page_setup.set_orientation(Gtk.PageOrientation.PORTRAIT)
148    else:
149        page_setup.set_orientation(Gtk.PageOrientation.LANDSCAPE)
150
151    # Set paper margins
152    page_setup.set_top_margin(paper_style.get_top_margin() * 10,
153                              Gtk.Unit.MM)
154    page_setup.set_bottom_margin(paper_style.get_bottom_margin() * 10,
155                                 Gtk.Unit.MM)
156    page_setup.set_left_margin(paper_style.get_left_margin() * 10,
157                               Gtk.Unit.MM)
158    page_setup.set_right_margin(paper_style.get_right_margin() * 10,
159                                Gtk.Unit.MM)
160
161    return page_setup
162
163#------------------------------------------------------------------------
164#
165# PrintPreview class
166#
167#------------------------------------------------------------------------
168class PrintPreview:
169    """Implement a dialog to show print preview.
170    """
171    zoom_factors = {
172        0.50: '50%',
173        0.75: '75%',
174        1.00: '100%',
175        1.25: '125%',
176        1.50: '150%',
177        1.75: '175%',
178        2.00: '200%',
179        3.00: '300%',
180        4.00: '400%',
181    }
182
183    def __init__(self, operation, preview, context, parent):
184        self._operation = operation
185        self._preview = preview
186        self._context = context
187        self._parent = parent
188
189        self.__build_window()
190        self._current_page = None
191
192    # Private
193
194    def __build_window(self):
195        """Build the window from Glade.
196        """
197        from gramps.gui.glade import Glade
198        glade_xml = Glade()
199        self._window = glade_xml.toplevel
200        self._window.set_transient_for(self._parent)
201
202        # remember active widgets for future use
203        self._swin = glade_xml.get_object('swin')
204        self._drawing_area = glade_xml.get_object('drawingarea')
205        self._first_button = glade_xml.get_object('first')
206        self._prev_button = glade_xml.get_object('prev')
207        self._next_button = glade_xml.get_object('next')
208        self._last_button = glade_xml.get_object('last')
209        self._pages_entry = glade_xml.get_object('entry')
210        self._pages_label = glade_xml.get_object('label')
211        self._zoom_fit_width_button = glade_xml.get_object('zoom_fit_width')
212        self._zoom_fit_width_button.set_stock_id('gramps-zoom-fit-width')
213        self._zoom_best_fit_button = glade_xml.get_object('zoom_best_fit')
214        self._zoom_best_fit_button.set_stock_id('gramps-zoom-best-fit')
215        self._zoom_in_button = glade_xml.get_object('zoom_in')
216        self._zoom_in_button.set_stock_id('gramps-zoom-in')
217        self._zoom_out_button = glade_xml.get_object('zoom_out')
218        self._zoom_out_button.set_stock_id('gramps-zoom-out')
219
220        # connect the signals
221        glade_xml.connect_signals(self)
222        self._drawing_area.connect("draw", self.on_drawingarea_draw_event)
223
224    ##def create_surface(self):
225        ##return cairo.PDFSurface(StringIO(),
226                                ##self._context.get_width(),
227                                ##self._context.get_height())
228
229    ##def get_page(self, page_no):
230        ##"""Get the cairo surface of the given page.
231
232        ##Surfaces are also cached for instant access.
233
234        ##"""
235        ##if page_no >= len(self._page_numbers):
236            ##LOG.debug("Page number %d doesn't exist." % page_no)
237            ##page_no = 0
238
239        ##if page_no not in self._page_surfaces:
240            ##surface = self.create_surface()
241            ##cr = cairo.Context(surface)
242
243            ##if PRINTER_SCALE != 1.0:
244                ##cr.scale(PRINTER_SCALE, PRINTER_SCALE)
245
246            ##self._context.set_cairo_context(cr, PRINTER_DPI, PRINTER_DPI)
247            ##self._preview.render_page(self._page_numbers[page_no])
248
249            ##self._page_surfaces[page_no] = surface
250
251        ##return self._page_surfaces[page_no]
252
253    def __set_page(self, page_no):
254        if page_no < 0 or page_no >= self._page_no:
255            return
256
257        if self._current_page != page_no:
258            self._drawing_area.queue_draw()
259
260        self._current_page = page_no
261
262        self._first_button.set_sensitive(self._current_page)
263        self._prev_button.set_sensitive(self._current_page)
264        self._next_button.set_sensitive(self._current_page < self._page_no - 1)
265        self._last_button.set_sensitive(self._current_page < self._page_no - 1)
266
267        self._pages_entry.set_text('%d' % (self._current_page + 1))
268
269    def __set_zoom(self, zoom):
270        self._zoom = zoom
271
272        screen_width = int(self._paper_width * self._zoom + 2 * MARGIN)
273        screen_height = int(self._paper_height * self._zoom + 2 * MARGIN)
274        self._drawing_area.set_size_request(screen_width, screen_height)
275        self._drawing_area.queue_draw()
276
277        self._zoom_in_button.set_sensitive(self._zoom !=
278                                           max(self.zoom_factors))
279        self._zoom_out_button.set_sensitive(self._zoom !=
280                                            min(self.zoom_factors))
281
282    def __zoom_in(self):
283        zoom = [z for z in self.zoom_factors if z > self._zoom]
284
285        if zoom:
286            return min(zoom)
287        else:
288            return self._zoom
289
290    def __zoom_out(self):
291        zoom = [z for z in self.zoom_factors if z < self._zoom]
292
293        if zoom:
294            return max(zoom)
295        else:
296            return self._zoom
297
298    def __zoom_fit_width(self):
299        width, height, vsb_w, hsb_h = self.__get_view_size()
300
301        zoom = width / self._paper_width
302        if self._paper_height * zoom > height:
303            zoom = (width - vsb_w) / self._paper_width
304
305        return zoom
306
307    def __zoom_best_fit(self):
308        width, height, vsb_w, hsb_h = self.__get_view_size()
309
310        zoom = min(width / self._paper_width, height / self._paper_height)
311
312        return zoom
313
314    def __get_view_size(self):
315        """Get the dimensions of the scrolled window.
316        """
317        width = self._swin.get_allocated_width() - 2 * MARGIN
318        height = self._swin.get_allocated_height() - 2 * MARGIN
319
320        if self._swin.get_shadow_type() != Gtk.ShadowType.NONE:
321            width -= 2 * self._swin.get_style().xthickness
322            height -= 2 * self._swin.get_style().ythickness
323
324        spacing = GObject.Value()
325        spacing.init(GObject.TYPE_INT)
326        spacing = self._swin.style_get_property('scrollbar-spacing', spacing)
327        if spacing:
328            spacing = spacing.get_int()
329        else:
330            spacing = 0
331
332        reqmin, req = self._swin.get_vscrollbar().get_preferred_size()
333        vsb_w = spacing + req.width
334        reqmin, req = self._swin.get_hscrollbar().get_preferred_size()
335        hsb_h = spacing + req.height
336
337        return width, height, vsb_w, hsb_h
338
339    def __end_preview(self):
340        self._operation.end_preview()
341
342    # Signal handlers
343
344    def on_drawingarea_draw_event(self, drawing_area, context):
345        cr = context
346        #cr.rectangle(event.area)
347        #cr.clip()
348
349        # get the extents of the page and the screen
350        paper_w = int(self._paper_width * self._zoom)
351        paper_h = int(self._paper_height * self._zoom)
352
353        width, height, vsb_w, hsb_h = self.__get_view_size()
354        if paper_h > height:
355            width -= vsb_w
356        if paper_w > width:
357            height -= hsb_h
358
359        # put the paper on the middle of the window
360        xtranslate = MARGIN
361        if  paper_w < width:
362            xtranslate += (width - paper_w) / 2
363
364        ytranslate = MARGIN
365        if  paper_h < height:
366            ytranslate += (height - paper_h) / 2
367
368        cr.translate(xtranslate, ytranslate)
369
370        # draw an empty white page
371        cr.set_source_rgb(1.0, 1.0, 1.0)
372        cr.rectangle(0, 0, paper_w, paper_h)
373        cr.fill_preserve()
374        cr.set_source_rgb(0, 0, 0)
375        cr.set_line_width(1)
376        cr.stroke()
377
378        if self._orientation == Gtk.PageOrientation.LANDSCAPE:
379            cr.rotate(radians(-90))
380            cr.translate(-paper_h, 0)
381
382        ##page_setup = self._context.get_page_setup()
383        ##cr.translate(page_setup.get_left_margin(Gtk.Unit.POINTS),
384                     ##page_setup.get_top_margin(Gtk.Unit.POINTS))
385
386        ##cr.set_source_surface(self.get_page(0))
387        ##cr.paint()
388
389        # draw the content of the currently selected page
390        #     Here we use dpi scaling instead of scaling the cairo context,
391        #     because it gives better result. In the latter case the distance
392        #     of glyphs was changing.
393        dpi = PRINTER_DPI * self._zoom
394        self._context.set_cairo_context(cr, dpi, dpi)
395        self._preview.render_page(self._current_page)
396
397    def on_swin_size_allocate(self, scrolledwindow, allocation):
398        if self._zoom_mode == ZOOM_FIT_WIDTH:
399            self.__set_zoom(self.__zoom_fit_width())
400
401        if self._zoom_mode == ZOOM_BEST_FIT:
402            self.__set_zoom(self.__zoom_best_fit())
403
404    def on_print_clicked(self, toolbutton):
405        pass
406
407    def on_first_clicked(self, toolbutton):
408        self.__set_page(0)
409
410    def on_prev_clicked(self, toolbutton):
411        self.__set_page(self._current_page - 1)
412
413    def on_next_clicked(self, toolbutton):
414        self.__set_page(self._current_page + 1)
415
416    def on_last_clicked(self, toolbutton):
417        self.__set_page(self._page_no - 1)
418
419    def on_entry_activate(self, entry):
420        try:
421            new_page = int(entry.get_text()) - 1
422        except ValueError:
423            new_page = self._current_page
424
425        if new_page < 0 or new_page >= self._page_no:
426            new_page = self._current_page
427
428        self.__set_page(new_page)
429
430    def on_zoom_fit_width_toggled(self, toggletoolbutton):
431        if toggletoolbutton.get_active():
432            self._zoom_best_fit_button.set_active(False)
433            self._zoom_mode = ZOOM_FIT_WIDTH
434            self.__set_zoom(self.__zoom_fit_width())
435        else:
436            self._zoom_mode = ZOOM_FREE
437
438    def on_zoom_best_fit_toggled(self, toggletoolbutton):
439        if toggletoolbutton.get_active():
440            self._zoom_fit_width_button.set_active(False)
441            self._zoom_mode = ZOOM_BEST_FIT
442            self.__set_zoom(self.__zoom_best_fit())
443        else:
444            self._zoom_mode = ZOOM_FREE
445
446    def on_zoom_in_clicked(self, toolbutton):
447        self._zoom_fit_width_button.set_active(False)
448        self._zoom_best_fit_button.set_active(False)
449        self._zoom_mode = ZOOM_FREE
450        self.__set_zoom(self.__zoom_in())
451
452    def on_zoom_out_clicked(self, toolbutton):
453        self._zoom_fit_width_button.set_active(False)
454        self._zoom_best_fit_button.set_active(False)
455        self._zoom_mode = ZOOM_FREE
456        self.__set_zoom(self.__zoom_out())
457
458    def on_window_delete_event(self, widget, event):
459        self.__end_preview()
460        return False
461
462    def on_quit_clicked(self, toolbutton):
463        self.__end_preview()
464        self._window.destroy()
465
466    # Public
467
468    def start(self):
469        # get paper/page dimensions
470        page_setup = self._context.get_page_setup()
471        self._paper_width = page_setup.get_paper_width(Gtk.Unit.POINTS)
472        self._paper_height = page_setup.get_paper_height(Gtk.Unit.POINTS)
473        self._page_width = page_setup.get_page_width(Gtk.Unit.POINTS)
474        self._page_height = page_setup.get_page_height(Gtk.Unit.POINTS)
475        self._orientation = page_setup.get_orientation()
476
477        # get the total number of pages
478        ##self._page_numbers = [0,]
479        ##self._page_surfaces = {}
480        self._page_no = self._operation.get_property('n_pages')
481        self._pages_label.set_text(_('of %d') % self._page_no)
482
483        # set zoom level and initial page number
484        self._zoom_mode = ZOOM_FREE
485        self.__set_zoom(1.0)
486        self.__set_page(0)
487
488        # let's the show begin...
489        self._window.show()
490
491#------------------------------------------------------------------------
492#
493# GtkPrint class
494#
495#------------------------------------------------------------------------
496class GtkPrint(libcairodoc.CairoDoc):
497    """Print document via GtkPrint* interface.
498
499    Requires Gtk+ 2.10.
500
501    """
502    def run(self):
503        """Run the Gtk Print operation.
504        """
505        global PRINT_SETTINGS
506
507        # get a page setup from the paper style we have
508        page_setup = paperstyle_to_pagesetup(self.paper)
509
510        # set up a print operation
511        operation = Gtk.PrintOperation()
512        operation.set_default_page_setup(page_setup)
513        operation.connect("begin_print", self.on_begin_print)
514        operation.connect("draw_page", self.on_draw_page)
515        operation.connect("paginate", self.on_paginate)
516        operation.connect("preview", self.on_preview)
517
518        # set print settings if it was stored previously
519        if PRINT_SETTINGS is not None:
520            operation.set_print_settings(PRINT_SETTINGS)
521
522        # run print dialog
523        while True:
524            self.preview = None
525            res = operation.run(Gtk.PrintOperationAction.PRINT_DIALOG,
526                                self.uistate.window)
527            if self.preview is None: # cancel or print
528                break
529            # set up printing again; can't reuse PrintOperation?
530            operation = Gtk.PrintOperation()
531            operation.set_default_page_setup(page_setup)
532            operation.connect("begin_print", self.on_begin_print)
533            operation.connect("draw_page", self.on_draw_page)
534            operation.connect("paginate", self.on_paginate)
535            operation.connect("preview", self.on_preview)
536            # set print settings if it was stored previously
537            if PRINT_SETTINGS is not None:
538                operation.set_print_settings(PRINT_SETTINGS)
539
540        # store print settings if printing was successful
541        if res == Gtk.PrintOperationResult.APPLY:
542            PRINT_SETTINGS = operation.get_print_settings()
543
544    def on_begin_print(self, operation, context):
545        """Setup environment for printing.
546        """
547        # get data from context here only once to save time on pagination
548        self.page_width = round(context.get_width())
549        self.page_height = round(context.get_height())
550        self.dpi_x = context.get_dpi_x()
551        self.dpi_y = context.get_dpi_y()
552
553    def on_paginate(self, operation, context):
554        """Paginate the whole document in chunks.
555        """
556        layout = context.create_pango_layout()
557
558        finished = self.paginate(layout,
559                                 self.page_width,
560                                 self.page_height,
561                                 self.dpi_x,
562                                 self.dpi_y)
563        # update page number
564        operation.set_n_pages(len(self._pages))
565
566        # start preview if needed
567        if finished and self.preview:
568            self.preview.start()
569
570        return finished
571
572    def on_draw_page(self, operation, context, page_nr):
573        """Draw the requested page.
574        """
575        cr = context.get_cairo_context()
576        layout = context.create_pango_layout()
577        width = round(context.get_width())
578        height = round(context.get_height())
579        dpi_x = context.get_dpi_x()
580        dpi_y = context.get_dpi_y()
581
582        self.draw_page(page_nr, cr, layout, width, height, dpi_x, dpi_y)
583
584    def on_preview(self, operation, preview, context, parent):
585        """Implement custom print preview functionality.
586        """
587        ##if constfunc.win()':
588            ##return False
589
590        self.preview = PrintPreview(operation, preview, context, parent)
591
592        # give a dummy cairo context to Gtk.PrintContext,
593        # PrintPreview will update it with the real one
594        try:
595            width = int(round(context.get_width()))
596        except ValueError:
597            width = 0
598        try:
599            height = int(round(context.get_height()))
600        except ValueError:
601            height = 0
602        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
603        cr = cairo.Context(surface)
604        context.set_cairo_context(cr, PRINTER_DPI, PRINTER_DPI)
605
606        return True
607