瀏覽代碼

revert: pdf gen

Timothy J. Baek 6 月之前
父節點
當前提交
112cbdccbb

+ 11 - 2
backend/open_webui/apps/webui/routers/utils.py

@@ -56,8 +56,17 @@ class ChatForm(BaseModel):
 async def download_chat_as_pdf(
 async def download_chat_as_pdf(
     form_data: ChatTitleMessagesForm,
     form_data: ChatTitleMessagesForm,
 ):
 ):
-    response = PDFGenerator(form_data).generate_chat_pdf()
-    return response
+    try:
+        pdf_bytes = PDFGenerator(form_data).generate_chat_pdf()
+
+        return Response(
+            content=pdf_bytes,
+            media_type="application/pdf",
+            headers={"Content-Disposition": "attachment;filename=chat.pdf"},
+        )
+    except Exception as e:
+        print(e)
+        raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 @router.get("/db/download")
 @router.get("/db/download")

+ 2 - 0
backend/open_webui/env.py

@@ -230,6 +230,8 @@ if FROM_INIT_PY:
     DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data"))
     DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data"))
 
 
 
 
+STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static"))
+
 FONTS_DIR = Path(os.getenv("FONTS_DIR", OPEN_WEBUI_DIR / "static" / "fonts"))
 FONTS_DIR = Path(os.getenv("FONTS_DIR", OPEN_WEBUI_DIR / "static" / "fonts"))
 
 
 FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve()
 FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve()

+ 145 - 109
backend/open_webui/static/assets/pdf-style.css

@@ -1,190 +1,230 @@
 /* HTML and Body */
 /* HTML and Body */
+@font-face {
+	font-family: 'NotoSans';
+	src: url('fonts/NotoSans-Variable.ttf');
+}
+
+@font-face {
+	font-family: 'NotoSansJP';
+	src: url('fonts/NotoSansJP-Variable.ttf');
+}
+
+@font-face {
+	font-family: 'NotoSansKR';
+	src: url('fonts/NotoSansKR-Variable.ttf');
+}
+
+@font-face {
+	font-family: 'NotoSansSC';
+	src: url('fonts/NotoSansSC-Variable.ttf');
+}
+
+@font-face {
+	font-family: 'NotoSansSC-Regular';
+	src: url('fonts/NotoSansSC-Regular.ttf');
+}
+
 html {
 html {
-    box-sizing: border-box;
-    font-size: 14px; /* Default font size */
-    line-height: 1.5;
+	font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'NotoSans', 'NotoSansJP', 'NotoSansKR',
+		'NotoSansSC', 'STSong-Light', 'MSung-Light', 'HeiseiMin-W3', 'HYSMyeongJo-Medium', Roboto,
+		'Helvetica Neue', Arial, sans-serif;
+	font-size: 14px; /* Default font size */
+	line-height: 1.5;
 }
 }
 
 
-*, *::before, *::after {
-    box-sizing: inherit;
+*,
+*::before,
+*::after {
+	box-sizing: inherit;
 }
 }
 
 
 body {
 body {
-    margin: 0;
-    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-    color: #212529;
-    background-color: #fff;
-    width: auto;
+	margin: 0;
+	color: #212529;
+	background-color: #fff;
+	width: auto;
 }
 }
 
 
 /* Typography */
 /* Typography */
-h1, h2, h3, h4, h5, h6 {
-    font-weight: 500;
-    margin: 0;
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+	font-weight: 500;
+	margin: 0;
 }
 }
 
 
 h1 {
 h1 {
-    font-size: 2.5rem;
+	font-size: 2.5rem;
 }
 }
 
 
 h2 {
 h2 {
-    font-size: 2rem;
+	font-size: 2rem;
 }
 }
 
 
 h3 {
 h3 {
-    font-size: 1.75rem;
+	font-size: 1.75rem;
 }
 }
 
 
 h4 {
 h4 {
-    font-size: 1.5rem;
+	font-size: 1.5rem;
 }
 }
 
 
 h5 {
 h5 {
-    font-size: 1.25rem;
+	font-size: 1.25rem;
 }
 }
 
 
 h6 {
 h6 {
-    font-size: 1rem;
+	font-size: 1rem;
 }
 }
 
 
 p {
 p {
-    margin-top: 0;
-    margin-bottom: 1rem;
+	margin-top: 0;
+	margin-bottom: 1rem;
 }
 }
 
 
 /* Grid System */
 /* Grid System */
 .container {
 .container {
-    width: 100%;
-    padding-right: 15px;
-    padding-left: 15px;
-    margin-right: auto;
-    margin-left: auto;
+	width: 100%;
+	padding-right: 15px;
+	padding-left: 15px;
+	margin-right: auto;
+	margin-left: auto;
 }
 }
 
 
 /* Utilities */
 /* Utilities */
 .text-center {
 .text-center {
-    text-align: center;
+	text-align: center;
 }
 }
 
 
 /* Additional Text Utilities */
 /* Additional Text Utilities */
 .text-muted {
 .text-muted {
-    color: #6c757d; /* Muted text color */
+	color: #6c757d; /* Muted text color */
 }
 }
 
 
 /* Small Text */
 /* Small Text */
 small {
 small {
-    font-size: 80%; /* Smaller font size relative to the base */
-    color: #6c757d; /* Lighter text color for secondary information */
-    margin-bottom: 0;
-    margin-top: 0;
+	font-size: 80%; /* Smaller font size relative to the base */
+	color: #6c757d; /* Lighter text color for secondary information */
+	margin-bottom: 0;
+	margin-top: 0;
 }
 }
 
 
 /* Strong Element Styles */
 /* Strong Element Styles */
 strong {
 strong {
-    font-weight: bolder; /* Ensures the text is bold */
-    color: inherit; /* Inherits the color from its parent element */
+	font-weight: bolder; /* Ensures the text is bold */
+	color: inherit; /* Inherits the color from its parent element */
 }
 }
 
 
 /* link */
 /* link */
 a {
 a {
-    color: #007bff;
-    text-decoration: none;
-    background-color: transparent;
+	color: #007bff;
+	text-decoration: none;
+	background-color: transparent;
 }
 }
 
 
 a:hover {
 a:hover {
-    color: #0056b3;
-    text-decoration: underline;
+	color: #0056b3;
+	text-decoration: underline;
 }
 }
 
 
 /* General styles for lists */
 /* General styles for lists */
-ol, ul, li {
-    padding-left: 40px; /* Increase padding to move bullet points to the right */
-    margin-left: 20px; /* Indent lists from the left */
+ol,
+ul,
+li {
+	padding-left: 40px; /* Increase padding to move bullet points to the right */
+	margin-left: 20px; /* Indent lists from the left */
 }
 }
 
 
 /* Ordered list styles */
 /* Ordered list styles */
 ol {
 ol {
-    list-style-type: decimal; /* Use numbers for ordered lists */
-    margin-bottom: 10px; /* Space after each list */
+	list-style-type: decimal; /* Use numbers for ordered lists */
+	margin-bottom: 10px; /* Space after each list */
 }
 }
 
 
-
 ol li {
 ol li {
-    margin-bottom: 0.5rem; /* Space between ordered list items */
+	margin-bottom: 0.5rem; /* Space between ordered list items */
 }
 }
 
 
 /* Unordered list styles */
 /* Unordered list styles */
 ul {
 ul {
-    list-style-type: disc; /* Use bullets for unordered lists */
-    margin-bottom: 10px; /* Space after each list */
+	list-style-type: disc; /* Use bullets for unordered lists */
+	margin-bottom: 10px; /* Space after each list */
 }
 }
 
 
 ul li {
 ul li {
-    margin-bottom: 0.5rem; /* Space between unordered list items */
+	margin-bottom: 0.5rem; /* Space between unordered list items */
 }
 }
 
 
 /* List item styles */
 /* List item styles */
 li {
 li {
-    margin-bottom: 5px; /* Space between list items */
-    line-height: 1.5; /* Line height for better readability */
+	margin-bottom: 5px; /* Space between list items */
+	line-height: 1.5; /* Line height for better readability */
 }
 }
 
 
 /* Nested lists */
 /* Nested lists */
-ol ol, ol ul, ul ol, ul ul {
-    padding-left: 20px;
-    margin-left: 30px; /* Further indent nested lists */
-    margin-bottom: 0; /* Remove extra margin at the bottom of nested lists */
+ol ol,
+ol ul,
+ul ol,
+ul ul {
+	padding-left: 20px;
+	margin-left: 30px; /* Further indent nested lists */
+	margin-bottom: 0; /* Remove extra margin at the bottom of nested lists */
 }
 }
 
 
 /* Code blocks */
 /* Code blocks */
 pre {
 pre {
-    background-color: #f4f4f4;
-    padding: 10px;
-    overflow-x: auto;
-    max-width: 100%; /* Ensure it doesn't overflow the page */
-    width: 80%; /* Set a specific width for a container-like appearance */
-    margin: 0 1em; /* Center the pre block */
-    box-sizing: border-box; /* Include padding in the width */
-    border: 1px solid #ccc; /* Optional: Add a border for better definition */
-    border-radius: 4px; /* Optional: Add rounded corners */
+	background-color: #f4f4f4;
+	padding: 10px;
+	overflow-x: auto;
+	max-width: 100%; /* Ensure it doesn't overflow the page */
+	width: 80%; /* Set a specific width for a container-like appearance */
+	margin: 0 1em; /* Center the pre block */
+	box-sizing: border-box; /* Include padding in the width */
+	border: 1px solid #ccc; /* Optional: Add a border for better definition */
+	border-radius: 4px; /* Optional: Add rounded corners */
 }
 }
 
 
 code {
 code {
-    font-family: 'Courier New', Courier, monospace;
-    background-color: #f4f4f4;
-    padding: 2px 4px;
-    border-radius: 4px;
-    box-sizing: border-box; /* Include padding in the width */
+	font-family: 'Courier New', Courier, monospace;
+	background-color: #f4f4f4;
+	padding: 2px 4px;
+	border-radius: 4px;
+	box-sizing: border-box; /* Include padding in the width */
 }
 }
 
 
 .message {
 .message {
-    margin-top: 8px;
-    margin-bottom: 8px;
+	margin-top: 8px;
+	margin-bottom: 8px;
+	max-width: 100%;
+	overflow-wrap: break-word;
 }
 }
 
 
 /* Table Styles */
 /* Table Styles */
 table {
 table {
-    width: 100%;
-    margin-bottom: 1rem;
-    color: #212529;
-    border-collapse: collapse; /* Removes the space between borders */
+	width: 100%;
+	margin-bottom: 1rem;
+	color: #212529;
+	border-collapse: collapse; /* Removes the space between borders */
 }
 }
 
 
-th, td {
-    margin: 0;
-    padding: 0.75rem;
-    vertical-align: top;
-    border-top: 1px solid #dee2e6;
+th,
+td {
+	margin: 0;
+	padding: 0.75rem;
+	vertical-align: top;
+	border-top: 1px solid #dee2e6;
 }
 }
 
 
 thead th {
 thead th {
-    vertical-align: bottom;
-    border-bottom: 2px solid #dee2e6;
+	vertical-align: bottom;
+	border-bottom: 2px solid #dee2e6;
 }
 }
 
 
 tbody + tbody {
 tbody + tbody {
-    border-top: 2px solid #dee2e6;
+	border-top: 2px solid #dee2e6;
 }
 }
 
 
 /* markdown-section styles */
 /* markdown-section styles */
@@ -199,8 +239,8 @@ tbody + tbody {
 .markdown-section pre,
 .markdown-section pre,
 .markdown-section table,
 .markdown-section table,
 .markdown-section ul {
 .markdown-section ul {
-    /* Give most block elements margin top and bottom */
-    margin-top: 1rem;
+	/* Give most block elements margin top and bottom */
+	margin-top: 1rem;
 }
 }
 
 
 /* Remove top margin if it's the first child */
 /* Remove top margin if it's the first child */
@@ -215,69 +255,65 @@ tbody + tbody {
 .markdown-section pre:first-child,
 .markdown-section pre:first-child,
 .markdown-section table:first-child,
 .markdown-section table:first-child,
 .markdown-section ul:first-child {
 .markdown-section ul:first-child {
-    margin-top: 0;
+	margin-top: 0;
 }
 }
 
 
-
 /* Remove top margin of <ul> following a <p> */
 /* Remove top margin of <ul> following a <p> */
 .markdown-section p + ul {
 .markdown-section p + ul {
-    margin-top: 0;
+	margin-top: 0;
 }
 }
 
 
 /* Remove bottom margin of <p> if it is followed by a <ul> */
 /* Remove bottom margin of <p> if it is followed by a <ul> */
 /* Note: :has is not supported in CSS, so you would need JavaScript for this behavior */
 /* Note: :has is not supported in CSS, so you would need JavaScript for this behavior */
 .markdown-section p {
 .markdown-section p {
-    margin-bottom: 0;
+	margin-bottom: 0;
 }
 }
 
 
 /* Add a rule to reset margin-bottom for <p> not followed by <ul> */
 /* Add a rule to reset margin-bottom for <p> not followed by <ul> */
 .markdown-section p + ul {
 .markdown-section p + ul {
-    margin-top: 0;
+	margin-top: 0;
 }
 }
 
 
 /* List item styles */
 /* List item styles */
 .markdown-section li {
 .markdown-section li {
-    padding: 2px;
+	padding: 2px;
 }
 }
 
 
 .markdown-section li p {
 .markdown-section li p {
-    margin-bottom: 0;
-    padding: 0;
+	margin-bottom: 0;
+	padding: 0;
 }
 }
 
 
 /* Avoid margins for nested lists */
 /* Avoid margins for nested lists */
 .markdown-section li > ul {
 .markdown-section li > ul {
-    margin-top: 0;
-    margin-bottom: 0;
+	margin-top: 0;
+	margin-bottom: 0;
 }
 }
 
 
 /* Table styles */
 /* Table styles */
 .markdown-section table {
 .markdown-section table {
-    width: 100%;
-    border-collapse: collapse;
-    margin: 1rem 0;
+	width: 100%;
+	border-collapse: collapse;
+	margin: 1rem 0;
 }
 }
 
 
 .markdown-section th,
 .markdown-section th,
 .markdown-section td {
 .markdown-section td {
-    border: 1px solid #ddd;
-    padding: 0.5rem;
-    text-align: left;
+	border: 1px solid #ddd;
+	padding: 0.5rem;
+	text-align: left;
 }
 }
 
 
 .markdown-section th {
 .markdown-section th {
-    background-color: #f2f2f2;
+	background-color: #f2f2f2;
 }
 }
 
 
 .markdown-section pre {
 .markdown-section pre {
-    padding: 10px;
-    margin: 10px;
+	padding: 10px;
+	margin: 10px;
 }
 }
 
 
 .markdown-section pre code {
 .markdown-section pre code {
-    position: relative;
-    color: rgb(172, 0, 95);
+	position: relative;
+	color: rgb(172, 0, 95);
 }
 }
-
-
-    

二進制
backend/open_webui/static/fonts/NotoSans-Bold.ttf


二進制
backend/open_webui/static/fonts/NotoSans-Italic.ttf


二進制
backend/open_webui/static/fonts/NotoSans-Regular.ttf


二進制
backend/open_webui/static/fonts/NotoSans-Variable.ttf


二進制
backend/open_webui/static/fonts/NotoSansJP-Regular.ttf


二進制
backend/open_webui/static/fonts/NotoSansJP-Variable.ttf


二進制
backend/open_webui/static/fonts/NotoSansKR-Regular.ttf


二進制
backend/open_webui/static/fonts/NotoSansKR-Variable.ttf


二進制
backend/open_webui/static/fonts/NotoSansSC-Regular.ttf


二進制
backend/open_webui/static/fonts/NotoSansSC-Variable.ttf


+ 90 - 51
backend/open_webui/utils/pdf_generator.py

@@ -4,9 +4,12 @@ from pathlib import Path
 from typing import Dict, Any, List
 from typing import Dict, Any, List
 
 
 from markdown import markdown
 from markdown import markdown
-from starlette.responses import Response
 from xhtml2pdf import pisa
 from xhtml2pdf import pisa
 
 
+import site
+from fpdf import FPDF
+
+from open_webui.env import STATIC_DIR, FONTS_DIR
 from open_webui.apps.webui.models.chats import ChatTitleMessagesForm
 from open_webui.apps.webui.models.chats import ChatTitleMessagesForm
 
 
 
 
@@ -30,21 +33,31 @@ class PDFGenerator:
         self.html_body = None
         self.html_body = None
         self.messages_html = None
         self.messages_html = None
         self.form_data = form_data
         self.form_data = form_data
-        self.css_style_file = Path("./backend/open_webui/static/assets/pdf-style.css")
 
 
-    def build_html_message(self, message: Dict[str, Any]) -> str:
+        self.css = Path(STATIC_DIR / "assets" / "pdf-style.css").read_text()
+
+    def format_timestamp(self, timestamp: float) -> str:
+        """Convert a UNIX timestamp to a formatted date string."""
+        try:
+            date_time = datetime.fromtimestamp(timestamp)
+            return date_time.strftime("%Y-%m-%d, %H:%M:%S")
+        except (ValueError, TypeError) as e:
+            # Log the error if necessary
+            return ""
+
+    def _build_html_message(self, message: Dict[str, Any]) -> str:
         """Build HTML for a single message."""
         """Build HTML for a single message."""
         role = message.get("role", "user")
         role = message.get("role", "user")
         content = message.get("content", "")
         content = message.get("content", "")
-        timestamp = message.get('timestamp')
+        timestamp = message.get("timestamp")
 
 
-        model = message.get('model') if role == 'assistant' else ''
+        model = message.get("model") if role == "assistant" else ""
 
 
-        date_str = self.format_timestamp(timestamp) if timestamp else ''
+        date_str = self.format_timestamp(timestamp) if timestamp else ""
 
 
         # extends pymdownx extension to convert markdown to html.
         # extends pymdownx extension to convert markdown to html.
         # - https://facelessuser.github.io/pymdown-extensions/usage_notes/
         # - https://facelessuser.github.io/pymdown-extensions/usage_notes/
-        html_content = markdown(content, extensions=['pymdownx.extra'])
+        html_content = markdown(content, extensions=["pymdownx.extra"])
 
 
         html_message = f"""
         html_message = f"""
               <div class="message">
               <div class="message">
@@ -62,63 +75,35 @@ class PDFGenerator:
           """
           """
         return html_message
         return html_message
 
 
-    def create_pdf_from_html(self) -> bytes:
+    def _fetch_resources(self, uri: str, rel: str) -> str:
+
+        print(str(STATIC_DIR / uri))
+        return str(STATIC_DIR / uri)
+
+    def _create_pdf_from_html(self) -> bytes:
         """Convert HTML content to PDF and return the bytes."""
         """Convert HTML content to PDF and return the bytes."""
         pdf_buffer = BytesIO()
         pdf_buffer = BytesIO()
-        pisa_status = pisa.CreatePDF(src=self.html_body, dest=pdf_buffer)
+        pisa_status = pisa.CreatePDF(
+            src=self.html_body.encode("UTF-8"),
+            dest=pdf_buffer,
+            encoding="UTF-8",
+            link_callback=self._fetch_resources,
+        )
         if pisa_status.err:
         if pisa_status.err:
             raise RuntimeError("Error generating PDF")
             raise RuntimeError("Error generating PDF")
 
 
         return pdf_buffer.getvalue()
         return pdf_buffer.getvalue()
 
 
-    def format_timestamp(self, timestamp: float) -> str:
-        """Convert a UNIX timestamp to a formatted date string."""
-        try:
-            date_time = datetime.fromtimestamp(timestamp)
-            return date_time.strftime("%Y-%m-%d, %H:%M:%S")
-        except (ValueError, TypeError) as e:
-            # Log the error if necessary
-            return ''
-
-    def generate_chat_pdf(self) -> Response:
-        """
-        Generate a PDF from chat messages.
-
-        Returns:
-            A FastAPI Response with the generated PDF or an error message.
-        """
-        try:
-            # Build HTML messages
-            messages_html_list: List[str] = [self.build_html_message(msg) for msg in self.form_data.messages]
-            self.messages_html = '<div>' + ''.join(messages_html_list) + '</div>'
-
-            # Generate full HTML body
-            self.html_body = self.generate_html_body()
-
-            # Create PDF
-            pdf_bytes = self.create_pdf_from_html()
-
-            # Return PDF as response
-            return Response(
-                content=pdf_bytes,
-                media_type="application/pdf",
-                headers={"Content-Disposition": "attachment;filename=chat.pdf"},
-            )
-        except RuntimeError as pdf_error:
-            # Handle PDF generation errors
-            return Response(content=str(pdf_error), status_code=500)
-        except Exception as e:
-            # Handle other unexpected errors
-            return Response(content="An unexpected error occurred.", status_code=500)
-
-    def generate_html_body(self) -> str:
+    def _generate_html_body(self) -> str:
         """Generate the full HTML body for the PDF."""
         """Generate the full HTML body for the PDF."""
         return f"""
         return f"""
         <html>
         <html>
             <head>
             <head>
                 <meta charset="UTF-8">
                 <meta charset="UTF-8">
                 <meta name="viewport" content="width=device-width, initial-scale=1.0">
                 <meta name="viewport" content="width=device-width, initial-scale=1.0">
-                <link rel="stylesheet" href="{self.css_style_file.as_posix()}">
+                <style type="text/css">
+                    {self.css}
+                </style>
             </head>
             </head>
             <body>
             <body>
                 <div class="container"> 
                 <div class="container"> 
@@ -132,3 +117,57 @@ class PDFGenerator:
             </body>
             </body>
         </html>
         </html>
         """
         """
+
+    def generate_chat_pdf(self) -> bytes:
+        """
+        Generate a PDF from chat messages.
+        """
+        try:
+            global FONTS_DIR
+
+            pdf = FPDF()
+            pdf.add_page()
+
+            # When running using `pip install` the static directory is in the site packages.
+            if not FONTS_DIR.exists():
+                FONTS_DIR = Path(site.getsitepackages()[0]) / "static/fonts"
+            # When running using `pip install -e .` the static directory is in the site packages.
+            # This path only works if `open-webui serve` is run from the root of this project.
+            if not FONTS_DIR.exists():
+                FONTS_DIR = Path("./backend/static/fonts")
+
+            pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
+            pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
+            pdf.add_font("NotoSans", "i", f"{FONTS_DIR}/NotoSans-Italic.ttf")
+            pdf.add_font("NotoSansKR", "", f"{FONTS_DIR}/NotoSansKR-Regular.ttf")
+            pdf.add_font("NotoSansJP", "", f"{FONTS_DIR}/NotoSansJP-Regular.ttf")
+            pdf.add_font("NotoSansSC", "", f"{FONTS_DIR}/NotoSansSC-Regular.ttf")
+
+            pdf.set_font("NotoSans", size=12)
+            pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP", "NotoSansSC"])
+
+            pdf.set_auto_page_break(auto=True, margin=15)
+
+            # Adjust the effective page width for multi_cell
+            effective_page_width = (
+                pdf.w - 2 * pdf.l_margin - 10
+            )  # Subtracted an additional 10 for extra padding
+
+            # Add chat messages
+            for message in self.form_data.messages:
+                role = message["role"]
+                content = message["content"]
+                pdf.set_font("NotoSans", "B", size=14)  # Bold for the role
+                pdf.multi_cell(effective_page_width, 10, f"{role.upper()}", 0, "L")
+                pdf.ln(1)  # Extra space between messages
+
+                pdf.set_font("NotoSans", size=10)  # Regular for content
+                pdf.multi_cell(effective_page_width, 6, content, 0, "L")
+                pdf.ln(1.5)  # Extra space between messages
+
+            # Save the pdf with name .pdf
+            pdf_bytes = pdf.output()
+
+            return bytes(pdf_bytes)
+        except Exception as e:
+            raise e