4 points par GN⁺ 2024-12-04 | 1 commentaires | Partager sur WhatsApp
  • Skia Canvas permet d’utiliser dans Node.js l’API de dessin HTML Canvas pour le rendu à l’écran et hors écran, et, comme il repose sur Skia de Google, il produit une sortie très proche de celle de <canvas> dans Chrome
  • Les rendus peuvent être exportés en formats vectoriels comme PDF·SVG et en formats bitmap comme JPEG·PNG·WEBP, avec une sortie possible vers un fichier, une dataURL, un Buffer ou un objet Sharp
  • Il couvre aussi des domaines au-delà du Canvas du navigateur, avec le rendu GUI Window, un framework d’événements, des Canvas multipages, des opérations sur les chemins, des transformations en perspective 3D, les filtres CSS et un contrôle typographique avancé
  • Le rendu asynchrone et les E/S de fichiers sont traités sur des threads natifs via un pool de workers configurable par l’utilisateur, ce qui convient bien à la génération d’images côté serveur et aux environnements serverless
  • Les benchmarks mesurent séparément les modes serial et async ; en async, plusieurs tâches répétées sont lancées simultanément pour comparer l’effet du multithreading

Implémentation Canvas pour Node.js

  • Skia Canvas est une implémentation de l’API de dessin HTML Canvas pour Node.js, qui prend en charge à la fois le rendu à l’écran et hors écran
  • Elle s’appuie sur le moteur graphique Google Skia pour produire des résultats très proches de l’élément Chrome <canvas>
  • Elle inclut aussi des fonctionnalités que le Canvas du navigateur ne propose pas encore

Formats de sortie et modes d’export

  • La sortie d’image prend en charge à la fois les formats vectoriels et les formats bitmap
    • Vectoriel : PDF, SVG
    • Bitmap : JPEG, PNG, WEBP
  • Le résultat du Canvas peut être envoyé vers plusieurs destinations
  • Il est possible de dessiner directement dans une Window GUI interactive, avec un framework d’événements proche de celui du navigateur

Rendu asynchrone et multipage

  • Le rendu et les E/S de fichiers s’exécutent de manière asynchrone dans un pool de workers utilisant des threads natifs
  • On peut créer plusieurs pages dans un même Canvas
    • Elles peuvent être exportées dans un seul PDF multipage
    • Elles peuvent aussi être enregistrées comme une séquence d’images répartie sur plusieurs fichiers

Fonctions graphiques au-delà du Canvas du navigateur

  • Les chemins de Bézier prennent en charge les opérations booléennes et l’interpolation point par point
  • Les transformations proposent, en plus du zoom, de la rotation et de la translation du Canvas standard, des transformations en perspective 3D
  • Pour le remplissage des formes, on peut utiliser non seulement des Pattern bitmap, mais aussi des Texture vectorielles
  • Il est possible d’ajouter des marker personnalisés au tracé des lignes
  • Le traitement d’image prend en charge l’ensemble des opérateurs CSS filter

Contrôle typographique

  • Le rendu de texte gère plusieurs lignes et le retour automatique à la ligne
  • Il fournit des métriques de texte ligne par ligne
  • Des fonctions OpenType comme small-caps et ligature peuvent être utilisées via la syntaxe standard font-variant
  • L’espacement des lettres, l’espacement des mots et l’interligne peuvent être contrôlés proportionnellement
  • Les variable fonts sont prises en charge, avec mappage transparent des valeurs de weight
  • Il est aussi possible d’utiliser des polices non système chargées depuis des fichiers locaux

Exemples d’utilisation

  • L’exemple de création de fichier image montre comment créer un Canvas, obtenir un contexte 2D avec getContext("2d"), puis dessiner un rectangle en utilisant un dégradé conique comme style de contour
    • saveAs("rainbox.png", {density:2}) enregistre une image haute densité
    • canvas.png s’utilise comme forme abrégée de canvas.toBuffer("png")
    • Le résultat de toDataURL("png") peut être inséré dans une chaîne HTML
    • saveAsSync("rainbox.pdf") permet aussi un enregistrement synchrone sur le thread principal
  • L’exemple de séquence multipage crée des pages de couleurs différentes avec canvas.newPage() avant de les enregistrer
    • Un PDF multipage est enregistré sous all-pages.pdf
    • Le motif page-{2}.png génère plusieurs fichiers comme page-01.png et page-02.png
  • L’exemple de rendu Window crée une Window(300, 300) et dessine, lors de l’événement draw, un cercle dont l’épaisseur de trait varie selon la valeur de frame
  • L’exemple d’intégration Sharp.js montre comment convertir le résultat du Canvas en objet Sharp pour du post-traitement, ou comment dessiner dans le Canvas une image créée par Sharp via loadImage

Méthode et résultats des benchmarks

  • Les benchmarks testent Skia Canvas selon deux modes
    • serial : chaque tâche de rendu est attendue avant de passer à l’itération suivante
    • async : toutes les itérations du test sont lancées en une fois et s’exécutent en parallèle grâce au support du multithreading
  • Les résultats complets sont disponibles sur la page de résultats de canvas-benchmarks
  • Le test de latence au démarrage, sur 100 itérations, montre que skia-canvas passe sous 1 ms par exécution, pour un total de 33 ms
    • canvaskit-wasm : 25 ms par exécution, 2,47 s au total
    • canvas : 88 ms par exécution, 8,77 s au total
    • @napi-rs/canvas : 69 ms par exécution, 6,87 s au total
  • Le test des courbes de Bézier, sur 20 itérations, met en évidence l’écart du traitement parallèle en mode async
    • skia-canvas serial : 137 ms par exécution, 2,74 s au total
    • skia-canvas async : 28 ms par exécution, 558 ms au total
    • Les comparatifs étaient de 790 ms pour canvaskit-wasm, 486 ms pour canvas et 230 ms pour @napi-rs/canvas
  • Le test SVG vers PNG, sur 100 itérations, n’était pas pris en charge par canvaskit-wasm ; skia-canvas a enregistré 58 ms par exécution en serial et 11 ms en async
    • skia-canvas serial : 5,83 s au total
    • skia-canvas async : 1,08 s au total
  • Résultats du test de mise à l’échelle et rotation d’image sur 50 itérations
    • skia-canvas serial : 100 ms par exécution, 5,00 s au total
    • skia-canvas async : 19 ms par exécution, 935 ms au total
  • Résultats du test de texte de base sur 200 itérations
    • skia-canvas serial : 21 ms par exécution, 4,26 s au total
    • skia-canvas async : 4 ms par exécution, 819 ms au total

1 commentaires

 
GN⁺ 2024-12-04
Avis sur Hacker News
  • https://windowjs.org repose sur un concept très similaire
    Il encapsule Skia et l’expose via la Canvas API, mais au lieu de Node, il embarque v8 et fonctionne comme un runtime très léger
    C’était mon premier projet open source, publié il y a environ trois ans, et il y avait aussi l’idée d’en faire une plateforme de jeux JavaScript pour desktop en exposant WebGL, l’audio, etc., mais le développement s’est arrêté à cause d’autres travaux et projets
    C’est agréable de voir apparaître ce genre de projet qui permet d’utiliser Canvas même en dehors du navigateur

    • C’est plutôt chouette, mais si l’isolation n’est pas indispensable, un environnement avec seulement v8 laisse un peu sur sa faim
      Parfois, on veut juste faire une requête réseau
    • Beau projet
      Je me demande à quel point la taille du binaire diminue quand on n’embarque que V8 au lieu de Node.js
      Node.js utilise aussi V8, donc je me demande aussi si l’essentiel de la taille du binaire Node ne vient pas plutôt du runtime que de V8
      J’ai parcouru le site web et le dépôt GitHub, mais je n’ai pas trouvé combien de ko ou de Mo fait généralement le binaire Window.js
  • Par curiosité, je me demande à quoi servent ces bibliothèques
    Sur desktop, j’aurais tendance à penser qu’il existe de meilleures bibliothèques natives pour dessiner des formes

    • Chez Soundslice, nous utilisons une bibliothèque graphique de rendu de partitions sur mesure, écrite en JavaScript/Canvas côté front-end
      Nous devons aussi générer des PDF vectoriels côté serveur, donc nous utilisons une bibliothèque Node qui permet d’utiliser l’HTML Canvas API et de produire des PDF
      L’avantage, c’est que le résultat est exactement le même que les partitions rendues dans le navigateur web
      En fin de compte, ce type de bibliothèque permet de réutiliser du code dans des environnements qui ne sont pas des navigateurs
    • En dessin 2D, il n’y a en réalité pas grand-chose d’aussi puissant que Skia que l’on pourrait couramment qualifier de « natif »
      Le grand avantage de Skia est de tirer efficacement parti des graphismes natifs, et c’est justement pour cela qu’il est utilisé dans les moteurs de navigateur
      Et le fait de n’avoir qu’une seule API cible est aussi un avantage
    • La portabilité n’est-elle pas un avantage ? Je me demande quelles autres bibliothèques de rendu 2D accéléré par GPU existent
      D’un côté, ceci est plus bas niveau, mais de l’autre, il m’est déjà arrivé d’utiliser Puppeteer comme une bibliothèque graphique 2D
      On obtient Canvas 2D, WebGL, WebGPU, tout HTML/CSS, et on peut aussi utiliser les effets de texte, les images d’arrière-plan et les transformations CSS
      On peut charger des images et des vidéos, puis produire une image du résultat composé grâce à la fonction de capture d’écran
      C’est excessif, mais pour mon usage ce n’était pas un problème ; ça fonctionne, c’est multiplateforme et ça résout le problème
    • Je serais surpris si l’on pouvait citer une plateforme ayant une meilleure API de dessin graphique 2D que Canvas
      Skia est un peu plus rapide que Direct2D et bien plus rapide que Cairo
      Il est très optimisé et dispose aussi de backends accélérés par GPU
    • Pouvoir exécuter le même code sur plusieurs plateformes est un gros bonus
      Canvas est aussi une API de dessin 2D très répandue
      Il existe une version native sur iOS et une version native sur Android, et les opérations de dessin sont globalement similaires
      On peut en tirer beaucoup de valeur
  • Skia inclut CanvasKit, un build WASM qui prend en charge Node, https://www.npmjs.com/package/canvaskit-wasm ; ce module semble être des bindings Rust
    Je me demande quels sont les avantages et inconvénients des deux approches

    • Plutôt que des « bindings Rust », c’est davantage un binaire Node précompilé
      Il se trouve simplement qu’il est généré en interne à partir d’une base de code mixte Rust/C++
      La différence essentielle est donc que WASM nécessite un runtime séparé, alors que le binaire Node n’en a pas besoin
  • Est-ce une sorte de wrapper autour d’un crate Rust ?
    https://rust-skia.github.io/doc/skia_safe/canvas/struct.Canvas.html

    • Si je comprends bien, Skia Canvas peut fonctionner dans presque n’importe quel langage de programmation dès lors qu’il existe des bindings https://www.swig.org/
      Je ne vois pas bien ce que Node a de spécial
    • Oui, c’est généré avec napi-rs
  • J’attendais quelque chose comme ça depuis longtemps
    Les tentatives précédentes pour faire des choses similaires nécessitaient l’installation de Node-Gyp, et c’était un vrai cauchemar

  • C’est plus qu’une simple API de rendu : il est indiqué que cela « peut rendre dans une fenêtre via le pipeline graphique natif de l’OS et fournit un framework d’événements UI façon navigateur »
    On pourrait aussi ajouter wgpu pour la prise en charge de WebGPU, et ANGLE pour celle de WebGL

    • Skia est une bibliothèque largement utilisée ; parmi les exemples qui me viennent en tête, Chrome, Firefox et LibreOffice l’utilisent
      Elle prend en charge de nombreuses cibles de rendu, y compris WebGPU et ANGLE
    • C’est assez similaire à ce que Qt fournit déjà
      QPainter gère les images brutes et les fenêtres, et il existe aussi des widgets plus sophistiqués avec prise en charge des événements
      Et c’est une fonctionnalité que je cherchais depuis un moment : Qt dispose de graphicsview, qui permet de rendre indépendamment de la résolution des millions d’objets sur un plan graphique plus grand que la fenêtre, tout en gérant les événements
  • Si les implémentations de Canvas compatibles avec Node vous intéressent, il en existe quelques-unes
    canvaskit-wasm du projet Skia ne semble pas être accéléré par GPU : https://github.com/google/skia/tree/main/modules/canvaskit/npm_build
    @napi-rs/canvas est le binding le plus rapide : https://github.com/Brooooooklyn/canvas?tab=readme-ov-file#performance
    node-canvas utilise Cairo au lieu de Skia : https://github.com/Automattic/node-canvas

  • Je l’utilise actuellement avec Deno quand j’ai besoin d’une petite fenêtre graphique pour Advent of Code
    Il y a aussi un exemple avec fenêtre : https://skia-canvas.org/#rendering-to-a-window
    Il suffit de l’ajouter avec deno add npm:skia-canvas, de mettre "nodeModulesDir": "auto" dans deno.json, puis d’exécuter deno install --allow-scripts

  • Voir ça me rend vraiment enthousiaste
    J’ai l’impression que cela va permettre d’écrire beaucoup plus facilement des graphismes à la fois autonomes et compatibles avec le web
    Plus besoin non plus de construire depuis les sources avec un second langage de script et de générer du code pour compiler une bibliothèque destinée au premier langage de script
    Ce sera aussi intéressant de voir à quel point cela se prête au rendu en temps réel de code Canvas dans des endroits comme VS Code, ou à l’exécution de code doté d’une couche de conversion pour tourner sur le Canvas web