vnote/src/vexporter.cpp

657 lines
19 KiB
C++

#include "vexporter.h"
#include <QDebug>
#include <QWidget>
#include <QWebChannel>
#include <QWebEngineProfile>
#include <QRegExp>
#include <QProcess>
#include "vconfigmanager.h"
#include "vfile.h"
#include "vwebview.h"
#include "utils/vutils.h"
#include "vpreviewpage.h"
#include "vconstants.h"
#include "vmarkdownconverter.h"
#include "vdocument.h"
#include "utils/vwebutils.h"
extern VConfigManager *g_config;
extern VWebUtils *g_webUtils;
VExporter::VExporter(QWidget *p_parent)
: QObject(p_parent),
m_webViewer(NULL),
m_state(ExportState::Idle)
{
}
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,
p_opt.m_renderBg,
p_opt.m_renderStyle,
p_opt.m_renderCodeBlockStyle,
p_opt.m_format == ExportFormat::PDF);
m_exportHtmlTemplate = VUtils::generateExportHtmlTemplate(p_opt.m_renderBg);
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,
const ExportOption &p_opt,
const QString &p_outputFile,
QString *p_errMsg)
{
return exportViaWebView(p_file, p_opt, p_outputFile, p_errMsg);
}
bool VExporter::exportHTML(VFile *p_file,
const ExportOption &p_opt,
const QString &p_outputFile,
QString *p_errMsg)
{
return exportViaWebView(p_file, p_opt, p_outputFile, p_errMsg);
}
void VExporter::initWebViewer(VFile *p_file, const ExportOption &p_opt)
{
Q_ASSERT(!m_webViewer);
m_webViewer = new VWebView(p_file, static_cast<QWidget *>(parent()));
m_webViewer->hide();
VPreviewPage *page = new VPreviewPage(m_webViewer);
m_webViewer->setPage(page);
connect(page, &VPreviewPage::loadFinished,
this, &VExporter::handleLoadFinished);
connect(page->profile(), &QWebEngineProfile::downloadRequested,
this, &VExporter::handleDownloadRequested);
m_webDocument = new VDocument(p_file, m_webViewer);
connect(m_webDocument, &VDocument::logicsFinished,
this, &VExporter::handleLogicsFinished);
QWebChannel *channel = new QWebChannel(m_webViewer);
channel->registerObject(QStringLiteral("content"), m_webDocument);
page->setWebChannel(channel);
// Need to generate HTML using Hoedown.
if (p_opt.m_renderer == MarkdownConverterType::Hoedown) {
VMarkdownConverter mdConverter;
QString toc;
QString html = mdConverter.generateHtml(p_file->getContent(),
g_config->getMarkdownExtensions(),
toc);
m_webDocument->setHtml(html);
}
m_baseUrl = p_file->getBaseUrl();
m_webViewer->setHtml(m_htmlTemplate, m_baseUrl);
}
void VExporter::handleLogicsFinished()
{
Q_ASSERT(!(m_noteState & NoteState::WebLogicsReady));
m_noteState = NoteState(m_noteState | NoteState::WebLogicsReady);
}
void VExporter::handleLoadFinished(bool p_ok)
{
Q_ASSERT(!(m_noteState & NoteState::WebLoadFinished));
m_noteState = NoteState(m_noteState | NoteState::WebLoadFinished);
if (!p_ok) {
m_noteState = NoteState(m_noteState | NoteState::Failed);
}
}
void VExporter::clearWebViewer()
{
// m_webDocument will be freeed by QObject.
delete m_webViewer;
m_webViewer = NULL;
m_webDocument = NULL;
m_baseUrl.clear();
}
bool VExporter::exportToPDF(VWebView *p_webViewer,
const QString &p_filePath,
const QPageLayout &p_layout)
{
int pdfPrinted = 0;
p_webViewer->page()->printToPdf([&, this](const QByteArray &p_result) {
if (p_result.isEmpty() || this->m_state == ExportState::Cancelled) {
pdfPrinted = -1;
return;
}
V_ASSERT(!p_filePath.isEmpty());
if (!VUtils::writeFileToDisk(p_filePath, p_result)) {
pdfPrinted = -1;
return;
}
pdfPrinted = 1;
}, p_layout);
while (pdfPrinted == 0) {
VUtils::sleepWait(100);
if (m_state == ExportState::Cancelled) {
break;
}
}
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,
QString *p_errMsg)
{
Q_UNUSED(p_errMsg);
bool ret = false;
bool isOpened = p_file->isOpened();
if (!isOpened && !p_file->open()) {
goto exit;
}
Q_ASSERT(m_state == ExportState::Idle);
m_state = ExportState::Busy;
clearNoteState();
initWebViewer(p_file, p_opt);
while (!isNoteStateReady()) {
VUtils::sleepWait(100);
if (m_state == ExportState::Cancelled) {
goto exit;
}
if (isNoteStateFailed()) {
m_state = ExportState::Failed;
goto exit;
}
}
// Wait to ensure Web side is really ready.
VUtils::sleepWait(200);
if (m_state == ExportState::Cancelled) {
goto exit;
}
{
bool exportRet = false;
switch (p_opt.m_format) {
case ExportFormat::PDF:
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:
if (p_opt.m_htmlOpt.m_mimeHTML) {
exportRet = exportToMHTML(m_webViewer,
p_opt.m_htmlOpt,
p_outputFile);
} else {
exportRet = exportToHTML(m_webDocument,
p_opt.m_htmlOpt,
p_outputFile);
}
break;
default:
break;
}
clearNoteState();
if (!isOpened) {
p_file->close();
}
if (exportRet) {
m_state = ExportState::Successful;
} else {
m_state = ExportState::Failed;
}
}
exit:
clearWebViewer();
if (m_state == ExportState::Successful) {
ret = true;
}
m_state = ExportState::Idle;
return ret;
}
bool VExporter::exportToHTML(VDocument *p_webDocument,
const ExportHTMLOption &p_opt,
const QString &p_filePath)
{
int htmlExported = 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) {
htmlExported = -1;
return;
}
Q_ASSERT(!p_filePath.isEmpty());
QFile file(p_filePath);
if (!file.open(QFile::WriteOnly)) {
htmlExported = -1;
return;
}
QString resFolder = QFileInfo(p_filePath).completeBaseName() + "_files";
QString resFolderPath = QDir(VUtils::basePathFromPath(p_filePath)).filePath(resFolder);
qDebug() << "HTML files folder" << resFolderPath;
QString html(m_exportHtmlTemplate);
if (!p_styleContent.isEmpty() && p_opt.m_embedCssStyle) {
QString content(p_styleContent);
fixStyleResources(resFolderPath, content);
html.replace(HtmlHolder::c_styleHolder, content);
}
if (!p_headContent.isEmpty()) {
html.replace(HtmlHolder::c_headHolder, p_headContent);
}
if (p_opt.m_completeHTML) {
QString content(p_bodyContent);
fixBodyResources(m_baseUrl, resFolderPath, content);
html.replace(HtmlHolder::c_bodyHolder, content);
} else {
html.replace(HtmlHolder::c_bodyHolder, p_bodyContent);
}
file.write(html.toUtf8());
file.close();
// Delete empty resource folder.
QDir dir(resFolderPath);
if (dir.isEmpty()) {
dir.cdUp();
dir.rmdir(resFolder);
}
htmlExported = 1;
});
p_webDocument->getHtmlContentAsync();
while (htmlExported == 0) {
VUtils::sleepWait(100);
if (m_state == ExportState::Cancelled) {
break;
}
}
return htmlExported == 1;
}
bool VExporter::fixStyleResources(const QString &p_folder,
QString &p_html)
{
bool altered = false;
QRegExp reg("\\burl\\(\"((file|qrc):[^\"\\)]+)\"\\);");
int pos = 0;
while (pos < p_html.size()) {
int idx = p_html.indexOf(reg, pos);
if (idx == -1) {
break;
}
QString targetFile = g_webUtils->copyResource(QUrl(reg.cap(1)), p_folder);
if (targetFile.isEmpty()) {
pos = idx + reg.matchedLength();
} else {
// Replace the url string in html.
QString newUrl = QString("url(\"%1\");").arg(getResourceRelativePath(targetFile));
p_html.replace(idx, reg.matchedLength(), newUrl);
pos = idx + newUrl.size();
altered = true;
}
}
return altered;
}
bool VExporter::fixBodyResources(const QUrl &p_baseUrl,
const QString &p_folder,
QString &p_html)
{
bool altered = false;
if (p_baseUrl.isEmpty()) {
return altered;
}
QRegExp reg("<img ([^>]*)src=\"([^\"]+)\"([^>]*)>");
int pos = 0;
while (pos < p_html.size()) {
int idx = p_html.indexOf(reg, pos);
if (idx == -1) {
break;
}
if (reg.cap(2).isEmpty()) {
pos = idx + reg.matchedLength();
continue;
}
QUrl srcUrl(p_baseUrl.resolved(reg.cap(2)));
QString targetFile = g_webUtils->copyResource(srcUrl, p_folder);
if (targetFile.isEmpty()) {
pos = idx + reg.matchedLength();
} else {
// Replace the url string in html.
QString newUrl = QString("<img %1src=\"%2\"%3>").arg(reg.cap(1))
.arg(getResourceRelativePath(targetFile))
.arg(reg.cap(3));
p_html.replace(idx, reg.matchedLength(), newUrl);
pos = idx + newUrl.size();
altered = true;
}
}
return altered;
}
QString VExporter::getResourceRelativePath(const QString &p_file)
{
int idx = p_file.lastIndexOf('/');
int idx2 = p_file.lastIndexOf('/', idx - 1);
Q_ASSERT(idx > 0 && idx2 < idx);
return "." + p_file.mid(idx2);
}
bool VExporter::exportToMHTML(VWebView *p_webViewer,
const ExportHTMLOption &p_opt,
const QString &p_filePath)
{
Q_UNUSED(p_opt);
m_downloadState = QWebEngineDownloadItem::DownloadRequested;
p_webViewer->page()->save(p_filePath, QWebEngineDownloadItem::MimeHtmlSaveFormat);
while (m_downloadState == QWebEngineDownloadItem::DownloadRequested
|| m_downloadState == QWebEngineDownloadItem::DownloadInProgress) {
VUtils::sleepWait(100);
}
return m_downloadState == QWebEngineDownloadItem::DownloadCompleted;
}
void VExporter::handleDownloadRequested(QWebEngineDownloadItem *p_item)
{
if (p_item->savePageFormat() == QWebEngineDownloadItem::MimeHtmlSaveFormat) {
connect(p_item, &QWebEngineDownloadItem::stateChanged,
this, [this](QWebEngineDownloadItem::DownloadState p_state) {
m_downloadState = p_state;
});
}
}
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;
}