Outils pour utilisateurs

Outils du site


issue195:inkscape

Ceci est une ancienne révision du document !


Correction: This article includes a fix to address an issue with the code published last month which failed to work if Inkscape inserted the <script> tag near the top of the SVG file. Last time I demonstrated how to write a small chunk of JavaScript that can be added to a multi-page Inkscape file in order for all the pages to be viewable in a web browser by appending ‘#page-2’ to the URL (without the quotes, and replacing ‘2’ with the relevant page number). That’s a useful step forward, but Inkscape lets you explicitly name pages, so it would be nice if we could use those names in the URL, rather than a generic string. This month I’m going to extend the existing script to add exactly this capability. But before I do, I need to address an issue that was discovered by ‘dwhall’ on the official Inkscape forum.

Although I tested the code extensively before writing the previous article, dwhall was unable to get it working in their own files. With a bit of digging, I was able to track the problem down : my test files had the <script> tag inserted at the end of the file, whereas theirs was inserted near the start. I’m not sure why this difference occurred – we had both used various Inkscape releases, and I’d also made some manual edits while testing the code, so the difference could potentially have been introduced in a number of different ways. Having the <script> tag near the start meant that the JS was being executed before the SVG content had finished being read and rendered, so the call to getElementsByTagName(), for example, was failing to return the correct elements. That same code worked fine when it was at the bottom of the file. One solution to this is to manually move the <script> tag to the end of the file – but that involves editing XML in a text editor, which is not a very user-friendly approach. Instead I’ve modified the code by wrapping it all in a function, named ‘createViewElements()’. That stops the code running immediately as the JavaScript is parsed, but we still need some way to make sure it still runs once the page has loaded. The answer to this is to add a line at the end, outside the function, which listens for the browser’s ‘onload’ event, and responds by calling the function.

With these changes in place, the code now looks like that shown above. I’ve added line numbers to make it clearer which parts of the code I’m referring to in the rest of this month’s article, but I’m using the convention from my BASIC days back in the 80s of incrementing the numbers in tens so that I can squeeze new lines of code in-between as necessary. Do not include any line numbers when entering this code into Inkscape! I’ve also removed the console.log() calls that I’d previously left in, for brevity. If your own code still contains them it won’t cause any problems. If we’re going to make this code support named pages, we’ll need to begin by naming some pages in Inkscape, then open the XML editor to see how those values are stored in the file. Using the same demo page as last time, I’ve given each page a name based on its contents – ‘Red star’, ‘Orange diamond’ and so on.

It may not be very obvious by the time this screenshot has been resized and compressed to fit the magazine’s PDF file, but if you do this yourself you’ll quickly find that the page names are stored as part of the <inkscape:page> element (no surprise there!), in an attribute called ‘label’ which is also in the ‘inkscape’ namespace. The use of the namespace on the attribute seems a little unnecessary to me, given that the whole element is non-standard and in a custom namespace already, but we have to work with the format we’ve got, not what we think it should be. We’ll need to extract the page names into JavaScript variables for further processing. In reality our code already works on one page at a time, courtesy of the forEach() loop, so we actually only need to get the name from the attribute in the page that’s currently being processed. We’ve already used the getAttribute() function to extract attribute values, but this time we’ve also got that pesky namespace to deal with. That requires the getAttributeNS() function which is exactly the same, but takes a namespace as the first parameter. Despite this small difference, we’ll keep all the attribute-getting code together, resulting in the insertion of line 105: 105 const label = page.getAttributeNS(inkNS, 'label');

It’s entirely possible that the label attribute doesn’t exist on a particular page. You should be free to just add labels to some pages and not others, without the code breaking. Fortunately, getAttributeNS() won’t just fall over in that case, but rather it will return a JS null value. So if ‘label’ ends up holding a string, we know that the page had a label applied, whereas if it is null, there was no page label. We can use that to write an ‘if’ statement containing code that should only run if a label is present. This goes inside the end of the existing loop, so the file looks like this (top right). Note the jump in line numbers after 162, as we’ll need more than just a single line of code in this section. The first thing we’ll need to do in here is to ensure that the label is valid for use as the ID for a <view> element. IDs have pretty strict rules about what characters they’re allowed to contain, whereas Inkscape page labels are far more lax. We’ll take a rather basic approach to this problem: we’ll just replace every character that’s not alphanumeric with an underscore. 163 const idLabel = label.replace(/\W/g, '_');

This calls the JS replace() method on our string, passing it two parameters. The second is pretty obviously the replacement string we want to use, but what about that first one? That’s a JavaScript regular expression – similar to those that you may be familiar with from grep or sed on the command line, but with some significant differences in syntax. In JS a regular expression is delimited by slash characters (‘/’) in much the same way that a string is delimited by quotes. So the actual regular expression itself is just the ‘\W’ between those slashes. What does that mean? In the command line world you might be familiar with character classes in regular expressions – special values such as [:alnum:] which match against the range of alphanumeric characters (equivalent to [a-zA-Z0-9]). JS has its own set of character classes, with the closest match to [:alnum:] being \w (lower-case ‘w’). This actually matches not only alphanumeric characters, but also the underscore character. No, I don’t know why they decided to throw an arbitrary underscore into the mix, but that’s the way it works. Each of these character classes has an inverse, using the upper-case version of the letter. So the ‘\W’ used in our code matches any character that is NOT an alphanumeric character or an underscore.

The ‘g’ at the end of the regex is a flag that turns this into a ‘global’ regular expression. All that means is that it will apply to all the substrings that match, not just the first one. Without this a label such as “Red star #1” would be transformed into “Red_star #1”. With this one extra character the transformation becomes “Red_star__1” instead. Note that both the second space and the hash character are transformed, resulting in a double-underscore in the final string. If you prefer to collapse sequences of multiple matching characters down to a single underscore, replace the regex with /\W+/g instead. The additional ‘+’ will cause the ‘\W’ to not just match a single character, but any consecutive sequence of one or more characters, replacing entire spans of non-alphanumerics with a single underscore. I think this is usually a little more user-friendly, so I’ll include the ‘+’ in the final code. Now that we’ve got an ID-friendly name, we can create the new <view> element, just like we did last time. In fact, the code will be virtually identical, which makes it a prime target for moving out into a separate function. For our purposes we want a function that takes the strings for the ID and viewBox as parameters, as well as a reference to the current <page> element. It will create the new <view> and append it to the <page>, as before. The current code looks like this (above).

Let’s extract lines 130 to 160 into a function, which we’ll put between the end of the existing function (line 180) and the event listener (line 200). The main change in this code is that the ID that we previously generated on line 140 is now going to be passed in to the function call, so on line 184 below we use the parameter name. We’ll also move the svgNS variable (previously on line 30, now on line 183) as it’s only used by the createElementNS() function and no longer needs to exist in the main function (next page, top right). If you’ve kept up so far, then well done! It’s time to take a break and test the new functionality. You should be able to access each page either by appending ‘#page-n’ to the URL (e.g. file.svg#page-3) or by appending a hash followed by the page label, with non-alphanumerics replaced by underscores. With my test file, appending ‘#Orange_diamond’ to the URL produces this (image shown next page, bottom left). I’ve opened the developer tools and switched to the Inspector in order to see what’s actually happened in the document. Sure enough, each of my <inkscape:page> elements now contains two children, both <view> elements, with an ID corresponding to the page number, and another corresponding to the label. Either can be used as a fragment identifier in the URL to give exactly the same result.

You can stop there if you want. We’ve achieved the goal we set for ourselves, of being able to access pages by their label. But with little effort we can make this script even more useful. There’s a way we can apply those additions to the file, permanently. Currently our SVG file has a significant limitation. The <view> elements are created dynamically when the JavaScript runs – but JS only runs when we load the SVG file directly, or via an <object> tag, neither of which are common in today’s web. More often our files are pulled into a page via an HTML <img> tag, or a CSS url() function, and in those cases we can’t use fragment identifiers unless we manually edit the file to add <view> elements as described a couple of articles ago. One workaround to this is to add the code from this article, load the SVG directly into a web browser, and then save the page (with a new name) from within the web browser. That creates a new file that contains the current version of the document – i.e. the one which contains the <view> elements that the code created. We’ve just automated the task of adding the new elements to the file, avoiding the need for our error-prone manual editing.

That works, but leaves us with a small problem. The new file already contains the additional <view> elements… but it also still contains the code that creates new <view> elements. If that file is loaded directly into a browser, all the new <view> elements get created again, resulting in twice as many as we wanted. Here’s how it looks in the Firefox developer tools (bottom left). To fix this we need to modify the function that creates the <view> elements to test for the existence of the ID in the page. If it already exists then the function just exits before creating any new elements. Otherwise it carries on as before. We can achieve this with a couple of lines at the top of the function. They go after line 182, but as I’ve already used the line numbers after that, I’ll show them without numbers in the code below (it was the same in the 80s – the idea of numbering in tens falls down as soon as you need to insert 11 new lines!). Shown top right. The first line assigns null to the ‘foundID’ variable if the ID does NOT already exist in the page. Otherwise it assigns a reference to the element. The second line then tests ‘foundID’ and returns from the function if it’s not null (i.e. if the element already exists).

With that in place, our enhanced function is complete. Here’s a breakdown of how to use it: • Create a multi-page Inkscape document. • Optionally add labels to some or all of the pages. • Add this entire JS script via the Document Properties dialog. • Save the file. • Load it directly into a web browser. That triggers the creation of the extra <view> elements. • Save it from the web browser, fixing those new elements into the file. Now you can load it any way you want, and access the additional pages with a fragment identifier based on the page number or label. Finally, next page, here’s the complete code we ended up with. No line numbers this time, to make it easier for you to copy and paste it into Inkscape.

issue195/inkscape.1690921521.txt.gz · Dernière modification : 2023/08/01 22:25 de d52fr