Bug 1304098 - SVG cursors are no longer HiDPI. r?mstange draft
authorEvan Wallace <evan.exe@gmail.com>
Sat, 09 Dec 2017 15:10:55 -0800
changeset 710552 98ebc1eea3f0b316e244b8fb54a60eeae1dbc8fc
parent 710280 5f52c2488a831edbc33fa0bc6003ed4df9a62732
child 743601 92b4947baf8be53f537a6015feea3ff75c5f3dbc
push id92846
push userbmo:evan.exe@gmail.com
push dateMon, 11 Dec 2017 00:02:26 +0000
reviewersmstange
bugs1304098
milestone59.0a1
Bug 1304098 - SVG cursors are no longer HiDPI. r?mstange This change fixes custom CSS cursors on high-DPI displays in Windows. The problem was that custom cursor images were being rasterized at 1x before being passed across the IPC boundary. That results in a blurry cursor if the image being rasterized was a vector image (e.g. SVG). The fix is to rasterize the vector image at the correct high-DPI scale and also pass the scale factor along so the other end of the IPC message can scale the image back down appropriately. MozReview-Commit-ID: 3mlCGdWt1Hc
dom/base/nsDOMWindowUtils.cpp
dom/interfaces/base/nsIDOMWindowUtils.idl
dom/ipc/PBrowser.ipdl
dom/ipc/TabParent.cpp
dom/ipc/TabParent.h
widget/PuppetWidget.cpp
widget/nsBaseWidget.cpp
widget/nsBaseWidget.h
widget/nsIWidget.h
widget/tests/chrome.ini
widget/tests/test_custom_cursor_win.xul
widget/tests/window_custom_cursor_win.html
widget/windows/nsWindow.cpp
widget/windows/nsWindow.h
widget/windows/nsWindowGfx.cpp
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -65,16 +65,17 @@
 
 #if defined(MOZ_X11) && defined(MOZ_WIDGET_GTK)
 #include <gdk/gdk.h>
 #include <gdk/gdkx.h>
 #endif
 
 #include "Layers.h"
 #include "gfxPrefs.h"
+#include "gfxUtils.h"
 
 #include "mozilla/dom/AudioDeviceInfo.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/TabChild.h"
 #include "mozilla/dom/IDBFactoryBinding.h"
 #include "mozilla/dom/IDBMutableFileBinding.h"
 #include "mozilla/dom/IDBMutableFile.h"
 #include "mozilla/dom/IndexedDatabaseManager.h"
@@ -105,16 +106,17 @@
 #include "nsNetUtil.h"
 #include "nsDocument.h"
 #include "HTMLImageElement.h"
 #include "HTMLCanvasElement.h"
 #include "mozilla/css/ImageLoader.h"
 #include "mozilla/layers/APZCTreeManager.h" // for layers::ZoomToRectBehavior
 #include "mozilla/dom/Promise.h"
 #include "mozilla/StyleSheetInlines.h"
+#include "mozilla/gfx/DataSurfaceHelpers.h"
 #include "mozilla/gfx/GPUProcessManager.h"
 #include "mozilla/dom/TimeoutManager.h"
 #include "mozilla/PreloadedStyleSheet.h"
 #include "mozilla/layers/WebRenderBridgeChild.h"
 #include "mozilla/layers/WebRenderLayerManager.h"
 
 #ifdef XP_WIN
 #undef GetClassName
@@ -683,16 +685,56 @@ nsDOMWindowUtils::GetPresShellId(uint32_
   if (presShell) {
     *aPresShellId = presShell->GetPresShellId();
     return NS_OK;
   }
   return NS_ERROR_FAILURE;
 }
 
 NS_IMETHODIMP
+nsDOMWindowUtils::GetCursorForTests(JSContext* aCx, JS::MutableHandleValue aResult)
+{
+  nsCOMPtr<nsIWidget> widget = GetWidget();
+  NS_ENSURE_TRUE(widget, NS_ERROR_FAILURE);
+
+  RefPtr<mozilla::gfx::SourceSurface> cursorSurface;
+  uint32_t hotspotX = 0, hotspotY = 0;
+  nsresult rv = widget->GetCursorForTests(cursorSurface, hotspotX, hotspotY);
+  NS_ENSURE_SUCCESS(rv, rv);
+  NS_ENSURE_TRUE(cursorSurface, NS_ERROR_FAILURE);
+
+  RefPtr<DataSourceSurface> dataSurface = cursorSurface->GetDataSurface();
+  NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
+
+  UniquePtr<uint8_t[]> pixels = SurfaceToPackedBGRA(dataSurface);
+  NS_ENSURE_TRUE(pixels, NS_ERROR_FAILURE);
+
+  IntSize size = cursorSurface->GetSize();
+  int length = size.width * size.height * 4;
+  gfxUtils::ConvertBGRAtoRGBA(pixels.get(), length);
+
+  JS::RootedObject obj(aCx, JS_NewObject(aCx, nullptr));
+  JS::RootedValue hotspotValueX(aCx, JS::Int32Value(hotspotX));
+  JS::RootedValue hotspotValueY(aCx, JS::Int32Value(hotspotY));
+  JS::RootedValue WidthValue(aCx, JS::Int32Value(size.width));
+  JS::RootedValue heightValue(aCx, JS::Int32Value(size.height));
+  JS::RootedString rgbaString(aCx, JS_NewStringCopyN(aCx,
+    reinterpret_cast<const char*>(pixels.get()), length));
+  JS::RootedValue rgbaValue(aCx, JS::StringValue(rgbaString));
+
+  JS_SetProperty(aCx, obj, "hotspotX", hotspotValueX);
+  JS_SetProperty(aCx, obj, "hotspotY", hotspotValueY);
+  JS_SetProperty(aCx, obj, "width", WidthValue);
+  JS_SetProperty(aCx, obj, "height", heightValue);
+  JS_SetProperty(aCx, obj, "rgba", rgbaValue);
+  aResult.setObject(*obj);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsDOMWindowUtils::SendMouseEvent(const nsAString& aType,
                                  float aX,
                                  float aY,
                                  int32_t aButton,
                                  int32_t aClickCount,
                                  int32_t aModifiers,
                                  bool aIgnoreRootScrollFrame,
                                  float aPressure,
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -255,16 +255,32 @@ interface nsIDOMWindowUtils : nsISupport
    *
    * Can only be accessed with chrome privileges.
    */
   attribute boolean isFirstPaint;
 
   uint32_t getPresShellId();
 
   /**
+   * This is used for testing custom CSS cursor images. The returned value is a
+   * JavaScript object that looks like this:
+   *
+   *   {
+   *     width: the pixel width of the image,
+   *     height: the pixel height of the image,
+   *     hotspotX: the x coordinate of the hotspot,
+   *     hotspotY: the x coordinate of the hotspot,
+   *     rgba: a string of length "width * height * 4" containing RGBA data
+   *   }
+   *
+   * Can only be accessed with chrome privileges.
+   */
+  [implicit_jscontext] jsval getCursorForTests();
+
+  /**
    * Following modifiers are for sent*Event() except sendNative*Event().
    * NOTE: MODIFIER_ALT, MODIFIER_CONTROL, MODIFIER_SHIFT and MODIFIER_META
    *       are must be same values as nsIDOMNSEvent::*_MASK for backward
    *       compatibility.
    */
   const long MODIFIER_ALT        = 0x0001;
   const long MODIFIER_CONTROL    = 0x0002;
   const long MODIFIER_SHIFT      = 0x0004;
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -370,34 +370,45 @@ parent:
      */
     async SetCursor(uint32_t value, bool force);
 
     /**
      * Set the native cursor using a custom image.
      * @param cursorData
      *   Serialized image data.
      * @param width
-     *   Width of the image.
+     *   Width of the image. This may be scaled by the display scale factor.
+     *   Vector images need to be pre-scaled before they pass across the IPC
+     *   boundary.
      * @param height
-     *   Height of the image.
+     *   Height of the image. This may be scaled by the display scale factor
+     *   for the same reason as the width parameter.
      * @param stride
      *   Stride used in the image data.
      * @param format
      *   Image format, see gfx::SurfaceFormat for possible values.
      * @param hotspotX
      *   Horizontal hotspot of the image, as specified by the css cursor property.
      * @param hotspotY
      *   Vertical hotspot of the image, as specified by the css cursor property.
      * @param force
      *   Invalidate any locally cached cursor settings and force an
      *   update.
+     * @param unscaledWidth
+     *   The original width of the image (i.e. not scaled by the display scale
+     *   factor). This may be different than the width parameter for vector
+     *   images.
+     * @param unscaledHeight
+     *   The original height of the image. This may be different than the height
+     *   parameter just like the unscaledWidth parameter.
      */
     async SetCustomCursor(nsCString cursorData, uint32_t width, uint32_t height,
-                          uint32_t stride, uint8_t format,
-                          uint32_t hotspotX, uint32_t hotspotY, bool force);
+                          uint32_t stride, uint8_t format, uint32_t hotspotX,
+                          uint32_t hotspotY, bool force, uint32_t unscaledWidth,
+                          uint32_t unscaledHeight);
 
     /**
      * Used to set the current text of the status tooltip.
      * Nowadays this is mainly used for link locations on hover.
      */
     async SetStatus(uint32_t type, nsString status);
 
     /**
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -1780,17 +1780,19 @@ TabParent::RecvSetCursor(const uint32_t&
 mozilla::ipc::IPCResult
 TabParent::RecvSetCustomCursor(const nsCString& aCursorData,
                                const uint32_t& aWidth,
                                const uint32_t& aHeight,
                                const uint32_t& aStride,
                                const uint8_t& aFormat,
                                const uint32_t& aHotspotX,
                                const uint32_t& aHotspotY,
-                               const bool& aForce)
+                               const bool& aForce,
+                               const uint32_t &aUnscaledWidth,
+                               const uint32_t &aUnscaledHeight)
 {
   mCursor = eCursorInvalid;
 
   nsCOMPtr<nsIWidget> widget = GetWidget();
   if (widget) {
     if (aForce) {
       widget->ClearCachedCursor();
     }
@@ -1799,17 +1801,23 @@ TabParent::RecvSetCustomCursor(const nsC
       const gfx::IntSize size(aWidth, aHeight);
 
       RefPtr<gfx::DataSourceSurface> customCursor =
           gfx::CreateDataSourceSurfaceFromData(size,
                                                static_cast<gfx::SurfaceFormat>(aFormat),
                                                reinterpret_cast<const uint8_t*>(aCursorData.BeginReading()),
                                                aStride);
 
-      RefPtr<gfxDrawable> drawable = new gfxSurfaceDrawable(customCursor, size);
+      Matrix matrix = Matrix::Scaling(
+        (float)aUnscaledWidth / (float)aWidth,
+        (float)aUnscaledHeight / (float)aHeight);
+
+      RefPtr<gfxDrawable> drawable = new gfxPatternDrawable(
+        new gfxPattern(customCursor, matrix), IntSize(aUnscaledWidth, aUnscaledHeight));
+
       nsCOMPtr<imgIContainer> cursorImage(image::ImageOps::CreateFromDrawable(drawable));
       widget->SetCursor(cursorImage, aHotspotX, aHotspotY);
       mCustomCursor = cursorImage;
       mCustomCursorHotspotX = aHotspotX;
       mCustomCursorHotspotY = aHotspotY;
     }
   }
 
--- a/dom/ipc/TabParent.h
+++ b/dom/ipc/TabParent.h
@@ -280,17 +280,19 @@ public:
 
   virtual mozilla::ipc::IPCResult RecvSetCustomCursor(const nsCString& aUri,
                                                       const uint32_t& aWidth,
                                                       const uint32_t& aHeight,
                                                       const uint32_t& aStride,
                                                       const uint8_t& aFormat,
                                                       const uint32_t& aHotspotX,
                                                       const uint32_t& aHotspotY,
-                                                      const bool& aForce) override;
+                                                      const bool& aForce,
+                                                      const uint32_t &aUnscaledWidth,
+                                                      const uint32_t &aUnscaledHeight) override;
 
   virtual mozilla::ipc::IPCResult RecvSetStatus(const uint32_t& aType,
                                                 const nsString& aStatus) override;
 
   virtual mozilla::ipc::IPCResult RecvIsParentWindowMainWidgetVisible(bool* aIsVisible) override;
 
   virtual mozilla::ipc::IPCResult RecvShowTooltip(const uint32_t& aX,
                                                   const uint32_t& aY,
--- a/widget/PuppetWidget.cpp
+++ b/widget/PuppetWidget.cpp
@@ -1008,18 +1008,30 @@ PuppetWidget::SetCursor(imgIContainer* a
   if (mCustomCursor == aCursor &&
       mCursorHotspotX == aHotspotX &&
       mCursorHotspotY == aHotspotY &&
       !mUpdateCursor) {
     return NS_OK;
   }
 #endif
 
+  int32_t width = 0;
+  int32_t height = 0;
+  nsresult rv;
+  rv = aCursor->GetWidth(&width);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = aCursor->GetHeight(&height);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  IntSize scaledSize(
+    NSToIntRound(width * mDefaultScale),
+    NSToIntRound(height * mDefaultScale));
+
   RefPtr<mozilla::gfx::SourceSurface> surface =
-    aCursor->GetFrame(imgIContainer::FRAME_CURRENT,
+    aCursor->GetFrameAtSize(scaledSize, imgIContainer::FRAME_CURRENT,
                       imgIContainer::FLAG_SYNC_DECODE);
   if (!surface) {
     return NS_ERROR_FAILURE;
   }
 
   RefPtr<mozilla::gfx::DataSourceSurface> dataSurface =
     surface->GetDataSurface();
   if (!dataSurface) {
@@ -1030,17 +1042,17 @@ PuppetWidget::SetCursor(imgIContainer* a
   int32_t stride;
   mozilla::UniquePtr<char[]> surfaceData =
     nsContentUtils::GetSurfaceData(WrapNotNull(dataSurface), &length, &stride);
 
   nsDependentCString cursorData(surfaceData.get(), length);
   mozilla::gfx::IntSize size = dataSurface->GetSize();
   if (!mTabChild->SendSetCustomCursor(cursorData, size.width, size.height, stride,
                                       static_cast<uint8_t>(dataSurface->GetFormat()),
-                                      aHotspotX, aHotspotY, mUpdateCursor)) {
+                                      aHotspotX, aHotspotY, mUpdateCursor, width, height)) {
     return NS_ERROR_FAILURE;
   }
 
   mCursor = eCursorInvalid;
   mCustomCursor = aCursor;
   mCursorHotspotX = aHotspotX;
   mCursorHotspotY = aHotspotY;
   mUpdateCursor = false;
--- a/widget/nsBaseWidget.cpp
+++ b/widget/nsBaseWidget.cpp
@@ -706,16 +706,22 @@ nsBaseWidget::SetCursor(nsCursor aCursor
 
 nsresult
 nsBaseWidget::SetCursor(imgIContainer* aCursor,
                         uint32_t aHotspotX, uint32_t aHotspotY)
 {
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
+nsresult
+nsBaseWidget::GetCursorForTests(RefPtr<mozilla::gfx::SourceSurface> &aCursor,
+                                uint32_t &aHotspotX, uint32_t &aHotspotY) {
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
 //-------------------------------------------------------------------------
 //
 // Window transparency methods
 //
 //-------------------------------------------------------------------------
 
 void nsBaseWidget::SetTransparencyMode(nsTransparencyMode aMode) {
 }
--- a/widget/nsBaseWidget.h
+++ b/widget/nsBaseWidget.h
@@ -172,16 +172,18 @@ public:
   virtual bool            IsFullyOccluded() const override
   {
     return mIsFullyOccluded;
   }
 
   virtual void            SetCursor(nsCursor aCursor) override;
   virtual nsresult        SetCursor(imgIContainer* aCursor,
                                     uint32_t aHotspotX, uint32_t aHotspotY) override;
+  virtual nsresult        GetCursorForTests(RefPtr<mozilla::gfx::SourceSurface> &aCursor,
+                                            uint32_t &aHotspotX, uint32_t &aHotspotY) override;
   virtual void            ClearCachedCursor() override { mUpdateCursor = true; }
   virtual void            SetTransparencyMode(nsTransparencyMode aMode) override;
   virtual nsTransparencyMode GetTransparencyMode() override;
   virtual void            GetWindowClipRegion(nsTArray<LayoutDeviceIntRect>* aRects) override;
   virtual void            SetWindowShadowStyle(int32_t aStyle) override {}
   virtual void            SetShowsToolbarButton(bool aShow) override {}
   virtual void            SetShowsFullScreenButton(bool aShow) override {}
   virtual void            SetWindowAnimationType(WindowAnimationType aType) override {}
--- a/widget/nsIWidget.h
+++ b/widget/nsIWidget.h
@@ -982,16 +982,29 @@ class nsIWidget : public nsISupports
      * @param aY the Y coordinate of the hotspot (from top).
      * @retval NS_ERROR_NOT_IMPLEMENTED if setting images as cursors is not
      *         supported
      */
     virtual nsresult SetCursor(imgIContainer* aCursor,
                                uint32_t aHotspotX, uint32_t aHotspotY) = 0;
 
     /**
+     * Returns the current custom cursor image and hotspot. This is only useful
+     * for tests.
+     *
+     * @param aCursor the cursor to get
+     * @param aX the X coordinate of the hotspot (from left).
+     * @param aY the Y coordinate of the hotspot (from top).
+     * @retval NS_ERROR_NOT_IMPLEMENTED if retrieving the cursor is not
+     *         supported
+     */
+    virtual nsresult GetCursorForTests(RefPtr<mozilla::gfx::SourceSurface> &aCursor,
+                                       uint32_t &aHotspotX, uint32_t &aHotspotY) = 0;
+
+    /**
      * Get the window type of this widget.
      */
     nsWindowType WindowType() { return mWindowType; }
 
     /**
      * Determines if this widget is one of the three types of plugin widgets.
      */
     bool IsPlugin() {
--- a/widget/tests/chrome.ini
+++ b/widget/tests/chrome.ini
@@ -94,13 +94,16 @@ skip-if = toolkit != "cocoa"
 [test_chrome_context_menus_win.xul]
 skip-if = toolkit != "windows"
 support-files = chrome_context_menus_win.xul
 [test_plugin_input_event.html]
 skip-if = toolkit != "windows"
 [test_mouse_scroll.xul]
 skip-if = toolkit != "windows"
 support-files = window_mouse_scroll_win.html
+[test_custom_cursor_win.xul]
+skip-if = toolkit != "windows"
+support-files = window_custom_cursor_win.html
 
 # Privacy relevant
 [test_bug1123480.xul]
 subsuite = clipboard
 
new file mode 100644
--- /dev/null
+++ b/widget/tests/test_custom_cursor_win.xul
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+                 type="text/css"?>
+<window title="Testing composition, text and query content events"
+  onload="setTimeout(onLoad, 0);"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+<body  xmlns="http://www.w3.org/1999/xhtml">
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+function onLoad()
+{
+  SpecialPowers.pushPrefEnv({"set": [
+    ["security.data_uri.unique_opaque_origin", false]]}, runTest);
+}
+
+function runTest()
+{
+  window.open("window_custom_cursor_win.html", "_blank",
+              "chrome,width=600,height=600");
+}
+]]>
+</script>
+</window>
new file mode 100644
--- /dev/null
+++ b/widget/tests/window_custom_cursor_win.html
@@ -0,0 +1,221 @@
+<html lang="en-US">
+<head>
+  <title>Test for custom CSS cursors on Windows</title>
+  <script type="text/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+  <link rel="stylesheet" type="text/css"
+          href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+  <style>
+
+body {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  margin: 0;
+}
+
+  </style>
+</head>
+<body onunload="onUnload();">
+<script class="testbody" type="application/javascript">
+
+window.parent.wrappedJSObject.SimpleTest.waitForFocus(prepareTests, window);
+
+const nsIDOMWindowUtils = Components.interfaces.nsIDOMWindowUtils;
+const kLayoutCssDevPixelsPerPxPref = 'layout.css.devPixelsPerPx';
+
+function ok(aCondition, aMessage)
+{
+  window.parent.wrappedJSObject.SimpleTest.ok(aCondition, aMessage);
+}
+
+function is(aLeft, aRight, aMessage)
+{
+  window.parent.wrappedJSObject.SimpleTest.is(aLeft, aRight, aMessage);
+}
+
+function onUnload()
+{
+  SpecialPowers.setCharPref(kLayoutCssDevPixelsPerPxPref, '-1.0');
+  window.parent.wrappedJSObject.SimpleTest.finish();
+}
+
+function createDemoImage()
+{
+  const width = 27;
+  const height = 29;
+  const bytes = new Uint8Array(width * height * 4);
+
+  for (let y = 0, i = 0; y < height; y++) {
+    for (let x = 0; x < width; x++, i += 4) {
+      if ((x ^ y) & 1) continue;
+      if ((x ^ y) & 2) bytes[i] = 0xFF;
+      if ((x ^ y) & 4) bytes[i + 1] = 0xFF;
+      if ((x ^ y) & 8) bytes[i + 2] = 0xFF;
+      bytes[i + 3] = 0xFF;
+    }
+  }
+
+  return {width, height, bytes};
+}
+
+function convertImageToSVG({width, height, bytes})
+{
+  let svg = `<svg xmlns="http://www.w3.org/2000/svg"
+    width="${width}px" height="${height}px">`;
+  for (let y = 0, i = 0; y < height; y++) {
+    for (let x = 0; x < width; x++, i += 4) {
+      const r = bytes[i], g = bytes[i + 1], b = bytes[i + 2], a = bytes[i + 3];
+      const fill = `rgba(${r}, ${g}, ${b}, ${a / 255})`;
+      svg += `<rect x="${x}" y="${y}" width="1" height="1" fill="${fill}" />`;
+    }
+  }
+  svg += '</svg>';
+  return `data:image/svg+xml;base64,${btoa(svg)}`;
+}
+
+function convertImageToPNG({width, height, bytes})
+{
+  const canvas = document.createElement('canvas');
+  const context = canvas.getContext('2d');
+  const imageData = context.createImageData(width, height);
+  imageData.data.set(bytes);
+  canvas.width = width;
+  canvas.height = height;
+  context.putImageData(imageData, 0, 0);
+  return canvas.toDataURL();
+}
+
+async function bug1304098()
+{
+  const sleep = time => new Promise(resolve => setTimeout(resolve, time));
+  const utils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+               .getInterface(nsIDOMWindowUtils);
+
+  // Generate a custom cursor image
+  const {width, height, bytes} = createDemoImage();
+  const hotspotX = 26;
+  const hotspotY = 13;
+
+  // Check PNG cursors
+  const pngURL = convertImageToPNG({width, height, bytes});
+  await checkCursor('PNG cursor @1x', pngURL, '1', checkCursor1x);
+  await checkCursor('PNG cursor @2x', pngURL, '2', checkCursor2xScaled);
+
+  // Check SVG cursors
+  const svgURL = convertImageToSVG({width, height, bytes});
+  await checkCursor('SVG cursor @1x', svgURL, '1', checkCursor1x);
+  await checkCursor('SVG cursor @2x', svgURL, '2', checkCursor2xSharp);
+
+  async function jiggleMouse()
+  {
+    for (let i = 0; i < 3; i++) {
+      const scale = utils.screenPixelsPerCSSPixel;
+      const x = (window.mozInnerScreenX + 50 + i) * scale;
+      const y = (window.mozInnerScreenY + 50 + i) * scale;
+      await utils.sendNativeMouseMove(x, y, null);
+      await sleep(100);
+    }
+  }
+
+  async function checkCursor(name, cursorURL, displayScale, checkCallback)
+  {
+    // Pretend we're on a screen with a specific display scale
+    SpecialPowers.setCharPref(kLayoutCssDevPixelsPerPxPref, displayScale);
+
+    // Change to the default cursor to clear the cached cursor
+    document.body.style.cursor = 'default';
+    await jiggleMouse();
+
+    // Change to our custom cursor
+    document.body.style.cursor = `url(${cursorURL}) ${hotspotX} ${hotspotY}, auto`;
+    await jiggleMouse();
+
+    // Check that the cursor is correct
+    const cursor = await utils.getCursorForTests();
+    checkCallback(name, cursor);
+  }
+
+  function checkCursor1x(name, cursor)
+  {
+    is(cursor.width, width, `Checking width for ${name}`);
+    is(cursor.height, height, `Checking height for ${name}`);
+    is(cursor.hotspotX, hotspotX, `Checking hotspotX for ${name}`);
+    is(cursor.hotspotY, hotspotY, `Checking hotspotY for ${name}`);
+
+    // Check that the cursor has the correct contents
+    const get = i => cursor.rgba.charCodeAt(i);
+    for (let y = 0, i = 0; y < height; y++) {
+      for (let x = 0; x < width; x++, i += 4) {
+        const expected = `${bytes[i]}, ${bytes[i + 1]}, ${bytes[i + 2]}, ${bytes[i + 3]}`;
+        const observed = `${get(i)}, ${get(i + 1)}, ${get(i + 2)}, ${get(i + 3)}`;
+
+        // Don't pollute the log with lots of checks
+        if (expected !== observed) {
+          is(observed, expected, `Checking pixel at (${x}, ${y}) for ${name}`);
+          return;
+        }
+      }
+    }
+  }
+
+  function checkCursor2xSharp(name, cursor)
+  {
+    is(cursor.width, width * 2, `Checking width for ${name}`);
+    is(cursor.height, height * 2, `Checking height for ${name}`);
+    is(cursor.hotspotX, hotspotX, `Checking hotspotX for ${name}`);
+    is(cursor.hotspotY, hotspotY, `Checking hotspotY for ${name}`);
+
+    // Check that the cursor has the correct contents
+    const get = i => cursor.rgba.charCodeAt(i);
+    for (let y = 0, i = 0; y < 2 * height; y++) {
+      for (let x = 0; x < 2 * width; x++, i += 4) {
+        const j = ((x >> 1) + (y >> 1) * width) * 4;
+        const expected = `${bytes[j]}, ${bytes[j + 1]}, ${bytes[j + 2]}, ${bytes[j + 3]}`;
+        const observed = `${get(i)}, ${get(i + 1)}, ${get(i + 2)}, ${get(i + 3)}`;
+
+        // Don't pollute the log with lots of checks
+        if (expected !== observed) {
+          is(observed, expected, `Checking pixel at (${x}, ${y}) for ${name}`);
+          return;
+        }
+      }
+    }
+  }
+
+  function checkCursor2xScaled(name, cursor)
+  {
+    is(cursor.width, width * 2, `Checking width for ${name}`);
+    is(cursor.height, height * 2, `Checking height for ${name}`);
+    is(cursor.hotspotX, hotspotX, `Checking hotspotX for ${name}`);
+    is(cursor.hotspotY, hotspotY, `Checking hotspotY for ${name}`);
+
+    // Raster cursors will be scaled up. Don't check the pixel values here
+    // because the details of the scaling algorithm shouldn't be baked into
+    // this test. Just check the scale to make sure it's 2x bigger.
+  }
+}
+
+async function prepareTests()
+{
+  try {
+    await bug1304098();
+
+    ok(true, 'Success');
+    window.close();
+  }
+
+  catch (e) {
+    ok(false, `Uncaught exception: ${e}`);
+    window.close();
+  }
+}
+
+</script>
+</body>
+
+</html>
--- a/widget/windows/nsWindow.cpp
+++ b/widget/windows/nsWindow.cpp
@@ -3029,16 +3029,85 @@ nsWindow::SetCursor(imgIContainer* aCurs
 
   if (sHCursor != nullptr)
     ::DestroyIcon(sHCursor);
   sHCursor = cursor;
 
   return NS_OK;
 }
 
+nsresult
+nsWindow::GetCursorForTests(RefPtr<mozilla::gfx::SourceSurface> &aCursor,
+                            uint32_t &aHotspotX, uint32_t &aHotspotY) {
+  ICONINFO iconInfo = {};
+
+  aCursor = nullptr;
+  aHotspotX = 0;
+  aHotspotY = 0;
+
+  if (!sHCursor || !GetIconInfo(sHCursor, &iconInfo)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  HDC dc = ::GetDC(nullptr);
+  auto releaseDC = MakeScopeExit([&] {
+    ::ReleaseDC(nullptr, dc);
+  });
+
+  BITMAPINFO bmInfo = {};
+  bmInfo.bmiHeader.biSize = sizeof(bmInfo.bmiHeader);
+
+  if (!GetDIBits(dc, iconInfo.hbmColor, 0, 0, nullptr, &bmInfo, DIB_RGB_COLORS)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  int width = bmInfo.bmiHeader.biWidth;
+  int height = abs(bmInfo.bmiHeader.biHeight);
+  int stride = width * 4;
+
+  RefPtr<DataSourceSurface> surface = Factory::CreateDataSourceSurface(
+    IntSize(width, height), SurfaceFormat::B8G8R8A8);
+
+  if (!surface || BytesPerPixel(surface->GetFormat()) != 4) {
+    return NS_ERROR_FAILURE;
+  }
+
+  DataSourceSurface::MappedSurface map;
+  bool mappedOK = surface->Map(DataSourceSurface::MapType::READ_WRITE, &map);
+
+  if (!mappedOK || map.mStride < stride) {
+    return NS_ERROR_FAILURE;
+  }
+
+  bmInfo.bmiHeader.biHeight = -height;
+  bmInfo.bmiHeader.biSizeImage = width * height * 4;
+  bmInfo.bmiHeader.biPlanes = 1;
+  bmInfo.bmiHeader.biBitCount = 32;
+  bmInfo.bmiHeader.biCompression = BI_RGB;
+
+  if (!GetDIBits(dc, iconInfo.hbmColor, 0, height, map.mData, &bmInfo, DIB_RGB_COLORS)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // GetDIBits returns data where all rows have been tightly packed together,
+  // but the mapped surface may have a larger stride. Expand the rows out by
+  // the stride of the surface so they're in the right place. This can be done
+  // in place if it's done bottom to top.
+  for (int y = height - 1; y > 0; y--) {
+    memmove(map.mData + y * map.mStride, map.mData + y * stride, stride);
+  }
+
+  surface->Unmap();
+
+  aCursor = surface;
+  aHotspotX = iconInfo.xHotspot;
+  aHotspotY = iconInfo.yHotspot;
+  return NS_OK;
+}
+
 /**************************************************************
  *
  * SECTION: nsIWidget::Get/SetTransparencyMode
  *
  * Manage the transparency mode of the window containing this
  * widget. Only works for popup and dialog windows when the
  * Desktop Window Manager compositor is not enabled.
  *
--- a/widget/windows/nsWindow.h
+++ b/widget/windows/nsWindow.h
@@ -145,16 +145,18 @@ public:
   virtual LayoutDeviceIntRect GetScreenBounds() override;
   virtual MOZ_MUST_USE nsresult GetRestoredBounds(LayoutDeviceIntRect& aRect) override;
   virtual LayoutDeviceIntRect GetClientBounds() override;
   virtual LayoutDeviceIntPoint GetClientOffset() override;
   void                    SetBackgroundColor(const nscolor& aColor) override;
   virtual nsresult        SetCursor(imgIContainer* aCursor,
                                     uint32_t aHotspotX, uint32_t aHotspotY) override;
   virtual void            SetCursor(nsCursor aCursor) override;
+  virtual nsresult        GetCursorForTests(RefPtr<mozilla::gfx::SourceSurface> &aCursor,
+                                            uint32_t &aHotspotX, uint32_t &aHotspotY) override;
   virtual nsresult        ConfigureChildren(const nsTArray<Configuration>& aConfigurations) override;
   virtual bool PrepareForFullscreenTransition(nsISupports** aData) override;
   virtual void PerformFullscreenTransition(FullscreenTransitionStage aStage,
                                            uint16_t aDuration,
                                            nsISupports* aData,
                                            nsIRunnable* aCallback) override;
   virtual nsresult        MakeFullScreen(bool aFullScreen,
                                          nsIScreen* aScreen = nullptr) override;
--- a/widget/windows/nsWindowGfx.cpp
+++ b/widget/windows/nsWindowGfx.cpp
@@ -465,17 +465,17 @@ nsresult nsWindowGfx::CreateIcon(imgICon
                                   IntSize aScaledSize,
                                   HICON *aIcon) {
 
   MOZ_ASSERT((aScaledSize.width > 0 && aScaledSize.height > 0) ||
              (aScaledSize.width == 0 && aScaledSize.height == 0));
 
   // Get the image data
   RefPtr<SourceSurface> surface =
-    aContainer->GetFrame(imgIContainer::FRAME_CURRENT,
+    aContainer->GetFrameAtSize(aScaledSize, imgIContainer::FRAME_CURRENT,
                          imgIContainer::FLAG_SYNC_DECODE);
   NS_ENSURE_TRUE(surface, NS_ERROR_NOT_AVAILABLE);
 
   IntSize frameSize = surface->GetSize();
   if (frameSize.IsEmpty()) {
     return NS_ERROR_FAILURE;
   }