blob: f2b213c731cd22ad851e51602457711579e467e5 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Uses the `ImageDecoder` class supplied by the browser.
//
// See also:
//
// * `image_wasm_codecs.dart`, which uses codecs supplied by the CanvasKit WASM bundle.
import 'dart:async';
import 'dart:convert' show base64;
import 'dart:js_interop';
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
/// Image decoder backed by the browser's `ImageDecoder`.
class CkBrowserImageDecoder extends BrowserImageDecoder {
CkBrowserImageDecoder._({
required super.contentType,
required super.dataSource,
required super.debugSource,
});
static Future<CkBrowserImageDecoder> create({
required Uint8List data,
required String debugSource,
}) async {
// ImageDecoder does not detect image type automatically. It requires us to
// tell it what the image type is.
final String? contentType = detectContentType(data);
if (contentType == null) {
final String fileHeader;
if (data.isNotEmpty) {
fileHeader = '[${bytesToHexString(data.sublist(0, math.min(10, data.length)))}]';
} else {
fileHeader = 'empty';
}
throw ImageCodecException(
'Failed to detect image file format using the file header.\n'
'File header was $fileHeader.\n'
'Image source: $debugSource'
);
}
final CkBrowserImageDecoder decoder = CkBrowserImageDecoder._(
contentType: contentType,
dataSource: data.toJS,
debugSource: debugSource,
);
// Call once to initialize the decoder and populate late fields.
await decoder.initialize();
return decoder;
}
@override
ui.Image generateImageFromVideoFrame(VideoFrame frame) {
final SkImage? skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo(
frame,
SkPartialImageInfo(
alphaType: canvasKit.AlphaType.Premul,
colorType: canvasKit.ColorType.RGBA_8888,
colorSpace: SkColorSpaceSRGB,
width: frame.displayWidth,
height: frame.displayHeight,
),
);
if (skImage == null) {
throw ImageCodecException(
"Failed to create image from pixel data decoded using the browser's ImageDecoder.",
);
}
return CkImage(skImage, videoFrame: frame);
}
}
Future<ByteData> readPixelsFromVideoFrame(VideoFrame videoFrame, ui.ImageByteFormat format) async {
if (format == ui.ImageByteFormat.png) {
final Uint8List png = await encodeVideoFrameAsPng(videoFrame);
return png.buffer.asByteData();
}
final ByteBuffer pixels = await readVideoFramePixelsUnmodified(videoFrame);
// Check if the pixels are already in the right format and if so, return the
// original pixels without modification.
if (_shouldReadPixelsUnmodified(videoFrame, format)) {
return pixels.asByteData();
}
// At this point we know we want to read unencoded pixels, and that the video
// frame is _not_ using the same format as the requested one.
final bool isBgrx = videoFrame.format == 'BGRX';
final bool isBgrFrame = videoFrame.format == 'BGRA' || isBgrx;
if (isBgrFrame) {
if (format == ui.ImageByteFormat.rawStraightRgba || isBgrx) {
_bgrToStraightRgba(pixels, isBgrx);
return pixels.asByteData();
} else if (format == ui.ImageByteFormat.rawRgba) {
_bgrToRawRgba(pixels);
return pixels.asByteData();
}
}
// Last resort, just return the original pixels.
return pixels.asByteData();
}
/// Mutates the [pixels], converting them from BGRX/BGRA to RGBA.
void _bgrToStraightRgba(ByteBuffer pixels, bool isBgrx) {
final Uint8List pixelBytes = pixels.asUint8List();
for (int i = 0; i < pixelBytes.length; i += 4) {
// It seems even in little-endian machines the BGR_ pixels are encoded as
// big-endian, i.e. the blue byte is written into the lowest byte in the
// memory address space.
final int b = pixelBytes[i];
final int r = pixelBytes[i + 2];
// So far the codec has reported 255 for the X component, so there's no
// special treatment for alpha. This may need to change if we ever face
// codecs that do something different.
pixelBytes[i] = r;
pixelBytes[i + 2] = b;
if (isBgrx) {
pixelBytes[i + 3] = 255;
}
}
}
/// Based on Chromium's SetRGBAPremultiply.
@pragma('dart2js:tryInline')
int _premultiply(int value, int alpha) {
if (alpha == 255) {
return value;
}
const int kRoundFractionControl = 257 * 128;
return (value * alpha * 257 + kRoundFractionControl) >> 16;
}
/// Mutates the [pixels], converting them from BGRX/BGRA to RGBA with
/// premultiplied alpha.
void _bgrToRawRgba(ByteBuffer pixels) {
final Uint8List pixelBytes = pixels.asUint8List();
for (int i = 0; i < pixelBytes.length; i += 4) {
final int a = pixelBytes[i + 3];
final int r = _premultiply(pixelBytes[i + 2], a);
final int g = _premultiply(pixelBytes[i + 1], a);
final int b = _premultiply(pixelBytes[i], a);
pixelBytes[i] = r;
pixelBytes[i + 1] = g;
pixelBytes[i + 2] = b;
}
}
bool _shouldReadPixelsUnmodified(VideoFrame videoFrame, ui.ImageByteFormat format) {
if (format == ui.ImageByteFormat.rawUnmodified) {
return true;
}
// Do not convert if the requested format is RGBA and the video frame is
// encoded as either RGBA or RGBX.
final bool isRgbFrame = videoFrame.format == 'RGBA' || videoFrame.format == 'RGBX';
return format == ui.ImageByteFormat.rawStraightRgba && isRgbFrame;
}
Future<ByteBuffer> readVideoFramePixelsUnmodified(VideoFrame videoFrame) async {
final int size = videoFrame.allocationSize().toInt();
// In dart2wasm, Uint8List is not the same as a JS Uint8Array. So we
// explicitly construct the JS object here.
final JSUint8Array destination = createUint8ArrayFromLength(size);
final JSPromise<JSAny?> copyPromise = videoFrame.copyTo(destination);
await promiseToFuture<void>(copyPromise);
// In dart2wasm, `toDart` incurs a copy here. On JS backends, this is a
// no-op.
return destination.toDart.buffer;
}
Future<Uint8List> encodeVideoFrameAsPng(VideoFrame videoFrame) async {
final int width = videoFrame.displayWidth.toInt();
final int height = videoFrame.displayHeight.toInt();
final DomCanvasElement canvas = createDomCanvasElement(width: width, height:
height);
final DomCanvasRenderingContext2D ctx = canvas.context2D;
ctx.drawImage(videoFrame, 0, 0);
final String pngBase64 = canvas.toDataURL().substring('data:image/png;base64,'.length);
return base64.decode(pngBase64);
}