Создаем скриншот и pdf документ из веб страницы

Создаем скриншот и pdf документ из веб страницы

Все мы любим фоточки, а прикиньте фоточки нужной веб страницы в полный рост! А если это все в pdf по формату читалки?! Да это же крутяк! Можно накидать статей в читалку, очистить список непрочитанного в pocket. Делаем! Нужен фотик для всего этого, и тут вариантов для реализации много, да вот только не все они хорошие. PhantomJS - для сложных страниц не подходит, хоть и идея хороша, к тому же проект, судя по всему, уже все, того... Для нормального отображения веб страницы нужен "полноценный" браузер. И тут очень в тему будет SeleniumHQ - инструмент автоматизации браузера, как заявляют на главной. Отлично, фото в полный рост будет, а что насчёт pdf? Тут все как обычно - куча платных библиотек, есть и бесплатные, но со звездочкой, есть и без звездочек, но они существуют как отдельное приложение. Как вариант, я выбрал iTextSharp. Для opensource проектов они предоставляют бесплатное использование - нам подходит. А теперь собирем все вместе и да начнется фотосет!

Для получение скриншота страницы нам нужен SeleniumHQ + Chrome. Так как мы уже прошареные ребята, то знаем как можно получить желаемое максимально быстро. Да-да, Selenium Docker и всех делов. Для запуска нам нужно всего-то выполнить одну команду для запуска контейнера.

docker run -d -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-chrome:3.14.0-curium

Теперь наведем объектив нашего браузера на нужную страницу и начнем снимать. Для подключения к браузеру мы будем использовать Selenium WebDriver. Всю документацию можно найти на странице инструмента.

var driver = new RemoteWebDriver(new Uri("http://localhost:4444/"), new ChromeOptions());
driver.Navigate().GoToUrl("https://google.com");

//Устанавливаем размер окна
driver.Manage().Window.Size = new Size(1024, 768);

//Перемещаем скрол на нужную позицию
driver.Manage().Window.Position = new Point(0, 0);

Общая схема создания скриншота такова - мы определяем "пазлы", которые формируют всю страницу, каждый пазл имеет свои размеры и свои координаты. Имея весь набор пазлов, мы проходимся в цикле по кажому и делаем скриншоты страницы. Каждый скриншот переносим в общее полотно изображения всей страницы.

private Image GetFullPageImage(IWebDriver driver, ShotOptions options)
{
    ResizeBrowser(driver,
                  options.Width + ScrollWidthOffset,
                  options.StepHeight + MenuHeightOffset);

    var jsExecutor = (IJavaScriptExecutor) driver;

    LoadAllPageHeight(jsExecutor);

    var pageSizes = GetWebPageSizes(jsExecutor);
    if (pageSizes.TotalWidth > pageSizes.ViewportWidth)
    {
        ResizeBrowser(driver,
                      options.Width + ScrollWidthOffset,
                      options.StepHeight + MenuHeightOffset + ScrollWidthOffset);
    }

    var rectangles = new List<Rectangle>();
    for (var yScroll = 0; yScroll < pageSizes.TotalHeight; yScroll += pageSizes.ViewportHeight)
    {
        var rectangleHeight = pageSizes.ViewportHeight;
        if (yScroll + pageSizes.ViewportHeight > pageSizes.TotalHeight)
        {
            rectangleHeight = pageSizes.TotalHeight - yScroll;
        }

        var currRect = new Rectangle(0, yScroll, options.Width, rectangleHeight);
        rectangles.Add(currRect);
    }

    var takerScreenshot = (ITakesScreenshot) driver;
    var fullPageImage = new Bitmap(options.Width, pageSizes.TotalHeight);
    var graphics = Graphics.FromImage(fullPageImage);

    var yPosition = 0;
    for (var rectangleIndex = 0; rectangleIndex < rectangles.Count; rectangleIndex++)
    {
        jsExecutor.ExecuteScript($"window.scroll(0, {yPosition.ToString()})");

        if (rectangleIndex > 0 ||
            options.HideOverlayElementsImmediate)
        {
            HideFloatingElements(jsExecutor);
        }

        var rectangle = rectangles[rectangleIndex];
        var screenshot = takerScreenshot.GetScreenshot();
        var screenshotImage = ScreenshotToImage(screenshot);

        var sourceRectangle = new Rectangle(0,
                                            pageSizes.ViewportHeight - rectangle.Height,
                                            rectangle.Width,
                                            rectangle.Height);

        graphics.DrawImage(screenshotImage, rectangle, sourceRectangle, GraphicsUnit.Pixel);

        yPosition = rectangle.Bottom;
    }

    return fullPageImage;
}

А когда у нас есть полная картинка мы можем делать уже все, что нам вздумается. А вздумается нам получить pdf документ для электронной читалки. Для полного удовлетворения потребностей нам нужно перевести изображения в черно-белый цвет и минимизировать. Каждая страница документа представляет из себя изображение на весь размер. Формирование pdf документа:

private PdfShot GetPdfDocument(Image fullPageImage, ShotOptions options, String title)
{
    var magicImage = new MagickImage(fullPageImage.ToMemoryStream());
    magicImage.Strip();

    if (options.IsGrayscale)
    {
        magicImage.Grayscale(PixelIntensityMethod.Lightness);
        magicImage.Contrast();
    }
    
    var partsCount = Math.Ceiling((Decimal) magicImage.Height / options.StepHeight);
    partsCount = Math.Ceiling((magicImage.Height + options.OverlaySize * partsCount) / options.StepHeight);
    
    var pageImageParts = new List<Byte[]>();
    for (var i = 0; i < partsCount; i++)
    {
        var y = i * options.StepHeight - i * options.OverlaySize;
        var images = magicImage.CropToTiles(new MagickGeometry(0,
                                                               y,
                                                               options.Width,
                                                               options.StepHeight))
                               .ToList();
        pageImageParts.Add(images.First().ToByteArray());
    }
    
    var pdfBytes = _pdfCreator.CreateDocument(options.Width, options.StepHeight, pageImageParts);
    return new PdfShot(pdfBytes, GetFileName(title));
}

Формирование документа pdf вынесено в абстракцию, можно подставить необходимую вам реализацию и не зависеть от той или иной библиотеки. В репозитории готового проекта представлена реализация с использованием ITextSharp. Так же там есть и консолька, которая может экспортировать список ссылок в pdf файлы.

Примечания

Я добавил два метода для получения более чистых скриншотов: прогрузка всей страницы, скрытие плавающих элементов. Не секрет, что сейчас множество веб страниц используют различные оптимизации, для нас самая критичная - ленивая подгрузка ресурсов. Ведь если размер страницы изменился, а мы уже обсчитали пазлы для съемки, то будет либо не вся страница, либо наложение контента. Для решения этой проблемы перед съемкой делается проход по всей странице с малыми паузами и только после этого производится обсчет пазлов. Так же, если на странице присутствуют плавающие элементы, то они будут на каждом скриншоте, это просто не допустимо, поэтому мы их скрываем перед съемкой.

Из неприятного, но факт: при некорректном использовании веб драйвера, вы можете получать ошибки по типу "session does not exist". Еще имеется открытый issue на github, который указывает на проблему использования подключений. При интенсивной нагрузке подключения множатся, драйвер работает в этом плане неверно.

Исходный код: https://github.com/d2funlife/screenshooter

Примечания по запуску под Linux: https://www.hanselman.com/blog/HowDoYouUseSystemDrawingInNETCore.aspx

d2funlife | Даниил Павлов 2015-2020
Powered by ASP.NET Core 2.2, Entity Framework Core 2.2. Web + UI