0

I have the following code which converts a HTML file upload to a PDF using a Python Flask web server REST endpoint. The file is uploaded as multi-part form data. It works great, but when I tried to add Swagger to the REST endpoint using flask_restx it broke. It seems to interpret the output PDF as JSON (even though I tried to tell flask_restx that it must produce a PDF in several ways) because it gives me the error at the end. I even wrote out the PDF to disk to check that it still works and it does. Where am I going wrong?

import io
import os
from flask import (Flask, redirect, render_template, render_template_string, request,
                   send_from_directory, url_for, make_response, jsonify)
from xhtml2pdf import pisa
from io import StringIO
#from flask_httpauth import HTTPBasicAuth
from flask_restx import Api, Resource, fields
from werkzeug.datastructures import FileStorage


# Create a Flask website app.
app = Flask(__name__)
# Add Basic Authentication to the Flask app.
#auth = HTTPBasicAuth()

# Add Swagger support to the Flask app.
api = Api(
    app = app,
    version = '1.0',    # REST API version not Swagger version (Swagger = 2.0)
    description = 'HTML to PDF API',
    doc = '/swagger/',
    default = 'api',
    default_label = 'HTML to PDF API'
)

# Create a file upload parser for the HTML file via Swagger.
upload_parser = api.parser()
upload_parser.add_argument('file', location='files', type=FileStorage, required=True)


@app.route('/')
def index():
   return render_template('index.html')


@app.route('/favicon.ico')
def favicon():
    return send_from_directory(os.path.join(app.root_path, 'static'),
                               'favicon.ico', mimetype='image/vnd.microsoft.icon')


@app.route('/hello', methods=['POST'])
def hello():
   name = request.form.get('name')
   if name:
       return render_template('hello.html', name = name)
   else:
       return redirect(url_for('index'))


#@auth.verify_password
def authenticate(username, password):
    """Authenticate the user via Basic Authentication."""
    if username and password:
        if username == 'username' and password == 'password':
            return True
        else:
            return False
    else:
        return False
    return False


class Pdf():
    def render_pdf(self, html):
        """Render HTML to PDF using xhtml2pdf"""
        pdf = io.BytesIO()
        pisa.CreatePDF(StringIO(html), pdf)
        return pdf.getvalue()


# Data model for the successful PDF response
pdf_model = api.model('PDFResponse', {
    'pdf_data': fields.Raw    # Assuming the 'pdf_data' will be the raw bytes of the PDF file.
})


@api.route("/ConvertHTMLtoPDF", methods=["POST"])
#@auth.login_required
class HTMLtoPDF(Resource):
    @api.expect(upload_parser)
    @api.response(200, 'Success', pdf_model)# headers={'content-type': 'application/pdf', 'content-disposition': 'attachment; filename=receiving.pdf'})
    @api.produces(['application/pdf'])
    # @api.response(422, 'Unprocessable Content', headers={'content-type': 'application/json'})
    def post(self):
        """Upload a HTML file via a REST POST and return a PDF of that HTML.
           ---
           consumes: ["multipart/form-data"]
           produces: ["application/pdf"]
           parameters:
             - in: formData
               name: file
               type: file
               required: true
               description: upload a HTML file
        """
        if request.method == "POST":
            file_content = None
            # file = request.files.get("file")
            #for f in request.files.getlist('file'):
            #    # file_content = file.read()
            #    # Read the HTML posted. In Postman use body, form-data with key=file and value=html file after choosing type file on key.
            #    file_content = f.read()
            #    # Only read one file
            #    break

            args = upload_parser.parse_args()
            file_content = args['file']    # This is FileStorage instance

            # Check if file loaded successfully or not.
            if file_content:
                # Renders a template from the given template source string with the given context. Template variables will be autoescaped.
                # Note: HTML must be in ANSI latin-1.
                # html = render_template_string(file_content.decode("latin-1"))
                s = file_content.read().decode("latin-1")
                print(s)
                html = render_template_string(s)
                file_class = Pdf()
                pdf = file_class.render_pdf(html)
                fout = open('c:/downloads/test.pdf', 'wb')
                fout.write(pdf)
                fout.close()
                headers = {
                    'content-type': 'application/pdf',
                    'content-disposition': 'attachment; filename=receiving.pdf'}
                return pdf, 200, headers

            else:
                return jsonify(message="Upload unsuccessful"), 422

        # Render the index.html if file was not posted.
        return render_template("index.html")


# Run the Flask web app.
if __name__ == '__main__':
   app.run()

The error from the flask server log:

expected string or bytes-like object
[2023-08-04 15:38:08,087] ERROR in app: Exception on /ConvertHTMLtoPDF [POST]
Traceback (most recent call last):
  File "D:\EBackup\Work\Python\azure\GMTPythonFunctionApps\.venv\lib\site-packages\flask\app.py", line 1516, in full_dispatch_request
    rv = self.dispatch_request()
  File "D:\EBackup\Work\Python\azure\GMTPythonFunctionApps\.venv\lib\site-packages\flask\app.py", line 1502, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
  File "D:\EBackup\Work\Python\azure\GMTPythonFunctionApps\.venv\lib\site-packages\flask_restx\api.py", line 408, in wrapper
    return self.make_response(data, code, headers=headers)
  File "D:\EBackup\Work\Python\azure\GMTPythonFunctionApps\.venv\lib\site-packages\flask_restx\api.py", line 432, in make_response
    resp = self.representations[mediatype](data, *args, **kwargs)
  File "D:\EBackup\Work\Python\azure\GMTPythonFunctionApps\.venv\lib\site-packages\flask_restx\representations.py", line 22, in output_json
    dumped = dumps(data, **settings) + "\n"
  File "C:\Python310\lib\json\__init__.py", line 231, in dumps
    return _default_encoder.encode(obj)
  File "C:\Python310\lib\json\encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "C:\Python310\lib\json\encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "C:\Python310\lib\json\encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type bytes is not JSON serializable
Superdooperhero
  • 7,584
  • 19
  • 83
  • 138

0 Answers0