Skip to content

Commit d4ae0aa

Browse files
authored
[INT-252] [Java/Local] add screenshot clipping (#232)
1 parent c3cb77d commit d4ae0aa

File tree

5 files changed

+163
-42
lines changed

5 files changed

+163
-42
lines changed

visual-java/src/main/java/com/saucelabs/visual/VisualApi.java

+46-23
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@
88
import com.saucelabs.visual.graphql.type.*;
99
import com.saucelabs.visual.model.*;
1010
import com.saucelabs.visual.model.DiffingMethodSensitivity;
11-
import com.saucelabs.visual.utils.CapabilityUtils;
12-
import com.saucelabs.visual.utils.ConsoleColors;
13-
import com.saucelabs.visual.utils.EnvironmentVariables;
11+
import com.saucelabs.visual.utils.*;
1412
import dev.failsafe.Failsafe;
1513
import dev.failsafe.RetryPolicy;
14+
import java.awt.image.BufferedImage;
1615
import java.lang.reflect.Field;
1716
import java.net.URL;
1817
import java.time.Duration;
@@ -565,10 +564,9 @@ private void sauceVisualCheckSauce(String snapshotName, CheckOptions options) {
565564
input.setCaptureDom(captureDom);
566565
}
567566

568-
if (options.getClipElement() != null) {
569-
input.setClipElement(options.getClipElement());
570-
} else if (options.getClipSelector() != null) {
571-
input.setClipElement(this.driver.findElement(By.cssSelector(options.getClipSelector())));
567+
WebElement clipElement = getClipElement(options);
568+
if (clipElement != null) {
569+
input.setClipElement(clipElement);
572570
}
573571

574572
FullPageScreenshotConfig fullPageScreenshotConfig =
@@ -604,7 +602,37 @@ private void sauceVisualCheckSauce(String snapshotName, CheckOptions options) {
604602
}
605603

606604
private void sauceVisualCheckLocal(String snapshotName, CheckOptions options) {
607-
byte[] screenshot = driver.getScreenshotAs(OutputType.BYTES);
605+
Window window = new Window(this.driver);
606+
Rectangle viewport = window.getViewport();
607+
608+
byte[] screenshot;
609+
610+
// clip image if required
611+
WebElement clipElement = getClipElement(options);
612+
if (clipElement != null) {
613+
Rectangle clipRect = clipElement.getRect();
614+
615+
// Scroll to the clipped element
616+
Rectangle newViewport = window.scrollTo(clipRect.getPoint());
617+
screenshot = driver.getScreenshotAs(OutputType.BYTES);
618+
619+
// Restore the original scroll
620+
window.scrollTo(viewport.getPoint());
621+
622+
Optional<Rectangle> cropRect = CartesianHelpers.intersect(clipRect, newViewport);
623+
if (!cropRect.isPresent()) {
624+
throw new VisualApiException("Clipping would result in an empty image");
625+
}
626+
627+
BufferedImage image = ImageHelpers.loadImage(screenshot);
628+
BufferedImage cropped =
629+
ImageHelpers.cropImage(
630+
image, CartesianHelpers.relativeTo(newViewport.getPoint(), cropRect.get()));
631+
screenshot = ImageHelpers.saveImage(cropped, "png");
632+
viewport = cropRect.get();
633+
} else {
634+
screenshot = driver.getScreenshotAs(OutputType.BYTES);
635+
}
608636

609637
// create upload and get urls
610638
CreateSnapshotUploadMutation mutation =
@@ -617,7 +645,6 @@ private void sauceVisualCheckLocal(String snapshotName, CheckOptions options) {
617645
this.client.upload(uploadResult.getImageUploadUrl(), screenshot, "image/png");
618646

619647
// add ignore regions
620-
WindowScroll scroll = getWindowScroll();
621648
List<RegionIn> ignoreRegions = extractIgnoreList(options);
622649

623650
for (WebElement element : options.getIgnoreElements()) {
@@ -633,8 +660,10 @@ private void sauceVisualCheckLocal(String snapshotName, CheckOptions options) {
633660
}
634661

635662
for (RegionIn region : ignoreRegions) {
636-
region.setX(region.getX() - scroll.getX());
637-
region.setY(region.getY() - scroll.getY());
663+
Point newPoint =
664+
CartesianHelpers.relativeTo(viewport.getPoint(), new Point(region.getX(), region.getY()));
665+
region.setX(newPoint.x);
666+
region.setY(newPoint.y);
638667
}
639668

640669
// upload dom if present / enabled
@@ -729,20 +758,14 @@ private DiffingMethodTolerance getDiffingMethodTolerance(CheckOptions checkOptio
729758
return sensitivity != null ? sensitivity : this.diffingMethodTolerance;
730759
}
731760

732-
private WindowScroll getWindowScroll() {
733-
Object result = driver.executeScript("return [window.scrollX, window.scrollY]");
734-
if (!(result instanceof List<?>)) {
735-
return new WindowScroll(0, 0);
761+
private WebElement getClipElement(CheckOptions checkOptions) {
762+
if (checkOptions.getClipElement() != null) {
763+
return checkOptions.getClipElement();
764+
} else if (checkOptions.getClipSelector() != null) {
765+
return this.driver.findElement(By.cssSelector(checkOptions.getClipSelector()));
736766
}
737767

738-
List<?> list = (List<?>) result;
739-
Object rawScrollX = list.get(0);
740-
Object rawScrollY = list.get(1);
741-
742-
int scrollX = rawScrollX instanceof Long ? ((Long) rawScrollX).intValue() : 0;
743-
int scrollY = rawScrollY instanceof Long ? ((Long) rawScrollY).intValue() : 0;
744-
745-
return new WindowScroll(scrollX, scrollY);
768+
return null;
746769
}
747770

748771
private VisualRegion getIgnoreRegionFromSelector(IgnoreSelectorIn ignoreSelector) {

visual-java/src/main/java/com/saucelabs/visual/model/WindowScroll.java

-19
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.saucelabs.visual.utils;
2+
3+
import java.util.Optional;
4+
import org.openqa.selenium.Point;
5+
import org.openqa.selenium.Rectangle;
6+
7+
public class CartesianHelpers {
8+
public static Rectangle relativeTo(Point origin, Rectangle rectangle) {
9+
int newX = rectangle.x - origin.x;
10+
int newY = rectangle.y - origin.y;
11+
return new Rectangle(new Point(newX, newY), rectangle.getDimension());
12+
}
13+
14+
public static Point relativeTo(Point origin, Point point) {
15+
int newX = point.x - origin.x;
16+
int newY = point.y - origin.y;
17+
return new Point(newX, newY);
18+
}
19+
20+
public static Optional<Rectangle> intersect(Rectangle r1, Rectangle r2) {
21+
int x1 = Math.max(r1.x, r2.x);
22+
int y1 = Math.max(r1.y, r2.y);
23+
int x2 = Math.min(r1.x + r1.width, r2.x + r2.width);
24+
int y2 = Math.min(r1.y + r1.height, r2.y + r2.height);
25+
26+
int width = x2 - x1;
27+
int height = y2 - y1;
28+
29+
if (width <= 0 || height <= 0) {
30+
return Optional.empty();
31+
}
32+
33+
return Optional.of(new Rectangle(x1, y1, height, width));
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.saucelabs.visual.utils;
2+
3+
import com.saucelabs.visual.exception.VisualApiException;
4+
import java.awt.image.BufferedImage;
5+
import java.io.ByteArrayInputStream;
6+
import java.io.ByteArrayOutputStream;
7+
import java.io.IOException;
8+
import javax.imageio.ImageIO;
9+
import org.openqa.selenium.Rectangle;
10+
11+
public class ImageHelpers {
12+
public static BufferedImage cropImage(BufferedImage image, Rectangle cropRegion) {
13+
return image.getSubimage(cropRegion.x, cropRegion.y, cropRegion.width, cropRegion.height);
14+
}
15+
16+
public static BufferedImage loadImage(byte[] imageData) {
17+
ByteArrayInputStream stream = new ByteArrayInputStream(imageData);
18+
try {
19+
return ImageIO.read(stream);
20+
} catch (IOException e) {
21+
throw new VisualApiException("Failed to load image from bytes", e);
22+
}
23+
}
24+
25+
public static byte[] saveImage(BufferedImage image, String imageFormat) {
26+
ByteArrayOutputStream stream = new ByteArrayOutputStream();
27+
try {
28+
ImageIO.write(image, imageFormat, stream);
29+
return stream.toByteArray();
30+
} catch (IOException e) {
31+
throw new VisualApiException("Failed to save image to bytes", e);
32+
}
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.saucelabs.visual.utils;
2+
3+
import java.util.List;
4+
import org.openqa.selenium.JavascriptExecutor;
5+
import org.openqa.selenium.Point;
6+
import org.openqa.selenium.Rectangle;
7+
8+
public class Window {
9+
private final JavascriptExecutor driver;
10+
11+
public Window(JavascriptExecutor javascriptExecutor) {
12+
this.driver = javascriptExecutor;
13+
}
14+
15+
public Rectangle getViewport() {
16+
Object result = driver.executeScript(jsViewportTupleSnippet);
17+
return convertJsTupleToRectangle(result);
18+
}
19+
20+
public Rectangle scrollTo(Point point) {
21+
Object result =
22+
driver.executeScript(
23+
String.format("window.scrollTo(%d, %d);", point.x, point.y) + jsViewportTupleSnippet);
24+
return convertJsTupleToRectangle(result);
25+
}
26+
27+
private final String jsViewportTupleSnippet =
28+
"return [window.scrollX, window.scrollY, window.innerWidth, window.innerHeight];";
29+
30+
private Rectangle convertJsTupleToRectangle(Object result) {
31+
if (!(result instanceof List<?>)) {
32+
return new Rectangle(0, 0, 0, 0);
33+
}
34+
35+
List<?> list = (List<?>) result;
36+
Object rawScrollX = list.get(0);
37+
Object rawScrollY = list.get(1);
38+
Object rawWidth = list.get(2);
39+
Object rawHeight = list.get(3);
40+
41+
int scrollX = rawScrollX instanceof Long ? ((Long) rawScrollX).intValue() : 0;
42+
int scrollY = rawScrollY instanceof Long ? ((Long) rawScrollY).intValue() : 0;
43+
int width = rawWidth instanceof Long ? ((Long) rawWidth).intValue() : 0;
44+
int height = rawHeight instanceof Long ? ((Long) rawHeight).intValue() : 0;
45+
46+
return new Rectangle(scrollX, scrollY, height, width);
47+
}
48+
}

0 commit comments

Comments
 (0)