Implementing Dynamic Watermarks on Images and Videos in Flutter with FFmpeg Kit

Dynamic Watermark Generation

To ensure the watermark maintains consistent proportions across varying media resolutions, dynamically generate a watermark image using Flutter's canvas rendering engine. This approach combines an icon asset with custom text, converting the result into raw byte data.

import 'dart:ui' as ui;
import 'dart:math';
import 'package:flutter/services.dart';

static Future buildWatermarkGraphic({required String identifier, required String username}) async {
  double screenDimension = ui.window.physicalSize.width;
  ui.PictureRecorder recorder = ui.PictureRecorder();
  Canvas canvas = Canvas(recorder);

  ui.Image logo = await loadAssetImage('assets/logo.png');
  Paint drawPaint = Paint()..color = const Color(0xFF000000);

  double logoXPosition = screenDimension - logo.width - 20.0;
  canvas.drawImage(logo, Offset(logoXPosition, 0), drawPaint);

  ui.ParagraphBuilder paraBuilder = ui.ParagraphBuilder(ui.ParagraphStyle(
    textAlign: TextAlign.right,
    fontWeight: FontWeight.w600,
    fontSize: 22.0,
  ))..pushStyle(ui.TextStyle(color: Colors.white))
    ..addText('$username:$identifier');

  ui.Paragraph textParagraph = paraBuilder.build()..layout(ui.ParagraphConstraints(width: screenDimension - 25));
  canvas.drawParagraph(textParagraph, Offset(-5, logo.height + 15.0));

  ui.Image renderedImage = await recorder.endRecording().toImage(
    screenDimension.toInt() - 10,
    logo.height + 80,
  );

  ByteData? byteData = await renderedImage.toByteData(format: ui.ImageByteFormat.png);
  return byteData?.buffer.asUint8List();
}

static Future loadAssetImage(String assetPath) async {
  ByteData data = await rootBundle.load(assetPath);
  ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
  ui.FrameInfo frame = await codec.getNextFrame();
  return frame.image;
}

Integrating Watermark with Images

Applying the generated watermark to an image requires calculating the appropriate scale factor based on the source image dimensions. The FFmpeg overlay filter positions the watermark at the bottom-right corner. The command scales the watermark relative to the source dimensions before overlaying.

static Future<void> processImageWithWatermark(String sourceUrl, {String? uid, String? name}) async {
  if (uid == null || name == null) return;

  Directory tempDir = await getTemporaryDirectory();
  Uint8List? watermarkBytes = await buildWatermarkGraphic(identifier: uid, username: name);

  String watermarkPath = '${tempDir.path}/wm_overlay.png';
  await File(watermarkPath).writeAsBytes(watermarkBytes!);

  Size sourceSize = await retrieveImageDimensions(sourceUrl);
  Size watermarkSize = await retrieveImageDimensions(watermarkPath);

  double scaleFactor = min(sourceSize.width / watermarkSize.width, sourceSize.height / watermarkSize.height);
  int scaledW = (watermarkSize.width * scaleFactor).toInt();
  int scaledH = (watermarkSize.height * scaleFactor).toInt();

  String outputPath = '${tempDir.path}/img_${DateTime.now().millisecondsSinceEpoch}.png';

  String ffmpegCmd = "-i $sourceUrl -i $watermarkPath "
      "-filter_complex [1:v]scale=$scaledW:$scaledH[wm];[0:v][wm]overlay=main_w-overlay_w-15:main_h-overlay_h-15 -qscale:v 1 "
      "$outputPath";

  await FFmpegKit.executeAsync(ffmpegCmd, (session) async {
    ReturnCode? rc = await session.getReturnCode();
    if (ReturnCode.isSuccess(rc)) {
      // Save to gallery or handle output
    } else {
      // Handle processing failure
    }
  });
}

Integrating Watermark with Videos

Video processing follows a similar pattern but necessitates fetching the video's resolution using VideoPlayerController after downloading the file locally. The FFmpeg command includes audio codec copying to preserve the original sound quality without re-encoding.

static Future<void> processVideoWithWatermark(String videoUrl, {String? uid, String? name}) async {
  Directory tempDir = await getTemporaryDirectory();
  String localVideoPath = '${tempDir.path}/temp_video.mp4';

  await Dio().download(videoUrl, localVideoPath);

  if (uid == null || name == null) {
    // Save directly without watermark
    return;
  }

  VideoPlayerController controller = VideoPlayerController.file(File(localVideoPath));
  await controller.initialize();

  double videoWidth = controller.value.size.width;
  double videoHeight = controller.value.size.height;
  controller.dispose();

  Uint8List? watermarkBytes = await buildWatermarkGraphic(identifier: uid, username: name);
  String watermarkPath = '${tempDir.path}/wm_overlay.png';
  await File(watermarkPath).writeAsBytes(watermarkBytes!);

  Size wmSize = await retrieveImageDimensions(watermarkPath);
  double scaleFactor = min(videoWidth / wmSize.width, videoHeight / wmSize.height);
  int scaledW = (wmSize.width * scaleFactor).toInt();
  int scaledH = (wmSize.height * scaleFactor).toInt();

  String outputPath = '${tempDir.path}/vid_${DateTime.now().millisecondsSinceEpoch}.mp4';

  String ffmpegCmd = "-i $localVideoPath -i $watermarkPath "
      "-filter_complex [1:v]scale=$scaledW:$scaledH[wm];[0:v][wm]overlay=main_w-overlay_w-15:main_h-overlay_h-15 "
      "-acodec copy -q:v 0 -q:a 0 $outputPath";

  await FFmpegKit.executeAsync(ffmpegCmd, (session) async {
    ReturnCode? rc = await session.getReturnCode();
    if (ReturnCode.isSuccess(rc)) {
      // Handle successful export
    } else {
      // Handle failure
    }
  });
}

Tags: Flutter ffmpeg Watermark Video Processing Image Manipulation

Posted on Thu, 14 May 2026 03:29:14 +0000 by matthiasone