![]()
École Nationale Supérieure
d'Informatique pour
l'Industrie et l'Entreprise
1 square de la résistance,
91025 Evry Cedex

INRIA Grenoble - Rhône-Alpes
Inovallée
655 avenue de l'Europe
Montbonnot
38 334 Saint Ismier Cedex France
Stagiaire : Frédéric WANG
Lieu du stage : INRIA Grenoble - Rhône-Alpes
Responsable du stage : Irène Vatton
Année 2008
Dans ce stage, j'ai travaillé sur l'éditeur de documents Web Amaya. L'objectif était de mettre place des capacités d'édition d'images vectorielles SVG en ne cherchant pas les fonctionnalités sophistiquées des logiciels de création artistique (gradient, flou...) mais en se concentrant plutôt sur la réalisation de schémas simples. Les fonctionnalités que j'ai mises en oeuvre incluent l'édition de formes de base (carré, rectangle, triangle...) de tracés (polygone, courbes de Bézier...) et de style simple (couleur, épaisseur des traits, transparence...), la gestion de calques (grouper, monter/descendre), des transformations (rotation, déplacement, étirement...), des outils de positionnement automatique (alignement et distribution), le tracé de connecteurs (lignes et flèches), l'insertion de composants prédéfinis (formes 3D, symboles électriques, portes logiques, verreries de chimie...), l'insertion d'éléments non SVG (image, texte et objets étrangers) ainsi que la gestion des méta-données (titre, description).
L'INRIA, institut national de recherche en informatique et en automatique a pour vocation d'entreprendre des recherches fondamentales et appliquées dans les domaines des sciences et technologies de l'information et de la communication. Il accueille 3800 personnes réparties dans 8 centres de recherche et travaillant dans plus de 150 équipes-projets de recherche.
J'ai effectué mon stage à l'INRIA Grenoble - Rhône-Alpes dont les trois grandes priorités thématiques du centre sont les suivantes :
J'ai travaillé au sein de l'équipe de recherche WAM qui s'intéresse à la création et la transformation de documents Web multimédia et à leur adaptation. Elle est dirigée par Vincent Quint, qui a occupé différentes fonctions au W3C et participe depuis plusieurs années aux travaux sur les technologies Web. La dizaine de chercheurs et ingénieurs de WAM travaille autour de différents sujets : recherche sur le traitement des documents XML, adaptation à différents terminaux et développement de deux logiciels : LimSee3 (présentation multimédia) et Amaya (éditeur de documents structurés pour le Web). C'est sur ce dernier que j'ai développé des fonctionnalités d'édition SVG.
Irène Vatton, ex-membre du W3C et actuellement à la tête du projet Amaya, a été ma directrice de stage. L'équipe se compose de 3 autres développeurs : Vincent Quint, Laurent Carcone (ingénieur expert à ERCIM, membre du W3C) et Émilien Kia (Ingénieur associé). Amaya est actuellement partiellement financé par le projet européen PALETTE [13] qui vise à développer et mettre en oeuvre des outils pour les communautés de pratique. En particulier, Amaya est susceptible d'être utilisé par la communauté ePrep [8], constituée d'enseignants des CPGE. Cette communauté est intéressée par l'édition de formules mathématiques et de schémas.
Mes remerciements vont tout d'abord à Irène Vatton pour m'avoir permis de réaliser ce stage et accompagné lors de son déroulement. Son aide dans la correction des bogues et ses indications sur la manière d'implémenter les nouvelles fonctionnalités m'ont été très précieux. Je suis aussi reconnaisant envers l'assistante de l'équipe WAM et le personnel admistratif du CROUS pour avoir facilité mon intégration à l'INRIA et mon logement sur Grenoble. Je tiens à remercier tous les membres du projet WAM pour leur accueil et plus particulièrement les développeurs d'Amaya : Irène, Vincent, Laurent et Émilien. Leurs commentaires, conseils et assistance m'ont été très utiles pour réaliser mon travail tandis que le logiciel qu'ils développent a été un outil indispensable à la rédaction de mon rapport. Finalement je souhaite exprimer ma gratitude envers toute la communauté du logiciel libre pour mettre à disposition les logiciels que j'ai utilisé au cours de mon stage ou ceux dont j'ai repris des idées et icônes.
Depuis toujours, le Web se distingue des autres média en offrant à chaque utilisateur la possibilité d'être aussi un auteur. C'est ainsi que le premier navigateur intégrait des capacités d'édition de façon à ce que chacun puisse lire et publier des pages. Ces dernières années de nombreux systèmes de gestion de contenu sont apparus et ont connu un succès croissant : blog, wiki, forum... Bien qu'ils permettent de gérer facilement les données en ligne, ils deviennent rapidement limités lorsqu'il s'agit d'éditer les pages. En effet, pour réaliser des structures complexes comme des listes, tableaux, formules mathématiques ou croquis, l'auteur doit apprendre et utiliser une pseudo-syntaxe. Certes plus simple que le XML, elle n'en reste pas moins difficile pour les non programmeurs.
L'un des objectifs d'Amaya [2] est de répondre à ce besoin d'un outil d'édition. Il permet ainsi d'éditer des pages Web conformes aux recommandations du W3C parmi lesquels XHTML (structuration de la page en paragraphe, liste, tableau...), CSS (information de mise en page) et MathML (formules mathématiques) tout en encourageant la production de contenu accessible [4]. Le travail réalisé au cours de mon stage ajoute des possibilités d'édition SVG (graphique vectoriel) pour la réalisation de schémas. Il viendra, je l'espère, s'ajouter aux progrès réalisés dans le cadre du projet PALETTE pour que l'édition Web soient réalisables par des utilisateurs novices.
Dans la suite de l'introduction, je présenterai brièvement les caractéristiques d'Amaya et du langage SVG. Une seconde partie présentera l'environnement dans lequel j'ai travaillé, l'état de l'art et mon plan de travail. Je développerai ensuite le travail que j'ai réalisé dans la création, la manipulation, l'édition et la transformation des objets, ainsi que l'ajout de style. La conclusion fera le bilan du travail effectué et de ce stage m'a apporté à titre personnel.
Amaya est un éditeur Web développé par l'INRIA et le W3C (organisme international de normalisation des formats Web). Il sert à la fois de démonstrateur des nouvelles technologies et d'outil pour réaliser facilement des pages respectant les recommandations du W3C.
Les fonctionnalités d'édition et de navigation sont intégrées de façon homogène dans un même environnement. L'utilisateur peut aussi bien créer une page entièrement nouvelle que en récupérer une sur le Web pour l'éditer. Il peut directement enregistrer ses pages sur un serveur s'il possède les droits d'écriture. Il peut charger simultanément plusieurs pages et facilement créer des liens entre elles, sur n'importe quel élément. Il peut attacher des commentaires au format XML à tout élément d'une page Web grace à son système d'annotation. Il peut directement envoyer par courriel les documents qu'il édite.
Il est possible d'ouvrir simultanément dans une même fenêtre plusieurs onglets. La fenêtre de l'application peut être divisée en plusieurs cadres pour pouvoir afficher en parallèle des documents. Un document peut être affiché et édité dans plusieurs vues synchronisées, parmi lesquelles la vue formatée (classique), la vue structure (arbre du document) et la vue source (code XML).
Sauf lorsque l'utilisateur effectue des modifications dans le code source et à l'inverse d'autres logiciels de création Web, l'édition d'Amaya se réalise à un niveau élevé : toutes les commandes agissent directement sur l'arbre XML du document. Ainsi, l'édition se caractérise à la fois par une interface Wysiwyg qui offre à chaque instant le rendu du document et une manipulation structurelle de l'arbre. Ce dernier est constamment maintenu valide vis-à-vis de doctypes connus.
On retrouve dans Amaya toutes les commandes classiques des éditeurs Wysiwyg (copier-coller, annuler/refaire, rechercher, zoom, panneau d'insertion éléments et de style, vérifieur orthographique, création de liens...) auxquelles s'ajoutent des fonctionnalités avancées commme la numérotation des sections ou la construction d'un livre à partir de plusieurs pages liées. Un système innovant de modèles sera bientôt fonctionnel.
Amaya propose aussi des manipulations structurelles pour les utilisateurs avancés : transformations de fragments d'arbres (par exemple conversion d'une suite de paragraphes en une liste d'items ou en un paragraphe unique), sélection directe des ancêtres du noeud courant affichés dans la barre inférieur, panneau d'édition des attributs XML ou des classes CSS de l'élément sélectionné...

Amaya permet enfin d'éditer facilement des documents avec des tableaux, listes, images, formulaires, formules mathématiques et dans la prochaine version, des schémas. Un exemple de tel document est donné sur la capture d'écran ci-dessus. L'édition du SVG mélangé avec plusieurs langages avait été expérimenté déjà dans le passé [7] et l'objectif de mon stage était de réaliser cette implémentation.
SVG (Scalable Vector Graphics) est un langage XML et une recommandation du W3C conçu pour réaliser des images vectorielles [14]. Le principe est de représenter l'image non pas par la couleur de chaque pixel mais par une description de chaque composant en utilisant des caractérisations géométriques, des changements de repères ou encore des informations de style. Les principaux avantages de SVG sont :
A titre d'exemple, le fragment XML suivant décrit un rectangle à coin arrondi :
<svg xmlns="http://www.w3.org/2000/svg" width="350" height="250">
<title>Un rectangle jaune à coins arrondis</title>
<rect width="300" height="150px" style="stroke: black; fill: yellow"
transform="rotate(-25,290,65) " rx="80px" ry="30px"/>
</svg>
L'élément <svg/> représente la zone de tracé qui est
un rectangle de dimension 350×250. Un fils <title/> lui
associe un titre. Un second fils <rect/> décrit le tracé
d'un rectangle de dimension 300×150. L'attribut style indique que
les bords du rectangle sont noirs et qu'il est rempli en jaune. L'attribut
transform indique que le rectangle subit une rotation de centre
(290,65) de -25°. Enfin les attributs rx et ry
précisent les rayons des coins arrondis :
Je développais sous Linux et avais à ma disposition un PC avec Windows XP et un portable Mac. J'ai aussi utilisé le dépôt CVS du W3C où est stocké la version de développement d'Amaya, en m'aidant des programmes cvs et tkcvs. Je codais avec emacs et utilisais les outils de développement GNU (gdb, gcc...). Le panneau SVG a été effectué à l'aide de l'éditeur de ressources XRCed. Les icônes ont été réalisées avec GIMP en reprenant éventuellement celles existant dans Open Office ou Inkscape. Ce dernier m'a été utile pour les conversions entre les formats SVG/PNG. Les "composants prédéfinis" (3.1.6) ont été créés à l'aide d'Amaya. Enfin, de nombreuses formules mathématiques ont été vérifiées à l'aide de Maxima [11].
Amaya est basé sur la librairie C "Thot", qui offre tout un ensemble de commandes permettant de gérer des documents structurés. Actuellement une partie de l'interface utilisateur est écrite à l'aide de la librairie C++ wxWidgets. Les graphiques SVG sont rendus grace à la librairie OpenGL. J'ai appris à travailler avec ces différentes librairies. Amaya est un logiciel fonctionnant sous Linux, MacOSX et Windows et est traduit dans une quinzaine de langues. J'ai donc du tester mon éditeur SVG sur les différentes plate-formes et réaliser les nouveaux messages à la fois en anglais et en français.
Open Office [12] comporte un module de dessin qui utilise en interne une syntaxe XML reprenant des attributs SVG. Il possède les caractéristiques suivantes :
Étant utilisé par des utilisateurs très variés, il est intéressant d'étudier ce logiciel pour avoir une interface non réservée à des spécialistes. Par contre les documents qu'il produit sont davantage destinés à l'impression qu'à la publication Web (l'édition se réalise entièrement sur une page blanche). De plus il utilise son propre format pour proposer des méthodes d'édition. Celles-ci ne sont donc pas forcément applicables au SVG.
Inkscape [10] est un éditeur d'image vectorielle SVG. Il possède les fonctionnalités d'Open Office citées précédemment, sauf pour l'insertion de constructions prédéfinies. Les formes de bases sont celles de la spécification SVG avec quelques structures supplémentaires (étoile, spirale...). Le logiciel montre des menus plus proches du SVG (par exemple le style ou les marqueurs) bien qu'il utilise aussi une extension XML pour des informations d'édition en plus. Il est par contre plus orienté "dessin d'art" et n'est pas vraiment adapté à l'édition de schémas.
Dia [6] est un logiciel spécialement conçu pour l'édition de diagrammes. Il permet l'insertion d'un grand nombre de composants prédéfinis (porte logique, circuit électrique...) et dispose d'une très bonne interface pour les "points de connexion". Il n'y a pas de tracé de segments/courbes mais seulement une insertion de formes que l'utilisateur peut ensuite éditer (augmenter/supprimer/déplacer les points). Il est possible d'insérer du texte et des images. Comme Open Office, il utilise son propre format. Il peut exporter en SVG mais le dessin ne peut alors plus être édité par le logiciel.
Cette section donne un aperçu des fonctionnalités du langage SVG [14], en rapport avec la création de schémas/graphiques, qu'il serait intéressant d'intégrer à Amaya. Elles sont données dans l'ordre de priorité qui a été fixé pendant les réunions avec l'équipe.
Une image SVG possède une racine <svg/> qui décrit la
zone de tracé. Ses fils sont les composants graphiques, chacun étant dessiné
sur ses prédécesseurs. Les fils peuvent être des éléments de base
(rectangle, polygone...) ou un élément "groupe" (<g/>)
dont les fils sont eux-même d'autres composants graphiques. Chaque élément
peut comporter des éléments <title/> et
<desc/> qui permettent de rendre accessible les contenus
graphiques en lui associant respectivement un titre et une description.
A titre d'exemple, considérons le cylindre ci-dessous, dessiné avec deux ellipses (représentant les bases) et d'un chemin (partie avant de la surface latérale) :
L'utilisateur doit à la fois être capable de sélectionner chaque composant individuellement (par exemple pour les mettre en couleur) ou de sélectionner en groupe (par exemple pour appliquer des transformations sur le cylindre). Il doit aussi pouvoir être capable de choisir l'ordre des éléments (par exemple la base du dessous, vue par transparence est derrière la partie avant). Enfin l'utilisateur doit pouvoir éditer les descriptions associées aux éléments.
Le langage SVG dispose de deux types d'éléments pour décrire les tracés : les figures élémentaires et les chemins. Pour les premiers, les éléments disponibles sont:
<rect/> qui décrit un rectangle par la
coordonnée de son coin inférieur gauche et ses dimensions.<circle/> et
<ellipse/> qui décrivent des cercles et ellipses par la
coordonnée de leurs centres et de leurs rayons/demi-axes.<line/> qui décrit un segment par les
coordonnées de ses extrémités.<polyline/> et
<polygon/> qui décrivent une suite de segments par une
liste de points. Le second élément permet de représenter des figures
fermées.Les chemins sont décrits par un éléments <path/>. Si
on imagine le tracé d'un chemin avec un crayon, un sous-chemin correspond à
un tracé effectué sans lever le crayon. Les principales opérations sont :
M : ouverture d'un sous-chemin.L : un tracé de ligneC et Q : un tracé de courbe de Bézier cubique
ou quadratique.A : un arc d'ellipsez : fermeture du sous-chemin courant par une ligne
droite.On rappelle qu'une courbe de Bézier de degré décrites par les points de controle est paramètrée par de la façon suivante [5] :
En pratique, on s'intéresse surtout au cas , une courbe de Bézier quadratique pouvant être décrite de façon cubique [5]. Le point est le point de départ et le point le point d'arrivée de la courbe. Les points et donnent les tangentes de la courbe au point de départ et d'arrivée. La courbe est dans le polygone formé par les 4 points de contrôle.
Dans les éditeurs d'images, ces points servent de "poignées" pour que l'utilisateur puisse changer la forme de la courbe. Lorsque plusieurs fragments de courbe de Bézier sont enchainées dans les chemins, il est d'usage de rajouter des contraintes sur les deux demi-tangentes associées à un point. Par exemple, en imposant l'alignement des poignées, on obtient une courbe sans point anguleux :
En résumé, les possibilités attendues pour l'éditeur SVG sont :
Outre le tracé de formes géométriques, le langage SVG permet d'inclure divers objets particuliers :
<image>) ou de nouveaux canevas
(<svg/>).<foreignObject>). Cet
élément autorise potentiellement l'inclusion de nouveaux langages XML à
l'intérieur même du SVG. Cette fonctionnalité est particulièrement
utile pour l'inclusion de formules mathématiques MathML.Les fonctionnalités intéressantes pour ces objets sont :
Le SVG offre la possibilité d'effectuer des transformations sur les objets
par le biais de changements de repère. Ceux-ci sont décrits par un attribut
transform pouvant être attaché à la plupart des éléments et
notamment des groupes. Si sont les coordonnées dans le système de coordonnées courant et
ceux dans le nouveau système de coordonnées, alors la spécification
donne pour relation :
Généralement, on écrit plutôt une telle transformation affine de la façon suivante, pour qu'apparaisse clairement la composante linéaire et le changement d'origine :
La valeur de l'attribut transform correspondant est
matrix(a, b, c, d, e, f). Une telle notation permet de décrire et
manipuler uniformément les transformations affines, ce qui est utile pour les
opérations internes du programme. Bien que certains éditeurs comme Inkscape
propose un menu d'édition des coefficients, il est souvent plus commode pour
l'utilisateur de se référer à des transformations simples. La spécification
SVG fournit ainsi d'autres valeurs pour l'attribut transform,
récapitulée ci-dessous.
| Nom de la transformation | Valeur de l'attribut transform |
Matrice de l'application linéaire |
|---|---|---|
| Translation | translate(tx, ty) |
/ |
| Étirement | scale(sx, sy) |
|
| Rotation | rotate(θ) |
|
| Inclinaison le long de l'axe des X | skewX(θ) |
|
| Inclinaison le long de l'axe des Y | skewY(θ) |
Notons que ces valeurs de l'attribut peuvent être concaténées pour
composer les transformations (avec l'ordre de lecture usuel). L'attribut
rotate permet aussi de spécifier un centre de rotation (cx, cy) :
ainsi "rotate(θ, cx, cy)" est équivalent à
"translate(cx, cy) rotate(θ) translate(-cx, -cy)".
Les cinq transformations du tableau sont beaucoup plus claires pour l'utilisateur et se réalisent assez naturellement avec la souris. Par exemple pour un déplacement on clique sur la figure et on la fait glisser à la nouvelle position, pour un étirement on clique sur un bord et on tire dessus etc. Pour les transformations, les fonctionnalités possibles sont :
Les styles SVG concernent le tracé (couleur, épaisseur, opacité, pointillé, type de jointures/extrémités) et le remplissage (couleur, opacité et règle de remplissage) des éléments. Deux méthodes sont possibles pour styler les graphiques :
style.Idéalement, il faudrait être capable de gérer les deux représentations et de pouvoir passer de l'une à l'autre.
Les marqueurs répondent au besoin d'avoir des symboles spéciaux pour les tracés, par exemple pour représenter des flèches :
L'utilisation typique se fait en définissant des marqueurs dans un
élément <defs>, dessinés comme tout autre élément SVG,
que l'on peut ensuite réutiliser comme jointures/extrémités des courbes :
<svg [...]>
<defs>
<marker id="ID_MARQUEUR1" [...]>
[Tracé du marqueur]
</marker>
[...]
</defs>
<path d="[Tracé du chemin]"
marker-start="url(#ID_MARQUEUR1)"
marker-mid="url(#ID_MARQUEUR2)"
marker-end="url(#ID_MARQUEUR3)" />
</svg>
Pour l'éditeur, il faudrait présenter un ensemble de symboles prédéfinis à l'utilisateur ainsi que lui permettre de créer et utiliser ses propres marqueurs. L'utilisation des marqueurs combinées à des courbes/segments de droite permet d'envisager le tracé "des connecteurs" comme on les trouve dans Dia. Toutefois le SVG ne permet pas d'exprimer des contraintes de connexion.
La zone de dessin est donnée par un élément <svg/> qui
peut être l'élément racine du document SVG : on retrouve le cas des
éditeurs "classiques" à savoir un document dont tout l'espace est réservé
au tracé. Néanmoins, Amaya permet aussi de mélanger plusieurs grammaires
XML. Il est possible que le canevas SVG soit par exemple intégré dans une
page XHTML ou soit lui même intégré dans un autre dessin SVG. Les boites
peuvent aussi être positionnées à l'aide de style CSS. Il existe ainsi
différents repères imbriqués et il faut être capable de passer du repère
de la souris au repère local en composant les différentes transformations.
Dans la suite on escamotte ces difficultés en raisonnant dans le repère local
mais il faut garder à l'esprit que des fonctions réalisent ces changements de
repères.
Pour simplifier, toutes les opérations effectuées en vue formatée
(sélection, édition...) ne seront possibles que sur les fils directs
du <svg/>. On peut toutefois adapter le code si on souhaite
un mécanisme d'entrer/sortir du groupe tel qu'indiqué dans la section 3.2.1. Il faut alors modifier les matrices de transformation
passées aux différents modules.
La capure ci-dessus montre un exemple typique d'édition dans Amaya. En haut, dans la vue formatée, on voit une image SVG (le logo d'Amaya) parmi du texte XHTML et positionné à l'aide de style CSS. En bas, dans la vue structure, on voit les éléments SVG correspondants : ellipse, groupe, polygone... Les deux vues sont synchronisées et montrent la sélection d'un groupe (en rouge dans la vue formattée, en bleu dans la vue structure). La sélection a été faite dans la vue formatée : comme indiqué plus haut, l'objet sélectionné est un fils direct du SVG. On peut directement éditer l'élément sélectionné dans la page.
Le premier bouton de la palette SVG regroupe des commandes spéciales. Deux d'entre elles servent à la gestion des méta-données et seront expliquées dans la section 3.3.1. Les autres commandes sont :
La création d'un canevas SVG insère un nouvel élément
<svg/> dans le document. Si la sélection est déjà dans un
canevas, l'utilisateur dessine un cadre pour délimiter la nouvelle zone de
dessin à l'intérieur de l'ancienne. Cette fonctionnalité permet d'emboiter
des éléments <svg/> comme l'autorise la spécification
(chapitre 5 de [14]) mais n'est pas vraiment utile pour le
problème qui nous concerne. Un comportement plus intéressant est le cas où
la sélection est à l'intérieur d'un autre langage XML. Actuellement, seul le
Working Draft "An XHTML + MathML + SVG Profile" [3]
précise les règles pour mélanger XHTML, MathML et SVG. Une DTD lui est
associée et est intégrée dans le validateur du W3C [15]. La note "Guidelines for Graphics in MathML 2" [9] présente aussi une réflexion sur le mélange de SVG avec
MathML. Actuellement la seule possibilité est l'insertion dans du XHTML. Dans
ce cas, le nouveau canevas est inséré avec une taille par défaut et
l'utilisateur peut ensuite le redimensionner à l'aide de la souris. Les
éléments appartenant à l'espace de nom SVG sont alors préfixés par
svg: de façon à produire des documents valides par rapport au
doctype XHTML+MathML+SVG.
Inversement, on peut insérer du XHTML/MathML dans du SVG via les objets
<switch/> et <foreignObject/>. Le premier
sert à décrire un affichage conditionnel : le navigateur parcourt les fils
jusqu'à tomber sur un élément qu' il est capable d'afficher. Le second
autorise l'insertion d'"élément étranger" et possède des attributs
précisant les capacités requises par le navigateur. Ses dimensions sont
obligatoires. Ainsi dans l'exemple ci-dessous, un navigateur capable d'afficher
des formules mathématiques affichera le contenu MathML tandis que les autres
afficheront l'élément <text/>.
<svg [...]>
<switch>
<foreignObject width="100" height="50"
requiredExtensions="[...]" [...]>
<math [...]>
<!-- Contenu MathML -->
[...]
</math>
</foreignObject>
<text>
<!-- Text alternatif -->
[...]
</text>
</switch>
</svg>
Lorsque l'utilisateur effectue la commande "Création d'un objet étranger"
il doit cliquer à la position d'insertion souhaitée puis un groupe contenant
la structure précédente est créé. Un <div/> est inséré
dans le <foreignObject/> et l'utilisateur peut éditer du XHTML. Ce
<foreignObject/> possède une dimension par défaut qui n'est pour
l'instant modifiable qu'en allant éditer les attributs correspondants. Si
l'utilisateur tente d'insérer du MathML alors que la sélection est dans du
SVG, la même opération se produit sauf que c'est un élément
<math/> que l'on insère. Le texte alternatif par défaut
est "embedded XHTML not supported" ou "embedded MathML not
supported" respectivement. Il y a actuellement un petit problème de
compatibilité puisque les valeurs du requiredExtensions à
utiliser ne sont pas explicitement indiquées dans les spécifications. Voir
l'annexe 5.2.2.
Les commandes de création de texte et d'image génèrent respectivement les
éléments SVG <text/> et <image/>. Pour
le premier, l'utilisateur effectue un clic à la position où il souhaite
insérer un élément textuel. Un mécanisme d'édition du texte existait
déjà et prend en compte la gestion de plusieurs lignes. L'appuie sur la
touche Entrer créé ainsi un élément SVG qui indique une nouvelle ligne.
Pour le second, une boite de dialogue similaire à celle d'insertion des images
XHTML (élément <img/>) s'ouvre pour que l'utilisateur
puisse aller chercher le fichier correspondant. Cette boite comporte un champs
"texte alternatif" qui génère un élément <desc/> dans
l'image.

La création de ces objets se réalise en indiquant deux points. L'utilisateur clique une fois pour désigner un point A et maintient le bouton de la souris appuyé, déplace le curseur de la souris en un deuxième point B puis relache le bouton. Un "fantôme" de la figure est affiché à l'écran pendant que l'utilisateur maintient le bouton appuyé. Pour les segments ou flèche, le tracé va du point A au point B et peut avoir une précision limitée si on maintient le bouton shift appuyé (voir section 3.1.5). Dans les autres cas, le tracé de la figure se fait en déterminant le rectangle encadrant : les points A et B donnent les coins supérieur gauche et inférieur droit. Les constructions que j'ai réalisées sont :
Dans cette section, on s'intéresse à la réalisation de formes plus générales. Pour cela, il est nécessaire d'avoir un sous-programme qui regarde les commandes effectuées à la souris et en déduit les tracés. Plutôt que de proposer toutes les fonctionnalités d'édition de chemins en même temps (courbes de bézier et poignées, arc, lignes...) on se restreint à quatre constructions :
<polyline/>) : une succession de
points reliés entre eux.On se fixe pour contrainte une interface de tracé simple et rapide. Il est en effet fort probable que ces tracés ne soient pas définitifs : l'idée est donc que l'utilisateur dessine une première approximation des contours de sa figure quitte à les raffiner ultérieument en éditant les points. Les commandes de bases sont :
Pour les segments de droite et polygones, l'interface n'a pas besoin d'explication supplémentaire. Par contre pour les courbes de Bézier, les points de contrôle se scindent en deux catégories : les points sur la courbe et les "poignées". A nouveau j'ai choisi une interface simple, inspirée de celle d'Inskape, où l'utilisateur rentre le moins de points possible. Sur les schémas ci-dessous, le curseur de la souris courant est dessiné en rouge et la partie en cours d'édition en bleu. La succession des différentes étapes après chaque clic de souris est la suivante :
Rien n'est tracé. On attend que l'utilisateur indique le premier point de la courbe.
Une droite est tracée entre le premier point et le curseur de la souris.
Deux points de la courbes sont fixés. Une courbe de Bézier quadratique est tracée entre le premier point et le second. Sa poignée est le symétrique du curseur de la souris par rapport au second point.
Deux points de la courbes et deux "poignées" sont fixés. Une courbe de Bézier quadratique est définivement tracée. Une courbe de Bézier cubique est tracée entre le second point et le curseur de la souris. Sa première poignée est donnée par le dernier clic. Sa seconde poignée est confondue avec le point d'arrivée (curseur de la souris).
Trois points de la courbe et deux poignées sont fixés. Une courbe de Bézier quadratique est définitivement tracée. Une courbe de Bézier cubique est tracée entre le second et troisième point. Ses poignées sont la dernière poignée tracée et le symétique du curseur de la souris par rapport au troisième point.
Après l'étape 5 on reboucle à l'étape 4, ce qui permet de tracer un nouveau fragment de Bézier cubique en deux clics. On continue jusqu'à ce que l'utilisateur indique la fin de l'interaction. L'utilisation des symétriques du curseur de la souris pour indiquer les tangentes permet à la fois à l'utilisateur de rentrer moins de points et d'obtenir une courbe sans point anguleux.
Dans le cas des courbes de Bézier fermées, on fait en sorte que la deuxième poignée du dernier fragment soit le symétrique de la toute première poignée de façon à éviter un point anguleux.
Lors du tracé de segment de droites, l'utilisateur peut souhaiter réaliser des angles précis. Par exemple sous OpenOffice, les droites sont tracées avec une précision de 45° si l'utilisateur maintient la touche "Shift" appuyée. J'ai repris le même mécanisme, avec une précision d'angle . Le problème est décrit par la figure suivante : l'utilisateur a positionnée une extrémitée en et place l'autre extrémitée en . On cherche à approximer l'angle suivant :
ApproximationLigne (T, x1, y1, *x2, *y2)
<< Complexité en O(1) >>
x := *x2 - x1
y := *y2 - y1
r := racine(x^2 + y^2)
Si r > 0
θ := signe(y)*arccos(x/r)
θ := Arrondir(θ/T)*T
*x2 := x1 + r*cos(θ)
*y2 := y1 + r*sin(θ)
FinSi
FinApproximationLigne
Initialement, j'avais approximé les distances verticales/horizontales entre
les deux points à l'instar d'Open Office. L'approximation de l'angle donne un
meilleur résultat (elle conserve la longueur du segment), s'applique
facilement à une valeur d'approximation quelconque (pas uniquement 45°) et
s'écrit avec un code plus léger (évite d'utiliser plusieurs conditions
if ... else ...).
Amaya possèdait autrefois une librairie graphique qui permettait de sauvegarder des images SVG de façon à pouvoir les réutiliser dans ses documents [1]. Après le passage à wxWidgets, il ne restait toutefois plus que la possibilité de copier/coller des images prédéfinies. J'ai choisi de remplacer cette librairie par un mécanisme plus rapide d'insertion. La palette SVG comporte donc un ensemble de boutons qui déclenche le chargement et l'insertion de composants prédéfinis dans le document. J'ai réalisé plusieurs composants en m'inspirant de ceux présents dans Open Office et Dia ou qui peuvent être utiles pour e-Prep. De nouveaux composants pourront être ajoutés, la liste actuelle comporte :

Ces composants sont dessinés en SVG et sont rangés dans le répertoire
resources/svg/ d'Amaya. Les images sont insérées comme des
groupes dans le document et peuvent donc subir plusieurs transformations (cf
section 3.4) : redimensionnement, rotation déplacement
etc. Ainsi, à l'inverse d'Open Office, on utilise pas d'images redondantes
(par exemple les flèches dans plusieurs directions) lorsqu'elles peuvent
s'obtenir rapidement par symétrie et/ou rotation de 90°. Enfin, les images
possèdent un élément <title/> qui leur associe un titre
et facilite donc la génération des descriptions (cf section 3.3.1).
Amaya est un éditeur structuré et il est possible d'afficher et éditer l'arborescence du fichier SVG en affichant la "vue structure". Lorsque l'on effectue un parcours du premier fils au dernier fils sur un niveau on se déplace dans l'image SVG de l'arrière-plan vers le premier plan. Lorsque l'on effectue un parcours en profondeur de l'arbre, on rentre dans des groupements d'objet. La vue structure permet donc de sélectionner les objets en contournant les contraintes liées aux niveaux (un objet peut être caché par un autre) et au groupe (quand on clique sur un objet d'un groupe, veut-on sélectionner l'objet ou le groupe en entier ?).
On aimerait maintenant pouvoir réaliser des parcours dans la vue formatée. Le choix qui a été fait est le suivant :
Généralement, la création d'un élément dans Amaya se fait au point
d'insertion courant. Cependant dans une image SVG, on s'attend à ce que le
tracé de nouvelles figures se fasse au dessus du dessin courant. Par
conséquent, les éléments créés dans la vue formatée sont d'abord
placés comme dernier fils du canevas SVG et l'utilisateur doit ensuite
utiliser les commandes monter/descendre pour le positionner correctement.
On peut toutefois, sans quitter la vue formatée, positionner directement
un élément en utilisant les commandes "insérer" et "ajouter" qui créer
respectivement avant et après le point d'insertion un élément
GraphicsElement. Ensuite, si on dessine un nouvel élément
SVG et que le point d'insertion est sur un tel
GraphicsElement, alors celui-ci est remplacé par le nouvel
élément.
Par défaut, lorsque l'on clique sur un élément, la sélection remonte
à l'ancêtre fils direct du canevas SVG qui sont donc les seuls
éditables. Si on désire descendre plus en profondeur, il faut utiliser
les commandes grouper/dégrouper des objets. L'implémentation actuelle du
parcours en profondeur pose toutefois quelques problèmes. Ainsi seul les
attributs transform sont conservés lors du dégroupage et ils
ne sont pas factorisés lors du regroupage. L'utilisation d'une commande
entrer/sortir du groupement serait donc préférable, mais elle n'a pas
été mise en place.
L'utilisateur peut appliquer des opérations sur un ou plusieurs calques
simultanément, fils directs du <svg/>. Pour sélectionner
les calques à traiter, il existe deux méthodes :
La manière dont est affichée la sélection varie selon l'élément. Pour
des <polyline/>, <polygon/> et
<path/>, on affiche généralement les points de controle de
la courbe, qui peuvent être déplacés. Lorsqu'il s'agit de figures
élémentaires, des poignées spécifiques sont affichées pour permettre
l'édition (changement de la taille, de la forme...). Enfin pour les groupes,
le cadre rouge de la boite englobante est affiché. De cette façon, on peut
par exemple distinguer une ellipse dans le canevas (elle a des poignées
d'édition) ou dans un groupe (sa boite englobante est affichée).
Les calques peuvent être groupés/dégroupés. Du point de vue de l'arbre,
cela consiste à créer un élément groupe <g/> et à y
rentrer tous les éléments sélectionnés ou inversement prendre tous les
<g/> sélectionnés, sortir les fils et à supprimer les
<g/> vides. Pour ces opérations, il faut prendre garde à
ne pas changer les niveaux des calques. Pour le dégroupage, il faut donc
dégrouper les <g/> dans l'ordre dans lequel ils sont
situés et, pour chacun, conserver l'ordre des fils. Pour le groupage, ce n'est
pas toujours possible. En effet, même si on conserve aussi l'ordre des calques
sélectionnés, il se peut qu'il existe un fils du <svg/>
non sélectionné entre deux autres sélectionnés. On choisi de placer le
nouveau <g/> comme successeur du fils sélectionné de
niveau le plus haut. Cela est illustré dans groupement du carré bleu et du
parallélogramme rose de la figure ci-dessous (les nombres indiquent les
niveaux des calques) :
On considère maintenant les opérations pour monter/descendre un calque. Deux types d'opérations sont disponibles :
Notons que pour le deuxième cas on interprète le déplacement comme une permutation de fils, ce qui ne correspond pas toujours à une "vraie" montée/descente si les éléments ne se chevauchent pas. On pourrait toutefois utiliser les boites englobantes pour déterminer si un objet est "réellement" devant un autre (cf section 3.3.1). Ci-dessous, on souhaite mettre le rectangle vert au dessus du triangle rose, on doit procéder en deux étapes :
On va s'intéresser à la montée de calques, la descente s'obtenant par
symétrie. Pour réaliser ces opérations, les fonctions disponibles dans Thot
permettent de couper un élément et de le placer comme premier
fils/successeur/prédécesseur d'un autre. On considère nombre
objets sélectionnés objet[0], ... objet[nombre - 1]
qui sont fils de canevas_svg.
Pour la fonction montant tous les objets, comme on n'a pas de fonction
d'insertion à la fin et pour accélérer les traitements on utilise un
pointeur sur le dernier objet (il y en a au moins un, ou sinon l'algorithme ne
serait pas appelé) qui sert de référence. On coupe et colle alors chaque
objet sélectionné à la fin du canevas_svg en utilisant cette
référence.
MettreAuDessus(canevas_svg, nombre, objet[])
curseur := DernierFils(canevas_svg)
Pour i de 0 à nombre - 1
Si objet[i] ≠ curseur
MettreAprès(objet[i], curseur)
curseur := objet[i]
FinSi
FinPour
FinMettreAuDessus
En ce qui concerne la fonction montant chaque objet sélectionné d'un niveau, l'algorithme naïf consistant à parcourir les fils sélectionnés et à les faire passer devant son successeur ne fonctionne pas dans le cas où plusieurs objets sont sélectionnés, puisque ce successeur peut lui-même être un objet qui a été ou va être monté. Pour pallier ce problème, on parcourt les éléments à l'envers (de façon à ce que lorsque l'on bouge un objet, tous ses successeurs aient déjà été déplacés) et on vérifie que le successeur n'est pas le dernier objet déplacé (pour conserver l'ordre des objets sélectionnés).
MonterUnNiveau(canevas_svg, nombre, objet[])
curseur := Indéfini
Pour i de nombre - 1 à 0
suivant := Successeur(objet[i])
Si suivant ≠ curseur
MettreAprès(objet[i], suivant)
FinSi
curseur := objet[i]
FinPour
FinMonterUnNiveau
Les éléments SVG peuvent comporter deux éléments
<title/> et <desc/> qui permettent
respectivement de leur associer un titre et une description. Ces éléments
sont importants pour rendre les contenus graphiques accessibles, par exemple
par les personnes avec des déficits visuels ou celles utilisant des
matériels/logiciels ayant des capacités graphiques limitées. J'ai donc mis
en place quelques commandes permettant à l'auteur de générer des contenus
accessibles, comme conseillé dans [4].
Tout d'abord, pour chaque élément SVG, on peut utiliser une boite de dialogue qui permet d'éditer le titre et la description. De plus, les composants pré-définis possède un titre par défaut, traduit dans la langue de l'utilisateur.

Contrairement à une image matricielle où seule une description peut être fournie, l'image SVG offre une véritable structure à l'image comme on l'a vu dans la section 3.2.1 sur le positionnement des calques. Cela permet de savoir quel objet est devant un autre et de connaitre ceux qui sont "regroupés" en une entité. Une information intéressante mais plus difficile à obtenir est la répartition spatiale des objets. J'ai donc réalisé un algorithme simple qui génère une description d'un élément donné en prenant en compte les titres de ses fils :
On souhaite afficher la description d'un élément en considérant les fils
comportant un titre ( fils[i] ). i varie de 1 au nombre
d'éléments à décrire. La fonction obtenir distance donne la distance entre
les centres des boites englobantes de deux éléments.
GenererDescription (nombre)
<< complexité en O(nombre^2)
AfficherNombreÉléments(nombre)
Pour i de 1 à nombre
AfficherPositionAbsolu(fils[i])
Si i > 0
j_min := 0
distance_min := ObtenirDistance(fils[i], fils[0] )
Pour j de 1 à i - 1
r := ObtenirDistance(fils[i], fils[j])
Si r < distance_min
j_min := j
distance_min := r
FinSi
FinPour
AfficherPositionRelative (fils[i], fils[j_min])
FinSi
FinPour
FinGenererDescription
Il y a deux types de fonctions d'affichage de positions :
AfficherPositionAbsolu : On découpe largeur et hauteur de
la boite englobante de l'objet en trois, ce qui donne neuf zones : coin
supérieur gauche, haut, coin supérieur droit, gauche, centre, droite,
coin inférieur gauche, bas, coin inférieur droit. On indique alors la
position du centre de la boite englobante du fils.AfficherPositionRelative : On regarde les positions des
bords des boites englobante des deux fils verticalement et horizontalement.
Par exemple 1 est à droite de 2 si le bord gauche de 1 est à droite du
bord droit de 2. On obtient ainsi 3 valeurs verticales et 3 valeurs
horizontales qui conduisent à 9 positions : au dessus et à gauche, au
dessus, au dessus et à droite, à gauche, devant, à droite, en dessous et
à gauche, en dessous, en dessous et à droite. La valeur "devant"
correspond au cas où les fils se chevauchent. On s'arrange donc pour que
les arguments soient passés dans un ordre précis.On a dessiné le schéma suivant à partir des composants prédéfinis
d'Amaya et de formes élémentaires. Par défaut, les composants ont un
<title/> "Burette" et "Bécher". En utilisant la commande
"Édition des informations", on ajoute les titres "Barreau aimanté" et
"Agitateur magnétique" aux éléments SVG correspondants. Les formules des
composants chimiques sont écrites en MathML. Elles comportent un texte
alternatif (un avertissement) qui est affiché si le navigateur n'est pas
capable de gérer le MathML dans du SVG (cf section 3.1.2). De plus elles sont groupées avec les éléments
<text/> associés ("Burette" et "Bécher").
La commande "Générer une description" appliquée au canevas SVG ajoute un
<desc/> avec pour contenu :
Il y a 4 objets : [Agitateur magnétique] en bas. [Bécher] en bas, au dessus de [Agitateur magnétique]. [Burette] au centre, au dessus de [Bécher]. [Barreau aimanté] en bas, au dessus de [Agitateur magnétique].
On remarque que le fait de ne prendre en compte que les éléments avec un intitulé permet de conserver uniquement ceux qui sont pertinents : on ignore les lignes ou la table de travail qui ne sont pas utiles pour la compréhension du schéma. On peut ensuite améliorer la description, par exemple en indiquant que le barreau aimanté est en réalité à l'intérieur du Bécher ou en ajoutant des informations sur les produits chimiques.
On remarque que contairement à une image matricielle, le contenu des
éléments <text/> utilisés est accessible. Grace aux
groupes <g/>, les navigateurs peuvent informer l'utilisateur
quelle formule mathématique est associée à "Burette" et "Bécher". L'accès
à ces formules dépend des capacités du navigateur. Ici, il convient de
remplacer le texte d'avertissement par le nom des composants "permanganate de
potassium" et "sulfate de fer".
Comme on l'a vu dans l'étude du langage SVG, il n'existe que très peu de figures élémentaires : rectangle, cercle et ellipse. Si cela peut être suffisant pour des logiciels à vocation artistique comme Inkscape, on aimerait avoir des formes plus diversifiée pour les schémas, par exemple pour faire de la géométrie. Les formes retenues sont celles mises à disposition dans l'ensemble de constructions "figures élémentaires" décrit précédemment (3.1.3).
Les deux dernières sont décrites par les éléments
<circle/> et <ellipse/>. Les autres
figures peuvent soit être décrites par un <polygon/> soit
par un <path> composé d'un seul sous-chemin avec des
segments de droites dont le départ et l'arrivée sont confondues. On pourrait
aussi utiliser les <polyline/>, mais je n'ai pas traité ce
cas qui correspond plus à des courbes ouvertes (section 9.6 de [14]). Enfin, les deux premiers peuvent aussi être des
<rect/> ce qui autorise les coins arrondis. Lors de
l'analyse syntaxique du code XML, les figures sont reconnues par l'algorithme
3.3.2.1 qui peut être désactivé en modifiant la variable d'environnement
ENABLE_SHAPE_RECOGNITION.
Comme Amaya travaille avant tout sur la structure et non le rendu visuel,
les éléments XML ne doivent pas être transformés sans l'accord de
l'utilisateur : ainsi un <path/> reconnu comme un losange
n'est pas changé en <polygon/>, mais il utilise simplement
une structure interne spécifique. Par exemple, Thot disposait déjà de la
structure losange, tracé en joignant les mileux des cotés de la boite
graphique.
La reconnaissance des figures se fait en utilisant les propriétés
élémentaires de la géométrie euclidienne, en se donnant une marge d'erreur
due aux approximations. Notons que l'on peut caractériser les figures par les
longueurs (pythagore, coté égaux) ou d'angle (angle 90°, angles égaux). On
choisi la seconde caractérisation qui semble donner de meilleurs résultats en
pratique aux regards des tests réalisés. De plus on peut fixer la constante
d'erreur EPSILON_MAX de façon uniforme, qui est de 1 degré.
Ainsi une fonction PresqueÉgaux vérifie si deux angles sont
environ égaux. De même, PresqueOrthogonaux,
PresquePositivementColinéaires vérifient si les angles de
vecteurs sont environ de 90° ou 0°. Le positivement colinénaire
utilisé lors de la reconnaissance des trapèzes permet d'assurer le convexité
du polygone.
Pour l'algorithme, on considère un polygone avec des points numérotés de
1 à NombreDePoints. (i.e. la liste des points obtenus par un
<polygon/> ou un <path/>).
PermutationCirculaire(X->Y) réalise une permutation circulaire
sur les points du polygone, qui place le point X en position Y. Cette fonction
est importante pour pouvoir ensuite convertir l'ensemble de points en une
structure interne : par exemple pour un triangle isocèle, le premier point
sera le sommet principal. On ajoute aussi quelque simplification comme le fait
que le parallélogramme est incliné vers la droite ou que le trapèze a sa
plus petite base en haut.
On utilise enfin les notations [A>B] et
[A>B>C] pour représenter respectivement le vecteur de
et l'angle .
ReconnaitreFigure (polygone[1, 2, ... NombreDePoints])
<< Complexité en O(1) >>
Si NombreDePoints = 3
Si PresqueOrthogonaux([1>2], [1>3])
figure := TRIANGLE_RECTANGLE
SinonSi PresqueOrthogonaux([2>3], [2>1])
figure := TRIANGLE_RECTANGLE
PermutationCirculaire(1->2)
SinonSi PresqueOrthogonaux([3>2], [3>1])
figure := TRIANGLE_RECTANGLE
PermutationCirculaire(1->3)
Sinon
Si PresqueÉgaux([1>2>3], [2>3>1])
figure := TRIANGLE_ISOCÈLE
SinonSi PresqueÉgaux([3>1>2], [1>2>3])
figure := TRIANGLE_ISOCÈLE
PermutationCirculaire(1->3)
SinonSi PresqueÉgaux([3>1>2], [2>3>1])
figure := TRIANGLE_ISOCÈLE
PermutationCirculaire(1->2)
FinSi
Si figure = TRIANGLE_ISOCÈLE
Si PresqueÉgaux([3>1>2], [1>2>3])
figure := TRIANGLE_ÉQUILATÉRAL;
FinSi
FinSi
FinSi
SinonSi NombreDePoints = 4
Si PresquePositivementColinéaires([1>2], [3>4])
figure := TRAPÈZE
SinonSi PresquePositivementColinéaires([2>3], [1>4])
figure := TRAPÈZE
PermutationCirculaire(1->2)
FinSi
Si figure = TRAPÈZE
Si PresqueÉgaux([1>2>3], [3>4>1])
figure := PARALLÈLOGRAMME
Si PresqueÉgaux([2>4>1], [1>2>4])
figure := LOSANGE
FinSi
Si PresqueOrthogonaux([1>2], [2>3])
Si figure = LOSANGE
figure := CARRÉ
Sinon
figure := RECTANGLE
FinSi
FinSi
Si figure = PARALLELOGRAMME et EstAigu([4, 1, 2])
PermutationCirculaire(1->2)
FinSi
FinSi
Si figure = TRAPÈZE et Norme(4>3) < Norme(1>2)
PermutationCirculaire(1->3)
FinSi
FinSi
FinSi
FinReconnaitreFigure
Une fois la figure reconnue avec ses points caractéritiques triés, on peut ensuite trouver une boite englobante dans laquelle la figure sera tracée. Toutefois la boite a toujours ses cotés parallèles aux axes du repère local alors que le polygone peut être "tourné". On va alors rechercher les coefficients de la matrice de transformation et de la translation à appliquer localement pour pouvoir mettre la boite dans le bon sens. Commençons par le cas du rectangle :
A partir des quatre points du rectangle, on détermine facilement les dimensions de la boite englobante. On souhaite ensuite avoir les équations suivantes :
On en déduit alors . Pour la plupart des figures, on peut trouver une expression directe des coefficients à partir des coordonnées de points. De telles expressions sont fournies dans l'algorithme 3.3.2.2 et je laisse au lecteur le soin de les retrouver. Pour d'autres comme le trapèze ou le parallélogramme, on peut se ramener au cas du rectangle en déterminant les coordonnées des points de la boites englobante comme intersection de deux droites. Par exemple pour le trapèze, on considère des droites passant par les sommets et dirigés par un vecteur coliénaire/orthogonal à l'axe du trapèze. De tels vecteurs s'expriment facilemement en fonction des coordonnées des points : est un vecteur coliénaire et un vecteur orthogonal.
On cherche maintenant à déterminer l'intersection de deux droites : l'une passant par un point de coordonnées dirigée par le vecteur , l'autre par un point de coordonnée dirigée par le vecteur . Le point d'intersection vérifie pour des paramètres à déterminer : et , ce qui conduit au système matriciel d'où on tire puis les coordonnées recherchées (en supposant que le système ait une unique solution, i.e que les droites ne sont pas parallèles).
Pour alléger l'algorithme, on n'indique pas les vérifications de "division
par zéro". PointIntersection(X, U, Y, V) donne le point
d'intersection des droites passant par X et Y et dirigée par U, V. On note
et les coordonnées du point
.
ObtenirTailleBoiteEtCoefficients(figure, [1, ... NombreDePoints])
<< Complexité en O(1) >>
ChoisirSelon figure
Cas TRIANGLE_ÉQUILATÉRAL, TRIANGLE_ISOCÈLE
L := Norme([3>2])
H := Norme([Milieu(2,3)>1])
e := x1 + (x3 - x2)/2
f := y1 + (y3 - y2)/2
a := (x2 - x3)/L
b := (y2 - y3)/L
c := (x3 - e)/H
d := (y3 - f)/H
FinCas
Cas TRIANGLE_RECTANGLE
L = Norme([1>2])
H = Norme([1>3])
a := (x2 - x1)/L
b := (y2 - y1)/L
c := (x3 - x1)/H
d := (y3 - y1)/H
e := x1
f := y1
FinCas
Cas TRAPÈZE
Si EstAigu([4>1>2])
1' = 1
4' = PointIntersection(1, Orthogonal([1>2]), 4, [4>3])
Sinon
1' = PointIntersection(1, [1>2], 4, Orthogonal([4>3]))
4' = 4
FinSi
Si EstAigu([1>2>3])
2' = 2
3' = PointIntersection(2, Orthogonal([1>2]), 3, [3>4])
Sinon
2' = PointIntersection(2, [1>2], 3, Orthogonal([3>4]))
3' = 3
FinSi
L = Norme(1'>2')
H = Norme(1'>4')
a := (x2' - x1')/W
b := (y2' - y1')/W
c := (x4' - x1')/H
d := (y4' - y1')/H
e := x1'
f := y1'
FinCas
Cas PARALLÉLOGRAMME
1' = PointIntersection(1, [1>2], 4, Orthogonal([4>3]))
2' = 2
3' = PointIntersection(2, Orthogonal([1>2]), 3, [3>4])
4' = 4
L = Norme(1'>2')
H = Norme(1'>4')
a := (x2' - x1')/W
b := (y2' - y1')/W
c := (x4' - x1')/H
d := (y4' - y1')/H
e := x1'
f := y1'
FinCas
Cas LOSANGE
L = Norme([2>4])
H = Norme([1>3])
a := (x2 - x4)/L
b := (y2 - y4)/L
c := (x3 - x1)/H
d := (y3 - y1)/H
e := x1 - a*L/2
f := y1 - b*L/2
FinCas
Cas CARRÉ, RECTANGLE
L = Norme(1>2)
H = Norme(1>4)
a := (x2 - x1)/W
b := (y2 - y1)/W
c := (x4 - x1)/H
d := (y4 - y1)/H
e := x1
f := y1
FinCas
FinChoisirSelon
FinObtenirTailleBoiteEtCoefficients
Notons que puisque le changement de repère n'est constitué que de
symétrie, rotation et translation, la matrice trouvée peut en théorie se
décomposer en produit d'un scale avec des coefficients ±1, d'un
rotate et d'un translate. En pratique, lorsque des
figures sont tournées on peut obtenir la décomposition générale avec des
skew+scale+translate, à cause de
l'accumulation des approximations. Il faudrait améliorer l'algorithme de
façon à directement rechercher une décomposition en
scale+rotate+translate au lieu d'une
forme générale pour la matrice de décomposition.
Une fois les figures reconnues, elles peuvent être manipulées de façon particulière par l'utilisateur. Lorsqu'elles sont sélectionnées, des poignées spécifiques sont affichées sur ces figures en des points caractéristiques du contour de la figure ou de celui de la boite englobante. Lorsque l'utilisateur tire sur une de ces poignées il déplace un point de la figure ou de la boite englobante. Les points de la figure sont alors mis à jour de façon à respecter les contraintes imposées. Certaines figures ont aussi des cercles rouges qui peuvent être déplacés pour les déformer.
| Figure ( char associé) |
Tracé dans la boite englobante | Contrainte sur la boite englobante | Image de la sélection |
|---|---|---|---|
| Carré (7) | Coïncide avec la boite englobante. | Hauteur = Largeur | ![]() |
| Carré à coins arrondis (1) | idem | Les cercles rouges restent sur une moitié des cotés. | ![]() |
| Rectangle (8) | idem | / | cf carré |
Rectangle à coins arrondis ('C') |
idem | cf carré arrondi | cf carré arrondi |
Losange ('L') |
Les sommets du losange sont les mileux de la boite englobante. | / | ![]() |
| Parallélogramme (2) | Deux points du parallélogramme correspondent à des sommets de la boite. Les autres sont précisés par un nombre (positif ou négatif). | Les cercles rouges restent sur les cotés de la boite englobante et sont déplacés simultanément pour rester symétriquement disposés. | ![]() |
| Trapèze (3) | Deux points du parallélogramme correspondent à des sommets de la boite. Les autres sont précisés par deux nombres (positifs ou négatifs). | Les cercles rouges restent sur les cotés de la boite englobante. Le trapèze ne peut pas être "croisé". | ![]() |
| Triangle équilatéral (4) | Le sommet principal est au milieu du coté supérieur de la boite englobante. Les deux autres sommets sont les extrémités du coté inférieur de la boite. | ![]() |
|
| Triangle isocèle (5) | idem | / | cf triangle équilatéral |
| Triangle rectangle (6) | Le triangle rectangle a son angle droit dans le coin supérieur gauche de la boite englobante. Les deux autres sommets sont dans les coins supérieur droit et inférieur gauche. | / | ![]() |
Cercle ('a') |
Inscrit dans la boite englobante. | Hauteur = Largeur | ![]() |
Ellipse ('c') |
Inscrit dans la boite englobante, les demi-axes sont parallèles aux cotés de la boite. | / | cf cercle |
Les rectangles (et carrés) spéciaux correspondent aux éléments
<rect/>. Ceux-ci peuvent en effet avoir des attributs
rx et ry variant entre 0 et les demi-dimensions du
rectangle qui indiquent les rayons des coins arrondis. Des poignées
représentées par de petits ronds rouges peuvent être glissés le long des
cotés du rectangle de façon à faire varier ces rayons. Dans le cas où seul
un des attribut rx ou ry est donné, la
spécification [14] précise que les rayons doivent être
égaux. Dans ce cas, le module d'édition modifie les rayons en conservant
cette contrainte.
Pour le parallélogramme, l'inclinaison est indiquée par une distance dont le signe donne la direction d'inclinaison et la valeur absolue l'amplitude. Les cercles rouges peuvent être glissés le long des cotés supérieurs et inférieurs.
Le trapèze ne possède pas de petits cercles coulissables car je n'ai pas programmé l'opération dans le module. L'inclinaison est décrite de la même façon que le parallélogramme mais il faut cette fois deux paramètres et indépendants :
Notons pour terminer que le module d'édition des trapèze et
parallélogramme n'a pas été connecté. La visualisation et l'édition ont
toutefois été testées en utilisant ce qui existait pour les boites
représentants le rectangle à coins arrondis (utilisation des deux paramètres
rx et ry pour représenter les distances).
Amaya possède un certain nombre de commandes génériques pouvant être
appliquées aux éléments de l'arbre Thot : couper, coller, insérer, ajouter,
détruire... Cependant les points des <polyline/>,
<polygon/> et <path/> sont décrits comme
des structures de données spécifiques attachées à une feuille du noeud
représentant l'élément SVG. Il est nécessaire d'ajouter un traitement
spécifiques pour ces éléments. Il existait déjà des fonctions pour
sélectionner un point d'un <polyline/> et un module bogué
pour déplacer des points. C'est sur cette interface que je me suis basé. Elle
présente néanmoins le désavantage de ne pouvoir sélectionner qu'un point à
la fois, ce qui empêche d'envisager des opérations sur des groupes de points.
La sélection est décrite visuellement de la façon suivante :
Les trois opérations retenues pour l'édition de points sont détruire, insérer et ajouter. La première supprime un point de la courbe. Les deux autres créer un nouveaux points respectivement sur le segment qui précède ou suit le point sélectionné, si il en existe un.

Pour les <polyline/>, le point est créé au mileu du
segment. Il y a aussi un traitement spécifique aux extrémités, par exemple
si on créé un point avant le premier point du <polyline/>,
il sera le symétrique du second point par rapport au premier. De même, si on
créé un point après le dernier point, il sera le symétrique de
l'avant-dernier par rapport au premier.
Pour les <path/>, on recherche le fragment adjacent au
point sélectionné et on le divise en deux selon son type. On s'arrête au
"milieu" de la courbe, au sens de la paramétrisation utilisée.
On conçoit facilement qu'un segment peut être découpé en deux segments et un arc d'ellipse en deux arcs d'ellipse. Pour les courbes de Bézier, cela est moins immédiat puisqu'il faut a priori repositionner les poignées.
Une courbe de bézier de point de contrôle ... peut être divisée en deux courbes de Bézier de même degré dont les points de départ et d'arrivée sont respectivement et pour la première et et pour la seconde.
Démonstration : on traite le cas et on cherche le premier fragment. Les expressions obtenues pourront facilement se généraliser. On a donc et on cherche une courbe de même forme telle que . Notons au passage que les fonctions étant polynomiales, on aura en fait cette égalité sur tout .
analyse. Si est la courbe recherchée alors . On trouve et de même pour . De plus et . En prenant les égalités entre et et leurs dérivées aux différents points, on obtient finalement .
synthèse. en réinjectant les coefficients trouvés, on retrouve bien . □
Le programme Maxima ci-dessous permet de vérifier les égalités pour les cas quadratique et cubique qui nous intéressent :
/* Courbe de Bézier cubique */ P(t):=(1-t)^3*P0+3*(1-t)^2*t*P1+3*(1-t)*t^2*P2+t^3*P3; Q(t):=(1-t)^3*Q0+3*(1-t)^2*t*Q1+3*(1-t)*t^2*Q2+t^3*Q3; /* Fragment 1 */ Q0 : P0; Q1 : (P0+P1)/2; Q2 : (P0+2*P1+P2)/4; Q3 : (P0+3*P1+3*P2+P3)/8; ratsimp(P(t) - Q(2*t)); /* Fragment 2 */ Q0 : (P0+3*P1+3*P2+P3)/8; Q1 : (P1+2*P2+P3)/4; Q2 : (P2+P3)/2; Q3 : P3; ratsimp(P(1/2+t) - Q(2*t)); /* Courbe de Bézier quadratique */ P(t):=(1-t)^2*P0+2*(1-t)*t*P1+t^2*P2; Q(t):=(1-t)^2*Q0+2*(1-t)*t*Q1+t^2*Q2; /* Fragment 1 */ Q0 : P0; Q1 : (P0+P1)/2; Q2 : (P0+2*P1+P2)/4; ratsimp(P(t) - Q(2*t)); /* Fragment 2 */ Q0 : (P0+2*P1+P2)/4; Q1 : (P1+P2)/2; Q2 : P2; ratsimp(P(1/2+t) - Q(2*t));
La suppression d'un point dans un polyline/polygone est possible jusqu'à un nombre minimum de 2 points. Elle se fait naturellement : on retire le point sélectionné de la liste des points.
Pour un chemin, la suppression est moins évidente puisque ce dernier est décrit comme une liste de segments (ligne, courbe de Bézier ou arc) non comme une liste de points. On supprime alors le segment arrivant sur ce point en s'arrangeant pour que les poignées de Bézier des segments encadrants (si ce sont des courbes de Bézier) ne soient pas modifiées.
Il existe encore quelques problèmes pour la suppression des points. Par exemple la suppression du premier point d'un sous-chemin n'est pas possible (il n'y a pas de segment arrivant à ce point) et la suppression du second point supprime aussi le premier.
Le déplacement des points d'une courbe ou d'une poignée se réalise
simplement en cliquant sur le point et en le faisant glisser à la nouvelle
position. Il n'y a pas de problème particulier pour les
<polyline/> ou <polygon/> puisqu'il
suffit de mettre à jour le point sélectionné et que le fait que ces figures
soient fermée ou ouverte est immédiatement connu. Pour les
<path/> cela est plus compliqué :
Si un point possède une poignée de Bézier avant et après lui, alors les deux demi-tangentes décrites par ces poignées peuvent être reliés entre elles de trois façons :
Lorsque l'on déplace une poignée, il faut d'assurer que ces contraintes soient conservées. Lorsque l'on déplace un point, les poignées subissent la même translation.
Comme précédemment, pour savoir si un sous-chemin est fermé on regarde si le premier et dernier point coïncident. Dans ce cas, tout se passe comme si ces deux points n'en formaient qu'un seul, se comportant comme n'importe quel point intermédiaire du sous-chemin : ils sont déplacés simultanéments et leurs poignées de Bézier sont liées.
Une poignée de Bézier peut provenir d'un segment quadratique ou cubique. Dans le cas quadratique, les deux tangentes successives sont "reliées" au point de controle. Si on souhaite conserver l'alignement des points, une succession de courbes quadratiques pourraient conduire à la mise à jour de toute une chaine :
Pour simplifier le problème, on effectue une conversion d'une courbe de Bézier quadratique à une courbe de Bézier cubique comme indiqué dans [5]. En notant les points de controle du fragment quadratique et ceux du fragment cubique, on effectue le changement . On peut ensuite effectuer le traitement comme une courbe cubique normale. Cette conversion n'est pas forcément très discrète puisque l'utilisateur voit les poignées rétrécir avant l'édition d'une courbe quadratique.
Pour les segments de chemin qui sont des lignes ou des arcs, on se contente de mettre à jour les positions des points. Cela est problématique pour les arcs puisque ceux-ci sont décrits par plusieurs paramètres : rayons, angles... J'ai essayé de réaliser une mise à jour pour conserver l'aspect de l'arc mais le résultat n'était pas satisfaisant. Voir l'annexe (5.1.2).
Thot possède une structure de liste chainée permettant de représenter des
compositions de transformations SVG. Des fonctions de manipulation sont
disponibles dans le module thotlib/content/contentapi.c. Pour les
opérations internes, j'ai réalisé des fonctions permettant de simplifier
cette liste en une seule matrice de transformation, d'appliquer une
transformation supplémentaire ou encore d'inverser une matrice de
transformation. J'ai été amené à étudier la forme générale d'une
transformation. Le point de départ de mon investigation est le théorème
suivant :
Toute matrice carré peut se décomposer comme un produit des matrices d'étirement, de rotation et d'inclinaisons. Plus précisément, on peut se limiter à un étirement, une inclinaison selon les X, une inclinaison selon les Y et enfin une rotation de 90° (ou une inclinaison de 45° supplémentaire).
Démonstration :
Remarquons pour conclure que les rotations s'écrivent en fonction d'étirement et d'inclinaisons :
En particulier, dans le troisième cas () on peut substituer la matrice de rotation et écrire comme produit de deux inclinaisons le long de l'axe des X, d'une le long de l'axe des Y et d'un étirement. □
Le programme Maxima suivant permet de vérifier les formules données dans le théorème 3.4.1.1 :
S(sx, sy) := matrix([sx,0], [0,sy]); R(T) := matrix([cos(T),-sin(T)], [sin(T),cos(T)]); Ix(T) := matrix([1,tan(T)], [0,1]); Iy(T) := matrix([1,0], [tan(T),1]); Iy(atan(b/a)).S(a,d-b*c/a).Ix(atan(c/a)); Ix(atan(c/d)).S(a-b*c/d,d).Iy(atan(b/d)); R(%pi/2).S(b,-c); R(-%pi/2); Ix(%pi/4).Iy(-%pi/4).Ix(%pi/4); R(%pi/2); Iy(%pi/4).Ix(-%pi/4).Iy(%pi/4); R(T); trigsimp(trigreduce(Ix(-T).S(1/cos(T), cos(T)).Iy(T)));
Une application affine peut s'écrire comme composée des transformations élémentaires comportant au plus une translation, un étirement, une inclinaison selon l'axe des X, une inclinaison selon l'axe des Y et une rotation de 90°.
Démonstration : Le résultat précédent a montré que (sans translation) on pouvait engendrer toutes les applications linéaires. L'adjonction des translations permet donc de généraliser au cas des applications affines. □
Les applications affines dans le plan sont engendrées par les translations, étirements, inclinaisons selon les X et inclinaisons selon les Y. Ce résultat est optimal au sens où on ne peut retirer un de ces types de transformations.
Démonstration : On utilise la version de la proposition utilisant l'inclinaison de 45° pour montrer que ces transformations suffisent à engendrer l'ensemble des applications affines. Si on retire les translations, on ne peut plus effectuer que des applications linéaires. Si on retire les étirements, on obtient plus que des matrices de déterminant égal à 1. Si on retire une inclinaison selon un axe, la matrice de la composante linéaire ne peut être que triangulaire. □
Du point de vue théorique, la décomposition du corollaire 3.4.1.3 permet
de concevoir plus clairement l'image d'un objet et d'étudier la transformation
réciproque (toutes les matrices sont facilement inversibles sauf l'étirement
qui l'est si et seulement si les facteurs d'échelle sont non nuls). Du point
de vue pratique, cela est utile si l'utilisateur souhaite éditer l'attribut
transform dans la vue source, dans la vue structure ou même dans
un menu. En effet, sauf dans des cas simples, la matrice de transformation est
difficilement compréhensible pour l'utilisateur et une écriture comme
composition de transformations élémentaires est donc préférable.
Le corollaire 3.4.1.4 indique quant à lui qu'il suffit simplement de donner à l'utilisateur la possibilité de réaliser trois transformations simples (translations, étirements et inclinaisons) pour qu'il puisse réaliser n'importe quelle transformation affine.
La démonstration est constructive et donne directement un algorithme de
décomposition. Bien que l'on puisse se restreindre à l'utilisation d'un
pivotement de 90° ou même se passer complètement des rotations, elles sont
parfois préférables aux inclinaisons. Ainsi dans le cas où l'utilisateur
rentre une valeur du type rotate(45°), il ne souhaite pas qu'elle
soit complexifiée en produit d'étirements et d'inclinaisons.
Pour éviter ce problème, on commence par rajouter quelques conditions pour déterminer si on peut mettre la matrice de l'application linéaire sous forme d'une rotation. Si la matrice est de la forme , alors la méthode usuelle consiste à prendre un facteur d'échelle et pour angle , la matrice devient . Plus généralement, on peut aussi se ramener à la forme précédente en essayant de multiplier les coefficients grâce à une matrice d'étirement. Ainsi si et alors et la première matrice est de la forme désirée. On a un cas similaire pour . En multipliant à droite par une matrice d'étirement on obtient deux cas supplémentaires (la condition devient ).
Ces cas particuliers ainsi que le troisième cas de la décomposition
générale () et le cas d'un simple étirement
() conduisent donc à des attributs de la forme "translate
scale", "translate scale rotate", "translate rotate
scale". Notons que quitte à changer les coefficients de la
translations, on peut permuter translation et étirement, puisque
"translate(tx,ty) scale(sx,sy)" est équivalent à
"scale(sx,sy) translate(tx/sx,ty/sy)". L'intérêt d'une telle
permutation est donnée par la proposition suivante :
Tout attribut transform de la forme "translate(tx,ty)
rotate(θ)" peut être simplifiée en un simple
"translate(tx,ty)" ou un "rotate(θ,cx,cy)".
Démonstration :
La matrice de "rotate(θ,cx,cy)" est
et celle de "translate(tx,ty) rotate(θ)" est
. Pour réduire cette dernière en une rotation avec centre, il suffit
donc de trouver satisfaisant :
en notant et .
Le déterminant de ce sytème est qui est non nul si et seulement si . On trouve alors pour solution
avec .
Enfin, si on a en fait une rotation d'angle et la transformation est en réalité une pure translation. □
A titre d'exemple, le cas qui se réduisait à "translate(e,f) rotate(90°) scale(b,
-c)" devient maintenant "rotate(90°, [e-f]/2, [e+f]/2) scale(b,
-c)". En reprenant toutes les idées énoncées précédemment, on
arrive finalement à l'algorithme suivant :
DecomposerMatrice (matrix(a, b, c, d, e, f))
<< Complexité en O(1) >>
resultat := ListeVide
Si b = c = 0
* Cas I *
Ajouter(resultat, translate(e, f))
Ajouter(resultat, scale(a, d))
SinonSi a = d = 0
* Cas II *
Ajouter(resultat, rotate(90°, [e-f]/2, [e+f]/2))
Ajouter(resultat, scale(b, -c))
SinonSi a = d et b = -c
* Cas III *
s := racine(a*a + b*b);
θ := signe(b)*arccos(a/s);
Si θ = 0
Ajouter(resultat, translate(e, f))
Sinon
k := sin(θ)/(1 - cos(θ))
Ajouter(resultat, rotate(θ°, [e - k*f]/2, [k*e + f]/2))
FinSi
Ajouter(resultat, scale(s, s))
SinonSi ab + cd = 0 et (b, c ≠ 0 ou a, d ≠ 0)
* Cas IV *
Si b, c ≠ 0
a := -a*b
d := b*c
s := racine(a*a + d*d)
sx := -s/b
sy := s/c
θ := signe(d)*arccos(a/s)
Sinon
b := a*d
c := c*d
s := racine(b*b + c*c)
sx := s/d
sy := s/a
θ := signe(c)*arccos(b/s)
FinSi
Si θ = 0
Ajouter(resultat, translate(e, f))
Ajouter(resultat, scale(sx, sy))
Sinon
Ajouter(resultat, scale(sx, sy))
e := e/sx
f := f/sy
k := sin(θ)/(1 - cos(θ))
Ajouter(resultat, rotate(θ°, [e - k*f]/2, [k*e + f]/2))
FinSi
SinonSi ac + bd = 0 et (b, c ≠ 0 ou a, d ≠ 0)
* Cas V *
Si b, c ≠ 0
a := a*c;
d := b*c;
s := racine(a*a + d*d);
sx := s/c;
sy := -s/b;
θ := signe(d)*arccos(a/s);
Sinon
b := a*d;
c := a*c;
s := racine(b*b + c*c);
sx := s/d;
sy := s/a;
θ := signe(c)*arccos(b/s);
FinSi
Si θ = 0
Ajouter(resultat, translate(e, f))
Sinon
Ajouter(resultat, scale(sx, sy))
k := sin(θ)/(1 - cos(θ))
Ajouter(resultat, rotate(θ°, [e - k*f]/2, [k*e + f]/2))
FinSi
Ajouter(resultat, scale(sx, sy))
SinonSi a ≠ 0
* Cas VI *
θ1 := arctan(b/a)
θ2 := arctan(c/a)
d := d - b*c/a
Ajouter(resultat, skewY(θ1°))
Ajouter(resultat, scale(a, d))
Ajouter(resultat, skewX(θ2°))
SinonSi d ≠ 0
* Cas VII *
θ1 := arctan(c/d)
θ2 := arctan(b/d)
a := a - b*c/d
Ajouter(resultat, skewX(θ1°))
Ajouter(resultat, scale(a, d))
Ajouter(resultat, skewY(θ2°))
FinSi
Renvoyer(resultat)
FinDecomposerMatrice
Il est intéressant de noter que si l'utilisateur ne déforme pas les objets, c'est-à-dire n'applique que des translations, rotations et des étirements conservant les proportions, alors on reste dans les décompositions n'utilisant pas d'inclinaison (i.e autres que VI et VII) de l'algorithme. En effet, une telle composition de transformations donne une similitude dont on sait qu'elle peut s'écrire sous forme "réduite". La proposition suivante montre que l'algorithme fournit effectivement cette écriture :
L'algorithme 3.4.1.6 décompose toute similitude en un attribut utilisant
des translate, rotate et scale.
Démonstration :
Soit la partie linéaire de la similitude, notons le déterminant de et le rapport de similitude, c'est-à-dire tel que pour tout vecteur , . Si est non inversible, alors en prenant non nul, on obtient . Réciproquement, si , alors clairement est non inversible. On s'intéresse donc au cas où est inversible (i.e ), on a alors et pour tout vecteur , .
Comme et on obtient en passant au carré des normes : .
De même, de et on déduit
En effectuant des combinaisons linéaires judicieuses de ces deux égalités, on obtient finalement et c'est-à-dire et . De plus ces égalités donnent aussi . Les différents cas possibles sont :
| 0 | 0 | |||
| 0 | 0 | |||
Le troisième cas, correspond au cas III de l'algorithme. Dans le deuxième cas, sachant que le déterminant n'est pas nul, on a soit soit non nul (et donc ou ) qui est le cas IV (et V) de l'algorithme. Pour les cas restants, on sait que et à nouveau en passant au carré des normes, . Avec les données du tableau, on trouve . Selon le signe de cette égalité se simplifie en :
□
Remarquons que les transformations décomposées par l'algorithme sans utiliser d'inclinaisons ne sont pas restreintes aux similitudes. Il suffit par exemple de considérer une transformation de composante linéaire (ce n'est pas une similitude puisque alors que les vecteurs et de la base canonique ont même norme). La question de savoir quelles transformations sont décomposables sans inclinaison est ouverte, mais le présent algorithme est pour l'instant largement satisfaisant.
Pour conclure, la décomposition peut être désactivée via la variable
d'envionnement ENABLE_DECOMPOSE_TRANSFORM. Dans ce cas, Amaya
génère un attribut transform="matrice(a, b, c, d, e, f)".
Nous allons maintenant voir des transformations simples que l'utilisateur peut effectuer en lançant une commande. La situation est la suivante : l'utilisateur veut transformer un objet SVG, fils direct d'un <svg/>. En rouge, on a dessiné la boite englobante qui nous sert de référence.
Les transformations de bases décrites dans cette section sont :
Pour les symétries, l'algorithme consiste à se placer dans le repère du centre de la boite englobante par des translations avant d'appliquer une symétrie selon l'axe des abscisses ou des ordonnées dont la matrice est respectivement et .
AppliquerSymetrie (element, est_horizontale)
<< Complexité en O(1) >>
ObtenirBoiteEnglobante(&x, &y, &largeur, &hauteur)
cx := x + largeur / 2
cy := y + hauteur / 2
AppliquerTransformation(element, translate(-cx, -cy))
Si est_horizontale
AppliquerTransformation(element, matrix(1, 0, 0, -1, 0, 0))
Sinon
AppliquerTransformation(element, matrix(-1, 0, 0, 1, 0, 0))
FinSi
AppliquerTransformation(element, translate(+cx, +cy))
FinAppliquerSymetriee
L'algorithme pour les pivotements de 90° est identique, sauf que l'on
applique la transformation rotate(±90°, cx, cy). On conçoit
aussi facilement une fonction DeplacerObjet (element, x, y) qui
nous servira pour les alignements et les distributions.
Pour conclure, si plusieurs objets ont été sélectionnés avant la commande chaque objet est transformé individuellement. Les algorithmes décrits dans cette section s'adapte facilement en ajoutant une boucle.
On décrit dans cette section des transformations se réalisant sur une sélection de plusieurs objets. Comme précédemment, chaque objet à sa propre boite englobante. En prenant le min et max selon chaque direction on obtient facilement une boite englobante de l'ensemble des objets.
Les deux types d'opérations considérés sont :
<svg/>.
L'algorithme pour les alignements est le suivant :
AlignerObjets (canvas, objets[], nombre, type)
<< Complexité en O(nombre) >>
Si nombre = 1
ObtenirTaille(canvas, &xmax, &ymax)
ObtenirBoiteEnglobante(element, &x, &y, &largeur, &hauteur)
ChoisirSelon type
Cas Gauche
DeplacerObjet(objets[0], 0, y)
FinCas
Cas Centre
DeplacerObjet(objets[0], (xmax-largeur)/2, y)
FinCas
Cas Droite
DeplacerObjet(objets[0], xmax-largeur, y)
FinCas
Cas Haut
DeplacerObjet(objets[0], x, 0)
FinCas
Cas Milieu
DeplacerObjet(objets[0], x, (ymax-hauteur)/2)
FinCas
Cas Bas
DeplacerObjet(objets[0], x, ymax-hauteur)
FinCas
FinChoisirSelon
SinonSi nombre > 1
ObtenirBoiteEnglobante(objets[0], &x, &y, &largeur, &hauteur)
xmin := x
xmax := x + largeur
ymin := y
ymax := y + hauteur
Pour i de 1 à nombre - 1
ObtenirBoiteEnglobante(objets[i], &x, &y, &largeur, &hauteur)
Si x < xmin
xmin := x
FinSi
Si y < ymin
ymin := y
FinSi
Si x + largeur > xmax
xmax := x + largeur
FinSi
Si y + hauteur > ymax
ymax := y + hauteur
FinSi
FinPour
xcentre := (xmin + xmax)/2
ycentre := (ymin + ymax)/2
Pour i de 1 à nombre - 1
ObtenirBoiteEnglobante(objets[i], &x, &y, &largeur, &hauteur)
ChoisirSelon type
Cas Gauche
DeplacerObjet(objets[i], xmin, y)
FinCas
Cas Centre
DeplacerObjet(objets[i], xcentre - largeur/2, y)
FinCas
CasDroite
DeplacerObjet(objets[i], xmax - largeur, y)
FinCas
Cas Haut
DeplacerObjet(objets[i], x, ymin)
FinCas
Cas Milieu
DeplacerObjet(objets[i], x, ycentre - hauteur/2)
FinCas
Cas Bas
DeplacerObjet(objets[i], x, ymax - hauteur)
FinCas
FinChoisirSelon
FinPour
FinSi
FinAlignerObjets
L'idée est la même pour les distributions. On ignore le cas où le nombre
d'objets nombre est strictement inférieur à 3, qui ne change
rien à la distribution des objets.
Pour les distributions selon les bords/axes, il faut aussi prendre garde à
l'ordre des objets (de gauche à droite et de haut en bas) qui doit être
conservé. On commence donc par calculer les coordonnées caractéristiques
position[i] pour chaque objet: abcisse gauche/centre/droite ou
ordonnée haut/milieu/bas selon le type de distribution attendue. On trie
ensuite les objets dans l'ordre croissant des valeurs de
position[i].
La nouvelle position du i-ème objet est alors donnée par la formule : avec un pas entre chaque position successive égal à ( est le nombre d'objets).
Pour les distributions selon les espacements, il n'y a a priori pas d'ordre naturel sur les objets. Comme généralement l'utilisateur applique une telle opération sur des objets qui ne se chevauchent pas (i.e. les écarts définis plus loin sont positifs), les ordres utilisés précédemment sont équivalents et on peut choisir un ordre arbitraire par exemple selon les axes centraux. Étudions le problème dans le cas horizontal, le cas vertical en découlant. Pour chaque objet , représentent respectivement les abscisses des bords gauche, droits et la longueur de l'objet :
L'espacement entre deux objets successifs est donné par . On utilise les même notations avec des primes pour les valeurs après transformation. Les conditions désirées sont :
La troisième égalité s'écrit ce qui donne en sommant de 0 à :
et finalement :
L'algorithme consite donc à calculer l'espacement à partir des bords extrêmes et des longueurs des boites, ce qui se fait en une boucle. Ensuite, on calcul récursivement les nouvelles positions gauches, via la relation :
où encore . Cette dernière écriture permet de réutiliser l'algorithme des distributions par rapport au bord gauche/supérieur, en mettant à jour l'origine à chaque étape (ajout de ).
On s'intéresse maintenant à des transformations plus générales
nécessitant une interaction avec l'utilisateur : tourner (rotation d'angle
quelconque), incliner (<skew*/>), redimensionner
(<scale/>) et déplacer (<translate/>). A
nouveau pour simplifier, ces opérations n'agissent que sur un objet à la
fois. On pourrait les améliorer en passant au module de transformation une
liste des objets à transformer et en y ajoutant des boucles for.
Les commandes sont disponibles dans la palette SVG mais comme elles sont très
utilisées on les retrouve aussi dans le menu contextuel accessible en
effectuant un clic droit :

Le déplacement peut être effectué plus rapidement en faisant un glisser-déposer de l'objet SVG. Toutefois on conserve la commande spécifique qui est utile pour certains cas particuliers :
<text/> ou un objet étranger, le
mouvement glisser-déposer rentre en conflit avec la sélection.Toutes les transformations sont effectuées en calculant la différence entre deux positions de souris (reportées dans le repère local) et non les positions effectives. Ainsi pour les rotations et translations il n'est pas utile de cliquer exactement sur l'objet. Pour les redimensionnement et inclinaison il faut par contre cliquer sur des flèches ou sinon l'interaction s'arrête. On peut quitter l'interaction à tout moment en effectuant un double-clic. Les modules se présentent ainsi :
On note (x1, y1) et (x2, y2) les coordonnées
entre deux mouvements successifs de souris. Certains cas sont similaires et ne
seront donc pas tous détaillés. cx et cy sont les centres de la boite
englobante ou de la rotation. haut, bas,
gauche, droite les coordonnées de la boite
englobante.
AppliquerTransformation (element, x1, y1, x2, y2, type)
ChoisirSelon
Cas Translation
AppliquerTransformation(element, translation(x2 - x1, y2 - y1))
FinCas
Cas Rotation
dx1 := x1 - cx
dx2 := x2 - cx
dy1 := y1 - cy
dy2 := y2 - cy
d := Racine((dx1^2 + dy1^2)*(dx2^2 + dy2^2));
Si d > 0
det := dx1*dy2 - dy1*dx2;
theta := Signe(det)*arccos((dx1*dx2 + dy1*dy2)/d)
AppliquerTransformation(element, translation(-cx, -cy))
AppliquerTransformation(element,
matrice(cos(theta), sin(theta),
-sin(theta), cos(theta),
0, 0))
AppliquerTransformation(element, translation(+cx, +cy))
FinSi
FinCas
Cas InclinerSelonAxeDesX_FlecheDuHaut
AppliquerTransformation(element, translation(-cx, -cy))
x1 := x1 - cx
x2 := x2 - cx
y1 := Haut - cy
y1 := y2
AppliquerTransformation(element, matrice(1, 0,
θ, 1,
0, 0))
AppliquerTransformation(element, translation(+cx, +cy))
FinCas
Cas RedimensionnerVertical_FlecheDuHaut
y1 := y1 - Bas
y2 := y2 - Bas
Si y1, y2 ≠ 0
AppliquerTransformation(element, translation(-cx, -Bas))
sy := y2/y1
Si GarderProportion
sx := sy
Sinon
sx := 1
FinSi
AppliquerTransformation(element, matrice(sx, 0,
0, sy,
0, 0))
AppliquerTransformation(element, translation(+cx, +Bas))
FinSi
FinCas
Cas RedimensionnerDiagonal_FlecheEnHautAGauche
x1 := x1 - droite
x2 := x2 - droite
y1 := y1 - bas
y2 := y2 - bas
Si x1, x2, y1, y2 ≠ 0
AppliquerTransformation(element, translation(-Droite, -Bas))
sx := x2/x1
sy := y2/y1
Si GarderProportion
Si |sx| < |sy|
sy := Signe(sy)*|sx|
Sinon
sx := Signe(sx)*|sy|
FinSi
FinSi
AppliquerTransformation(element, matrice(sx, 0,
0, sy,
0, 0))
AppliquerTransformation(element, translation(+Droite, +Bas))
FinSi
FinCas
[...]
FinChoisirSelon
FinAppliquerTranformation
Lorsque l'utilisateur sélectionne un élément SVG, des commandes spécifiques apparaissent dans le panneau de style. Seuls des attributs simples qui peuvent être rendus par Amaya sont disponibles. Ils concernent la couleur, l'opacité et l'épaisseur des traits :

Le premier champs donne le pourcentage d'opacité de l'élément. On a
ensuite deux catégories de style qui s'appliquent au remplissage et au
contour. Ceux-ci peuvent être désactivés (la valeur de la couleur est alors
none) ou activés (la valeur de la couleur prend celle affichée
dans le panneau). Les remplissages et contour ont chacun leur valeur d'opacité
qui s'ajoute à l'opacité globale. Enfin on peut préciser une épaisseur pour
le contour.
Lorsque l'utilisateur souhaite modifier le style d'un objet, il s'attend à pouvoir récupérer les valeurs courantes. Ainsi, chaque fois qu'un objet SVG est sélectionné, son style de présentation est chargé dans la palette.
Tous les styles sont appliqués dans un attribut style (pour
lequel il existait déjà des fonctions de manipulation) et non les attributs
de présentation. Le passage entre les deux syntaxes n'a pas été pris en
compte. Cependant si un objet comporte des attributs de présentation SVG alors
son style peut tout de même être chargé dans la palette. En le modifiant, on
regénère tout l'attribut style dont les valeurs sont prioritaires sur les
attributs de présentation déjà existantes (voir section 6.4 de [14]). Ainsi le résutat visuel n'est pas choquant pour
l'utilisateur même si il y a des informations redondantes dans le code
source.
Les éléments SVG sont par défaut sans bordure et remplis en noir. Lors de la création d'un nouvel objet, il convient de le modifier pour le rendre plus agréable. Le panneau de style prenant les valeurs du dernier objet sélectionné, si on utilise ces valeurs pour les nouveaux éléments créés alors on peut tracer plusieurs éléments avec le même style. Cette méthode pose toutefois quelques problèmes :
Par conséquent on préfère donner un style par défaut à chaque création et on laisse ensuite le soin à l'utilisateur de modifier ce style. Pour la plupart des objets, ce style est un remplissage bleu et une bordure noir. Certains n'ont pas de remplissage par exemple les courbes ouvertes ou encore les composants de schémas où la mise en couleur n'est pas pertinente (symbole de circuit électrique par exemple).
Lorsque l'on applique ou récupère le style d'un groupe on le fait comme
tout élément SVG : on prend les valeurs de l'attribut style
attaché au groupe. Un objet héritant les propriétés CSS de ses parents, la
modification du style du groupe permet de modifier celui de ses descendants.
Cela ne fonctionne pas si les descendants ont déjà eux-même un style
appliqué : dans ce cas leur style local est prioritaire sur le style du
groupe. Si on souhaite appliquer un style à plusieurs éléments il ne faut
donc pas les grouper mais simplement les sélectionner. L'application du style
sur un groupe est donc pour l'instant réservé à des utilisateurs avancés
pour tirer profit des méthodes d'héritage.
L'héritage est utilisé pour permettre à l'utilisateur de styler les
composants prédéfinis. Ces éléments ne possèdent pratiquement aucun style,
de façon à ce qu'ils héritent le style du groupe. De plus les zones
distinctes sont tracées avec plusieurs éléments pour que l'utilisateur
puisse leur appliquer des styles différents. A titre d'exemple, un cube est
dessiné avec 6 polygones représentant ses faces. Si on applique une couleur
au groupe, elle sera directement appliquée à toutes les faces du cube.
L'utilisateur peut aussi donner des couleurs pour chaque face puisqu'elles sont
dessinées avec des éléments différents. Un autre exemple concerne les
verreries de chimie qui peuvent contenir des liquides. Dans ce cas, le flacon a
localement un remplissage none pour que la couleur ne s'applique
qu'au liquide.
L'objectif du stage était de mettre en place la création et l'édition de formes élémentaires SVG dans Amaya ainsi que d'y ajouter des fonctionnalités comparables à ce que l'on trouve dans les autres éditeurs d'images vectorielles. A l'issue de ces trois mois de stage, la mission qui m'était attribuée a été remplie puisque les méthodes d'édition indispensables sont dorénavant intégrées. Certains modules pourront reservir pour d'autres fonctionnalités d'Amaya telle que l'édition des images mappées. L'outil est actuellement utilisable comme en témoigne les schémas que j'ai réalisé dans mon rapport. Il reste toutefois quelques bogues mineurs à corriger et des fonctionnalités dont on peut espérer qu'elles seront améliorées :
En contrepartie, un certain nombre de fonctionnalités inédites et de résultats nouveaux ont été mis en place dans Amaya. Rappelons que les deux derniers algorithmes sont "en temps constant" et peuvent être désactivés en éditant un fichier de configuration.
transform en un
produit d'au plus trois transformations SVG simples. Si la transformation
est une similitude, elle est décomposée en produit d'au plus un
translate, rotate et d'un scale.Ce stage a été pour moi l'occasion de travailler sur un gros projet, fruit de dix ans de travail de recherche pour Amaya et encore davantage pour Thot. J'ai ainsi pu mettre en pratique les connaissances que j'avais acquises au cours de ma première année à l'ENSIIE. Le sujet de stage permettait une liste de fonctionnalités d'édition assez vaste et j'ai pu constaster l'importance de la phase d'analyse en effectuant l'état de l'art et en dressant le plan de travail. J'ai pu expérimenter davantage le travail en équipe et l'usage d'un gestionnaire de conflit comme cvs. Le fait qu'Amaya soit un gros projet m'a montré l'importance des outils comme gdb pour ne pas être perdu dans le code. Amaya possède une liste de diffusion ouverte avec une communauté active d'utilisateurs. Les membres du W3C et du projet PALETTE sont aussi des testeurs potentiels. Cette confrontation avec des utilisateurs m'a sensibilisé aux problèmes d'utilisabilité du logiciel.
En ce qui concerne les connaissances théoriques, j'ai pu approfondir un peu plus ce que je savais du langage XML. La programmation des fonctionnalités classiques d'édition SVG m'ont permis d'étudier plus en profondeur ce langage. J'ai aussi pu travaillé sur des sujets de développement actuels comme le mélange de plusieurs grammaires XML, en prenant le cas de l'édition du SVG avec du MathML/XHTML. Enfin j'ai fait un usage important des mathématiques pour l'analyse et la programmation d'algorithmes non triviaux.
Pour toutes les transformations opérées sur un objet SVG, il est nécessaire de déterminer une boite englobante. Lorsque j'ai réalisé les modules de transformations, j'ai remarqué que le calcul de cette boite n'était pas effectué pour les chemins et ai donc commencé à le programmer. Il s'est averé par la suite que ce calcul était déjà fait lors du tracé des chemins : les courbes sont approximées par des segments de droites et il suffit de prendre le min et le max (horizontaux et verticaux) des différents sommets. Néanmoins voici le travail que j'ai effectué pour l'encadrement de courbe de Bézier.
Les courbes de Bézier peuvent être décrites comme des courbes paramètrées par . Comme elle utilise la notion de barycentre, les coordonnées des points de la courbe sont des fonctions de même forme. Donnons-les pour l'abscisse (les sont les abcisses des points de controle de la courbe) :
Les dérivées de ces fonctions sont et comme on peut le vérifier avec le programme maxima suivant :
f2(t) := a2*t^2 + 2*a1*(1-t)*t + a0*(1-t)^2; diff(f2(t), t); partfrac(%, t); f3(t) := a3*t^3+3*a2*(1-t)*t^2+3*a1*(1-t)^2*t+a0*(1-t)^3; diff(f3(t), t); partfrac(%, t);
Il ne reste ensuite plus qu'à regarder les min/max pour des valeurs de qui annulent les dérivées. Ces valeurs sont faciles à trouver par un algorithme de résolution d'équation du second degré. La méthode est beaucoup plus économique que celle consistant à approximer la courbe par un polygone. Cependant en pratique l'approximation par un polygone se réalise très rapidement et est de toute façon indispensable au tracé de la courbe ou encore pour vérifier que l'utilisateur clique sur un point de la courbe.
Dans cette partie, on reprend les notations de l'appendix F.6 "Elliptical arc implementation notes" de la spécification SVG [14]. Un chemin SVG, un arc est un fragment d'ellipse décrit par les paramètres suivants :
La spécification donne une paramétrisation alternative des coordonnées des points de l'arc pour en faciliter le tracé :
où θ varie entre deux angles et et est le centre de l'ellipse.
Lorsque l'utilisateur édite le chemin en déplaçant le point en on souhaite mettre à jour les paramètres de façon à conserver une forme d'arc similaire. Les angles de départ et d'arrivée et sont donc inchangés (les booléens et aussi). Notons et supposons que et . On agrandit l'ellipse par le rapport (qui est bien défini et non nul par les hypothèses précédentes), ce qui revient à multiplier les demi-axes et par ce coefficient. Il reste donc à trouver la bonne orientation de l'ellipse. On note les nouvelles coordonnées du centre de l'ellipse recherchée.
Il existe au plus un angle ψ (modulo ) satisfaisant les conditions :
Démonstration :
On note et . Supposons qu'un tel angle existe et commençons par éliminer les centres des ellipses par soustraction : et , d'où et enfin .
On peut réécrire cette égalité de façon équivalente, en posant des coefficents qui conviennent : . On obtient alors une équation d'inconnues et : . Comme est non nul par hypothèse, le déterminant du système est strictement positif et on obtient une solution . L'angle , s'il existe est donc entièrement déterminé par son cosinus et son sinus . □
Remarquons que l'on peut expliciter à partir des données initiales (). Le programme Maxima suivant calcule la valeur de :
p1 : matrix([x1], [y1]); p2 : matrix([x2], [y2]); p3 : matrix([x3], [y3]); k : sqrt(((x1-x3)^2+(y1-y3)^2)/((x1-x2)^2+(y1-y2)^2)); M(f) := matrix( cos(f),-sin(f)], sin(f),cos(f)] ); a: (k*(p1-p2))[1][1]; b: (k*(p1-p2))[2][1]; Y: (M(f).(p1-p3)); A : matrix( [a,-b], [b,a] ); X : invert(A).Y; x : X[1][1]; y : X[2][1]; trigsimp(radcan(x^2 + y^2));
Le programme trouve effectivement ce qui permet de toujours calculer une valeur . Je n'ai pas vérifié la réciproque à savoir que l'angle trouvé permettait bien de tracer un fragment d'arc de à sous les conditions désirées. J'ai tenté d'introduire cette mise à jour dans mon module d'édition, mais le résultat n'est pas toujours satisfaisant et l'arc dégénère parfois (en ligne ou en point) sûrement à cause de l'accumulation des erreurs d'arrondis.
Le travail que j'ai effectué dans Amaya m'a conduit à regarder d'autres projets utilisant SVG pour assurer la compatibilité des différents outils et leurs conformités vis-à-vis de la spécification.
Dans la description d'un arc elliptique entre deux points, il se peut que les rayons soient trop petits. L'appendix F.6.6 de [14] indique qu'il faut alors appliquer une homothétie sur l'arc pour que la jonction puisse se faire. Si un des rayons est nul, on transforme l'arc en ligne droite. A titre d'exemple, le fichier XML décrit trois demi-arcs de cercle : un bleu en haut, un noir au milieu que l'on transforme en ligne droite et enfin un rouge en bas que l'on ajuste à la bonne taille.
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="500" height="500">
<title>Test F.6.6 - Correction of out-of-range radii</title>
<!-- blue arc: rx = ry = 250. -->
<path d="M 0,250 A 250,250 0 0 1 500,250" fill="none" stroke="blue"/>
<!-- black arc: rx = ry = 0.
It's actually a straight line from (0, 250) to (500, 250)
-->
<path d="M 0,250 A 0,0 0 0 0 500,250" fill="none" stroke="black"/>
<!-- red arc: rx = ry = 1.
Arc is not large enough. Take rx = ry = 250
-->
<path d="M 0,250 A 1,1 0 0 0 500,250" fill="none" stroke="red"/>
</svg>
Dans Amaya la correction des rayons était bien effectuée mais aucun tracé n'était réalisé si un des rayons était nul. De plus l'algorithme de tracé utilisait un arccosinus qui pouvait être indéfini : l'erreur de calcul avec les nombres flottants conduisait à des arguments légèrement inférieur à -1 ou supérieur à +1. Cela se traduisait dans le tracé par une ligne droite. Enfin, les rayons des arcs n'étaient pas agrandis avec le zoom. Des utilisateurs ont reporté ces problèmes sur la liste de diffusion d'Amaya, ce qui m'a amené à les corriger.
En réalisant des dessins SVG, j'avais aussi remarqué ce type d'erreurs dans le visionneur d'image de Gnome. Ainsi, dans le fichier XML ci-dessus, seul l'arc bleu est correctement affiché car aucune correction des rayons n'est effectuée. Le problème vient en réalité de Librsvg, une librairie utilisée dans d'autres projets. Grâce à l'expérience acquise avec Amaya, j'ai pu proposer un patch aux développeurs de Librsvg.
Les <foreignObject/> comportent un attribut
requiredExtensions qui sert à indiquer les capacités necessaires
pour afficher l'élément. J'ai néanmoins constaté un problème de
compatibilité, du au fait que les spécifications ne précisent pas les
valeurs de l'attribut requiredExtensions à utiliser avec le XHTML
et le MathML. L'élément <svg/> dans le fichier XHTML
ci-dessous contient ainsi un <foreignObject/> avec une
fraction écrite en MathML et un texte "a/b" en SVG. En théorie, l'URI MathML
dans le requiredExtensions indique les capacités nécessaires
pour afficher l'objet étranger. Si le navigateur ne les possède pas il
affichera le texte alternatif.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN"
"http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type"
content="application/xhtml+xml; charset=UTF-8" />
<title>Test requiredExtensions</title>
<meta name="generator" content="Amaya, see http://www.w3.org/Amaya/" />
</head>
<body>
<svg:svg xmlns:svg="http://www.w3.org/2000/svg" width="120" height="120">
<svg:switch>
<svg:foreignObject width="120" height="120"
requiredExtensions="http://www.w3.org/1998/Math/MathML">
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mfrac bevelled="true">
<mi>a</mi>
<mi>b</mi>
</mfrac>
</math>
</svg:foreignObject>
<svg:text>a/b</svg:text>
</svg:switch>
</svg:svg>
</body>
</html>
Amaya génère et affiche correctement ce type de structure. Toutefois,
Firefox n'affiche pas l'objet étranger si un attribut
requiredExtensions lui est attaché, quelle que soit la valeur de
l'attribut. Par conséquent dans l'exemple, il n'affichera pas le MathML même
s'il en est capable !
J'ai reporté ce problème au groupe de travail SVG du W3C. Je leur ai
proposé de préciser dans la spécification que les URI du MathML et du XHTML
devaient être utilisés dans l'attribut requiredExtensions. J'ai
aussi soumis un patch pour que Gecko reconnaisse ces valeurs.
Rapport de stage deuxième année ISIMA (2002) - Étienne Bonnet
W3C's Editor/Browser
W3C Working Draft 9 August 2002 - 石川 雅康 (Ishikawa Masayasu).
W3C Recommendation 3 February 2000
Jutta Treviranus, Charles McCathieNevile, Ian Jacobs and Jan Richards
Article Wikipedia
A diagram creation program
Paper SVG.Open/Carto.net 2002, Zurich - Paul Cheyrou-Lagrèze
W3C Note 01 July 2003
Michael Kohlhase and David Carlisle
Open Source Scalable Vector Graphics Editor
A Computer Algebra System
The free and open productivity suite
Pedagogically sustained Adaptive Learning through the Exploitation of Tacit and Explicit Knowledge
W3C Recommendation 14 January 2003