Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
fix-broken-links.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * tracks external resources such as image and css files.
4 *
5 * Copyright 2011 Jon A. Cruz <jon@joncruz.org>
6 *
7 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
8 */
9
10#include <set>
11#include <algorithm>
12
13#include <gtkmm/recentmanager.h>
14#include <glibmm/i18n.h>
15#include <glibmm/miscutils.h>
16#include <glibmm/fileutils.h>
17#include <glibmm/uriutils.h>
18#include <glibmm/convert.h>
19
20#include "fix-broken-links.h"
21
22#include "document.h"
23#include "document-undo.h"
24
25#include "object/sp-object.h"
26
27#include "ui/icon-names.h"
28
29#include "xml/node.h"
31
32namespace Inkscape {
33
34std::vector<std::string> splitPath( std::string const &path )
35{
36 std::vector<std::string> parts;
37
38 std::string prior;
39 std::string tmp = path;
40 while ( !tmp.empty() && (tmp != prior) ) {
41 prior = tmp;
42
43 parts.push_back( Glib::path_get_basename(tmp) );
44 tmp = Glib::path_get_dirname(tmp);
45 }
46 if ( !parts.empty() ) {
47 std::reverse(parts.begin(), parts.end());
48 if ( (parts[0] == ".") && (path[0] != '.') ) {
49 parts.erase(parts.begin());
50 }
51 }
52
53 return parts;
54}
55
67std::string optimizePath(std::string const &path, std::string const &base, unsigned int parents)
68{
69 std::string result = path;
70
71 if (!path.empty() && Glib::path_is_absolute(path)) {
72
73 // Whack the parts into pieces
74 std::vector<std::string> parts = splitPath(path);
75 std::vector<std::string> baseParts = splitPath(base);
76
77 if ( !parts.empty() && !baseParts.empty() && (parts[0] == baseParts[0]) ) {
78 // Both paths have the same root. We can proceed.
79 while ( !parts.empty() && !baseParts.empty() && (parts[0] == baseParts[0]) ) {
80 parts.erase( parts.begin() );
81 baseParts.erase( baseParts.begin() );
82 }
83
84 if (!parts.empty() && baseParts.size() <= parents) {
85 result.clear();
86
87 for ( size_t i = 0; i < baseParts.size(); ++i ) {
88 parts.insert(parts.begin(), "..");
89 }
90 result = Glib::build_filename( parts );
91 }
92 }
93 }
94
95 return result;
96}
97
98
99bool fixBrokenLinks(SPDocument *doc);
100
101
107static std::vector<Glib::ustring> findBrokenLinks(SPDocument *doc);
108
117static std::map<Glib::ustring, Glib::ustring> locateLinks(Glib::ustring const & docbase, std::vector<Glib::ustring> const & brokenLinks);
118
119
125static bool extractFilepath(Glib::ustring const &href, std::string &filename);
126
133static bool reconstructFilepath(Glib::ustring const &href, std::string &filename);
134
135static bool searchUpwards( std::string const &base, std::string const &subpath, std::string &dest );
136
137
138
139static bool extractFilepath(Glib::ustring const &href, std::string &filename)
140{
141 bool isFile = false;
142
143 filename.clear();
144
145 auto scheme = Glib::uri_parse_scheme(href.raw());
146 if ( !scheme.empty() ) {
147 // TODO debug g_message("Scheme is now [%s]", scheme.c_str());
148 if ( scheme == "file" ) {
149 // TODO debug g_message("--- is a file URI [%s]", href.c_str());
150
151 // throws Glib::ConvertError:
152 try {
153 filename = Glib::filename_from_uri(href);
154 isFile = true;
155 } catch(Glib::ConvertError e) {
156 g_warning("%s", e.what());
157 }
158 }
159 } else {
160 // No scheme. Assuming it is a file path (absolute or relative).
161 // throws Glib::ConvertError:
162 filename = Glib::filename_from_utf8(href);
163 isFile = true;
164 }
165
166 return isFile;
167}
168
169static bool reconstructFilepath(Glib::ustring const &href, std::string &filename)
170{
171 bool isFile = false;
172
173 filename.clear();
174
175 auto scheme = Glib::uri_parse_scheme(href.raw());
176 if ( !scheme.empty() ) {
177 if ( scheme == "file" ) {
178 // try to build a relative filename for URIs like "file:image.png"
179 // they're not standard conformant but not uncommon
180 Glib::ustring href_new = Glib::ustring(href, 5);
181 filename = Glib::filename_from_utf8(href_new);
182 isFile = true;
183 }
184 }
185 return isFile;
186}
187
188
189static std::vector<Glib::ustring> findBrokenLinks( SPDocument *doc )
190{
191 std::vector<Glib::ustring> result;
192 std::set<Glib::ustring> uniques;
193
194 if ( doc ) {
195 std::vector<SPObject *> images = doc->getResourceList("image");
196 for (auto image : images) {
197 Inkscape::XML::Node *ir = image->getRepr();
198
199 gchar const *href = Inkscape::getHrefAttribute(*ir).second;
200 if ( href && ( uniques.find(href) == uniques.end() ) ) {
201 std::string filename;
202 if (extractFilepath(href, filename)) {
203 if (Glib::path_is_absolute(filename)) {
204 if (!Glib::file_test(filename, Glib::FileTest::EXISTS)) {
205 result.emplace_back(href);
206 uniques.insert(href);
207 }
208 } else {
209 std::string combined = Glib::build_filename(doc->getDocumentBase(), filename);
210 if ( !Glib::file_test(combined, Glib::FileTest::EXISTS) ) {
211 result.emplace_back(href);
212 uniques.insert(href);
213 }
214 }
215 } else if (reconstructFilepath(href, filename)) {
216 result.emplace_back(href);
217 uniques.insert(href);
218 }
219 }
220 }
221 }
222
223 return result;
224}
225
226
227static std::map<Glib::ustring, Glib::ustring> locateLinks(Glib::ustring const & docbase, std::vector<Glib::ustring> const & brokenLinks)
228{
229 std::map<Glib::ustring, Glib::ustring> result;
230
231
232 // Note: we use a vector because we want them to stay in order:
233 std::vector<std::string> priorLocations;
234
235 Glib::RefPtr<Gtk::RecentManager> recentMgr = Gtk::RecentManager::get_default();
236 std::vector< Glib::RefPtr<Gtk::RecentInfo> > recentItems = recentMgr->get_items();
237 for (auto & recentItem : recentItems) {
238 Glib::ustring uri = recentItem->get_uri();
239 auto scheme = Glib::uri_parse_scheme(uri.raw());
240 if ( scheme == "file" ) {
241 try {
242 std::string path = Glib::filename_from_uri(uri);
243 path = Glib::path_get_dirname(path);
244 if ( std::find(priorLocations.begin(), priorLocations.end(), path) == priorLocations.end() ) {
245 // TODO debug g_message(" ==>[%s]", path.c_str());
246 priorLocations.push_back(path);
247 }
248 } catch (Glib::ConvertError e) {
249 g_warning("%s", e.what());
250 }
251 }
252 }
253
254 // At the moment we expect this list to contain file:// references, or simple relative or absolute paths.
255 for (const auto & brokenLink : brokenLinks) {
256 // TODO debug g_message("========{%s}", it->c_str());
257
258 std::string filename;
259 if (extractFilepath(brokenLink, filename) || reconstructFilepath(brokenLink, filename)) {
260 auto const docbase_native = Glib::filename_from_utf8(docbase);
261
262 // We were able to get some path. Check it
263 std::string origPath = filename;
264
265 if (!Glib::path_is_absolute(filename)) {
266 filename = Glib::build_filename(docbase_native, filename);
267 }
268
269 bool exists = Glib::file_test(filename, Glib::FileTest::EXISTS);
270
271 // search in parent folders
272 if (!exists) {
273 exists = searchUpwards(docbase_native, origPath, filename);
274 }
275
276 // Check if the MRU bases point us to it.
277 if ( !exists ) {
278 if ( !Glib::path_is_absolute(origPath) ) {
279 for ( std::vector<std::string>::iterator it = priorLocations.begin(); !exists && (it != priorLocations.end()); ++it ) {
280 exists = searchUpwards(*it, origPath, filename);
281 }
282 }
283 }
284
285 if ( exists ) {
286 if (Glib::path_is_absolute(filename)) {
287 filename = optimizePath(filename, docbase_native);
288 }
289
290 bool isAbsolute = Glib::path_is_absolute(filename);
291 Glib::ustring replacement =
292 isAbsolute ? Glib::filename_to_uri(filename) : Glib::filename_to_utf8(filename);
293 result[brokenLink] = replacement;
294 }
295 }
296 }
297
298 return result;
299}
300
302{
303 bool changed = false;
304 if ( doc ) {
305 // TODO debug g_message("FIXUP FIXUP FIXUP FIXUP FIXUP FIXUP FIXUP FIXUP FIXUP FIXUP");
306 // TODO debug g_message(" base is [%s]", doc->getDocumentBase());
307
308 std::vector<Glib::ustring> brokenHrefs = findBrokenLinks(doc);
309 if ( !brokenHrefs.empty() ) {
310 // TODO debug g_message(" FOUND SOME LINKS %d", static_cast<int>(brokenHrefs.size()));
311 for ( std::vector<Glib::ustring>::iterator it = brokenHrefs.begin(); it != brokenHrefs.end(); ++it ) {
312 // TODO debug g_message(" [%s]", it->c_str());
313 }
314 }
315
316 Glib::ustring base;
317 if (doc->getDocumentBase()) {
318 base = doc->getDocumentBase();
319 }
320
321 std::map<Glib::ustring, Glib::ustring> mapping = locateLinks(base, brokenHrefs);
322 for ( std::map<Glib::ustring, Glib::ustring>::iterator it = mapping.begin(); it != mapping.end(); ++it )
323 {
324 // TODO debug g_message(" [%s] ==> {%s}", it->first.c_str(), it->second.c_str());
325 }
326
328
329 std::vector<SPObject *> images = doc->getResourceList("image");
330 for (auto image : images) {
331 Inkscape::XML::Node *ir = image->getRepr();
332
333 auto [href_key, href] = Inkscape::getHrefAttribute(*ir);
334 if ( href ) {
335 // TODO debug g_message(" consider [%s]", href);
336
337 if ( mapping.find(href) != mapping.end() ) {
338 // TODO debug g_message(" Found a replacement");
339
340 ir->setAttributeOrRemoveIfEmpty(href_key, mapping[href]);
341 if ( ir->attribute( "sodipodi:absref" ) ) {
342 ir->removeAttribute("sodipodi:absref"); // Remove this attribute
343 }
344
345 SPObject *updated = doc->getObjectByRepr(ir);
346 if (updated) {
347 // force immediate update of dependent attributes
348 updated->updateRepr();
349 }
350
351 changed = true;
352 }
353 }
354 }
355 if ( changed ) {
356 DocumentUndo::done( doc, _("Fixup broken links"), INKSCAPE_ICON("dialog-xml-editor"));
357 }
358 }
359
360 return changed;
361}
362
363static bool searchUpwards( std::string const &base, std::string const &subpath, std::string &dest )
364{
365 bool exists = false;
366 // TODO debug g_message("............");
367
368 std::vector<std::string> parts = splitPath(subpath);
369 std::vector<std::string> baseParts = splitPath(base);
370
371 while ( !exists && !baseParts.empty() ) {
372 std::vector<std::string> current;
373 current.insert(current.begin(), parts.begin(), parts.end());
374 // TODO debug g_message(" ---{%s}", Glib::build_filename( baseParts ).c_str());
375 while ( !exists && !current.empty() ) {
376 std::vector<std::string> combined;
377 combined.insert( combined.end(), baseParts.begin(), baseParts.end() );
378 combined.insert( combined.end(), current.begin(), current.end() );
379 std::string filepath = Glib::build_filename( combined );
380 exists = Glib::file_test(filepath, Glib::FileTest::EXISTS);
381 // TODO debug g_message(" ...[%s] %s", filepath.c_str(), (exists ? "XXX" : ""));
382 if ( exists ) {
383 dest = filepath;
384 }
385 current.erase( current.begin() );
386 }
387 baseParts.pop_back();
388 }
389
390 return exists;
391}
392
393} // namespace Inkscape
394
395/*
396 Local Variables:
397 mode:c++
398 c-file-style:"stroustrup"
399 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
400 indent-tabs-mode:nil
401 fill-column:99
402 End:
403*/
404// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
RAII-style mechanism for creating a temporary undo-insensitive context.
static void done(SPDocument *document, Glib::ustring const &event_description, Glib::ustring const &undo_icon, unsigned int object_modified_tag=0)
Interface for refcounted XML nodes.
Definition node.h:80
void setAttributeOrRemoveIfEmpty(Inkscape::Util::const_char_ptr key, Inkscape::Util::const_char_ptr value)
Change an attribute of this node.
Definition node.cpp:167
virtual char const * attribute(char const *key) const =0
Get the string representation of a node's attribute.
void removeAttribute(Inkscape::Util::const_char_ptr key)
Remove an attribute of this node.
Definition node.h:280
Typed SVG document implementation.
Definition document.h:101
char const * getDocumentBase() const
Definition document.h:233
std::vector< SPObject * > const getResourceList(char const *key)
SPObject * getObjectByRepr(Inkscape::XML::Node *repr) const
SPObject is an abstract base class of all of the document nodes at the SVG document level.
Definition sp-object.h:160
Inkscape::XML::Node * updateRepr(unsigned int flags=SP_OBJECT_WRITE_EXT)
Updates the object's repr based on the object's state.
Css & result
static char const *const current
Definition dir-util.cpp:71
TODO: insert short description here.
Macro for icon names used in Inkscape.
std::unique_ptr< Magick::Image > image
Helper class to stream background task notifications as a series of messages.
static bool searchUpwards(std::string const &base, std::string const &subpath, std::string &dest)
static bool extractFilepath(Glib::ustring const &href, std::string &filename)
Try to parse href into a local filename using standard methods.
std::vector< std::string > splitPath(std::string const &path)
bool fixBrokenLinks(SPDocument *doc)
static bool reconstructFilepath(Glib::ustring const &href, std::string &filename)
Try to parse href into a local filename using some non-standard methods.
static std::map< Glib::ustring, Glib::ustring > locateLinks(Glib::ustring const &docbase, std::vector< Glib::ustring > const &brokenLinks)
Resolve broken links as a whole and return a map for those that can be found.
std::pair< char const *, char const * > getHrefAttribute(XML::Node const &node)
Get the 'href' or 'xlink:href' (fallback) attribute from an XML node.
std::string optimizePath(std::string const &path, std::string const &base, unsigned int parents)
Convert an absolute path into a relative one if possible to do in the given number of parent steps.
static std::vector< Glib::ustring > findBrokenLinks(SPDocument *doc)
Walk all links in a document and create a listing of unique broken links.
Interface for XML nodes.