diff --git a/README.md b/README.md index bef64df2..89aa1dfa 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ into the file, so if the PDF is deleted, the background is lost. Therefor Xournal++ reads *.xoj files, and can also export *.xoj. On exporting to *.xoj all Xournal++ specific Extension are lost, like addtional Background types. -*.xopp can theretically be read by Xournal, as long as you do not use any new +*.xopp can theoretically be read by Xournal, as long as you do not use any new feature, Xournal does not open files at all if there are new attributes or unknown values, because of this Xournal++ will add the extension .xopp to all saved files. @@ -136,6 +136,23 @@ merged, even if they are not 100% finished. See [GitHub:xournalpp](http://github.com/xournalpp/xournalpp) for current development. You can also join our Gitter channel via badge on top. +## FAQ +### Q: Secondary stylus button only works when there is no contact +This is due to a driver setting, which you can configure with `TPCButton` or `TabletPCButton` directive if using `Wacom` driver (but other drivers might have this setting too). + +Here's a `/usr/share/X11/xorg.conf.d/30-wacom.conf` example snippet: +``` +Section "InputClass" + Identifier "Wacom tablets class" + MatchProduct "Wacom" + MatchDevicePath "/dev/input/event*" + MatchIsTablet "on" + Driver "wacom" + Option "TabletPCButton" "on" + Option "TPCButton" "on" +EndSection +``` + ## Code documentation The code documentation is generated using Doxygen. diff --git a/src/control/XournalMain.cpp b/src/control/XournalMain.cpp index 24f5ec7e..93dbf165 100644 --- a/src/control/XournalMain.cpp +++ b/src/control/XournalMain.cpp @@ -2,6 +2,8 @@ #include "Control.h" +#include "control/jobs/ImageExport.h" +#include "control/jobs/ProgressListener.h" #include "gui/GladeSearchpath.h" #include "gui/MainWindow.h" #include "gui/toolbarMenubar/model/ToolbarColorNames.h" @@ -11,6 +13,7 @@ #include "undo/EmergencySaveRestore.h" #include "xojfile/LoadHandler.h" + #include #include #include @@ -206,6 +209,58 @@ void XournalMain::checkForEmergencySave(Control* control) { gtk_widget_destroy(dialog); } +int XournalMain::exportImg(const char* input, const char* output) +{ + XOJ_CHECK_TYPE(XournalMain); + + LoadHandler loader; + + Document* doc = loader.loadDocument(input); + if (doc == NULL) + { + g_error("%s", loader.getLastError().c_str()); + return -2; + } + + GFile* file = g_file_new_for_commandline_arg(output); + + char* cpath = g_file_get_path(file); + string path = cpath; + g_free(cpath); + g_object_unref(file); + + ExportGraphicsFormat format = EXPORT_GRAPHICS_PNG; + + if (StringUtils::endsWith(path, ".svg")) + { + format = EXPORT_GRAPHICS_SVG; + } + + PageRangeVector exportRange; + exportRange.push_back(new PageRangeEntry(0, doc->getPageCount() - 1)); + DummyProgressListener progress; + + ImageExport imgExport(doc, path, format, false, exportRange); + imgExport.exportGraphics(&progress); + + for (PageRangeEntry* e : exportRange) + { + delete e; + } + exportRange.clear(); + + string errorMsg = imgExport.getLastErrorMsg(); + if (errorMsg != "") + { + g_message("Error exporting image: %s\n", errorMsg.c_str()); + return -3; + } + + g_message("%s", _("Image file successfully created")); + + return 0; // no error +} + int XournalMain::exportPdf(const char* input, const char* output) { XOJ_CHECK_TYPE(XournalMain); @@ -222,18 +277,20 @@ int XournalMain::exportPdf(const char* input, const char* output) GFile* file = g_file_new_for_commandline_arg(output); XojPdfExport* pdfe = XojPdfExportFactory::createExport(doc, NULL); - if (!pdfe->createPdf(g_file_get_path(file))) + char* cpath = g_file_get_path(file); + string path = cpath; + g_free(cpath); + g_object_unref(file); + + if (!pdfe->createPdf(path)) { g_error("%s", pdfe->getLastError().c_str()); - g_object_unref(file); delete pdfe; return -3; } delete pdfe; - g_object_unref(file); - g_message("%s", _("PDF file successfully created")); return 0; // no error @@ -250,13 +307,16 @@ int XournalMain::run(int argc, char* argv[]) gchar** optFilename = NULL; gchar* pdfFilename = NULL; + gchar* imgFilename = NULL; int openAtPageNumber = -1; string create_pdf = _("PDF output filename"); + string create_img = _("Image output filename (.png / .svg)"); string page_jump = _("Jump to Page (first Page: 1)"); string audio_folder = _("Absolute path for the audio files playback"); GOptionEntry options[] = { { "create-pdf", 'p', 0, G_OPTION_ARG_FILENAME, &pdfFilename, create_pdf.c_str(), NULL }, + { "create-img", 'i', 0, G_OPTION_ARG_FILENAME, &imgFilename, create_img.c_str(), NULL }, { "page", 'n', 0, G_OPTION_ARG_INT, &openAtPageNumber, page_jump.c_str(), "N" }, {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &optFilename, "", NULL }, {NULL} @@ -280,6 +340,10 @@ int XournalMain::run(int argc, char* argv[]) { return exportPdf(*optFilename, pdfFilename); } + if (imgFilename && optFilename && *optFilename) + { + return exportImg(*optFilename, imgFilename); + } // Checks for input method compatibility diff --git a/src/control/XournalMain.h b/src/control/XournalMain.h index 0b94587c..1f03d96c 100644 --- a/src/control/XournalMain.h +++ b/src/control/XournalMain.h @@ -33,11 +33,12 @@ private: void checkForEmergencySave(Control* control); int exportPdf(const char* input, const char* output); + int exportImg(const char* input, const char* output); + void initSettingsPath(); void initResourcePath(GladeSearchpath* gladePath); string findResourcePath(string searchFile); private: XOJ_TYPE_ATTRIB; - }; diff --git a/src/control/jobs/BaseExportJob.h b/src/control/jobs/BaseExportJob.h index 4eb3a695..077b0b60 100644 --- a/src/control/jobs/BaseExportJob.h +++ b/src/control/jobs/BaseExportJob.h @@ -48,6 +48,9 @@ protected: Path filename; + /** + * Error message to show to the user + */ string errorMsg; class ExportType diff --git a/src/control/jobs/CustomExportJob.cpp b/src/control/jobs/CustomExportJob.cpp index 973fe1ea..a5d24e3a 100644 --- a/src/control/jobs/CustomExportJob.cpp +++ b/src/control/jobs/CustomExportJob.cpp @@ -1,5 +1,6 @@ #include "CustomExportJob.h" #include "SaveJob.h" +#include "ImageExport.h" #include "control/Control.h" #include "control/xojfile/XojExportHandler.h" @@ -23,6 +24,8 @@ CustomExportJob::CustomExportJob(Control* control) filters[_("PDF with plain background")] = new ExportType(".pdf", true); filters[_("PNG graphics")] = new ExportType(".png", false); filters[_("PNG with transparent background")] = new ExportType(".png", true); + filters[_("SVG graphics")] = new ExportType(".svg", false); + filters[_("SVG with transparent background")] = new ExportType(".svg", true); filters[_("Xournal (Compatibility)")] = new ExportType(".xoj", false); } @@ -41,7 +44,6 @@ CustomExportJob::~CustomExportJob() delete filter.second; } - XOJ_RELEASE_TYPE(CustomExportJob); } @@ -94,7 +96,17 @@ bool CustomExportJob::showFilechooser() if (filename.hasExtension(".pdf")) { dlg->removeDpiSelection(); - exportTypePdf = true; + format = EXPORT_GRAPHICS_PDF; + } + else if (filename.hasExtension(".svg")) + { + dlg->removeDpiSelection(); + format = EXPORT_GRAPHICS_SVG; + } + else if (filename.hasExtension(".png")) + { + dlg->removeDpiSelection(); + format = EXPORT_GRAPHICS_PNG; } dlg->initPages(control->getCurrentPageNo() + 1, doc->getPageCount()); @@ -114,141 +126,20 @@ bool CustomExportJob::showFilechooser() return true; } -void CustomExportJob::createSurface(double width, double height) -{ - XOJ_CHECK_TYPE(CustomExportJob); - - this->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, - width * this->pngDpi / 72.0, - height * this->pngDpi / 72.0); - this->cr = cairo_create(this->surface); - double factor = this->pngDpi / 72.0; - cairo_scale(this->cr, factor, factor); -} - -bool CustomExportJob::freeSurface(int id) -{ - XOJ_CHECK_TYPE(CustomExportJob); - - cairo_destroy(this->cr); - - string filepath = getFilenameWithNumber(id); - cairo_status_t status = cairo_surface_write_to_png(surface, filepath.c_str()); - cairo_surface_destroy(surface); - - // we ignore this problem - if (status != CAIRO_STATUS_SUCCESS) - { - return false; - } - - return true; -} - -/** - * Get a filename with a numer, e.g. .../export-1.png, if the no is -1, return .../export.png - */ -string CustomExportJob::getFilenameWithNumber(int no) -{ - if (no == -1) - { - // No number to add - return filename.str(); - } - - string filepath = filename.str(); - size_t dotPos = filepath.find_last_of("."); - if (dotPos == string::npos) - { - // No file extension, add number - return filepath + "-" + std::to_string(no); - } - - return filepath.substr(0, dotPos) + "-" + std::to_string(no) + filepath.substr(dotPos); -} - /** - * Export a single PNG page + * Create one Graphics file per page */ -void CustomExportJob::exportPngPage(int pageId, int id, double zoom, DocumentView& view) -{ - Document* doc = control->getDocument(); - doc->lock(); - PageRef page = doc->getPage(pageId); - doc->unlock(); - - createSurface(page->getWidth(), page->getHeight()); - - if (page->getBackgroundType().isPdfPage()) - { - int pgNo = page->getPdfPageNr(); - XojPdfPageSPtr popplerPage = doc->getPdfPage(pgNo); - - PdfView::drawPage(NULL, popplerPage, cr, zoom, page->getWidth(), page->getHeight()); - } - - bool hideBackground = filters[this->chosenFilterName]->withoutBackground; - - view.drawPage(page, this->cr, true, hideBackground); - - if (!freeSurface(id)) - { - // could not create this file... - this->errorMsg = _("Error export PDF Page"); - return; - } -} - -/** - * Create one PNG file per page - */ -void CustomExportJob::exportPng() +void CustomExportJob::exportGraphics() { XOJ_CHECK_TYPE(CustomExportJob); - // don't lock the page here for the whole flow, else we get a dead lock... - // the ui is blocked, so there should be no changes... - Document* doc = control->getDocument(); - - int count = doc->getPageCount(); - - bool onePage = ((this->exportRange.size() == 1) && (this->exportRange[0]->getFirst() == this->exportRange[0]->getLast())); - - char selectedPages[count]; - int selectedCount = 0; - for (int i = 0; i < count; i++) - { - selectedPages[i] = 0; - } - for (PageRangeEntry* e : this->exportRange) - { - for (int x = e->getFirst(); x <= e->getLast(); x++) - { - selectedPages[x] = 1; - selectedCount++; - } - } - - this->control->setMaximumState(selectedCount); + bool hideBackground = filters[this->chosenFilterName]->withoutBackground; - DocumentView view; - double zoom = this->pngDpi / 72.0; - int current = 0; + ImageExport imgExport(control->getDocument(), filename, format, hideBackground, exportRange); + imgExport.setPngDpi(pngDpi); + imgExport.exportGraphics(control); - for (int i = 0; i < count; i++) - { - int id = i + 1; - if (onePage) - { - id = -1; - } - - if (selectedPages[i]) - { - this->control->setCurrentState(current++); - exportPngPage(i, id, zoom, view); - } - } + errorMsg = imgExport.getLastErrorMsg(); } void CustomExportJob::run() @@ -273,7 +164,7 @@ void CustomExportJob::run() callAfterRun(); } } - else if (exportTypePdf) + else if (format == EXPORT_GRAPHICS_PDF) { // don't lock the page here for the whole flow, else we get a dead lock... // the ui is blocked, so there should be no changes... @@ -292,7 +183,7 @@ void CustomExportJob::run() } else { - exportPng(); + exportGraphics(); } } diff --git a/src/control/jobs/CustomExportJob.h b/src/control/jobs/CustomExportJob.h index 11a18511..d346c41d 100644 --- a/src/control/jobs/CustomExportJob.h +++ b/src/control/jobs/CustomExportJob.h @@ -12,6 +12,7 @@ #pragma once #include "BaseExportJob.h" +#include "ImageExport.h" #include "view/DocumentView.h" @@ -19,6 +20,7 @@ #include #include + class CustomExportJob : public BaseExportJob { public: @@ -39,33 +41,29 @@ protected: virtual void addFilterToDialog(); /** - * Export a single PNG page + * Create one Graphics file per page */ - void exportPngPage(int pageId, int id, double zoom, DocumentView& view); + void exportGraphics(); - /** - * Create one PNG file per page - */ - void exportPng(); - - void createSurface(double width, double height); - bool freeSurface(int id); - string getFilenameWithNumber(int no); virtual bool isUriValid(string& uri); private: XOJ_TYPE_ATTRIB; + /** + * The range to export + */ PageRangeVector exportRange; - int pngDpi = 300; - cairo_surface_t* surface = NULL; - cairo_t* cr = NULL; + /** + * PNG dpi + */ + int pngDpi = 300; /** - * PDF Export, else PNG Export + * Export graphics format */ - bool exportTypePdf = false; + ExportGraphicsFormat format = EXPORT_GRAPHICS_UNDEFINED; /** * XOJ Export, else PNG Export diff --git a/src/control/jobs/ImageExport.cpp b/src/control/jobs/ImageExport.cpp new file mode 100644 index 00000000..e99d6819 --- /dev/null +++ b/src/control/jobs/ImageExport.cpp @@ -0,0 +1,215 @@ +#include "ImageExport.h" + +#include "control/jobs/ProgressListener.h" +#include "model/Document.h" +#include "view/PdfView.h" + +#include +#include + + +ImageExport::ImageExport(Document* doc, Path filename, ExportGraphicsFormat format, bool hideBackground, PageRangeVector& exportRange) + : doc(doc), + filename(filename), + format(format), + hideBackground(hideBackground), + exportRange(exportRange) +{ + XOJ_INIT_TYPE(ImageExport); +} + +ImageExport::~ImageExport() +{ + XOJ_CHECK_TYPE(ImageExport); + + XOJ_RELEASE_TYPE(ImageExport); +} + +/** + * PNG dpi + */ +void ImageExport::setPngDpi(int dpi) +{ + XOJ_CHECK_TYPE(ImageExport); + + this->pngDpi = dpi; +} + +/** + * @return the last error message to show to the user + */ +string ImageExport::getLastErrorMsg() +{ + XOJ_CHECK_TYPE(ImageExport); + + return lastError; +} + +/** + * Create surface + */ +void ImageExport::createSurface(double width, double height, int id) +{ + XOJ_CHECK_TYPE(ImageExport); + + if (format == EXPORT_GRAPHICS_PNG) + { + this->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + width * this->pngDpi / 72.0, + height * this->pngDpi / 72.0); + this->cr = cairo_create(this->surface); + double factor = this->pngDpi / 72.0; + cairo_scale(this->cr, factor, factor); + } + else if (format == EXPORT_GRAPHICS_SVG) + { + string filepath = getFilenameWithNumber(id); + this->surface = cairo_svg_surface_create(filepath.c_str(), width, height); + cairo_svg_surface_restrict_to_version(this->surface, CAIRO_SVG_VERSION_1_2); + this->cr = cairo_create(this->surface); + } + else + { + g_error("Unsupported graphics format: %i", format); + } +} + +/** + * Free / store the surface + */ +bool ImageExport::freeSurface(int id) +{ + XOJ_CHECK_TYPE(ImageExport); + + cairo_destroy(this->cr); + + cairo_status_t status = CAIRO_STATUS_SUCCESS; + if (format == EXPORT_GRAPHICS_PNG) + { + string filepath = getFilenameWithNumber(id); + status = cairo_surface_write_to_png(surface, filepath.c_str()); + } + cairo_surface_destroy(surface); + + // we ignore this problem + if (status != CAIRO_STATUS_SUCCESS) + { + return false; + } + + return true; +} + +/** + * Get a filename with a number, e.g. .../export-1.png, if the no is -1, return .../export.png + */ +string ImageExport::getFilenameWithNumber(int no) +{ + XOJ_CHECK_TYPE(ImageExport); + + if (no == -1) + { + // No number to add + return filename.str(); + } + + string filepath = filename.str(); + size_t dotPos = filepath.find_last_of("."); + if (dotPos == string::npos) + { + // No file extension, add number + return filepath + "-" + std::to_string(no); + } + + return filepath.substr(0, dotPos) + "-" + std::to_string(no) + filepath.substr(dotPos); +} + +/** + * Export a single PNG page + */ +void ImageExport::exportImagePage(int pageId, int id, double zoom, ExportGraphicsFormat format, DocumentView& view) +{ + XOJ_CHECK_TYPE(ImageExport); + + doc->lock(); + PageRef page = doc->getPage(pageId); + doc->unlock(); + + createSurface(page->getWidth(), page->getHeight(), id); + + cairo_status_t state = cairo_surface_status(this->surface); + if (state != CAIRO_STATUS_SUCCESS) + { + this->lastError = _("Error save image #1"); + return; + } + + if (page->getBackgroundType().isPdfPage()) + { + int pgNo = page->getPdfPageNr(); + XojPdfPageSPtr popplerPage = doc->getPdfPage(pgNo); + + PdfView::drawPage(NULL, popplerPage, cr, zoom, page->getWidth(), page->getHeight()); + } + + view.drawPage(page, this->cr, true, hideBackground); + + if (!freeSurface(id)) + { + // could not create this file... + this->lastError = _("Error save image #2"); + return; + } +} + +/** + * Create one Graphics file per page + */ +void ImageExport::exportGraphics(ProgressListener* stateListener) +{ + XOJ_CHECK_TYPE(ImageExport); + + // don't lock the page here for the whole flow, else we get a dead lock... + // the ui is blocked, so there should be no changes... + int count = doc->getPageCount(); + + bool onePage = ((this->exportRange.size() == 1) && (this->exportRange[0]->getFirst() == this->exportRange[0]->getLast())); + + char selectedPages[count]; + int selectedCount = 0; + for (int i = 0; i < count; i++) + { + selectedPages[i] = 0; + } + for (PageRangeEntry* e : this->exportRange) + { + for (int x = e->getFirst(); x <= e->getLast(); x++) + { + selectedPages[x] = 1; + selectedCount++; + } + } + + stateListener->setMaximumState(selectedCount); + + DocumentView view; + double zoom = this->pngDpi / 72.0; + int current = 0; + + for (int i = 0; i < count; i++) + { + int id = i + 1; + if (onePage) + { + id = -1; + } + + if (selectedPages[i]) + { + stateListener->setCurrentState(current++); + + exportImagePage(i, id, zoom, format, view); + } + } +} + diff --git a/src/control/jobs/ImageExport.h b/src/control/jobs/ImageExport.h new file mode 100644 index 00000000..57ef6852 --- /dev/null +++ b/src/control/jobs/ImageExport.h @@ -0,0 +1,122 @@ +/* + * Xournal++ + * + * Image export implementation + * + * @author Xournal++ Team + * https://github.com/xournalpp/xournalpp + * + * @license GNU GPLv2 or later + */ + +#pragma once + +#include "view/DocumentView.h" + +#include +#include +#include + +#include + +class Document; +class ProgressListener; + +enum ExportGraphicsFormat { + EXPORT_GRAPHICS_UNDEFINED, + EXPORT_GRAPHICS_PDF, + EXPORT_GRAPHICS_PNG, + EXPORT_GRAPHICS_SVG +}; + +class ImageExport +{ +public: + ImageExport(Document* doc, Path filename, ExportGraphicsFormat format, bool hideBackground, PageRangeVector& exportRange); + virtual ~ImageExport(); + +public: + /** + * PNG dpi + */ + void setPngDpi(int dpi); + + /** + * @return The last error message to show to the user + */ + string getLastErrorMsg(); + + /** + * Create one Graphics file per page + */ + void exportGraphics(ProgressListener* stateListener); + +private: + /** + * Create surface + */ + void createSurface(double width, double height, int id); + + /** + * Free / store the surface + */ + bool freeSurface(int id); + + /** + * Get a filename with a number, e.g. .../export-1.png, if the no is -1, return .../export.png + */ + string getFilenameWithNumber(int no); + + /** + * Export a single Image page + */ + void exportImagePage(int pageId, int id, double zoom, ExportGraphicsFormat format, DocumentView& view); + +public: + XOJ_TYPE_ATTRIB; + + /** + * Document to export + */ + Document* doc = NULL; + + /** + * Filename for export + */ + Path filename; + + /** + * Export graphics format + */ + ExportGraphicsFormat format = EXPORT_GRAPHICS_UNDEFINED; + + /** + * Do not export the Background + */ + bool hideBackground = false; + + /** + * The range to export + */ + PageRangeVector& exportRange; + + /** + * PNG dpi + */ + int pngDpi = 300; + + /** + * Export surface + */ + cairo_surface_t* surface = NULL; + + /** + * Cairo context + */ + cairo_t* cr = NULL; + + /** + * The last error message to show to the user + */ + string lastError; +}; diff --git a/src/control/jobs/ProgressListener.h b/src/control/jobs/ProgressListener.h index 91eef9b6..922df7a4 100644 --- a/src/control/jobs/ProgressListener.h +++ b/src/control/jobs/ProgressListener.h @@ -19,3 +19,12 @@ public: virtual ~ProgressListener() { }; }; + +class DummyProgressListener : public ProgressListener +{ +public: + virtual void setMaximumState(int max) {}; + virtual void setCurrentState(int state) {}; + + virtual ~DummyProgressListener() { }; +}; diff --git a/src/model/Document.cpp b/src/model/Document.cpp index e6ddaf56..00bd63bc 100644 --- a/src/model/Document.cpp +++ b/src/model/Document.cpp @@ -435,6 +435,9 @@ void Document::setPageSize(PageRef p, double width, double height) } } +/** + * @return The last error message to show to the user + */ string Document::getLastErrorMsg() { XOJ_CHECK_TYPE(Document); diff --git a/src/model/Document.h b/src/model/Document.h index b900323e..a7c42edc 100644 --- a/src/model/Document.h +++ b/src/model/Document.h @@ -54,6 +54,9 @@ public: size_t indexOf(PageRef page); + /** + * @return The last error message to show to the user + */ string getLastErrorMsg(); bool isPdfDocumentLoaded(); diff --git a/src/util/XournalTypeList.h b/src/util/XournalTypeList.h index f856bba1..72a4a1e6 100644 --- a/src/util/XournalTypeList.h +++ b/src/util/XournalTypeList.h @@ -155,8 +155,8 @@ XOJ_DECLARE_TYPE(PdfExportJob, 144); XOJ_DECLARE_TYPE(BackgroundImage, 145); XOJ_DECLARE_TYPE(PartList, 146); XOJ_DECLARE_TYPE(PageRangeEntry, 147); - - +XOJ_DECLARE_TYPE(ImageExport, 148); +XOJ_DECLARE_TYPE(ColorSelectImage, 149); XOJ_DECLARE_TYPE(EditSelectionContents, 150); XOJ_DECLARE_TYPE(PageLayerPosEntry, 151); XOJ_DECLARE_TYPE(BackgroundImageContents, 152); @@ -285,7 +285,6 @@ XOJ_DECLARE_TYPE(Plugin, 275); XOJ_DECLARE_TYPE(MenuEntry, 276); XOJ_DECLARE_TYPE(PluginDialog, 277); XOJ_DECLARE_TYPE(PluginDialogEntry, 278); -XOJ_DECLARE_TYPE(ColorSelectImage, 279);