vexporter.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. #include "vexporter.h"
  2. #include <QDebug>
  3. #include <QWidget>
  4. #include <QWebChannel>
  5. #include <QWebEngineProfile>
  6. #include <QRegExp>
  7. #include <QProcess>
  8. #include <QTemporaryDir>
  9. #include "vconfigmanager.h"
  10. #include "vfile.h"
  11. #include "vwebview.h"
  12. #include "utils/vutils.h"
  13. #include "vpreviewpage.h"
  14. #include "vconstants.h"
  15. #include "vmarkdownconverter.h"
  16. #include "vdocument.h"
  17. #include "utils/vwebutils.h"
  18. extern VConfigManager *g_config;
  19. extern VWebUtils *g_webUtils;
  20. VExporter::VExporter(QWidget *p_parent)
  21. : QObject(p_parent),
  22. m_webViewer(NULL),
  23. m_state(ExportState::Idle)
  24. {
  25. }
  26. static QString marginToStrMM(qreal p_margin)
  27. {
  28. return QString("%1mm").arg(p_margin);
  29. }
  30. void VExporter::prepareExport(const ExportOption &p_opt)
  31. {
  32. m_htmlTemplate = VUtils::generateHtmlTemplate(p_opt.m_renderer,
  33. p_opt.m_renderBg,
  34. p_opt.m_renderStyle,
  35. p_opt.m_renderCodeBlockStyle,
  36. p_opt.m_format == ExportFormat::PDF
  37. || p_opt.m_format == ExportFormat::OnePDF);
  38. m_exportHtmlTemplate = VUtils::generateExportHtmlTemplate(p_opt.m_renderBg);
  39. m_pageLayout = *(p_opt.m_pdfOpt.m_layout);
  40. prepareWKArguments(p_opt.m_pdfOpt);
  41. }
  42. // From QProcess code.
  43. static QStringList parseCombinedArgString(const QString &program)
  44. {
  45. QStringList args;
  46. QString tmp;
  47. int quoteCount = 0;
  48. bool inQuote = false;
  49. // handle quoting. tokens can be surrounded by double quotes
  50. // "hello world". three consecutive double quotes represent
  51. // the quote character itself.
  52. for (int i = 0; i < program.size(); ++i) {
  53. if (program.at(i) == QLatin1Char('"')) {
  54. ++quoteCount;
  55. if (quoteCount == 3) {
  56. // third consecutive quote
  57. quoteCount = 0;
  58. tmp += program.at(i);
  59. }
  60. continue;
  61. }
  62. if (quoteCount) {
  63. if (quoteCount == 1)
  64. inQuote = !inQuote;
  65. quoteCount = 0;
  66. }
  67. if (!inQuote && program.at(i).isSpace()) {
  68. if (!tmp.isEmpty()) {
  69. args += tmp;
  70. tmp.clear();
  71. }
  72. } else {
  73. tmp += program.at(i);
  74. }
  75. }
  76. if (!tmp.isEmpty())
  77. args += tmp;
  78. return args;
  79. }
  80. void VExporter::prepareWKArguments(const ExportPDFOption &p_opt)
  81. {
  82. m_wkArgs.clear();
  83. m_wkArgs << "--quiet";
  84. m_wkArgs << "--encoding" << "utf-8";
  85. m_wkArgs << "--page-size" << m_pageLayout.pageSize().key();
  86. m_wkArgs << "--orientation"
  87. << (m_pageLayout.orientation() == QPageLayout::Portrait ? "Portrait" : "Landscape");
  88. QMarginsF marginsMM = m_pageLayout.margins(QPageLayout::Millimeter);
  89. m_wkArgs << "--margin-bottom" << marginToStrMM(marginsMM.bottom());
  90. m_wkArgs << "--margin-left" << marginToStrMM(marginsMM.left());
  91. m_wkArgs << "--margin-right" << marginToStrMM(marginsMM.right());
  92. m_wkArgs << "--margin-top" << marginToStrMM(marginsMM.top());
  93. m_wkArgs << (p_opt.m_wkEnableBackground ? "--background" : "--no-background");
  94. QString footer;
  95. switch (p_opt.m_wkPageNumber) {
  96. case ExportPageNumber::Left:
  97. footer = "--footer-left";
  98. break;
  99. case ExportPageNumber::Center:
  100. footer = "--footer-center";
  101. break;
  102. case ExportPageNumber::Right:
  103. footer = "--footer-right";
  104. break;
  105. default:
  106. break;
  107. }
  108. if (!footer.isEmpty()) {
  109. m_wkArgs << footer << "[page]"
  110. << "--footer-spacing" << QString::number(marginsMM.bottom() / 3, 'f', 2);
  111. }
  112. // Title.
  113. if (!p_opt.m_wkTitle.isEmpty()) {
  114. m_wkArgs << "--title" << p_opt.m_wkTitle;
  115. }
  116. // Append additional arguments.
  117. if (!p_opt.m_wkExtraArgs.isEmpty()) {
  118. m_wkArgs.append(parseCombinedArgString(p_opt.m_wkExtraArgs));
  119. }
  120. if (p_opt.m_wkEnableTableOfContents) {
  121. m_wkArgs << "toc" << "--toc-text-size-shrink" << "1.0";
  122. }
  123. }
  124. bool VExporter::exportPDF(VFile *p_file,
  125. const ExportOption &p_opt,
  126. const QString &p_outputFile,
  127. QString *p_errMsg)
  128. {
  129. return exportViaWebView(p_file, p_opt, p_outputFile, p_errMsg);
  130. }
  131. bool VExporter::exportHTML(VFile *p_file,
  132. const ExportOption &p_opt,
  133. const QString &p_outputFile,
  134. QString *p_errMsg)
  135. {
  136. return exportViaWebView(p_file, p_opt, p_outputFile, p_errMsg);
  137. }
  138. void VExporter::initWebViewer(VFile *p_file, const ExportOption &p_opt)
  139. {
  140. Q_ASSERT(!m_webViewer);
  141. m_webViewer = new VWebView(p_file, static_cast<QWidget *>(parent()));
  142. m_webViewer->hide();
  143. VPreviewPage *page = new VPreviewPage(m_webViewer);
  144. m_webViewer->setPage(page);
  145. connect(page, &VPreviewPage::loadFinished,
  146. this, &VExporter::handleLoadFinished);
  147. connect(page->profile(), &QWebEngineProfile::downloadRequested,
  148. this, &VExporter::handleDownloadRequested);
  149. m_webDocument = new VDocument(p_file, m_webViewer);
  150. connect(m_webDocument, &VDocument::logicsFinished,
  151. this, &VExporter::handleLogicsFinished);
  152. QWebChannel *channel = new QWebChannel(m_webViewer);
  153. channel->registerObject(QStringLiteral("content"), m_webDocument);
  154. page->setWebChannel(channel);
  155. // Need to generate HTML using Hoedown.
  156. if (p_opt.m_renderer == MarkdownConverterType::Hoedown) {
  157. VMarkdownConverter mdConverter;
  158. QString toc;
  159. QString html = mdConverter.generateHtml(p_file->getContent(),
  160. g_config->getMarkdownExtensions(),
  161. toc);
  162. m_webDocument->setHtml(html);
  163. }
  164. m_baseUrl = p_file->getBaseUrl();
  165. m_webViewer->setHtml(m_htmlTemplate, m_baseUrl);
  166. }
  167. void VExporter::handleLogicsFinished()
  168. {
  169. Q_ASSERT(!(m_noteState & NoteState::WebLogicsReady));
  170. m_noteState = NoteState(m_noteState | NoteState::WebLogicsReady);
  171. }
  172. void VExporter::handleLoadFinished(bool p_ok)
  173. {
  174. Q_ASSERT(!(m_noteState & NoteState::WebLoadFinished));
  175. m_noteState = NoteState(m_noteState | NoteState::WebLoadFinished);
  176. if (!p_ok) {
  177. m_noteState = NoteState(m_noteState | NoteState::Failed);
  178. }
  179. }
  180. void VExporter::clearWebViewer()
  181. {
  182. // m_webDocument will be freeed by QObject.
  183. delete m_webViewer;
  184. m_webViewer = NULL;
  185. m_webDocument = NULL;
  186. m_baseUrl.clear();
  187. }
  188. bool VExporter::exportToPDF(VWebView *p_webViewer,
  189. const QString &p_filePath,
  190. const QPageLayout &p_layout)
  191. {
  192. int pdfPrinted = 0;
  193. p_webViewer->page()->printToPdf([&, this](const QByteArray &p_result) {
  194. if (p_result.isEmpty() || this->m_state == ExportState::Cancelled) {
  195. pdfPrinted = -1;
  196. return;
  197. }
  198. V_ASSERT(!p_filePath.isEmpty());
  199. if (!VUtils::writeFileToDisk(p_filePath, p_result)) {
  200. pdfPrinted = -1;
  201. return;
  202. }
  203. pdfPrinted = 1;
  204. }, p_layout);
  205. while (pdfPrinted == 0) {
  206. VUtils::sleepWait(100);
  207. if (m_state == ExportState::Cancelled) {
  208. break;
  209. }
  210. }
  211. return pdfPrinted == 1;
  212. }
  213. bool VExporter::exportToPDFViaWK(VDocument *p_webDocument,
  214. const ExportPDFOption &p_opt,
  215. const QString &p_filePath,
  216. QString *p_errMsg)
  217. {
  218. int pdfExported = 0;
  219. connect(p_webDocument, &VDocument::htmlContentFinished,
  220. this, [&, this](const QString &p_headContent,
  221. const QString &p_styleContent,
  222. const QString &p_bodyContent) {
  223. if (p_bodyContent.isEmpty() || this->m_state == ExportState::Cancelled) {
  224. pdfExported = -1;
  225. return;
  226. }
  227. Q_ASSERT(!p_filePath.isEmpty());
  228. // Save HTML to a temp dir.
  229. QTemporaryDir tmpDir;
  230. if (!tmpDir.isValid()) {
  231. pdfExported = -1;
  232. return;
  233. }
  234. QString htmlPath = tmpDir.filePath("vnote_tmp.html");
  235. QFile file(htmlPath);
  236. if (!file.open(QFile::WriteOnly)) {
  237. pdfExported = -1;
  238. return;
  239. }
  240. QString resFolder = QFileInfo(htmlPath).completeBaseName() + "_files";
  241. QString resFolderPath = QDir(VUtils::basePathFromPath(htmlPath)).filePath(resFolder);
  242. qDebug() << "temp HTML files folder" << resFolderPath;
  243. QString html(m_exportHtmlTemplate);
  244. if (!p_styleContent.isEmpty()) {
  245. QString content(p_styleContent);
  246. fixStyleResources(resFolderPath, content);
  247. html.replace(HtmlHolder::c_styleHolder, content);
  248. }
  249. if (!p_headContent.isEmpty()) {
  250. html.replace(HtmlHolder::c_headHolder, p_headContent);
  251. }
  252. QString content(p_bodyContent);
  253. fixBodyResources(m_baseUrl, resFolderPath, content);
  254. html.replace(HtmlHolder::c_bodyHolder, content);
  255. file.write(html.toUtf8());
  256. file.close();
  257. // Convert via wkhtmltopdf.
  258. QList<QString> files;
  259. files.append(htmlPath);
  260. if (!htmlsToPDFViaWK(files, p_filePath, p_opt, p_errMsg)) {
  261. pdfExported = -1;
  262. } else {
  263. pdfExported = 1;
  264. }
  265. });
  266. p_webDocument->getHtmlContentAsync();
  267. while (pdfExported == 0) {
  268. VUtils::sleepWait(100);
  269. if (m_state == ExportState::Cancelled) {
  270. break;
  271. }
  272. }
  273. return pdfExported == 1;
  274. }
  275. bool VExporter::exportViaWebView(VFile *p_file,
  276. const ExportOption &p_opt,
  277. const QString &p_outputFile,
  278. QString *p_errMsg)
  279. {
  280. Q_UNUSED(p_errMsg);
  281. bool ret = false;
  282. bool isOpened = p_file->isOpened();
  283. if (!isOpened && !p_file->open()) {
  284. goto exit;
  285. }
  286. Q_ASSERT(m_state == ExportState::Idle);
  287. m_state = ExportState::Busy;
  288. clearNoteState();
  289. initWebViewer(p_file, p_opt);
  290. while (!isNoteStateReady()) {
  291. VUtils::sleepWait(100);
  292. if (m_state == ExportState::Cancelled) {
  293. goto exit;
  294. }
  295. if (isNoteStateFailed()) {
  296. m_state = ExportState::Failed;
  297. goto exit;
  298. }
  299. }
  300. // Wait to ensure Web side is really ready.
  301. VUtils::sleepWait(200);
  302. if (m_state == ExportState::Cancelled) {
  303. goto exit;
  304. }
  305. {
  306. bool exportRet = false;
  307. switch (p_opt.m_format) {
  308. case ExportFormat::PDF:
  309. V_FALLTHROUGH;
  310. case ExportFormat::OnePDF:
  311. if (p_opt.m_pdfOpt.m_wkhtmltopdf) {
  312. exportRet = exportToPDFViaWK(m_webDocument,
  313. p_opt.m_pdfOpt,
  314. p_outputFile,
  315. p_errMsg);
  316. } else {
  317. exportRet = exportToPDF(m_webViewer,
  318. p_outputFile,
  319. m_pageLayout);
  320. }
  321. break;
  322. case ExportFormat::HTML:
  323. if (p_opt.m_htmlOpt.m_mimeHTML) {
  324. exportRet = exportToMHTML(m_webViewer,
  325. p_opt.m_htmlOpt,
  326. p_outputFile);
  327. } else {
  328. exportRet = exportToHTML(m_webDocument,
  329. p_opt.m_htmlOpt,
  330. p_outputFile);
  331. }
  332. break;
  333. default:
  334. break;
  335. }
  336. clearNoteState();
  337. if (!isOpened) {
  338. p_file->close();
  339. }
  340. if (exportRet) {
  341. m_state = ExportState::Successful;
  342. } else {
  343. m_state = ExportState::Failed;
  344. }
  345. }
  346. exit:
  347. clearWebViewer();
  348. if (m_state == ExportState::Successful) {
  349. ret = true;
  350. }
  351. m_state = ExportState::Idle;
  352. return ret;
  353. }
  354. bool VExporter::exportToHTML(VDocument *p_webDocument,
  355. const ExportHTMLOption &p_opt,
  356. const QString &p_filePath)
  357. {
  358. int htmlExported = 0;
  359. connect(p_webDocument, &VDocument::htmlContentFinished,
  360. this, [&, this](const QString &p_headContent,
  361. const QString &p_styleContent,
  362. const QString &p_bodyContent) {
  363. if (p_bodyContent.isEmpty() || this->m_state == ExportState::Cancelled) {
  364. htmlExported = -1;
  365. return;
  366. }
  367. Q_ASSERT(!p_filePath.isEmpty());
  368. QFile file(p_filePath);
  369. if (!file.open(QFile::WriteOnly)) {
  370. htmlExported = -1;
  371. return;
  372. }
  373. QString resFolder = QFileInfo(p_filePath).completeBaseName() + "_files";
  374. QString resFolderPath = QDir(VUtils::basePathFromPath(p_filePath)).filePath(resFolder);
  375. qDebug() << "HTML files folder" << resFolderPath;
  376. QString html(m_exportHtmlTemplate);
  377. if (!p_styleContent.isEmpty() && p_opt.m_embedCssStyle) {
  378. QString content(p_styleContent);
  379. fixStyleResources(resFolderPath, content);
  380. html.replace(HtmlHolder::c_styleHolder, content);
  381. }
  382. if (!p_headContent.isEmpty()) {
  383. html.replace(HtmlHolder::c_headHolder, p_headContent);
  384. }
  385. if (p_opt.m_completeHTML) {
  386. QString content(p_bodyContent);
  387. fixBodyResources(m_baseUrl, resFolderPath, content);
  388. html.replace(HtmlHolder::c_bodyHolder, content);
  389. } else {
  390. html.replace(HtmlHolder::c_bodyHolder, p_bodyContent);
  391. }
  392. file.write(html.toUtf8());
  393. file.close();
  394. // Delete empty resource folder.
  395. QDir dir(resFolderPath);
  396. if (dir.isEmpty()) {
  397. dir.cdUp();
  398. dir.rmdir(resFolder);
  399. }
  400. htmlExported = 1;
  401. });
  402. p_webDocument->getHtmlContentAsync();
  403. while (htmlExported == 0) {
  404. VUtils::sleepWait(100);
  405. if (m_state == ExportState::Cancelled) {
  406. break;
  407. }
  408. }
  409. return htmlExported == 1;
  410. }
  411. bool VExporter::fixStyleResources(const QString &p_folder,
  412. QString &p_html)
  413. {
  414. bool altered = false;
  415. QRegExp reg("\\burl\\(\"((file|qrc):[^\"\\)]+)\"\\);");
  416. int pos = 0;
  417. while (pos < p_html.size()) {
  418. int idx = p_html.indexOf(reg, pos);
  419. if (idx == -1) {
  420. break;
  421. }
  422. QString targetFile = g_webUtils->copyResource(QUrl(reg.cap(1)), p_folder);
  423. if (targetFile.isEmpty()) {
  424. pos = idx + reg.matchedLength();
  425. } else {
  426. // Replace the url string in html.
  427. QString newUrl = QString("url(\"%1\");").arg(getResourceRelativePath(targetFile));
  428. p_html.replace(idx, reg.matchedLength(), newUrl);
  429. pos = idx + newUrl.size();
  430. altered = true;
  431. }
  432. }
  433. return altered;
  434. }
  435. bool VExporter::fixBodyResources(const QUrl &p_baseUrl,
  436. const QString &p_folder,
  437. QString &p_html)
  438. {
  439. bool altered = false;
  440. if (p_baseUrl.isEmpty()) {
  441. return altered;
  442. }
  443. QRegExp reg("<img ([^>]*)src=\"([^\"]+)\"([^>]*)>");
  444. int pos = 0;
  445. while (pos < p_html.size()) {
  446. int idx = p_html.indexOf(reg, pos);
  447. if (idx == -1) {
  448. break;
  449. }
  450. if (reg.cap(2).isEmpty()) {
  451. pos = idx + reg.matchedLength();
  452. continue;
  453. }
  454. QUrl srcUrl(p_baseUrl.resolved(reg.cap(2)));
  455. QString targetFile = g_webUtils->copyResource(srcUrl, p_folder);
  456. if (targetFile.isEmpty()) {
  457. pos = idx + reg.matchedLength();
  458. } else {
  459. // Replace the url string in html.
  460. QString newUrl = QString("<img %1src=\"%2\"%3>").arg(reg.cap(1))
  461. .arg(getResourceRelativePath(targetFile))
  462. .arg(reg.cap(3));
  463. p_html.replace(idx, reg.matchedLength(), newUrl);
  464. pos = idx + newUrl.size();
  465. altered = true;
  466. }
  467. }
  468. return altered;
  469. }
  470. QString VExporter::getResourceRelativePath(const QString &p_file)
  471. {
  472. int idx = p_file.lastIndexOf('/');
  473. int idx2 = p_file.lastIndexOf('/', idx - 1);
  474. Q_ASSERT(idx > 0 && idx2 < idx);
  475. return "." + p_file.mid(idx2);
  476. }
  477. bool VExporter::exportToMHTML(VWebView *p_webViewer,
  478. const ExportHTMLOption &p_opt,
  479. const QString &p_filePath)
  480. {
  481. Q_UNUSED(p_opt);
  482. m_downloadState = QWebEngineDownloadItem::DownloadRequested;
  483. p_webViewer->page()->save(p_filePath, QWebEngineDownloadItem::MimeHtmlSaveFormat);
  484. while (m_downloadState == QWebEngineDownloadItem::DownloadRequested
  485. || m_downloadState == QWebEngineDownloadItem::DownloadInProgress) {
  486. VUtils::sleepWait(100);
  487. }
  488. return m_downloadState == QWebEngineDownloadItem::DownloadCompleted;
  489. }
  490. void VExporter::handleDownloadRequested(QWebEngineDownloadItem *p_item)
  491. {
  492. if (p_item->savePageFormat() == QWebEngineDownloadItem::MimeHtmlSaveFormat) {
  493. connect(p_item, &QWebEngineDownloadItem::stateChanged,
  494. this, [this](QWebEngineDownloadItem::DownloadState p_state) {
  495. m_downloadState = p_state;
  496. });
  497. }
  498. }
  499. static QString combineArgs(QStringList &p_args)
  500. {
  501. QString str;
  502. for (const QString &arg : p_args) {
  503. QString tmp;
  504. if (arg.contains(' ')) {
  505. tmp = '"' + arg + '"';
  506. } else {
  507. tmp = arg;
  508. }
  509. if (str.isEmpty()) {
  510. str = tmp;
  511. } else {
  512. str = str + ' ' + tmp;
  513. }
  514. }
  515. return str;
  516. }
  517. bool VExporter::htmlsToPDFViaWK(const QList<QString> &p_htmlFiles,
  518. const QString &p_filePath,
  519. const ExportPDFOption &p_opt,
  520. QString *p_errMsg)
  521. {
  522. // Note: system's locale settings (Language for non-Unicode programs) is important to wkhtmltopdf.
  523. // Input file could be encoded via QUrl::fromLocalFile(p_htmlFile).toString(QUrl::EncodeUnicode) to
  524. // handle non-ASCII path.
  525. QStringList args(m_wkArgs);
  526. for (auto const & it : p_htmlFiles) {
  527. args << QDir::toNativeSeparators(it);
  528. }
  529. args << QDir::toNativeSeparators(p_filePath);
  530. QString cmd = p_opt.m_wkPath + " " + combineArgs(args);
  531. emit outputLog(cmd);
  532. int ret = QProcess::execute(p_opt.m_wkPath, args);
  533. qDebug() << "wkhtmltopdf returned" << ret << cmd;
  534. switch (ret) {
  535. case -2:
  536. VUtils::addErrMsg(p_errMsg, tr("Fail to start wkhtmltopdf (%1).").arg(cmd));
  537. break;
  538. case -1:
  539. VUtils::addErrMsg(p_errMsg, tr("wkhtmltopdf crashed (%1).").arg(cmd));
  540. break;
  541. default:
  542. break;
  543. }
  544. return ret == 0;
  545. }
  546. int VExporter::exportPDFInOne(const QList<QString> &p_htmlFiles,
  547. const ExportOption &p_opt,
  548. const QString &p_outputFile,
  549. QString *p_errMsg)
  550. {
  551. if (!htmlsToPDFViaWK(p_htmlFiles, p_outputFile, p_opt.m_pdfOpt, p_errMsg)) {
  552. return 0;
  553. }
  554. return p_htmlFiles.size();
  555. }