Introduce the image_decoder service

Moves OOP image decoding from a one-off utility process IPC
to its own service_manager service. This is then hooked up
by Chrome to be launched by content's utility-process-based
sandboxed service launcher.

This serves as a precursor to extracing process launch
code from content, as it defines a service which is used
in production across all platforms that support multiprocess.

The ImageDecoder helper defined in src/chrome is substantially
simplified here, as it's reduced to a thin wrapper around the
image_decoder client library.

In order to support Chrome content_browser packaging the
service, this also adds support for packaging services within
manifest overlays by introducing a new service_manifest_overlay
GN template.

Finally this also introduces a new service_unittests binary
as a default home for service implementations to stash their
unittest sources. Follow-up work will add this test suite to
bot configurations.

BUG=654986

Review-Url: https://codereview.chromium.org/2475543003
Cr-Commit-Position: refs/heads/master@{#431605}
diff --git a/BUILD.gn b/BUILD.gn
index 64ab231..73c3053 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -882,6 +882,7 @@
     deps = [
       "//ipc:ipc_tests",
       "//mojo:tests",
+      "//services:service_unittests",
     ]
   }
 }
diff --git a/chrome/app/BUILD.gn b/chrome/app/BUILD.gn
index 1e77e51..cbb7cd8 100644
--- a/chrome/app/BUILD.gn
+++ b/chrome/app/BUILD.gn
@@ -353,6 +353,14 @@
   }
 }
 
+service_manifest_overlay("chrome_content_browser_manifest_overlay") {
+  source = "//chrome/browser/chrome_content_browser_manifest_overlay.json"
+  packaged_services = [ "image_decoder" ]
+  deps = [
+    "//services/image_decoder:manifest",
+  ]
+}
+
 if (use_aura) {
   # NOTE: These rules generate compiled versions of the content service
   # manifests with Chrome's overlays applied. These are only used at run-time,
@@ -365,8 +373,9 @@
     output_name = "chrome_content_browser"
     source = "${root_out_dir}/Packages/content_browser/manifest.json"
     overlays =
-        [ "//chrome/browser/chrome_content_browser_manifest_overlay.json" ]
+        [ "${root_gen_dir}/chrome_content_browser_manifest_overlay.json" ]
     deps = [
+      ":chrome_content_browser_manifest_overlay",
       "//content/public/app:browser_manifest",
     ]
   }
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 74c463a..52e761c 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -1443,6 +1443,7 @@
     "//net:extras",
     "//net:net_with_v8",
     "//printing/features",
+    "//services/image_decoder/public/cpp",
     "//services/service_manager/public/cpp",
     "//skia",
     "//sql",
@@ -4066,6 +4067,7 @@
 
     # Depend only on the generated mojo bindings since we read the .mojom.js
     # file, rather than the whole mojo target which will link the C++ bindings.
+    "//chrome/app:chrome_content_browser_manifest_overlay",
     "//chrome/browser/ui/webui/engagement:mojo_bindings__generator",
     "//chrome/browser/ui/webui/omnibox:mojo_bindings__generator",
     "//chrome/browser/ui/webui/plugins:mojo_bindings__generator",
diff --git a/chrome/browser/DEPS b/chrome/browser/DEPS
index 0eebd89..6bc62ef 100644
--- a/chrome/browser/DEPS
+++ b/chrome/browser/DEPS
@@ -37,6 +37,7 @@
   "+media/cdm/cdm_paths.h",
   "+media/midi",  # For midi switches
   "+media/mojo",  # For mojo media services.
+  "+services/image_decoder/public/cpp",
   "+services/service_manager/public/cpp",
   "+services/ui/public",
   "+ppapi/c",  # For various types.
diff --git a/chrome/browser/android/download/ui/thumbnail_provider.h b/chrome/browser/android/download/ui/thumbnail_provider.h
index dcdab20..aa3ea8e0 100644
--- a/chrome/browser/android/download/ui/thumbnail_provider.h
+++ b/chrome/browser/android/download/ui/thumbnail_provider.h
@@ -8,6 +8,7 @@
 #include <string>
 
 #include "base/android/jni_android.h"
+#include "base/memory/weak_ptr.h"
 #include "chrome/browser/image_decoder.h"
 
 // Kicks off asynchronous pipelines for creating thumbnails for local files.
diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd
index 6590f23..bd05bf6 100644
--- a/chrome/browser/browser_resources.grd
+++ b/chrome/browser/browser_resources.grd
@@ -606,7 +606,7 @@
         <include name="IDR_IME_WINDOW_CLOSE_C" file="resources\input_ime\ime_window_close_click.png" type="BINDATA" />
         <include name="IDR_IME_WINDOW_CLOSE_H" file="resources\input_ime\ime_window_close_hover.png" type="BINDATA" />
       </if>
-      <include name="IDR_CHROME_CONTENT_BROWSER_MANIFEST_OVERLAY" file="chrome_content_browser_manifest_overlay.json" type="BINDATA" />
+      <include name="IDR_CHROME_CONTENT_BROWSER_MANIFEST_OVERLAY" file="${root_gen_dir}\chrome_content_browser_manifest_overlay.json" use_base_dir="false" type="BINDATA" />
       <include name="IDR_CHROME_CONTENT_GPU_MANIFEST_OVERLAY" file="chrome_content_gpu_manifest_overlay.json" type="BINDATA" />
       <include name="IDR_CHROME_CONTENT_PLUGIN_MANIFEST_OVERLAY" file="chrome_content_plugin_manifest_overlay.json" type="BINDATA" />
       <include name="IDR_CHROME_CONTENT_RENDERER_MANIFEST_OVERLAY" file="chrome_content_renderer_manifest_overlay.json" type="BINDATA" />
diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index 3e19a3b..47900fbf 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -183,6 +183,7 @@
 #include "net/ssl/ssl_cert_request_info.h"
 #include "ppapi/host/ppapi_host.h"
 #include "printing/features/features.h"
+#include "services/image_decoder/public/cpp/constants.h"
 #include "services/service_manager/public/cpp/interface_provider.h"
 #include "services/service_manager/public/cpp/interface_registry.h"
 #include "services/service_manager/public/cpp/service.h"
@@ -3012,6 +3013,8 @@
   services->insert(std::make_pair("service:media",
                                   base::ASCIIToUTF16("Media Service")));
 #endif
+  services->insert(std::make_pair(image_decoder::kServiceName,
+                                  base::ASCIIToUTF16("Image Decoder Service")));
 }
 
 std::unique_ptr<base::Value>
diff --git a/chrome/browser/chrome_content_browser_manifest_overlay.json b/chrome/browser/chrome_content_browser_manifest_overlay.json
index 8f7fec4..ed6a8a1 100644
--- a/chrome/browser/chrome_content_browser_manifest_overlay.json
+++ b/chrome/browser/chrome_content_browser_manifest_overlay.json
@@ -33,7 +33,8 @@
         ]
       },
       "requires": {
-        "service:ash": [ "ash" ]
+        "service:ash": [ "ash" ],
+        "service:image_decoder": [ "decode" ]
       }
     },
     "navigation:frame": {
diff --git a/chrome/browser/image_decoder.cc b/chrome/browser/image_decoder.cc
index 3815f26dc..93360b70 100644
--- a/chrome/browser/image_decoder.cc
+++ b/chrome/browser/image_decoder.cc
@@ -7,53 +7,78 @@
 #include <utility>
 
 #include "base/bind.h"
+#include "base/callback.h"
+#include "base/lazy_instance.h"
 #include "base/threading/thread_task_runner_handle.h"
 #include "build/build_config.h"
-#include "chrome/browser/browser_process.h"
-#include "chrome/common/image_decoder.mojom.h"
-#include "chrome/grit/generated_resources.h"
 #include "content/public/browser/browser_thread.h"
-#include "content/public/browser/utility_process_host.h"
-#include "services/service_manager/public/cpp/interface_provider.h"
+#include "content/public/common/service_manager_connection.h"
+#include "ipc/ipc_channel.h"
+#include "services/image_decoder/public/cpp/decode.h"
+#include "services/service_manager/public/cpp/connector.h"
 #include "third_party/skia/include/core/SkBitmap.h"
-#include "ui/base/l10n/l10n_util.h"
-
-using content::BrowserThread;
-using content::UtilityProcessHost;
 
 namespace {
 
 // static, Leaky to allow access from any thread.
 base::LazyInstance<ImageDecoder>::Leaky g_decoder = LAZY_INSTANCE_INITIALIZER;
 
-// How long to wait after the last request has been received before ending
-// batch mode.
-const int kBatchModeTimeoutSeconds = 5;
+const int64_t kMaxImageSizeInBytes =
+    static_cast<int64_t>(IPC::Channel::kMaximumMessageSize);
 
+// Note that this is always called on the thread which initiated the
+// corresponding image_decoder::Decode request.
 void OnDecodeImageDone(
     base::Callback<void(int)> fail_callback,
     base::Callback<void(const SkBitmap&, int)> success_callback,
     int request_id,
     const SkBitmap& image) {
-  DCHECK_CURRENTLY_ON(BrowserThread::IO);
   if (!image.isNull() && !image.empty())
     success_callback.Run(image, request_id);
   else
     fail_callback.Run(request_id);
 }
 
+void BindToBrowserConnector(service_manager::mojom::ConnectorRequest request) {
+  if (!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) {
+    content::BrowserThread::PostTask(
+        content::BrowserThread::UI, FROM_HERE,
+        base::Bind(&BindToBrowserConnector, base::Passed(&request)));
+    return;
+  }
+
+  content::ServiceManagerConnection::GetForProcess()->GetConnector()
+      ->BindRequest(std::move(request));
+}
+
+void RunDecodeCallbackOnTaskRunner(
+    const image_decoder::mojom::ImageDecoder::DecodeImageCallback& callback,
+    scoped_refptr<base::SequencedTaskRunner> task_runner,
+    const SkBitmap& image) {
+  task_runner->PostTask(FROM_HERE, base::Bind(callback, image));
+}
+
+void DecodeImage(
+    std::vector<uint8_t> image_data,
+    image_decoder::mojom::ImageCodec codec,
+    bool shrink_to_fit,
+    const image_decoder::mojom::ImageDecoder::DecodeImageCallback& callback,
+    scoped_refptr<base::SequencedTaskRunner> callback_task_runner) {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
+
+  service_manager::mojom::ConnectorRequest connector_request;
+  std::unique_ptr<service_manager::Connector> connector =
+      service_manager::Connector::Create(&connector_request);
+  BindToBrowserConnector(std::move(connector_request));
+
+  image_decoder::Decode(connector.get(), image_data, codec, shrink_to_fit,
+                        kMaxImageSizeInBytes,
+                        base::Bind(&RunDecodeCallbackOnTaskRunner,
+                                   callback, callback_task_runner));
+}
+
 }  // namespace
 
-ImageDecoder::ImageDecoder()
-    : image_request_id_counter_(0) {
-  // A single ImageDecoder instance should live for the life of the program.
-  // Explicitly add a reference so the object isn't deleted.
-  AddRef();
-}
-
-ImageDecoder::~ImageDecoder() {
-}
-
 ImageDecoder::ImageRequest::ImageRequest()
     : task_runner_(base::ThreadTaskRunnerHandle::Get()) {
   DCHECK(sequence_checker_.CalledOnValidSequence());
@@ -70,6 +95,10 @@
   ImageDecoder::Cancel(this);
 }
 
+ImageDecoder::ImageDecoder() : image_request_id_counter_(0) {}
+
+ImageDecoder::~ImageDecoder() {}
+
 // static
 void ImageDecoder::Start(ImageRequest* image_request,
                          std::vector<uint8_t> image_data) {
@@ -88,9 +117,8 @@
                                     std::vector<uint8_t> image_data,
                                     ImageCodec image_codec,
                                     bool shrink_to_fit) {
-  g_decoder.Pointer()->StartWithOptionsImpl(image_request,
-                                            std::move(image_data),
-                                            image_codec, shrink_to_fit);
+  g_decoder.Get().StartWithOptionsImpl(image_request, std::move(image_data),
+                                       image_codec, shrink_to_fit);
 }
 
 // static
@@ -117,68 +145,35 @@
     image_request_id_map_.insert(std::make_pair(request_id, image_request));
   }
 
-  BrowserThread::PostTask(
-      BrowserThread::IO, FROM_HERE,
-      base::Bind(
-          &ImageDecoder::DecodeImageInSandbox,
-          g_decoder.Pointer(), request_id,
-          base::Passed(std::move(image_data)),
-          image_codec, shrink_to_fit));
+  image_decoder::mojom::ImageCodec codec =
+      image_decoder::mojom::ImageCodec::DEFAULT;
+#if defined(OS_CHROMEOS)
+  if (image_codec == ROBUST_JPEG_CODEC)
+    codec = image_decoder::mojom::ImageCodec::ROBUST_JPEG;
+  if (image_codec == ROBUST_PNG_CODEC)
+    codec = image_decoder::mojom::ImageCodec::ROBUST_PNG;
+#endif  // defined(OS_CHROMEOS)
+
+  auto callback = base::Bind(
+      &OnDecodeImageDone,
+      base::Bind(&ImageDecoder::OnDecodeImageFailed, base::Unretained(this)),
+      base::Bind(&ImageDecoder::OnDecodeImageSucceeded, base::Unretained(this)),
+      request_id);
+
+  // NOTE: There exist ImageDecoder consumers which implicitly rely on this
+  // operation happening on a thread which always has a ThreadTaskRunnerHandle.
+  // We arbitrarily use the IO thread here to match details of the legacy
+  // implementation.
+  content::BrowserThread::PostTask(
+      content::BrowserThread::IO, FROM_HERE,
+      base::Bind(&DecodeImage, base::Passed(&image_data), codec, shrink_to_fit,
+                 callback, make_scoped_refptr(image_request->task_runner())));
 }
 
 // static
 void ImageDecoder::Cancel(ImageRequest* image_request) {
   DCHECK(image_request);
-  g_decoder.Pointer()->CancelImpl(image_request);
-}
-
-void ImageDecoder::DecodeImageInSandbox(
-    int request_id,
-    std::vector<uint8_t> image_data,
-    ImageCodec image_codec,
-    bool shrink_to_fit) {
-  DCHECK_CURRENTLY_ON(BrowserThread::IO);
-  base::AutoLock lock(map_lock_);
-  const auto it = image_request_id_map_.find(request_id);
-  if (it == image_request_id_map_.end())
-    return;
-
-  ImageRequest* image_request = it->second;
-  if (!utility_process_host_) {
-    StartBatchMode();
-  }
-  if (!utility_process_host_) {
-    // Utility process failed to start; notify delegate and return.
-    // Without this check, we were seeing crashes on startup. Further
-    // investigation is needed to determine why the utility process
-    // is failing to start. See crbug.com/472272
-    image_request->task_runner()->PostTask(
-        FROM_HERE,
-        base::Bind(&ImageDecoder::RunOnDecodeImageFailed, this, request_id));
-    return;
-  }
-
-  if (!batch_mode_timer_) {
-    // Created here so it will call StopBatchMode() on the right thread.
-    batch_mode_timer_.reset(new base::DelayTimer(
-        FROM_HERE, base::TimeDelta::FromSeconds(kBatchModeTimeoutSeconds), this,
-        &ImageDecoder::StopBatchMode));
-  }
-  batch_mode_timer_->Reset();
-
-  mojom::ImageCodec mojo_codec = mojom::ImageCodec::DEFAULT;
-#if defined(OS_CHROMEOS)
-  if (image_codec == ROBUST_JPEG_CODEC)
-    mojo_codec = mojom::ImageCodec::ROBUST_JPEG;
-  if (image_codec == ROBUST_PNG_CODEC)
-    mojo_codec = mojom::ImageCodec::ROBUST_PNG;
-#endif  // defined(OS_CHROMEOS)
-  decoder_->DecodeImage(
-      image_data, mojo_codec, shrink_to_fit,
-      base::Bind(&OnDecodeImageDone,
-                 base::Bind(&ImageDecoder::OnDecodeImageFailed, this),
-                 base::Bind(&ImageDecoder::OnDecodeImageSucceeded, this),
-                 request_id));
+  g_decoder.Get().CancelImpl(image_request);
 }
 
 void ImageDecoder::CancelImpl(ImageRequest* image_request) {
@@ -193,104 +188,9 @@
   }
 }
 
-void ImageDecoder::StartBatchMode() {
-  DCHECK_CURRENTLY_ON(BrowserThread::IO);
-  utility_process_host_ =
-      UtilityProcessHost::Create(
-          this, base::ThreadTaskRunnerHandle::Get().get())->AsWeakPtr();
-  utility_process_host_->SetName(l10n_util::GetStringUTF16(
-      IDS_UTILITY_PROCESS_IMAGE_DECODER_NAME));
-  if (!utility_process_host_->Start()) {
-    delete utility_process_host_.get();
-    return;
-  }
-  utility_process_host_->GetRemoteInterfaces()->GetInterface(&decoder_);
-}
-
-void ImageDecoder::StopBatchMode() {
-  DCHECK_CURRENTLY_ON(BrowserThread::IO);
-  {
-    // Check for outstanding requests and wait for them to finish.
-    base::AutoLock lock(map_lock_);
-    if (!image_request_id_map_.empty()) {
-      batch_mode_timer_->Reset();
-      return;
-    }
-  }
-
-  if (utility_process_host_) {
-    // With Mojo, the utility process needs to be explicitly shut down by
-    // deleting the host.
-    delete utility_process_host_.get();
-    decoder_.reset();
-    utility_process_host_.reset();
-  }
-}
-
-void ImageDecoder::FailAllRequests() {
-  RequestMap requests;
-  {
-    base::AutoLock lock(map_lock_);
-    requests = image_request_id_map_;
-  }
-
-  // Since |OnProcessCrashed| and |OnProcessLaunchFailed| are run asynchronously
-  // from the actual event, it's possible for a new utility process to have been
-  // created and sent requests by the time these functions are run. This results
-  // in failing requests that are unaffected by the crash. Although not ideal,
-  // this is valid and simpler than tracking which request is sent to which
-  // utility process, and whether the request has been sent at all.
-  for (const auto& request : requests)
-    OnDecodeImageFailed(request.first);
-}
-
-void ImageDecoder::OnProcessCrashed(int exit_code) {
-  DCHECK_CURRENTLY_ON(BrowserThread::IO);
-  FailAllRequests();
-}
-
-void ImageDecoder::OnProcessLaunchFailed(int error_code) {
-  DCHECK_CURRENTLY_ON(BrowserThread::IO);
-  FailAllRequests();
-}
-
-bool ImageDecoder::OnMessageReceived(const IPC::Message& message) {
-  return false;
-}
-
 void ImageDecoder::OnDecodeImageSucceeded(
     const SkBitmap& decoded_image,
     int request_id) {
-  DCHECK_CURRENTLY_ON(BrowserThread::IO);
-  base::AutoLock lock(map_lock_);
-  auto it = image_request_id_map_.find(request_id);
-  if (it == image_request_id_map_.end())
-    return;
-
-  ImageRequest* image_request = it->second;
-  image_request->task_runner()->PostTask(
-      FROM_HERE,
-      base::Bind(&ImageDecoder::RunOnImageDecoded,
-                 this,
-                 decoded_image,
-                 request_id));
-}
-
-void ImageDecoder::OnDecodeImageFailed(int request_id) {
-  DCHECK_CURRENTLY_ON(BrowserThread::IO);
-  base::AutoLock lock(map_lock_);
-  auto it = image_request_id_map_.find(request_id);
-  if (it == image_request_id_map_.end())
-    return;
-
-  ImageRequest* image_request = it->second;
-  image_request->task_runner()->PostTask(
-      FROM_HERE,
-      base::Bind(&ImageDecoder::RunOnDecodeImageFailed, this, request_id));
-}
-
-void ImageDecoder::RunOnImageDecoded(const SkBitmap& decoded_image,
-                                     int request_id) {
   ImageRequest* image_request;
   {
     base::AutoLock lock(map_lock_);
@@ -305,7 +205,7 @@
   image_request->OnImageDecoded(decoded_image);
 }
 
-void ImageDecoder::RunOnDecodeImageFailed(int request_id) {
+void ImageDecoder::OnDecodeImageFailed(int request_id) {
   ImageRequest* image_request;
   {
     base::AutoLock lock(map_lock_);
diff --git a/chrome/browser/image_decoder.h b/chrome/browser/image_decoder.h
index 2c8160d..d8bda97 100644
--- a/chrome/browser/image_decoder.h
+++ b/chrome/browser/image_decoder.h
@@ -6,35 +6,29 @@
 #define CHROME_BROWSER_IMAGE_DECODER_H_
 
 #include <map>
-#include <memory>
 #include <string>
 #include <vector>
 
-#include "base/lazy_instance.h"
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
 #include "base/sequence_checker.h"
 #include "base/sequenced_task_runner.h"
 #include "base/synchronization/lock.h"
-#include "base/timer/timer.h"
-#include "build/build_config.h"
-#include "chrome/common/image_decoder.mojom.h"
-#include "content/public/browser/utility_process_host.h"
-#include "content/public/browser/utility_process_host_client.h"
 
 class SkBitmap;
 
-// This is a helper class for decoding images safely in a utility process. To
+// This is a helper class for decoding images safely in a sandboxed service. To
 // use this, call ImageDecoder::Start(...) or
 // ImageDecoder::StartWithOptions(...) on any thread.
 //
-// Internally, most of the work happens on the IO thread, and then
-// the callback (ImageRequest::OnImageDecoded or
-// ImageRequest::OnDecodeImageFailed) is posted back to the |task_runner_|
-// associated with the ImageRequest.
-// The Cancel() method runs on whichever thread called it. |map_lock_| is used
-// to protect the data that is accessed from multiple threads.
-class ImageDecoder : public content::UtilityProcessHostClient {
+// ImageRequest::OnImageDecoded or ImageRequest::OnDecodeImageFailed is posted
+// back to the |task_runner_| associated with the ImageRequest.
+//
+// The Cancel() method runs on whichever thread called it.
+//
+// TODO(rockot): Use of this class should be replaced with direct image_decoder
+// client library usage.
+class ImageDecoder {
  public:
   // ImageRequest objects needs to be created and destroyed on the same
   // SequencedTaskRunner.
@@ -52,8 +46,9 @@
     }
 
    protected:
-    // Creates an ImageRequest that runs on the thread creating it.
+    // Creates an ImageRequest that runs on the thread which created it.
     ImageRequest();
+
     // Explicitly pass in |task_runner| if the current thread is part of a
     // thread pool.
     explicit ImageRequest(
@@ -76,6 +71,9 @@
 #endif  // defined(OS_CHROMEOS)
   };
 
+  ImageDecoder();
+  ~ImageDecoder();
+
   // Calls StartWithOptions() with ImageCodec::DEFAULT_CODEC and
   // shrink_to_fit = false.
   static void Start(ImageRequest* image_request,
@@ -101,54 +99,19 @@
   static void Cancel(ImageRequest* image_request);
 
  private:
-  friend struct base::DefaultLazyInstanceTraits<ImageDecoder>;
-
   using RequestMap = std::map<int, ImageRequest*>;
 
-  ImageDecoder();
-  // It's a reference counted object, so destructor is private.
-  ~ImageDecoder() override;
-
-  // Sends a request to the sandboxed process to decode the image. Starts
-  // batch mode if necessary. If the utility process fails to start,
-  // an OnDecodeImageFailed task is posted to image_request's |task_runner_|.
-  void DecodeImageInSandbox(int request_id,
-                            std::vector<uint8_t> image_data,
-                            ImageCodec image_codec,
-                            bool shrink_to_fit);
-
   void StartWithOptionsImpl(ImageRequest* image_request,
                             std::vector<uint8_t> image_data,
                             ImageCodec image_codec,
                             bool shrink_to_fit);
+
   void CancelImpl(ImageRequest* image_request);
 
-  // Starts UtilityProcessHost in batch mode and starts |batch_mode_timer_|.
-  // If the utility process fails to start, the method resets
-  // |utility_process_host_| and returns.
-  void StartBatchMode();
-
-  // Stops batch mode if no requests have come in since
-  // |kBatchModeTimeoutSeconds|.
-  void StopBatchMode();
-
-  // Fails all outstanding requests.
-  void FailAllRequests();
-
-  // Overidden from UtilityProcessHostClient.
-  void OnProcessCrashed(int exit_code) override;
-  void OnProcessLaunchFailed(int error_code) override;
-  bool OnMessageReceived(const IPC::Message& message) override;
-
   // IPC message handlers.
   void OnDecodeImageSucceeded(const SkBitmap& decoded_image, int request_id);
   void OnDecodeImageFailed(int request_id);
 
-  // For the ImageRequest identified by |request_id|, call its OnImageDecoded()
-  // or OnDecodeImageFailed() method on its task runner thread.
-  void RunOnImageDecoded(const SkBitmap& decoded_image, int request_id);
-  void RunOnDecodeImageFailed(int request_id);
-
   // id to use for the next Start() request that comes in.
   int image_request_id_counter_;
 
@@ -158,17 +121,6 @@
   // Protects |image_request_id_map_| and |image_request_id_counter_|.
   base::Lock map_lock_;
 
-  // The UtilityProcessHost requests are sent to.
-  base::WeakPtr<content::UtilityProcessHost> utility_process_host_;
-
-  // Calls StopBatchMode() after |kBatchModeTimeoutSeconds| have elapsed,
-  // unless a new decoding request resets the timer.
-  std::unique_ptr<base::DelayTimer> batch_mode_timer_;
-
-  // Mojo service connection. Must always be bound/reset and used on the IO
-  // thread.
-  mojom::ImageDecoderPtr decoder_;
-
   DISALLOW_COPY_AND_ASSIGN(ImageDecoder);
 };
 
diff --git a/chrome/browser/search/suggestions/image_decoder_impl.cc b/chrome/browser/search/suggestions/image_decoder_impl.cc
index 2b7543d..bb3094c 100644
--- a/chrome/browser/search/suggestions/image_decoder_impl.cc
+++ b/chrome/browser/search/suggestions/image_decoder_impl.cc
@@ -5,6 +5,7 @@
 #include <algorithm>
 #include <vector>
 
+#include "base/callback.h"
 #include "chrome/browser/search/suggestions/image_decoder_impl.h"
 #include "ui/gfx/image/image.h"
 
diff --git a/chrome/common/BUILD.gn b/chrome/common/BUILD.gn
index 8830d23..96ac6a7d 100644
--- a/chrome/common/BUILD.gn
+++ b/chrome/common/BUILD.gn
@@ -662,14 +662,12 @@
 
 mojom("mojo_bindings") {
   sources = [
-    "image_decoder.mojom",
     "network_diagnostics.mojom",
     "resource_usage_reporter.mojom",
     "shell_handler_win.mojom",
   ]
 
   public_deps = [
-    "//skia/public/interfaces",
     "//url/mojo:url_mojom_gurl",
   ]
 }
diff --git a/chrome/common/image_decoder.mojom b/chrome/common/image_decoder.mojom
deleted file mode 100644
index 3467afa..0000000
--- a/chrome/common/image_decoder.mojom
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright 2016 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-module mojom;
-
-import "skia/public/interfaces/bitmap.mojom";
-
-enum ImageCodec {
-  DEFAULT,
-  ROBUST_JPEG,
-  ROBUST_PNG,
-};
-
-interface ImageDecoder {
-  DecodeImage(array<uint8> encoded_data, ImageCodec codec, bool shrink_to_fit)
-      => (skia.mojom.Bitmap? decoded_image);
-};
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 4d6ee71d..bda31843 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -3343,7 +3343,6 @@
     "../test/base/v8_unit_test.cc",
     "../test/base/v8_unit_test.h",
     "../test/logging/win/mof_data_parser_unittest.cc",
-    "../utility/image_decoder_impl_unittest.cc",
 
     # Duplicate these tests here because PathService has more items in
     # unit_tests than in base_unittests.
diff --git a/chrome/utility/BUILD.gn b/chrome/utility/BUILD.gn
index f4f8a8f..596dc18 100644
--- a/chrome/utility/BUILD.gn
+++ b/chrome/utility/BUILD.gn
@@ -17,8 +17,6 @@
     "cloud_print/bitmap_image.h",
     "cloud_print/pwg_encoder.cc",
     "cloud_print/pwg_encoder.h",
-    "image_decoder_impl.cc",
-    "image_decoder_impl.h",
     "ipc_shell_handler_win.cc",
     "ipc_shell_handler_win.h",
     "printing_handler.cc",
@@ -52,6 +50,8 @@
     "//media",
     "//net:net_with_v8",
     "//printing/features",
+    "//services/image_decoder:lib",
+    "//services/image_decoder/public/cpp",
     "//services/service_manager/public/cpp",
     "//skia",
     "//sql",
diff --git a/chrome/utility/DEPS b/chrome/utility/DEPS
index a43b1da..e3811b9 100644
--- a/chrome/utility/DEPS
+++ b/chrome/utility/DEPS
@@ -8,6 +8,7 @@
   "+courgette",
   "+extensions/common",
   "+media",
+  "+services/image_decoder",
   "+services/service_manager/public/cpp",
   "+skia/ext",
   "+third_party/libxml",
diff --git a/chrome/utility/chrome_content_utility_client.cc b/chrome/utility/chrome_content_utility_client.cc
index 0c2fc19..d653678 100644
--- a/chrome/utility/chrome_content_utility_client.cc
+++ b/chrome/utility/chrome_content_utility_client.cc
@@ -16,17 +16,19 @@
 #include "chrome/common/safe_browsing/zip_analyzer.h"
 #include "chrome/common/safe_browsing/zip_analyzer_results.h"
 #include "chrome/utility/chrome_content_utility_ipc_whitelist.h"
-#include "chrome/utility/image_decoder_impl.h"
 #include "chrome/utility/utility_message_handler.h"
 #include "components/safe_json/utility/safe_json_parser_mojo_impl.h"
 #include "content/public/child/image_decoder_utils.h"
 #include "content/public/common/content_switches.h"
+#include "content/public/common/service_info.h"
 #include "content/public/utility/utility_thread.h"
 #include "courgette/courgette.h"
 #include "courgette/third_party/bsdiff/bsdiff.h"
 #include "ipc/ipc_channel.h"
 #include "mojo/public/cpp/bindings/strong_binding.h"
 #include "printing/features/features.h"
+#include "services/image_decoder/image_decoder_service.h"
+#include "services/image_decoder/public/cpp/constants.h"
 #include "services/service_manager/public/cpp/interface_registry.h"
 #include "third_party/zlib/google/zip.h"
 #include "ui/gfx/geometry/size.h"
@@ -100,10 +102,9 @@
 }
 #endif  // !defined(OS_ANDROID)
 
-void CreateImageDecoder(mojo::InterfaceRequest<mojom::ImageDecoder> request) {
+std::unique_ptr<service_manager::Service> CreateImageDecoderService() {
   content::UtilityThread::Get()->EnsureBlinkInitialized();
-  mojo::MakeStrongBinding(base::MakeUnique<ImageDecoderImpl>(),
-                          std::move(request));
+  return image_decoder::ImageDecoderService::Create();
 }
 
 }  // namespace
@@ -206,7 +207,6 @@
       base::Bind(CreateProxyResolverFactory));
   registry->AddInterface(base::Bind(CreateResourceUsageReporter));
 #endif
-  registry->AddInterface(base::Bind(&CreateImageDecoder));
   registry->AddInterface(
       base::Bind(&safe_json::SafeJsonParserMojoImpl::Create));
 #if defined(OS_WIN)
@@ -214,6 +214,13 @@
 #endif
 }
 
+void ChromeContentUtilityClient::RegisterServices(StaticServiceMap* services) {
+  content::ServiceInfo image_decoder_info;
+  image_decoder_info.factory = base::Bind(&CreateImageDecoderService);
+  services->insert(
+      std::make_pair(image_decoder::kServiceName, image_decoder_info));
+}
+
 void ChromeContentUtilityClient::AddHandler(
     std::unique_ptr<UtilityMessageHandler> handler) {
   handlers_.push_back(std::move(handler));
diff --git a/chrome/utility/chrome_content_utility_client.h b/chrome/utility/chrome_content_utility_client.h
index 0f2f481f..c1c8eb7 100644
--- a/chrome/utility/chrome_content_utility_client.h
+++ b/chrome/utility/chrome_content_utility_client.h
@@ -35,6 +35,7 @@
   bool OnMessageReceived(const IPC::Message& message) override;
   void ExposeInterfacesToBrowser(
       service_manager::InterfaceRegistry* registry) override;
+  void RegisterServices(StaticServiceMap* services) override;
 
   void AddHandler(std::unique_ptr<UtilityMessageHandler> handler);
 
diff --git a/chrome/utility/image_decoder_impl.cc b/chrome/utility/image_decoder_impl.cc
deleted file mode 100644
index 472f987..0000000
--- a/chrome/utility/image_decoder_impl.cc
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright 2016 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "chrome/utility/image_decoder_impl.h"
-
-#include <string.h>
-
-#include <utility>
-
-#include "base/logging.h"
-#include "content/public/child/image_decoder_utils.h"
-#include "ipc/ipc_channel.h"
-#include "mojo/public/cpp/bindings/strong_binding.h"
-#include "skia/ext/image_operations.h"
-#include "third_party/skia/include/core/SkBitmap.h"
-#include "ui/gfx/geometry/size.h"
-
-#if defined(OS_CHROMEOS)
-#include "ui/gfx/chromeos/codec/jpeg_codec_robust_slow.h"
-#include "ui/gfx/codec/png_codec.h"
-#endif
-
-namespace {
-int64_t kMaxMessageSize = IPC::Channel::kMaximumMessageSize;
-int64_t kPadding = 64;
-}
-
-ImageDecoderImpl::ImageDecoderImpl() : ImageDecoderImpl(kMaxMessageSize) {}
-
-ImageDecoderImpl::ImageDecoderImpl(int64_t max_message_size)
-    : max_message_size_(max_message_size) {}
-
-ImageDecoderImpl::~ImageDecoderImpl() {
-}
-
-void ImageDecoderImpl::DecodeImage(const std::vector<uint8_t>& encoded_data,
-                                   mojom::ImageCodec codec,
-                                   bool shrink_to_fit,
-                                   const DecodeImageCallback& callback) {
-  if (encoded_data.empty()) {
-    callback.Run(SkBitmap());
-    return;
-  }
-
-  SkBitmap decoded_image;
-#if defined(OS_CHROMEOS)
-  if (codec == mojom::ImageCodec::ROBUST_JPEG) {
-    // Our robust jpeg decoding is using IJG libjpeg.
-    std::unique_ptr<SkBitmap> decoded_jpeg(gfx::JPEGCodecRobustSlow::Decode(
-        encoded_data.data(), encoded_data.size()));
-    if (decoded_jpeg.get() && !decoded_jpeg->empty())
-      decoded_image = *decoded_jpeg;
-  } else if (codec == mojom::ImageCodec::ROBUST_PNG) {
-    // Our robust PNG decoding is using libpng.
-    SkBitmap decoded_png;
-    if (gfx::PNGCodec::Decode(encoded_data.data(), encoded_data.size(),
-                              &decoded_png)) {
-      decoded_image = decoded_png;
-    }
-  }
-#endif  // defined(OS_CHROMEOS)
-  if (codec == mojom::ImageCodec::DEFAULT) {
-    decoded_image = content::DecodeImage(encoded_data.data(), gfx::Size(),
-                                         encoded_data.size());
-  }
-
-  if (!decoded_image.isNull()) {
-    // When serialized, the space taken up by a skia::mojom::Bitmap excluding
-    // the pixel data payload should be:
-    //   sizeof(skia::mojom::Bitmap::Data_) + pixel data array header (8 bytes)
-    // Use a bigger number in case we need padding at the end.
-    int64_t struct_size = sizeof(skia::mojom::Bitmap::Data_) + kPadding;
-    int64_t image_size = decoded_image.computeSize64();
-    int halves = 0;
-    while (struct_size + (image_size >> 2 * halves) > max_message_size_)
-      halves++;
-    if (halves) {
-      if (shrink_to_fit) {
-        // If decoded image is too large for IPC message, shrink it by halves.
-        // This prevents quality loss, and should never overshrink on displays
-        // smaller than 3600x2400.
-        // TODO (Issue 416916): Instead of shrinking, return via shared memory
-        decoded_image = skia::ImageOperations::Resize(
-            decoded_image, skia::ImageOperations::RESIZE_LANCZOS3,
-            decoded_image.width() >> halves, decoded_image.height() >> halves);
-      } else {
-        decoded_image.reset();
-      }
-    }
-  }
-
-  callback.Run(decoded_image);
-}
diff --git a/chrome/utility/image_decoder_impl.h b/chrome/utility/image_decoder_impl.h
deleted file mode 100644
index 7f82054d..0000000
--- a/chrome/utility/image_decoder_impl.h
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright 2016 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CHROME_UTILITY_IMAGE_DECODER_IMPL_H_
-#define CHROME_UTILITY_IMAGE_DECODER_IMPL_H_
-
-#include "base/macros.h"
-#include "chrome/common/image_decoder.mojom.h"
-
-class ImageDecoderImpl : public mojom::ImageDecoder {
- public:
-  ImageDecoderImpl();
-  explicit ImageDecoderImpl(int64_t max_message_size);
-  ~ImageDecoderImpl() override;
-
-  // Overridden from mojom::ImageDecoder:
-  void DecodeImage(const std::vector<uint8_t>& encoded_data,
-                   mojom::ImageCodec codec,
-                   bool shrink_to_fit,
-                   const DecodeImageCallback& callback) override;
-
- private:
-  int64_t max_message_size_;
-
-  DISALLOW_COPY_AND_ASSIGN(ImageDecoderImpl);
-};
-
-#endif  // CHROME_UTILITY_IMAGE_DECODER_IMPL_H_
diff --git a/chrome/utility/image_decoder_impl_unittest.cc b/chrome/utility/image_decoder_impl_unittest.cc
deleted file mode 100644
index ea22b05..0000000
--- a/chrome/utility/image_decoder_impl_unittest.cc
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright 2016 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "chrome/utility/image_decoder_impl.h"
-
-#include <vector>
-
-#include "base/bind.h"
-#include "ipc/ipc_channel.h"
-#include "testing/gtest/include/gtest/gtest.h"
-#include "third_party/skia/include/core/SkBitmap.h"
-#include "ui/gfx/codec/jpeg_codec.h"
-
-namespace mojom {
-
-namespace {
-
-bool CreateJPEGImage(int width,
-                     int height,
-                     SkColor color,
-                     std::vector<unsigned char>* output) {
-  SkBitmap bitmap;
-  bitmap.allocN32Pixels(width, height);
-  bitmap.eraseColor(color);
-
-  const int kQuality = 50;
-  if (!gfx::JPEGCodec::Encode(
-          static_cast<const unsigned char*>(bitmap.getPixels()),
-          gfx::JPEGCodec::FORMAT_SkBitmap, width, height,
-          static_cast<int>(bitmap.rowBytes()), kQuality, output)) {
-    LOG(ERROR) << "Unable to encode " << width << "x" << height << " bitmap";
-    return false;
-  }
-  return true;
-}
-
-class Request {
- public:
-  explicit Request(ImageDecoderImpl* decoder) : decoder_(decoder) {}
-
-  void DecodeImage(const std::vector<unsigned char>& image, bool shrink) {
-    decoder_->DecodeImage(
-        image, ImageCodec::DEFAULT, shrink,
-        base::Bind(&Request::OnRequestDone, base::Unretained(this)));
-  }
-
-  const SkBitmap& bitmap() const { return bitmap_; }
-
- private:
-  void OnRequestDone(const SkBitmap& result_image) { bitmap_ = result_image; }
-
-  ImageDecoderImpl* decoder_;
-  SkBitmap bitmap_;
-};
-
-}  // namespace
-
-// Test that DecodeImage() doesn't return image message > (max message size)
-TEST(ImageDecoderImplTest, DecodeImageSizeLimit) {
-  // Using actual limit generates 14000 x 9400 images, which causes the test to
-  // timeout.  We test with a smaller limit for efficiency.
-  const size_t kTestMessageSize = IPC::Channel::kMaximumMessageSize / 1024;
-
-  ImageDecoderImpl decoder(kTestMessageSize);
-
-  // Approx max height for 3:2 image that will fit in IPC message;
-  // 1.5 for width/height ratio, 4 for bytes/pixel
-  int max_height_for_msg = sqrt(kTestMessageSize / (1.5 * 4));
-  int base_msg_size = sizeof(skia::mojom::Bitmap::Data_);
-
-  // Sizes which should trigger dimension-halving 0, 1 and 2 times
-  int heights[] = {max_height_for_msg - 10,
-                   max_height_for_msg + 10,
-                   2 * max_height_for_msg + 10};
-  int widths[] = {heights[0] * 3 / 2, heights[1] * 3 / 2, heights[2] * 3 / 2};
-  for (size_t i = 0; i < arraysize(heights); i++) {
-    std::vector<unsigned char> jpg;
-    ASSERT_TRUE(CreateJPEGImage(widths[i], heights[i], SK_ColorRED, &jpg));
-
-    Request request(&decoder);
-    request.DecodeImage(jpg, true);
-    ASSERT_FALSE(request.bitmap().isNull());
-
-    // Check that image has been shrunk appropriately
-    EXPECT_LT(request.bitmap().computeSize64() + base_msg_size,
-              static_cast<int64_t>(kTestMessageSize));
-// Android does its own image shrinking for memory conservation deeper in
-// the decode, so more specific tests here won't work.
-#if !defined(OS_ANDROID)
-    EXPECT_EQ(widths[i] >> i, request.bitmap().width());
-    EXPECT_EQ(heights[i] >> i, request.bitmap().height());
-
-    // Check that if resize not requested and image exceeds IPC size limit,
-    // an empty image is returned
-    if (heights[i] > max_height_for_msg) {
-      Request request(&decoder);
-      request.DecodeImage(jpg, false);
-      EXPECT_TRUE(request.bitmap().isNull());
-    }
-#endif
-  }
-}
-
-TEST(ImageDecoderImplTest, DecodeImageFailed) {
-  ImageDecoderImpl decoder(IPC::Channel::kMaximumMessageSize);
-
-  // The "jpeg" is just some "random" data;
-  const char kRandomData[] = "u gycfy7xdjkhfgui bdui ";
-  std::vector<unsigned char> jpg(kRandomData,
-                                 kRandomData + sizeof(kRandomData));
-
-  Request request(&decoder);
-  request.DecodeImage(jpg, false);
-  EXPECT_TRUE(request.bitmap().isNull());
-}
-
-}  // namespace mojom
diff --git a/services/BUILD.gn b/services/BUILD.gn
new file mode 100644
index 0000000..5cb1983
--- /dev/null
+++ b/services/BUILD.gn
@@ -0,0 +1,17 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//testing/test.gni")
+
+# One Big Target for services to register their unit test sources. This exists
+# to avoid having to maintain a separate test binary for every service.
+#
+# To add tests for a new service, please define a "tests" source_set in the
+# service subdirectory and add it as a dependency here.
+test("service_unittests") {
+  deps = [
+    "//services/image_decoder:tests",
+    "//services/service_manager/public/cpp/test:run_all_service_tests",
+  ]
+}
diff --git a/services/OWNERS b/services/OWNERS
new file mode 100644
index 0000000..f5ec3de
--- /dev/null
+++ b/services/OWNERS
@@ -0,0 +1,3 @@
[email protected]
[email protected]
[email protected]
diff --git a/services/image_decoder/BUILD.gn b/services/image_decoder/BUILD.gn
new file mode 100644
index 0000000..7556c0c
--- /dev/null
+++ b/services/image_decoder/BUILD.gn
@@ -0,0 +1,59 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//services/service_manager/public/cpp/service.gni")
+import("//services/service_manager/public/service_manifest.gni")
+
+source_set("lib") {
+  sources = [
+    "image_decoder_impl.cc",
+    "image_decoder_impl.h",
+    "image_decoder_service.cc",
+    "image_decoder_service.h",
+  ]
+
+  deps = [
+    "//base",
+    "//content/public/child",
+    "//mojo/public/cpp/bindings",
+    "//skia",
+    "//ui/gfx",
+    "//ui/gfx/geometry",
+  ]
+
+  public_deps = [
+    "//services/image_decoder/public/interfaces",
+    "//services/service_manager/public/cpp",
+  ]
+
+  data_deps = [
+    ":manifest",
+  ]
+}
+
+source_set("tests") {
+  testonly = true
+
+  sources = [
+    "image_decoder_impl_unittest.cc",
+  ]
+
+  deps = [
+    ":lib",
+    "//base",
+    "//gin",
+    "//gin:gin_test",
+    "//skia",
+    "//testing/gtest",
+    "//third_party/WebKit/public:blink",
+    "//ui/gfx",
+  ]
+
+  configs += [ "//v8:external_startup_data" ]
+}
+
+service_manifest("manifest") {
+  name = "image_decoder"
+  source = "manifest.json"
+}
diff --git a/services/image_decoder/DEPS b/services/image_decoder/DEPS
new file mode 100644
index 0000000..9dca8ee
--- /dev/null
+++ b/services/image_decoder/DEPS
@@ -0,0 +1,8 @@
+include_rules = [
+  "+content/public/child",
+  "+gin",
+  "+skia",
+  "+third_party/WebKit/public",
+  "+third_party/skia",
+  "+ui/gfx",
+]
diff --git a/services/image_decoder/README.md b/services/image_decoder/README.md
new file mode 100644
index 0000000..8b983626
--- /dev/null
+++ b/services/image_decoder/README.md
@@ -0,0 +1,3 @@
+The image_decoder service exists to facilitate safe image decoding within an
+isolated sandboxed process.
+
diff --git a/services/image_decoder/image_decoder_impl.cc b/services/image_decoder/image_decoder_impl.cc
new file mode 100644
index 0000000..e82c497
--- /dev/null
+++ b/services/image_decoder/image_decoder_impl.cc
@@ -0,0 +1,103 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/image_decoder/image_decoder_impl.h"
+
+#include <string.h>
+
+#include <utility>
+
+#include "base/logging.h"
+#include "content/public/child/image_decoder_utils.h"
+#include "mojo/public/cpp/bindings/strong_binding.h"
+#include "skia/ext/image_operations.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "ui/gfx/geometry/size.h"
+
+#if defined(OS_CHROMEOS)
+#include "ui/gfx/chromeos/codec/jpeg_codec_robust_slow.h"
+#include "ui/gfx/codec/png_codec.h"
+#endif
+
+namespace image_decoder {
+
+namespace {
+
+int64_t kPadding = 64;
+
+}  // namespace
+
+ImageDecoderImpl::ImageDecoderImpl(
+    std::unique_ptr<service_manager::ServiceContextRef> service_ref)
+    : service_ref_(std::move(service_ref)) {}
+
+ImageDecoderImpl::~ImageDecoderImpl() = default;
+
+void ImageDecoderImpl::DecodeImage(const std::vector<uint8_t>& encoded_data,
+                                   mojom::ImageCodec codec,
+                                   bool shrink_to_fit,
+                                   int64_t max_size_in_bytes,
+                                   const DecodeImageCallback& callback) {
+  if (encoded_data.size() == 0) {
+    callback.Run(SkBitmap());
+    return;
+  }
+
+  SkBitmap decoded_image;
+#if defined(OS_CHROMEOS)
+  if (codec == mojom::ImageCodec::ROBUST_JPEG) {
+    // Our robust jpeg decoding is using IJG libjpeg.
+    if (encoded_data.size()) {
+      std::unique_ptr<SkBitmap> decoded_jpeg(gfx::JPEGCodecRobustSlow::Decode(
+          encoded_data.data(), encoded_data.size()));
+      if (decoded_jpeg.get() && !decoded_jpeg->empty())
+        decoded_image = *decoded_jpeg;
+    }
+  } else if (codec == mojom::ImageCodec::ROBUST_PNG) {
+    // Our robust PNG decoding is using libpng.
+    if (encoded_data.size()) {
+      SkBitmap decoded_png;
+      if (gfx::PNGCodec::Decode(
+            encoded_data.data(), encoded_data.size(), &decoded_png)) {
+        decoded_image = decoded_png;
+      }
+    }
+  }
+#endif  // defined(OS_CHROMEOS)
+  if (codec == mojom::ImageCodec::DEFAULT) {
+    decoded_image = content::DecodeImage(
+        encoded_data.data(), gfx::Size(), encoded_data.size());
+  }
+
+  if (!decoded_image.isNull()) {
+    // When serialized, the space taken up by a skia::mojom::Bitmap excluding
+    // the pixel data payload should be:
+    //   sizeof(skia::mojom::Bitmap::Data_) + pixel data array header (8 bytes)
+    // Use a bigger number in case we need padding at the end.
+    int64_t struct_size = sizeof(skia::mojom::Bitmap::Data_) + kPadding;
+    int64_t image_size = decoded_image.computeSize64();
+    int halves = 0;
+    while (struct_size + (image_size >> 2 * halves) > max_size_in_bytes)
+      halves++;
+    if (halves) {
+      // If the decoded image is too large, either discard it or shrink it.
+      //
+      // TODO(rockot): Also support exposing the bytes via shared memory for
+      // larger images. https://crbug.com/416916.
+      if (shrink_to_fit) {
+        // Shrinking by halves prevents quality loss and should never overshrink
+        // on displays smaller than 3600x2400.
+        decoded_image = skia::ImageOperations::Resize(
+            decoded_image, skia::ImageOperations::RESIZE_LANCZOS3,
+            decoded_image.width() >> halves, decoded_image.height() >> halves);
+      } else {
+        decoded_image.reset();
+      }
+    }
+  }
+
+  callback.Run(decoded_image);
+}
+
+}  // namespace image_decoder
diff --git a/services/image_decoder/image_decoder_impl.h b/services/image_decoder/image_decoder_impl.h
new file mode 100644
index 0000000..8d07486
--- /dev/null
+++ b/services/image_decoder/image_decoder_impl.h
@@ -0,0 +1,38 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_UTILITY_IMAGE_DECODER_IMPL_H_
+#define CHROME_UTILITY_IMAGE_DECODER_IMPL_H_
+
+#include <stdint.h>
+
+#include "base/macros.h"
+#include "services/image_decoder/public/interfaces/image_decoder.mojom.h"
+#include "services/service_manager/public/cpp/service_context_ref.h"
+
+namespace image_decoder {
+
+class ImageDecoderImpl : public mojom::ImageDecoder {
+ public:
+  explicit ImageDecoderImpl(
+      std::unique_ptr<service_manager::ServiceContextRef> service_ref);
+  ~ImageDecoderImpl() override;
+
+  // Overridden from mojom::ImageDecoder:
+  void DecodeImage(
+      const std::vector<uint8_t>& encoded_data,
+      mojom::ImageCodec codec,
+      bool shrink_to_fit,
+      int64_t max_size_in_bytes,
+      const DecodeImageCallback& callback) override;
+
+ private:
+  const std::unique_ptr<service_manager::ServiceContextRef> service_ref_;
+
+  DISALLOW_COPY_AND_ASSIGN(ImageDecoderImpl);
+};
+
+}  // namespace image_decoder
+
+#endif  // CHROME_UTILITY_IMAGE_DECODER_IMPL_H_
diff --git a/services/image_decoder/image_decoder_impl_unittest.cc b/services/image_decoder/image_decoder_impl_unittest.cc
new file mode 100644
index 0000000..82c7a00
--- /dev/null
+++ b/services/image_decoder/image_decoder_impl_unittest.cc
@@ -0,0 +1,161 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <memory>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/lazy_instance.h"
+#include "base/message_loop/message_loop.h"
+#include "gin/array_buffer.h"
+#include "gin/public/isolate_holder.h"
+#include "services/image_decoder/image_decoder_impl.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/WebKit/public/platform/scheduler/utility/webthread_impl_for_utility_thread.h"
+#include "third_party/WebKit/public/web/WebKit.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "ui/gfx/codec/jpeg_codec.h"
+
+#if defined(V8_USE_EXTERNAL_STARTUP_DATA)
+#include "gin/v8_initializer.h"
+#endif
+
+namespace image_decoder {
+
+namespace {
+
+const int64_t kTestMaxImageSize = 128 * 1024;
+
+bool CreateJPEGImage(int width,
+                     int height,
+                     SkColor color,
+                     std::vector<unsigned char>* output) {
+  SkBitmap bitmap;
+  bitmap.allocN32Pixels(width, height);
+  bitmap.eraseColor(color);
+
+  const int kQuality = 50;
+  if (!gfx::JPEGCodec::Encode(
+          static_cast<const unsigned char*>(bitmap.getPixels()),
+          gfx::JPEGCodec::FORMAT_SkBitmap, width, height,
+          static_cast<int>(bitmap.rowBytes()), kQuality, output)) {
+    LOG(ERROR) << "Unable to encode " << width << "x" << height << " bitmap";
+    return false;
+  }
+  return true;
+}
+
+class Request {
+ public:
+  explicit Request(ImageDecoderImpl* decoder) : decoder_(decoder) {}
+
+  void DecodeImage(const std::vector<unsigned char>& image, bool shrink) {
+    decoder_->DecodeImage(
+        image, mojom::ImageCodec::DEFAULT, shrink, kTestMaxImageSize,
+        base::Bind(&Request::OnRequestDone, base::Unretained(this)));
+  }
+
+  const SkBitmap& bitmap() const { return bitmap_; }
+
+ private:
+  void OnRequestDone(const SkBitmap& result_image) { bitmap_ = result_image; }
+
+  ImageDecoderImpl* decoder_;
+  SkBitmap bitmap_;
+};
+
+// We need to ensure that Blink and V8 are initialized in order to use content's
+// image decoding call.
+class BlinkInitializer : public blink::Platform {
+ public:
+  BlinkInitializer()
+      : main_thread_(new blink::scheduler::WebThreadImplForUtilityThread()) {
+#if defined(V8_USE_EXTERNAL_STARTUP_DATA)
+    gin::V8Initializer::LoadV8Snapshot();
+    gin::V8Initializer::LoadV8Natives();
+#endif
+
+    blink::initialize(this);
+  }
+
+  ~BlinkInitializer() override {}
+
+ private:
+  std::unique_ptr<blink::scheduler::WebThreadImplForUtilityThread> main_thread_;
+
+  DISALLOW_COPY_AND_ASSIGN(BlinkInitializer);
+};
+
+base::LazyInstance<BlinkInitializer>::Leaky g_blink_initializer =
+    LAZY_INSTANCE_INITIALIZER;
+
+class ImageDecoderImplTest : public testing::Test {
+ public:
+  ImageDecoderImplTest() : decoder_(nullptr) {}
+  ~ImageDecoderImplTest() override {}
+
+  void SetUp() override { g_blink_initializer.Get(); }
+
+ protected:
+  ImageDecoderImpl* decoder() { return &decoder_; }
+
+ private:
+  base::MessageLoop message_loop_;
+  ImageDecoderImpl decoder_;
+};
+
+}  // namespace
+
+// Test that DecodeImage() doesn't return image message > (max message size)
+TEST_F(ImageDecoderImplTest, DecodeImageSizeLimit) {
+  // Approx max height for 3:2 image that will fit in the allotted space.
+  // 1.5 for width/height ratio, 4 for bytes/pixel.
+  int max_height_for_msg = sqrt(kTestMaxImageSize / (1.5 * 4));
+  int base_msg_size = sizeof(skia::mojom::Bitmap::Data_);
+
+  // Sizes which should trigger dimension-halving 0, 1 and 2 times
+  int heights[] = {max_height_for_msg - 10,
+                   max_height_for_msg + 10,
+                   2 * max_height_for_msg + 10};
+  int widths[] = {heights[0] * 3 / 2, heights[1] * 3 / 2, heights[2] * 3 / 2};
+  for (size_t i = 0; i < arraysize(heights); i++) {
+    std::vector<unsigned char> jpg;
+    ASSERT_TRUE(CreateJPEGImage(widths[i], heights[i], SK_ColorRED, &jpg));
+
+    Request request(decoder());
+    request.DecodeImage(jpg, true);
+    ASSERT_FALSE(request.bitmap().isNull());
+
+    // Check that image has been shrunk appropriately
+    EXPECT_LT(request.bitmap().computeSize64() + base_msg_size,
+              kTestMaxImageSize);
+// Android does its own image shrinking for memory conservation deeper in
+// the decode, so more specific tests here won't work.
+#if !defined(OS_ANDROID)
+    EXPECT_EQ(widths[i] >> i, request.bitmap().width());
+    EXPECT_EQ(heights[i] >> i, request.bitmap().height());
+
+    // Check that if resize not requested and image exceeds IPC size limit,
+    // an empty image is returned
+    if (heights[i] > max_height_for_msg) {
+      Request request(decoder());
+      request.DecodeImage(jpg, false);
+      EXPECT_TRUE(request.bitmap().isNull());
+    }
+#endif
+  }
+}
+
+TEST_F(ImageDecoderImplTest, DecodeImageFailed) {
+  // The "jpeg" is just some "random" data;
+  const char kRandomData[] = "u gycfy7xdjkhfgui bdui ";
+  std::vector<unsigned char> jpg(kRandomData,
+                                 kRandomData + sizeof(kRandomData));
+
+  Request request(decoder());
+  request.DecodeImage(jpg, false);
+  EXPECT_TRUE(request.bitmap().isNull());
+}
+
+}  // namespace image_decoder
diff --git a/services/image_decoder/image_decoder_service.cc b/services/image_decoder/image_decoder_service.cc
new file mode 100644
index 0000000..ddfbf93d
--- /dev/null
+++ b/services/image_decoder/image_decoder_service.cc
@@ -0,0 +1,66 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/image_decoder/image_decoder_service.h"
+
+#include "base/macros.h"
+#include "mojo/public/cpp/bindings/strong_binding.h"
+#include "services/image_decoder/image_decoder_impl.h"
+#include "services/image_decoder/public/interfaces/image_decoder.mojom.h"
+#include "services/service_manager/public/cpp/interface_registry.h"
+#include "services/service_manager/public/cpp/service_context.h"
+
+namespace image_decoder {
+
+namespace {
+
+void OnConnectionLost(std::unique_ptr<service_manager::ServiceContextRef> ref) {
+  // No-op. This merely takes ownership of |ref| so it can be destroyed when
+  // this function is invoked.
+}
+
+void OnImageDecoderRequest(
+    service_manager::ServiceContextRefFactory* ref_factory,
+    mojom::ImageDecoderRequest request) {
+  mojo::MakeStrongBinding(
+      base::MakeUnique<ImageDecoderImpl>(ref_factory->CreateRef()),
+      std::move(request));
+}
+
+}  // namespace
+
+ImageDecoderService::ImageDecoderService() = default;
+
+ImageDecoderService::~ImageDecoderService() = default;
+
+// static
+std::unique_ptr<service_manager::Service> ImageDecoderService::Create() {
+  return base::MakeUnique<ImageDecoderService>();
+}
+
+void ImageDecoderService::OnStart(service_manager::ServiceContext* context) {
+  ref_factory_.reset(new service_manager::ServiceContextRefFactory(
+      base::Bind(&service_manager::ServiceContext::RequestQuit,
+                 base::Unretained(context))));
+}
+
+bool ImageDecoderService::OnConnect(
+    const service_manager::ServiceInfo& remote_info,
+    service_manager::InterfaceRegistry* registry) {
+  // Add a reference to the service and tie it to the lifetime of the
+  // InterfaceRegistry's connection.
+  std::unique_ptr<service_manager::ServiceContextRef> connection_ref =
+      ref_factory_->CreateRef();
+  registry->AddConnectionLostClosure(
+      base::Bind(&OnConnectionLost, base::Passed(&connection_ref)));
+  registry->AddInterface(
+      base::Bind(&OnImageDecoderRequest, ref_factory_.get()));
+  return true;
+}
+
+bool ImageDecoderService::OnStop() {
+  return true;
+}
+
+}  // namespace image_decoder
diff --git a/services/image_decoder/image_decoder_service.h b/services/image_decoder/image_decoder_service.h
new file mode 100644
index 0000000..2f4643f
--- /dev/null
+++ b/services/image_decoder/image_decoder_service.h
@@ -0,0 +1,39 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_IMAGE_DECODER_IMAGE_DECODER_SERVICE_H_
+#define SERVICES_IMAGE_DECODER_IMAGE_DECODER_SERVICE_H_
+
+#include <memory>
+
+#include "base/callback.h"
+#include "base/macros.h"
+#include "services/service_manager/public/cpp/service.h"
+#include "services/service_manager/public/cpp/service_context_ref.h"
+
+namespace image_decoder {
+
+class ImageDecoderService : public service_manager::Service {
+ public:
+  ImageDecoderService();
+  ~ImageDecoderService() override;
+
+  // Factory function for use as an embedded service.
+  static std::unique_ptr<service_manager::Service> Create();
+
+  // service_manager::Service:
+  void OnStart(service_manager::ServiceContext* context) override;
+  bool OnConnect(const service_manager::ServiceInfo& remote_info,
+                 service_manager::InterfaceRegistry* registry) override;
+  bool OnStop() override;
+
+ private:
+  std::unique_ptr<service_manager::ServiceContextRefFactory> ref_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(ImageDecoderService);
+};
+
+}  // namespace image_decoder
+
+#endif  // SERVICES_IMAGE_DECODER_IMAGE_DECODER_SERVICE_H_
diff --git a/services/image_decoder/manifest.json b/services/image_decoder/manifest.json
new file mode 100644
index 0000000..69aaf730
--- /dev/null
+++ b/services/image_decoder/manifest.json
@@ -0,0 +1,12 @@
+{
+  "name": "service:image_decoder",
+  "display_name": "Image Decoder Service",
+  "interface_provider_specs": {
+    "service_manager:connector": {
+      "provides": {
+        "decode": [ "image_decoder::mojom::ImageDecoder" ]
+      },
+      "requires": {}
+    }
+  }
+}
diff --git a/services/image_decoder/public/cpp/BUILD.gn b/services/image_decoder/public/cpp/BUILD.gn
new file mode 100644
index 0000000..8bc897a
--- /dev/null
+++ b/services/image_decoder/public/cpp/BUILD.gn
@@ -0,0 +1,19 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//mojo/public/tools/bindings/mojom.gni")
+
+source_set("cpp") {
+  sources = [
+    "constants.cc",
+    "constants.h",
+    "decode.cc",
+    "decode.h",
+  ]
+
+  public_deps = [
+    "//services/image_decoder/public/interfaces",
+    "//services/service_manager/public/cpp",
+  ]
+}
diff --git a/services/image_decoder/public/cpp/constants.cc b/services/image_decoder/public/cpp/constants.cc
new file mode 100644
index 0000000..e87e47bb
--- /dev/null
+++ b/services/image_decoder/public/cpp/constants.cc
@@ -0,0 +1,11 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/image_decoder/public/cpp/constants.h"
+
+namespace image_decoder {
+
+const char kServiceName[] = "service:image_decoder";
+
+}  // namespace image_decoder
diff --git a/services/image_decoder/public/cpp/constants.h b/services/image_decoder/public/cpp/constants.h
new file mode 100644
index 0000000..9c9029d
--- /dev/null
+++ b/services/image_decoder/public/cpp/constants.h
@@ -0,0 +1,14 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_IMAGE_DECODER_PUBLIC_CPP_CONSTANTS_H_
+#define SERVICES_IMAGE_DECODER_PUBLIC_CPP_CONSTANTS_H_
+
+namespace image_decoder {
+
+extern const char kServiceName[];
+
+}  // namespace image_decoder
+
+#endif  // SERVICES_IMAGE_DECODER_PUBLIC_CPP_CONSTANTS_H_
diff --git a/services/image_decoder/public/cpp/decode.cc b/services/image_decoder/public/cpp/decode.cc
new file mode 100644
index 0000000..24dbd5bf
--- /dev/null
+++ b/services/image_decoder/public/cpp/decode.cc
@@ -0,0 +1,49 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/image_decoder/public/cpp/decode.h"
+
+#include "services/image_decoder/public/cpp/constants.h"
+#include "services/service_manager/public/cpp/connector.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+namespace image_decoder {
+
+namespace {
+
+// Helper callback which owns an ImageDecoderPtr until invoked. This keeps the
+// ImageDecoder pipe open just long enough to dispatch a reply, at which point
+// the reply is forwarded to the wrapped |callback|.
+void OnDecodeImage(mojom::ImageDecoderPtr decoder,
+                   const mojom::ImageDecoder::DecodeImageCallback& callback,
+                   const SkBitmap& bitmap) {
+  callback.Run(bitmap);
+}
+
+// Called in the case of a connection error on an ImageDecoder proxy.
+void OnConnectionError(
+    const mojom::ImageDecoder::DecodeImageCallback& callback) {
+  SkBitmap null_bitmap;
+  callback.Run(null_bitmap);
+}
+
+}  // namespace
+
+void Decode(service_manager::Connector* connector,
+            const std::vector<uint8_t>& encoded_bytes,
+            mojom::ImageCodec codec,
+            bool shrink_to_fit,
+            uint64_t max_size_in_bytes,
+            const mojom::ImageDecoder::DecodeImageCallback& callback) {
+  mojom::ImageDecoderPtr decoder;
+  connector->ConnectToInterface(kServiceName, &decoder);
+  decoder.set_connection_error_handler(
+      base::Bind(&OnConnectionError, callback));
+  mojom::ImageDecoder* raw_decoder = decoder.get();
+  raw_decoder->DecodeImage(
+      encoded_bytes, codec, shrink_to_fit, max_size_in_bytes,
+      base::Bind(&OnDecodeImage, base::Passed(&decoder), callback));
+}
+
+}  // namespace image_decoder
diff --git a/services/image_decoder/public/cpp/decode.h b/services/image_decoder/public/cpp/decode.h
new file mode 100644
index 0000000..3996172
--- /dev/null
+++ b/services/image_decoder/public/cpp/decode.h
@@ -0,0 +1,35 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_IMAGE_DECODER_PUBLIC_CPP_DECODE_H_
+#define SERVICES_IMAGE_DECODER_PUBLIC_CPP_DECODE_H_
+
+#include <stdint.h>
+
+#include <vector>
+
+#include "services/image_decoder/public/interfaces/image_decoder.mojom.h"
+
+namespace service_manager {
+class Connector;
+}
+
+namespace image_decoder {
+
+const uint64_t kDefaultMaxSizeInBytes = 128 * 1024 * 1024;
+
+// Helper function to decode an image via the image_decoder service. Upon
+// completion, |callback| is invoked on the calling thread TaskRunner with an
+// SkBitmap argument. The SkBitmap will be null on failure and non-null on
+// success.
+void Decode(service_manager::Connector* connector,
+            const std::vector<uint8_t>& encoded_bytes,
+            mojom::ImageCodec codec,
+            bool shrink_to_fit,
+            uint64_t max_size_in_bytes,
+            const mojom::ImageDecoder::DecodeImageCallback& callback);
+
+}  // namespace image_decoder
+
+#endif  // SERVICES_IMAGE_DECODER_PUBLIC_CPP_DECODE_H_
diff --git a/services/image_decoder/public/interfaces/BUILD.gn b/services/image_decoder/public/interfaces/BUILD.gn
new file mode 100644
index 0000000..18abeac5
--- /dev/null
+++ b/services/image_decoder/public/interfaces/BUILD.gn
@@ -0,0 +1,15 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//mojo/public/tools/bindings/mojom.gni")
+
+mojom("interfaces") {
+  sources = [
+    "image_decoder.mojom",
+  ]
+
+  public_deps = [
+    "//skia/public/interfaces",
+  ]
+}
diff --git a/services/image_decoder/public/interfaces/OWNERS b/services/image_decoder/public/interfaces/OWNERS
new file mode 100644
index 0000000..08850f4
--- /dev/null
+++ b/services/image_decoder/public/interfaces/OWNERS
@@ -0,0 +1,2 @@
+per-file *.mojom=set noparent
+per-file *.mojom=file://ipc/SECURITY_OWNERS
diff --git a/services/image_decoder/public/interfaces/image_decoder.mojom b/services/image_decoder/public/interfaces/image_decoder.mojom
new file mode 100644
index 0000000..39a9acb
--- /dev/null
+++ b/services/image_decoder/public/interfaces/image_decoder.mojom
@@ -0,0 +1,28 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module image_decoder.mojom;
+
+import "skia/public/interfaces/bitmap.mojom";
+
+enum ImageCodec {
+  DEFAULT,
+  ROBUST_JPEG,
+  ROBUST_PNG,
+};
+
+interface ImageDecoder {
+  // Decodes image data to a raw skia bitmap.
+  //
+  // If the total size of the decoded image data in bytes exceeds
+  // |max_size_in_bytes| and |shrink_to_fit| is true, the image is halved
+  // successively until its total size no longer exceeds |max_size_in_bytes|.
+  //
+  // If the total size of the decoded image data in bytes exceeds
+  // |max_size_in_bytes| and |shrink_to_fit| is false, this is treated as a
+  // decoding failure and the |decoded_image| response is null.
+  DecodeImage(array<uint8> encoded_data, ImageCodec codec, bool shrink_to_fit,
+              int64 max_size_in_bytes)
+      => (skia.mojom.Bitmap? decoded_image);
+};
diff --git a/services/service_manager/public/cpp/connector.h b/services/service_manager/public/cpp/connector.h
index 1b39e977..5251b26f 100644
--- a/services/service_manager/public/cpp/connector.h
+++ b/services/service_manager/public/cpp/connector.h
@@ -109,6 +109,9 @@
   // The returned object may be passed multiple times until Connect() is called,
   // at which point this method must be called again to pass again.
   virtual std::unique_ptr<Connector> Clone() = 0;
+
+  // Binds a Connector request to the other end of this Connector.
+  virtual void BindRequest(mojom::ConnectorRequest request) = 0;
 };
 
 }  // namespace service_manager
diff --git a/services/service_manager/public/cpp/lib/connector_impl.cc b/services/service_manager/public/cpp/lib/connector_impl.cc
index d4d4ec7..e704037 100644
--- a/services/service_manager/public/cpp/lib/connector_impl.cc
+++ b/services/service_manager/public/cpp/lib/connector_impl.cc
@@ -94,6 +94,12 @@
   return base::MakeUnique<ConnectorImpl>(connector.PassInterface());
 }
 
+void ConnectorImpl::BindRequest(mojom::ConnectorRequest request) {
+  if (!BindIfNecessary())
+    return;
+  connector_->Clone(std::move(request));
+}
+
 bool ConnectorImpl::BindIfNecessary() {
   // Bind this object to the current thread the first time it is used to
   // connect.
diff --git a/services/service_manager/public/cpp/lib/connector_impl.h b/services/service_manager/public/cpp/lib/connector_impl.h
index 487a33b0..a07858c 100644
--- a/services/service_manager/public/cpp/lib/connector_impl.h
+++ b/services/service_manager/public/cpp/lib/connector_impl.h
@@ -27,6 +27,7 @@
   std::unique_ptr<Connection> Connect(const std::string& name) override;
   std::unique_ptr<Connection> Connect(ConnectParams* params) override;
   std::unique_ptr<Connector> Clone() override;
+  void BindRequest(mojom::ConnectorRequest request) override;
 
   bool BindIfNecessary();
 
diff --git a/services/service_manager/public/service_manifest.gni b/services/service_manager/public/service_manifest.gni
index 58e38d48..22fc38e 100644
--- a/services/service_manager/public/service_manifest.gni
+++ b/services/service_manager/public/service_manifest.gni
@@ -2,6 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import("//build/config/dcheck_always_on.gni")
 import("//services/service_manager/public/constants.gni")
 
 # Used to produce a Service Manifest for a Service.
@@ -104,6 +105,10 @@
       "--output=$rebase_output",
     ]
 
+    if (is_debug || dcheck_always_on) {
+      args += [ "--pretty" ]
+    }
+
     if (defined(invoker.overlays)) {
       args += [ "--overlays" ]
       foreach(overlay_path, invoker.overlays) {
@@ -112,6 +117,7 @@
     }
 
     if (defined(invoker.packaged_services)) {
+      args += [ "--packaged-services" ]
       foreach(name, invoker.packaged_services) {
         input = "$root_out_dir/$packages_directory/$name/manifest.json"
         inputs += [ input ]
@@ -131,17 +137,134 @@
     all_deps += invoker.deps
   }
 
-  group("${target_name}__is_service_manifest") {
+  group("${target_name}__is_service_manifest_or_overlay") {
   }
 
   # Explicitly ensure that all dependencies are service_manifest
-  # targets themselves.
+  # or service_manifest_overlay targets themselves.
   group("${target_name}__check_deps_are_all_service_manifest") {
     deps = []
     foreach(d, all_deps) {
       name = get_label_info(d, "label_no_toolchain")
       toolchain = get_label_info(d, "toolchain")
-      deps += [ "${name}__is_service_manifest(${toolchain})" ]
+      deps += [ "${name}__is_service_manifest_or_overlay(${toolchain})" ]
+    }
+  }
+}
+
+# A simple derivative of the service_manifest template above. This allows for
+# embedding of packaged services into a manifest overlay.
+#
+# Parameters:
+#
+#   source
+#       The manifest overlay file to procss.
+#
+#   overlays (optional)
+#       A list of partial manifests to overlay onto the source manifest before
+#       emitting the final output. Overlays are applied as the last step, after
+#       any packaged manifests are embedded.
+#
+#   output_name (optional)
+#       The output name of the manifest. Defaults to the target name.
+#
+#   deps (optional)
+#       An array of the service_manifest targets this overlay depends on.
+#
+#   packaged_services (optional)
+#       An array of names of the services packaged into this overlay.
+#
+# Outputs:
+#
+#   An instantiation of this template produces in
+#       ${root_gen_dir}/${output_name}.json
+#
+template("service_manifest_overlay") {
+  assert(defined(invoker.source),
+         "\"source\" must be defined for the $target_name template")
+  if (defined(invoker.deps)) {
+    assert(defined(invoker.packaged_services) || defined(invoker.overlays),
+           "\"deps\" implies that you also want \"packaged_services\" and/or" +
+               "\"overlays\", but you have neither.")
+  }
+  if (defined(invoker.packaged_services)) {
+    assert(defined(invoker.deps),
+           "\"deps\" building the dependent packaged services must be " +
+               "provided.")
+  }
+  if (defined(invoker.type)) {
+    assert(invoker.type == "mojo" || invoker.type == "exe",
+           "\"type\" must be one of \"mojo\" or \"exe\".")
+  }
+
+  action(target_name) {
+    script =
+        "//services/service_manager/public/tools/manifest/manifest_collator.py"
+
+    inputs = [
+      invoker.source,
+    ]
+    if (defined(invoker.overlays)) {
+      inputs += invoker.overlays
+    }
+
+    output_name = target_name
+    if (defined(invoker.output_name)) {
+      output_name = invoker.output_name
+    }
+    output = "${root_gen_dir}/${output_name}.json"
+
+    outputs = [
+      output,
+    ]
+
+    rebase_parent = rebase_path(invoker.source, root_build_dir)
+    rebase_output = rebase_path(output, root_build_dir)
+
+    args = [
+      "--parent=$rebase_parent",
+      "--output=$rebase_output",
+    ]
+
+    if (is_debug || dcheck_always_on) {
+      args += [ "--pretty" ]
+    }
+
+    if (defined(invoker.overlays)) {
+      args += [ "--overlays" ]
+      foreach(overlay_path, invoker.overlays) {
+        args += [ rebase_path(overlay_path, root_build_dir) ]
+      }
+    }
+
+    if (defined(invoker.packaged_services)) {
+      args += [ "--packaged-services" ]
+      foreach(name, invoker.packaged_services) {
+        input = "$root_out_dir/$packages_directory/$name/manifest.json"
+        inputs += [ input ]
+        args += [ rebase_path(input, root_build_dir) ]
+      }
+    }
+
+    deps = []
+    data_deps = []
+    if (defined(invoker.deps)) {
+      deps += invoker.deps
+      data_deps += invoker.deps
+    }
+  }
+
+  group("${target_name}__is_service_manifest_or_overlay") {
+  }
+
+  # Explicitly ensure that all dependencies are service_manifest
+  # or service_manifest_overlay targets themselves.
+  group("${target_name}__check_deps_are_all_service_manifest") {
+    deps = []
+    foreach(d, all_deps) {
+      name = get_label_info(d, "label_no_toolchain")
+      toolchain = get_label_info(d, "toolchain")
+      deps += [ "${name}__is_service_manifest_or_overlay(${toolchain})" ]
     }
   }
 }
diff --git a/services/service_manager/public/tools/manifest/manifest_collator.py b/services/service_manager/public/tools/manifest/manifest_collator.py
index 1d2d76e..3944d2f 100755
--- a/services/service_manager/public/tools/manifest/manifest_collator.py
+++ b/services/service_manager/public/tools/manifest/manifest_collator.py
@@ -72,8 +72,11 @@
   parser.add_argument("--parent")
   parser.add_argument("--output")
   parser.add_argument("--name")
+  parser.add_argument("--pretty", action="store_true")
   parser.add_argument("--overlays", nargs="+", dest="overlays", default=[])
-  args, children = parser.parse_known_args()
+  parser.add_argument("--packaged-services", nargs="+",
+                      dest="packaged_services", default=[])
+  args, _ = parser.parse_known_args()
 
   parent = ParseJSONFile(args.parent)
 
@@ -82,23 +85,26 @@
     raise ValueError("Service name path component '%s' must not start " \
                      "with //" % service_path)
 
-  if args.name != service_path:
+  if args.name and args.name != service_path:
     raise ValueError("Service name '%s' specified in build file does not " \
                      "match name '%s' specified in manifest." %
                      (args.name, service_path))
 
-  services = []
-  for child in children:
-    services.append(ParseJSONFile(child))
+  packaged_services = []
+  for child_manifest in args.packaged_services:
+    packaged_services.append(ParseJSONFile(child_manifest))
 
-  if len(services) > 0:
-    parent['services'] = services
+  if len(packaged_services) > 0:
+    if 'services' not in parent:
+      parent['services'] = packaged_services
+    else:
+      parent['services'].extend(packaged_services)
 
   for overlay_path in args.overlays:
     MergeManifestOverlay(parent, ParseJSONFile(overlay_path))
 
   with open(args.output, 'w') as output_file:
-    json.dump(parent, output_file)
+    json.dump(parent, output_file, indent=2 if args.pretty else -1)
 
   return 0