Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

how to convert the flutter_quill textData with images into html with encoding the images into base64 #1574

Open
1 task done
rajcrack opened this issue Dec 7, 2023 · 5 comments
Labels
help wanted Extra attention is needed

Comments

@rajcrack
Copy link

rajcrack commented Dec 7, 2023

Is there an existing issue for this?

The question

this is my used packages with flutter_quill_extensions: ^0.7.2
vsc_quill_delta_to_html: ^1.0.3, flutter_quill: ^8.6.4 ,

@rajcrack rajcrack added the help wanted Extra attention is needed label Dec 7, 2023
@EchoEllet
Copy link
Collaborator

Hi, there is already a package called quill_html_converter
it uses vsc_quill_delta_to_html to convert the delta to HTML but the converting back from HTML to delta is experimental

@alexrabin-sentracam
Copy link

@ellet0 I don't think you answered @rajcrack's question. The quill_html_converter packages does convert the delta to html, however the toHtml() function doesn't convert the image file path to base64. This issue should be reopened.

@EchoEllet EchoEllet reopened this Mar 27, 2024
@EchoEllet
Copy link
Collaborator

EchoEllet commented Mar 27, 2024

HTML conversation is out of scope and is not a priority of the project. You can read the image files, convert them manually, and replace the image URLs.

@toknT
Copy link

toknT commented Jun 21, 2024

@ellet0 Yeah I write some code to convert the image file in local cache to base64 before user upload rich text to server. But it may be optional feat in flutter_quill_extensions.An other package quill_html_editor can covert image to base64 just select them.

@AlexBeehave
Copy link

AlexBeehave commented Oct 25, 2024

The #quill_html_editor has other issues that might be blocking for some users (like me). It renders in an iFrame so creates issue for popup and even select box that have overlay over the iFrame.

I use successfully flutter_quill_extensions, vsc_quill_delta_to_html, flutter_quill to keep data in HTML (because I use it to send emails), including img in base 64.

My method delta to HTML looks like this:

// Key is the id of the image (usually the URL blob:.... given in quill), the value is the base64 value of the image
  Map<String, String> cachedImages = {};
  // RegEx to match img html tag and capture the blob value to then replace with base64 value
  final RegExp regexBlob = RegExp(r'src="(blob:[^"]*)"');

String deltaToHTML(QuillController controller) {
    final deltaJson = controller.document.toDelta().toJson();
    final converter = QuillDeltaToHtmlConverter(
        List.castFrom(deltaJson),
        ConverterOptions(
            multiLineHeader: false,
            converterOptions: OpConverterOptions(
                inlineStylesFlag: true,
                customCssStyles: (op) {
                  if (op.isImage()) {
                    // Fit images within restricted parent width.
                    return ['max-width: 100%', 'object-fit: contain'];
                  }
                  if (op.isBlockquote()) {
                    return ['border-left: 4px solid #ccc', 'padding-left: 16px'];
                  }
                  return null;
                })));

    // Function to replace the blob src with base64 value
    String finalHtml = converter.convert().replaceAllMapped(regexBlob, (match) {
      String blobValue = match.group(1)!;
      String? base64Value = cachedImages[blobValue];
      return 'src="data:image/png;base64,$base64Value"';
    });

    //print("Delta To HTML: \n\n" + deltaJson.toString() + "\n\n\n " + finalHtml);

    return finalHtml;
  }

And the cachedImages is a Map that is filled by a custom imageProviderBuilder given in the QuillEditorImageEmbedConfigurations:

QuillEditorImageEmbedConfigurations imageConfigurationEditor = QuillEditorImageEmbedConfigurations(
        shouldRemoveImageCallback: (imageUrl) async {
          cachedImages.remove(imageUrl);
          return true;
        },
        imageProviderBuilder: (context, imageUrl) {
          // If the image is local (base64 doesn't contain :// or a Blob), we use our custom ImageProvider
          if (!imageUrl.contains("://") || imageUrl.contains("blob")) {
            return Base64ImageProvider(value: imageUrl, cache: cachedImages);
          } else {
            // To avoid CORS restriction for Flutter Web (Canvaskit) dealing with images with go through a proxy
            return NetworkImage("https://corsproxy.io/?$imageUrl");
          }
        }

and the custom imageProviderBuilder named Base64ImageProvider looks like this (each time a local image is provided, it keeps its base 64 representation in cache):

class Base64ImageProvider extends ImageProvider<Base64ImageProvider> {
  final String value; // Used to uniquely identify the image. Can be the blob URL or the base64 value
  final Map<String, String> cache;

  Base64ImageProvider({required this.value, required this.cache});

  @override
  Future<Base64ImageProvider> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<Base64ImageProvider>(this);
  }

  @override
  ImageStreamCompleter loadImage(Base64ImageProvider key, ImageDecoderCallback decode) {
    // Use `decode` to process the image bytes
    return OneFrameImageStreamCompleter(_loadAsync(decode));
  }

  // The async function that decodes the image bytes into an ImageInfo
  Future<ImageInfo> _loadAsync(ImageDecoderCallback decode) async {
    // Convert the base64 string into Uint8List (image bytes)
    Uint8List imageBytes = base64Decode(await getBase64());

    // Convert the image bytes to an ImmutableBuffer
    final ImmutableBuffer buffer = await ImmutableBuffer.fromUint8List(imageBytes);

    // Decode the ImmutableBuffer into a Codec
    final codec = await decode(buffer);

    // Get the first frame from the Codec
    final frame = await codec.getNextFrame();

    // Return the image as ImageInfo
    return ImageInfo(image: frame.image, scale: 1.0);
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    if (other.runtimeType != runtimeType) return false;
    final Base64ImageProvider typedOther = other as Base64ImageProvider;
    return value == typedOther.value;
  }

  @override
  int get hashCode => value.hashCode;

  Future<String> getBase64() async {
    if (!value.contains("blob")) {
      // it's already a base64 representation
      return value;
    }

    // value is a url like blob:http://etc.
    if (cache.containsKey(value)) {
      return SynchronousFuture(cache[value]!);
    }

    NetworkImage networkProvider = NetworkImage(value);
    ImageStream imageStream = networkProvider.resolve(ImageConfiguration());

    final completer = Completer<ui.Image>();

    // Listen to the ImageStream
    imageStream!.addListener(ImageStreamListener((ImageInfo info, bool synchronousCall) {
      completer.complete(info.image); // Retrieve the image from the ImageInfo
    }));

    // Wait for the image to be available
    ui.Image image = await completer.future;

    // Convert the image to ByteData
    ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);

    if (byteData != null) {
      // Convert ByteData to Uint8List
      Uint8List imageBytes = byteData.buffer.asUint8List();

      // Encode the Uint8List (image bytes) to base64
      cache[value] = base64Encode(imageBytes);
      return cache[value]!;
    } else {
      throw Exception('Failed to convert image to ByteData');
    }
  }
}

Hope this helps.

Repository owner locked as resolved and limited conversation to collaborators Oct 26, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

5 participants