Blob Blame History Raw
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "imgTools.h"

#include "DecodePool.h"
#include "gfxUtils.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/gfx/Logging.h"
#include "mozilla/RefPtr.h"
#include "nsCOMPtr.h"
#include "nsIDocument.h"
#include "nsIDOMDocument.h"
#include "nsError.h"
#include "imgLoader.h"
#include "imgICache.h"
#include "imgIContainer.h"
#include "imgIEncoder.h"
#include "nsStreamUtils.h"
#include "nsStringStream.h"
#include "nsContentUtils.h"
#include "nsProxyRelease.h"
#include "ImageFactory.h"
#include "Image.h"
#include "ScriptedNotificationObserver.h"
#include "imgIScriptedNotificationObserver.h"
#include "gfxPlatform.h"
#include "jsfriendapi.h"

using namespace mozilla::gfx;

namespace mozilla {
namespace image {

namespace {

class ImageDecoderHelper final : public Runnable,
                                 public nsIInputStreamCallback {
 public:
  NS_DECL_ISUPPORTS_INHERITED

  ImageDecoderHelper(already_AddRefed<image::Image> aImage,
                     already_AddRefed<nsIInputStream> aInputStream,
                     nsIEventTarget* aEventTarget,
                     imgIContainerCallback* aCallback,
                     nsIEventTarget* aCallbackEventTarget)
      : Runnable("ImageDecoderHelper"),
        mImage(Move(aImage)),
        mInputStream(Move(aInputStream)),
        mEventTarget(aEventTarget),
        mCallback(aCallback),
        mCallbackEventTarget(aCallbackEventTarget),
        mStatus(NS_OK) {
    MOZ_ASSERT(NS_IsMainThread());
  }

  NS_IMETHOD
  Run() override {
    // This runnable is dispatched on the Image thread when reading data, but
    // at the end, it goes back to the main-thread in order to complete the
    // operation.
    if (NS_IsMainThread()) {
      // Let the Image know we've sent all the data.
      mImage->OnImageDataComplete(nullptr, nullptr, mStatus, true);

      RefPtr<ProgressTracker> tracker = mImage->GetProgressTracker();
      tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE);

      nsCOMPtr<imgIContainer> container;
      if (NS_SUCCEEDED(mStatus)) {
        container = do_QueryInterface(mImage);
      }

      mCallback->OnImageReady(container, mStatus);
      return NS_OK;
    }

    uint64_t length;
    nsresult rv = mInputStream->Available(&length);
    if (rv == NS_BASE_STREAM_CLOSED) {
      return OperationCompleted(NS_OK);
    }

    if (NS_WARN_IF(NS_FAILED(rv))) {
      return OperationCompleted(rv);
    }

    // Nothing else to read, but maybe we just need to wait.
    if (length == 0) {
      nsCOMPtr<nsIAsyncInputStream> asyncInputStream =
          do_QueryInterface(mInputStream);
      if (asyncInputStream) {
        rv = asyncInputStream->AsyncWait(this, 0, 0, mEventTarget);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return OperationCompleted(rv);
        }
        return NS_OK;
      }

      // We really have nothing else to read.
      if (length == 0) {
        return OperationCompleted(NS_OK);
      }
    }

    // Send the source data to the Image.
    rv = mImage->OnImageDataAvailable(nullptr, nullptr, mInputStream, 0,
                                      uint32_t(length));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return OperationCompleted(rv);
    }

    rv = mEventTarget->Dispatch(this, NS_DISPATCH_NORMAL);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return OperationCompleted(rv);
    }

    return NS_OK;
  }

  NS_IMETHOD
  OnInputStreamReady(nsIAsyncInputStream* aAsyncInputStream) override {
    MOZ_ASSERT(!NS_IsMainThread());
    return Run();
  }

  nsresult OperationCompleted(nsresult aStatus) {
    MOZ_ASSERT(!NS_IsMainThread());

    mStatus = aStatus;
    mCallbackEventTarget->Dispatch(this, NS_DISPATCH_NORMAL);
    return NS_OK;
  }

 private:
  ~ImageDecoderHelper() {
    NS_ReleaseOnMainThreadSystemGroup("ImageDecoderHelper::mImage",
                                      mImage.forget());
    NS_ReleaseOnMainThreadSystemGroup("ImageDecoderHelper::mCallback",
                                      mCallback.forget());
  }

  RefPtr<image::Image> mImage;

  nsCOMPtr<nsIInputStream> mInputStream;
  nsCOMPtr<nsIEventTarget> mEventTarget;
  nsCOMPtr<imgIContainerCallback> mCallback;
  nsCOMPtr<nsIEventTarget> mCallbackEventTarget;

  nsresult mStatus;
};

NS_IMPL_ISUPPORTS_INHERITED(ImageDecoderHelper, Runnable,
                            nsIInputStreamCallback)

}  // namespace

/* ========== imgITools implementation ========== */

NS_IMPL_ISUPPORTS(imgTools, imgITools)

imgTools::imgTools() { /* member initializers and constructor code */
}

imgTools::~imgTools() { /* destructor code */
}

NS_IMETHODIMP
imgTools::DecodeImageFromArrayBuffer(JS::HandleValue aArrayBuffer,
                                     const nsACString& aMimeType,
                                     JSContext* aCx,
                                     imgIContainer** aContainer) {
  if (!aArrayBuffer.isObject()) {
    return NS_ERROR_FAILURE;
  }

  JS::Rooted<JSObject*> obj(aCx,
                            js::UnwrapArrayBuffer(&aArrayBuffer.toObject()));
  if (!obj) {
    return NS_ERROR_FAILURE;
  }

  uint8_t* bufferData = nullptr;
  uint32_t bufferLength = 0;
  bool isSharedMemory = false;

  js::GetArrayBufferLengthAndData(obj, &bufferLength, &isSharedMemory,
                                  &bufferData);
  return DecodeImageFromBuffer((char*)bufferData, bufferLength, aMimeType,
                               aContainer);
}

NS_IMETHODIMP
imgTools::DecodeImageFromBuffer(const char* aBuffer, uint32_t aSize,
                                const nsACString& aMimeType,
                                imgIContainer** aContainer) {
  MOZ_ASSERT(NS_IsMainThread());

  NS_ENSURE_ARG_POINTER(aBuffer);

  // Create a new image container to hold the decoded data.
  nsAutoCString mimeType(aMimeType);
  RefPtr<image::Image> image =
      ImageFactory::CreateAnonymousImage(mimeType, aSize);
  RefPtr<ProgressTracker> tracker = image->GetProgressTracker();

  if (image->HasError()) {
    return NS_ERROR_FAILURE;
  }

  // Let's create a temporary inputStream.
  nsCOMPtr<nsIInputStream> stream;
  nsresult rv = NS_NewByteInputStream(getter_AddRefs(stream), aBuffer, aSize,
                                      NS_ASSIGNMENT_DEPEND);
  NS_ENSURE_SUCCESS(rv, rv);
  MOZ_ASSERT(stream);
  MOZ_ASSERT(NS_InputStreamIsBuffered(stream));

  rv = image->OnImageDataAvailable(nullptr, nullptr, stream, 0, aSize);
  NS_ENSURE_SUCCESS(rv, rv);

  // Let the Image know we've sent all the data.
  rv = image->OnImageDataComplete(nullptr, nullptr, NS_OK, true);
  tracker->SyncNotifyProgress(FLAG_LOAD_COMPLETE);
  NS_ENSURE_SUCCESS(rv, rv);

  // All done.
  image.forget(aContainer);
  return NS_OK;
}

NS_IMETHODIMP
imgTools::DecodeImageAsync(nsIInputStream* aInStr, const nsACString& aMimeType,
                           imgIContainerCallback* aCallback,
                           nsIEventTarget* aEventTarget) {
  MOZ_ASSERT(NS_IsMainThread());

  NS_ENSURE_ARG_POINTER(aInStr);
  NS_ENSURE_ARG_POINTER(aCallback);
  NS_ENSURE_ARG_POINTER(aEventTarget);

  nsresult rv;

  // Let's continuing the reading on a separate thread.
  DecodePool* decodePool = DecodePool::Singleton();
  MOZ_ASSERT(decodePool);

  RefPtr<nsIEventTarget> target = decodePool->GetIOEventTarget();
  NS_ENSURE_TRUE(target, NS_ERROR_FAILURE);

  // Prepare the input stream.
  nsCOMPtr<nsIInputStream> stream = aInStr;
  if (!NS_InputStreamIsBuffered(aInStr)) {
    nsCOMPtr<nsIInputStream> bufStream;
    rv = NS_NewBufferedInputStream(getter_AddRefs(bufStream), stream.forget(),
                                   1024);
    NS_ENSURE_SUCCESS(rv, rv);
    stream = bufStream.forget();
  }

  // Create a new image container to hold the decoded data.
  nsAutoCString mimeType(aMimeType);
  RefPtr<image::Image> image = ImageFactory::CreateAnonymousImage(mimeType, 0);

  // Already an error?
  if (image->HasError()) {
    return NS_ERROR_FAILURE;
  }

  RefPtr<ImageDecoderHelper> helper = new ImageDecoderHelper(
      image.forget(), stream.forget(), target, aCallback, aEventTarget);
  rv = target->Dispatch(helper.forget(), NS_DISPATCH_NORMAL);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

/**
 * This takes a DataSourceSurface rather than a SourceSurface because some
 * of the callers have a DataSourceSurface and we don't want to call
 * GetDataSurface on such surfaces since that may incure a conversion to
 * SurfaceType::DATA which we don't need.
 */
static nsresult EncodeImageData(DataSourceSurface* aDataSurface,
                                const nsACString& aMimeType,
                                const nsAString& aOutputOptions,
                                nsIInputStream** aStream) {
  MOZ_ASSERT(aDataSurface->GetFormat() == SurfaceFormat::B8G8R8A8,
             "We're assuming B8G8R8A8");

  // Get an image encoder for the media type
  nsAutoCString encoderCID(
      NS_LITERAL_CSTRING("@mozilla.org/image/encoder;2?type=") + aMimeType);

  nsCOMPtr<imgIEncoder> encoder = do_CreateInstance(encoderCID.get());
  if (!encoder) {
    return NS_IMAGELIB_ERROR_NO_ENCODER;
  }

  DataSourceSurface::MappedSurface map;
  if (!aDataSurface->Map(DataSourceSurface::MapType::READ, &map)) {
    return NS_ERROR_FAILURE;
  }

  IntSize size = aDataSurface->GetSize();
  uint32_t dataLength = map.mStride * size.height;

  // Encode the bitmap
  nsresult rv = encoder->InitFromData(
      map.mData, dataLength, size.width, size.height, map.mStride,
      imgIEncoder::INPUT_FORMAT_HOSTARGB, aOutputOptions);
  aDataSurface->Unmap();
  NS_ENSURE_SUCCESS(rv, rv);

  encoder.forget(aStream);
  return NS_OK;
}

NS_IMETHODIMP
imgTools::EncodeImage(imgIContainer* aContainer, const nsACString& aMimeType,
                      const nsAString& aOutputOptions,
                      nsIInputStream** aStream) {
  // Use frame 0 from the image container.
  RefPtr<SourceSurface> frame = aContainer->GetFrame(
      imgIContainer::FRAME_FIRST, imgIContainer::FLAG_SYNC_DECODE);
  NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);

  RefPtr<DataSourceSurface> dataSurface;

  if (frame->GetFormat() == SurfaceFormat::B8G8R8A8) {
    dataSurface = frame->GetDataSurface();
  } else {
    // Convert format to SurfaceFormat::B8G8R8A8
    dataSurface = gfxUtils::CopySurfaceToDataSourceSurfaceWithFormat(
        frame, SurfaceFormat::B8G8R8A8);
  }

  NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);

  return EncodeImageData(dataSurface, aMimeType, aOutputOptions, aStream);
}

NS_IMETHODIMP
imgTools::EncodeScaledImage(imgIContainer* aContainer,
                            const nsACString& aMimeType, int32_t aScaledWidth,
                            int32_t aScaledHeight,
                            const nsAString& aOutputOptions,
                            nsIInputStream** aStream) {
  NS_ENSURE_ARG(aScaledWidth >= 0 && aScaledHeight >= 0);

  // If no scaled size is specified, we'll just encode the image at its
  // original size (no scaling).
  if (aScaledWidth == 0 && aScaledHeight == 0) {
    return EncodeImage(aContainer, aMimeType, aOutputOptions, aStream);
  }

  // Retrieve the image's size.
  int32_t imageWidth = 0;
  int32_t imageHeight = 0;
  aContainer->GetWidth(&imageWidth);
  aContainer->GetHeight(&imageHeight);

  // If the given width or height is zero we'll replace it with the image's
  // original dimensions.
  IntSize scaledSize(aScaledWidth == 0 ? imageWidth : aScaledWidth,
                     aScaledHeight == 0 ? imageHeight : aScaledHeight);

  // Use frame 0 from the image container.
  RefPtr<SourceSurface> frame =
      aContainer->GetFrameAtSize(scaledSize, imgIContainer::FRAME_FIRST,
                                 imgIContainer::FLAG_HIGH_QUALITY_SCALING |
                                     imgIContainer::FLAG_SYNC_DECODE);
  NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);

  RefPtr<DataSourceSurface> dataSurface =
      Factory::CreateDataSourceSurface(scaledSize, SurfaceFormat::B8G8R8A8);
  if (NS_WARN_IF(!dataSurface)) {
    return NS_ERROR_FAILURE;
  }

  DataSourceSurface::MappedSurface map;
  if (!dataSurface->Map(DataSourceSurface::MapType::WRITE, &map)) {
    return NS_ERROR_FAILURE;
  }

  RefPtr<DrawTarget> dt = Factory::CreateDrawTargetForData(
      BackendType::CAIRO, map.mData, dataSurface->GetSize(), map.mStride,
      SurfaceFormat::B8G8R8A8);
  if (!dt) {
    gfxWarning() << "imgTools::EncodeImage failed in CreateDrawTargetForData";
    return NS_ERROR_OUT_OF_MEMORY;
  }

  IntSize frameSize = frame->GetSize();
  dt->DrawSurface(frame, Rect(0, 0, scaledSize.width, scaledSize.height),
                  Rect(0, 0, frameSize.width, frameSize.height),
                  DrawSurfaceOptions(),
                  DrawOptions(1.0f, CompositionOp::OP_SOURCE));

  dataSurface->Unmap();

  return EncodeImageData(dataSurface, aMimeType, aOutputOptions, aStream);
}

NS_IMETHODIMP
imgTools::EncodeCroppedImage(imgIContainer* aContainer,
                             const nsACString& aMimeType, int32_t aOffsetX,
                             int32_t aOffsetY, int32_t aWidth, int32_t aHeight,
                             const nsAString& aOutputOptions,
                             nsIInputStream** aStream) {
  NS_ENSURE_ARG(aOffsetX >= 0 && aOffsetY >= 0 && aWidth >= 0 && aHeight >= 0);

  // Offsets must be zero when no width and height are given or else we're out
  // of bounds.
  NS_ENSURE_ARG(aWidth + aHeight > 0 || aOffsetX + aOffsetY == 0);

  // If no size is specified then we'll preserve the image's original dimensions
  // and don't need to crop.
  if (aWidth == 0 && aHeight == 0) {
    return EncodeImage(aContainer, aMimeType, aOutputOptions, aStream);
  }

  // Use frame 0 from the image container.
  RefPtr<SourceSurface> frame = aContainer->GetFrame(
      imgIContainer::FRAME_FIRST, imgIContainer::FLAG_SYNC_DECODE);
  NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);

  int32_t frameWidth = frame->GetSize().width;
  int32_t frameHeight = frame->GetSize().height;

  // If the given width or height is zero we'll replace it with the image's
  // original dimensions.
  if (aWidth == 0) {
    aWidth = frameWidth;
  } else if (aHeight == 0) {
    aHeight = frameHeight;
  }

  // Check that the given crop rectangle is within image bounds.
  NS_ENSURE_ARG(frameWidth >= aOffsetX + aWidth &&
                frameHeight >= aOffsetY + aHeight);

  RefPtr<DataSourceSurface> dataSurface = Factory::CreateDataSourceSurface(
      IntSize(aWidth, aHeight), SurfaceFormat::B8G8R8A8,
      /* aZero = */ true);
  if (NS_WARN_IF(!dataSurface)) {
    return NS_ERROR_FAILURE;
  }

  DataSourceSurface::MappedSurface map;
  if (!dataSurface->Map(DataSourceSurface::MapType::WRITE, &map)) {
    return NS_ERROR_FAILURE;
  }

  RefPtr<DrawTarget> dt = Factory::CreateDrawTargetForData(
      BackendType::CAIRO, map.mData, dataSurface->GetSize(), map.mStride,
      SurfaceFormat::B8G8R8A8);
  if (!dt) {
    gfxWarning()
        << "imgTools::EncodeCroppedImage failed in CreateDrawTargetForData";
    return NS_ERROR_OUT_OF_MEMORY;
  }
  dt->CopySurface(frame, IntRect(aOffsetX, aOffsetY, aWidth, aHeight),
                  IntPoint(0, 0));

  dataSurface->Unmap();

  return EncodeImageData(dataSurface, aMimeType, aOutputOptions, aStream);
}

NS_IMETHODIMP
imgTools::CreateScriptedObserver(imgIScriptedNotificationObserver* aInner,
                                 imgINotificationObserver** aObserver) {
  NS_ADDREF(*aObserver = new ScriptedNotificationObserver(aInner));
  return NS_OK;
}

NS_IMETHODIMP
imgTools::GetImgLoaderForDocument(nsIDOMDocument* aDoc, imgILoader** aLoader) {
  nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDoc);
  NS_IF_ADDREF(*aLoader = nsContentUtils::GetImgLoaderForDocument(doc));
  return NS_OK;
}

NS_IMETHODIMP
imgTools::GetImgCacheForDocument(nsIDOMDocument* aDoc, imgICache** aCache) {
  nsCOMPtr<imgILoader> loader;
  nsresult rv = GetImgLoaderForDocument(aDoc, getter_AddRefs(loader));
  NS_ENSURE_SUCCESS(rv, rv);
  return CallQueryInterface(loader, aCache);
}

}  // namespace image
}  // namespace mozilla