Over the previous few instalments, I’ve been looking at Inkscape’s new multi-page feature, culminating in last month’s analysis of how it’s implemented within the SVG file. What I found was that the details of each page are stored in a proprietary ‘page’ element in the Inkscape namespace – appearing in the XML editor as <inkscape:page> – rather than in a standard SVG <view> element. This is a strange choice, in my opinion, as using the latter would have made the extra pages more accessible via a web browser. Last month, I described a couple of methods by which a browser can be coaxed into showing the extra pages. The first used a little-known version of the URL fragment identifier to target each page by its viewBox values. This doesn’t require the file to be modified, but does need you to dig into the XML to extract the relevant numbers. The second approach modifies the file to manually add <view> elements, which is more work but does allow you to use a more semantic URL. If you haven’t read the previous article, I strongly advise doing so before proceeding, as this one directly builds on that information. Our aim here is to make it easier to access all the pages of an Inkscape document via a web browser. One thing browsers do, which Inkscape doesn’t, is to run JavaScript. So, by adding a small JS function that will be executed when the file is opened in a browser, we can automatically create <view> elements from each of the <inkscape:page> elements, without the need to manually copy-and-paste coordinates. This is obviously useful when there are a lot of pages to consider, but even for files with just a couple of pages, the ability to just paste the same snippet of code into any file without having to modify it each time makes this approach arguably simpler than manually creating the <view> elements yourself.
Au cours des derniers numéros, j'ai étudié la nouvelle fonctionnalité multi-pages d'Inkscape, avec en point d'orgue l'analyse du mois dernier sur la façon dont elle est implémentée dans le fichier SVG. J'ai découvert que les détails de chaque page sont stockés dans un élément propriétaire « page » de l'espace de noms Inkscape - apparaissant dans l'éditeur XML comme <inkscape:page> - plutôt que dans un élément SVG standard <view>. C'est un choix étrange, à mon avis, car l'utilisation de ce dernier aurait rendu les pages supplémentaires plus accessibles via un navigateur Web.
Le mois dernier, j'ai décrit deux méthodes permettant d'inciter un navigateur à afficher les pages supplémentaires. La première utilise une version peu connue de l'identificateur de fragment d'URL pour cibler chaque page par ses valeurs viewBox. Il n'est pas nécessaire de modifier le fichier, mais il faut fouiller dans le XML pour extraire les chiffres pertinents. La seconde approche consiste à modifier le fichier pour ajouter manuellement des éléments <view>, ce qui demande plus de travail, mais permet d'utiliser une URL plus sémantique. Si vous n'avez pas lu l'article précédent, je vous conseille vivement de le faire avant de continuer, car celui-ci s'appuie directement sur les informations données alors.
Notre objectif ici est de faciliter l'accès à toutes les pages d'un document Inkscape via un navigateur Web. Une chose que les navigateurs font, et qu'Inkscape ne fait pas, est d'exécuter JavaScript. Ainsi, en ajoutant une petite fonction JS qui sera exécutée lorsque le fichier est ouvert dans un navigateur, nous pouvons créer automatiquement des éléments <view> à partir de chacun des éléments <inkscape:page>, sans devoir copier/coller les coordonnées à la main. C'est évidemment utile lorsqu'il y a beaucoup de pages à prendre en compte, mais même pour des fichiers ne comportant que quelques pages, la possibilité de coller le même bout de code dans n'importe quel fichier sans avoir à le modifier à chaque fois rend cette approche sans doute plus simple que de créer manuellement les éléments <view> soi-même.
We’ll be using the same multi-page SVG file from last month, consisting of four pages arranged horizontally, with a different background color and content in each. Having created a multi-page Inkscape file, the first thing to do is to save it to disk and open it inside a web browser. This will display only the first page, as expected. As we progress through this code, we’ll log some data to the developer console, so open the dev tools in the browser (typically by pressing F12), and switch to the ‘Console’ tab. Below is how it looks in Firefox on my Ubuntu Mate machine.
Nous utiliserons le même fichier SVG multipage que le mois dernier, composé de quatre pages disposées horizontalement, avec une couleur d'arrière-plan et un contenu différents pour chacune d'entre elles.
Après avoir créé un fichier Inkscape multipage, la première chose à faire est de l'enregistrer sur le disque et de l'ouvrir dans un navigateur Web. Comme prévu, seule la première page s'affichera. Au fur et à mesure que nous progresserons dans ce code, nous enregistrerons certaines données dans la console de développement, donc ouvrez les outils de développement dans le navigateur (généralement en appuyant sur F12), et passez à l'onglet « Console ». Voici ce que cela donne dans Firefox sur ma machine Ubuntu Mate.
I’ve talked about adding JavaScript code to Inkscape files in the past, so I won’t go into depth about the options here. For what we’re trying to achieve, we just want a simple chunk of script embedded directly into the file which will run at load time. The easiest way to create this within Inkscape is as follows: • Open the File > Document Properties… dialog • Switch to the ‘Scripting’ tab. • Within that tab, choose the ‘Embedded scripts’ tab. • Click the ‘+’ button below the (empty) list of embedded scripts. • You should see a new entry appear in the list with an arbitrary ID. • Ensure that entry is selected. • Write your code in the ‘Content’ box below. • You can save (Ctrl-S) as you develop, without having to close the dialog. • Whenever you save the file, manually reload in the browser (F5) to see the effect. Let’s begin by logging out the existing <inkscape:page> elements to the console using this code (top right).
J'ai déjà parlé de l'ajout de code JavaScript dans des fichiers Inkscape par le passé ; je n'entrerai donc pas dans les détails des options ici. Pour ce que nous essayons de réaliser, nous voulons juste un simple morceau de script incorporé directement dans le fichier qui s'exécutera au moment du chargement. La façon la plus simple de créer cela dans Inkscape est la suivante : ••Ouvrez la boîte de dialogue Fichier > Propriétés du document… ••Passez dans l'onglet « Script ». ••Dans cet onglet, choisissez l'onglet « Scripts intégrés ». ••Cliquez sur le bouton « + » sous la liste (vide) des scripts intégrés. ••Vous devriez voir apparaître une nouvelle entrée dans la liste avec un ID quelconque. ••Assurez-vous que l'entrée est sélectionnée. ••Écrivez votre code dans la case « Contenu » en dessous. ••Vous pouvez enregistrer (Ctrl-S) au fur et à mesure que vous développez, sans devoir fermer la boîte de dialogue. ••Chaque fois que vous enregistrez le fichier, rechargez-le manuellement dans le navigateur (F5) pour en voir l'effet.
Commençons par déconnecter les éléments <inkscape:page> existants vers la console à l'aide de ce code (en haut à droite).
It’s not essential to fully understand the workings of this code in order to use it, but I’ll describe it anyway for those who are interested. The first two lines simply set up variables for the namespaces we’ll need. We won’t be using the SVG namespace just yet, but we will require it soon, so this is a good time to introduce it. The third line uses one of the browser’s built-in functions to find all the ‘page’ elements in the Inkscape namespace, and assign them to a variable as a ‘collection’. For historical and technical reasons, a ‘collection’ is very similar to a JS array, but not quite actually the same thing. These days, JavaScript’s array functions are pretty powerful, so we would really like to create an array from the content of our collection so we can use those functions. The ‘array.from(pages)’ part does exactly that, iterating over each entry in the collection to build up a temporary array. We can then use the ‘forEach’ array method to execute a block of code for each entry in the array. The forEach() method expects to have one parameter, and that parameter should be a function.
Il n'est pas indispensable de bien comprendre le fonctionnement de ce code pour pouvoir l'utiliser, mais je vais quand même le décrire pour ceux que cela intéresse. Les deux premières lignes définissent simplement des variables pour les espaces de noms dont nous aurons besoin. Nous n'utiliserons pas encore l'espace de noms SVG, mais nous en aurons bientôt besoin et c'est donc le bon moment pour l'introduire. La troisième ligne utilise l'une des fonctions intégrées du navigateur pour trouver tous les éléments « page » dans l'espace de noms Inkscape, et les assigner à une variable en tant que « collection ».
Pour des raisons historiques et techniques, une « collection » est très similaire à un tableau JS, mais ce n'est pas tout à fait la même chose. De nos jours, les fonctions de tableau de JavaScript sont très puissantes, et nous aimerions donc créer un tableau à partir du contenu de notre collection afin de pouvoir utiliser ces fonctions. C'est exactement ce que fait la partie « array.from(pages) », en itérant par-dessus chaque entrée de la collection pour construire un tableau temporaire. Nous pouvons ensuite utiliser la méthode « forEach » pour exécuter un bloc de code pour chaque entrée du tableau.
La méthode forEach() s'attend à avoir un paramètre, et ce paramètre doit être une fonction.
We could build a function elsewhere and pass its name in here, but it’s more common in JS to see ‘anonymous’ functions used for small tasks like this one. In this case, the anonymous function is called once for each element in the array, and, each time it’s called, it’s passed the current element (in a variable we’ve called ‘page’) and the index of that element in the array (‘idx’). The ‘⇒’ syntax is used for so-called arrow functions, and can be thought of largely as an alternative to the ‘function’ keyword you might be more familiar with if you haven’t gone near JS for a while. Inside curly braces (‘{…}’) we have the body of the function – just a single line that uses the console.log() function to print the idx and pages values to the developer console. Finally, the last line closes not only the function body, but also the end of the forEach() method. If you’re not very familiar with JavaScript, then do take a few minutes to try to understand the code above. It’s particularly useful to examine where each pair of brackets (‘(…)’) and braces (‘{…}’) start and end, and what content is inside each one.
Nous pourrions créer une fonction ailleurs et passer son nom ici, mais il est plus courant en JS de voir des fonctions « anonymes » utilisées pour de petites tâches comme celle-ci. Dans ce cas, la fonction anonyme est appelée une fois pour chaque élément du tableau et, à chaque fois qu'elle est appelée, on lui passe l'élément courant (dans une variable que nous avons appelée « page ») et l'index de cet élément dans le tableau (« idx »). La syntaxe « ⇒ » est utilisée pour les fonctions dites « flèches » et peut être considérée comme une alternative au mot-clé « function » qui vous est peut-être plus familier si vous n'avez pas approché JS depuis un certain temps.
À l'intérieur des accolades (« {…} ») se trouve le corps de la fonction - une seule ligne qui utilise la fonction console.log() pour imprimer les valeurs idx et pages sur la console du développeur. Enfin, la dernière ligne ferme non seulement le corps de la fonction, mais aussi la fin de la méthode forEach().
Si vous n'êtes pas très familier avec JavaScript, prenez quelques minutes pour essayer de comprendre le code ci-dessus. Il est particulièrement utile d'examiner où chaque paire de crochets (« (…) ») et d'accolades (« {…} ») commence et se termine, et quel contenu se trouve à l'intérieur de chacune d'entre elles.
With that code in place, save the file and reload it in the web browser, and you should see something in the console looking similar to this: The green numbers are the idx values, starting at zero because… well, there are good reasons, but this is not the place to go into them. Suffice to say that most programming languages use zero-based indexing for things like arrays, and JavaScript is no exception. Following each green number, you can see an XML representation of each SVG element in the array – the four <inkscape:page> nodes we’re interested in. Each node also has a whole load of other baggage attached to it in the JS world, and you can see much of that by expanding the small triangle next to each one. In practice, we don’t need any of that for our task, so feel free to leave that triangle unexpanded, or to collapse it back down again if curiosity does get the better of you.
Une fois ce code en place, enregistrez le fichier et rechargez-le dans le navigateur Web. Vous devriez voir quelque chose dans la console qui ressemble à ceci :
Les chiffres verts sont les valeurs idx, qui commencent à zéro parce que… eh bien, il y a de bonnes raisons, mais ce n'est pas l'endroit pour en parler. Il suffit de dire que la plupart des langages de programmation utilisent l'indexation basée sur zéro pour des choses comme les tableaux, et Javascript ne fait pas exception.
Après chaque chiffre vert, vous pouvez voir une représentation XML de chaque élément SVG dans le tableau - les quatre nœuds <inkscape:page> qui nous intéressent. Chaque nœud est également associé à tout un tas d'autres choses dans le monde JS et vous pouvez en voir une grande partie en agrandissant le petit triangle à côté de chacun d'entre eux. En pratique, nous n'avons besoin de rien de tout cela pour notre tâche, alors n'hésitez pas à laisser ce triangle sans l'agrandir, ou à le réduire à nouveau si la curiosité prend le dessus.
So far, our code hasn’t really achieved very much – just printing the index, and the same nodes we can see in Inkscape’s XML editor. But now that we’ve got a way to grab a handle to each <page> element, we can start to pull them apart to get to the individual details we’ll need. We’re going to want to extract the x, y, width, and height values. These are stored as ‘attributes’ on the element, and can be retrieved using the getAttribute() method. Let’s add four lines after the console.log() to retrieve these values, and assign each to a JS variable (x, y, w, h). const x = page.getAttribute('x'); const y = page.getAttribute('y'); const w = page.getAttribute('width'); const h = page.getAttribute('height'); We could log them out at this point, but ultimately we’re going to want these formatted into a space-separated string for use in a viewBox attribute. One additional line of code will do this for us, and then we’ll log the result: const viewBox = `${x} ${y} ${w} ${h}`; console.log(viewBox);
Jusqu'ici, notre code n'a pas accompli grand-chose - juste imprimer l'index et les mêmes nœuds que nous pouvons voir dans l'éditeur XML d'Inkscape. Mais maintenant que nous avons un moyen d'accéder à chaque élément <page>, nous pouvons commencer à les séparer pour obtenir les détails individuels dont nous avons besoin. Nous allons vouloir extraire les valeurs x, y, width et height (largeur et hauteur). Ces valeurs sont stockées en tant qu'« attributs » de l'élément et peuvent être récupérées à l'aide de la méthode getAttribute(). Ajoutons quatre lignes après console.log() pour récupérer ces valeurs et les assigner à une variable JS (x, y, w, h).
const x = page.getAttribute('x') ;
const y = page.getAttribute('y') ;
const w = page.getAttribute('width') ;
const h = page.getAttribute('height') ;
Nous pourrions les déconnecter à ce stade, mais en fin de compte, nous allons vouloir les formater en une chaîne de caractères séparée par des espaces pour les utiliser dans un attribut viewBox. Une ligne de code supplémentaire le fera pour nous, puis nous enregistrerons le résultat :
const viewBox = `${x} ${y} ${w} ${h}` ;
console.log(viewBox) ;
Be very careful! The ‘const viewBox’ line uses a feature of JS called ‘template strings’. These are delimited by backticks (`) rather than normal quotes or apostrophes. Using this method lets us put our variables directly into the string using the ${} notation, and the JS engine will swap them out for the variables’ values when the code runs. If your log ends up containing the actual ${} string, then you’ve used the wrong type of quotes, and will need to search your keyboard again for the easily-overlooked backtick character. All those previous 6 lines should have been added after the existing console.log() and before the closing brace. If you’ve done it correctly, saving the file and reloading it in the browser should show something like this – similar to the previous output, but with the viewBox values displayed after each XML node (see image below).
Soyez très prudent ! La ligne « const viewBox » utilise une caractéristique de JS appelée « template strings ». Celles-ci sont délimitées par des backticks (`) plutôt que par des guillemets normaux ou des apostrophes. L'utilisation de cette méthode nous permet de placer nos variables directement dans la chaîne en utilisant la notation ${}, et le moteur JS les remplacera par les valeurs des variables lors de l'exécution du code. Si votre journal contient la chaîne ${}, c'est que vous n'avez pas utilisé le bon type de guillemets et que vous devrez à nouveau chercher sur votre clavier le caractère backtick, facilement oubliable.
Les 6 lignes précédentes auraient dû être ajoutées après le console.log() existant et avant l'accolade fermante. Si vous l'avez fait correctement, l'enregistrement du fichier et son rechargement dans le navigateur devraient donner quelque chose comme ceci - semblable à la sortie précédente, mais avec les valeurs viewBox affichées après chaque nœud XML (voir l'image ci-dessous).
Now we’ve got all the details we need, it’s time to create a new <view> element for each page. For this, we need to use the document.createElementNS() function – the NS on the end referring to the fact that this lets us specify a namespace for our new element. This is where our earlier assignment of the svgNS variable will be used, ensuring that we end up with what is effectively an <svg:view> element, rather than an <inkscape:view> element, or anything else. Each <view> element will also require two attributes. One is the ‘viewBox’ for which we’ve already prepared the value. The other is an ‘id’ attribute which will define the string we have to append to our URL to view this page.
Maintenant que nous avons tous les détails dont nous avons besoin, il est temps de créer un nouvel élément <view> pour chaque page. Pour ce faire, nous devons utiliser la fonction document.createElementNS() - le NS à la fin faisant référence au fait que cette fonction nous permet de spécifier un espace de noms pour notre nouvel élément. C'est là que notre affectation précédente de la variable svgNS sera utilisée, garantissant que nous obtenons ce qui est effectivement un élément <svg:view>, plutôt qu'un élément <inkscape:view>, ou quoi que ce soit d'autre.
Chaque élément <view> aura également besoin de deux attributs. L'un est le « viewBox » pour lequel nous avons déjà préparé la valeur. L'autre est un attribut « id » qui définira la chaîne que nous devons ajouter à l'URL pour voir cette page.
For the sake of simplicity, we’re just going to name the pages ‘page-1’, ‘page-2’, and so on, using another JS template string in which we’ll also add 1 to the value to rid ourselves of those pesky zero-indexed numbers. Therefore, to create our new element, and set both attributes, we’ll need these three lines of code: const view = document.createElementNS(svgNS, 'view'); view.setAttribute('id', `page-${idx + 1}`); view.setAttribute('viewBox', viewBox); There’s just one thing left to do. Although we’ve created our new <view> element, it currently just lives as an object in the JS world, and needs to be inserted into the browser’s internal model of the document. We’ll insert each <view> as a child of the corresponding <inkscape:page> element in order to keep things neatly together. This last line will do the job: page.appendChild(view); With that, the final code should look like this – albeit that you can’t actually see all the lines at once in Inkscape’s unfortunately inflexible editor field (top right).
Par souci de simplicité, nous allons simplement nommer les pages « page-1 », « page-2 », et ainsi de suite, en utilisant une autre chaîne de caractères JS dans laquelle nous ajouterons également 1 à la valeur pour nous débarrasser de ces nombres à indexation zéro. Par conséquent, pour créer notre nouvel élément et définir les deux attributs, nous aurons besoin de ces trois lignes de code :
const view = document.createElementNS(svgNS, 'view') ;
view.setAttribute('id', `page-${idx + 1}`) ;
view.setAttribute('viewBox', viewBox) ;
Il ne reste plus qu'une chose à faire. Bien que nous ayons créé notre nouvel élément <view>, il vit actuellement comme un objet dans le monde JS et doit être inséré dans le modèle interne du document du navigateur. Nous allons insérer chaque <view> en tant qu'enfant de l'élément <inkscape:page> correspondant afin de garder les choses bien en ordre. Cette dernière ligne fera l'affaire :
page.appendChild(view) ;
Avec cela, le code final devrait ressembler à ceci - bien que vous ne puissiez pas voir toutes les lignes en même temps dans le champ de l'éditeur malheureusement inflexible d'Inkscape (en haut à droite).
You can optionally remove the console.log() lines if you wish, as they’re purely there for educational and debugging purposes, and have no effect on the actual operation of the code. Now that the code is done, how do you actually use it? Simply load the SVG file directly into your browser, and append ‘#page-2’ to the end of the URL to view the second page. I’m sure you can work out the syntax for the other pages. Entering an invalid ID (e.g. ‘#page-22’) simply causes the browser to show the first page. There we have it: a small chunk of JS that you can add to any multi-page Inkscape file to make the additional pages available via a web browser. What more could you possibly want? Quite a bit as it happens. This code is good, but it suffers from a significant limitation: in order for the browser to execute it, the SVG file has to be loaded directly (or within an <object> tag). Most common ways of including SVG files in a web page – via an <img> tag or a CSS url() function – are deliberately prevented from executing JavaScript. This significantly limits the usefulness of this code – at least as it stands.
Vous pouvez éventuellement supprimer les lignes console.log() si vous le voulez, car elles ne sont là que dans un but éducatif et de mise au propre ; elles n'ont aucun effet dans le fonctionnement réel du code. Maintenant que le code est réalisé, comment vraiment l'utiliser ? Chargez simplement le fichier SVG directement dans le navigateur, et ajoutez « #page-2 » à la fin de l'URL pour voir la seconde page. Je suis sûr que vous pouvez trouver la syntaxe pour les autres pages. La saisie d'un ID invalide (par ex., ‘#page-22’) amène simplement le navigateur à afficher la première page.
Et voilà : un simple bout de code JS que nous pouvons ajouter à un fichier Inkscape mutipage pour rendre les pages additionnelles disponibles dans un navigateur Web. Que voulez-vous de plus ?
Beaucoup, en fait. Ce code est bon, mais il souffre d'une limitation importante : de façon à ce que le navigateur l'exécute, le fichier SVG doit être chargé directement (ou dans une balise <object>). Les façons les plus habituelles d'inclure des fichiers SVG dans une page Web – via une balise <img> ou une fonction CSS url() – sont expressément empêchées d'exécuter du Javascript. Ça limite grandement l'utilité de ce code - au moins tel qu'il est.
Another issue is that we’ve just given each page a rather generic ID. Within Inkscape, it’s possible to name each page – wouldn’t it be nicer if we could use those names when referring to each page, rather than just page-1 and page-2? Next month, I’ll extend this code a little further to address both these issues. It still won’t give the simple, seamless experience that we could have had if Inkscape natively created named views, but it’s better than being stuck with multi-page files that can display only the first page!
Un autre problème est que nous avons donné à chaque page un identifiant plutôt générique. Puisque, dans Inkscape, il est possible de nommer chaque page, ne serait-il pas plus agréable d'utiliser ces noms pour faire référence à chaque page, plutôt que de se contenter de page-1 et page-2 ?
Le mois prochain, j'étendrai ce code un peu plus loin pour résoudre ces deux problèmes. Cela ne donnera toujours pas l'expérience simple et transparente que nous aurions pu avoir si Inkscape avait créé nativement des vues nommées, mais c'est mieux que d'être coincé avec des fichiers multi-pages qui ne peuvent afficher que la première page !