You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

157 lines
4.1 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. from __future__ import unicode_literals
  4. import pdfkit, os, frappe
  5. from frappe.utils import scrub_urls
  6. from frappe import _
  7. from bs4 import BeautifulSoup
  8. from PyPDF2 import PdfFileWriter, PdfFileReader
  9. def get_pdf(html, options=None, output = None):
  10. html = scrub_urls(html)
  11. html, options = prepare_options(html, options)
  12. fname = os.path.join("/tmp", "frappe-pdf-{0}.pdf".format(frappe.generate_hash()))
  13. try:
  14. pdfkit.from_string(html, fname, options=options or {})
  15. if output:
  16. append_pdf(PdfFileReader(file(fname,"rb")),output)
  17. else:
  18. with open(fname, "rb") as fileobj:
  19. filedata = fileobj.read()
  20. except IOError as e:
  21. if ("ContentNotFoundError" in e.message
  22. or "ContentOperationNotPermittedError" in e.message
  23. or "UnknownContentError" in e.message
  24. or "RemoteHostClosedError" in e.message):
  25. # allow pdfs with missing images if file got created
  26. if os.path.exists(fname):
  27. with open(fname, "rb") as fileobj:
  28. filedata = fileobj.read()
  29. else:
  30. frappe.throw(_("PDF generation failed because of broken image links"))
  31. else:
  32. raise
  33. finally:
  34. cleanup(fname, options)
  35. if output:
  36. return output
  37. return filedata
  38. def append_pdf(input,output):
  39. # Merging multiple pdf files
  40. [output.addPage(input.getPage(page_num)) for page_num in range(input.numPages)]
  41. def prepare_options(html, options):
  42. if not options:
  43. options = {}
  44. options.update({
  45. 'print-media-type': None,
  46. 'background': None,
  47. 'images': None,
  48. 'quiet': None,
  49. # 'no-outline': None,
  50. 'encoding': "UTF-8",
  51. #'load-error-handling': 'ignore',
  52. # defaults
  53. 'margin-right': '15mm',
  54. 'margin-left': '15mm',
  55. 'margin-top': '15mm',
  56. 'margin-bottom': '15mm',
  57. })
  58. html, html_options = read_options_from_html(html)
  59. options.update(html_options or {})
  60. # cookies
  61. if frappe.session and frappe.session.sid:
  62. options['cookie'] = [('sid', '{0}'.format(frappe.session.sid))]
  63. # page size
  64. if not options.get("page-size"):
  65. options['page-size'] = frappe.db.get_single_value("Print Settings", "pdf_page_size") or "A4"
  66. return html, options
  67. def read_options_from_html(html):
  68. options = {}
  69. soup = BeautifulSoup(html, "html5lib")
  70. # extract pdfkit options from html
  71. for html_id in ("margin-top", "margin-bottom", "margin-left", "margin-right", "page-size"):
  72. try:
  73. tag = soup.find(id=html_id)
  74. if tag and tag.contents:
  75. options[html_id] = tag.contents
  76. except:
  77. pass
  78. options.update(prepare_header_footer(soup))
  79. toggle_visible_pdf(soup)
  80. return soup.prettify(), options
  81. def prepare_header_footer(soup):
  82. options = {}
  83. head = soup.find("head").contents
  84. styles = soup.find_all("style")
  85. bootstrap = frappe.read_file(os.path.join(frappe.local.sites_path, "assets/frappe/css/bootstrap.css"))
  86. fontawesome = frappe.read_file(os.path.join(frappe.local.sites_path, "assets/frappe/css/font-awesome.css"))
  87. # extract header and footer
  88. for html_id in ("header-html", "footer-html"):
  89. content = soup.find(id=html_id)
  90. if content:
  91. # there could be multiple instances of header-html/footer-html
  92. for tag in soup.find_all(id=html_id):
  93. tag.extract()
  94. toggle_visible_pdf(content)
  95. html = frappe.render_template("templates/print_formats/pdf_header_footer.html", {
  96. "head": head,
  97. "styles": styles,
  98. "content": content,
  99. "html_id": html_id,
  100. "bootstrap": bootstrap,
  101. "fontawesome": fontawesome
  102. })
  103. # create temp file
  104. fname = os.path.join("/tmp", "frappe-pdf-{0}.html".format(frappe.generate_hash()))
  105. with open(fname, "w") as f:
  106. f.write(html.encode("utf-8"))
  107. # {"header-html": "/tmp/frappe-pdf-random.html"}
  108. options[html_id] = fname
  109. return options
  110. def cleanup(fname, options):
  111. if os.path.exists(fname):
  112. os.remove(fname)
  113. for key in ("header-html", "footer-html"):
  114. if options.get(key) and os.path.exists(options[key]):
  115. os.remove(options[key])
  116. def toggle_visible_pdf(soup):
  117. for tag in soup.find_all(attrs={"class": "visible-pdf"}):
  118. # remove visible-pdf class to unhide
  119. tag.attrs['class'].remove('visible-pdf')
  120. for tag in soup.find_all(attrs={"class": "hidden-pdf"}):
  121. # remove tag from html
  122. tag.extract()