Outils pour utilisateurs

Outils du site


issue195:inkscape

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.

Correction : Cet article inclut une correction pour résoudre un problème dans le code publié le mois dernier qui ne fonctionnait pas si Inkscape insérait la balise <script> près du haut du fichier SVG.

La dernière fois, j'ai montré comment écrire un petit morceau de JavaScript qui peut être ajouté à un fichier Inkscape de plusieurs pages afin que toutes les pages soient visibles dans un navigateur Web en ajoutant « #page-2 » à l'URL (sans les guillemets, et en remplaçant « 2 » par le numéro de la page concernée). C'est une avancée utile, mais Inkscape vous permet de nommer explicitement les pages ; il serait donc intéressant de pouvoir utiliser ces noms dans l'URL, plutôt qu'une chaîne générique. Ce mois-ci, je vais étendre le script existant pour ajouter cette capacité. Mais avant cela, je dois résoudre un problème qui a été découvert par « dwhall » sur le forum officiel d'Inkscape.

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.

Bien que j'aie largement testé le code avant d'écrire l'article précédent, dwhall n'a pas réussi à le faire fonctionner dans ses propres fichiers. En creusant un peu, j'ai pu trouver le problème : dans mes fichiers de test, la balise <script> était insérée à la fin du fichier, alors que la leur l'était au début. Je ne sais pas exactement pourquoi cette différence s'est produite - nous avions tous deux utilisé différentes versions d'Inkscape, et j'avais également fait quelques modifications manuelles en testant le code, de sorte que la différence aurait pu être introduite de plusieurs façons. Le fait que la balise <script> soit placée au début du fichier signifiait que le code JS était exécuté avant que le contenu SVG n'ait fini d'être lu et rendu, de sorte que l'appel à getElementsByTagName(), par exemple, ne renvoyait pas les éléments corrects. Ce même code fonctionnait correctement lorsque la balise se trouvait au bas du fichier.

Une solution consiste à déplacer manuellement la balise <script> à la fin du fichier, mais cela implique de modifier le XML dans un éditeur de texte, ce qui n'est pas une approche très conviviale. Au lieu de cela, j'ai modifié le code en l'enveloppant dans une fonction, nommée « createViewElements() ». Cela empêche le code de s'exécuter immédiatement lorsque le JavaScript est analysé, mais nous devons encore nous assurer qu'il s'exécute toujours une fois que la page a été chargée. La solution consiste à ajouter une ligne à la fin, en dehors de la fonction, qui écoute l'événement « onload » du navigateur et y répond en appelant la fonction.

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.

Avec ces changements, le code ressemble maintenant à ce qui est montré ci-dessus.

J'ai ajouté des numéros de ligne pour indiquer plus clairement à quelles parties du code je fais référence dans le reste de cet article, mais j'utilise la convention qui date de mes années BASIC dans les années 80 et qui consiste à incrémenter les nombres par dizaines afin de pouvoir insérer de nouvelles lignes de code entre les lignes si nécessaire. N'incluez aucun numéro de ligne lorsque vous entrez ce code dans Inkscape ! J'ai également supprimé les appels à console.log() que j'avais précédemment laissés, par souci de concision. Si votre propre code les contient encore, cela ne posera aucun problème.

Si nous voulons que ce code prenne en charge les pages nommées, nous devons commencer par nommer quelques pages dans Inkscape, puis ouvrir l'éditeur XML pour voir comment ces valeurs sont stockées dans le fichier. En utilisant la même page de démonstration que la dernière fois, j'ai donné à chaque page un nom basé sur son contenu - « Étoile rouge », « Diamant orange » et ainsi de suite.

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');

Ce n'est peut-être pas très évident lorsque cette capture d'écran a été redimensionnée et compressée pour correspondre au fichier PDF du magazine, mais si vous le faites vous-même, vous constaterez rapidement que les noms de page sont stockés dans l'élément <inkscape:page> (rien d'étonnant à cela !), dans un attribut appelé « label » qui se trouve également dans l'espace de noms « inkscape ». L'utilisation de l'espace de noms sur l'attribut me semble un peu inutile, étant donné que l'élément entier n'est pas standard et se trouve déjà dans un espace de noms personnalisé, mais nous devons travailler avec le format que nous avons, et non avec ce que nous pensons qu'il devrait être.

Nous devrons extraire les noms de page dans des variables JavaScript pour les traiter ultérieurement. En réalité, notre code fonctionne déjà sur une page à la fois, grâce à la boucle forEach(), et nous n'avons donc besoin que d'extraire le nom venant de l'attribut de la page en cours de traitement. Nous avons déjà utilisé la fonction getAttribute() pour extraire les valeurs d'attribut, mais, cette fois-ci, nous devons également nous occuper de l'espace de noms. Pour cela, il faut utiliser la fonction getAttributeNS(), qui est exactement la même, mais qui prend un espace de noms comme premier paramètre. Malgré cette petite différence, nous garderons regroupé tout le code d'obtention des attributs, ce qui se traduit par l'insertion de la ligne 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, '_');

Il est tout à fait possible que l'attribut label n'existe pas sur une page particulière. Vous devriez être libre d'ajouter des étiquettes à certaines pages et pas à d'autres, sans que le code ne soit cassé. Heureusement, getAttributeNS() ne tombera pas à l'eau dans ce cas, mais retournera une valeur JS nulle. Ainsi, si « label » contient une chaîne de caractères, nous savons que la page a été étiquetée, alors que si elle est nulle, il n'y a pas eu d'étiquetage de la page. Nous pouvons utiliser cela pour écrire une instruction « if » contenant du code qui ne doit être exécuté que si une étiquette est présente. Cette instruction est insérée à la fin de la boucle existante, de sorte que le fichier se présente comme suit (en haut à droite).

Notez le saut dans les numéros de ligne après 162, car nous aurons besoin de plus qu'une seule ligne de code dans cette section. La première chose à faire ici est de s'assurer que l'étiquette est valide pour être utilisée comme identifiant d'un élément <view>. Les ID ont des règles assez strictes sur les caractères qu'ils sont autorisés à contenir, alors que les étiquettes de page Inkscape sont beaucoup plus laxistes. Nous allons adopter une approche plutôt basique de ce problème : nous allons simplement remplacer chaque caractère qui n'est pas alphanumérique par un trait de soulignement.

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.

Celà appelle la méthode JS replace() sur notre chaîne, en lui passant deux paramètres. Le second est évidemment la chaîne de remplacement que nous voulons utiliser, mais qu'en est-il du premier ? Il s'agit d'une expression régulière JavaScript - similaire à celles que vous connaissez peut-être avec grep ou sed en ligne de commande, mais avec quelques différences significatives dans la syntaxe. En JavaScript, une expression régulière est délimitée par des barres obliques (« / »), de la même manière qu'une chaîne de caractères est délimitée par des guillemets. L'expression régulière proprement dite n'est donc que le « \W » entre ces barres obliques. Qu'est-ce que cela signifie ?

Dans le monde de la ligne de commande, vous êtes peut-être à l'aise avec les classes de caractères dans les expressions régulières - des valeurs spéciales telles que [:alnum :] qui correspondent à la plage de caractères alphanumériques (équivalente à [a-zA-Z0-9]). JS possède son propre ensemble de classes de caractères, la correspondance la plus proche de [:alnum :] étant \w (w minuscule). Ce caractère correspond non seulement aux caractères alphanumériques, mais aussi au caractère de soulignement. Non, je ne sais pas pourquoi ils ont décidé d'ajouter un trait de soulignement arbitraire dans le mélange, mais c'est ainsi que cela fonctionne. Chacune de ces classes de caractères a son inverse, qui utilise la version majuscule de la lettre. Ainsi, le « \W » utilisé dans notre code correspond à tout caractère qui n'est PAS un caractère alphanumérique ou un trait de soulignement.

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_star1” 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). Le « g » à la fin de l'expression rationnelle est un indicateur qui la transforme en expression rationnelle « globale ». Cela signifie qu'elle s'appliquera à toutes les sous-chaînes qui correspondent et pas seulement à la première. Sans cela, une étiquette telle que « Étoile rouge n° 1 » serait transformée en « Étoile_rouge n° 1 ». Avec ce caractère supplémentaire, la transformation devient « Red_star__1 ». Notez que le deuxième espace et le caractère dièse sont tous deux transformés, ce qui donne un double trait dans la chaîne finale. Si vous préférez réduire les séquences de plusieurs caractères correspondants à un seul trait de soulignement, remplacez la regex par /\W+/g. Le « + » supplémentaire fera en sorte que « \W » ne corresponde pas seulement à un seul caractère, mais à toute séquence consécutive d'un ou plusieurs caractères, remplaçant ainsi des étendues entières de caractères non alphanumériques par un seul trait de soulignement. Je pense que c'est généralement un peu plus convivial et c'est pourquoi j'inclurai le « + » dans le code final. Maintenant que nous avons un nom convivial, nous pouvons créer le nouvel élément <view>, comme nous l'avons fait la dernière fois. En fait, le code sera pratiquement identique, ce qui en fait une cible de choix pour le transférer dans une fonction distincte. Pour nos besoins, nous voulons une fonction qui prenne en paramètre les chaînes de l'ID et de la viewBox, ainsi qu'une référence à l'élément <page> actuel. Elle créera la nouvelle <view> et l'ajoutera à la <page>, comme auparavant. Le code actuel ressemble à ceci (ci-dessus). 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. Extrayons les lignes 130 à 160 dans une fonction, que nous placerons entre la fin de la fonction existante (ligne 180) et l'écouteur d'événements (ligne 200). Le principal changement dans ce code est que l'ID que nous avons précédemment généré à la ligne 140 va maintenant être transmis à l'appel de la fonction ; ainsi, à la ligne 184 en dessous, nous utilisons le nom du paramètre. Nous allons également déplacer la variable svgNS (précédemment à la ligne 30, maintenant à la ligne 183), car elle n'est utilisée que par la fonction createElementNS() et n'a plus besoin d'exister dans la fonction principale (page suivante, en haut à droite). Si vous avez suivi jusqu'ici, bravo ! Il est temps de faire une pause et de tester les nouvelles fonctionnalités. Vous devriez pouvoir accéder à chaque page soit en ajoutant « #page-n » à l'URL (par exemple, file.svg#page-3), soit en ajoutant un dièse suivi du titre de la page, les caractères non alphanumériques étant remplacés par des traits de soulignement. Avec mon fichier test, l'ajout de « #Orange_diamond » à l'URL produit ceci (image de la page suivante, en bas à gauche). J'ai ouvert les outils de développement et je suis allé dans l'inspecteur afin de voir ce qui s'est réellement passé dans le document. Effectivement, chacun de mes éléments <inkscape:page> contient maintenant deux enfants, tous deux des éléments <view>, avec un ID correspondant au numéro de page et un autre correspondant à l'étiquette. L'un ou l'autre peut être utilisé comme identifiant de fragment dans l'URL pour donner exactement le même résultat. 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. Vous pouvez vous arrêter là si vous voulez. Nous avons atteint l'objectif que nous nous étions fixé d'être capables d'accéder à des pages par leur étiquette. Mais, avec un petit effort, nous pouvons rendre ce script encore plus utile. Il existe une façon pour que ces ajouts soient appliqués en permanence au fichier. Actuellement, notre fichier SVG a une sérieuse limitation. Les éléments <view> sont créés dynamiquement quand le Javascript tourne - mais JS ne tourne que quand le fichier SVG est chargé directement ou via une balise <objet>, deux cas peu communs dans le Web actuel. Le plus souvent, nos fichiers sont tirés sur une page par une balise HTML <img> ou une fonction CSS url(), et dans ces cas nous ne pouvons pas utiliser les identifieurs de fragment, sauf si nous éditons manuellement le fichier pour ajouter des éléments <view> comme décrit dans un article précédent. Pour contourner cela, le code de cet article doit être ajouté, puis le fichier SVG chargé directement dans le navigateur Web et ensuite le fichier sauvé (avec un nouveau nom) dans le navigateur Web. Le nouveau fichier contient la version actuelle du document - c'est-à-dire celle qui contient les éléments <view> que le code a créé. Nous avons simplement automatisé la tache d'ajout des nouveaux éléments au fichier, en évitant le besoin d'une modification manuelle, source d'erreur. 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). Cela fonctionne, mais nous laisse avec un petit problème. Le nouveau fichier contient déjà les éléments <view> supplémentaires… mais il contient également le code qui crée les nouveaux éléments <view>. Si ce fichier est chargé directement dans un navigateur, tous les nouveaux éléments <view> sont créés à nouveau, ce qui fait qu'il y en a deux fois plus que ce que nous voulions. Voici ce que cela donne dans les outils de développement de Firefox (en bas à gauche). Pour résoudre ce problème, nous devons modifier la fonction qui crée les éléments <view> afin de tester l'existence de l'ID dans la page. S'il en existe déjà, la fonction quitte la page avant de créer de nouveaux éléments. Dans le cas contraire, elle continue comme avant. Nous pouvons y parvenir en plaçant quelques lignes au début de la fonction. Leur place est après la ligne 182, mais comme j'ai déjà utilisé les numéros de ligne après celle-ci, je les montrerai sans numéro dans le code ci-dessous (c'était la même chose dans les années 80 - l'idée de numéroter par dizaines tombe à l'eau dès qu'il faut insérer 11 nouvelles lignes !) En haut à droite. La première ligne assigne null à la variable « foundID » si l'ID n'existe PAS déjà dans la page. Dans le cas contraire, elle attribue une référence à l'élément. La deuxième ligne teste ensuite « foundID » et renvoie la fonction si elle n'est pas nulle (c'est-à-dire si l'élément existe déjà). 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. Une fois ces éléments en place, notre fonction améliorée est complète. Voici comment l'utiliser : ••Créez un document Inkscape de plusieurs pages. ••Ajoutez éventuellement des étiquettes à certaines ou à toutes les pages. ••Ajoutez ce script JS complet via la boîte de dialogue Propriétés du document. ••Enregistrez le fichier. ••Chargez-le directement dans un navigateur Web. Cela déclenche la création des éléments <view> supplémentaires. ••Enregistrez le fichier à partir du navigateur Web, en fixant ces nouveaux éléments dans le fichier. Vous pouvez maintenant le charger comme vous le souhaitez et accéder aux pages supplémentaires à l'aide d'un identifiant de fragment basé sur le numéro ou l'étiquette de la page. Enfin, page suivante, voici le code complet que nous avons obtenu. Il n'y a pas de numéro de ligne cette fois-ci, pour faciliter le copier-coller dans Inkscape. Lignes noires de l'encadré p 35 Now we can replace lines 130-160 with a single call to the new function, as follows: Nous pouvons maintenant remplacer les lignes 130-160 par un seul appel à la nouvelle fonction, comme suit: Save the file and make sure it still works as it did previously. Assuming it does, then our last step is to call the new function inside our ‘if’ block, creating another <view> element if the page has a label. This is as simple as it sounds – just adding a new line after we’ve created the ID:** Sauvegardez le fichier et assurez-vous qu'il fonctionne toujours comme avant. Si c'est le cas, notre dernière étape consiste à appeler la nouvelle fonction à l'intérieur de notre bloc « if », en créant un autre élément <view> si la page comporte une étiquette. C'est aussi simple que cela en a l'air - il suffit d'ajouter une nouvelle ligne après avoir créé l'ID:

issue195/inkscape.txt · Dernière modification : 2023/08/03 12:42 de auntiee