You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

687 lines
28 KiB

#include "XournalMain.h"
#include <algorithm>
#include <memory>
#include <glib/gstdio.h>
#include <gtk/gtk.h>
#include <gui/toolbarMenubar/model/ToolbarColorNames.h>
#include <libintl.h>
#include "control/jobs/ImageExport.h"
#include "control/jobs/ProgressListener.h"
#include "gui/GladeSearchpath.h"
#include "gui/MainWindow.h"
#include "gui/XournalView.h"
#include "pdf/base/XojPdfExport.h"
#include "pdf/base/XojPdfExportFactory.h"
#include "undo/EmergencySaveRestore.h"
#include "xojfile/LoadHandler.h"
#include "Control.h"
#include "Stacktrace.h"
#include "StringUtils.h"
#include "XojMsgBox.h"
#include "config-dev.h"
#include "config-paths.h"
#include "config.h"
#include "filesystem.h"
#include "i18n.h"
#if __linux__
#include <libgen.h>
#endif
namespace {
constexpr auto APP_FLAGS = GApplicationFlags(G_APPLICATION_SEND_ENVIRONMENT | G_APPLICATION_NON_UNIQUE);
/// Configuration migration status.
enum class MigrateStatus {
NotNeeded, ///< No migration was needed.
Success, ///< Migration was carried out successfully.
Failure, ///< Migration failed. */
};
struct MigrateResult {
MigrateStatus status{};
std::string message; ///< Any additional information about the migration status.
};
auto migrateSettings() -> MigrateResult;
void checkForErrorlog();
void checkForEmergencySave(Control* control);
auto exportPdf(const char* input, const char* output, const char* range, ExportBackgroundType exportBackground,
bool progressiveMode) -> int;
auto exportImg(const char* input, const char* output, const char* range, int pngDpi, int pngWidth, int pngHeight,
ExportBackgroundType exportBackground) -> int;
void initResourcePath(GladeSearchpath* gladePath, const gchar* relativePathAndFile, bool failIfNotFound = true);
void initLocalisation() {
#ifdef ENABLE_NLS
#ifdef _WIN32
#undef PACKAGE_LOCALE_DIR
#define PACKAGE_LOCALE_DIR "../share/locale/"
#endif
#ifdef __APPLE__
#undef PACKAGE_LOCALE_DIR
fs::path p = Stacktrace::getExePath();
p /= "../Resources/share/locale/";
const char* PACKAGE_LOCALE_DIR = p.c_str();
#endif
fs::path localeDir = Util::getGettextFilepath(PACKAGE_LOCALE_DIR);
bindtextdomain(GETTEXT_PACKAGE, localeDir.u8string().c_str());
textdomain(GETTEXT_PACKAGE);
#ifdef _WIN32
bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
#endif
#endif // ENABLE_NLS
// Not working on GNU g++(mingww) forWindows! Only working on Linux/macOS and with msvc
try {
std::locale::global(std::locale("")); // "" - system default locale
} catch (std::runtime_error& e) {
g_warning("XournalMain: System default locale could not be set.\n - Caused by: %s\n - Note that it is not "
"supported to set the locale using mingw-w64 on windows.\n - This could be solved by compiling "
"xournalpp with msvc",
e.what());
}
std::cout.imbue(std::locale());
}
auto migrateSettings() -> MigrateResult {
fs::path newConfigPath = Util::getConfigFolder();
if (!fs::exists(newConfigPath)) {
std::array oldPaths = {
Util::getConfigFolder().parent_path() /= "com.github.xournalpp.xournalpp",
Util::getConfigFolder().parent_path() /= "com.github.xournalpp.xournalpp.exe",
fs::u8path(g_get_home_dir()) /= ".xournalpp",
};
for (auto const& oldPath: oldPaths) {
if (!fs::is_directory(oldPath)) {
continue;
}
g_message("Migrating configuration from %s to %s", oldPath.string().c_str(),
newConfigPath.string().c_str());
Util::ensureFolderExists(newConfigPath.parent_path());
try {
fs::copy(oldPath, newConfigPath, fs::copy_options::recursive);
constexpr auto msg = "Due to a recent update, Xournal++ has changed where it's configuration files are "
"stored.\nThey have been automatically copied from\n\t{1}\nto\n\t{2}";
return {MigrateStatus::Success, FS(_F(msg) % oldPath.u8string() % newConfigPath.u8string())};
} catch (fs::filesystem_error const& except) {
constexpr auto msg =
"Due to a recent update, Xournal++ has changed where it's configuration files are "
"stored.\nHowever, when attempting to copy\n\t{1}\nto\n\t{2}\nmigration failed:\n{3}";
g_message("Migration failed: %s", except.what());
return {MigrateStatus::Failure,
FS(_F(msg) % oldPath.u8string() % newConfigPath.u8string() % except.what())};
}
}
}
return {MigrateStatus::NotNeeded, ""};
}
void checkForErrorlog() {
fs::path errorDir = Util::getCacheSubfolder(ERRORLOG_DIR);
if (!fs::exists(errorDir)) {
return;
}
std::vector<fs::path> errorList;
for (auto const& f: fs::directory_iterator(errorDir)) {
if (f.is_regular_file() && f.path().stem() == "errorlog") {
errorList.emplace_back(f);
}
}
if (errorList.empty()) {
return;
}
std::sort(errorList.begin(), errorList.end());
string msg =
errorList.size() == 1 ?
_("There is an errorlogfile from Xournal++. Please send a Bugreport, so the bug may be fixed.") :
_("There are errorlogfiles from Xournal++. Please send a Bugreport, so the bug may be fixed.");
msg += "\n";
msg += FS(_F("You're using \"{1}/{2}\" branch. Send Bugreport will direct you to this repo's issue tracker.") %
GIT_ORIGIN_OWNER % GIT_BRANCH);
msg += "\n";
msg += FS(_F("The most recent log file name: {1}") % errorList[0].string());
GtkWidget* dialog = gtk_message_dialog_new(nullptr, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s",
msg.c_str());
gtk_dialog_add_button(GTK_DIALOG(dialog), _("Send Bugreport"), 1);
gtk_dialog_add_button(GTK_DIALOG(dialog), _("Open Logfile"), 2);
gtk_dialog_add_button(GTK_DIALOG(dialog), _("Open Logfile directory"), 3);
gtk_dialog_add_button(GTK_DIALOG(dialog), _("Delete Logfile"), 4);
gtk_dialog_add_button(GTK_DIALOG(dialog), _("Cancel"), 5);
int res = gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
auto const& errorlogPath = Util::getCacheSubfolder(ERRORLOG_DIR) / errorList[0];
if (res == 1) // Send Bugreport
{
Util::openFileWithDefaultApplication(PROJECT_BUGREPORT);
Util::openFileWithDefaultApplication(errorlogPath);
} else if (res == 2) // Open Logfile
{
Util::openFileWithDefaultApplication(errorlogPath);
} else if (res == 3) // Open Logfile directory
{
Util::openFileWithFilebrowser(errorlogPath.parent_path());
} else if (res == 4) // Delete Logfile
{
if (!fs::exists(errorlogPath)) {
msg = FS(_F("Errorlog cannot be deleted. You have to do it manually.\nLogfile: {1}") %
errorlogPath.u8string());
XojMsgBox::showErrorToUser(nullptr, msg);
}
} else if (res == 5) // Cancel
{
// Nothing to do
}
}
void checkForEmergencySave(Control* control) {
auto file = Util::getConfigFile("emergencysave.xopp");
if (!fs::exists(file)) {
return;
}
string msg = _("Xournal++ crashed last time. Would you like to restore the last edited file?");
GtkWidget* dialog = gtk_message_dialog_new(nullptr, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s",
msg.c_str());
gtk_dialog_add_button(GTK_DIALOG(dialog), _("Delete file"), 1);
gtk_dialog_add_button(GTK_DIALOG(dialog), _("Restore file"), 2);
int res = gtk_dialog_run(GTK_DIALOG(dialog));
if (res == 1) // Delete file
{
fs::remove(file);
} else if (res == 2) // Open File
{
if (control->openFile(file, -1, true)) {
control->getDocument()->setFilepath("");
// Make sure the document is changed, there is a question to ask for save
control->getUndoRedoHandler()->addUndoAction(std::make_unique<EmergencySaveRestore>());
control->updateWindowTitle();
fs::remove(file);
}
}
gtk_widget_destroy(dialog);
}
/**
* @brief Export the input file as a bunch of image files (one per page)
* @param input Path to the input file
* @param output Path to the output file(s)
* @param range Page range to be parsed. If range=nullptr, exports the whole file
* @param pngDpi Set dpi for Png files. Non positive values are ignored
* @param pngWidth Set the width for Png files. Non positive values are ignored
* @param pngHeight Set the height for Png files. Non positive values are ignored
* @param exportBackground If EXPORT_BACKGROUND_NONE, the exported image file has transparent background
*
* The priority is: pngDpi overwrites pngWidth overwrites pngHeight
*
* @return 0 on success, -2 on failure opening the input file, -3 on export failure
*/
auto exportImg(const char* input, const char* output, const char* range, int pngDpi, int pngWidth, int pngHeight,
ExportBackgroundType exportBackground) -> int {
LoadHandler loader;
Document* doc = loader.loadDocument(input);
if (doc == nullptr) {
g_error("%s", loader.getLastError().c_str());
}
fs::path const path(output);
ExportGraphicsFormat format = EXPORT_GRAPHICS_PNG;
if (path.extension() == ".svg") {
format = EXPORT_GRAPHICS_SVG;
}
PageRangeVector exportRange;
if (range) {
exportRange = PageRange::parse(range, int(doc->getPageCount()));
} else {
exportRange.push_back(new PageRangeEntry(0, int(doc->getPageCount() - 1)));
}
DummyProgressListener progress;
ImageExport imgExport(doc, path, format, exportBackground, exportRange);
if (format == EXPORT_GRAPHICS_PNG) {
if (pngDpi > 0) {
imgExport.setQualityParameter(EXPORT_QUALITY_DPI, pngDpi);
} else if (pngWidth > 0) {
imgExport.setQualityParameter(EXPORT_QUALITY_WIDTH, pngWidth);
} else if (pngHeight > 0) {
imgExport.setQualityParameter(EXPORT_QUALITY_HEIGHT, pngHeight);
}
}
imgExport.exportGraphics(&progress);
for (PageRangeEntry* e: exportRange) {
delete e;
}
exportRange.clear();
string errorMsg = imgExport.getLastErrorMsg();
if (!errorMsg.empty()) {
g_message("Error exporting image: %s\n", errorMsg.c_str());
}
g_message("%s", _("Image file successfully created"));
return 0; // no error
}
/**
* @brief Export the input file as pdf
* @param input Path to the input file
* @param output Path to the output file
* @param range Page range to be parsed. If range=nullptr, exports the whole file
* @param exportBackground If EXPORT_BACKGROUND_NONE, the exported pdf file has white background
* @param progressiveMode If true, then for each xournalpp page, instead of rendering one PDF page, the page layers are
* rendered one by one to produce as many pages as there are layers.
*
* @return 0 on success, -2 on failure opening the input file, -3 on export failure
*/
auto exportPdf(const char* input, const char* output, const char* range, ExportBackgroundType exportBackground,
bool progressiveMode) -> int {
LoadHandler loader;
Document* doc = loader.loadDocument(input);
if (doc == nullptr) {
g_error("%s", loader.getLastError().c_str());
}
GFile* file = g_file_new_for_commandline_arg(output);
XojPdfExport* pdfe = XojPdfExportFactory::createExport(doc, nullptr);
pdfe->setExportBackground(exportBackground);
char* cpath = g_file_get_path(file);
string path = cpath;
g_free(cpath);
g_object_unref(file);
bool exportSuccess; // Return of the export job
if (range) {
// Parse the range
PageRangeVector exportRange = PageRange::parse(range, doc->getPageCount());
// Do the export
exportSuccess = pdfe->createPdf(path, exportRange, progressiveMode);
// Clean up
for (PageRangeEntry* e: exportRange) {
delete e;
}
exportRange.clear();
} else {
exportSuccess = pdfe->createPdf(path, progressiveMode);
}
if (!exportSuccess) {
g_error("%s", pdfe->getLastError().c_str());
// delete pdfe; Unreachable. Todo: use std::unique_ptr
}
delete pdfe;
g_message("%s", _("PDF file successfully created"));
return 0; // no error
}
struct XournalMainPrivate {
XournalMainPrivate() = default;
XournalMainPrivate(XournalMainPrivate&&) = delete;
XournalMainPrivate(XournalMainPrivate const&) = delete;
auto operator=(XournalMainPrivate&&) -> XournalMainPrivate = delete;
auto operator=(XournalMainPrivate const&) -> XournalMainPrivate = delete;
~XournalMainPrivate() {
g_strfreev(optFilename);
g_free(pdfFilename);
g_free(imgFilename);
}
gchar** optFilename{};
gchar* pdfFilename{};
gchar* imgFilename{};
gboolean showVersion = false;
int openAtPageNumber = 0; // when no --page is used, the document opens at the page specified in the metadata file
gchar* exportRange{};
int exportPngDpi = -1;
int exportPngWidth = -1;
int exportPngHeight = -1;
gboolean exportNoBackground = false;
gboolean exportNoRuling = false;
gboolean progressiveMode = false;
std::unique_ptr<GladeSearchpath> gladePath;
std::unique_ptr<Control> control;
std::unique_ptr<MainWindow> win;
};
using XMPtr = XournalMainPrivate*;
/// Checks for input method compatibility and ensures it
void ensure_input_model_compatibility() {
const char* imModule = g_getenv("GTK_IM_MODULE");
if (imModule != nullptr && strcmp(imModule, "xim") == 0) {
g_setenv("GTK_IM_MODULE", "ibus", true);
g_warning("Unsupported input method: xim, changed to: ibus");
}
}
/**
* Find a file in a resource folder, and return the resource folder path
* Return an empty string, if the folder was not found
*/
auto findResourcePath(const fs::path& searchFile) -> fs::path {
auto search_for = [&searchFile](fs::path start) -> std::optional<fs::path> {
constexpr auto* postfix = "share/xournalpp";
/// 1. relative install
/// 2. windows install
/// 3. build dir
for (int i = 0; i < 3; ++i, start = start.parent_path()) {
if (auto target = start / searchFile; fs::exists(target)) {
return target.parent_path();
}
if (auto folder = start / postfix / searchFile; fs::exists(folder)) {
return folder.parent_path();
}
}
return std::nullopt;
};
/* /// relative execution path
if (auto path = search_for(fs::path{}); path) {
return *path;
}*/
/// real execution path
if (auto path = search_for(Stacktrace::getExePath().parent_path()); path) {
return *path;
}
// Not found
return {};
}
void initResourcePath(GladeSearchpath* gladePath, const gchar* relativePathAndFile, bool failIfNotFound) {
auto uiPath = findResourcePath(relativePathAndFile); // i.e. relativePathAndFile = "ui/about.glade"
if (!uiPath.empty()) {
gladePath->addSearchDirectory(uiPath);
return;
}
// -----------------------------------------------------------------------
#ifdef __APPLE__
fs::path p = Stacktrace::getExePath();
p /= "../Resources";
p /= relativePathAndFile;
if (fs::exists(p)) {
gladePath->addSearchDirectory(p.parent_path());
return;
}
string msg = FS(_F("Missing the needed UI file:\n{1}\n .app corrupted?\nPath: {2}") % relativePathAndFile %
p.u8string());
if (!failIfNotFound) {
msg += _("\nWill now attempt to run without this file.");
}
XojMsgBox::showErrorToUser(nullptr, msg);
#else
// Check at the target installation directory
fs::path absolute = PACKAGE_DATA_DIR;
absolute /= PROJECT_PACKAGE;
absolute /= relativePathAndFile;
if (fs::exists(absolute)) {
gladePath->addSearchDirectory(absolute.parent_path());
return;
}
string msg = FS(_F("<span foreground='red' size='x-large'>Missing the needed UI file:\n<b>{1}</b></span>\nCould "
"not find them at any location.\n Not relative\n Not in the Working Path\n Not in {2}") %
relativePathAndFile % PACKAGE_DATA_DIR);
if (!failIfNotFound) {
msg += _("\n\nWill now attempt to run without this file.");
}
XojMsgBox::showErrorToUser(nullptr, msg);
#endif
if (failIfNotFound) {
exit(12);
}
}
void on_activate(GApplication*, XMPtr) {}
void on_command_line(GApplication*, GApplicationCommandLine*, XMPtr) {
g_message("XournalMain::on_command_line: This should never happen, please file a bugreport with a detailed "
"description how to reproduce this message");
// Todo: implement this, if someone files the bug report
}
void on_open_files(GApplication*, gpointer, gint, gchar*, XMPtr) {
g_message("XournalMain::on_open_files: This should never happen, please file a bugreport with a detailed "
"description how to reproduce this message");
// Todo: implement this, if someone files the bug report
}
void on_startup(GApplication* application, XMPtr app_data) {
initLocalisation();
ensure_input_model_compatibility();
MigrateResult migrateResult = migrateSettings();
app_data->gladePath = std::make_unique<GladeSearchpath>();
initResourcePath(app_data->gladePath.get(), "ui/about.glade");
initResourcePath(app_data->gladePath.get(), "ui/xournalpp.css", false);
// init singleton
// ToolbarColorNames::getInstance();
app_data->control = std::make_unique<Control>(application, app_data->gladePath.get());
{
auto icon = app_data->gladePath->getFirstSearchPath() / "icons";
gtk_icon_theme_prepend_search_path(gtk_icon_theme_get_default(), icon.u8string().c_str());
}
if (app_data->control->getSettings()->isDarkTheme()) {
auto icon = app_data->gladePath->getFirstSearchPath() / "iconsDark";
gtk_icon_theme_prepend_search_path(gtk_icon_theme_get_default(), icon.u8string().c_str());
}
auto& globalLatexTemplatePath = app_data->control->getSettings()->latexSettings.globalTemplatePath;
if (globalLatexTemplatePath.empty()) {
globalLatexTemplatePath = findResourcePath("resources/") / "default_template.tex";
g_message("Using default latex template in %s", globalLatexTemplatePath.string().c_str());
app_data->control->getSettings()->save();
}
app_data->win = std::make_unique<MainWindow>(app_data->gladePath.get(), app_data->control.get());
app_data->control->initWindow(app_data->win.get());
if (migrateResult.status != MigrateStatus::NotNeeded) {
Util::execInUiThread(
[=]() { XojMsgBox::showErrorToUser(app_data->control->getGtkWindow(), migrateResult.message); });
}
app_data->win->show(nullptr);
bool opened = false;
if (app_data->optFilename) {
if (g_strv_length(app_data->optFilename) != 1) {
string msg = _("Sorry, Xournal++ can only open one file at once.\n"
"Others are ignored.");
XojMsgBox::showErrorToUser(static_cast<GtkWindow*>(*app_data->win), msg);
}
fs::path p = Util::fromGFilename(app_data->optFilename[0], false);
try {
if (fs::exists(p)) {
opened = app_data->control->openFile(p,
app_data->openAtPageNumber - 1); // First page for user is page 1
} else {
opened = app_data->control->newFile("", p);
}
} catch (fs::filesystem_error const& e) {
string msg = FS(_F("Sorry, Xournal++ cannot open remote files at the moment.\n"
"You have to copy the file to a local directory.") %
p.u8string() % e.what());
XojMsgBox::showErrorToUser(static_cast<GtkWindow*>(*app_data->win), msg);
opened = app_data->control->newFile("", p);
}
}
app_data->control->getScheduler()->start();
if (!opened) {
app_data->control->newFile();
}
checkForErrorlog();
checkForEmergencySave(app_data->control.get());
// There is a timing issue with the layout
// This fixes it, see #405
Util::execInUiThread([=]() { app_data->control->getWindow()->getXournal()->layoutPages(); });
gtk_application_add_window(GTK_APPLICATION(application), GTK_WINDOW(app_data->win->getWindow()));
}
auto on_handle_local_options(GApplication*, GVariantDict*, XMPtr app_data) -> gint {
if (app_data->showVersion) {
std::cout << PROJECT_NAME << " " << PROJECT_VERSION << std::endl;
std::cout << "└──libgtk: " << gtk_get_major_version() << "." //
<< gtk_get_minor_version() << "." //
<< gtk_get_micro_version() << std::endl; //
return 0;
}
if (app_data->pdfFilename && app_data->optFilename && *app_data->optFilename) {
return exportPdf(*app_data->optFilename, app_data->pdfFilename, app_data->exportRange,
app_data->exportNoBackground ? EXPORT_BACKGROUND_NONE :
app_data->exportNoRuling ? EXPORT_BACKGROUND_UNRULED :
EXPORT_BACKGROUND_ALL,
app_data->progressiveMode);
}
if (app_data->imgFilename && app_data->optFilename && *app_data->optFilename) {
return exportImg(*app_data->optFilename, app_data->imgFilename, app_data->exportRange, app_data->exportPngDpi,
app_data->exportPngWidth, app_data->exportPngHeight,
app_data->exportNoBackground ? EXPORT_BACKGROUND_NONE :
app_data->exportNoRuling ? EXPORT_BACKGROUND_UNRULED :
EXPORT_BACKGROUND_ALL);
}
return -1;
}
void on_shutdown(GApplication*, XMPtr app_data) {
app_data->control->saveSettings();
app_data->win->getXournal()->clearSelection();
app_data->control->getScheduler()->stop();
ToolbarColorNames::getInstance().save();
}
} // namespace
auto XournalMain::run(int argc, char** argv) -> int {
XournalMainPrivate app_data;
GtkApplication* app = gtk_application_new("com.github.xournalpp.xournalpp", APP_FLAGS);
g_signal_connect(app, "activate", G_CALLBACK(&on_activate), &app_data);
g_signal_connect(app, "command-line", G_CALLBACK(&on_command_line), &app_data);
g_signal_connect(app, "open", G_CALLBACK(&on_open_files), &app_data);
g_signal_connect(app, "startup", G_CALLBACK(&on_startup), &app_data);
g_signal_connect(app, "shutdown", G_CALLBACK(&on_shutdown), &app_data);
g_signal_connect(app, "handle-local-options", G_CALLBACK(&on_handle_local_options), &app_data);
std::array options = {GOptionEntry{"page", 'n', 0, G_OPTION_ARG_INT, &app_data.openAtPageNumber,
_("Jump to Page (first Page: 1)"), "N"},
GOptionEntry{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &app_data.optFilename,
"<input>", nullptr},
GOptionEntry{"version", 0, 0, G_OPTION_ARG_NONE, &app_data.showVersion,
_("Get version of xournalpp"), nullptr},
GOptionEntry{nullptr}}; // Must be terminated by a nullptr. See gtk doc
g_application_add_main_option_entries(G_APPLICATION(app), options.data());
/**
* Export related options
*/
std::array exportOptions = {
GOptionEntry{"create-pdf", 'p', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_FILENAME, &app_data.pdfFilename,
_("Export FILE as PDF"), "PDFFILE"},
GOptionEntry{"create-img", 'i', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_FILENAME, &app_data.imgFilename,
_("Export FILE as image files (one per page)\n"
" Guess the output format from the extension of IMGFILE\n"
" Supported formats: .png, .svg"),
"IMGFILE"},
GOptionEntry{"export-no-background", 0, 0, G_OPTION_ARG_NONE, &app_data.exportNoBackground,
_("Export without background\n"
" The exported file has transparent or white background,\n"
" depending on what its format supports\n"),
0},
GOptionEntry{"export-no-ruling", 0, 0, G_OPTION_ARG_NONE, &app_data.exportNoRuling,
_("Export without ruling\n"
" The exported file has no paper ruling\n"),
0},
GOptionEntry{"export-layers-progressively", 0, 0, G_OPTION_ARG_NONE, &app_data.progressiveMode,
_("Export layers progressively\n"
" In PDF export, Render layers progressively one by one.\n"
" This results in N export pages per page with N layers,\n"
" building up the layer stack progressively.\n"
" The resulting PDF file can be used for a presentation.\n"),
0},
GOptionEntry{"export-range", 0, 0, G_OPTION_ARG_STRING, &app_data.exportRange,
_("Only export the pages specified by RANGE (e.g. \"2-3,5,7-\")\n"
" No effect without -p/--create-pdf or -i/--create-img"),
nullptr},
GOptionEntry{"export-png-dpi", 0, 0, G_OPTION_ARG_INT, &app_data.exportPngDpi,
_("Set DPI for PNG exports. Default is 300\n"
" No effect without -i/--create-img=foo.png"),
"N"},
GOptionEntry{"export-png-width", 0, 0, G_OPTION_ARG_INT, &app_data.exportPngWidth,
_("Set page width for PNG exports\n"
" No effect without -i/--create-img=foo.png\n"
" Ignored if --export-png-dpi is used"),
"N"},
GOptionEntry{
"export-png-height", 0, 0, G_OPTION_ARG_INT, &app_data.exportPngHeight,
_("Set page height for PNG exports\n"
" No effect without -i/--create-img=foo.png\n"
" Ignored if --export-png-dpi or --export-png-width is used"),
"N"},
GOptionEntry{nullptr}}; // Must be terminated by a nullptr. See gtk doc
GOptionGroup* exportGroup = g_option_group_new("export", _("Advanced export options"),
_("Display advanced export options"), nullptr, nullptr);
g_option_group_add_entries(exportGroup, exportOptions.data());
g_application_add_option_group(G_APPLICATION(app), exportGroup);
auto rv = g_application_run(G_APPLICATION(app), argc, argv);
g_object_unref(app);
return rv;
}