Skip to content

[vector_graphics] SVG <text> is not drawn when the ambient Directionality is RTL #195182

Closed
flutter/packages
#13408

Description

@francisrath

What package does this bug report belong to?

vector_graphics

What target platforms are you seeing this bug on?

Android, iOS, macOS

Have you already upgraded your packages?

Yes

Dependency versions

pubspec.lock (relevant entries)
flutter_svg: 2.3.0
vector_graphics: 1.2.4
vector_graphics_codec: 1.1.13
vector_graphics_compiler: 1.3.2

Also reproduced on flutter/packages main at b7ec2d0 (vector_graphics 1.2.4).

Steps to reproduce

  1. Render any SVG that contains <text> with SvgPicture.string (or VectorGraphic) inside Directionality(textDirection: TextDirection.rtl). Any app whose locale is Arabic, Hebrew, Persian or Urdu does this automatically.
  2. Compare it with the same SVG under LTR (code sample below).

Expected results

The text renders at its x/y in both directions, as it does in browsers. An SVG's text position comes from its own coordinates and text-anchor, not from the host app's layout direction.

Actual results

Under RTL, no <text> is drawn at all. Shapes still render. We found this through support reports from Arabic-speaking users where important texts were missing from illustrations.

Cause: FlutterVectorGraphicsListener.onDrawText (packages/vector_graphics/lib/src/listener.dart) builds each paragraph with ParagraphStyle(textDirection: _textDirection), which is the ambient direction, and lays it out with ParagraphConstraints(width: double.infinity). The default TextAlign.start resolves to right under RTL, so the line sits at the right edge of an infinitely wide box, at x = +∞. drawParagraph is then given the correct Offset, but the glyphs are infinitely far to the right of it.

The listener already does the horizontal positioning itself (x, dx, and text-anchor through maxIntrinsicWidth). The paragraph's own alignment should therefore be neutral, i.e. TextAlign.left, whatever the direction.

Workaround: wrap the SVG in Directionality(textDirection: TextDirection.ltr).

Possible follow-up (not verified): the ambient direction also sets the paragraph's bidi base direction. SVG says the base direction comes from the direction property, which defaults to ltr. So mixed strings such as Weight: 2612 may reorder their neutral characters under an RTL app even after this fix. I'm happy to file that separately if maintainers agree it's in scope.

I have a fix with a test ready and will open a PR referencing this issue.

Code sample

Code sample
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';

const svg = '<svg width="200" height="60" xmlns="http://www.w3.org/2000/svg">'
    '<rect width="200" height="60" fill="#eee"/>'
    '<text x="10" y="40" font-size="24">Hello 123</text></svg>';

void main() => runApp(MaterialApp(
      home: Scaffold(
        body: Column(children: [
          const Directionality(
              textDirection: TextDirection.ltr, child: SvgPicture.string(svg)), // text visible
          const Directionality(
              textDirection: TextDirection.rtl, child: SvgPicture.string(svg)), // grey box, no text
        ]),
      ),
    ));
Failing unit test against vector_graphics main (b7ec2d0)
for (final ui.TextDirection direction in ui.TextDirection.values) {
  test('Text is drawn under ${direction.name}', () async {
    final Uint8List bytes = encodeSvg(
      xml: '<svg width="100" height="40"><text x="10" y="30" font-size="20">AB</text></svg>',
      debugName: 'text',
      enableClippingOptimizer: false,
      enableMaskingOptimizer: false,
      enableOverdrawOptimizer: false,
    );
    final PictureInfo info = await decodeVectorGraphics(
      bytes.buffer.asByteData(),
      locale: ui.PlatformDispatcher.instance.locale,
      textDirection: direction,
      clipViewbox: true,
      loader: const AssetBytesLoader('test'),
    );
    final ui.Image image = info.picture.toImageSync(100, 40);
    final Uint32List pixels = (await image.toByteData())!.buffer.asUint32List();
    expect(pixels.indexWhere((int p) => p != 0), isNot(-1)); // fails under rtl
  });
}

ltr passes; rtl fails with Expected: not <-1>, Actual: <-1>: not a single pixel is painted.

Screenshots or Videos

No response

Logs

No response

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.44.8, on macOS 27.0.1 26A434 darwin-arm64, locale en-US)
[!] Android toolchain - develop for Android devices (Android SDK version 36.1.0)
    ! Some Android licenses not accepted.
[!] Xcode - develop for iOS and macOS (Xcode 27.1)
    ! CocoaPods not installed.
[✓] Chrome - develop for the web
[✓] Connected device (4 available)
[✓] Network resources

Activity

  1. added
    a: internationalizationSupporting other languages or locales. (aka i18n)
    packageflutter/packages repository. See also p: labels.
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    on Oct 24, 2026
  2. mbcorona commented on Oct 24, 2026

    @mbcorona
    Member

    Hi @francisrath, thanks for the report and PR.
    Routing to team-engine for evaluation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    a: internationalizationSupporting other languages or locales. (aka i18n)has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: vector_graphicsThe vector_graphics package setpackageflutter/packages repository. See also p: labels.team-engineOwned by Engine team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions