From 8a443da4d25c3168b9b16331dfb81ae5e0485032 Mon Sep 17 00:00:00 2001 From: Nick Hall Date: Sun, 12 Feb 2012 21:55:07 +0000 Subject: [PATCH] 5326: Add Alphabetical Index and Table of Contents generation for pdf reports svn: r18870 --- po/POTFILES.in | 2 + src/gen/plug/docgen/textdoc.py | 13 ++ src/plugins/docgen/PdfDoc.py | 159 +++++++++++++++++++- src/plugins/lib/libcairodoc.py | 117 +++++++++++++- src/plugins/textreport/AlphabeticalIndex.py | 108 +++++++++++++ src/plugins/textreport/Makefile.am | 4 +- src/plugins/textreport/TableOfContents.py | 125 +++++++++++++++ src/plugins/textreport/textplugins.gpr.py | 45 ++++++ 8 files changed, 566 insertions(+), 7 deletions(-) create mode 100644 src/plugins/textreport/AlphabeticalIndex.py create mode 100644 src/plugins/textreport/TableOfContents.py diff --git a/po/POTFILES.in b/po/POTFILES.in index cdd04f708..5bfd45d91 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -356,6 +356,7 @@ src/plugins/rel/relplugins.gpr.py src/plugins/sidebar/sidebar.gpr.py # plugins/textreport directory +src/plugins/textreport/AlphabeticalIndex.py src/plugins/textreport/AncestorReport.py src/plugins/textreport/BirthdayReport.py src/plugins/textreport/CustomBookText.py @@ -370,6 +371,7 @@ src/plugins/textreport/NumberOfAncestorsReport.py src/plugins/textreport/PlaceReport.py src/plugins/textreport/SimpleBookTitle.py src/plugins/textreport/Summary.py +src/plugins/textreport/TableOfContents.py src/plugins/textreport/TagReport.py src/plugins/textreport/textplugins.gpr.py diff --git a/src/gen/plug/docgen/textdoc.py b/src/gen/plug/docgen/textdoc.py index 9c8a525be..058a3ad94 100644 --- a/src/gen/plug/docgen/textdoc.py +++ b/src/gen/plug/docgen/textdoc.py @@ -299,3 +299,16 @@ class TextDoc(object): """ pass + def insert_toc(self): + """ + Insert a Table of Contents at this point in the document. This passes + without error so that docgen types are not required to have this. + """ + pass + + def insert_index(self): + """ + Insert an Alphabetical Index at this point in the document. This passes + without error so that docgen types are not required to have this. + """ + pass diff --git a/src/plugins/docgen/PdfDoc.py b/src/plugins/docgen/PdfDoc.py index 97dc99416..de39d9d33 100644 --- a/src/plugins/docgen/PdfDoc.py +++ b/src/plugins/docgen/PdfDoc.py @@ -38,6 +38,8 @@ import sys # #------------------------------------------------------------------------ import libcairodoc +from gen.plug.docgen import INDEX_TYPE_ALP, INDEX_TYPE_TOC +from gen.plug.report.toc_index import write_toc, write_index import Errors #------------------------------------------------------------------------ @@ -109,11 +111,69 @@ class PdfDoc(libcairodoc.CairoDoc): cr.update_context(pango_context) # paginate the document - finished = self.paginate(layout, page_width, page_height, DPI, DPI) - while not finished: - finished = self.paginate(layout, page_width, page_height, DPI, DPI) + self.paginate_document(layout, page_width, page_height, DPI, DPI) + body_pages = self._pages + + # build the table of contents and alphabetical index + toc_page = None + index_page = None + toc = [] + index = {} + for page_nr, page in enumerate(body_pages): + if page.has_toc(): + toc_page = page_nr + if page.has_index(): + index_page = page_nr + for mark in page.get_marks(): + if mark.type == INDEX_TYPE_ALP: + if mark.key in index: + if page_nr + 1 not in index[mark.key]: + index[mark.key].append(page_nr + 1) + else: + index[mark.key] = [page_nr + 1] + elif mark.type == INDEX_TYPE_TOC: + toc.append([mark, page_nr + 1]) + + # paginate the table of contents + rebuild_required = False + if toc_page is not None: + toc_pages = self.__generate_toc(layout, page_width, page_height, + toc) + offset = len(toc_pages) - 1 + if offset > 0: + self.__increment_pages(toc, index, toc_page, offset) + rebuild_required = True + else: + toc_pages = [] + + # paginate the index + if index_page is not None: + index_pages = self.__generate_index(layout, page_width, page_height, + index) + offset = len(index_pages) - 1 + if offset > 0: + self.__increment_pages(toc, index, index_page, offset) + rebuild_required = True + else: + index_pages = [] + + # rebuild the table of contents and index if required + if rebuild_required: + if toc_page is not None: + toc_pages = self.__generate_toc(layout, page_width, page_height, + toc) + if index_page is not None: + index_pages = self.__generate_index(layout, page_width, + page_height, index) # render the pages + if toc_page is not None: + body_pages = body_pages[:toc_page] + toc_pages + \ + body_pages[toc_page+1:] + if index_page is not None: + body_pages = body_pages[:index_page] + index_pages + \ + body_pages[index_page+1:] + self._pages = body_pages for page_nr in range(len(self._pages)): cr.save() cr.translate(left_margin, top_margin) @@ -130,3 +190,96 @@ class PdfDoc(libcairodoc.CairoDoc): # if we don't restore the resolution. fontmap.set_resolution(saved_resolution) + def __increment_pages(self, toc, index, start_page, offset): + """ + Increment the page numbers in the table of contents and index. + """ + for n, value in enumerate(toc): + page_nr = toc[n][1] + toc[n][1] = page_nr + (offset if page_nr > start_page else 0) + for key, value in index.iteritems(): + index[key] = [page_nr + (offset if page_nr > start_page else 0) + for page_nr in value] + + def __generate_toc(self, layout, page_width, page_height, toc): + """ + Generate the table of contents. + """ + self._doc = libcairodoc.GtkDocDocument() + self._active_element = self._doc + self._pages = [] + write_toc(toc, self) + self.paginate_document(layout, page_width, page_height, DPI, DPI) + return self._pages + + def __generate_index(self, layout, page_width, page_height, index): + """ + Generate the index. + """ + self._doc = libcairodoc.GtkDocDocument() + self._active_element = self._doc + self._pages = [] + write_index(index, self) + self.paginate_document(layout, page_width, page_height, DPI, DPI) + return self._pages + +def write_toc(toc, doc): + """ + Write the table of contents. + """ + if not toc: + return + + doc.start_paragraph('TOC-Title') + doc.write_text(_('Contents')) + doc.end_paragraph() + + doc.start_table('toc', 'TOC-Table') + for mark, page_nr in toc: + doc.start_row() + doc.start_cell('TOC-Cell') + if mark.level == 1: + style_name = "TOC-Heading1" + elif mark.level == 2: + style_name = "TOC-Heading2" + else: + style_name = "TOC-Heading3" + doc.start_paragraph(style_name) + doc.write_text(mark.key) + doc.end_paragraph() + doc.end_cell() + doc.start_cell('TOC-Cell') + doc.start_paragraph('TOC-Number') + doc.write_text(str(page_nr)) + doc.end_paragraph() + doc.end_cell() + doc.end_row() + doc.end_table() + +def write_index(index, doc): + """ + Write the alphabetical index. + """ + if not index: + return + + doc.start_paragraph('Index-Title') + doc.write_text(_('Index')) + doc.end_paragraph() + + doc.start_table('index', 'Index-Table') + for key in sorted(index.iterkeys()): + doc.start_row() + doc.start_cell('Index-Cell') + doc.start_paragraph('Index-Number') + doc.write_text(key) + doc.end_paragraph() + doc.end_cell() + doc.start_cell('Index-Cell') + doc.start_paragraph('Index-Number') + pages = [str(page_nr) for page_nr in index[key]] + doc.write_text(', '.join(pages)) + doc.end_paragraph() + doc.end_cell() + doc.end_row() + doc.end_table() diff --git a/src/plugins/lib/libcairodoc.py b/src/plugins/lib/libcairodoc.py index c453babba..dd4b1229f 100644 --- a/src/plugins/lib/libcairodoc.py +++ b/src/plugins/lib/libcairodoc.py @@ -35,6 +35,7 @@ #------------------------------------------------------------------------ from gen.ggettext import gettext as _ from math import radians +import re #------------------------------------------------------------------------ # @@ -174,6 +175,18 @@ def tabstops_to_tabarray(tab_stops, dpi): return tab_array +def raw_length(s): + """ + Return the length of the raw string after all pango markup has been removed. + """ + s = re.sub('<.*?>', '', s) + s = s.replace('&', '&') + s = s.replace('<', '<') + s = s.replace('>', '>') + s = s.replace('"', '"') + s = s.replace(''', "'") + return len(s) + ###------------------------------------------------------------------------ ### ### Table row style @@ -321,6 +334,14 @@ class GtkDocBaseElement(object): """ return self._children + def get_marks(self): + """Get the list of index marks for this element. + """ + marks = [] + for child in self._children: + marks.extend(child.get_marks()) + return marks + def divide(self, layout, width, height, dpi_x, dpi_y): """Divide the element into two depending on available space. @@ -365,7 +386,8 @@ class GtkDocDocument(GtkDocBaseElement): """The whole document or a page. """ _type = 'DOCUMENT' - _allowed_children = ['PARAGRAPH', 'PAGEBREAK', 'TABLE', 'IMAGE', 'FRAME'] + _allowed_children = ['PARAGRAPH', 'PAGEBREAK', 'TABLE', 'IMAGE', 'FRAME', + 'TOC', 'INDEX'] def draw(self, cairo_context, pango_layout, width, dpi_x, dpi_y): @@ -378,7 +400,19 @@ class GtkDocDocument(GtkDocBaseElement): y += elem_height return y - + + def has_toc(self): + for elem in self._children: + if elem.get_type() == 'TOC': + return True + return False + + def has_index(self): + for elem in self._children: + if elem.get_type() == 'INDEX': + return True + return False + class GtkDocPagebreak(GtkDocBaseElement): """Implement a page break. """ @@ -388,6 +422,30 @@ class GtkDocPagebreak(GtkDocBaseElement): def divide(self, layout, width, height, dpi_x, dpi_y): return (None, None), 0 +class GtkDocTableOfContents(GtkDocBaseElement): + """Implement a table of contents. + """ + _type = 'TOC' + _allowed_children = [] + + def divide(self, layout, width, height, dpi_x, dpi_y): + return (self, None), 0 + + def draw(self, cr, layout, width, dpi_x, dpi_y): + return 0 + +class GtkDocAlphabeticalIndex(GtkDocBaseElement): + """Implement an alphabetical index. + """ + _type = 'INDEX' + _allowed_children = [] + + def divide(self, layout, width, height, dpi_x, dpi_y): + return (self, None), 0 + + def draw(self, cr, layout, width, dpi_x, dpi_y): + return 0 + class GtkDocParagraph(GtkDocBaseElement): """Paragraph. """ @@ -410,12 +468,32 @@ class GtkDocParagraph(GtkDocBaseElement): self._plaintext = None self._attrlist = None + self._marklist = [] + def add_text(self, text): if self._plaintext is not None: raise PluginError('CairoDoc: text is already parsed.' ' You cannot add text anymore') self._text = self._text + text + def add_mark(self, mark): + """ + Add an index mark to this paragraph + """ + self._marklist.append((mark, raw_length(self._text))) + + def get_marks(self): + """ + Return a list of index marks for this paragraph + """ + return [elem[0] for elem in self._marklist] + + def __set_marklist(self, marklist): + """ + Internal method to allow for splitting of paragraphs + """ + self._marklist = marklist + def __set_plaintext(self, plaintext): """ Internal method to allow for splitting of paragraphs @@ -562,7 +640,18 @@ class GtkDocParagraph(GtkDocBaseElement): # then update the first one self.__set_plaintext(self._plaintext.encode('utf-8')[:index]) self._style.set_bottom_margin(0) - + + # split the list of index marks + para1 = [] + para2 = [] + for mark, position in self._marklist: + if position < index: + para1.append((mark, position)) + else: + para2.append((mark, position - index)) + self.__set_marklist(para1) + new_paragraph.__set_marklist(para2) + paragraph_height = endheight - startheight + spacing + t_margin + 2 * v_padding return (self, new_paragraph), paragraph_height @@ -1374,6 +1463,10 @@ links (like ODF) and write PDF from that format. # The markup in the note editor is not in the text so is not # considered. It must be added by pango too text = self._backend.ESCAPE_FUNC()(text) + + if mark: + self._active_element.add_mark(mark) + self._active_element.add_text(text) def write_text(self, text, mark=None, links=False): @@ -1416,6 +1509,18 @@ links (like ODF) and write PDF from that format. new_paragraph.add_text('\n'.join(alt)) self._active_element.add_child(new_paragraph) + def insert_toc(self): + """ + Insert a Table of Contents at this point in the document. + """ + self._doc.add_child(GtkDocTableOfContents()) + + def insert_index(self): + """ + Insert an Alphabetical Index at this point in the document. + """ + self._doc.add_child(GtkDocAlphabeticalIndex()) + # DrawDoc implementation def start_page(self): @@ -1515,6 +1620,12 @@ links (like ODF) and write PDF from that format. """ raise NotImplementedError + def paginate_document(self, layout, page_width, page_height, dpi_x, dpi_y): + """Paginate the entire document. + """ + while not self.paginate(layout, page_width, page_height, dpi_x, dpi_y): + pass + def paginate(self, layout, page_width, page_height, dpi_x, dpi_y): """Paginate the meta document in chunks. diff --git a/src/plugins/textreport/AlphabeticalIndex.py b/src/plugins/textreport/AlphabeticalIndex.py new file mode 100644 index 000000000..83aab8b04 --- /dev/null +++ b/src/plugins/textreport/AlphabeticalIndex.py @@ -0,0 +1,108 @@ +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2012 Nick Hall +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# $Id$ + +#------------------------------------------------------------------------ +# +# Python modules +# +#------------------------------------------------------------------------ +from gen.ggettext import sgettext as _ + +#------------------------------------------------------------------------ +# +# Gramps modules +# +#------------------------------------------------------------------------ +from gen.plug.report import Report +from gen.plug.report import MenuReportOptions +from gen.plug.docgen import (FontStyle, ParagraphStyle, TableStyle, + TableCellStyle, FONT_SANS_SERIF) + +#------------------------------------------------------------------------ +# +# AlphabeticalIndex +# +#------------------------------------------------------------------------ +class AlphabeticalIndex(Report): + """ This report class generates an alphabetical index for a book. """ + def __init__(self, database, options, user): + """ + Create AlphabeticalIndex object that produces the report. + + The arguments are: + + database - the GRAMPS database instance + options - instance of the Options class for this report + user - a gen.user.User() instance + """ + Report.__init__(self, database, options, user) + self._user = user + + menu = options.menu + + def write_report(self): + """ Generate the contents of the report """ + self.doc.insert_index() + +#------------------------------------------------------------------------ +# +# AlphabeticalIndexOptions +# +#------------------------------------------------------------------------ +class AlphabeticalIndexOptions(MenuReportOptions): + + """ + Defines options and provides handling interface. + """ + + def __init__(self, name, dbase): + self.__db = dbase + MenuReportOptions.__init__(self, name, dbase) + + def add_menu_options(self, menu): + """ Add the options for this report """ + pass + + def make_default_style(self, default_style): + """Make the default output style for the AlphabeticalIndex report.""" + font = FontStyle() + font.set(face=FONT_SANS_SERIF, size=14) + para = ParagraphStyle() + para.set_font(font) + para.set_bottom_margin(0.25) + para.set_description(_('The style used for the Index title.')) + default_style.add_paragraph_style("Index-Title", para) + + table = TableStyle() + table.set_width(100) + table.set_columns(2) + table.set_column_width(0, 80) + table.set_column_width(1, 20) + default_style.add_table_style("Index-Table", table) + + cell = TableCellStyle() + default_style.add_cell_style("Index-Cell", cell) + + font = FontStyle() + font.set(face=FONT_SANS_SERIF, size=10) + para = ParagraphStyle() + para.set_font(font) + para.set_description(_('The style used for the Index page numbers.')) + default_style.add_paragraph_style("Index-Number", para) diff --git a/src/plugins/textreport/Makefile.am b/src/plugins/textreport/Makefile.am index 84a971961..f3dce7bef 100644 --- a/src/plugins/textreport/Makefile.am +++ b/src/plugins/textreport/Makefile.am @@ -7,6 +7,7 @@ pkgpythondir = $(datadir)/@PACKAGE@/plugins/textreport pkgpython_PYTHON = \ + AlphabeticalIndex.py\ AncestorReport.py\ BirthdayReport.py\ CustomBookText.py\ @@ -21,7 +22,8 @@ pkgpython_PYTHON = \ PlaceReport.py\ SimpleBookTitle.py\ Summary.py\ - TagReport.py\ + TableOfContents.py\ + TagReport.py\ textplugins.gpr.py pkgpyexecdir = @pkgpyexecdir@/plugins/textreport diff --git a/src/plugins/textreport/TableOfContents.py b/src/plugins/textreport/TableOfContents.py new file mode 100644 index 000000000..2fccab290 --- /dev/null +++ b/src/plugins/textreport/TableOfContents.py @@ -0,0 +1,125 @@ +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2012 Nick Hall +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# $Id$ + +#------------------------------------------------------------------------ +# +# Python modules +# +#------------------------------------------------------------------------ +from gen.ggettext import sgettext as _ + +#------------------------------------------------------------------------ +# +# Gramps modules +# +#------------------------------------------------------------------------ +from gen.plug.report import Report +from gen.plug.report import MenuReportOptions +from gen.plug.docgen import (FontStyle, ParagraphStyle, TableStyle, + TableCellStyle, FONT_SANS_SERIF) + +#------------------------------------------------------------------------ +# +# TableOfContents +# +#------------------------------------------------------------------------ +class TableOfContents(Report): + """ This report class generates a table of contents for a book. """ + def __init__(self, database, options, user): + """ + Create TableOfContents object that produces the report. + + The arguments are: + + database - the GRAMPS database instance + options - instance of the Options class for this report + user - a gen.user.User() instance + """ + Report.__init__(self, database, options, user) + self._user = user + + menu = options.menu + + def write_report(self): + """ Generate the contents of the report """ + self.doc.insert_toc() + +#------------------------------------------------------------------------ +# +# TableOfContentsOptions +# +#------------------------------------------------------------------------ +class TableOfContentsOptions(MenuReportOptions): + + """ + Defines options and provides handling interface. + """ + + def __init__(self, name, dbase): + self.__db = dbase + MenuReportOptions.__init__(self, name, dbase) + + def add_menu_options(self, menu): + """ Add the options for this report """ + pass + + def make_default_style(self, default_style): + """Make the default output style for the TableOfContents report.""" + font = FontStyle() + font.set(face=FONT_SANS_SERIF, size=14) + para = ParagraphStyle() + para.set_font(font) + para.set_bottom_margin(0.25) + para.set_description(_('The style used for the TOC title.')) + default_style.add_paragraph_style("TOC-Title", para) + + table = TableStyle() + table.set_width(100) + table.set_columns(2) + table.set_column_width(0, 80) + table.set_column_width(1, 20) + default_style.add_table_style("TOC-Table", table) + + cell = TableCellStyle() + default_style.add_cell_style("TOC-Cell", cell) + + font = FontStyle() + font.set(face=FONT_SANS_SERIF, size=10) + para = ParagraphStyle() + para.set_font(font) + para.set_description(_('The style used for the TOC page numbers.')) + default_style.add_paragraph_style("TOC-Number", para) + + para = ParagraphStyle() + para.set_font(font) + para.set_description(_('The style used for the TOC first level heading.')) + default_style.add_paragraph_style("TOC-Heading1", para) + + para = ParagraphStyle() + para.set_font(font) + para.set_first_indent(0.5) + para.set_description(_('The style used for the TOC second level heading.')) + default_style.add_paragraph_style("TOC-Heading2", para) + + para = ParagraphStyle() + para.set_font(font) + para.set_first_indent(1) + para.set_description(_('The style used for the TOC third level heading.')) + default_style.add_paragraph_style("TOC-Heading3", para) diff --git a/src/plugins/textreport/textplugins.gpr.py b/src/plugins/textreport/textplugins.gpr.py index 79c4a1622..b6efdaca7 100644 --- a/src/plugins/textreport/textplugins.gpr.py +++ b/src/plugins/textreport/textplugins.gpr.py @@ -353,3 +353,48 @@ plg.reportclass = 'SummaryReport' plg.optionclass = 'SummaryOptions' plg.report_modes = [REPORT_MODE_GUI, REPORT_MODE_BKI, REPORT_MODE_CLI] plg.require_active = False + +#------------------------------------------------------------------------ +# +# Table Of Contents +# +#------------------------------------------------------------------------ + +plg = newplugin() +plg.id = 'table_of_contents' +plg.name = _("Table Of Contents") +plg.description = _("Produces a table of contents for book reports.") +plg.version = '1.0' +plg.gramps_target_version = '3.4' +plg.status = STABLE +plg.fname = 'TableOfContents.py' +plg.ptype = REPORT +plg.authors = ["Brian G. Matherly"] +plg.authors_email = ["brian@gramps-project.org"] +plg.category = CATEGORY_TEXT +plg.reportclass = 'TableOfContents' +plg.optionclass = 'TableOfContentsOptions' +plg.report_modes = [REPORT_MODE_BKI] + +#------------------------------------------------------------------------ +# +# Alphabetical Index +# +#------------------------------------------------------------------------ + +plg = newplugin() +plg.id = 'alphabetical_index' +plg.name = _("Alphabetical Index") +plg.description = _("Produces aa alphabetical index for book reports.") +plg.version = '1.0' +plg.gramps_target_version = '3.4' +plg.status = STABLE +plg.fname = 'AlphabeticalIndex.py' +plg.ptype = REPORT +plg.authors = ["Brian G. Matherly"] +plg.authors_email = ["brian@gramps-project.org"] +plg.category = CATEGORY_TEXT +plg.reportclass = 'AlphabeticalIndex' +plg.optionclass = 'AlphabeticalIndexOptions' +plg.report_modes = [REPORT_MODE_BKI] +