urls;
503 | for (int i = 0; i < files.size(); ++i) {
504 | urls.append(QUrl::fromUserInput(files.at(i)));
505 | }
506 | if (urls.size() > 0) { handleFoundPDF(urls); }
507 | }
508 | }
509 |
510 | void FusePDF::on_actionSave_triggered()
511 | {
512 | ui->toolBox->setCurrentIndex(FUSEPDF_TOOLBOX_OUTPUT);
513 | int totalPages = pagesToExport();
514 | qDebug() << "total pages to export" << totalPages;
515 | if (ui->inputs->topLevelItemCount() == 0 || totalPages < 1) {
516 | QMessageBox::warning(this,
517 | tr("No documents/pages"),
518 | tr("No documents/pages to merge, please add some documents or enable some pages before trying to save."));
519 | return;
520 | }
521 | QString file = QFileDialog::getSaveFileName(this,
522 | tr("Save document"),
523 | !_lastSaveDir.isEmpty()?_lastSaveDir:QDir::homePath(),
524 | "*.pdf");
525 | if (file.isEmpty()) { return; }
526 | if (!file.endsWith(".pdf", Qt::CaseInsensitive)) { file.append(".pdf"); }
527 |
528 | showProgress(true);
529 | QtConcurrent::run(this, &FusePDF::prepCommand, file);
530 | }
531 |
532 | void FusePDF::on_actionQuit_triggered()
533 | {
534 | qApp->quit();
535 | }
536 |
537 | void FusePDF::on_actionAbout_triggered()
538 | {
539 | QMessageBox::about(this,
540 | QString("FusePDF"),
541 | QString("FusePDF %1
"
542 | "Developed by Ole-André Rodlie for NettStudio AS.
"
543 | "Copyright © 2021, 2022 NettStudio AS. All rights reserved.
"
544 | "This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
"
545 | "This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
"
546 | "You should have received a copy of the GNU General Public License along with this program. If not, see www.gnu.org/licenses.
"
547 | "
"
548 | ).arg(VERSION_APP));
549 | }
550 |
551 | void FusePDF::prepCommand(const QString &filename)
552 | {
553 | QString compat = ui->compat->currentData().toString();
554 | int pdfa = 0;
555 | if (compat == "A1") { pdfa = 1; }
556 | else if (compat == "A2") { pdfa = 2; }
557 | else if (compat == "A3") { pdfa = 3; }
558 | /*if (pdfa > 0 && ui->metaTitle->text().isEmpty()) {
559 | QMessageBox::warning(this,
560 | tr("PDF/A Warning"),
561 | tr("Document title is not optional in PDF/A. Please add title in 'Output properties'."));
562 | return;
563 | }*/
564 | QString command = makeCommand(filename, pdfa);
565 | if (!command.isEmpty()) { emit commandReady(filename, command); }
566 | else {
567 | showProgress(false);
568 | QMessageBox::warning(this,
569 | tr("Failed to make process"),
570 | tr("Failed to make process, this should not happen, please contact support."));
571 | }
572 | }
573 |
574 | const QString FusePDF::makeCommand(const QString &filename,
575 | int pdfa)
576 | {
577 | if (filename.isEmpty()) { return QString(); }
578 |
579 | QString command = findGhost();
580 | #ifdef Q_OS_WIN
581 | command = QString("\"%1\"").arg(findGhost());
582 | #endif
583 | command.append(" -sDEVICE=pdfwrite");
584 | if (pdfa == 0 &&
585 | !ui->compat->currentData().toString().isEmpty() &&
586 | ui->compat->currentData().toString().toLower() != "default") {
587 | command.append(QString(" -dCompatibilityLevel=%1").arg(ui->compat->currentData().toString()));
588 | }
589 | if (pdfa == 0 &&
590 | !ui->preset->currentData().toString().isEmpty() &&
591 | ui->preset->currentData().toString().toLower() != "none")
592 | {
593 | command.append(QString(" -dPDFSETTINGS=/%1").arg(ui->preset->currentData().toString().toLower()));
594 | }
595 | command.append(" -dNOPAUSE -dBATCH");
596 |
597 | if (pdfa == 0) { command.append(" -dDetectDuplicateImages -dCompressFonts=true"); }
598 |
599 | command.append(QString(" -sOutputFile=\"%1\"").arg(filename));
600 |
601 | QString title = ui->metaTitle->text();
602 | QString subject = ui->metaSubject->text();
603 | QString author = ui->metaAuthor->text();
604 |
605 | if (pdfa > 0) {
606 | command.append(QString(" -dEmbedAllFonts=true -dNOSAFER -dNOOUTERSAVE -dPDFACompatibilityPolicy=1 -sProcessColorModel=DeviceRGB -sColorConversionStrategy=RGB -dPDFA=%1").arg(pdfa));
607 | command.append(QString(" \"%1\"").arg(findGhostPdfa()));
608 | }
609 | for (int i = 0; i < ui->inputs->topLevelItemCount(); ++i) {
610 | QString filename = ui->inputs->topLevelItem(i)->data(0, FUSEPDF_PATH_ROLE).toString();
611 | PagesListWidget *tab = getTab(filename);
612 | int enabledPages = tab? tab->getPagesState(true).size() : 0;
613 | bool modified = false;
614 | if (tab && tab->isModified() && enabledPages > 0) {
615 | modified = true;
616 | QVector pages = tab->getPagesState(true);
617 | for (int i = 0; i < pages.count(); ++i) {
618 | QString extracted = extractPDF(filename, getChecksum(filename), pages.at(i));
619 | if (!extracted.isEmpty()) {
620 | command.append(QString(" \"%1\"").arg(extracted));
621 | }
622 | }
623 | }
624 | if (enabledPages < 1) { continue; }
625 | if (!modified) {
626 | command.append(QString(" \"%1\"").arg(filename));
627 | }
628 | }
629 |
630 | QString marks;
631 | marks.append("/Creator(FusePDF - https://fusepdf.no)");
632 | if (!title.isEmpty()) { marks.append(QString(pdfa > 0 ? "/Title(%1)" : "/Title<%1>").arg(pdfa > 0 ? stripMarks(title) : QString(toUtf16Hex(title)).toUpper())); }
633 | if (!subject.isEmpty()) { marks.append(QString(pdfa > 0 ? "/Subject(%1)" : "/Subject<%1>").arg(pdfa > 0 ? stripMarks(subject) : QString(toUtf16Hex(subject)).toUpper())); }
634 | if (!author.isEmpty()) { marks.append(QString(pdfa > 0 ? "/Author(%1)" : "/Author<%1>").arg(pdfa > 0 ? stripMarks(author) : QString(toUtf16Hex(author)).toUpper())); }
635 | command.append(QString(" -c \"[%1/DOCINFO pdfmark\"").arg(marks));
636 |
637 | qDebug() << command;
638 | return command;
639 | }
640 |
641 | void FusePDF::runCommand(const QString &filename,
642 | const QString &command)
643 | {
644 | showProgress(false);
645 | if (missingGhost()) { return; }
646 |
647 | if (ui->inputs->topLevelItemCount() == 0 || filename.isEmpty()) {
648 | QMessageBox::warning(this,
649 | tr("Unable to process"),
650 | tr("Input and/or output is missing."));
651 | return;
652 | }
653 | if (hasFile(filename)) {
654 | QMessageBox::warning(this,
655 | tr("Unable to save file"),
656 | tr("Unable to save to file, the output file is found in input."));
657 | return;
658 | }
659 | if (QFile::exists(filename)) {
660 | int ret = QMessageBox::question(this,
661 | tr("File exists"),
662 | tr("File already exists, are you sure you want to overwrite it?"));
663 | if (ret != QMessageBox::Yes) { return; }
664 | }
665 | if (_proc->isOpen()) {
666 | QMessageBox::warning(this,
667 | tr("Still active"),
668 | QString("FusePDF %1").arg(tr("process is still active, please wait until done.")));
669 | return;
670 | }
671 |
672 | if (command.isEmpty()) {
673 | QMessageBox::warning(this,
674 | tr("Failed to make process"),
675 | tr("Failed to make process, this should not happen, please contact support."));
676 | return;
677 | }
678 | _output = filename;
679 | qDebug() << command;
680 | _proc->start(command);
681 | }
682 |
683 | void FusePDF::commandStarted()
684 | {
685 | ui->cmd->clear();
686 | ui->actionSave->setDisabled(true);
687 | showProgress(true);
688 | }
689 |
690 | void FusePDF::commandFinished(int exitCode)
691 | {
692 | ui->actionSave->setDisabled(false);
693 | showProgress(false);
694 | _proc->close();
695 |
696 | if (exitCode == 0) {
697 | QFileInfo fileInfo(_output);
698 | if (_lastSaveDir != fileInfo.absoluteDir().absolutePath()) {
699 | _lastSaveDir = fileInfo.absoluteDir().absolutePath();
700 | }
701 | if (ui->actionOpen_saved_PDF->isChecked()) {
702 | QDesktopServices::openUrl(QUrl::fromUserInput(_output));
703 | }
704 | return;
705 | }
706 |
707 | QMessageBox::warning(this,
708 | tr("Failed to save PDF"),
709 | tr("Failed to save PDF, see log (CTRL+L) for more information"));
710 | }
711 |
712 | void FusePDF::populateUI()
713 | {
714 | QIcon docIcon(QIcon::fromTheme(HICOLOR_ICON_DOC,
715 | QIcon(FUSEPDF_ICON_DOC)));
716 |
717 | ui->actionOpen->setIcon(QIcon::fromTheme(HICOLOR_ICON_OPEN,
718 | QIcon(FUSEPDF_ICON_OPEN)));
719 | ui->actionQuit->setIcon(QIcon::fromTheme(HICOLOR_ICON_QUIT,
720 | QIcon(FUSEPDF_ICON_QUIT)));
721 | ui->actionSave->setIcon(QIcon::fromTheme(HICOLOR_ICON_SAVE,
722 | QIcon(FUSEPDF_ICON_SAVE)));
723 |
724 |
725 | QString versionString = tr("version");
726 |
727 | ui->compat->blockSignals(true);
728 | ui->compat->clear();
729 | ui->compat->addItem(docIcon, QString("PDF %1 1.0 (Acrobat 1.0)").arg(versionString), "1.0");
730 | ui->compat->addItem(docIcon, QString("PDF %1 1.1 (Acrobat 2.0)").arg(versionString), "1.1");
731 | ui->compat->addItem(docIcon, QString("PDF %1 1.2 (Acrobat 3.0)").arg(versionString), "1.2");
732 | ui->compat->addItem(docIcon, QString("PDF %1 1.3 (Acrobat 4.0)").arg(versionString), "1.3");
733 | ui->compat->addItem(docIcon, QString("PDF %1 1.4 (Acrobat 5.0)").arg(versionString), "1.4");
734 | ui->compat->addItem(docIcon, QString("PDF %1 1.5 (Acrobat 6.0)").arg(versionString), "1.5");
735 | ui->compat->addItem(docIcon, QString("PDF %1 1.6 (Acrobat 7.0)").arg(versionString), "1.6");
736 | ui->compat->addItem(docIcon, QString("PDF %1 1.7 (Acrobat 8.0/ISO 32000-1)").arg(versionString), "1.7");
737 | ui->compat->addItem(docIcon, QString("PDF %1 2.0 (ISO 32000-2)").arg(versionString), "2.0");
738 | ui->compat->addItem(docIcon, QString("PDF/A-1 (ISO 19005-1)"), "A1");
739 | ui->compat->addItem(docIcon, QString("PDF/A-2 (ISO 19005-2)"), "A2");
740 | ui->compat->addItem(docIcon, QString("PDF/A-3 (ISO 19005-3)"), "A3");
741 | ui->compat->blockSignals(false);
742 |
743 | ui->preset->blockSignals(true);
744 | ui->preset->clear();
745 | ui->preset->addItem(docIcon, tr("None"), "None");
746 | ui->preset->addItem(docIcon, tr("Default"), "Default");
747 | ui->preset->addItem(docIcon, tr("Prepress"), "Prepress");
748 | ui->preset->addItem(docIcon, tr("eBook"), "eBook");
749 | ui->preset->addItem(docIcon, tr("Screen"), "Screen");
750 | ui->preset->addItem(docIcon, tr("Printer"), "Printer");
751 | ui->preset->blockSignals(false);
752 |
753 | ui->preview->setViewMode(QListView::IconMode);
754 | ui->preview->setIconSize(QSize(310, 310));
755 | ui->preview->setUniformItemSizes(true);
756 | ui->preview->setWrapping(true);
757 | ui->preview->setResizeMode(QListView::Adjust);
758 | ui->preview->setFrameShape(QFrame::NoFrame);
759 | ui->preview->setContextMenuPolicy(Qt::CustomContextMenu);
760 | ui->preview->setItemDelegate(new PageDelegate(this));
761 | ui->preview->setSpacing(10);
762 |
763 | connect(this, SIGNAL(generatedOutputPreview(QStringList)),
764 | this, SLOT(showOutputPreview(QStringList)));
765 | }
766 |
767 | void FusePDF::loadSettings()
768 | {
769 | QSettings settings;
770 | settings.beginGroup("ui");
771 | QByteArray lastGeo = settings.value("geometry").toByteArray();
772 | if (!lastGeo.isNull()) {
773 | restoreGeometry(lastGeo);
774 | QApplication::processEvents();
775 | }
776 | QByteArray lastState = settings.value("state").toByteArray();
777 | if (!lastState.isNull()) {
778 | restoreState(lastState);
779 | QApplication::processEvents();
780 | }
781 | bool wasMax = settings.value("maximized", false).toBool();
782 | if (wasMax) { showMaximized(); }
783 | settings.endGroup();
784 |
785 | loadOptions();
786 |
787 | if (getCachePath().isEmpty()) {
788 | QMessageBox::warning(this,
789 | tr("Missing cache folder"),
790 | tr("Unable to create the cache folder, please check your system permissions."));
791 | QTimer::singleShot(100, qApp, SLOT(quit()));
792 | }
793 | if (missingGhost()) {
794 | QTimer::singleShot(100, qApp, SLOT(quit()));
795 | }
796 | }
797 |
798 | void FusePDF::saveSettings()
799 | {
800 | saveOptions();
801 |
802 | QSettings settings;
803 | settings.beginGroup("ui");
804 | settings.setValue("state", saveState());
805 | settings.setValue("geometry", saveGeometry());
806 | settings.setValue("maximized", isMaximized());
807 | settings.endGroup();
808 | settings.sync();
809 | }
810 |
811 | void FusePDF::handleProcOutput()
812 | {
813 | QString log;
814 | log.append(_proc->readAllStandardError());
815 | log.append(_proc->readAllStandardOutput());
816 | ui->cmd->appendPlainText(log.simplified());
817 | ui->statusBar->showMessage(log.simplified(), 1000);
818 | qDebug() << log;
819 | }
820 |
821 | void FusePDF::clearInput(bool askFirst)
822 | {
823 | if (ui->inputs->topLevelItemCount() < 1) { return; }
824 | if (askFirst) {
825 | int res = QMessageBox::question(this,
826 | tr("Clear?"),
827 | tr("Clear all documents?"),
828 | QMessageBox::Yes | QMessageBox::No,
829 | QMessageBox::Yes);
830 | if (res != QMessageBox::Yes) { return; }
831 | }
832 | ui->inputs->clear();
833 | for (int i = 0; i < ui->tabs->count(); ++i) {
834 | PagesListWidget *tab = qobject_cast(ui->tabs->widget(i));
835 | if (!tab) { continue; }
836 | tab->deleteLater();
837 | }
838 | ui->tabs->clear();
839 | ui->tabs->addTab(ui->tabInputs,
840 | QIcon(FUSEPDF_ICON_MAIN),
841 | tr("Output"));
842 | ui->cmd->clear();
843 | _output.clear();
844 | ui->preview->clear();
845 | ui->toolBox->setCurrentIndex(FUSEPDF_TOOLBOX_OUTPUT);
846 | }
847 |
848 | const QString FusePDF::findGhost(bool pathOnly)
849 | {
850 | #ifdef Q_OS_WIN
851 | QString appDir = QString("%1/gs").arg(qApp->applicationDirPath());
852 | if (QFile::exists(appDir)) {
853 | QString bin64 = appDir + "/bin/gswin64c.exe";
854 | if (QFile::exists(bin64)) { return pathOnly ? appDir : bin64; }
855 | QString bin32 = appDir + "/bin/gswin32c.exe";
856 | if (QFile::exists(bin32)) { return pathOnly ? appDir : bin32; }
857 | }
858 | QString programFilesPath(qgetenv("PROGRAMFILES"));
859 | QDirIterator it(programFilesPath + "/gs", QStringList() << "*.*", QDir::Dirs/*, QDirIterator::Subdirectories*/);
860 | while (it.hasNext()) {
861 | QString folder = it.next();
862 | QString bin64 = folder + "/bin/gswin64c.exe";
863 | if (QFile::exists(bin64)) { return pathOnly ? folder : bin64; }
864 | QString bin32 = folder + "/bin/gswin32c.exe";
865 | if (QFile::exists(bin32)) { return pathOnly ? folder : bin32; }
866 | }
867 | return QString();
868 | #endif
869 | #ifdef Q_OS_MAC
870 | if (pathOnly) { return QString(); }
871 | QStringList gs;
872 | gs << "/opt/local/bin/gs" << "/usr/local/bin/gs";
873 | for (int i = 0; i < gs.size(); ++i) { if (QFile::exists(gs.at(i))) { return gs.at(i); } }
874 | return QString();
875 | #endif
876 | return pathOnly ? QString() : QString("gs");
877 | }
878 |
879 | const QString FusePDF::findGhostPdfInfo()
880 | {
881 | QString folder = findGhost(true);
882 | if (!QFile::exists(folder)) { return QString(); }
883 |
884 | QString filename = QString("%1/lib/pdf_info.ps").arg(folder);
885 | qDebug() << "have pdf_info.ps?" << filename;
886 | if (QFile::exists(filename)) { return filename; }
887 |
888 | return QString();
889 | }
890 |
891 | const QString FusePDF::findGhostPdfa(const QString &title)
892 | {
893 | Q_UNUSED(title)
894 | QString cachePath = getCachePath();
895 | if (cachePath.isEmpty() || !QFile::exists(cachePath)) { return QString(); }
896 |
897 | QString iccPath = QString("%1/srgb.icc").arg(cachePath);
898 | QString pdfaPath = QString("%1/PDFA_def.ps").arg(cachePath);
899 | if ((QFile::exists(iccPath) /*&& title.isEmpty()*/) && QFile::exists(pdfaPath)) { return pdfaPath; }
900 |
901 | if (!QFile::exists(pdfaPath) || !title.isEmpty()) {
902 | QString def;
903 | QFile defFile(":/assets/ps/PDFA_def.ps");
904 | if (defFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
905 | def = defFile.readAll();
906 | defFile.close();
907 | }
908 | if (def.isEmpty()) { return QString(); }
909 | def = def.replace("srgb.icc", iccPath);
910 | //if (!title.isEmpty()) { def = def.replace("Title (Title)", QString("Title (%1)").arg(title)); }
911 | QFile pdfa(pdfaPath);
912 | if (pdfa.open(QIODevice::WriteOnly)) {
913 | pdfa.write(def.toUtf8());
914 | pdfa.close();
915 | }
916 | }
917 |
918 | if (!QFile::exists(iccPath)) {
919 | QFile iccfile(":/assets/icc/srgb.icc");
920 | QByteArray iccBuffer;
921 | if (iccfile.open(QIODevice::ReadOnly)) {
922 | iccBuffer = iccfile.readAll();
923 | iccfile.close();
924 | }
925 | if (iccBuffer.size() < 2500) { return QString(); }
926 | QFile srgb(iccPath);
927 | if (srgb.open(QIODevice::WriteOnly)) {
928 | srgb.write(iccBuffer);
929 | srgb.close();
930 | }
931 | }
932 |
933 | if (QFile::exists(iccPath) && QFile::exists(pdfaPath)) { return pdfaPath; }
934 | return QString();
935 | }
936 |
937 | void FusePDF::on_actionShow_log_triggered()
938 | {
939 | ui->cmd->setVisible(ui->actionShow_log->isChecked());
940 | }
941 |
942 | void FusePDF::on_actionAbout_Qt_triggered()
943 | {
944 | QMessageBox::aboutQt(this, QString("%1 Qt").arg(tr("About")));
945 | }
946 |
947 | void FusePDF::handleFoundPDF(const QList &urls)
948 | {
949 | for (int i=0;i< urls.size();++i) {
950 | const QFileInfo info(urls.at(i).toLocalFile());
951 | if (!isPDF(info.filePath()) || hasFile(info.filePath())) { continue; }
952 | int pages = getPageCount(info.filePath());
953 | QString checksum = getChecksum(info.filePath());
954 | QTreeWidgetItem *item = new QTreeWidgetItem(ui->inputs);
955 | item->setData(0, FUSEPDF_PATH_ROLE, info.filePath());
956 | item->setData(0, FUSEPDF_PAGES_ROLE, pages);
957 | item->setData(0, FUSEPDF_CHECKSUM_ROLE, checksum);
958 | item->setText(0, info.fileName());
959 | item->setText(1, QString::number(pages));
960 | item->setText(2, QString::number(pages));
961 | item->setIcon(0, QIcon(FUSEPDF_ICON_MAIN));
962 | item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren);
963 | if (!info.absolutePath().isEmpty()) { _lastLoadDir = info.absolutePath(); }
964 | if (hasTab(info.filePath())) { continue; }
965 | ui->tabs->addTab(new PagesListWidget(this, info.filePath(), checksum, pages),
966 | QIcon(QIcon::fromTheme(HICOLOR_ICON_DOC, QIcon(FUSEPDF_ICON_DOC))),
967 | info.fileName());
968 | // don't set palette on macos as it breaks dark mode
969 | #ifndef Q_OS_MAC
970 | QPalette pal = getTab(info.filePath())->palette();
971 | if (hasDarkMode()) { pal.setColor(QPalette::Base, QColor(30, 30, 30)); }
972 | else { pal.setColor(QPalette::Base, Qt::lightGray); }
973 | getTab(info.filePath())->setPalette(pal);
974 | #endif
975 | connect(this, SIGNAL(foundPagePreview(QString,QString,QString,int)),
976 | getTab(info.filePath()), SLOT(setPageIcon(QString,QString,QString,int)));
977 | connect(getTab(info.filePath()), SIGNAL(requestExportPage(QString,int)),
978 | this, SLOT(handleExport(QString,int)));
979 | connect(getTab(info.filePath()), SIGNAL(requestExportPages(QString,QVector)),
980 | this, SLOT(handleExports(QString,QVector)));
981 | connect(getTab(info.filePath()), SIGNAL(changed()),
982 | this, SLOT(handleOutputPagesChanged()));
983 | QtConcurrent::run(this,
984 | &FusePDF::getPagePreviews,
985 | info.filePath(),
986 | checksum,
987 | pages);
988 | QtConcurrent::run(this,
989 | &FusePDF::getPdfInfo,
990 | info.filePath(),
991 | checksum);
992 | if (ui->actionOutput_preview->isChecked()) {
993 | QtConcurrent::run(this, &FusePDF::generateOutputPreview);
994 | }
995 | }
996 | }
997 |
998 | void FusePDF::on_inputs_itemDoubleClicked(QTreeWidgetItem *item,
999 | int column)
1000 | {
1001 | Q_UNUSED(column)
1002 | if (!item) { return; }
1003 | QString filename = item->data(0, FUSEPDF_PATH_ROLE).toString();
1004 | int index = getTabIndex(filename);
1005 | if (index < 1) { return; }
1006 | ui->tabs->setCurrentIndex(index);
1007 | }
1008 |
1009 | void FusePDF::on_actionAuto_Sort_triggered()
1010 | {
1011 | ui->inputs->setSortingEnabled(ui->actionAuto_Sort->isChecked());
1012 | }
1013 |
1014 | bool FusePDF::hasFile(const QString &file)
1015 | {
1016 | if (file.isEmpty()) { return false; }
1017 | QFileInfo fileInfo(file);
1018 | for (int i = 0; i < ui->inputs->topLevelItemCount(); ++i) {
1019 | QFileInfo inputInfo(ui->inputs->topLevelItem(i)->data(0, FUSEPDF_PATH_ROLE).toString());
1020 | if (inputInfo.absoluteFilePath() == fileInfo.absoluteFilePath()) { return true; }
1021 | }
1022 | return false;
1023 | }
1024 |
1025 | void FusePDF::loadOptions()
1026 | {
1027 | QSettings settings;
1028 | settings.beginGroup("options");
1029 |
1030 | QString savedCompat = settings.value("compat", "1.5").toString();
1031 | ui->compat->blockSignals(true);
1032 | for (int i = 0; i < ui->compat->count(); i++) {
1033 | QString value = ui->compat->itemData(i).toString();
1034 | if (value == savedCompat) {
1035 | ui->compat->setCurrentIndex(i);
1036 | qDebug() << "set compat index" << i << savedCompat;
1037 | break;
1038 | }
1039 | }
1040 | ui->compat->blockSignals(false);
1041 |
1042 | QString savedPreset = settings.value("preset", "Default").toString();
1043 | ui->preset->blockSignals(true);
1044 | for (int i = 0; i < ui->preset->count(); i++) {
1045 | QString value = ui->preset->itemData(i).toString();
1046 | if (value == savedPreset) {
1047 | ui->preset->setCurrentIndex(i);
1048 | qDebug() << "set preset index" << i << savedPreset;
1049 | break;
1050 | }
1051 | }
1052 | ui->preset->blockSignals(false);
1053 |
1054 | #ifdef Q_OS_WIN
1055 | ui->actionSupport_dark_mode->setChecked(settings.value("supportDarkMode", false).toBool());
1056 | #endif
1057 | ui->actionOutput_preview->setChecked(settings.value("outputPreview", true).toBool());
1058 | ui->actionShow_log->setChecked(settings.value("showLog", false).toBool());
1059 | ui->actionAuto_Sort->setChecked(settings.value("autoSort", false).toBool());
1060 | ui->actionRemember_meta_author->setChecked(settings.value("metaAuthor", true).toBool());
1061 | ui->actionRemember_meta_subject->setChecked(settings.value("metaSubject", true).toBool());
1062 | ui->actionRemember_meta_title->setChecked(settings.value("metaTitle", true).toBool());
1063 | _lastLoadDir = settings.value("lastLoadDir", "").toString();
1064 | _lastSaveDir = settings.value("lastSaveDir", "").toString();
1065 | _lastExportDir = settings.value("lastExportDir").toString();
1066 | ui->actionOpen_saved_PDF->setChecked(settings.value("openSavedPDF", true).toBool());
1067 | ui->actionShow_tooltips->setChecked(settings.value("showTooltips", true).toBool());
1068 | showTooltips(settings.value("showTooltips", true).toBool());
1069 | ui->cmd->setVisible(ui->actionShow_log->isChecked());
1070 | ui->inputs->setSortingEnabled(ui->actionAuto_Sort->isChecked());
1071 |
1072 | if (ui->actionRemember_meta_author->isChecked()) {
1073 | QString author = settings.value("metaAuthorText").toString();
1074 | if (!author.isEmpty()) {
1075 | ui->metaAuthor->blockSignals(true);
1076 | ui->metaAuthor->setText(author);
1077 | ui->metaAuthor->blockSignals(false);
1078 | }
1079 | }
1080 | if (ui->actionRemember_meta_subject->isChecked()) {
1081 | QString subject = settings.value("metaSubjectText").toString();
1082 | if (!subject.isEmpty()) {
1083 | ui->metaSubject->blockSignals(true);
1084 | ui->metaSubject->setText(subject);
1085 | ui->metaSubject->blockSignals(false);
1086 | }
1087 | }
1088 | if (ui->actionRemember_meta_title->isChecked()) {
1089 | QString title = settings.value("metaTitleText").toString();
1090 | if (!title.isEmpty()) {
1091 | ui->metaTitle->blockSignals(true);
1092 | ui->metaTitle->setText(title);
1093 | ui->metaTitle->blockSignals(false);
1094 | }
1095 | }
1096 |
1097 | settings.endGroup();
1098 |
1099 | ui->toolBox->setItemEnabled(FUSEPDF_TOOLBOX_PREVIEW,
1100 | ui->actionOutput_preview->isChecked());
1101 | }
1102 |
1103 | void FusePDF::saveOptions()
1104 | {
1105 | QSettings settings;
1106 | settings.beginGroup("options");
1107 |
1108 | #ifdef Q_OS_WIN
1109 | settings.setValue("supportDarkMode", ui->actionSupport_dark_mode->isChecked());
1110 | #endif
1111 | settings.setValue("outputPreview", ui->actionOutput_preview->isChecked());
1112 | settings.setValue("showLog", ui->actionShow_log->isChecked());
1113 | settings.setValue("autoSort", ui->actionAuto_Sort->isChecked());
1114 | settings.setValue("openSavedPDF", ui->actionOpen_saved_PDF->isChecked());
1115 | settings.setValue("showTooltips", ui->actionShow_tooltips->isChecked());
1116 | settings.setValue("metaAuthor", ui->actionRemember_meta_author->isChecked());
1117 | settings.setValue("metaSubject", ui->actionRemember_meta_subject->isChecked());
1118 | settings.setValue("metaTitle", ui->actionRemember_meta_title->isChecked());
1119 |
1120 | if (!_lastLoadDir.isEmpty()) { settings.setValue("lastLoadDir", _lastLoadDir); }
1121 | if (!_lastSaveDir.isEmpty()) { settings.setValue("lastSaveDir", _lastSaveDir); }
1122 | if (!_lastExportDir.isEmpty()) { settings.setValue("lastExportDir", _lastExportDir); }
1123 | settings.endGroup();
1124 | }
1125 |
1126 | bool FusePDF::isNewVersion()
1127 | {
1128 | QSettings settings;
1129 | if (settings.value("version") != VERSION_APP) {
1130 | settings.setValue("version", VERSION_APP);
1131 | return true;
1132 | }
1133 | return false;
1134 | }
1135 |
1136 | bool FusePDF::missingGhost()
1137 | {
1138 | if (findGhost().isEmpty()) {
1139 | QString mac = QString(". %1 uoregon.edu, %3 macports %5 homebrew %7 Ghostscript")
1140 | .arg(tr("On macOS you can find installers from"),
1141 | FUSEPDF_GS_MAC_URL,
1142 | tr("or use"),
1143 | FUSEPDF_GS_MACPORTS_URL,
1144 | tr("or"),
1145 | FUSEPDF_GS_HOMEBREW_URL,
1146 | tr("to install"));
1147 | QMessageBox::warning(this,
1148 | QString("%1 Ghostscript").arg(tr("Missing")),
1149 | QString("%1 Ghostscript, %2 ghostscript.com %3%5.")
1150 | .arg(tr("Unable to find"),
1151 | tr("please download the latest Windows installer from"),
1152 | tr("and install it before running this application again"),
1153 | FUSEPDF_GS_URL,
1154 | mac));
1155 | return true;
1156 | }
1157 | return false;
1158 | }
1159 |
1160 | void FusePDF::handleProcessError(QProcess::ProcessError error)
1161 | {
1162 | _proc->close();
1163 | QString errorMsg;
1164 | switch (error) {
1165 | case QProcess::FailedToStart:
1166 | errorMsg = tr("The process failed to start.");
1167 | break;
1168 | case QProcess::Crashed:
1169 | errorMsg = tr("The process crashed.");
1170 | break;
1171 | default:
1172 | errorMsg = tr("An unknown error occured.");
1173 | break;
1174 | }
1175 | errorMsg.append(QString(" %1").arg(tr("See log (CTRL+L) for more information")));
1176 | QMessageBox::warning(this, tr("Process failed"), errorMsg);
1177 | }
1178 |
1179 | void FusePDF::deleteDocumentItem()
1180 | {
1181 | if (ui->inputs->topLevelItemCount() == 0 || ui->inputs->currentItem() == nullptr) { return; }
1182 | QString filename = ui->inputs->currentItem()->data(0, FUSEPDF_PATH_ROLE).toString();
1183 | delete ui->inputs->currentItem();
1184 |
1185 | int index = getTabIndex(filename);
1186 | if (index < 0) { return; }
1187 | PagesListWidget *tab = getTab(filename);
1188 | if (!tab) { return; }
1189 | ui->tabs->removeTab(index);
1190 | tab->deleteLater();
1191 |
1192 | ui->toolBox->setCurrentIndex(ui->actionOutput_preview->isChecked() ? FUSEPDF_TOOLBOX_PREVIEW : FUSEPDF_TOOLBOX_OUTPUT);
1193 |
1194 | handleOutputPagesChanged();
1195 | }
1196 |
1197 | QByteArray FusePDF::toUtf16Hex(QString str)
1198 | {
1199 | // https://stackoverflow.com/a/38831604
1200 | str.prepend(QChar::ByteOrderMark);
1201 | return QByteArray::fromRawData(reinterpret_cast(str.constData()),
1202 | (str.size()+1)*2).toHex();
1203 | }
1204 |
1205 | int FusePDF::getPageCount(const QString &filename)
1206 | {
1207 | int result = 0;
1208 | if (!isPDF(filename)) { return result; }
1209 |
1210 | QString command = findGhost();
1211 | if (command.isEmpty()) { return result; }
1212 | #ifdef Q_OS_WIN
1213 | command = QString("\"%1\"").arg(findGhost());
1214 | #endif
1215 | command.append(QString(FUSEPDF_GS_COUNT).arg(filename));
1216 |
1217 | QProcess proc;
1218 | proc.start(command);
1219 | proc.waitForFinished();
1220 | result = proc.readAll().replace("PageCount:", "").simplified().toInt();
1221 | proc.close();
1222 |
1223 | qDebug() << command << result;
1224 |
1225 | return result;
1226 | }
1227 |
1228 | bool FusePDF::isFileType(const QString &filename,
1229 | const QString &mime,
1230 | bool startsWith)
1231 | {
1232 | QMimeDatabase db;
1233 | QMimeType type = db.mimeTypeForFile(filename);
1234 | return (startsWith? type.name().startsWith(mime) : type.name() == mime);
1235 | }
1236 |
1237 | bool FusePDF::isPDF(const QString &filename)
1238 | {
1239 | return isFileType(filename, "application/pdf");
1240 | }
1241 |
1242 | bool FusePDF::isJPG(const QString &filename)
1243 | {
1244 | return isFileType(filename, "image/jpeg");
1245 | }
1246 |
1247 | bool FusePDF::isTIFF(const QString &filename)
1248 | {
1249 | return isFileType(filename, "image/tiff");
1250 | }
1251 |
1252 | bool FusePDF::isPNG(const QString &filename)
1253 | {
1254 | return isFileType(filename, "image/png");
1255 | }
1256 |
1257 | bool FusePDF::isImage(const QString &filename)
1258 | {
1259 | return isFileType(filename, "image/", true);
1260 | }
1261 |
1262 | QString FusePDF::ghostImageFormat(int type)
1263 | {
1264 | QString format;
1265 | switch (type) {
1266 | case exportTiffTypeGray:
1267 | format = "tiffgray";
1268 | break;
1269 | case exportTiffTypeRGB12:
1270 | format = "tiff12nc";
1271 | break;
1272 | case exportTiffTypeRGB24:
1273 | format = "tiff24nc";
1274 | break;
1275 | case exportTiffTypeRGB48:
1276 | format = "tiff48nc";
1277 | break;
1278 | case exportTiffTypeCMYK32:
1279 | format = "tiff32nc";
1280 | break;
1281 | case exportTiffTypeCMYK64:
1282 | format = "tiff64nc";
1283 | break;
1284 | case exportPNGTypeGray:
1285 | format = "pnggray";
1286 | break;
1287 | case exportPNGType16:
1288 | format = "png16m";
1289 | break;
1290 | default: break;
1291 | }
1292 | return format;
1293 | }
1294 |
1295 | const QString FusePDF::getCachePath()
1296 | {
1297 | QString path = QDir::tempPath();
1298 | path.append("/fusepdf");
1299 | if (!QFile::exists(path)) {
1300 | QDir dir(path);
1301 | if (!dir.mkpath(path)) { return QString(); }
1302 | }
1303 | return path;
1304 | }
1305 |
1306 | PagesListWidget *FusePDF::getTab(const QString &filename)
1307 | {
1308 | if (filename.isEmpty()) { return nullptr; }
1309 | for (int i = 0; i < ui->tabs->count(); ++i) {
1310 | PagesListWidget *tab = qobject_cast(ui->tabs->widget(i));
1311 | if (tab && tab->getFilename() == filename) { return tab; }
1312 | }
1313 | return nullptr;
1314 | }
1315 |
1316 | bool FusePDF::hasTab(const QString &filename)
1317 | {
1318 | PagesListWidget *tab = getTab(filename);
1319 | if (tab && tab->getFilename() == filename) { return true; }
1320 | return false;
1321 | }
1322 |
1323 | int FusePDF::getTabIndex(const QString &filename)
1324 | {
1325 | int result = -1;
1326 | if (filename.isEmpty()) { return result; }
1327 | for (int i = 0; i < ui->tabs->count(); ++i) {
1328 | PagesListWidget *tab = qobject_cast(ui->tabs->widget(i));
1329 | if (tab && tab->getFilename() == filename) { return i; }
1330 | }
1331 | return result;
1332 | }
1333 |
1334 | const QString FusePDF::getPagePreview(const QString &filename,
1335 | const QString &checksum,
1336 | int page,
1337 | int quality)
1338 | {
1339 | emit statusMessage(tr("Generating preview %1 for %2 ...").arg(page).arg(QFileInfo(filename).fileName()), 1000);
1340 | QString cache = getCachePath();
1341 | if (!isPDF(filename) || cache.isEmpty()) { return QString(); }
1342 |
1343 | QString image = QString(FUSEPDF_CACHE_JPEG).arg(cache, checksum, QString::number(page));
1344 |
1345 | QString command = findGhost();
1346 | if (command.isEmpty()) { return QString(); }
1347 | #ifdef Q_OS_WIN
1348 | command = QString("\"%1\"").arg(findGhost());
1349 | #endif
1350 | command.append(QString(FUSEPDF_GS_PREVIEW).arg(filename).arg(image).arg(page).arg(quality));
1351 |
1352 | QProcess proc;
1353 | proc.start(command);
1354 | proc.waitForFinished();
1355 | proc.close();
1356 |
1357 | qDebug() << command << image;
1358 |
1359 | if (isJPG(image)) { return image; }
1360 | return QString();
1361 | }
1362 |
1363 | void FusePDF::getPagePreviews(const QString &filename,
1364 | const QString &checksum,
1365 | int pages)
1366 | {
1367 | QString cache = getCachePath();
1368 | if (!isPDF(filename) || pages < 1 || cache.isEmpty()) { return; }
1369 | for (int i = 1; i <= pages; ++i) {
1370 | QString image = QString(FUSEPDF_CACHE_JPEG).arg(cache, checksum, QString::number(i));
1371 | if (!QFile::exists(image)) {
1372 | image = getPagePreview(filename, checksum, i);
1373 | }
1374 | if (!image.isEmpty()) { emit foundPagePreview(filename, checksum, image, i); }
1375 | }
1376 | }
1377 |
1378 | const QString FusePDF::getChecksum(const QString &filename)
1379 | {
1380 | if (!isPDF(filename)) { return QString(); }
1381 | QFile file(filename);
1382 | QString result;
1383 | if (file.open(QFile::ReadOnly)) {
1384 | QCryptographicHash hash(QCryptographicHash::Sha256);
1385 | if (hash.addData(&file)) { result = hash.result().toHex(); }
1386 | file.close();
1387 | }
1388 | return result;
1389 | }
1390 |
1391 | const QString FusePDF::extractPDF(const QString &filename,
1392 | const QString &checksum,
1393 | int page,
1394 | bool force)
1395 | {
1396 | emit statusMessage(tr("Extracting pages ..."), 1000);
1397 | QString cache = getCachePath();
1398 | QString command = findGhost();
1399 |
1400 | if (command.isEmpty() || cache.isEmpty()) { return QString(); }
1401 | cache = QString(FUSEPDF_CACHE_PDF).arg(cache, checksum, QString::number(page));
1402 | if (!force && QFile::exists(cache) && isPDF(cache)) { return cache; }
1403 | #ifdef Q_OS_WIN
1404 | command = QString("\"%1\"").arg(findGhost());
1405 | #endif
1406 | command.append(QString(FUSEPDF_GS_EXTRACT).arg(filename).arg(cache).arg(page));
1407 |
1408 | QProcess proc;
1409 | proc.start(command);
1410 | proc.waitForFinished();
1411 | proc.close();
1412 |
1413 | qDebug() << command << cache;
1414 |
1415 | if (isPDF(cache)) { return cache; }
1416 | return QString();
1417 | }
1418 |
1419 | void FusePDF::showProgress(bool progress)
1420 | {
1421 | ui->progressBar->setMaximum(progress?0:100);
1422 | ui->progressBar->setValue(progress?0:100);
1423 | ui->progressBar->setHidden(!progress);
1424 | }
1425 |
1426 | void FusePDF::on_actionOpen_cache_folder_triggered()
1427 | {
1428 | QDesktopServices::openUrl(QUrl::fromUserInput(getCachePath()));
1429 | }
1430 |
1431 | void FusePDF::on_actionShow_tooltips_triggered()
1432 | {
1433 | showTooltips(ui->actionShow_tooltips->isChecked());
1434 | }
1435 |
1436 | void FusePDF::showTooltips(bool show)
1437 | {
1438 | ui->metaTitleLabel->setToolTip(show?tr("Set custom document title"):QString());
1439 | ui->metaTitle->setToolTip(show?ui->metaTitleLabel->toolTip():QString());
1440 | ui->metaAuthorLabel->setToolTip(show?tr("Set custom document author"):QString());
1441 | ui->metaAuthor->setToolTip(show?ui->metaAuthorLabel->toolTip():QString());
1442 | ui->metaSubjectLabel->setToolTip(show?tr("Set custom document subject"):QString());
1443 | ui->metaSubject->setToolTip(show?ui->metaSubjectLabel->toolTip():QString());
1444 | ui->presetLabel->setToolTip(show?tr("Distiller presets\n\n"
1445 | "- DEFAULT: selects output intended to be useful across a wide variety of uses, possibly at the expense of a larger output file.\n"
1446 | "- PREPRESS: selects output similar to Acrobat Distiller \"Prepress Optimized\" setting.\n"
1447 | "- EBOOK: selects medium-resolution output similar to the Acrobat Distiller \"eBook\" setting.\n"
1448 | "- SCREEN: selects low-resolution output similar to the Acrobat Distiller \"Screen Optimized\" setting.\n"
1449 | "- PRINTER: selects output similar to the Acrobat Distiller \"Print Optimized\" setting."):QString());
1450 | ui->preset->setToolTip(show?ui->presetLabel->toolTip():QString());
1451 | ui->compatLabel->setToolTip(show?tr("Select the PDF version this document should be compatible with."):QString());
1452 | ui->compat->setToolTip(show?ui->compatLabel->toolTip():QString());
1453 | /*ui->inputs->setToolTip(show?tr("Drag and drop PDF documents you want to merge here. You can re-arrange after adding them (if sorting is disabled).\n\n"
1454 | "Note that the first document will define the paper size on the final output.\n\n"
1455 | "You can remove a document with the DEL key."):QString());*/
1456 | }
1457 |
1458 | void FusePDF::on_actionCheck_for_updates_triggered()
1459 | {
1460 | QDesktopServices::openUrl(QUrl::fromUserInput(FUSEPDF_RELEASES_URL));
1461 | }
1462 |
1463 | void FusePDF::on_actionReport_issue_triggered()
1464 | {
1465 | QDesktopServices::openUrl(QUrl::fromUserInput(FUSEPDF_ISSUE_URL));
1466 | }
1467 |
1468 | bool FusePDF::exportImage(const QString &filename,
1469 | const QString &image,
1470 | int page,
1471 | int type,
1472 | int res,
1473 | int alpha)
1474 | {
1475 | if (!isPDF(filename) || image.isEmpty() || page < 1 || res < 72 || alpha < 1) {
1476 | return false;
1477 | }
1478 | QString format = ghostImageFormat(type);
1479 | QString command = findGhost();
1480 |
1481 | if (command.isEmpty() || format.isEmpty()) { return false; }
1482 | #ifdef Q_OS_WIN
1483 | command = QString("\"%1\"").arg(findGhost());
1484 | #endif
1485 | command.append(QString(FUSEPDF_GS_EXPORT).arg(filename).arg(image).arg(page).arg(format).arg(res));
1486 |
1487 | QProcess proc;
1488 | proc.start(command);
1489 | proc.waitForFinished();
1490 | proc.close();
1491 |
1492 | qDebug() << command << image;
1493 |
1494 | if (isImage(image)) { return true; }
1495 | return false;
1496 | }
1497 |
1498 | void FusePDF::handleExport(const QString &filename, int page)
1499 | {
1500 | QString image = QFileDialog::getSaveFileName(this,
1501 | tr("Save Image"),
1502 | !_lastExportDir.isEmpty()?_lastExportDir:QDir::homePath(),
1503 | "*.tif *.tiff *.png");
1504 | if (image.isEmpty()) { return; }
1505 | ExportImageDialog dialog(this, Qt::WindowFlags(), QFileInfo(image).suffix());
1506 | int d = dialog.exec();
1507 | if (d != QDialog::Accepted) { return; }
1508 | int type = dialog.getImageType();
1509 | int res = dialog.getImageRes();
1510 | QVector pages;
1511 | pages << page;
1512 |
1513 | showProgress(true);
1514 | QtConcurrent::run(this,
1515 | &FusePDF::doExport,
1516 | filename,
1517 | image,
1518 | pages,
1519 | type,
1520 | res);
1521 | }
1522 |
1523 | void FusePDF::handleExports(const QString &filename,
1524 | QVector pages)
1525 | {
1526 | QString image = QFileDialog::getSaveFileName(this,
1527 | tr("Save Image"),
1528 | !_lastExportDir.isEmpty()?_lastExportDir:QDir::homePath(),
1529 | "*.tif *.tiff *.png");
1530 | if (image.isEmpty()) { return; }
1531 | ExportImageDialog dialog(this, Qt::WindowFlags(), QFileInfo(image).suffix());
1532 | int d = dialog.exec();
1533 | if (d != QDialog::Accepted) { return; }
1534 | int type = dialog.getImageType();
1535 | int res = dialog.getImageRes();
1536 |
1537 | showProgress(true);
1538 | QtConcurrent::run(this,
1539 | &FusePDF::doExport,
1540 | filename,
1541 | image,
1542 | pages,
1543 | type,
1544 | res);
1545 | }
1546 |
1547 | void FusePDF::doExport(const QString &filename,
1548 | const QString &image,
1549 | QVector pages,
1550 | int type,
1551 | int res)
1552 | {
1553 | QFileInfo info(image);
1554 | for (int i = 0; i < pages.size(); ++i) {
1555 | emit statusMessage(tr("Exporting image %1 from %2 ...")
1556 | .arg(pages.at(i))
1557 | .arg(QFileInfo(filename).fileName()),
1558 | 1000);
1559 | if (!exportImage(filename,
1560 | QString("%1/%2-%3.%4")
1561 | .arg(info.dir().absolutePath(),
1562 | info.baseName(),
1563 | QString::number(pages.at(i)),
1564 | info.suffix()),
1565 | pages.at(i),
1566 | type,
1567 | res))
1568 | {
1569 | emit exportDone(QString());
1570 | return;
1571 | }
1572 | }
1573 | if (pages.size() < 2) {
1574 | emit exportDone(QString("%1/%2-%3.%4")
1575 | .arg(info.dir().absolutePath(),
1576 | info.baseName(),
1577 | QString::number(pages.at(0)),
1578 | info.suffix()));
1579 | } else {
1580 | emit exportDone(info.dir().absolutePath());
1581 | }
1582 | }
1583 |
1584 | void FusePDF::handleExportDone(const QString &path)
1585 | {
1586 | qDebug() << "export done" << path;
1587 | showProgress(false);
1588 | if (path.isEmpty()) {
1589 | QMessageBox::warning(this,
1590 | tr("Failed to export"),
1591 | tr("Failed to export image(s)."));
1592 | return;
1593 | }
1594 | if (_lastExportDir != QFileInfo(path).dir().absolutePath()) {
1595 | _lastExportDir = QFileInfo(path).dir().absolutePath();
1596 | }
1597 | QDesktopServices::openUrl(QUrl::fromUserInput(path));
1598 | }
1599 |
1600 | void FusePDF::on_preset_currentIndexChanged(int index)
1601 | {
1602 | if (index >= ui->preset->count()) { return; }
1603 | QString newPreset = ui->preset->itemData(index).toString();
1604 |
1605 | QSettings settings;
1606 | settings.beginGroup("options");
1607 | QString savedPreset = settings.value("preset").toString();
1608 | if (newPreset != savedPreset && !newPreset.isEmpty()) {
1609 | settings.setValue("preset", newPreset);
1610 | qDebug() << "updated preset settings" << newPreset << "vs." << savedPreset;
1611 | }
1612 | settings.endGroup();
1613 | }
1614 |
1615 | void FusePDF::on_compat_currentIndexChanged(int index)
1616 | {
1617 | if (index >= ui->compat->count()) { return; }
1618 | QString newCompat = ui->compat->itemData(index).toString();
1619 |
1620 | QSettings settings;
1621 | settings.beginGroup("options");
1622 | QString savedCompat = settings.value("compat").toString();
1623 | if (newCompat != savedCompat && !newCompat.isEmpty()) {
1624 | settings.setValue("compat", newCompat);
1625 | qDebug() << "updated compat settings" << newCompat << "vs." << savedCompat;
1626 | }
1627 | settings.endGroup();
1628 | }
1629 |
1630 | void FusePDF::on_metaTitle_textChanged(const QString &arg1)
1631 | {
1632 | QSettings settings;
1633 | settings.beginGroup("options");
1634 | if (ui->actionRemember_meta_title->isChecked()) {
1635 | settings.setValue("metaTitleText", arg1);
1636 | }
1637 | settings.endGroup();
1638 | }
1639 |
1640 | void FusePDF::on_metaSubject_textChanged(const QString &arg1)
1641 | {
1642 | QSettings settings;
1643 | settings.beginGroup("options");
1644 | if (ui->actionRemember_meta_subject->isChecked()) {
1645 | settings.setValue("metaSubjectText", arg1);
1646 | }
1647 | settings.endGroup();
1648 | }
1649 |
1650 | void FusePDF::on_metaAuthor_textChanged(const QString &arg1)
1651 | {
1652 | QSettings settings;
1653 | settings.beginGroup("options");
1654 | if (ui->actionRemember_meta_author->isChecked()) {
1655 | settings.setValue("metaAuthorText", arg1);
1656 | }
1657 | settings.endGroup();
1658 | }
1659 |
1660 | void FusePDF::on_tabs_currentChanged(int index)
1661 | {
1662 | if (index == 0) {
1663 | _tabButton->setIcon(QIcon(QIcon::fromTheme(HICOLOR_ICON_CLEAR,
1664 | QIcon(FUSEPDF_ICON_CLEAR))));
1665 | _tabButton->setToolTip(tr("Clear"));
1666 | } else {
1667 | _tabButton->setIcon(QIcon(QIcon::fromTheme(HICOLOR_ICON_GOFIRST,
1668 | QIcon(FUSEPDF_ICON_GOFIRST))));
1669 | _tabButton->setToolTip(tr("Show documents"));
1670 | }
1671 | }
1672 |
1673 | void FusePDF::handleTabButtonClicked(bool checked)
1674 | {
1675 | Q_UNUSED(checked)
1676 | int index = ui->tabs->currentIndex();
1677 | if (index == 0) {
1678 | clearInput(true);
1679 | } else if (index > 0) {
1680 | ui->tabs->setCurrentIndex(0);
1681 | }
1682 | }
1683 |
1684 | bool FusePDF::hasDarkMode()
1685 | {
1686 | #ifdef Q_OS_WIN
1687 | QSettings options;
1688 | options.beginGroup("options");
1689 | bool supportDarkMode = options.value("supportDarkMode", false).toBool();
1690 | options.endGroup();
1691 | if (!supportDarkMode) { return false; }
1692 | QSettings settings("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", QSettings::NativeFormat);
1693 | if (settings.value("AppsUseLightTheme") == 0) { return true; }
1694 | #endif
1695 | return false;
1696 | }
1697 |
1698 | void FusePDF::generateOutputPreview()
1699 | {
1700 | QString cache = getCachePath();
1701 | if (cache.isEmpty()) { return; }
1702 | QStringList docs, images;
1703 | for (int i = 0; i < ui->inputs->topLevelItemCount(); ++i) {
1704 | QString filename = ui->inputs->topLevelItem(i)->data(0, FUSEPDF_PATH_ROLE).toString();
1705 | PagesListWidget *tab = getTab(filename);
1706 | bool modified = false;
1707 | int enabledPages = tab? tab->getPagesState(true).size() : 0;
1708 | if (tab && tab->isModified() && enabledPages > 0) {
1709 | modified = true;
1710 | QVector pages = tab->getPagesState(true);
1711 | for (int i = 0; i < pages.count(); ++i) {
1712 | QString extracted = extractPDF(filename, getChecksum(filename), pages.at(i), false);
1713 | if (!extracted.isEmpty()) {
1714 | docs << extracted;
1715 | QString checksum = getChecksum(filename);
1716 | QString image = QString(FUSEPDF_CACHE_JPEG).arg(cache, checksum, QString::number(pages.at(i)));
1717 | if (!QFile::exists(image)) { image = getPagePreview(filename, checksum, i); }
1718 | if (!image.isEmpty()) { images << image; }
1719 | }
1720 | }
1721 | }
1722 | if (enabledPages < 1) { continue; }
1723 | if (!modified) {
1724 | docs << filename;
1725 | int pages = getPageCount(filename);
1726 | QString checksum = getChecksum(filename);
1727 | for (int i = 1; i <= pages; ++i) {
1728 | QString image = QString(FUSEPDF_CACHE_JPEG).arg(cache, checksum, QString::number(i));
1729 | if (!QFile::exists(image)) { image = getPagePreview(filename, checksum, i); }
1730 | if (!image.isEmpty() && !images.contains(image)) { images << image; }
1731 | }
1732 | }
1733 | }
1734 | if (images.size() > 0) { emit generatedOutputPreview(images); }
1735 | }
1736 |
1737 | void FusePDF::handleOutputPagesChanged()
1738 | {
1739 | if (!ui->actionOutput_preview->isChecked()) { return; }
1740 | int totalPages = pagesToExport();
1741 | if (totalPages < 1) {
1742 | ui->preview->clear();
1743 | return;
1744 | }
1745 | QtConcurrent::run(this, &FusePDF::generateOutputPreview);
1746 | }
1747 |
1748 | void FusePDF::showOutputPreview(const QStringList &images)
1749 | {
1750 | if (!ui->actionOutput_preview->isChecked()) { return; }
1751 | if (images.size() < 1) { return; }
1752 | ui->preview->clear();
1753 | ui->preview->show();
1754 | for (int i = 0; i < images.size(); ++i) {
1755 | QListWidgetItem *item = new QListWidgetItem(QIcon(FUSEPDF_ICON_LOGO),
1756 | QString(),
1757 | ui->preview);
1758 | item->setFlags(Qt::ItemIsEnabled);
1759 | QPixmap pix(FUSEPDF_PAGE_ICON_SIZE, FUSEPDF_PAGE_ICON_SIZE);
1760 | pix.fill(QColor(Qt::transparent));
1761 | QPainter p(&pix);
1762 | QPixmap ppix = QPixmap::fromImage(QImage(images.at(i))).scaledToHeight(FUSEPDF_PAGE_ICON_SIZE,
1763 | Qt::SmoothTransformation);
1764 | ppix = ppix.copy(0, 0, FUSEPDF_PAGE_ICON_SIZE, FUSEPDF_PAGE_ICON_SIZE);
1765 | QPainter pp(&ppix);
1766 | QPainterPath ppath;
1767 | ppath.addRect(0, 0, ppix.width(), ppix.height());
1768 | QPen ppen(Qt::black, 2);
1769 | pp.setPen(ppen);
1770 | pp.drawPath(ppath);
1771 | p.drawPixmap((pix.width()/2)-(ppix.width()/2), 0, ppix);
1772 | item->setIcon(pix);
1773 | }
1774 | if (ui->actionOutput_preview->isChecked()) { ui->toolBox->setCurrentIndex(FUSEPDF_TOOLBOX_PREVIEW); }
1775 | }
1776 |
1777 | void FusePDF::handleOutputRemoveSelected()
1778 | {
1779 | deleteDocumentItem();
1780 | }
1781 |
1782 | void FusePDF::handleOutputClearAll()
1783 | {
1784 | clearInput(true);
1785 | }
1786 |
1787 | void FusePDF::handleOutputAdd()
1788 | {
1789 | on_actionOpen_triggered();
1790 | }
1791 |
1792 |
1793 | void FusePDF::on_actionDocumentation_triggered()
1794 | {
1795 | QString url = "https://fusepdf.no";
1796 | QString readme = QString("%1/README.pdf").arg(qApp->applicationDirPath());
1797 | if (QFile::exists(readme)) { url = readme; }
1798 | QDesktopServices::openUrl(QUrl::fromUserInput(url));
1799 | }
1800 |
1801 |
1802 | void FusePDF::on_actionOutput_preview_triggered()
1803 | {
1804 | bool checked = ui->actionOutput_preview->isChecked();
1805 | ui->toolBox->setItemEnabled(FUSEPDF_TOOLBOX_PREVIEW, checked);
1806 | ui->preview->clear();
1807 | if (checked) {
1808 | ui->toolBox->setCurrentIndex(FUSEPDF_TOOLBOX_PREVIEW);
1809 | handleOutputPagesChanged();
1810 | } else {
1811 | ui->toolBox->setCurrentIndex(FUSEPDF_TOOLBOX_OUTPUT);
1812 | }
1813 | }
1814 |
1815 | int FusePDF::pagesToExport()
1816 | {
1817 | int result = 0;
1818 | for (int i = 0; i < ui->inputs->topLevelItemCount(); ++i) {
1819 | QString filename = ui->inputs->topLevelItem(i)->data(0, FUSEPDF_PATH_ROLE).toString();
1820 | PagesListWidget *tab = getTab(filename);
1821 | bool modified = false;
1822 | int enabledPages = tab? tab->getPagesState(true).size() : 0;
1823 | if (tab && tab->isModified() && enabledPages > 0) {
1824 | modified = true;
1825 | QVector pages = tab->getPagesState(true);
1826 | for (int i = 0; i < pages.count(); ++i) { result++; }
1827 | }
1828 | ui->inputs->topLevelItem(i)->setText(1, QString::number(enabledPages));
1829 | if (enabledPages < 1) { continue; }
1830 | if (!modified) {
1831 | result += ui->inputs->topLevelItem(i)->text(1).toInt();
1832 | }
1833 | }
1834 | return result;
1835 | }
1836 |
1837 | void FusePDF::getPdfInfo(const QString &filename,
1838 | const QString &checksum)
1839 | {
1840 | QString ps = FusePDF::findGhostPdfInfo();
1841 | if (!isPDF(filename) || ps.isEmpty() || checksum.isEmpty()) { return; }
1842 |
1843 | QString command = findGhost();
1844 | if (command.isEmpty()) { return; }
1845 | #ifdef Q_OS_WIN
1846 | command = QString("\"%1\"").arg(findGhost());
1847 | #endif
1848 | command.append(QString(FUSEPDF_GS_INFO).arg(filename, ps));
1849 |
1850 | FusePDF::pdfInfo pdf;
1851 | pdf.filename = filename;
1852 | pdf.checksum = checksum;
1853 |
1854 | QProcess proc;
1855 | proc.start(command);
1856 | proc.waitForFinished();
1857 | pdf.info = proc.readAll();
1858 | proc.close();
1859 |
1860 | qDebug() << command;
1861 | if (!pdf.info.isEmpty()) { emit foundPdfInfo(pdf); }
1862 | }
1863 |
1864 | void FusePDF::handleFoundPdfInfo(const pdfInfo &pdf)
1865 | {
1866 | if (pdf.filename.isEmpty() || pdf.checksum.isEmpty() || pdf.info.isEmpty()) { return; }
1867 | for (int i = 0; i < ui->inputs->topLevelItemCount(); ++i) {
1868 | QString filename = ui->inputs->topLevelItem(i)->data(0, FUSEPDF_PATH_ROLE).toString();
1869 | QString checksum = ui->inputs->topLevelItem(i)->data(0, FUSEPDF_CHECKSUM_ROLE).toString();
1870 | if (filename == pdf.filename && checksum == pdf.checksum) {
1871 | ui->inputs->topLevelItem(i)->setToolTip(0, pdf.info);
1872 | break;
1873 | }
1874 | }
1875 | }
1876 |
1877 | const QString FusePDF::stripMarks(QString s)
1878 | {
1879 | s.remove(QRegularExpression("[" + QRegularExpression::escape("'!*,?|¡¿") + "]"));
1880 | if (s.contains(QRegularExpression("[" + QRegularExpression::escape("$/:ÅØÆåøæÀÁÄÙÛÜàáäçèéêëïñóöùûü") + "]"))) {
1881 | s.replace(QRegularExpression("[" + QRegularExpression::escape(":/") + "]"), "-");
1882 | s.replace(QRegularExpression("[$]"), "s");
1883 |
1884 | s.replace(QRegularExpression("[Å]"), "AA");
1885 | s.replace(QRegularExpression("[Ø]"), "OE");
1886 | s.replace(QRegularExpression("[Æ]"), "AE");
1887 |
1888 | s.replace(QRegularExpression("[ÁÀ]"), "A");
1889 | s.replace(QRegularExpression("[Ä]"), "Ae");
1890 | s.replace(QRegularExpression("[ÜÛÙ]"), "U");
1891 |
1892 | s.replace(QRegularExpression("[å]"), "aa");
1893 | s.replace(QRegularExpression("[ø]"), "oe");
1894 | s.replace(QRegularExpression("[æ]"), "ae");
1895 |
1896 | s.replace(QRegularExpression("[áà]"), "a");
1897 | s.replace(QRegularExpression("[ä]"), "ae");
1898 | s.replace(QRegularExpression("[ç]"), "c");
1899 | s.replace(QRegularExpression("[ëêéè]"), "e");
1900 | s.replace(QRegularExpression("[ï]"), "i");
1901 | s.replace(QRegularExpression("[ñ]"), "n");
1902 | s.replace(QRegularExpression("[óö]"), "o");
1903 | s.replace(QRegularExpression("[ûù]"), "u");
1904 | s.replace(QRegularExpression("[ü]"), "ue");
1905 | }
1906 | return s;
1907 | }
1908 |
1909 | const QString FusePDF::getCacheSize()
1910 | {
1911 | QString cachePath = getCachePath();
1912 | if (QFile::exists(cachePath)) {
1913 | QDirIterator it (cachePath, QDirIterator::Subdirectories);
1914 | qint64 total = 0;
1915 | while (it.hasNext()) {
1916 | it.next();
1917 | total += it.fileInfo().size();
1918 | }
1919 | if (total > 0) {
1920 | QLocale locale = this->locale();
1921 | QString value = locale.formattedDataSize(total);
1922 | return value;
1923 | }
1924 | }
1925 | return QString();
1926 | }
1927 |
1928 | void FusePDF::on_actionClear_cache_triggered()
1929 | {
1930 | QString cachePath = getCachePath();
1931 | QString cacheSize = getCacheSize();
1932 | if (cacheSize.isEmpty() || cachePath.isEmpty() || !QFile::exists(cachePath)) { return; }
1933 | QString info = QString("%1 %2 %4:
%5?
%6.").arg(tr("Are you sure you want to remove"),
1934 | cacheSize,
1935 | tr("from"),
1936 | cachePath,
1937 | tr("Everything in the folder will be removed"));
1938 | int ret = QMessageBox::question(this,
1939 | tr("Clear cache?"),
1940 | info,
1941 | QMessageBox::Yes | QMessageBox::No,
1942 | QMessageBox::No);
1943 | if (ret == QMessageBox::Yes) {
1944 | QDir dir(cachePath);
1945 | dir.removeRecursively();
1946 | getCachePath();
1947 | }
1948 | }
1949 |
1950 |
--------------------------------------------------------------------------------
/src/fusepdf.h:
--------------------------------------------------------------------------------
1 | /*
2 | #
3 | # FusePDF - https://fusepdf.no
4 | #
5 | # Copyright (c) 2021, 2022 NettStudio AS .
6 | # All rights reserved.
7 | #
8 | # This program is free software; you can redistribute it and/or modify
9 | # it under the terms of the GNU General Public License as published by
10 | # the Free Software Foundation; either version 3 of the License, or
11 | # (at your option) any later version.
12 | #
13 | # This program is distributed in the hope that it will be useful,
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | # GNU General Public License for more details.
17 | #
18 | # You should have received a copy of the GNU General Public License
19 | # along with this program. If not, see
20 | #
21 | */
22 |
23 | #ifndef FUSEPDF_H
24 | #define FUSEPDF_H
25 |
26 | #include
27 | #include
28 | #include
29 | #include
30 | #include
31 | #include
32 | #include
33 | #include
34 | #include
35 | #include
36 | #include
37 | #include
38 | #include
39 | #include
40 | #include
41 | #include
42 | #include
43 | #include
44 | #include
45 | #include
46 | #include
47 | #include
48 | #include
49 | #include
50 | #include
51 | #include
52 | #include
53 | #include
54 | #include
55 | #include
56 | #include
57 | #include
58 | #include
59 | #include
60 | #include
61 | #include
62 | #include
63 | #include
64 | #include
65 | #include
66 | #include
67 | #include
68 |
69 | #define FUSEPDF_SITE_URL "https://fusepdf.no"
70 | #define FUSEPDF_RELEASES_URL "https://github.com/nettstudio/fusepdf/releases"
71 | #define FUSEPDF_ISSUE_URL "https://github.com/nettstudio/fusepdf/issues"
72 | #define FUSEPDF_GS_URL "https://www.ghostscript.com/download/gsdnld.html"
73 | #define FUSEPDF_GS_MAC_URL "https://pages.uoregon.edu/koch/"
74 | #define FUSEPDF_GS_MACPORTS_URL "https://ports.macports.org/port/ghostscript/"
75 | #define FUSEPDF_GS_HOMEBREW_URL "https://formulae.brew.sh/formula/ghostscript"
76 |
77 | #define FUSEPDF_TOOLBOX_PREVIEW 0
78 | #define FUSEPDF_TOOLBOX_OUTPUT 1
79 |
80 | #define FUSEPDF_PATH_ROLE Qt::UserRole + 1
81 | #define FUSEPDF_PAGES_ROLE Qt::UserRole + 2
82 | #define FUSEPDF_PAGE_ROLE Qt::UserRole + 3
83 | #define FUSEPDF_SELECTED_ROLE Qt::UserRole + 4
84 | #define FUSEPDF_CHECKSUM_ROLE Qt::UserRole + 5
85 | #define FUSEPDF_IMAGE_TYPE_ROLE Qt::UserRole + 6
86 | #define FUSEPDF_IMAGE_RES_ROLE Qt::UserRole + 7
87 | #define FUSEPDF_CHECKED_ROLE Qt::UserRole + 8
88 | #define FUSEPDF_PDFA_ROLE Qt::userRole + 9
89 |
90 | #define FUSEPDF_PAGE_ICON_SIZE 470
91 | #define FUSEPDF_ICON_DEFAULT_SIZE 22
92 |
93 | #define FUSEPDF_CACHE_JPEG "%1/%2-%3.jpg"
94 | #define FUSEPDF_CACHE_PDF "%1/%2-%3.pdf"
95 |
96 | #define FUSEPDF_ICON_MAIN ":/assets/fusepdf-document.png"
97 | #define FUSEPDF_ICON_DOC ":/assets/document.png"
98 | #define FUSEPDF_ICON_LOGO ":/assets/fusepdf.png"
99 | #define FUSEPDF_ICON_CLEAR ":/assets/fusepdf-clear.png"
100 | #define FUSEPDF_ICON_VALID ":/assets/fusepdf-valid.png"
101 | #define FUSEPDF_ICON_DENIED ":/assets/fusepdf-denied.png"
102 | #define FUSEPDF_ICON_GOFIRST ":/assets/fusepdf-gofirst.png"
103 | #define FUSEPDF_ICON_OPEN ":/assets/fusepdf-open.png"
104 | #define FUSEPDF_ICON_SAVE ":/assets/fusepdf-save.png"
105 | #define FUSEPDF_ICON_QUIT ":/assets/fusepdf-quit.png"
106 |
107 | #define HICOLOR_ICON_DOC "document"
108 | #define HICOLOR_ICON_CLEAR "edit-clear"
109 | #define HICOLOR_ICON_VALID "dialog-apply"
110 | #define HICOLOR_ICON_DENIED "dialog-cancel"
111 | #define HICOLOR_ICON_GOFIRST "go-first"
112 | #define HICOLOR_ICON_OPEN "document-open"
113 | #define HICOLOR_ICON_SAVE "document-save"
114 | #define HICOLOR_ICON_QUIT "appliction-exit"
115 |
116 | #define FUSEPDF_GS_PREVIEW " -q -sDEVICE=jpeg -o \"%2\" -dFirstPage=%3 -dLastPage=%3 -dJPEGQ=%4 -r72x72 \"%1\""
117 | #define FUSEPDF_GS_EXPORT " -q -sDEVICE=%4 -o \"%2\" -dFirstPage=%3 -dLastPage=%3 -dTextAlphaBits=4 -dGraphicsAlphaBits=4 -r%5x%5 \"%1\""
118 | #define FUSEPDF_GS_COUNT " -q -dNODISPLAY -dNOSAFER -c \"/pdffile (%1) (r) file runpdfbegin (PageCount: ) print pdfpagecount = quit\""
119 | #define FUSEPDF_GS_EXTRACT " -q -dNOPAUSE -dBATCH -sOutputFile=\"%2\" -dFirstPage=%3 -dLastPage=%3 -sDEVICE=pdfwrite \"%1\""
120 | #define FUSEPDF_GS_INFO " -q -dNODISPLAY -dNOSAFER -sFile=\"%1\" -dDumpMediaSizes=false -dDumpFontsUsed=false \"%2\""
121 |
122 | enum exportImageType {
123 | exportImageTypeUndefined,
124 | exportTiffTypeGray,
125 | exportTiffTypeRGB12,
126 | exportTiffTypeRGB24,
127 | exportTiffTypeRGB48,
128 | exportTiffTypeCMYK32,
129 | exportTiffTypeCMYK64,
130 | exportPNGTypeGray,
131 | exportPNGType16
132 | };
133 |
134 | class ExportImageDialog : public QDialog
135 | {
136 | Q_OBJECT
137 |
138 | public:
139 | ExportImageDialog(QWidget *parent = nullptr,
140 | Qt::WindowFlags f = Qt::WindowFlags(),
141 | QString suffix = QString());
142 | int getImageType();
143 | int getImageRes();
144 |
145 | private slots:
146 | void handleButtonCancel();
147 | void handleButtonOk();
148 |
149 | private:
150 | QComboBox *_type;
151 | QComboBox *_res;
152 | };
153 |
154 | class PageDelegate : public QStyledItemDelegate
155 | {
156 | Q_OBJECT
157 |
158 | public:
159 | using QStyledItemDelegate::QStyledItemDelegate;
160 |
161 | void paint(QPainter *painter,
162 | const QStyleOptionViewItem &option,
163 | const QModelIndex &index) const override
164 | {
165 | QStyledItemDelegate::paint(painter, option, index);
166 | painter->save();
167 |
168 | int dim = FUSEPDF_ICON_DEFAULT_SIZE;
169 | int x = option.rect.x() + ((option.rect.width()/2) - (dim/2));
170 | int y = option.rect.y() + (option.rect.height() - dim) - (dim/2);
171 |
172 | if (index.data(FUSEPDF_CHECKED_ROLE).isValid()) {
173 | QPixmap pix;
174 | pix.load(index.data(FUSEPDF_CHECKED_ROLE).toBool()? FUSEPDF_ICON_VALID : FUSEPDF_ICON_DENIED);
175 | painter->drawPixmap(QRect(x, y, dim, dim), pix);
176 | }
177 |
178 | painter->restore();
179 | }
180 | };
181 |
182 | class PagesListWidget : public QListWidget
183 | {
184 | Q_OBJECT
185 |
186 | public:
187 | PagesListWidget(QWidget *parent = nullptr,
188 | const QString &filename = QString(),
189 | const QString &checksum = QString(),
190 | int pages = 0);
191 | const QString getFilename() { return _filename; }
192 | const QString getChecksum() { return _checksum; }
193 | int getPageCount() { return _pages; }
194 | bool isModified();
195 | QVector getPagesState(bool state);
196 |
197 | signals:
198 | void requestExportPage(const QString &filename,
199 | int page);
200 | void requestExportPages(const QString &filename,
201 | QVector pages);
202 | void changed();
203 |
204 | public slots:
205 | void setPageIcon(const QString &filename,
206 | const QString &checksum,
207 | const QString &image,
208 | int page);
209 |
210 | private slots:
211 | void handleItemClicked(QListWidgetItem *item);
212 | void handleContextMenu(QPoint pos);
213 | void selectAllPages();
214 | void selectNoPages();
215 | void setCheckedState(bool state);
216 | void exportSelectedPage();
217 | void exportSelectedPages();
218 |
219 | private:
220 | QString _filename;
221 | QString _checksum;
222 | int _pages;
223 | };
224 |
225 | class FilesTreeWidget : public QTreeWidget
226 | {
227 | Q_OBJECT
228 |
229 | public:
230 | FilesTreeWidget(QWidget *parent = nullptr);
231 |
232 | signals:
233 | void foundPDF(const QList &urls);
234 | void changed();
235 | void removeSelected();
236 | void clearAll();
237 | void add();
238 |
239 | private slots:
240 | void handleContextMenu(QPoint pos);
241 | void handleRemoveSelectedAction();
242 | void handleClearAllAction();
243 | void handleAddAction();
244 | void delayedChanged();
245 |
246 | protected:
247 | void dragEnterEvent(QDragEnterEvent *e) { e->acceptProposedAction(); }
248 | void dropEvent(QDropEvent *e);
249 | };
250 |
251 | QT_BEGIN_NAMESPACE
252 | namespace Ui { class FusePDF; }
253 | QT_END_NAMESPACE
254 |
255 | class FusePDF : public QMainWindow
256 | {
257 | Q_OBJECT
258 |
259 | public:
260 | struct pdfInfo {
261 | QString filename;
262 | QString checksum;
263 | QString info;
264 | };
265 |
266 | FusePDF(QWidget *parent = nullptr);
267 | ~FusePDF();
268 |
269 | signals:
270 | void foundPagePreview(const QString &filename,
271 | const QString &checksum,
272 | const QString &image,
273 | int page);
274 | void commandReady(const QString &filename,
275 | const QString &command);
276 | void statusMessage(const QString &message,
277 | int timeout);
278 | void exportDone(const QString &path);
279 | void generatedOutputPreview(const QStringList &images);
280 | void foundPdfInfo(const FusePDF::pdfInfo &pdf);
281 |
282 | private slots:
283 | void on_actionOpen_triggered();
284 | void on_actionSave_triggered();
285 | void on_actionQuit_triggered();
286 | void on_actionAbout_triggered();
287 | void prepCommand(const QString &filename);
288 | const QString makeCommand(const QString &filename,
289 | int pdfa = 0);
290 | void runCommand(const QString &filename,
291 | const QString &command);
292 | void commandStarted();
293 | void commandFinished(int exitCode);
294 | void populateUI();
295 | void loadSettings();
296 | void saveSettings();
297 | void handleProcOutput();
298 | void clearInput(bool askFirst = false);
299 | static const QString findGhost(bool pathOnly = false);
300 | static const QString findGhostPdfInfo();
301 | static const QString findGhostPdfa(const QString &title = QString());
302 | void on_actionShow_log_triggered();
303 | void on_actionAbout_Qt_triggered();
304 | void handleFoundPDF(const QList &urls);
305 | void on_inputs_itemDoubleClicked(QTreeWidgetItem *item,
306 | int column);
307 | void on_actionAuto_Sort_triggered();
308 | bool hasFile(const QString &file);
309 | void loadOptions();
310 | void saveOptions();
311 | bool isNewVersion();
312 | bool missingGhost();
313 | void handleProcessError(QProcess::ProcessError error);
314 | void deleteDocumentItem();
315 | QByteArray toUtf16Hex(QString str);
316 | int getPageCount(const QString &filename);
317 | static bool isFileType(const QString &filename,
318 | const QString &mime,
319 | bool startsWith = false);
320 | static bool isPDF(const QString &filename);
321 | static bool isJPG(const QString &filename);
322 | static bool isTIFF(const QString &filename);
323 | static bool isPNG(const QString &filename);
324 | static bool isImage(const QString &filename);
325 | static QString ghostImageFormat(int type);
326 | static const QString getCachePath();
327 | PagesListWidget* getTab(const QString &filename);
328 | bool hasTab(const QString &filename);
329 | int getTabIndex(const QString &filename);
330 | const QString getPagePreview(const QString &filename,
331 | const QString &checksum,
332 | int page,
333 | int quality = 90);
334 | void getPagePreviews(const QString &filename,
335 | const QString &checksum,
336 | int pages);
337 | const QString getChecksum(const QString &filename);
338 | const QString extractPDF(const QString &filename,
339 | const QString &checksum,
340 | int page,
341 | bool force = true);
342 | void showProgress(bool progress);
343 | void on_actionOpen_cache_folder_triggered();
344 | void on_actionShow_tooltips_triggered();
345 | void showTooltips(bool show);
346 | void on_actionCheck_for_updates_triggered();
347 | void on_actionReport_issue_triggered();
348 | bool exportImage(const QString &filename,
349 | const QString &image,
350 | int page,
351 | int type,
352 | int res,
353 | int alpha = 4);
354 | void handleExport(const QString &filename,
355 | int page);
356 | void handleExports(const QString &filename,
357 | QVector pages);
358 | void doExport(const QString &filename,
359 | const QString &image,
360 | QVector pages,
361 | int type,
362 | int res);
363 | void handleExportDone(const QString &path);
364 | void on_preset_currentIndexChanged(int index);
365 | void on_compat_currentIndexChanged(int index);
366 | void on_metaTitle_textChanged(const QString &arg1);
367 | void on_metaSubject_textChanged(const QString &arg1);
368 | void on_metaAuthor_textChanged(const QString &arg1);
369 | void on_tabs_currentChanged(int index);
370 | void handleTabButtonClicked(bool checked);
371 | bool hasDarkMode();
372 | void generateOutputPreview();
373 | void handleOutputPagesChanged();
374 | void showOutputPreview(const QStringList &images);
375 | void handleOutputRemoveSelected();
376 | void handleOutputClearAll();
377 | void handleOutputAdd();
378 | void on_actionDocumentation_triggered();
379 | void on_actionOutput_preview_triggered();
380 | int pagesToExport();
381 | void getPdfInfo(const QString &filename,
382 | const QString &checksum);
383 | void handleFoundPdfInfo(const FusePDF::pdfInfo &pdf);
384 | static const QString stripMarks(QString s);
385 | const QString getCacheSize();
386 | void on_actionClear_cache_triggered();
387 |
388 | private:
389 | Ui::FusePDF *ui;
390 | QString _output;
391 | QString _lastSaveDir;
392 | QString _lastLoadDir;
393 | QString _lastExportDir;
394 | QProcess *_proc;
395 | QPushButton *_tabButton;
396 | };
397 | #endif // FUSEPDF_H
398 |
--------------------------------------------------------------------------------
/src/main.cpp:
--------------------------------------------------------------------------------
1 | /*
2 | #
3 | # FusePDF - https://fusepdf.no
4 | #
5 | # Copyright (c) 2021, 2022 NettStudio AS .
6 | # All rights reserved.
7 | #
8 | # This program is free software; you can redistribute it and/or modify
9 | # it under the terms of the GNU General Public License as published by
10 | # the Free Software Foundation; either version 3 of the License, or
11 | # (at your option) any later version.
12 | #
13 | # This program is distributed in the hope that it will be useful,
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | # GNU General Public License for more details.
17 | #
18 | # You should have received a copy of the GNU General Public License
19 | # along with this program. If not, see
20 | #
21 | */
22 |
23 | #include "fusepdf.h"
24 |
25 | #include
26 | #include
27 | #include
28 | #include
29 |
30 | int main(int argc, char *argv[])
31 | {
32 | QApplication a(argc, argv);
33 | QApplication::setApplicationName("FusePDF");
34 | QApplication::setApplicationDisplayName("FusePDF");
35 | QApplication::setApplicationVersion(VERSION_APP);
36 | QApplication::setOrganizationName("NettStudio AS");
37 | QApplication::setOrganizationDomain("nettstudio.no");
38 |
39 | #ifdef FORCE_NORWEGIAN
40 | QLocale curLocale(QLocale("no"));
41 | QLocale::setDefault(curLocale);
42 | #endif
43 |
44 | QTranslator i18n;
45 | if (i18n.load(QLocale(),
46 | QLatin1String("fusepdf"),
47 | QLatin1String("_"),
48 | QLatin1String(":/i18n"))) { a.installTranslator(&i18n); }
49 | #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
50 | qDebug() << "Locale?" << QLocale().name() << i18n.filePath();
51 | #endif
52 |
53 | FusePDF w;
54 | w.show();
55 | return a.exec();
56 | }
57 |
--------------------------------------------------------------------------------
/tools/header.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: |
3 | {width=2.5in}
4 | FusePDF 2.3.0
5 | author: NettStudio AS
6 | date: 'April 7, 2022'
7 | mainfont: DejaVuSans.ttf
8 | fontsize: 12pt
9 | ---
10 |
--------------------------------------------------------------------------------
/tools/header.tex:
--------------------------------------------------------------------------------
1 | \usepackage{sectsty}
2 | \sectionfont{\clearpage}
3 | \usepackage{fancyhdr}
4 | \pagestyle{fancy}
5 | \fancyhead[CO,CE]{FusePDF}
6 | \fancyfoot[CO,CE]{FusePDF}
7 | \fancyfoot[LE,RO]{\thepage}
8 |
--------------------------------------------------------------------------------
/tools/md2pdf.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | PLANG=${3:-en}
4 | ./tools/pandoc "$1" \
5 | --include-in-header tools/header.tex \
6 | -V linkcolor:blue \
7 | -V geometry:a4paper \
8 | -V geometry:margin=2cm \
9 | -V lang=$PLANG \
10 | --pdf-engine=xelatex \
11 | --toc \
12 | -o "$2"
13 |
--------------------------------------------------------------------------------
/tools/mkdoc.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Make FusePDF docs
4 | #
5 |
6 | #
7 | # sudo apt install texlive-xetex
8 | #
9 | # Download pandoc and copy 'pandoc' binary to this folder.
10 | # https://github.com/jgm/pandoc/releases/download/2.7.2/pandoc-2.7.2-linux.tar.gz
11 | #
12 |
13 | cat tools/header.md > tmp.md
14 | cat docs/index.md >> tmp.md
15 |
16 | echo "## GNU General Public License v3.0" >> tmp.md
17 | echo "" >> tmp.md
18 | cat COPYING >> tmp.md
19 |
20 | ./tools/md2pdf.sh tmp.md docs/README.pdf
21 |
22 | rm tmp.md
23 |
--------------------------------------------------------------------------------