Merge pull request #31069 from maharshivpatel/add_pdf_backend_hook

feat: added hook to use chrome pdf generator
This commit is contained in:
Suraj Shetty 2025-03-08 12:50:09 +05:30 committed by GitHub
commit e34a820256
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 152 additions and 60 deletions

View file

@ -11,7 +11,6 @@ be used to build database driven apps.
Read the documentation: https://frappeframework.com/docs
"""
import copy
import functools
import importlib
import inspect
@ -1673,14 +1672,22 @@ def get_file_json(path):
return json.load(f)
def read_file(path, raise_not_found=False):
"""Open a file and return its content as Unicode."""
def read_file(path, raise_not_found=False, as_base64=False):
"""Open a file and return its content as Unicode or Base64 string."""
if isinstance(path, str):
path = path.encode("utf-8")
if os.path.exists(path):
with open(path) as f:
return as_unicode(f.read())
if as_base64:
import base64
with open(path, "rb") as f:
content = f.read()
return base64.b64encode(content).decode("utf-8")
else:
with open(path) as f:
content = f.read()
return as_unicode(content)
elif raise_not_found:
raise OSError(f"{path} Not Found")
else:
@ -2076,52 +2083,6 @@ def format(*args, **kwargs):
return frappe.utils.formatters.format_value(*args, **kwargs)
def get_print(
doctype=None,
name=None,
print_format=None,
style=None,
as_pdf=False,
doc=None,
output=None,
no_letterhead=0,
password=None,
pdf_options=None,
letterhead=None,
):
"""Get Print Format for given document.
:param doctype: DocType of document.
:param name: Name of document.
:param print_format: Print Format name. Default 'Standard',
:param style: Print Format style.
:param as_pdf: Return as PDF. Default False.
:param password: Password to encrypt the pdf with. Default None"""
from frappe.utils.pdf import get_pdf
from frappe.website.serve import get_response_without_exception_handling
original_form_dict = copy.deepcopy(local.form_dict)
try:
local.form_dict.doctype = doctype
local.form_dict.name = name
local.form_dict.format = print_format
local.form_dict.style = style
local.form_dict.doc = doc
local.form_dict.no_letterhead = no_letterhead
local.form_dict.letterhead = letterhead
pdf_options = pdf_options or {}
if password:
pdf_options["password"] = password
response = get_response_without_exception_handling("printview", 200)
html = str(response.data, "utf-8")
finally:
local.form_dict = original_form_dict
return get_pdf(html, options=pdf_options, output=output) if as_pdf else html
def attach_print(
doctype,
name,
@ -2376,5 +2337,6 @@ from frappe.config import get_common_site_config, get_site_config
from frappe.core.doctype.system_settings.system_settings import get_system_settings
from frappe.utils import parse_json
from frappe.utils.error import log_error
from frappe.utils.print_utils import get_print
frappe._optimizations.optimize_all()

View file

@ -242,3 +242,4 @@ execute:frappe.db.set_single_value("Workspace Settings", "workspace_setup_comple
frappe.patches.v16_0.add_app_launcher_in_navbar_settings
frappe.desk.doctype.workspace.patches.update_app
frappe.patches.v16_0.move_role_desk_settings_to_user
frappe.printing.doctype.print_format.patches.sets_wkhtmltopdf_as_default_for_pdf_generator_field

View file

@ -0,0 +1,7 @@
import frappe
def execute():
"""sets "wkhtmltopdf" as default for pdf_generator field"""
for pf in frappe.get_all("Print Format", pluck="name"):
frappe.db.set_value("Print Format", pf, "pdf_generator", "wkhtmltopdf", update_modified=False)

View file

@ -13,6 +13,7 @@
"standard",
"custom_format",
"disabled",
"pdf_generator",
"section_break_6",
"print_format_type",
"raw_printing",
@ -255,12 +256,19 @@
"fieldtype": "Select",
"label": "Page Number",
"options": "Hide\nTop Left\nTop Center\nTop Right\nBottom Left\nBottom Center\nBottom Right"
},
{
"default": "wkhtmltopdf",
"fieldname": "pdf_generator",
"fieldtype": "Select",
"label": "PDF Generator",
"options": "wkhtmltopdf"
}
],
"icon": "fa fa-print",
"idx": 1,
"links": [],
"modified": "2024-03-23 16:03:34.964767",
"modified": "2025-02-14 14:49:39.181074",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Format",

View file

@ -40,6 +40,7 @@ class PrintFormat(Document):
page_number: DF.Literal[
"Hide", "Top Left", "Top Center", "Top Right", "Bottom Left", "Bottom Center", "Bottom Right"
]
pdf_generator: DF.Literal["wkhtmltopdf"]
print_format_builder: DF.Check
print_format_builder_beta: DF.Check
print_format_type: DF.Literal["Jinja", "JS"]

View file

@ -33,9 +33,11 @@ PDF_CONTENT_ERRORS = [
]
def pdf_header_html(soup, head, content, styles, html_id, css):
def pdf_header_html(soup, head, content, styles, html_id, css, path=None):
if not path:
path = "templates/print_formats/pdf_header_footer.html"
return frappe.render_template(
"templates/print_formats/pdf_header_footer.html",
path,
{
"head": head,
"content": content,
@ -75,8 +77,10 @@ def _guess_template_error_line_number(template) -> int | None:
return frame.lineno
def pdf_footer_html(soup, head, content, styles, html_id, css):
return pdf_header_html(soup=soup, head=head, content=content, styles=styles, html_id=html_id, css=css)
def pdf_footer_html(soup, head, content, styles, html_id, css, path=None):
return pdf_header_html(
soup=soup, head=head, content=content, styles=styles, html_id=html_id, css=css, path=path
)
def get_pdf(html, options=None, output: PdfWriter | None = None):
@ -316,7 +320,8 @@ def prepare_header_footer(soup: BeautifulSoup):
toggle_visible_pdf(content)
id_map = {"header-html": "pdf_header_html", "footer-html": "pdf_footer_html"}
hook_func = frappe.get_hooks(id_map.get(html_id))
html = frappe.get_attr(hook_func[-1])(
html = frappe.call(
hook_func[-1],
soup=soup,
head=head,
content=content,

View file

@ -3,6 +3,7 @@ import json
import os
import uuid
from io import BytesIO
from typing import Literal
from pypdf import PdfWriter
@ -218,14 +219,28 @@ from frappe.deprecation_dumpster import read_multi_pdf
@frappe.whitelist(allow_guest=True)
def download_pdf(
doctype: str, name: str, format=None, doc=None, no_letterhead=0, language=None, letterhead=None
doctype: str,
name: str,
format=None,
doc=None,
no_letterhead=0,
language=None,
letterhead=None,
pdf_generator: Literal["wkhtmltopdf", "chrome"] | None = None,
):
doc = doc or frappe.get_doc(doctype, name)
validate_print_permission(doc)
with print_language(language):
pdf_file = frappe.get_print(
doctype, name, format, doc=doc, as_pdf=True, letterhead=letterhead, no_letterhead=no_letterhead
doctype,
name,
format,
doc=doc,
as_pdf=True,
letterhead=letterhead,
no_letterhead=no_letterhead,
pdf_generator=pdf_generator,
)
frappe.local.response.filename = "{name}.pdf".format(name=name.replace(" ", "-").replace("/", "-"))

View file

@ -0,0 +1,89 @@
from typing import Literal
import frappe
def get_print(
doctype=None,
name=None,
print_format=None,
style=None,
as_pdf=False,
doc=None,
output=None,
no_letterhead=0,
password=None,
pdf_options=None,
letterhead=None,
pdf_generator: Literal["wkhtmltopdf", "chrome"] | None = None,
):
"""Get Print Format for given document.
:param doctype: DocType of document.
:param name: Name of document.
:param print_format: Print Format name. Default 'Standard',
:param style: Print Format style.
:param as_pdf: Return as PDF. Default False.
:param password: Password to encrypt the pdf with. Default None
:param pdf_generator: PDF generator to use. Default 'wkhtmltopdf'
"""
"""
local.form_dict.pdf_generator is set from before_request hook (print designer app) for download_pdf endpoint
if it is not set (internal function call) then set it
"""
import copy
from frappe.utils.pdf import get_pdf
from frappe.website.serve import get_response_without_exception_handling
local = frappe.local
if "pdf_generator" not in local.form_dict:
# if arg is passed, use that, else get setting from print format
if pdf_generator is None:
pdf_generator = (
frappe.get_cached_value("Print Format", print_format, "pdf_generator") or "wkhtmltopdf"
)
local.form_dict.pdf_generator = pdf_generator
original_form_dict = copy.deepcopy(local.form_dict)
try:
local.form_dict.doctype = doctype
local.form_dict.name = name
local.form_dict.format = print_format
local.form_dict.style = style
local.form_dict.doc = doc
local.form_dict.no_letterhead = no_letterhead
local.form_dict.letterhead = letterhead
pdf_options = pdf_options or {}
if password:
pdf_options["password"] = password
response = get_response_without_exception_handling("printview", 200)
html = str(response.data, "utf-8")
finally:
local.form_dict = original_form_dict
if not as_pdf:
return html
if local.form_dict.pdf_generator != "wkhtmltopdf":
hook_func = frappe.get_hooks("pdf_generator")
for hook in hook_func:
"""
check pdf_generator value in your hook function.
if it matches run and return pdf else return None
"""
pdf = frappe.call(
hook,
print_format=print_format,
html=html,
options=pdf_options,
output=output,
pdf_generator=local.form_dict.pdf_generator,
)
# if hook returns a value, assume it was the correct pdf_generator and return it
if pdf:
return pdf
return get_pdf(html, options=pdf_options, output=output)

View file

@ -72,6 +72,9 @@ def get_context(context) -> PrintContext:
print_format = get_print_format_doc(None, meta=meta)
make_access_log(
doctype=frappe.form_dict.doctype, document=frappe.form_dict.name, file_type="PDF", method="Print"
)
body = get_rendered_template(
doc,
print_format=print_format,
@ -99,6 +102,7 @@ def get_context(context) -> PrintContext:
"print_format": getattr(print_format, "name", None),
"letterhead": letterhead,
"no_letterhead": frappe.form_dict.no_letterhead,
"pdf_generator": frappe.form_dict.get("pdf_generator", "wkhtmltopdf"),
}
@ -173,7 +177,7 @@ def get_rendered_template(
template = None
if hook_func := frappe.get_hooks("get_print_format_template"):
template = frappe.get_attr(hook_func[-1])(jenv=jenv, print_format=print_format)
template = frappe.call(hook_func[-1], jenv=jenv, print_format=print_format)
if template:
pass