From ab81986cf59e6db3e638ec411962d944727d32ee Mon Sep 17 00:00:00 2001 From: Cyril Danilevski Date: Tue, 20 Jul 2021 12:15:07 +0200 Subject: [PATCH] Rebase fixes from Gourmet * Fix pdf export when a locale is set https://github.com/kirienko/gourmet/pull/363 * Only scale available reportlab attributes https://github.com/kirienko/gourmet/pull/365 * Use latest Python3.8 for building AppImage https://github.com/kirienko/gourmet/pull/367 Sorry for the squashed changes. --- .github/workflows/appimage.yml | 6 +- .github/workflows/tests.yml | 2 +- .../import_export/pdf_plugin/pdf_exporter.py | 274 +++++++++++------- .../nutritional_information/nutrition.py | 2 +- src/gourmand/reccard.py | 18 +- tests/test_pdf_exporter.py | 32 ++ 6 files changed, 213 insertions(+), 121 deletions(-) create mode 100644 tests/test_pdf_exporter.py diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index a106edbf..0a49133a 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -17,9 +17,9 @@ jobs: run: | sudo apt-get update -q sudo apt-get -y install python3-testresources intltool imagemagick libgirepository1.0-dev - wget https://github.com/niess/python-appimage/releases/download/python3.8/python3.8.10-cp38-cp38-manylinux1_x86_64.AppImage - chmod +x ./python3.8.10-cp38-cp38-manylinux1_x86_64.AppImage - ./python3.8.10-cp38-cp38-manylinux1_x86_64.AppImage --appimage-extract + wget -c https://github.com/$(wget -q https://github.com/niess/python-appimage/releases/tag/python3.8 -O - | grep "python3.8.*-cp38-cp38-manylinux1_x86_64.AppImage" | head -n 1 | cut -d '"' -f 2) + chmod +x ./python3*.AppImage + ./python3*.AppImage --appimage-extract wget -c https://github.com/$(wget -q https://github.com/probonopd/go-appimage/releases -O - | grep "appimagetool-.*-x86_64.AppImage" | head -n 1 | cut -d '"' -f 2) chmod +x appimagetool-*.AppImage diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2f4d0f45..be257a1e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,7 +36,7 @@ jobs: run: sudo pip3 install -r development.in - name: Test with pytest - run: xvfb-run -a pytest -vv tests/test_* + run: LANGUAGE=de_DE.utf-8 xvfb-run -a pytest -vv tests/test_* - name: Check imports ordering run: > diff --git a/src/gourmand/plugins/import_export/pdf_plugin/pdf_exporter.py b/src/gourmand/plugins/import_export/pdf_plugin/pdf_exporter.py index 6c121a65..53a05e0d 100644 --- a/src/gourmand/plugins/import_export/pdf_plugin/pdf_exporter.py +++ b/src/gourmand/plugins/import_export/pdf_plugin/pdf_exporter.py @@ -2,10 +2,11 @@ import xml.sax.saxutils from gettext import ngettext from io import BytesIO +from typing import Dict, List, Optional, Tuple, Union -from reportlab.lib import colors, pagesizes, styles -import reportlab.platypus as platypus from gi.repository import Gtk +from reportlab import platypus +from reportlab.lib import colors, pagesizes, styles from reportlab.lib.units import inch, mm import gourmand.exporters.exporter as exporter @@ -216,7 +217,7 @@ def draw(self): class PdfWriter: - def __init__ (self, allrecs=[]): + def __init__ (self, allrecs=None): pass def setup_document (self, file, mode=('column',1), size='default', pagesize='letter', @@ -225,9 +226,9 @@ def setup_document (self, file, mode=('column',1), size='default', pagesize='let bottom_margin=inch, base_font_size=10 ): - frames = self.setup_frames(mode,size,pagesize,pagemode, - left_margin,right_margin,top_margin, - bottom_margin,base_font_size) + frames = self.setup_frames(mode, size,pagesize, pagemode, + left_margin, right_margin, top_margin, + bottom_margin, base_font_size) pt = platypus.PageTemplate(frames=frames) self.doc = platypus.BaseDocTemplate(file,pagesize=self.pagesize, pageTemplates=[pt],) @@ -239,34 +240,45 @@ def setup_document (self, file, mode=('column',1), size='default', pagesize='let self.scale_stylesheet(perc_scale) self.txt = [] - def setup_frames (self,mode=('column',1), size='default', pagesize='letter', - pagemode='portrait',left_margin=inch,right_margin=inch, - top_margin=inch, - bottom_margin=inch, - base_font_size=10): - if not isinstance(mode, tuple): - raise Exception("What is this mode! %s" % str(mode)) + def setup_frames(self, + mode: Optional[Tuple[str, int]] = None, + size: str = 'default', + pagesize: Union[str, Tuple] = 'LETTER', + pagemode: str = 'portrait', + left_margin: float = inch, + right_margin: float = inch, + top_margin: float = inch, + bottom_margin: float = inch, + base_font_size: Optional[int] = 10) -> List['Frame']: if isinstance(pagesize, str): - self.pagesize = getattr(pagesizes,pagemode)(getattr(pagesizes,pagesize)) + pagesize = getattr(pagesizes, pagesize) + + self.pagesize = getattr(pagesizes, pagemode)(pagesize) + self.margins = (left_margin, right_margin, top_margin, bottom_margin) + + if mode is not None: + mode, count = mode else: - self.pagesize = getattr(pagesizes,pagemode)(pagesize) - self.margins = (left_margin,right_margin,top_margin,bottom_margin) - if mode[0] == 'column': - frames = self.setup_column_frames(mode[1]) - elif mode[0] == 'index_cards': - frames = self.setup_multiple_index_cards(mode[1]) + mode, count = 'column', 1 + + if mode == 'column': + frames = self.setup_column_frames(count) + elif mode == 'index_cards': + frames = self.setup_multiple_index_cards(count) else: - raise Exception("WTF - mode = %s" % str(mode)) + raise ValueError(f'Expected mode to be "column" or "index_cards" ' + f'not {mode}') return frames - def scale_stylesheet (self, perc): - for name,sty in list(self.styleSheet.byName.items()): + def scale_stylesheet(self, perc: float): + for name, sty in list(self.styleSheet.byName.items()): for attr in ['firstLineIndent', 'fontSize', 'leftIndent', 'rightIndent', 'leading']: - setattr(sty,attr,int(perc*getattr(sty,attr))) + if hasattr(sty, attr): + setattr(sty, attr, int(perc * getattr(sty, attr))) def setup_column_frames (self, n): COLUMN_SEPARATOR = 0.5 * inch @@ -399,10 +411,13 @@ class PdfExporter (exporter.exporter_mult, PdfWriter): def __init__ (self, rd, r, out, doc=None, styleSheet=None, - txt=[], - pdf_args=DEFAULT_PDF_ARGS, - all_recipes=[], # For learning about references... + txt=None, + pdf_args=None, + all_recipes=None, # For learning about references... **kwargs): + txt = txt if txt is not None else [] + pdf_args = pdf_args if pdf_args is not None else DEFAULT_PDF_ARGS + all_recipes = all_recipes if all_recipes is not None else [] self.all_recipes = all_recipes PdfWriter.__init__(self) if isinstance(out, str): @@ -681,11 +696,11 @@ def write_footer (self): self.close() self.output_file.close() -class Sizer (PdfWriter): - def get_size (self, *args, **kwargs): +class Sizer(PdfWriter): + def get_size(self, *args, **kwargs): frames = self.setup_frames(*args,**kwargs) - return self.pagesize,frames + return self.pagesize, frames def get_pagesize_and_frames_for_widget (self, *args, **kwargs): ps,ff = self.get_size(*args,**kwargs) @@ -708,14 +723,14 @@ def set_page (self, *args, **kwargs): self.set_page_area(size[0],size[1],areas) PDF_PREF_DEFAULT={ - 'page_size':_('Letter'), - 'orientation':_('Portrait'), - 'font_size':10, - 'page_layout':_('Plain'), - 'left_margin':1.0*inch, - 'right_margin':1.0*inch, - 'top_margin':1.0*inch, - 'bottom_margin':1.0*inch, + 'page_size': 'letter', + 'orientation': 'portrait', + 'font_size': 10, + 'page_layout': 'plain', + 'left_margin': 1.0 * inch, + 'right_margin': 1.0 * inch, + 'top_margin': 1.0 * inch, + 'bottom_margin': 1.0 * inch, } class CustomUnitOption (optionTable.CustomOption): @@ -816,71 +831,118 @@ def change_cb (other_cuo): self.__quiet__ = False cuo.connect('changed',change_cb) + class PdfPrefGetter: page_sizes = { - _('11x17"'):'elevenSeventeen', - _('Index Card (3.5x5")'):(3.5*inch,5*inch), - _('Index Card (4x6")'):(4*inch,6*inch), - _('Index Card (5x8")'):(5*inch,8*inch), - _('Index Card (A7)'):(74*mm,105*mm), - _('Letter'):'letter', - _('Legal'):'legal', - 'A0':'A0','A1':'A1','A2':'A2','A3':'A3','A4':'A4','A5':'A5','A6':'A6', - 'B0':'B0','B1':'B1','B2':'B2','B3':'B3','B4':'B4','B5':'B5','B6':'B6', + _('11x17"'): 'elevenSeventeen', + _('Index Card (3.5x5")'): (3.5 * inch, 5 * inch), + _('Index Card (4x6")'): (4 * inch, 6 * inch), + _('Index Card (5x8")'): (5 * inch, 8 * inch), + _('Index Card (A7)'): (74 * mm, 105 * mm), + _('Letter'): 'letter', + _('Legal'): 'legal', + 'A0': 'A0', + 'A1': 'A1', + 'A2': 'A2', + 'A3': 'A3', + 'A4': 'A4', + 'A5': 'A5', + 'A6': 'A6', + 'B0': 'B0', + 'B1': 'B1', + 'B2': 'B2', + 'B3': 'B3', + 'B4': 'B4', + 'B5': 'B5', + 'B6': 'B6', } - INDEX_CARDS = [(3.5*inch,5*inch),(4*inch,6*inch),(5*inch,8*inch),(74*mm,105*mm)] + INDEX_CARDS = [(3.5 * inch, 5 * inch), + (4 * inch, 6 * inch), + (5 * inch, 8 * inch), + (74 * mm, 105 * mm)] INDEX_CARD_LAYOUTS = [_('Index Cards (3.5x5)'), _('Index Cards (4x6)'), - _('Index Cards (A7)'), - ] - layouts = { - _('Plain'):('column',1), - _('Index Cards (3.5x5)'):('index_cards',(5*inch,3.5*inch)), - _('Index Cards (4x6)'):('index_cards',(6*inch,4*inch)), - _('Index Cards (A7)'):('index_cards',(105*mm,74*mm)), - } - - page_modes = { - _('Portrait'):'portrait', - _('Landscape'):'landscape', - } - - OPT_PS,OPT_PO,OPT_FS,OPT_PL,OPT_LM,OPT_RM,OPT_TM,OPT_BM = list(range(8)) + _('Index Cards (A7)')] + layouts = {_('Plain'): ('column', 1), + _('2 Columns'): ('column', 2), + _('3 Columns'): ('column', 3), + _('4 Columns'): ('column', 4), + _('5 Columns'): ('column', 5), + _('Index Cards (3.5x5)'): ('index_cards', (5 * inch, 3.5 * inch)), + _('Index Cards (4x6)'): ('index_cards', (6 * inch, 4 * inch)), + _('Index Cards (A7)'): ('index_cards', (105 * mm, 74 * mm))} + for n in range(2, 5): + layouts[ngettext('%s Column', '%s Columns', n) % n] = ('column', n) + + # These two dictionaries are here to be able to store the preferences in the persistent toml file. + # This allows us to have a locale-agnostic GUI. + _layouts_to_settings = {_('Plain'): 'plain', + _('2 Columns'): '2_columns', + _('3 Columns'): '3_columns', + _('4 Columns'): '4_columns', + _('5 Columns'): '5_columns', + _('Index Cards (3.5x5)'): 'index_cards_35_5', + _('Index Cards (4x6)'): 'index_cards_4_6', + _('Index Cards (A7)'): 'a7'} + + _settings_to_layout = {v: k for k, v in _layouts_to_settings.items()} + + page_modes = {_('Portrait'): 'portrait', + _('Landscape'): 'landscape'} + + OPT_PS, OPT_PO, OPT_FS, OPT_PL, OPT_LM, OPT_RM, OPT_TM, OPT_BM = list(range(8)) def __init__(self): - self.prefs = Prefs.instance() - defaults = self.prefs.get('PDF_EXP', PDF_PREF_DEFAULT) self.size_strings = list(self.page_sizes.keys()) self.size_strings.sort() - for n in range(2,5): - self.layouts[ngettext('%s Column','%s Columns',n)%n]=('column',n) - self.make_reverse_dicts() + self.page_sizes_r = {v: k for k, v in self.page_sizes.items()} + self.layouts_r = {v: k for k, v in self.layouts.items()} + self.page_modes_r = {v: k for k, v in self.page_modes.items()} + self.layout_strings = list(self.layouts.keys()) self.layout_strings.sort() - margin_widgets = [ - CustomUnitOption(defaults.get(pref,PDF_PREF_DEFAULT[pref])) - for pref in ['left_margin','right_margin','top_margin','bottom_margin'] - ] + + defaults = Prefs.instance().get('PDF_EXP', PDF_PREF_DEFAULT) + + margin_widgets = [CustomUnitOption(defaults.get(pref, PDF_PREF_DEFAULT[pref])) + for pref in ['left_margin', 'right_margin', 'top_margin', 'bottom_margin'] + ] # Make unit changes to one widget affect all the others! for m in margin_widgets: for mm in margin_widgets: if mm is not m: m.sync_to_other_cuo(mm) - self.opts = [ - [_('Paper _Size')+':',(defaults.get('page_size',PDF_PREF_DEFAULT['page_size']), - self.size_strings)], - [_('_Orientation')+':',(defaults.get('orientation',PDF_PREF_DEFAULT['orientation']), - list(self.page_modes.keys()))], - [_('_Font Size')+':',int(defaults.get('font_size',PDF_PREF_DEFAULT['font_size']))], - [_('Page _Layout'),(defaults.get('page_layout',PDF_PREF_DEFAULT['page_layout']), - self.layout_strings)], - [_('Left Margin')+':',margin_widgets[0]], - [_('Right Margin')+':',margin_widgets[1]], - [_('Top Margin')+':',margin_widgets[2]], - [_('Bottom Margin')+':',margin_widgets[3]], - ] + default_page_size = defaults.get('page_size', + PDF_PREF_DEFAULT['page_size']) + default_page_size = self.page_sizes_r[default_page_size] + page_size = [_('Paper _Size')+':', + (default_page_size, self.size_strings)] + + default_orientation = defaults.get('orientation', + PDF_PREF_DEFAULT['orientation']) + default_orientation = self.page_modes_r[default_orientation] + orientation = [_('_Orientation')+':', + (default_orientation, list(self.page_modes.keys()))] + + default_layout = defaults.get('page_layout', + PDF_PREF_DEFAULT['page_layout']) + default_layout = self._settings_to_layout[default_layout] + layout = [_('Page _Layout'), + (default_layout, self.layout_strings)] + + self.opts = ( + page_size, + orientation, + [_('_Font Size')+':', + int(defaults.get('font_size', PDF_PREF_DEFAULT['font_size']))], + layout, + [_('Left Margin')+':', margin_widgets[0]], + [_('Right Margin')+':', margin_widgets[1]], + [_('Top Margin')+':', margin_widgets[2]], + [_('Bottom Margin')+':', margin_widgets[3]], + ) self.page_drawer = PdfPageDrawer(yalign=0.0) self.in_ccb = False @@ -890,14 +952,6 @@ def __init__(self): self.page_drawer.set_size_request(200,100) self.page_drawer.show() - def make_reverse_dicts (self): - self.page_sizes_r = {}; self.layouts_r = {}; self.page_modes_r = {} - for dict,dict_r in [ - (self.page_sizes,self.page_sizes_r), - (self.layouts,self.layouts_r), - (self.page_modes,self.page_modes_r)]: - for k,v in list(dict.items()): dict_r[v]=k - def setup_widgets (self): self.pd = de.PreferencesDialog(self.opts,option_label=None,value_label=None, label=_('PDF Options'), @@ -910,23 +964,33 @@ def run (self): self.pd.run() return self.get_args_from_opts(self.opts) - def get_args_from_opts (self, opts): + def get_args_from_opts(self, opts: Tuple[List]) -> Dict[str, Union[str, int, float]]: + """Get information from the dialog. + + As the information is retrieved from the dialog and converted into + system values, they are also saved to the persistent preferences. + """ args = {} - prefs = self.prefs.get('PDF_EXP', {}) - args['pagesize'] = self.page_sizes[opts[self.OPT_PS][1]] # PAGE SIZE - prefs['page_size'] = self.page_sizes_r[args['pagesize']] - args['pagemode'] = self.page_modes[opts[self.OPT_PO][1]] # PAGE MODE - prefs['orientation'] = self.page_modes_r[args['pagemode']] - prefs['font_size'] = args['base_font_size'] = opts[self.OPT_FS][1] # FONT SIZE - args['mode'] = self.layouts[opts[self.OPT_PL][1]] # LAYOUT/MODE - prefs['page_layout'] = self.layouts_r[args['mode']] + prefs = Prefs.instance().get('PDF_EXP', {}) + args['pagesize'] = self.page_sizes[opts[self.OPT_PS][1]] + prefs['page_size'] = args['pagesize'] + + args['pagemode'] = self.page_modes[opts[self.OPT_PO][1]] + prefs['orientation'] = args['pagemode'] + + prefs['font_size'] = args['base_font_size'] = opts[self.OPT_FS][1] + + args['mode'] = self.layouts[opts[self.OPT_PL][1]] + layout = self.layouts_r[args['mode']] + layout = self._layouts_to_settings[layout] + prefs['page_layout'] = layout prefs['left_margin'] = args['left_margin'] = opts[self.OPT_LM][1] prefs['right_margin'] = args['right_margin'] = opts[self.OPT_RM][1] prefs['top_margin'] = args['top_margin'] = opts[self.OPT_TM][1] prefs['bottom_margin'] = args['bottom_margin'] = opts[self.OPT_BM][1] return args - def change_cb (self, option_table, *args,**kwargs): + def change_cb(self, option_table, *args,**kwargs): if self.in_ccb: return self.in_ccb = True option_table.apply() @@ -956,10 +1020,6 @@ def change_cb (self, option_table, *args,**kwargs): if n in [i[0] for i in self.index_card_layouts_to_put_back]: default_pos = self.layout_strings.index(_('Plain')) cb.set_active(default_pos) - self.index_card_layouts_to_put_back.reverse() - for pos,txt in self.index_card_layouts_to_put_back: - cb.remove_text(pos) - self.index_card_layouts_to_put_back.reverse() elif pagesize not in self.INDEX_CARDS and last_pagesize in self.INDEX_CARDS: changed = True option_table.set_option(self.OPT_PO,_('Portrait')) diff --git a/src/gourmand/plugins/nutritional_information/nutrition.py b/src/gourmand/plugins/nutritional_information/nutrition.py index fbd3c874..e1e131b3 100644 --- a/src/gourmand/plugins/nutritional_information/nutrition.py +++ b/src/gourmand/plugins/nutritional_information/nutrition.py @@ -71,7 +71,7 @@ def set_conversion (self, key, unit, factor): else: self.db.do_add(self.db.nutritionconversions_table,{'ingkey':key,'unit':unit,'factor':factor}) - def get_matches (self, key, max=50): + def get_matches(self, key, max=50): """Handed a string, get a list of likely USDA database matches. We return a list of lists: diff --git a/src/gourmand/reccard.py b/src/gourmand/reccard.py index fecc98d5..155c39f2 100644 --- a/src/gourmand/reccard.py +++ b/src/gourmand/reccard.py @@ -33,20 +33,20 @@ from gourmand.plugins.clipboard_exporter import ClipboardExporter from gourmand.recindex import RecIndex -from .image_utils import load_pixbuf_from_resource - -def find_entry(w) -> Optional[Gtk.Entry]: - if isinstance(w, Gtk.Entry): - return w +def find_entry(widget) -> Optional[Gtk.Entry]: + """Recurse through all the children widgets to find the first Gtk.Entry.""" + if isinstance(widget, Gtk.Entry): + return widget else: - if not hasattr(w,'get_children'): + if not hasattr(widget, 'get_children'): return - for child in w.get_children(): + for child in widget.get_children(): e = find_entry(child) if e is not None: return e + class RecRef: def __init__ (self, refid, title): self.refid = refid @@ -369,7 +369,7 @@ def reflow_on_allocate_cb (self, sw, allocation): # Main GUI setup def setup_main_window (self): self.window = Gtk.Window() - self.window.set_icon(load_pixbuf_from_resource('reccard.png')) + self.window.set_icon(iu.load_pixbuf_from_resource('reccard.png')) self.window.connect('delete-event',self.hide) self.conf.append(WidgetSaver.WindowSaver(self.window, self.prefs.get('reccard_window_%s'%self.current_rec.id, @@ -960,7 +960,7 @@ def show_module(self, module_name: str) -> None: def setup_main_interface (self): self.window = Gtk.Window() - self.window.set_icon(load_pixbuf_from_resource('reccard_edit.png')) + self.window.set_icon(iu.load_pixbuf_from_resource('reccard_edit.png')) title = ((self.current_rec and self.current_rec.title) or _('New Recipe')) + ' (%s)'%_('Edit') self.window.set_title(title) self.window.connect('delete-event', diff --git a/tests/test_pdf_exporter.py b/tests/test_pdf_exporter.py new file mode 100644 index 00000000..58cdba10 --- /dev/null +++ b/tests/test_pdf_exporter.py @@ -0,0 +1,32 @@ +"""This test may leave marks in the user preferences file.""" +import os +os.environ['LANGUAGE'] = 'de_DE.utf-8' # must happend before Gourmand import + +from gourmand.gglobals import gourmetdir +from gourmand.plugins.import_export.pdf_plugin.pdf_exporter import PdfPrefGetter # noqa: import not at top of file + +def test_get_args_from_opts(tmp_path): + gourmetdir = tmp_path + pref_getter = PdfPrefGetter() + + options = (['Papiergröße:', 'Letter'], + ['_Ausrichtung:', 'Hochformat'], + ['_Schriftgröße:', 10], + ['Seiten-Layout', 'Eben'], + ['Linker Rand:', 70.86614173228347], + ['Rechter Rand:', 70.86614173228347], + ['Oberer Rand:', 70.86614173228347], + ['Unterer Rand:', 70.86614173228347]) + + expected = {'pagesize': 'letter', + 'pagemode': 'portrait', + 'base_font_size': 10, + 'mode': ('column', 1), + 'left_margin': 70.86614173228347, + 'right_margin': 70.86614173228347, + 'top_margin': 70.86614173228347, + 'bottom_margin': 70.86614173228347} + + ret = pref_getter.get_args_from_opts(options) + + assert ret == expected