private void StitchScreenshot_(Size stitchOffset, IPositionProvider stitchProvider, ICollection <SubregionForStitching> screenshotParts, Bitmap stitchedImage, double scaleRatio, ICutProvider scaledCutProvider, float sizeRatio) { int index = 0; logger_.Verbose($"enter: {nameof(stitchOffset)}: {{0}} ; {nameof(screenshotParts)}.Count: {{1}}, {nameof(scaleRatio)}: {{2}}", stitchOffset, screenshotParts.Count, scaleRatio); Stopwatch stopwatch = Stopwatch.StartNew(); foreach (SubregionForStitching partRegion in screenshotParts) { if (stopwatch.Elapsed > TimeSpan.FromMinutes(5)) { logger_.Log("Still Running..."); // this is so CI systems won't kill the build due to lack of activity. stopwatch.Restart(); } logger_.Verbose("Part: {0}", partRegion); // Scroll to the part's top/left Point partAbsoluteLocationInCurrentFrame = partRegion.ScrollTo; partAbsoluteLocationInCurrentFrame += stitchOffset; Point scrollPosition = new Point( (int)Math.Round(partAbsoluteLocationInCurrentFrame.X * sizeRatio), (int)Math.Round(partAbsoluteLocationInCurrentFrame.Y * sizeRatio)); Point originPosition = stitchProvider.SetPosition(scrollPosition); int dx = scrollPosition.X - originPosition.X; int dy = scrollPosition.Y - originPosition.Y; Point partPastePosition = partRegion.PasteLocation; //partPastePosition.Offset(-fullarea.Left, -fullarea.Top); partPastePosition.Offset(dx, dy); // Actually taking the screenshot. Thread.Sleep(waitBeforeScreenshots_); using (Bitmap partImage = imageProvider_.GetImage()) using (Bitmap cutPart = scaledCutProvider.Cut(partImage)) { Bitmap croppedPart; Rectangle r = partRegion.PhysicalCropArea; if ((r.Width * r.Height) != 0) { croppedPart = BasicImageUtils.Crop(cutPart, r); } else { croppedPart = cutPart; } Rectangle r2 = partRegion.LogicalCropArea; using (Bitmap scaledPartImage = BasicImageUtils.ScaleImage(croppedPart, scaleRatio)) using (Bitmap scaledCroppedPartImage = BasicImageUtils.Crop(scaledPartImage, r2)) using (Graphics g = Graphics.FromImage(stitchedImage)) { debugScreenshotsProvider_.Save(partImage, "partImage-" + originPosition.X + "_" + originPosition.Y); debugScreenshotsProvider_.Save(cutPart, "cutPart-" + originPosition.X + "_" + originPosition.Y); debugScreenshotsProvider_.Save(croppedPart, "croppedPart-" + originPosition.X + "_" + originPosition.Y); debugScreenshotsProvider_.Save(scaledPartImage, "scaledPartImage-" + originPosition.X + "_" + originPosition.Y); debugScreenshotsProvider_.Save(scaledCroppedPartImage, "scaledCroppedPartImage-" + partPastePosition.X + "_" + partPastePosition.Y); logger_.Verbose("pasting part at {0}", partPastePosition); g.DrawImage(scaledCroppedPartImage, partPastePosition); } if (!object.ReferenceEquals(croppedPart, cutPart)) { croppedPart.Dispose(); } debugScreenshotsProvider_.Save(stitchedImage, $"stitched_{index}_({partPastePosition.X}_{partPastePosition.Y})"); index++; } } debugScreenshotsProvider_.Save(stitchedImage, "stitched"); }
/// <summary> /// Encapsulates an algorithm for creating full-page images of a page. /// </summary> /// <param name="positionProvider">The position provider used for moving to the actual stitch points.</param> /// <param name="region">The region to stitch. If <see cref="Region.Empty"/>, the entire image will be stitched.</param> /// <param name="fullarea">The wanted area of the resulting image. If unknown, pass in <c>null</c> or <see cref="Region.Empty"/>.</param> /// <param name="originProvider">A position provider used for saving the state before /// starting the stitching, as well as moving to (0,0). The reason it is separated from /// the <c>stitchProvider</c>is that the stitchProvider might have side-effects /// (e.g., changing the CSS transform of the page can cause a layout change at the /// top of the page), which we can avoid for the first screenshot (since it might be a /// full page screenshot anyway).</param> /// <param name="stitchOffset"></param> /// <returns>The screenshot as Bitmap.</returns> public Bitmap GetStitchedRegion(Region region, Region fullarea, IPositionProvider positionProvider, IPositionProvider originProvider, Size stitchOffset) { ArgumentGuard.NotNull(region, nameof(region)); ArgumentGuard.NotNull(positionProvider, nameof(positionProvider)); logger_.Verbose("region: {0} ; fullarea: {1} ; positionProvider: {2}", region, fullarea, positionProvider.GetType().Name); Point originalStitchedState = positionProvider.GetCurrentPosition(); logger_.Verbose("region size: {0}, originalStitchedState: {1}", region, originalStitchedState); PositionMemento originProviderState = originProvider.GetState(); logger_.Verbose("originProviderState: {0}", originProviderState); originProvider.SetPosition(Point.Empty); Thread.Sleep(waitBeforeScreenshots_); Bitmap initialScreenshot = imageProvider_.GetImage(); Size initialPhysicalSize = initialScreenshot.Size; SaveDebugScreenshotPart_(initialScreenshot, region.ToRectangle(), "initial"); IScaleProvider scaleProvider = scaleProviderFactory_.GetScaleProvider(initialScreenshot.Width); double pixelRatio = 1 / scaleProvider.ScaleRatio; Size initialSizeScaled = new Size((int)Math.Round(initialScreenshot.Width / pixelRatio), (int)Math.Round(initialScreenshot.Height / pixelRatio)); ICutProvider scaledCutProvider = cutProvider_.Scale(pixelRatio); if (pixelRatio != 1 && !(scaledCutProvider is NullCutProvider)) { initialScreenshot = cutProvider_.Cut(initialScreenshot); debugScreenshotsProvider_.Save(initialScreenshot, "original-cut"); } Region regionInScreenshot = GetRegionInScreenshot_(region, initialScreenshot, pixelRatio); Bitmap croppedInitialScreenshot = CropScreenshot_(initialScreenshot, regionInScreenshot); debugScreenshotsProvider_.Save(croppedInitialScreenshot, "cropped"); Bitmap scaledInitialScreenshot = BasicImageUtils.ScaleImage(croppedInitialScreenshot, scaleProvider); if (!object.ReferenceEquals(scaledInitialScreenshot, croppedInitialScreenshot)) { SaveDebugScreenshotPart_(scaledInitialScreenshot, regionInScreenshot.ToRectangle(), "scaled"); } if (fullarea.IsEmpty) { Size entireSize; try { entireSize = positionProvider.GetEntireSize(); logger_.Verbose("Entire size of region context: {0}", entireSize); } catch (EyesException e) { logger_.Log("WARNING: Failed to extract entire size of region context" + e.Message); logger_.Log("Using image size instead: " + scaledInitialScreenshot.Width + "x" + scaledInitialScreenshot.Height); entireSize = new Size(scaledInitialScreenshot.Width, scaledInitialScreenshot.Height); } // Notice that this might still happen even if we used // "getImagePart", since "entirePageSize" might be that of a frame. if (scaledInitialScreenshot.Width >= entireSize.Width && scaledInitialScreenshot.Height >= entireSize.Height) { logger_.Log("WARNING: Seems the image is already a full page screenshot."); if (!object.ReferenceEquals(scaledInitialScreenshot, initialScreenshot)) { initialScreenshot.Dispose(); } return(scaledInitialScreenshot); } fullarea = new Region(Point.Empty, entireSize, CoordinatesTypeEnum.SCREENSHOT_AS_IS); } float currentFullWidth = fullarea.Width; fullarea = sizeAdjuster_.AdjustRegion(fullarea, initialSizeScaled); float sizeRatio = currentFullWidth / fullarea.Width; logger_.Verbose("adjusted fullarea: {0}", fullarea); Point scaledCropLocation = fullarea.Location; Point physicalCropLocation = new Point( (int)Math.Ceiling(scaledCropLocation.X * pixelRatio), (int)Math.Ceiling(scaledCropLocation.Y * pixelRatio)); Rectangle sourceRegion; if (regionInScreenshot.IsSizeEmpty) { Size physicalCropSize = new Size(initialPhysicalSize.Width - physicalCropLocation.X, initialPhysicalSize.Height - physicalCropLocation.Y); sourceRegion = new Rectangle(physicalCropLocation, physicalCropSize); } else { // Starting with the screenshot we already captured at (0,0). sourceRegion = regionInScreenshot.ToRectangle(); } Rectangle scaledCroppedSourceRect = cutProvider_.ToRectangle(sourceRegion.Size); scaledCroppedSourceRect.Offset(sourceRegion.Location); Rectangle scaledCroppedSourceRegion = new Rectangle( (int)Math.Ceiling(scaledCroppedSourceRect.X / pixelRatio), (int)Math.Ceiling(scaledCroppedSourceRect.Y / pixelRatio), (int)Math.Ceiling(scaledCroppedSourceRect.Width / pixelRatio), (int)Math.Ceiling(scaledCroppedSourceRect.Height / pixelRatio)); Size scaledCropSize = scaledCroppedSourceRegion.Size; // The screenshot part is a bit smaller than the screenshot size, in order to eliminate // duplicate bottom/right-side scroll bars, as well as fixed position footers. Size screenshotPartSize = new Size( Math.Max(scaledCropSize.Width, MinScreenshotPartSize_), Math.Max(scaledCropSize.Height, MinScreenshotPartSize_) ); logger_.Verbose("Screenshot part size: {0}", screenshotPartSize); // Getting the list of viewport regions composing the page (we'll take screenshot for each one). Rectangle rectInScreenshot; if (regionInScreenshot.IsSizeEmpty) { int x = Math.Max(0, fullarea.Left); int y = Math.Max(0, fullarea.Top); int w = Math.Min(fullarea.Width, scaledCropSize.Width); int h = Math.Min(fullarea.Height, scaledCropSize.Height); rectInScreenshot = new Rectangle( (int)Math.Round(x * pixelRatio), (int)Math.Round(y * pixelRatio), (int)Math.Round(w * pixelRatio), (int)Math.Round(h * pixelRatio)); } else { rectInScreenshot = regionInScreenshot.Rectangle; } fullarea = CoerceImageSize_(fullarea); ICollection <SubregionForStitching> screenshotParts = fullarea.GetSubRegions(screenshotPartSize, stitchOverlap_, pixelRatio, rectInScreenshot, logger_); Bitmap stitchedImage = new Bitmap(fullarea.Width, fullarea.Height); // Take screenshot and stitch for each screenshot part. StitchScreenshot_(stitchOffset, positionProvider, screenshotParts, stitchedImage, scaleProvider.ScaleRatio, scaledCutProvider, sizeRatio); positionProvider.SetPosition(originalStitchedState); originProvider.RestoreState(originProviderState); croppedInitialScreenshot.Dispose(); return(stitchedImage); }