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