Преглед изворни кода

Fix RTL text direction rendering (#1687)

Co-authored-by: Lipis <lipiridis@gmail.com>
Youness Fkhach пре 5 година
родитељ
комит
a118bed82f
4 измењених фајлова са 41 додато и 10 уклоњено
  1. 0 1
      src/data/index.ts
  2. 29 9
      src/renderer/renderElement.ts
  3. 2 0
      src/scene/export.ts
  4. 10 0
      src/utils.ts

+ 0 - 1
src/data/index.ts

@@ -330,7 +330,6 @@ export const exportCanvas = async (
     shouldAddWatermark,
   });
   tempCanvas.style.display = "none";
-  document.body.appendChild(tempCanvas);
 
   if (type === "png") {
     const fileName = `${name}.png`;

+ 29 - 9
src/renderer/renderElement.ts

@@ -14,7 +14,13 @@ import { Drawable, Options } from "roughjs/bin/core";
 import { RoughSVG } from "roughjs/bin/svg";
 import { RoughGenerator } from "roughjs/bin/generator";
 import { SceneState } from "../scene/types";
-import { SVG_NS, distance, getFontString, getFontFamilyString } from "../utils";
+import {
+  SVG_NS,
+  distance,
+  getFontString,
+  getFontFamilyString,
+  isRTL,
+} from "../utils";
 import { isPathALoop } from "../math";
 import rough from "roughjs/bin/rough";
 
@@ -38,6 +44,11 @@ const generateElementCanvas = (
   const canvas = document.createElement("canvas");
   const context = canvas.getContext("2d")!;
 
+  // To be able to draw a nested element with RTL, we have to append it to the DOM
+  canvas.style.display = "none";
+  canvas.id = "nested-canvas-element";
+  document.body.appendChild(canvas);
+
   let canvasOffsetX = 0;
   let canvasOffsetY = 0;
 
@@ -100,12 +111,14 @@ const drawElementOnCanvas = (
     }
     default: {
       if (isTextElement(element)) {
+        context.canvas.setAttribute("dir", isRTL(element.text) ? "rtl" : "ltr");
         const font = context.font;
         context.font = getFontString(element);
         const fillStyle = context.fillStyle;
         context.fillStyle = element.strokeColor;
         const textAlign = context.textAlign;
         context.textAlign = element.textAlign as CanvasTextAlign;
+
         // Canvas does not support multiline text by default
         const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
         const lineHeight = element.height / lines.length;
@@ -119,7 +132,7 @@ const drawElementOnCanvas = (
         for (let i = 0; i < lines.length; i++) {
           context.fillText(
             lines[i],
-            0 + horizontalOffset,
+            horizontalOffset,
             (i + 1) * lineHeight - verticalOffset,
           );
         }
@@ -342,6 +355,12 @@ const drawElementFromCanvas = (
   context.rotate(-element.angle);
   context.translate(-cx, -cy);
   context.scale(window.devicePixelRatio, window.devicePixelRatio);
+
+  // Clear the nested element we appended to the DOM
+  const node = document.querySelector("#nested-canvas-element");
+  if (node && node.parentNode) {
+    node.parentNode.removeChild(node);
+  }
 };
 
 export const renderElement = (
@@ -375,9 +394,12 @@ export const renderElement = (
     case "draw":
     case "arrow":
     case "text": {
-      const elementWithCanvas = generateElement(element, generator, sceneState);
-
       if (renderOptimizations) {
+        const elementWithCanvas = generateElement(
+          element,
+          generator,
+          sceneState,
+        );
         drawElementFromCanvas(elementWithCanvas, rc, context, sceneState);
       } else {
         const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
@@ -487,17 +509,14 @@ export const renderElementToSvg = (
         const lineHeight = element.height / lines.length;
         const verticalOffset = element.height - element.baseline;
         const horizontalOffset =
-          element.textAlign === "center"
-            ? element.width / 2
-            : element.textAlign === "right"
-            ? element.width
-            : 0;
+          element.textAlign === "center" ? element.width / 2 : element.width;
         const textAnchor =
           element.textAlign === "center"
             ? "middle"
             : element.textAlign === "right"
             ? "end"
             : "start";
+        const direction = isRTL(element.text) ? "rtl" : "ltr";
         for (let i = 0; i < lines.length; i++) {
           const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
           text.textContent = lines[i];
@@ -508,6 +527,7 @@ export const renderElementToSvg = (
           text.setAttribute("fill", element.strokeColor);
           text.setAttribute("text-anchor", textAnchor);
           text.setAttribute("style", "white-space: pre;");
+          text.setAttribute("direction", direction);
           node.appendChild(text);
         }
         svgRoot.appendChild(node);

+ 2 - 0
src/scene/export.ts

@@ -32,6 +32,8 @@ export const exportToCanvas = (
     const tempCanvas = document.createElement("canvas");
     tempCanvas.width = width * scale;
     tempCanvas.height = height * scale;
+    // We append the canvas before drawing it to it for RTL to work
+    document.body.appendChild(tempCanvas);
     return tempCanvas;
   },
 ) => {

+ 10 - 0
src/utils.ts

@@ -227,3 +227,13 @@ export const sceneCoordsToViewportCoords = (
 
 export const getGlobalCSSVariable = (name: string) =>
   getComputedStyle(document.documentElement).getPropertyValue(`--${name}`);
+
+export const isRTL = (text: string) => {
+  const ltrChars =
+    "A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF" +
+    "\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF";
+  const rtlChars = "\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC";
+  const rtlDirCheck = new RegExp(`^[^${ltrChars}]*[${rtlChars}]`);
+
+  return rtlDirCheck.test(text);
+};