1 /*
2  * Xournal++
3  *
4  * This small program extracts a preview out of a xoj file
5  *
6  * @author Xournal++ Team
7  * https://github.com/xournalpp/xournalpp
8  *
9  * @license GPL
10  */
11 
12 // Set to true to write a log with errors and debug logs to /tmp/xojtmb.log
13 #include "filesystem.h"
14 #define DEBUG_THUMBNAILER false
15 
16 
17 #include <algorithm>
18 #include <fstream>
19 #include <iostream>
20 
21 #include <config-paths.h>
22 #include <config.h>
23 
24 #include "XojPreviewExtractor.h"
25 #include "i18n.h"
26 using std::cerr;
27 using std::cout;
28 using std::endl;
29 #include <cairo-svg.h>
30 #include <cairo.h>
31 #include <librsvg/rsvg.h>
32 
initLocalisation()33 void initLocalisation() {
34 #ifdef ENABLE_NLS
35     bindtextdomain(GETTEXT_PACKAGE, PACKAGE_LOCALE_DIR);
36     textdomain(GETTEXT_PACKAGE);
37 #endif  // ENABLE_NLS
38 
39     std::locale::global(std::locale(""));  //"" - system default locale
40     std::cout.imbue(std::locale());
41 }
42 
logMessage(string msg,bool error)43 void logMessage(string msg, bool error) {
44     if (error) {
45         cerr << msg << endl;
46     } else {
47         cout << msg << endl;
48     }
49 
50 #if DEBUG_THUMBNAILER
51     std::ofstream ofs;
52     ofs.open("/tmp/xojtmb.log", std::ofstream::out | std::ofstream::app);
53 
54     if (error) {
55         ofs << "E: ";
56     } else {
57         ofs << "I: ";
58     }
59 
60     ofs << msg << endl;
61 
62     ofs.close();
63 #endif
64 }
65 
66 static const std::string iconName = "com.github.xournalpp.xournalpp";
67 
68 /**
69  * Search for Xournal++ icon based on the freedesktop icon theme specification
70  */
findAppIcon()71 fs::path findAppIcon() {
72     std::vector<fs::path> basedirs;
73 #if DEBUG_THUMBNAILER
74     basedirs.emplace_back(fs::u8path("../ui/pixmaps"));
75 #endif
76     // $HOME/.icons
77     basedirs.emplace_back(fs::u8path(g_get_home_dir()) / ".icons");
78     // $XDG_DATA_DIRS/icons
79     if (const char* datadirs = g_getenv("XDG_DATA_DIRS")) {
80         std::string dds = datadirs;
81         std::string::size_type lastp = 0;
82         std::string::size_type p;
83         while ((p = dds.find(":", lastp)) != std::string::npos) {
84             std::string path = dds.substr(lastp, p - lastp);
85             basedirs.emplace_back(fs::u8path(path) / "icons");
86             lastp = p + 1;
87         }
88     }
89     basedirs.emplace_back(fs::u8path("/usr/share/pixmaps"));
90 
91     const auto iconFile = iconName + ".svg";
92     // Search through base directories
93     for (auto&& d: basedirs) {
94         fs::path svgPath;
95         if (fs::exists((svgPath = d / "hicolor/scalable/apps" / iconFile))) {
96             return svgPath;
97         } else if (fs::exists((svgPath = d / iconFile))) {
98             return svgPath;
99         }
100     }
101 
102     return "";
103 }
104 
main(int argc,char * argv[])105 int main(int argc, char* argv[]) {
106     initLocalisation();
107 
108     // check args count
109     if (argc != 3) {
110         logMessage(_("xoj-preview-extractor: call with INPUT.xoj OUTPUT.png"), true);
111         return 1;
112     }
113 
114     XojPreviewExtractor extractor;
115     PreviewExtractResult result = extractor.readFile(argv[1]);
116 
117     switch (result) {
118         case PREVIEW_RESULT_IMAGE_READ:
119             // continue to write preview
120             break;
121 
122         case PREVIEW_RESULT_BAD_FILE_EXTENSION:
123             logMessage((_F("xoj-preview-extractor: file \"{1}\" is not .xoj file") % argv[1]).str(), true);
124             return 2;
125 
126         case PREVIEW_RESULT_COULD_NOT_OPEN_FILE:
127             logMessage((_F("xoj-preview-extractor: opening input file \"{1}\" failed") % argv[1]).str(), true);
128             return 3;
129 
130         case PREVIEW_RESULT_NO_PREVIEW:
131             logMessage((_F("xoj-preview-extractor: file \"{1}\" contains no preview") % argv[1]).str(), true);
132             return 4;
133 
134         case PREVIEW_RESULT_ERROR_READING_PREVIEW:
135         default:
136             logMessage(_("xoj-preview-extractor: no preview and page found, maybe an invalid file?"), true);
137             return 5;
138     }
139 
140 
141     gsize dataLen = 0;
142     unsigned char* imageData = extractor.getData(dataLen);
143 
144     // The following code is for rendering the Xournal++ icon on top of thumbnails.
145 
146     // Struct for reading imageData into a cairo surface
147     struct ReadClosure {
148         unsigned int pos;
149         unsigned char* data;
150         gsize maxLen;
151     };
152     cairo_read_func_t processRead =
153             (cairo_read_func_t) + [](ReadClosure* closure, unsigned char* data, unsigned int length) {
154                 if (closure->pos + length > closure->maxLen) {
155                     return CAIRO_STATUS_READ_ERROR;
156                 }
157 
158                 for (auto i = 0; i < length; i++) {
159                     data[i] = closure->data[closure->pos + i];
160                 }
161                 closure->pos += length;
162                 return CAIRO_STATUS_SUCCESS;
163             };
164     ReadClosure closure{0, imageData, dataLen};
165     cairo_surface_t* thumbnail = cairo_image_surface_create_from_png_stream(processRead, &closure);
166     // This application is short-lived, so we'll purposefully be sloppy and let the OS free memory.
167     if (cairo_surface_status(thumbnail) == CAIRO_STATUS_SUCCESS) {
168         GError* err = nullptr;
169         const auto width = cairo_image_surface_get_width(thumbnail);
170         const auto height = cairo_image_surface_get_height(thumbnail);
171         const auto iconSize = 0.5 * std::min(width, height);
172 
173         const auto svgPath = findAppIcon();
174         RsvgHandle* handle = rsvg_handle_new_from_file(svgPath.c_str(), &err);
175         if (err) {
176             logMessage((_F("xoj-preview-extractor: could not find icon \"{1}\"") % iconName).str(), true);
177         } else {
178             rsvg_handle_set_dpi(handle, 90);  // does the dpi matter for an icon overlay?
179             RsvgDimensionData dims;
180             rsvg_handle_get_dimensions(handle, &dims);
181 
182             // Render at bottom right
183             cairo_t* cr = cairo_create(thumbnail);
184             cairo_translate(cr, width - iconSize, height - iconSize);
185             cairo_scale(cr, iconSize / dims.width, iconSize / dims.height);
186             rsvg_handle_render_cairo(handle, cr);
187         }
188         cairo_surface_write_to_png(thumbnail, argv[2]);
189     } else {
190         // Cairo was unable to load the image, so fallback to writing the PNG data to disk.
191         FILE* fp = fopen(argv[2], "wb");
192         if (!fp) {
193             logMessage((_F("xoj-preview-extractor: opening output file \"{1}\" failed") % argv[2]).str(), true);
194             return 6;
195         }
196         fwrite(imageData, dataLen, 1, fp);
197         fclose(fp);
198     }
199 
200     logMessage(_("xoj-preview-extractor: successfully extracted"), false);
201     return 0;
202 }
203