1
0
Эх сурвалжийг харах

export: support exporting PDF via wkhtmltopdf tool

Le Tan 7 жил өмнө
parent
commit
935bb4d3b4

+ 252 - 46
src/dialog/vexportdialog.cpp

@@ -2,6 +2,7 @@
 
 #include <QtWidgets>
 #include <QCoreApplication>
+#include <QProcess>
 
 #ifndef QT_NO_PRINTER
 #include <QPrinter>
@@ -18,6 +19,7 @@
 #include "vnotefile.h"
 #include "vnote.h"
 #include "vexporter.h"
+#include "vlineedit.h"
 
 extern VConfigManager *g_config;
 
@@ -25,7 +27,7 @@ extern VNote *g_vnote;
 
 QString VExportDialog::s_lastOutputFolder;
 
-ExportFormat VExportDialog::s_lastExportFormat = ExportFormat::Markdown;
+ExportOption VExportDialog::s_opt;
 
 #define LOGERR(x) do { QString msg = (x); \
                        VUtils::addErrMsg(p_errMsg, msg); \
@@ -45,7 +47,8 @@ VExportDialog::VExportDialog(VNotebook *p_notebook,
       m_cart(p_cart),
       m_pageLayout(QPageLayout(QPageSize(QPageSize::A4),
                                QPageLayout::Portrait,
-                               QMarginsF(0.3, 0.3, 0.3, 0.3))),
+                               QMarginsF(10, 16, 10, 10),
+                               QPageLayout::Millimeter)),
       m_inExport(false),
       m_askedToStop(false)
 {
@@ -56,6 +59,10 @@ VExportDialog::VExportDialog(VNotebook *p_notebook,
     setupUI();
 
     m_exporter = new VExporter(this);
+    connect(m_exporter, &VExporter::outputLog,
+            this, [this](const QString &p_log) {
+                appendLogLine(p_log);
+            });
 
     initUIFields(p_renderer);
 
@@ -68,6 +75,8 @@ void VExportDialog::setupUI()
     m_srcCB = VUtils::getComboBox();
     m_srcCB->setToolTip(tr("Choose notes to export"));
     m_srcCB->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
+    connect(m_srcCB, SIGNAL(currentIndexChanged(int)),
+            this, SLOT(handleCurrentSrcChanged(int)));
 
     // Target format.
     m_formatCB = VUtils::getComboBox();
@@ -97,12 +106,12 @@ void VExportDialog::setupUI()
     m_outputEdit = new VLineEdit(s_lastOutputFolder);
     connect(m_outputEdit, &QLineEdit::textChanged,
             this, &VExportDialog::handleInputChanged);
-    m_browseBtn = new QPushButton(tr("&Browse"));
-    connect(m_browseBtn, &QPushButton::clicked,
-            this, &VExportDialog::handleBrowseBtnClicked);
+    QPushButton *browseBtn = new QPushButton(tr("&Browse"));
+    connect(browseBtn, &QPushButton::clicked,
+            this, &VExportDialog::handleOutputBrowseBtnClicked);
     QHBoxLayout *outputLayout = new QHBoxLayout();
     outputLayout->addWidget(m_outputEdit);
-    outputLayout->addWidget(m_browseBtn);
+    outputLayout->addWidget(browseBtn);
 
     m_basicBox = new QGroupBox(tr("Information"));
 
@@ -150,10 +159,12 @@ void VExportDialog::setupUI()
     m_basicBox->setLayout(basicLayout);
 
     // Settings box.
+    m_generalSettings = setupGeneralAdvancedSettings();
     m_htmlSettings = setupHTMLAdvancedSettings();
     m_pdfSettings = setupPDFAdvancedSettings();
 
     QVBoxLayout *advLayout = new QVBoxLayout();
+    advLayout->addWidget(m_generalSettings);
     advLayout->addWidget(m_htmlSettings);
     advLayout->addWidget(m_pdfSettings);
 
@@ -190,8 +201,75 @@ QWidget *VExportDialog::setupPDFAdvancedSettings()
     layoutLayout->addWidget(layoutBtn);
     layoutLayout->addStretch();
 
+    // Use wkhtmltopdf.
+    m_wkhtmltopdfCB = new QCheckBox(tr("Use wkhtmltopdf"));
+    m_wkhtmltopdfCB->setToolTip(tr("Use wkhtmltopdf tool to generate PDF (wkhtmltopdf needed to be installed)"));
+    connect(m_wkhtmltopdfCB, &QCheckBox::stateChanged,
+            this, [this](int p_state) {
+                bool checked = p_state == Qt::Checked;
+                m_wkPathEdit->setEnabled(checked);
+                m_wkPathBrowseBtn->setEnabled(checked);
+                m_wkBackgroundCB->setEnabled(checked);
+                m_wkTableOfContentsCB->setEnabled(checked);
+                m_wkPageNumberCB->setEnabled(checked);
+                m_wkExtraArgsEdit->setEnabled(checked);
+            });
+
+    QPushButton *wkBtn = new QPushButton(tr("Download wkhtmltopdf"));
+    connect(wkBtn, &QPushButton::clicked,
+            this, [this]() {
+                QString url("https://wkhtmltopdf.org/downloads.html");
+                QDesktopServices::openUrl(QUrl(url));
+            });
+
+    QHBoxLayout *wkLayout = new QHBoxLayout();
+    wkLayout->addWidget(m_wkhtmltopdfCB);
+    wkLayout->addStretch();
+    wkLayout->addWidget(wkBtn);
+
+    // wkhtmltopdf Path.
+    m_wkPathEdit = new VLineEdit();
+    m_wkPathEdit->setToolTip(tr("Tell VNote where to find wkhtmlpdf tool"));
+    m_wkPathEdit->setEnabled(m_wkhtmltopdfCB->isChecked());
+
+    m_wkPathBrowseBtn = new QPushButton(tr("&Browse"));
+    m_wkPathBrowseBtn->setEnabled(m_wkhtmltopdfCB->isChecked());
+    connect(m_wkPathBrowseBtn, &QPushButton::clicked,
+            this, &VExportDialog::handleWkPathBrowseBtnClicked);
+
+    QHBoxLayout *wkPathLayout = new QHBoxLayout();
+    wkPathLayout->addWidget(m_wkPathEdit);
+    wkPathLayout->addWidget(m_wkPathBrowseBtn);
+
+    // wkhtmltopdf enable background.
+    m_wkBackgroundCB = new QCheckBox(tr("Enable background"));
+    m_wkBackgroundCB->setToolTip(tr("Enable background when printing"));
+    m_wkBackgroundCB->setEnabled(m_wkhtmltopdfCB->isChecked());
+
+    // wkhtmltopdf enable table of contents.
+    m_wkTableOfContentsCB = new QCheckBox(tr("Enable Table Of Contents"));
+    m_wkTableOfContentsCB->setToolTip(tr("Add a table of contents to the document"));
+    m_wkTableOfContentsCB->setEnabled(m_wkhtmltopdfCB->isChecked());
+
+    // wkhtmltopdf page number.
+    m_wkPageNumberCB = VUtils::getComboBox();
+    m_wkPageNumberCB->setToolTip(tr("Append page number as footer"));
+    m_wkPageNumberCB->setEnabled(m_wkhtmltopdfCB->isChecked());
+
+    // wkhtmltopdf extra argumnets.
+    m_wkExtraArgsEdit = new VLineEdit();
+    m_wkExtraArgsEdit->setToolTip(tr("Additional arguments passed to wkhtmltopdf"));
+    m_wkExtraArgsEdit->setPlaceholderText(tr("Use \" to enclose arguments containing space"));
+    m_wkExtraArgsEdit->setEnabled(m_wkhtmltopdfCB->isChecked());
+
     QFormLayout *advLayout = new QFormLayout();
     advLayout->addRow(tr("Page layout:"), layoutLayout);
+    advLayout->addRow(wkLayout);
+    advLayout->addRow(tr("wkhtmltopdf path:"), wkPathLayout);
+    advLayout->addRow(m_wkBackgroundCB);
+    advLayout->addRow(m_wkTableOfContentsCB);
+    advLayout->addRow(tr("Page number:"), m_wkPageNumberCB);
+    advLayout->addRow(tr("Additional arguments:"), m_wkExtraArgsEdit);
 
     advLayout->setContentsMargins(0, 0, 0, 0);
 
@@ -206,14 +284,12 @@ QWidget *VExportDialog::setupHTMLAdvancedSettings()
     // Embed CSS styles.
     m_embedStyleCB = new QCheckBox(tr("Embed CSS styles"), this);
     m_embedStyleCB->setToolTip(tr("Embed CSS styles in HTML file"));
-    m_embedStyleCB->setChecked(true);
 
     // Complete HTML.
     m_completeHTMLCB = new QCheckBox(tr("Complete page"), this);
     m_completeHTMLCB->setToolTip(tr("Export the whole web page along with pictures "
                                     "which may not keep the HTML link structure of "
                                     "the original page"));
-    m_completeHTMLCB->setChecked(true);
 
     // Mime HTML.
     m_mimeHTMLCB = new QCheckBox(tr("MIME HTML"), this);
@@ -238,6 +314,24 @@ QWidget *VExportDialog::setupHTMLAdvancedSettings()
     return wid;
 }
 
+QWidget *VExportDialog::setupGeneralAdvancedSettings()
+{
+    // Include subfolders.
+    m_subfolderCB = new QCheckBox(tr("Process subfolders"));
+    m_subfolderCB->setToolTip(tr("Process subfolders recursively"));
+    m_subfolderCB->setChecked(true);
+
+    QFormLayout *advLayout = new QFormLayout();
+    advLayout->addRow(m_subfolderCB);
+
+    advLayout->setContentsMargins(0, 0, 0, 0);
+
+    QWidget *wid = new QWidget();
+    wid->setLayout(advLayout);
+
+    return wid;
+}
+
 void VExportDialog::initUIFields(MarkdownConverterType p_renderer)
 {
     // Notes to export.
@@ -248,7 +342,7 @@ void VExportDialog::initUIFields(MarkdownConverterType p_renderer)
 
     if (m_directory) {
         m_srcCB->addItem(tr("Current Folder (%1)").arg(m_directory->getName()),
-                         (int)ExportSource::CurrentDirectory);
+                         (int)ExportSource::CurrentFolder);
     }
 
     if (m_notebook) {
@@ -265,7 +359,7 @@ void VExportDialog::initUIFields(MarkdownConverterType p_renderer)
     m_formatCB->addItem(tr("Markdown"), (int)ExportFormat::Markdown);
     m_formatCB->addItem(tr("HTML"), (int)ExportFormat::HTML);
     m_formatCB->addItem(tr("PDF"), (int)ExportFormat::PDF);
-    m_formatCB->setCurrentIndex(m_formatCB->findData((int)s_lastExportFormat));
+    m_formatCB->setCurrentIndex(m_formatCB->findData((int)s_opt.m_format));
 
     // Markdown renderer.
     m_rendererCB->addItem(tr("Hoedown"), MarkdownConverterType::Hoedown);
@@ -281,8 +375,11 @@ void VExportDialog::initUIFields(MarkdownConverterType p_renderer)
         m_renderBgCB->addItem(bgColors[i].m_name, bgColors[i].m_name);
     }
 
-    m_renderBgCB->setCurrentIndex(
-        m_renderBgCB->findData(g_config->getCurRenderBackgroundColor()));
+    if (s_opt.m_renderBg.isEmpty()) {
+        s_opt.m_renderBg = g_config->getCurRenderBackgroundColor();
+    }
+
+    m_renderBgCB->setCurrentIndex(m_renderBgCB->findData(s_opt.m_renderBg));
 
     // Markdown rendering style.
     QList<QString> styles = g_config->getCssStyles();
@@ -301,6 +398,56 @@ void VExportDialog::initUIFields(MarkdownConverterType p_renderer)
 
     m_renderCodeBlockStyleCB->setCurrentIndex(
         m_renderCodeBlockStyleCB->findData(g_config->getCodeBlockCssStyle()));
+
+    m_embedStyleCB->setChecked(s_opt.m_htmlOpt.m_embedCssStyle);
+
+    m_completeHTMLCB->setChecked(s_opt.m_htmlOpt.m_completeHTML);
+
+    m_mimeHTMLCB->setChecked(s_opt.m_htmlOpt.m_mimeHTML);
+
+    m_wkhtmltopdfCB->setChecked(s_opt.m_pdfOpt.m_wkhtmltopdf);
+
+    // wkhtmltopdf path.
+    m_wkPathEdit->setText(g_config->getWkhtmltopdfPath());
+
+    m_wkBackgroundCB->setChecked(s_opt.m_pdfOpt.m_wkEnableBackground);
+
+    m_wkTableOfContentsCB->setChecked(s_opt.m_pdfOpt.m_wkEnableTableOfContents);
+
+    // wkhtmltopdf page number.
+    m_wkPageNumberCB->addItem(tr("None"), (int)ExportPageNumber::None);
+    m_wkPageNumberCB->addItem(tr("Left"), (int)ExportPageNumber::Left);
+    m_wkPageNumberCB->addItem(tr("Center"), (int)ExportPageNumber::Center);
+    m_wkPageNumberCB->addItem(tr("Right"), (int)ExportPageNumber::Right);
+    m_wkPageNumberCB->setCurrentIndex(m_wkPageNumberCB->findData((int)s_opt.m_pdfOpt.m_wkPageNumber));
+
+    m_wkExtraArgsEdit->setText(g_config->getWkhtmltopdfArgs());
+}
+
+bool VExportDialog::checkWkhtmltopdfExecutable(const QString &p_file)
+{
+    QStringList args;
+    args << "--version";
+    int ret = QProcess::execute(p_file, args);
+    switch (ret) {
+    case -2:
+        appendLogLine(tr("Fail to start wkhtmltopdf."));
+        break;
+
+    case -1:
+        appendLogLine(tr("wkhtmltopdf crashed."));
+        break;
+
+    case 0:
+        appendLogLine(tr("Use %1.").arg(p_file));
+        break;
+
+    default:
+        appendLogLine(tr("wkhtmltopdf returned %1.").arg(ret));
+        break;
+    }
+
+    return ret == 0;
 }
 
 void VExportDialog::startExport()
@@ -315,45 +462,62 @@ void VExportDialog::startExport()
 
     QString outputFolder = QDir::cleanPath(QDir(getOutputDirectory()).absolutePath());
 
-    ExportOption opt((ExportSource)m_srcCB->currentData().toInt(),
-                     (ExportFormat)m_formatCB->currentData().toInt(),
-                     (MarkdownConverterType)m_rendererCB->currentData().toInt(),
-                     m_renderBgCB->currentData().toString(),
-                     m_renderStyleCB->currentData().toString(),
-                     m_renderCodeBlockStyleCB->currentData().toString(),
-                     &m_pageLayout,
-                     ExportHTMLOption(m_embedStyleCB->isChecked(),
-                                      m_completeHTMLCB->isChecked(),
-                                      m_mimeHTMLCB->isChecked()));
-
-    s_lastExportFormat = opt.m_format;
+    s_opt = ExportOption((ExportSource)m_srcCB->currentData().toInt(),
+                         (ExportFormat)m_formatCB->currentData().toInt(),
+                         (MarkdownConverterType)m_rendererCB->currentData().toInt(),
+                         m_renderBgCB->currentData().toString(),
+                         m_renderStyleCB->currentData().toString(),
+                         m_renderCodeBlockStyleCB->currentData().toString(),
+                         m_subfolderCB->isChecked(),
+                         ExportPDFOption(&m_pageLayout,
+                                         m_wkhtmltopdfCB->isChecked(),
+                                         QDir::toNativeSeparators(m_wkPathEdit->text()),
+                                         m_wkBackgroundCB->isChecked(),
+                                         m_wkTableOfContentsCB->isChecked(),
+                                         (ExportPageNumber)m_wkPageNumberCB->currentData().toInt(),
+                                         m_wkExtraArgsEdit->text()),
+                         ExportHTMLOption(m_embedStyleCB->isChecked(),
+                                          m_completeHTMLCB->isChecked(),
+                                          m_mimeHTMLCB->isChecked()));
 
     m_consoleEdit->clear();
     appendLogLine(tr("Export to %1.").arg(outputFolder));
 
-    if (opt.m_format == ExportFormat::PDF
-        || opt.m_format == ExportFormat::HTML) {
-        m_exporter->prepareExport(opt);
+    if (s_opt.m_format == ExportFormat::PDF
+        || s_opt.m_format == ExportFormat::HTML) {
+        m_exporter->prepareExport(s_opt);
+
+        if (s_opt.m_format == ExportFormat::PDF
+            && s_opt.m_pdfOpt.m_wkhtmltopdf) {
+            g_config->setWkhtmltopdfPath(s_opt.m_pdfOpt.m_wkPath);
+            g_config->setWkhtmltopdfArgs(s_opt.m_pdfOpt.m_wkExtraArgs);
+
+            if (!checkWkhtmltopdfExecutable(s_opt.m_pdfOpt.m_wkPath)) {
+                m_inExport = false;
+                m_exportBtn->setEnabled(true);
+                return;
+            }
+        }
     }
 
     int ret = 0;
     QString msg;
 
-    switch ((int)opt.m_source) {
-    case (int)ExportSource::CurrentNote:
-        ret = doExport(m_file, opt, outputFolder, &msg);
+    switch (s_opt.m_source) {
+    case ExportSource::CurrentNote:
+        ret = doExport(m_file, s_opt, outputFolder, &msg);
         break;
 
-    case (int)ExportSource::CurrentDirectory:
-        ret = doExport(m_directory, opt, outputFolder, &msg);
+    case ExportSource::CurrentFolder:
+        ret = doExport(m_directory, s_opt, outputFolder, &msg);
         break;
 
-    case (int)ExportSource::CurrentNotebook:
-        ret = doExport(m_notebook, opt, outputFolder, &msg);
+    case ExportSource::CurrentNotebook:
+        ret = doExport(m_notebook, s_opt, outputFolder, &msg);
         break;
 
-    case (int)ExportSource::Cart:
-        ret = doExport(m_cart, opt, outputFolder, &msg);
+    case ExportSource::Cart:
+        ret = doExport(m_cart, s_opt, outputFolder, &msg);
         break;
 
     default:
@@ -386,7 +550,7 @@ QString VExportDialog::getOutputDirectory() const
     return m_outputEdit->text();
 }
 
-void VExportDialog::handleBrowseBtnClicked()
+void VExportDialog::handleOutputBrowseBtnClicked()
 {
     QString initPath = getOutputDirectory();
     if (!QFileInfo::exists(initPath)) {
@@ -405,6 +569,29 @@ void VExportDialog::handleBrowseBtnClicked()
     }
 }
 
+void VExportDialog::handleWkPathBrowseBtnClicked()
+{
+    QString initPath = m_wkPathEdit->text();
+    if (!QFileInfo::exists(initPath)) {
+        initPath = QCoreApplication::applicationDirPath();
+    }
+
+#if defined(Q_OS_WIN)
+    QString filter = tr("Executable (*.exe)");
+#else
+    QString filter;
+#endif
+
+    QString filePath = QFileDialog::getOpenFileName(this,
+                                                    tr("Select wkhtmltopdf Executable"),
+                                                    initPath,
+                                                    filter);
+
+    if (!filePath.isEmpty()) {
+        m_wkPathEdit->setText(filePath);
+    }
+}
+
 void VExportDialog::handleInputChanged()
 {
     // Source.
@@ -492,13 +679,15 @@ int VExportDialog::doExport(VDirectory *p_directory,
         ret += doExport(file, p_opt, outputPath, p_errMsg);
     }
 
-    // Export subfolder.
-    for (auto const & dir : p_directory->getSubDirs()) {
-        if (!checkUserAction()) {
-            goto exit;
-        }
+    // Export subfolders.
+    if (p_opt.m_processSubfolders) {
+        for (auto const & dir : p_directory->getSubDirs()) {
+            if (!checkUserAction()) {
+                goto exit;
+            }
 
-        ret += doExport(dir, p_opt, outputPath, p_errMsg);
+            ret += doExport(dir, p_opt, outputPath, p_errMsg);
+        }
     }
 
 exit:
@@ -731,8 +920,9 @@ void VExportDialog::handleLayoutBtnClicked()
         return;
     }
 
+    m_pageLayout.setUnits(QPageLayout::Millimeter);
     m_pageLayout.setPageSize(printer.pageLayout().pageSize());
-    m_pageLayout.setMargins(printer.pageLayout().margins());
+    m_pageLayout.setMargins(printer.pageLayout().margins(QPageLayout::Millimeter));
     m_pageLayout.setOrientation(printer.pageLayout().orientation());
 
     updatePageLayoutLabel();
@@ -741,8 +931,6 @@ void VExportDialog::handleLayoutBtnClicked()
 
 void VExportDialog::updatePageLayoutLabel()
 {
-    qDebug() << "page layout margins:" << m_pageLayout.margins();
-
     m_layoutLabel->setText(QString("%1, %2").arg(m_pageLayout.pageSize().name())
                                             .arg(m_pageLayout.orientation() == QPageLayout::Portrait ?
                                                  tr("Portrait") : tr("Landscape")));
@@ -771,3 +959,21 @@ void VExportDialog::handleCurrentFormatChanged(int p_index)
     m_pdfSettings->setVisible(pdfEnabled);
     m_htmlSettings->setVisible(htmlEnabled);
 }
+
+void VExportDialog::handleCurrentSrcChanged(int p_index)
+{
+    bool subfolderEnabled = false;
+
+    if (p_index >= 0) {
+        switch (m_srcCB->currentData().toInt()) {
+        case (int)ExportSource::CurrentFolder:
+            subfolderEnabled = true;
+            break;
+
+        default:
+            break;
+        }
+    }
+
+    m_subfolderCB->setVisible(subfolderEnabled);
+}

+ 101 - 9
src/dialog/vexportdialog.h

@@ -19,12 +19,13 @@ class VFile;
 class VCart;
 class VExporter;
 class QCheckBox;
+class VLineEdit;
 
 
 enum class ExportSource
 {
     CurrentNote = 0,
-    CurrentDirectory,
+    CurrentFolder,
     CurrentNotebook,
     Cart
 };
@@ -38,8 +39,24 @@ enum class ExportFormat
 };
 
 
+enum class ExportPageNumber
+{
+    None = 0,
+    Left,
+    Center,
+    Right
+};
+
+
 struct ExportHTMLOption
 {
+    ExportHTMLOption()
+        : m_embedCssStyle(true),
+          m_completeHTML(true),
+          m_mimeHTML(false)
+    {
+    }
+
     ExportHTMLOption(bool p_embedCssStyle,
                      bool p_completeHTML,
                      bool p_mimeHTML)
@@ -55,15 +72,62 @@ struct ExportHTMLOption
 };
 
 
+struct ExportPDFOption
+{
+    ExportPDFOption()
+        : m_layout(NULL),
+          m_wkhtmltopdf(false),
+          m_wkEnableBackground(true),
+          m_wkEnableTableOfContents(false),
+          m_wkPageNumber(ExportPageNumber::None)
+    {
+    }
+
+    ExportPDFOption(QPageLayout *p_layout,
+                    bool p_wkhtmltopdf,
+                    const QString &p_wkPath,
+                    bool p_wkEnableBackground,
+                    bool p_wkEnableTableOfContents,
+                    ExportPageNumber p_wkPageNumber,
+                    const QString &p_wkExtraArgs)
+        : m_layout(p_layout),
+          m_wkhtmltopdf(p_wkhtmltopdf),
+          m_wkPath(p_wkPath),
+          m_wkEnableBackground(p_wkEnableBackground),
+          m_wkEnableTableOfContents(p_wkEnableTableOfContents),
+          m_wkPageNumber(p_wkPageNumber),
+          m_wkExtraArgs(p_wkExtraArgs)
+    {
+    }
+
+    QPageLayout *m_layout;
+    bool m_wkhtmltopdf;
+    QString m_wkPath;
+    bool m_wkEnableBackground;
+    bool m_wkEnableTableOfContents;
+    ExportPageNumber m_wkPageNumber;
+    QString m_wkExtraArgs;
+};
+
+
 struct ExportOption
 {
+    ExportOption()
+        : m_source(ExportSource::CurrentNote),
+          m_format(ExportFormat::Markdown),
+          m_renderer(MarkdownConverterType::MarkdownIt),
+          m_processSubfolders(false)
+    {
+    }
+
     ExportOption(ExportSource p_source,
                  ExportFormat p_format,
                  MarkdownConverterType p_renderer,
                  const QString &p_renderBg,
                  const QString &p_renderStyle,
                  const QString &p_renderCodeBlockStyle,
-                 QPageLayout *p_layout,
+                 bool p_processSubfolders,
+                 const ExportPDFOption &p_pdfOpt,
                  const ExportHTMLOption &p_htmlOpt)
         : m_source(p_source),
           m_format(p_format),
@@ -71,7 +135,8 @@ struct ExportOption
           m_renderBg(p_renderBg),
           m_renderStyle(p_renderStyle),
           m_renderCodeBlockStyle(p_renderCodeBlockStyle),
-          m_layout(p_layout),
+          m_processSubfolders(p_processSubfolders),
+          m_pdfOpt(p_pdfOpt),
           m_htmlOpt(p_htmlOpt)
     {
     }
@@ -85,7 +150,11 @@ struct ExportOption
 
     QString m_renderStyle;
     QString m_renderCodeBlockStyle;
-    QPageLayout *m_layout;
+
+    // Whether process subfolders recursively when source is CurrentFolder.
+    bool m_processSubfolders;
+
+    ExportPDFOption m_pdfOpt;
 
     ExportHTMLOption m_htmlOpt;
 };
@@ -105,7 +174,9 @@ public:
 private slots:
     void startExport();
 
-    void handleBrowseBtnClicked();
+    void handleOutputBrowseBtnClicked();
+
+    void handleWkPathBrowseBtnClicked();
 
     void handleInputChanged();
 
@@ -113,6 +184,8 @@ private slots:
 
     void handleCurrentFormatChanged(int p_index);
 
+    void handleCurrentSrcChanged(int p_index);
+
 private:
     void setupUI();
 
@@ -120,6 +193,8 @@ private:
 
     QWidget *setupHTMLAdvancedSettings();
 
+    QWidget *setupGeneralAdvancedSettings();
+
     void initUIFields(MarkdownConverterType p_renderer);
 
     QString getOutputDirectory() const;
@@ -167,6 +242,8 @@ private:
 
     void updatePageLayoutLabel();
 
+    bool checkWkhtmltopdfExecutable(const QString &p_file);
+
     QComboBox *m_srcCB;
 
     QComboBox *m_formatCB;
@@ -181,8 +258,6 @@ private:
 
     VLineEdit *m_outputEdit;
 
-    QPushButton *m_browseBtn;
-
     QGroupBox *m_basicBox;
 
     QGroupBox *m_settingBox;
@@ -191,6 +266,8 @@ private:
 
     QWidget *m_htmlSettings;
 
+    QWidget *m_generalSettings;
+
     QPlainTextEdit *m_consoleEdit;
 
     QDialogButtonBox *m_btnBox;
@@ -201,12 +278,28 @@ private:
 
     QLabel *m_layoutLabel;
 
+    QCheckBox *m_wkhtmltopdfCB;
+
+    VLineEdit *m_wkPathEdit;
+
+    QPushButton *m_wkPathBrowseBtn;
+
+    QCheckBox *m_wkBackgroundCB;
+
+    QCheckBox *m_wkTableOfContentsCB;
+
+    QComboBox *m_wkPageNumberCB;
+
+    VLineEdit *m_wkExtraArgsEdit;
+
     QCheckBox *m_embedStyleCB;
 
     QCheckBox *m_completeHTMLCB;;
 
     QCheckBox *m_mimeHTMLCB;
 
+    QCheckBox *m_subfolderCB;
+
     VNotebook *m_notebook;
 
     VDirectory *m_directory;
@@ -229,8 +322,7 @@ private:
     // Last output folder path.
     static QString s_lastOutputFolder;
 
-    // Last export format.
-    static ExportFormat s_lastExportFormat;
+    static ExportOption s_opt;
 };
 
 #endif // VEXPORTDIALOG_H

+ 1 - 1
src/resources/export_template.html

@@ -1,5 +1,5 @@
 <!doctype html>
-<html lang="en">
+<html>
 <meta charset="utf-8">
 <head>
     <style type="text/css">

+ 1 - 1
src/resources/markdown_template.html

@@ -1,5 +1,5 @@
 <!doctype html>
-<html lang="en">
+<html>
 <meta charset="utf-8">
 <head>
     <style type="text/css">

+ 1 - 1
src/resources/simple_template.html

@@ -1,5 +1,5 @@
 <!doctype html>
-<html lang="en">
+<html>
 <meta charset="utf-8">
 <head>
     <link rel="stylesheet" type="text/css" href="CSS_PLACE_HOLDER">

+ 9 - 0
src/resources/themes/v_moonlight/arrow_dropdown_disabled.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<g>
+	<polygon fill="#4D5765" points="128,192 256,320 384,192 	"/>
+</g>
+</svg>

+ 2 - 0
src/resources/themes/v_moonlight/v_moonlight.palette

@@ -226,6 +226,7 @@ button_icon_danger_fg=@danger_icon_fg
 combobox_border=@border_bg
 combobox_fg=@content_fg
 combobox_bg=@content_bg
+combobox_disabled_fg=@disabled_fg
 combobox_view_border=@border_bg
 combobox_view_selected_bg=@selected_bg
 combobox_view_selected_fg=@selected_fg
@@ -252,6 +253,7 @@ label_titlelabel_bg=@title_bg
 lineedit_border=@border_bg
 lineedit_fg=@edit_fg
 lineedit_bg=@edit_bg
+lineedit_disabled_fg=@disabled_fg
 lineedit_focus_bg=@edit_focus_bg
 lineedit_focus_border=@edit_focus_border
 lineedit_selection_fg=@edit_selection_fg

+ 14 - 0
src/resources/themes/v_moonlight/v_moonlight.qss

@@ -521,6 +521,10 @@ QComboBox {
     border: 1px solid @combobox_border;
 }
 
+QComboBox:disabled {
+    color: @combobox_disabled_fg;
+}
+
 QComboBox:focus, QComboBox:on {
     background-color: @combobox_focus_bg;
     border: 2px solid @combobox_focus_border;
@@ -540,6 +544,12 @@ QComboBox::down-arrow {
     height: 20px;
 }
 
+QComboBox::down-arrow:disabled {
+    image: url(arrow_dropdown_disabled.svg);
+    width: 20px;
+    height: 20px;
+}
+
 QComboBox QAbstractItemView {
     padding: 2px;
     border: 1px solid @combobox_view_border;
@@ -697,6 +707,10 @@ QLineEdit:focus {
     background: @lineedit_focus_bg;
 }
 
+QLineEdit:disabled {
+    color: @lineedit_disabled_fg;
+}
+
 QLineEdit[VimCommandLine="true"] {
     padding: 0px;
     margin: 0px;

+ 9 - 0
src/resources/themes/v_pure/arrow_dropdown_disabled.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<g>
+	<polygon fill="#C0C0C0" points="128,192 256,320 384,192 	"/>
+</g>
+</svg>

+ 2 - 0
src/resources/themes/v_pure/v_pure.palette

@@ -220,6 +220,7 @@ buttonmenuitem_decoration_text_fg=@master_dark_bg
 combobox_border=@border_bg
 combobox_fg=@content_fg
 combobox_bg=@content_bg
+combobox_disabled_fg=@disabled_fg
 combobox_view_border=@border_bg
 combobox_view_selected_bg=@selected_bg
 combobox_view_selected_fg=@selected_fg
@@ -246,6 +247,7 @@ label_titlelabel_bg=@title_bg
 lineedit_border=@border_bg
 lineedit_fg=@edit_fg
 lineedit_bg=@edit_bg
+lineedit_disabled_fg=@disabled_fg
 lineedit_focus_bg=@edit_focus_bg
 lineedit_focus_border=@edit_focus_border
 lineedit_selection_fg=@edit_selection_fg

+ 14 - 0
src/resources/themes/v_pure/v_pure.qss

@@ -521,6 +521,10 @@ QComboBox {
     border: 1px solid @combobox_border;
 }
 
+QComboBox:disabled {
+    color: @combobox_disabled_fg;
+}
+
 QComboBox:focus, QComboBox:on {
     background-color: @combobox_focus_bg;
     border: 2px solid @combobox_focus_border;
@@ -540,6 +544,12 @@ QComboBox::down-arrow {
     height: 20px;
 }
 
+QComboBox::down-arrow:disabled {
+    image: url(arrow_dropdown_disabled.svg);
+    width: 20px;
+    height: 20px;
+}
+
 QComboBox QAbstractItemView {
     padding: 2px;
     border: 1px solid @combobox_view_border;
@@ -697,6 +707,10 @@ QLineEdit:focus {
     background: @lineedit_focus_bg;
 }
 
+QLineEdit:disabled {
+    color: @lineedit_disabled_fg;
+}
+
 QLineEdit[VimCommandLine="true"] {
     padding: 0px;
     margin: 0px;

+ 1 - 1
src/resources/themes/v_white/arrow_dropdown.svg

@@ -4,6 +4,6 @@
 <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
 <g>
-	<polygon points="128,192 256,320 384,192 	"/>
+	<polygon points="128,192 256,320 384,192 	" fill="#333333"/>
 </g>
 </svg>

+ 9 - 0
src/resources/themes/v_white/arrow_dropdown_disabled.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<g>
+	<polygon points="128,192 256,320 384,192 	" fill="#C0C0C0"/>
+</g>
+</svg>

+ 2 - 0
src/resources/themes/v_white/v_white.palette

@@ -193,6 +193,7 @@ buttonmenuitem_decoration_text_fg=#00796B
 combobox_border=@border_bg
 combobox_fg=@content_fg
 combobox_bg=@content_bg
+combobox_disabled_fg=@disabled_fg
 combobox_view_border=@border_bg
 combobox_view_selected_bg=@selected_bg
 combobox_view_selected_fg=@selected_fg
@@ -210,6 +211,7 @@ label_titlelabel_bg=@title_bg
 lineedit_border=@border_bg
 lineedit_fg=@content_fg
 lineedit_bg=@content_bg
+lineedit_disabled_fg=@disabled_fg
 lineedit_selection_fg=@selection_fg
 lineedit_selection_bg=@selection_bg
 

+ 14 - 0
src/resources/themes/v_white/v_white.qss

@@ -453,6 +453,10 @@ QComboBox {
     border: 1px solid @combobox_border;
 }
 
+QComboBox:disabled {
+    color: @combobox_disabled_fg;
+}
+
 QComboBox:focus {
     background-color: @combobox_focus_bg;
 }
@@ -471,6 +475,12 @@ QComboBox::down-arrow {
     height: 20px;
 }
 
+QComboBox::down-arrow:disabled {
+    image: url(arrow_dropdown_disabled.svg);
+    width: 20px;
+    height: 20px;
+}
+
 QComboBox QAbstractItemView {
     padding: 2px;
     border: 1px solid @combobox_view_border;
@@ -602,6 +612,10 @@ QLineEdit {
     selection-background-color: @lineedit_selection_bg;
 }
 
+QLineEdit:disabled {
+    color: @lineedit_disabled_fg;
+}
+
 QLineEdit[VimCommandLine="true"] {
     padding: 0px;
     margin: 0px;

+ 8 - 0
src/resources/vnote.ini

@@ -201,6 +201,14 @@ single_click_close_previous_tab=true
 ; Whether enable auto wildcard match in simple search like list and tree widgets
 enable_wildcard_in_simple_search=true
 
+[export]
+; Path of the wkhtmltopdf tool
+wkhtmltopdf=wkhtmltopdf
+
+; Additional arguments to wkhtmltopdf
+; Double quotes to enclose arguments with spaces
+wkhtmltopdfArgs=
+
 [web]
 ; Location and configuration for Mathjax
 mathjax_javascript=https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_HTMLorMML

+ 28 - 0
src/vconfigmanager.h

@@ -454,6 +454,12 @@ public:
     bool getEnableAutoSave() const;
     void setEnableAutoSave(bool p_enabled);
 
+    QString getWkhtmltopdfPath() const;
+    void setWkhtmltopdfPath(const QString &p_path);
+
+    QString getWkhtmltopdfArgs() const;
+    void setWkhtmltopdfArgs(const QString &p_args);
+
 private:
     // Look up a config from user and default settings.
     QVariant getConfigFromSettings(const QString &section, const QString &key) const;
@@ -2083,4 +2089,26 @@ inline void VConfigManager::setEnableAutoSave(bool p_enabled)
 {
     setConfigToSettings("global", "enable_auto_save", p_enabled);
 }
+
+inline QString VConfigManager::getWkhtmltopdfPath() const
+{
+    return getConfigFromSettings("export",
+                                 "wkhtmltopdf").toString();
+}
+
+inline void VConfigManager::setWkhtmltopdfPath(const QString &p_file)
+{
+    setConfigToSettings("export", "wkhtmltopdf", p_file);
+}
+
+inline QString VConfigManager::getWkhtmltopdfArgs() const
+{
+    return getConfigFromSettings("export",
+                                 "wkhtmltopdfArgs").toString();
+}
+
+inline void VConfigManager::setWkhtmltopdfArgs(const QString &p_file)
+{
+    setConfigToSettings("export", "wkhtmltopdfArgs", p_file);
+}
 #endif // VCONFIGMANAGER_H

+ 238 - 5
src/vexporter.cpp

@@ -5,6 +5,7 @@
 #include <QWebChannel>
 #include <QWebEngineProfile>
 #include <QRegExp>
+#include <QProcess>
 
 #include "vconfigmanager.h"
 #include "vfile.h"
@@ -27,6 +28,11 @@ VExporter::VExporter(QWidget *p_parent)
 {
 }
 
+static QString marginToStrMM(qreal p_margin)
+{
+    return QString("%1mm").arg(p_margin);
+}
+
 void VExporter::prepareExport(const ExportOption &p_opt)
 {
     m_htmlTemplate = VUtils::generateHtmlTemplate(p_opt.m_renderer,
@@ -37,7 +43,100 @@ void VExporter::prepareExport(const ExportOption &p_opt)
 
     m_exportHtmlTemplate = VUtils::generateExportHtmlTemplate(p_opt.m_renderBg);
 
-    m_pageLayout = *(p_opt.m_layout);
+    m_pageLayout = *(p_opt.m_pdfOpt.m_layout);
+
+    prepareWKArguments(p_opt.m_pdfOpt);
+}
+
+// From QProcess code.
+static QStringList parseCombinedArgString(const QString &program)
+{
+    QStringList args;
+    QString tmp;
+    int quoteCount = 0;
+    bool inQuote = false;
+
+    // handle quoting. tokens can be surrounded by double quotes
+    // "hello world". three consecutive double quotes represent
+    // the quote character itself.
+    for (int i = 0; i < program.size(); ++i) {
+        if (program.at(i) == QLatin1Char('"')) {
+            ++quoteCount;
+            if (quoteCount == 3) {
+                // third consecutive quote
+                quoteCount = 0;
+                tmp += program.at(i);
+            }
+            continue;
+        }
+        if (quoteCount) {
+            if (quoteCount == 1)
+                inQuote = !inQuote;
+            quoteCount = 0;
+        }
+        if (!inQuote && program.at(i).isSpace()) {
+            if (!tmp.isEmpty()) {
+                args += tmp;
+                tmp.clear();
+            }
+        } else {
+            tmp += program.at(i);
+        }
+    }
+    if (!tmp.isEmpty())
+        args += tmp;
+
+    return args;
+}
+
+void VExporter::prepareWKArguments(const ExportPDFOption &p_opt)
+{
+    m_wkArgs.clear();
+    m_wkArgs << "--quiet";
+    m_wkArgs << "--encoding" << "utf-8";
+    m_wkArgs << "--page-size" << m_pageLayout.pageSize().key();
+    m_wkArgs << "--orientation"
+             << (m_pageLayout.orientation() == QPageLayout::Portrait ? "Portrait" : "Landscape");
+
+    QMarginsF marginsMM = m_pageLayout.margins(QPageLayout::Millimeter);
+    m_wkArgs << "--margin-bottom" << marginToStrMM(marginsMM.bottom());
+    m_wkArgs << "--margin-left" << marginToStrMM(marginsMM.left());
+    m_wkArgs << "--margin-right" << marginToStrMM(marginsMM.right());
+    m_wkArgs << "--margin-top" << marginToStrMM(marginsMM.top());
+
+    m_wkArgs << (p_opt.m_wkEnableBackground ? "--background" : "--no-background");
+
+    QString footer;
+    switch (p_opt.m_wkPageNumber) {
+    case ExportPageNumber::Left:
+        footer = "--footer-left";
+        break;
+
+    case ExportPageNumber::Center:
+        footer = "--footer-center";
+        break;
+
+    case ExportPageNumber::Right:
+        footer = "--footer-right";
+        break;
+
+    default:
+        break;
+    }
+
+    if (!footer.isEmpty()) {
+        m_wkArgs << footer << "[page]"
+                 << "--footer-spacing" << QString::number(marginsMM.bottom() / 3, 'f', 2);
+    }
+
+    // Append additional arguments.
+    if (!p_opt.m_wkExtraArgs.isEmpty()) {
+        m_wkArgs.append(parseCombinedArgString(p_opt.m_wkExtraArgs));
+    }
+
+    if (p_opt.m_wkEnableTableOfContents) {
+        m_wkArgs << "toc" << "--toc-text-size-shrink" << "1.0";
+    }
 }
 
 bool VExporter::exportPDF(VFile *p_file,
@@ -149,6 +248,81 @@ bool VExporter::exportToPDF(VWebView *p_webViewer,
     return pdfPrinted == 1;
 }
 
+bool VExporter::exportToPDFViaWK(VDocument *p_webDocument,
+                                 const ExportPDFOption &p_opt,
+                                 const QString &p_filePath,
+                                 QString *p_errMsg)
+{
+    int pdfExported = 0;
+
+    connect(p_webDocument, &VDocument::htmlContentFinished,
+            this, [&, this](const QString &p_headContent,
+                            const QString &p_styleContent,
+                            const QString &p_bodyContent) {
+                if (p_bodyContent.isEmpty() || this->m_state == ExportState::Cancelled) {
+                    pdfExported = -1;
+                    return;
+                }
+
+                Q_ASSERT(!p_filePath.isEmpty());
+                QString htmlPath = p_filePath + ".vnote.html";
+
+                QFile file(htmlPath);
+                if (!file.open(QFile::WriteOnly)) {
+                    pdfExported = -1;
+                    return;
+                }
+
+                QString resFolder = QFileInfo(htmlPath).completeBaseName() + "_files";
+                QString resFolderPath = QDir(VUtils::basePathFromPath(htmlPath)).filePath(resFolder);
+
+                qDebug() << "temp HTML files folder" << resFolderPath;
+
+                QString html(m_exportHtmlTemplate);
+                if (!p_styleContent.isEmpty()) {
+                    QString content(p_styleContent);
+                    fixStyleResources(resFolderPath, content);
+                    html.replace(HtmlHolder::c_styleHolder, content);
+                }
+
+                if (!p_headContent.isEmpty()) {
+                    html.replace(HtmlHolder::c_headHolder, p_headContent);
+                }
+
+                QString content(p_bodyContent);
+                fixBodyResources(m_baseUrl, resFolderPath, content);
+                html.replace(HtmlHolder::c_bodyHolder, content);
+
+                file.write(html.toUtf8());
+                file.close();
+
+                // Convert vis wkhtmltopdf.
+                if (!htmlToPDFViaWK(htmlPath, p_filePath, p_opt, p_errMsg)) {
+                    pdfExported = -1;
+                }
+
+                // Clean up.
+                VUtils::deleteFile(htmlPath);
+                VUtils::deleteDirectory(resFolderPath);
+
+                if (pdfExported == 0) {
+                    pdfExported = 1;
+                }
+            });
+
+    p_webDocument->getHtmlContentAsync();
+
+    while (pdfExported == 0) {
+        VUtils::sleepWait(100);
+
+        if (m_state == ExportState::Cancelled) {
+            break;
+        }
+    }
+
+    return pdfExported == 1;
+}
+
 bool VExporter::exportViaWebView(VFile *p_file,
                                  const ExportOption &p_opt,
                                  const QString &p_outputFile,
@@ -195,9 +369,17 @@ bool VExporter::exportViaWebView(VFile *p_file,
     bool exportRet = false;
     switch (p_opt.m_format) {
     case ExportFormat::PDF:
-        exportRet = exportToPDF(m_webViewer,
-                                p_outputFile,
-                                m_pageLayout);
+        if (p_opt.m_pdfOpt.m_wkhtmltopdf) {
+            exportRet = exportToPDFViaWK(m_webDocument,
+                                         p_opt.m_pdfOpt,
+                                         p_outputFile,
+                                         p_errMsg);
+        } else {
+            exportRet = exportToPDF(m_webViewer,
+                                    p_outputFile,
+                                    m_pageLayout);
+        }
+
         break;
 
     case ExportFormat::HTML:
@@ -261,7 +443,6 @@ bool VExporter::exportToHTML(VDocument *p_webDocument,
                 Q_ASSERT(!p_filePath.isEmpty());
 
                 QFile file(p_filePath);
-
                 if (!file.open(QFile::WriteOnly)) {
                     htmlExported = -1;
                     return;
@@ -421,3 +602,55 @@ void VExporter::handleDownloadRequested(QWebEngineDownloadItem *p_item)
                 });
     }
 }
+
+static QString combineArgs(QStringList &p_args)
+{
+    QString str;
+    for (const QString &arg : p_args) {
+        QString tmp;
+        if (arg.contains(' ')) {
+            tmp = '"' + arg + '"';
+        } else {
+            tmp = arg;
+        }
+
+        if (str.isEmpty()) {
+            str = tmp;
+        } else {
+            str = str + ' ' + tmp;
+        }
+    }
+
+    return str;
+}
+
+bool VExporter::htmlToPDFViaWK(const QString &p_htmlFile,
+                               const QString &p_filePath,
+                               const ExportPDFOption &p_opt,
+                               QString *p_errMsg)
+{
+    // Note: system's locale settings (Language for non-Unicode programs) is important to wkhtmltopdf.
+    // Input file could be encoded via QUrl::fromLocalFile(p_htmlFile).toString(QUrl::EncodeUnicode) to
+    // handle non-ASCII path.
+    QStringList args(m_wkArgs);
+    args << QDir::toNativeSeparators(p_htmlFile);
+    args << QDir::toNativeSeparators(p_filePath);
+    QString cmd = p_opt.m_wkPath + " " + combineArgs(args);
+    emit outputLog(cmd);
+    int ret = QProcess::execute(p_opt.m_wkPath, args);
+    qDebug() << "wkhtmltopdf returned" << ret << cmd;
+    switch (ret) {
+    case -2:
+        VUtils::addErrMsg(p_errMsg, tr("Fail to start wkhtmltopdf (%1).").arg(cmd));
+        break;
+
+    case -1:
+        VUtils::addErrMsg(p_errMsg, tr("wkhtmltopdf crashed (%1).").arg(cmd));
+        break;
+
+    default:
+        break;
+    }
+
+    return ret == 0;
+}

+ 20 - 0
src/vexporter.h

@@ -5,6 +5,7 @@
 #include <QPageLayout>
 #include <QUrl>
 #include <QWebEngineDownloadItem>
+#include <QStringList>
 
 #include "dialog/vexportdialog.h"
 
@@ -14,6 +15,7 @@ class VDocument;
 
 class VExporter : public QObject
 {
+    Q_OBJECT
 public:
     explicit VExporter(QWidget *p_parent = nullptr);
 
@@ -29,6 +31,10 @@ public:
                     const QString &p_outputFile,
                     QString *p_errMsg = NULL);
 
+signals:
+    // Request to output log.
+    void outputLog(const QString &p_log);
+
 private slots:
     void handleLogicsFinished();
 
@@ -76,6 +82,11 @@ private:
                      const QString &p_filePath,
                      const QPageLayout &p_layout);
 
+    bool exportToPDFViaWK(VDocument *p_webDocument,
+                          const ExportPDFOption &p_opt,
+                          const QString &p_filePath,
+                          QString *p_errMsg = NULL);
+
     bool exportToHTML(VDocument *p_webDocument,
                       const ExportHTMLOption &p_opt,
                       const QString &p_filePath);
@@ -84,6 +95,12 @@ private:
                        const ExportHTMLOption &p_opt,
                        const QString &p_filePath);
 
+    bool htmlToPDFViaWK(const QString &p_htmlFile,
+                        const QString &p_filePath,
+                        const ExportPDFOption &p_opt,
+                        QString *p_errMsg = NULL);
+
+    void prepareWKArguments(const ExportPDFOption &p_opt);
 
     // Fix @p_html's resources like url("...") with "file" or "qrc" schema.
     // Copy the resource to @p_folder and fix the url string.
@@ -119,6 +136,9 @@ private:
 
     // Download state used for MIME HTML.
     QWebEngineDownloadItem::DownloadState m_downloadState;
+
+    // Arguments for wkhtmltopdf.
+    QStringList m_wkArgs;
 };
 
 inline void VExporter::clearNoteState()

+ 3 - 0
src/vnote.qrc

@@ -240,5 +240,8 @@
         <file>resources/themes/v_pure/v_pure_mermaid.css</file>
         <file>resources/themes/v_white/v_white_mermaid.css</file>
         <file>resources/themes/v_moonlight/v_moonlight_mermaid.css</file>
+        <file>resources/themes/v_moonlight/arrow_dropdown_disabled.svg</file>
+        <file>resources/themes/v_pure/arrow_dropdown_disabled.svg</file>
+        <file>resources/themes/v_white/arrow_dropdown_disabled.svg</file>
     </qresource>
 </RCC>