From a118bed82f898a365cf5f71ed32f69f51fd08a47 Mon Sep 17 00:00:00 2001 From: Youness Fkhach Date: Tue, 2 Jun 2020 19:31:34 +0100 Subject: [PATCH] Fix RTL text direction rendering (#1687) Co-authored-by: Lipis --- src/data/index.ts | 1 - src/renderer/renderElement.ts | 38 ++++++++++++++++++++++++++--------- src/scene/export.ts | 2 ++ src/utils.ts | 10 +++++++++ 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/data/index.ts b/src/data/index.ts index f9c3bec1..1f5662c3 100644 --- a/src/data/index.ts +++ b/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`; diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index a965900d..70b4a7a9 100644 --- a/src/renderer/renderElement.ts +++ b/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); diff --git a/src/scene/export.ts b/src/scene/export.ts index 3caa6bcc..f737fd3a 100644 --- a/src/scene/export.ts +++ b/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; }, ) => { diff --git a/src/utils.ts b/src/utils.ts index b0224a59..f81677e2 100644 --- a/src/utils.ts +++ b/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); +};