def test_override_pdf_kwargs(self): """Test settings override of margin (should override the four default margins)""" defaults = clean_pdf_kwargs() self.assertEqual(convert_to_inches('2cm'), defaults['marginLeft']) self.assertEqual(convert_to_inches('2cm'), defaults['marginRight']) self.assertEqual(convert_to_inches('2cm'), defaults['marginTop']) self.assertEqual(convert_to_inches('2cm'), defaults['marginBottom'])
def test_unit_conversions_uppercase(self): # uppercase is apparently valid CSS. And chromepdf currently takes it. So, preserve that behavior. self.assertEqual(1, convert_to_inches('2.54CM')) self.assertEqual(1, convert_to_inches('2.54Cm')) # uppercase has never worked for this function and I see no reason to change that. # must user lowercase. maybe this is inconsistent? with self.assertRaises(ValueError): convert_to_unit(1, 'IN') with self.assertRaises(ValueError): convert_to_unit(1, 'In')
def test_override_pdf_kwargs_three_way_margins(self): """ pdf_kwargs should override everything settings' PDF_KWARGS should override only the true defaults. True defaults should be final fallback if no overrides were provided anywhere """ pdf_kwargs = {'marginLeft': '3cm', 'marginBottom': '4cm'} cleaned = clean_pdf_kwargs(**pdf_kwargs) self.assertEqual(convert_to_inches('3cm'), cleaned['marginRight']) # override in settings self.assertEqual(convert_to_inches('3cm'), cleaned['marginLeft']) # override in pdf_kwargs (takes priority over settings) self.assertEqual(convert_to_inches('4cm'), cleaned['marginBottom']) # override in pdf_kwargs self.assertEqual(convert_to_inches('1cm'), cleaned['marginTop']) # true default (no overrides)
def test_convert_return_types(self): """Return types must always be floats or ints (since they are JSON-serializable). Otherwise, Selenium will fail when creating JSON.""" JSON_NUMERIC_TYPES = (int, float) self.assertIsInstance(convert_to_inches(1), JSON_NUMERIC_TYPES) self.assertIsInstance(convert_to_inches('1in'), JSON_NUMERIC_TYPES) self.assertIsInstance(convert_to_inches('.75in'), JSON_NUMERIC_TYPES) # Important! Decimal is NOT json-serializable. It must be converted to int/float self.assertIsInstance(convert_to_inches(Decimal('1.75')), JSON_NUMERIC_TYPES) self.assertIsInstance(convert_to_unit(1, 'in'), JSON_NUMERIC_TYPES) self.assertIsInstance(convert_to_unit('1in', 'in'), JSON_NUMERIC_TYPES) self.assertIsInstance(convert_to_unit('.75in', 'in'), JSON_NUMERIC_TYPES) # Important! Decimal is NOT json-serializable. It must be converted to int/float self.assertIsInstance(convert_to_unit(Decimal('1.75'), 'in'), JSON_NUMERIC_TYPES) # string lengths MUST provide a unit type. EG '1in' not '1' with self.assertRaises(ValueError): convert_to_inches('1') with self.assertRaises(ValueError): convert_to_unit('1', 'in') # test invalid unit types with self.assertRaises(ValueError): convert_to_unit('1', 'zz') # CSS does not allow whitespace in length values, so we do not, either with self.assertRaises(ValueError): convert_to_inches('1 in') with self.assertRaises(ValueError): convert_to_unit('1 in', 'in')
def test_clean_pdf_kwargs_priority(self): """ Ensure priority of overrides: keyword arguments > Django setting > hardcoded DEFAULT_PDF_KWARGS """ kwargs = clean_pdf_kwargs(marginTop='9cm') self.assertEqual(kwargs['marginTop'], convert_to_inches( '9cm')) # overridden by keyword arg (overrides Django setting) self.assertEqual( kwargs['marginBottom'], convert_to_inches('7cm')) # overridden by Django setting self.assertEqual(kwargs['marginLeft'], convert_to_inches('1cm')) # DEFAULT_PDF_KWARGS value
def test_clean_pdf_kwargs_priority2(self): """ Ensure convenience Django settings still get overridden by subsetted keyword arguments. """ kwargs = clean_pdf_kwargs(marginTop='3cm') self.assertEqual( kwargs['marginTop'], convert_to_inches('3cm')) # overridden by keyword argument self.assertEqual( kwargs['marginBottom'], convert_to_inches('2cm')) # overridden by Django setting self.assertEqual( kwargs['marginLeft'], convert_to_inches('2cm')) # overridden by Django setting self.assertEqual( kwargs['marginRight'], convert_to_inches('2cm')) # overridden by Django setting
def test_unit_conversions(self): self.assertEqual(1, convert_to_inches('25.4mm')) self.assertEqual(1, convert_to_inches('2.54cm')) self.assertEqual(1, convert_to_inches('96px')) self.assertEqual(1, convert_to_inches('1in')) self.assertEqual(0.0394, round(convert_to_inches('1mm'), 4)) self.assertEqual(0.3937, round(convert_to_inches('1cm'), 4)) self.assertEqual(0.0104, round(convert_to_inches('1px'), 4)) self.assertEqual(1, round(convert_to_inches('1in'), 4)) self.assertEqual(25.4, convert_to_unit(1, 'mm')) self.assertEqual(2.54, convert_to_unit(1, 'cm')) self.assertEqual(96, convert_to_unit(1, 'px')) self.assertEqual(1, convert_to_unit(1, 'in')) self.assertEqual(25.4, convert_to_unit('96px', 'mm')) self.assertEqual(2.54, convert_to_unit('96px', 'cm')) self.assertEqual(96, convert_to_unit('96px', 'px')) self.assertEqual(1, convert_to_unit('96px', 'in')) # Allow decimal, int and float self.assertEqual(1.75, convert_to_inches(Decimal('1.75'))) self.assertEqual(1.75, convert_to_inches(1.75)) self.assertEqual(2, convert_to_inches(2)) # <1 conversion self.assertEqual(0.75, convert_to_inches('0.75in')) # leading zero self.assertEqual(0.75, convert_to_inches('.75in')) # no leading zero self.assertEqual(0.75, convert_to_inches(Decimal('0.75'))) self.assertEqual(0.75, convert_to_inches(Decimal('.75'))) self.assertEqual(0.75, convert_to_inches(.75)) # zero conversion (ensure no division by zero issues) self.assertEqual(0, convert_to_inches('0mm')) self.assertEqual(0, convert_to_inches('0in')) self.assertEqual(0, convert_to_inches(Decimal('0.0'))) self.assertEqual(0, convert_to_inches(Decimal('0'))) self.assertEqual(0, convert_to_inches(0)) self.assertEqual(0, convert_to_unit('0mm', 'cm')) self.assertEqual(0, convert_to_unit('0in', 'cm')) self.assertEqual(0, convert_to_unit(0, 'cm'))
def test_clean_pdf_kwargs_settings(self): """Make sure Django settings overrides are accounted for.""" kwargs = clean_pdf_kwargs() self.assertEqual(kwargs['marginTop'], convert_to_inches('8cm'))
def test_clean_pdf_kwargs_margins(self): DEFAULTS = get_default_pdf_kwargs() margin_kwargs = ('marginTop', 'marginBottom', 'marginLeft', 'marginRight') # test setting each margin individually for k in margin_kwargs: with self.subTest(k=k): kwargs = clean_pdf_kwargs(**{k: 2}) self.assertEqual(kwargs[k], 2) # overrides the one for k2 in margin_kwargs: if k != k2: self.assertEqual(kwargs[k2], DEFAULTS[k2]) # leaves others alone # set all margins to different values, in various formats kwargs = clean_pdf_kwargs(marginTop=3, marginBottom='2.5cm', marginLeft='10mm', marginRight='30px') self.assertEqual(kwargs['marginTop'], 3) self.assertEqual(kwargs['marginBottom'], convert_to_inches('2.5cm')) self.assertEqual(kwargs['marginLeft'], convert_to_inches('10mm')) self.assertEqual(kwargs['marginRight'], convert_to_inches('30px')) # set all four using the 'margin' kwarg: kwargs = clean_pdf_kwargs(margin='11mm') self.assertTrue('margin' not in kwargs) self.assertEqual(kwargs['marginTop'], convert_to_inches('11mm')) self.assertEqual(kwargs['marginBottom'], convert_to_inches('11mm')) self.assertEqual(kwargs['marginLeft'], convert_to_inches('11mm')) self.assertEqual(kwargs['marginRight'], convert_to_inches('11mm')) # set all four using margin, but override some as well kwargs = clean_pdf_kwargs(margin='11mm', marginTop='5mm', marginBottom='6mm') self.assertTrue('margin' not in kwargs) self.assertEqual(kwargs['marginTop'], convert_to_inches('5mm')) self.assertEqual(kwargs['marginBottom'], convert_to_inches('6mm')) self.assertEqual(kwargs['marginLeft'], convert_to_inches('11mm')) self.assertEqual(kwargs['marginRight'], convert_to_inches('11mm')) # cannot set margins to None for m in margin_kwargs: with self.subTest(m=m): with self.assertRaises(TypeError): _kwargs = clean_pdf_kwargs(**{m: None})
def test_clean_pdf_kwargs_pagesizes(self): DEFAULTS = get_default_pdf_kwargs() # set the height, width=default kwargs = clean_pdf_kwargs(paperWidth=12) self.assertEqual(kwargs['paperWidth'], 12) self.assertEqual(kwargs['paperHeight'], DEFAULTS['paperHeight']) # set the width, height=default kwargs = clean_pdf_kwargs(paperHeight=12) self.assertEqual(kwargs['paperWidth'], DEFAULTS['paperWidth']) self.assertEqual(kwargs['paperHeight'], 12) # set width and height to inches kwargs = clean_pdf_kwargs(paperWidth=10.5, paperHeight=12.5) self.assertEqual(kwargs['paperWidth'], 10.5) self.assertEqual(kwargs['paperHeight'], 12.5) # set width and height to non-inches value (lowercase) kwargs = clean_pdf_kwargs(paperWidth='8cm', paperHeight='12cm') self.assertEqual(kwargs['paperWidth'], convert_to_inches('8cm')) self.assertEqual(kwargs['paperHeight'], convert_to_inches('12cm')) # set width and height to non-inches value (uppercase) kwargs = clean_pdf_kwargs(paperWidth='8CM', paperHeight='12CM') self.assertEqual(kwargs['paperWidth'], convert_to_inches('8cm')) self.assertEqual(kwargs['paperHeight'], convert_to_inches('12cm')) # set width and height to float inch string values kwargs = clean_pdf_kwargs(paperWidth='8.0in', paperHeight='12.0in') self.assertEqual(kwargs['paperWidth'], convert_to_inches(8)) self.assertEqual(kwargs['paperHeight'], convert_to_inches(12)) # raise ValueErrors for bad unit types with self.assertRaises(ValueError): _kwargs = clean_pdf_kwargs(paperWidth='8zz') with self.assertRaises(ValueError): _kwargs = clean_pdf_kwargs(paperHeight='8zz') with self.assertRaises(ValueError): _kwargs = clean_pdf_kwargs(paperHeight='inin') with self.assertRaises(ValueError): _kwargs = clean_pdf_kwargs(paperHeight='888') with self.assertRaises(ValueError): _kwargs = clean_pdf_kwargs(paperHeight='') # disallow whitespace? with self.assertRaises(ValueError): _kwargs = clean_pdf_kwargs( paperHeight='8 in') # isn't valid CSS so don't allow it here. with self.assertRaises(ValueError): _kwargs = clean_pdf_kwargs(paperHeight=' 8in') with self.assertRaises(ValueError): _kwargs = clean_pdf_kwargs(paperHeight='8in ') # type error (these cannot be None) with self.assertRaises(TypeError): _kwargs = clean_pdf_kwargs(paperHeight=None) with self.assertRaises(TypeError): _kwargs = clean_pdf_kwargs(paperWidth=None) # raise ValueError if paperFormat is not recognized with self.assertRaises(ValueError): kwargs = clean_pdf_kwargs(paperFormat='A9') # set a paperFormat (lowercase) kwargs = clean_pdf_kwargs(paperFormat='a4') self.assertTrue( 'paperFormat' not in kwargs) # should have affected width and height, then be ditched. self.assertEqual(kwargs['paperWidth'], PAPER_FORMATS['a4']['width']) self.assertEqual(kwargs['paperHeight'], PAPER_FORMATS['a4']['height']) # set a paperFormat (uppercase) kwargs = clean_pdf_kwargs(paperFormat='A4') self.assertTrue( 'paperFormat' not in kwargs) # should have affected width and height, then be ditched. self.assertEqual(kwargs['paperWidth'], PAPER_FORMATS['a4']['width']) self.assertEqual(kwargs['paperHeight'], PAPER_FORMATS['a4']['height']) # raise ValueError if paperFormat passed along with paperWidth and/or paperHeight with self.assertRaises(ValueError): kwargs = clean_pdf_kwargs(paperFormat='A4', paperWidth=10.5) with self.assertRaises(ValueError): kwargs = clean_pdf_kwargs(paperFormat='A4', paperHeight=12.5) with self.assertRaises(ValueError): kwargs = clean_pdf_kwargs(paperFormat='A4', paperWidth=10.5, paperHeight=12.5)
def clean_pdf_kwargs(**options): """ Clean the pdf_kwargs into a format that Page.printToPDF accepts. Our defaults should match the default arguments of the Page.printToPDF API. For more information, see: https://chromedevtools.github.io/devtools-protocol/1-3/Page/#method-printToPDF :param scale: Scale of the PDF. Default 1. :param landscape: True to use landscape mode. Default False. :param displayHeaderFooter: True to display header and footer. Default False. :param headerTemplate: HTML containing the header for all pages. Default is an empty string. You may pass html tags with specific classes in order to insert values: * date: formatted print date * title: document title * url: document location * pageNumber: current page number * totalPages: total pages in the document For example, <span class="title"></span> would generate span containing the title. :param footerTemplate: HTML containing the footer for all pages. Default is an empty string. You may pass html tags with specific classes in order to insert values (same as above) :param printBackground: True to print background graphics. Default False. :param paperWidth: Width of the paper, in inches. Can also use some CSS string values, like "30cm". Default: 8.5 :param paperHeight: Height of the paper, in inches. Can also use some CSS string values, like "30cm". Default: 11 :param paperFormat: A string indicating a paper size format, such as "letter" or "A4". Case-insensitive. This will override paperWidth and paperHeight. Not part of Page.printToPDF API. Provided for convenience. :param margin: Shortcut used to set all four margin values at once. Not part of Page.printToPDF API. Provided for convenience. :param marginTop: Top margin. Default '1cm' :param marginBottom: Bottom margin. Default '1cm'. :param marginLeft: Left margin. Default '1cm'. :param marginRight: Right margin. Default '1cm'. :param pageRanges: String indicating page ranges to use. Example: '1-5, 8, 11-13' :param ignoreInvalidPageRanges: If True, will silently ignore invalid "pageRanges" values. Default False. :param _defaults: Internal-only. An optional dict of default parameter overrides that have already been 'cleaned'. :return: A dict containing an options dict that be used for Page.printToPDF """ if '_defaults' in options: defaults = options.pop('_defaults') else: defaults = get_default_pdf_kwargs() try: scale = float(options.get( 'scale', defaults['scale'])) # can be int or float, or numeric string except ValueError: # passed a character string? raise TypeError( 'You must pass a numeric value for the "scale" of the PDF.') landscape = bool(options.get('landscape', defaults['landscape'])) displayHeaderFooter = bool( options.get('displayHeaderFooter', defaults['displayHeaderFooter'])) headerTemplate = options.get('headerTemplate', defaults['headerTemplate']) footerTemplate = options.get('footerTemplate', defaults['footerTemplate']) printBackground = bool( options.get('printBackground', defaults['printBackground'])) paperWidth = defaults['paperWidth'] paperHeight = defaults['paperHeight'] if 'paperFormat' in options: # convenience option that's not part of Chrome's API if options['paperFormat'].lower() not in PAPER_FORMATS: raise ValueError('Unrecognized paper format: "%s"' % options['paperFormat']) if 'paperWidth' in options or 'paperHeight' in options: raise ValueError( 'Cannot pass a paperFormat at the same time as a paperWidth/paperHeight.' ) paperFormat = PAPER_FORMATS.get(options.pop( 'paperFormat').lower()) # pop it so we can validate options below paperWidth = paperFormat['width'] paperHeight = paperFormat['height'] else: if 'paperWidth' in options: paperWidth = convert_to_inches(options['paperWidth']) if 'paperHeight' in options: paperHeight = convert_to_inches(options['paperHeight']) if paperWidth is None: raise TypeError('You must set a paperWidth for this PDF.') if paperHeight is None: raise TypeError('You must set a paperHeight for this PDF.') marginTop = defaults['marginTop'] marginBottom = defaults['marginBottom'] marginLeft = defaults['marginLeft'] marginRight = defaults['marginRight'] # margin affects all four sides. margin in options will override default side-specific defaults. if 'margin' in options: margin = convert_to_inches(options.pop('margin')) marginTop = marginBottom = marginLeft = marginRight = margin # margin overrides for specific sides marginTop = convert_to_inches(options.get('marginTop', marginTop)) marginBottom = convert_to_inches(options.get('marginBottom', marginBottom)) marginLeft = convert_to_inches(options.get('marginLeft', marginLeft)) marginRight = convert_to_inches(options.get('marginRight', marginRight)) if marginTop is None: raise TypeError('You cannot set marginTop to None') if marginBottom is None: raise TypeError('You cannot set marginBottom to None') if marginLeft is None: raise TypeError('You cannot set marginLeft to None') if marginRight is None: raise TypeError('You cannot set marginRight to None') pageRanges = str(options.get('pageRanges', defaults['pageRanges'])) ignoreInvalidPageRanges = bool( options.get('ignoreInvalidPageRanges', defaults['ignoreInvalidPageRanges'])) # transferMode: not applicable. # preferCSSPageSize = options.get('preferCSSPageSize','') # the actual dict that we will pass to Chrome (convenience kwargs like margin and paperFormat are removed) parameters = dict( scale=scale, landscape=landscape, # preferCSSPageSize=preferCSSPageSize, # too new, causes error displayHeaderFooter=displayHeaderFooter, headerTemplate=headerTemplate, footerTemplate=footerTemplate, printBackground=printBackground, paperWidth=paperWidth, paperHeight=paperHeight, marginTop=marginTop, marginBottom=marginBottom, marginLeft=marginLeft, marginRight=marginRight, pageRanges=pageRanges, ignoreInvalidPageRanges=ignoreInvalidPageRanges, ) # check for bad options parameters_keys = set(parameters.keys()) options_keys = set(options.keys()) if not options_keys.issubset(parameters_keys): raise ValueError( 'Unrecognized pdf_kwargs passed to generate_pdf(): %s' % (', '.join(options_keys - parameters_keys))) return parameters
from chromepdf.conf import get_chromepdf_settings_dict from chromepdf.sizes import PAPER_FORMATS, convert_to_inches # Specified in printToPDF API - https://chromedevtools.github.io/devtools-protocol/1-3/Page/#method-printToPDF # These are the "TRUE" defaults. # If no overrides are provided in Django's settings.CHROMEPDF['PDF_KWARGS'], and no pdf_kwargs are passed to render functions, # Then these are the parameters that would be used and passed to Chrome. # You do NOT need to call convert_to_inches() when overriding these values yourself. DEFAULT_PDF_KWARGS = dict( landscape=False, displayHeaderFooter=False, printBackground=False, scale=1, paperWidth=convert_to_inches('8.5in'), paperHeight=convert_to_inches('11in'), marginTop=convert_to_inches('1cm'), marginLeft=convert_to_inches('1cm'), marginRight=convert_to_inches('1cm'), marginBottom=convert_to_inches('1cm'), pageRanges='', ignoreInvalidPageRanges=False, headerTemplate='', footerTemplate='', ) def get_default_pdf_kwargs(): """ Return the default pdf_kwargs used for rendering a PDF in Chrome. Use the values specified in Django settings.CHROMEPDF['PDF_KWARGS'] if they exist. Otherwise, fallback to the `DEFAULT_PDF_KWARGS` above.