bug 857089 - add a device-pixel zoom level to image viewer. r=bz
authorJonathan Kew <jkew@mozilla.com>
Thu, 18 Apr 2013 16:27:37 +0100
changeset 129208 b85353b6cc389d16ee2b072f1cdfb942f3b3b79c
parent 129207 1d31d983075a85f966d09130c3a608da197eb039
child 129209 0c0d7f9cb43d30fc08936ffac3f5de58ca0405ea
push id24562
push userryanvm@gmail.com
push dateFri, 19 Apr 2013 01:24:04 +0000
treeherdermozilla-central@f8d27fe5d7c0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz
bugs857089
milestone23.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 857089 - add a device-pixel zoom level to image viewer. r=bz
content/html/document/public/nsIImageDocument.idl
content/html/document/src/ImageDocument.cpp
content/html/document/test/test_bug369370.html
toolkit/themes/osx/global/media/TopLevelImageDocument.css
toolkit/themes/windows/global/media/TopLevelImageDocument.css
--- a/content/html/document/public/nsIImageDocument.idl
+++ b/content/html/document/public/nsIImageDocument.idl
@@ -6,41 +6,55 @@
 #include "nsISupports.idl"
 
 /**
  * @status UNDER_DEVELOPMENT
  */
 
 interface imgIRequest;
 
-[scriptable, uuid(7b80eebc-c98e-4461-8bdb-6e3b6e828890)]
+[scriptable, uuid(0A70F5AF-2A78-4B52-9D96-11FF2A091E88)]
 interface nsIImageDocument : nsISupports {
 
   /* Whether the pref for image resizing has been set. */
   readonly attribute boolean imageResizingEnabled;
 
   /* Whether the image is overflowing visible area. */
   readonly attribute boolean imageIsOverflowing;
 
   /* Whether the image has been resized to fit visible area. */
   readonly attribute boolean imageIsResized;
 
+  /* Whether the image has been scaled to device-pixel size. */
+  readonly attribute boolean imageIsScaledToDevicePixels;
+
   /* The image request being displayed in the content area */
   readonly attribute imgIRequest imageRequest;
 
   /* Resize the image to fit visible area. */
   void shrinkToFit();
 
-  /* Restore image original size. */
+  /* Restore image original size (1:1 image pixels : CSS px). */
   void restoreImage();
 
+  /* Set image to display at 1:1 image pixel : device-pixel size.
+   * If CSS px == device pixels, this is equivalent to restoreImage().
+   */
+  void scaleToDevicePixels();
+
   /* Restore the image, trying to keep a certain pixel in the same position.
    * The coordinate system is that of the shrunken image.
    */
   void restoreImageTo(in long x, in long y);
 
+  /* Scale to 1:1 device-pixel size, trying to keep a certain pixel
+   * in the same position.
+   * The coordinate system is that of the shrunken image.
+   */
+  void scaleToDevicePixelsTo(in long x, in long y);
+
   /* A helper method for switching between states.
    * The switching logic is as follows. If the image has been resized
    * restore image original size, otherwise if the image is overflowing
    * current visible area resize the image to fit the area.
    */
   void toggleImageSize();
 };  
--- a/content/html/document/src/ImageDocument.cpp
+++ b/content/html/document/src/ImageDocument.cpp
@@ -36,16 +36,17 @@
 #include "nsURILoader.h"
 #include "nsIDocShell.h"
 #include "nsIContentViewer.h"
 #include "nsIMarkupDocumentViewer.h"
 #include "nsThreadUtils.h"
 #include "nsIScrollableFrame.h"
 #include "nsContentUtils.h"
 #include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLImageElement.h"
 #include "mozilla/Preferences.h"
 #include <algorithm>
 
 #define AUTOMATIC_IMAGE_RESIZING_PREF "browser.enable_automatic_image_resizing"
 #define CLICK_IMAGE_RESIZING_PREF "browser.enable_click_image_resizing"
 //XXX A hack needed for Firefox's site specific zoom.
 #define SITE_SPECIFIC_ZOOM "browser.zoom.siteSpecific"
 
@@ -105,29 +106,47 @@ public:
   virtual nsXPCClassInfo* GetClassInfo();
 protected:
   virtual nsresult CreateSyntheticDocument();
 
   nsresult CheckOverflowing(bool changeState);
 
   void UpdateTitleAndCharset();
 
-  nsresult ScrollImageTo(int32_t aX, int32_t aY, bool restoreImage);
+  enum eScaleOptions {
+    eDevicePixelScale,
+    eCSSPixelScale
+  };
+  nsresult ScrollImageTo(int32_t aX, int32_t aY, eScaleOptions aScaleOption);
 
-  float GetRatio() {
+  float GetDevicePixelSizeInCSSPixels() {
+    nsIPresShell *shell = GetShell();
+    if (!shell) {
+      return 1.0f;
+    }
+    return shell->GetPresContext()->DevPixelsToFloatCSSPixels(1);
+  }
+
+  float GetShrinkToFitRatio() {
     return std::min(mVisibleWidth / mImageWidth,
                     mVisibleHeight / mImageHeight);
   }
 
+  float GetCurrentRatio() {
+    return mImageIsScaledToDevicePixels ? GetDevicePixelSizeInCSSPixels() :
+           mImageIsResized ? GetShrinkToFitRatio() : 1.0f;
+  }
+
   void ResetZoomLevel();
   float GetZoomLevel();
 
   enum eModeClasses {
     eNone,
     eShrinkToFit,
+    eScaleToDevicePixels,
     eOverflowing
   };
   void SetModeClass(eModeClasses mode);
 
   nsresult OnStartContainer(imgIRequest* aRequest, imgIContainer* aImage);
   nsresult OnStopRequest(imgIRequest *aRequest, nsresult aStatus);
 
   nsCOMPtr<nsIContent>          mImageContent;
@@ -135,18 +154,22 @@ protected:
   float                         mVisibleWidth;
   float                         mVisibleHeight;
   int32_t                       mImageWidth;
   int32_t                       mImageHeight;
 
   bool                          mResizeImageByDefault;
   bool                          mClickResizingEnabled;
   bool                          mImageIsOverflowing;
-  // mImageIsResized is true if the image is currently resized
+  // mImageIsResized is true if the image is resized to fit the window;
+  // mImageIsScaledToDevicePixels is true if the image is scaled to
+  // the 1:1 device-pixel size.
+  // These two flags cannot both be true at the same time.
   bool                          mImageIsResized;
+  bool                          mImageIsScaledToDevicePixels;
   // mShouldResize is true if the image should be resized when it doesn't fit
   // mImageIsResized cannot be true when this is false, but mImageIsResized
   // can be false when this is true
   bool                          mShouldResize;
   bool                          mFirstResize;
   // mObservingImageLoader is true while the observer is set.
   bool                          mObservingImageLoader;
 
@@ -391,16 +414,23 @@ ImageDocument::GetImageIsOverflowing(boo
 NS_IMETHODIMP
 ImageDocument::GetImageIsResized(bool* aImageIsResized)
 {
   *aImageIsResized = mImageIsResized;
   return NS_OK;
 }
 
 NS_IMETHODIMP
+ImageDocument::GetImageIsScaledToDevicePixels(bool* aImageIsScaledToDevPix)
+{
+  *aImageIsScaledToDevPix = mImageIsScaledToDevicePixels;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 ImageDocument::GetImageRequest(imgIRequest** aImageRequest)
 {
   nsCOMPtr<nsIImageLoadingContent> imageLoader = do_QueryInterface(mImageContent);
   if (imageLoader) {
     return imageLoader->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST,
                                    aImageRequest);
   }
 
@@ -417,98 +447,176 @@ ImageDocument::ShrinkToFit()
   if (GetZoomLevel() != mOriginalZoomLevel && mImageIsResized &&
       !nsContentUtils::IsChildOfSameType(this)) {
     return NS_OK;
   }
 
   // Keep image content alive while changing the attributes.
   nsCOMPtr<nsIContent> imageContent = mImageContent;
   nsCOMPtr<nsIDOMHTMLImageElement> image = do_QueryInterface(mImageContent);
-  image->SetWidth(std::max(1, NSToCoordFloor(GetRatio() * mImageWidth)));
-  image->SetHeight(std::max(1, NSToCoordFloor(GetRatio() * mImageHeight)));
-  
-  // The view might have been scrolled when zooming in, scroll back to the
-  // origin now that we're showing a shrunk-to-window version.
-  (void) ScrollImageTo(0, 0, false);
+  image->SetWidth(std::max(1, NSToCoordFloor(GetShrinkToFitRatio() * mImageWidth)));
+  image->SetHeight(std::max(1, NSToCoordFloor(GetShrinkToFitRatio() * mImageHeight)));
 
   SetModeClass(eShrinkToFit);
-  
+
   mImageIsResized = true;
-  
+
   UpdateTitleAndCharset();
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 ImageDocument::RestoreImageTo(int32_t aX, int32_t aY)
 {
-  return ScrollImageTo(aX, aY, true);
+  return ScrollImageTo(aX, aY, eCSSPixelScale);
+}
+
+NS_IMETHODIMP
+ImageDocument::ScaleToDevicePixelsTo(int32_t aX, int32_t aY)
+{
+  return ScrollImageTo(aX, aY, eDevicePixelScale);
 }
 
 nsresult
-ImageDocument::ScrollImageTo(int32_t aX, int32_t aY, bool restoreImage)
+ImageDocument::ScrollImageTo(int32_t aX, int32_t aY,
+                             eScaleOptions aScaleOption)
 {
-  float ratio = GetRatio();
-
-  if (restoreImage) {
-    RestoreImage();
-    FlushPendingNotifications(Flush_Layout);
-  }
+  // Here, (aX, aY) is a position (in CSS pixels) within the displayed image
+  // (at its current scale) that we want to place as close as possible to the
+  // same position on screen after rescaling according to aScaleOption.
 
   nsIPresShell *shell = GetShell();
-  if (!shell)
+  if (!shell) {
     return NS_OK;
+  }
 
   nsIScrollableFrame* sf = shell->GetRootScrollFrameAsScrollable();
-  if (!sf)
+  if (!sf) {
     return NS_OK;
+  }
+
+  // To figure out the unscaled image pixel location of interest, we need to
+  // undo the scaling from image pixels to the current display.
+  float ratio = GetCurrentRatio();
+  float imageX = aX / ratio;
+  float imageY = aY / ratio;
+
+  // And to preserve its position on screen, we'll need to account for the
+  // original position of the client rect, before it is moved/resized by
+  // rescaling the image.
+  nsRefPtr<nsClientRect> clientRect =
+    mImageContent->AsElement()->GetBoundingClientRect();
+  float origLeft = clientRect->Left();
+  float origTop = clientRect->Top();
 
-  nsRect portRect = sf->GetScrollPortRect();
-  sf->ScrollTo(nsPoint(nsPresContext::CSSPixelsToAppUnits(aX/ratio) - portRect.width/2,
-                       nsPresContext::CSSPixelsToAppUnits(aY/ratio) - portRect.height/2),
-               nsIScrollableFrame::INSTANT);
+  // Update scaling and flush layout, so that we can get the new client rect.
+  switch (aScaleOption) {
+  case eDevicePixelScale:
+    ScaleToDevicePixels();
+    break;
+  case eCSSPixelScale:
+    RestoreImage();
+    break;
+  }
+  FlushPendingNotifications(Flush_Layout);
+
+  nsPoint scroll = sf->GetScrollPosition();
+
+  // Now update scroll to put the desired image pixel (at the new scale) at
+  // its original position, adjusting for the possibly-moved client rect.
+  clientRect = mImageContent->AsElement()->GetBoundingClientRect();
+  float clientLeft = clientRect->Left();
+  float clientTop = clientRect->Top();
+  ratio = GetCurrentRatio();
+
+  // Here (client{Left,Top} + image{X,Y}*ratio) is the new viewport-relative
+  // position of the click point in CSS px, while (orig{Left,Top} + a{X,Y}) is
+  // its old viewport-relative position. Adjust scroll pos by the difference.
+  scroll.x +=
+    nsPresContext::CSSPixelsToAppUnits((clientLeft + imageX * ratio) -
+                                       (origLeft + aX));
+  scroll.y +=
+    nsPresContext::CSSPixelsToAppUnits((clientTop + imageY * ratio) -
+                                       (origTop + aY));
+
+  // Finally set the scroll position (note that scrolling will be pinned to
+  // the limits of the frame's scrollable range, so we don't need to check
+  // for negative or excessively large scroll values here).
+  sf->ScrollTo(scroll, nsIScrollableFrame::INSTANT);
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 ImageDocument::RestoreImage()
 {
   if (!mImageContent) {
     return NS_OK;
   }
   // Keep image content alive while changing the attributes.
   nsCOMPtr<nsIContent> imageContent = mImageContent;
   imageContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::width, true);
   imageContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::height, true);
-  
-  if (mImageIsOverflowing) {
+
+  // the "overflowing" mode really means "we could zoom out", so it applies
+  // if we are truly overflowing, OR if there's a device-pixel scale that
+  // would be smaller than the original (css-pixel) size
+  if (mImageIsOverflowing || GetDevicePixelSizeInCSSPixels() < 1.0f) {
     SetModeClass(eOverflowing);
   }
   else {
     SetModeClass(eNone);
   }
-  
+
   mImageIsResized = false;
-  
+  mImageIsScaledToDevicePixels = false;
+
   UpdateTitleAndCharset();
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
+ImageDocument::ScaleToDevicePixels()
+{
+  if (!mImageContent) {
+    return NS_OK;
+  }
+
+  float ratio = GetDevicePixelSizeInCSSPixels();
+  if (ratio == 1.0f) {
+    // if CSS px == device pix, we don't treat this as a separate scale option
+    return RestoreImage();
+  }
+
+  nsCOMPtr<nsIDOMHTMLImageElement> image = do_QueryInterface(mImageContent);
+  image->SetWidth(std::max(1, NSToCoordFloor(ratio * mImageWidth)));
+  image->SetHeight(std::max(1, NSToCoordFloor(ratio * mImageHeight)));
+
+  SetModeClass(eScaleToDevicePixels);
+
+  mImageIsResized = false;
+  mImageIsScaledToDevicePixels = true;
+
+  UpdateTitleAndCharset();
+
+  return NS_OK;
+}
+
+// TODO: make this cycle through the new ScaleToDevicePixels size as well
+NS_IMETHODIMP
 ImageDocument::ToggleImageSize()
 {
   mShouldResize = true;
   if (mImageIsResized) {
     mShouldResize = false;
     ResetZoomLevel();
     RestoreImage();
-  }
-  else if (mImageIsOverflowing) {
+  } else if (mImageIsOverflowing) {
     ResetZoomLevel();
     ShrinkToFit();
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
@@ -559,16 +667,22 @@ ImageDocument::SetModeClass(eModeClasses
   mozilla::ErrorResult rv;
 
   if (mode == eShrinkToFit) {
     classList->Add(NS_LITERAL_STRING("shrinkToFit"), rv);
   } else {
     classList->Remove(NS_LITERAL_STRING("shrinkToFit"), rv);
   }
 
+  if (mode == eScaleToDevicePixels) {
+    classList->Add(NS_LITERAL_STRING("scaleToDevicePixels"), rv);
+  } else {
+    classList->Remove(NS_LITERAL_STRING("scaleToDevicePixels"), rv);
+  }
+
   if (mode == eOverflowing) {
     classList->Add(NS_LITERAL_STRING("overflowing"), rv);
   } else {
     classList->Remove(NS_LITERAL_STRING("overflowing"), rv);
   }
 }
 
 nsresult
@@ -609,39 +723,85 @@ ImageDocument::OnStopRequest(imgIRequest
 
 NS_IMETHODIMP
 ImageDocument::HandleEvent(nsIDOMEvent* aEvent)
 {
   nsAutoString eventType;
   aEvent->GetType(eventType);
   if (eventType.EqualsLiteral("resize")) {
     CheckOverflowing(false);
+    return NS_OK;
   }
-  else if (eventType.EqualsLiteral("click") && mClickResizingEnabled) {
+
+  if (eventType.EqualsLiteral("click") && mClickResizingEnabled) {
     ResetZoomLevel();
     mShouldResize = true;
-    if (mImageIsResized) {
-      int32_t x = 0, y = 0;
-      nsCOMPtr<nsIDOMMouseEvent> event(do_QueryInterface(aEvent));
-      if (event) {
-        event->GetClientX(&x);
-        event->GetClientY(&y);
-        int32_t left = 0, top = 0;
-        nsCOMPtr<nsIDOMHTMLElement> htmlElement =
-          do_QueryInterface(mImageContent);
-        htmlElement->GetOffsetLeft(&left);
-        htmlElement->GetOffsetTop(&top);
-        x -= left;
-        y -= top;
+
+    float devPixelRatio = GetDevicePixelSizeInCSSPixels();
+    float shrinkToFitRatio = GetShrinkToFitRatio();
+
+    // Figure out the target pixel to use if the image does not
+    // completely fit in the window after resizing.
+    int32_t x = 0, y = 0;
+    nsCOMPtr<nsIDOMMouseEvent> event(do_QueryInterface(aEvent));
+    if (event) {
+      event->GetClientX(&x);
+      event->GetClientY(&y);
+      // Adjust for any current scroll amount to get a location within
+      // the image as a whole (but still at its scaled size)
+      nsRefPtr<nsClientRect> clientRect =
+        mImageContent->AsElement()->GetBoundingClientRect();
+      x -= NSToIntRound(clientRect->Left());
+      y -= NSToIntRound(clientRect->Top());
+    }
+
+    if (mImageIsResized || mImageIsScaledToDevicePixels) {
+      // if CSS px == dev pix, there's only one thing to do here
+      if (devPixelRatio == 1.0f) {
+        RestoreImageTo(x, y);
+        mShouldResize = false;
+        return NS_OK;
       }
-      mShouldResize = false;
-      RestoreImageTo(x, y);
+
+      if (mImageIsResized) {
+        // currently at shrink-to-fit size;
+        // if scaling to device pixels would make it bigger, do that;
+        // else scale to original 1:1 size
+        if (devPixelRatio > shrinkToFitRatio) {
+          ScaleToDevicePixelsTo(x, y);
+        } else {
+          RestoreImageTo(x, y);
+        }
+        mShouldResize = false;
+      } else {
+        if (shrinkToFitRatio > devPixelRatio && shrinkToFitRatio < 1.0f) {
+          ShrinkToFit();
+        } else {
+          RestoreImageTo(x, y);
+          mShouldResize = false;
+        }
+      }
+      return NS_OK;
     }
-    else if (mImageIsOverflowing) {
-      ShrinkToFit();
+
+    if (mImageIsOverflowing) {
+      // If the image is overflowing, we will shrink to the smaller
+      // of our possible sizes
+      if (devPixelRatio < shrinkToFitRatio) {
+        ScaleToDevicePixelsTo(x, y);
+        mShouldResize = false;
+      } else {
+        ShrinkToFit();
+      }
+      return NS_OK;
+    }
+
+    if (devPixelRatio != 1.0f) {
+      ScaleToDevicePixelsTo(x, y);
+      mShouldResize = false;
     }
   }
 
   return NS_OK;
 }
 
 nsresult
 ImageDocument::CreateSyntheticDocument()
@@ -787,19 +947,19 @@ ImageDocument::UpdateTitleAndCharset()
       }
       typeStr = Substring(iter, end);
     } else {
       typeStr = mimeType;
     }
   }
 
   nsXPIDLString status;
-  if (mImageIsResized) {
+  if (mImageIsResized || mImageIsScaledToDevicePixels) {
     nsAutoString ratioStr;
-    ratioStr.AppendInt(NSToCoordFloor(GetRatio() * 100));
+    ratioStr.AppendInt(NSToCoordFloor(GetCurrentRatio() * 100));
 
     const PRUnichar* formatString[1] = { ratioStr.get() };
     mStringBundle->FormatStringFromName(NS_LITERAL_STRING("ScaledImage").get(),
                                         formatString, 1,
                                         getter_Copies(status));
   }
 
   static const char* const formatNames[4] = 
--- a/content/html/document/test/test_bug369370.html
+++ b/content/html/document/test/test_bug369370.html
@@ -41,46 +41,57 @@ https://bugzilla.mozilla.org/show_bug.cg
         // Image just loaded and is scaled to window size.
         is(img.width,  400, "image width");
         is(img.height, 300, "image height");
         is(kidDoc.body.scrollLeft,  0, "Checking scrollLeft");
         is(kidDoc.body.scrollTop,   0, "Checking scrollTop");
 
         // ========== test 1 ==========
         // Click in the upper left to zoom in
-        var event = makeClickFor(25,25);
+        var event = makeClickFor(25, 25);
         img.dispatchEvent(event);
         ok(true, "----- click 1 -----");
 
         is(img.width,  800, "image width");
         is(img.height, 600, "image height");
-        is(kidDoc.body.scrollLeft,  0, "Checking scrollLeft");
-        is(kidDoc.body.scrollTop,   0, "Checking scrollTop");
+
+        // The image pixel at (25, 25) in the scaled image will be
+        // (50, 50) in the original image. To maintain its screen
+        // location, the image should have been scrolled up and left
+        // by (25, 25) px at its new size.
+        is(kidDoc.body.scrollLeft,  25, "Checking scrollLeft");
+        is(kidDoc.body.scrollTop,   25, "Checking scrollTop");
 
         // ========== test 2 ==========
         // Click there again to zoom out
-        event = makeClickFor(25,25);
+        event = makeClickFor(25, 25);
         img.dispatchEvent(event);
         ok(true, "----- click 2 -----");
 
         is(img.width,  400, "image width");
         is(img.height, 300, "image height");
         is(kidDoc.body.scrollLeft,  0, "Checking scrollLeft");
         is(kidDoc.body.scrollTop,   0, "Checking scrollTop");
 
         // ========== test 3 ==========
         // Click in the lower right to zoom in
         event = makeClickFor(350, 250);
         img.dispatchEvent(event);
         ok(true, "----- click 3 -----");
 
         is(img.width,  800, "image width");
         is(img.height, 600, "image height");
-        is(kidDoc.body.scrollLeft,  400, "Checking scrollLeft");
-        is(kidDoc.body.scrollTop,   300, "Checking scrollTop");
+
+        // The image pixel at (350, 250) in the scaled image will be
+        // (700, 500) in the original image. To maintain its screen
+        // location, the image should have been scrolled up and left
+        // by (350, 250) px at its new size.
+
+        is(kidDoc.body.scrollLeft,  350, "Checking scrollLeft");
+        is(kidDoc.body.scrollTop,   250, "Checking scrollTop");
 
         // ========== test 4 ==========
         // Click there again to zoom out
         event = makeClickFor(350, 250);
         img.dispatchEvent(event);
         ok(true, "----- click 4 -----");
 
         is(img.width,  400, "image width");
--- a/toolkit/themes/osx/global/media/TopLevelImageDocument.css
+++ b/toolkit/themes/osx/global/media/TopLevelImageDocument.css
@@ -14,16 +14,17 @@
     background: hsl(0,0%,90%) url("chrome://global/skin/media/imagedoc-lightnoise.png");
     color: #222;
   }
 
   .overflowing {
     cursor: -moz-zoom-out;
   }
 
-  .shrinkToFit {
+  .shrinkToFit,
+  .scaleToDevicePixels {
     cursor: -moz-zoom-in;
   }
 
   .completeRotation {
     transition: transform 0.3s ease 0s;
   }
 }
--- a/toolkit/themes/windows/global/media/TopLevelImageDocument.css
+++ b/toolkit/themes/windows/global/media/TopLevelImageDocument.css
@@ -14,16 +14,17 @@
     background: hsl(0,0%,90%) url("chrome://global/skin/media/imagedoc-lightnoise.png");
     color: #222;
   }
 
   .overflowing {
     cursor: -moz-zoom-out;
   }
 
-  .shrinkToFit {
+  .shrinkToFit,
+  .scaleTodevicePixels {
     cursor: -moz-zoom-in;
   }
 
   .completeRotation {
     transition: transform 0.3s ease 0s;
   }
 }