Использование HTML5 / Canvas / JavaScript для создания скриншотов в браузере

924

Google «Сообщить об ошибке» или «Инструмент обратной связи» позволяет выбрать область окна браузера, чтобы создать снимок экрана, который будет отправлен вместе с вашим отзывом об ошибке.

Скриншот Google Feedback Tool Снимок экрана Джейсона Смолла, размещенный в дублирующем вопросе .

Как они это делают? API обратной связи JavaScript Google загружается отсюда, и их обзор модуля обратной связи продемонстрирует возможность снимка экрана.

joelvh
источник
2
Эллиот Спрен написал в твиттере несколько дней назад:> @CatChen Этот пост-поток не является точным. Скриншот Google Feedback сделан полностью на стороне клиента. :)
Горан Ракич
1
Это кажется логичным, поскольку они хотят точно понять, как браузер пользователя отображает страницу, а не то, как он будет отображать ее на стороне сервера, используя свой движок. Если вы отправите только DOM текущей страницы на сервер, он пропустит любые несоответствия в том, как браузер отображает HTML. Это не означает, что ответ Чена неверен при съемке скриншотов, просто похоже, что Google делает это по-другому.
Горан Ракич
Эллиот упомянул Яна Кучу сегодня, и я нашел эту ссылку в твиттере Яна: jankuca.tumblr.com/post/7391640769/…
Кошка Чен
Я углублюсь в это позже и посмотрю, как это можно сделать с помощью механизма рендеринга на стороне клиента, и проверим, действительно ли Google так поступил.
Кот Чен
Я вижу использование CompareDocumentPosition, getBoxObjectFor, toDataURL, drawImage, отслеживание заполнения и тому подобное. Это тысячи строк запутанного кода, чтобы де-запутывать и просматривать. Я хотел бы видеть лицензионную версию с открытым исходным кодом, я связался с Эллиоттом Sprehn!
Люк Стэнли

Ответы:

1155

JavaScript может читать DOM и довольно точно отображать это, используя canvas. Я работал над сценарием, который преобразует HTML в изображение холста. Решил сегодня реализовать это в рассылке отзывов, как вы описали.

Сценарий позволяет создавать формы обратной связи, которые включают в себя скриншот, созданный в браузере клиента, а также форму. Снимок экрана основан на DOM и, как таковой, может не быть на 100% точным к реальному представлению, поскольку он не создает фактического снимка экрана, а создает снимок экрана на основе информации, доступной на странице.

Она не требует какого - либо рендеринга с сервера , а все изображение создается в браузере клиента. Сам скрипт HTML2Canvas все еще находится в очень экспериментальном состоянии, так как он не анализирует почти столько атрибутов CSS3, сколько мне бы хотелось, ни поддерживает загрузку изображений CORS, даже если прокси-сервер был доступен.

Все еще довольно ограниченная совместимость браузера (не потому, что больше не может быть поддержано, просто не было времени, чтобы сделать его более кросс-браузерным).

Для получения дополнительной информации посмотрите примеры здесь:

http://hertzen.com/experiments/jsfeedback/

edit Сценарий html2canvas теперь доступен отдельно здесь и некоторые примеры здесь .

edit 2 Еще одно подтверждение того, что Google использует очень похожий метод (фактически, на основе документации, единственным существенным отличием является их асинхронный метод обхода / рисования), можно найти в этой презентации Эллиотта Спрена из команды Google Feedback: http: //www.elliottsprehn.com/preso/fluentconf/

Никлас
источник
1
Очень круто, Sikuli или Selenium могут быть хороши для перехода на разные сайты, сравнивая снимок сайта из инструмента тестирования с визуализированным изображением html2canvas.js с точки зрения сходства пикселей! Интересно, если бы вы могли автоматически обходить части DOM с помощью очень простого средства расчета формул, чтобы найти способ анализа альтернативных источников данных для браузеров, где getBoundingClientRect недоступен. Я бы, наверное, воспользовался бы этим, если бы это был открытый исходный код, подумывал сам поиграть с ним. Отличная работа, Никлас!
Люк Стэнли
1
@Luke Stanley Я, скорее всего, выложу исходники на github на этих выходных, оставив некоторые мелкие исправления и изменения, которые я хочу сделать до этого, а также избавлюсь от ненужной зависимости jQuery, которая у него есть в настоящее время.
Никлас
43
Исходный код теперь доступен по адресу github.com/niklasvh/html2canvas , некоторые примеры используемого скрипта html2canvas.hertzen.com там. Еще много ошибок, которые нужно исправить, поэтому я бы не рекомендовал использовать скрипт в реальной среде.
Никлас
2
Любое решение, чтобы заставить это работать для SVG, будет большой помощью. Это не работает с highcharts.com
Jagdeep
3
@Niklas Я вижу, твой пример превратился в настоящий проект. Может быть, обновите свой самый восторженный комментарий об экспериментальной природе проекта. После почти 900 коммитов я бы подумал, что это немного больше, чем эксперимент на данный момент ;-)
Jogai
70

Ваше веб-приложение теперь может сделать «родной» скриншот всего рабочего стола клиента, используя getUserMedia():

Посмотрите на этот пример:

https://www.webrtc-experiment.com/Pluginfree-Screen-Sharing/

Клиент должен будет использовать Chrome (на данный момент) и должен будет включить поддержку захвата экрана в chrome: // flags.

Мэтт Синклер
источник
2
Я не могу найти демоверсии, просто сделав скриншот - все дело в совместном использовании экрана. придется попробовать.
JWL
8
@XMight, вы можете выбрать, разрешить ли это, переключив флаг поддержки захвата экрана.
Мэтт Синклер
19
@XMight Пожалуйста, не думай так. Веб-браузеры должны уметь многое делать, но, к сожалению, они не соответствуют их реализации. Это абсолютно нормально, если браузер имеет такую ​​функциональность, пока пользователь спрашивает. Никто не сможет сделать скриншот без вашего внимания. Но слишком сильный страх приводит к плохим реализациям, таким как API буфера обмена, который был полностью отключен, вместо этого создаются диалоговые окна подтверждения, такие как веб-камеры, микрофоны, возможность снимков экрана и т. Д.
StanE
3
Это устарело и будет удалено из стандарта в соответствии с developer.mozilla.org/en-US/docs/Web/API/Navigator/getUserMedia
Агустин Каутин
7
@AgustinCautin Navigator.getUserMedia()устарела, но чуть ниже написано: «... Пожалуйста, используйте более новый navigator.mediaDevices.getUserMedia () », то есть он был просто заменен более новым API.
Левант
37

Как упоминал Никлас, вы можете использовать библиотеку html2canvas, чтобы сделать скриншот с помощью JS в браузере. В этом пункте я расширю его ответ, приведя пример создания снимка экрана с помощью этой библиотеки:

В report()функции onrenderedпосле получения изображения в качестве URI данных вы можете показать его пользователю и позволить ему нарисовать «область ошибки» с помощью мыши, а затем отправить скриншот и координаты области на сервер.

В этом примере async/await была сделана версия: с хорошей makeScreenshot()функцией .

ОБНОВИТЬ

Простой пример, который позволяет вам сделать скриншот, выбрать регион, описать ошибку и отправить POST-запрос ( здесь jsfiddle ) (основная функция - report()).

Камил Келчевски
источник
10
Если вы хотите дать минус балл, оставьте также комментарий с объяснением
Камиль Келчевски
Я думаю, что причина, по которой вы получаете отрицательное голосование, заключается в том, что библиотека html2canvas - это его библиотека, а не инструмент, на который он просто указал.
Zfrisch
Это хорошо, если вы не хотите захватывать эффекты постобработки (как фильтр размытия).
Винтпроект
Ограничения Все изображения, которые использует скрипт, должны находиться в одном источнике, чтобы они могли читать их без помощи прокси. Точно так же, если у вас есть другие элементы canvas на странице, которые были испорчены контентом из разных источников, они станут грязными и больше не будут читаться html2canvas.
aravind3
14

Получите снимок экрана как Canvas или Jpeg Blob / ArrayBuffer с помощью API getDisplayMedia :

// docs: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
// see: https://www.webrtc-experiment.com/Pluginfree-Screen-Sharing/#20893521368186473
// see: https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Pluginfree-Screen-Sharing/conference.js

function getDisplayMedia(options) {
    if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
        return navigator.mediaDevices.getDisplayMedia(options)
    }
    if (navigator.getDisplayMedia) {
        return navigator.getDisplayMedia(options)
    }
    if (navigator.webkitGetDisplayMedia) {
        return navigator.webkitGetDisplayMedia(options)
    }
    if (navigator.mozGetDisplayMedia) {
        return navigator.mozGetDisplayMedia(options)
    }
    throw new Error('getDisplayMedia is not defined')
}

function getUserMedia(options) {
    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
        return navigator.mediaDevices.getUserMedia(options)
    }
    if (navigator.getUserMedia) {
        return navigator.getUserMedia(options)
    }
    if (navigator.webkitGetUserMedia) {
        return navigator.webkitGetUserMedia(options)
    }
    if (navigator.mozGetUserMedia) {
        return navigator.mozGetUserMedia(options)
    }
    throw new Error('getUserMedia is not defined')
}

async function takeScreenshotStream() {
    // see: https://developer.mozilla.org/en-US/docs/Web/API/Window/screen
    const width = screen.width * (window.devicePixelRatio || 1)
    const height = screen.height * (window.devicePixelRatio || 1)

    const errors = []
    let stream
    try {
        stream = await getDisplayMedia({
            audio: false,
            // see: https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints/video
            video: {
                width,
                height,
                frameRate: 1,
            },
        })
    } catch (ex) {
        errors.push(ex)
    }

    try {
        // for electron js
        stream = await getUserMedia({
            audio: false,
            video: {
                mandatory: {
                    chromeMediaSource: 'desktop',
                    // chromeMediaSourceId: source.id,
                    minWidth         : width,
                    maxWidth         : width,
                    minHeight        : height,
                    maxHeight        : height,
                },
            },
        })
    } catch (ex) {
        errors.push(ex)
    }

    if (errors.length) {
        console.debug(...errors)
    }

    return stream
}

async function takeScreenshotCanvas() {
    const stream = await takeScreenshotStream()

    if (!stream) {
        return null
    }

    // from: https://stackoverflow.com/a/57665309/5221762
    const video = document.createElement('video')
    const result = await new Promise((resolve, reject) => {
        video.onloadedmetadata = () => {
            video.play()
            video.pause()

            // from: https://github.com/kasprownik/electron-screencapture/blob/master/index.js
            const canvas = document.createElement('canvas')
            canvas.width = video.videoWidth
            canvas.height = video.videoHeight
            const context = canvas.getContext('2d')
            // see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement
            context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
            resolve(canvas)
        }
        video.srcObject = stream
    })

    stream.getTracks().forEach(function (track) {
        track.stop()
    })

    return result
}

// from: https://stackoverflow.com/a/46182044/5221762
function getJpegBlob(canvas) {
    return new Promise((resolve, reject) => {
        // docs: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
        canvas.toBlob(blob => resolve(blob), 'image/jpeg', 0.95)
    })
}

async function getJpegBytes(canvas) {
    const blob = await getJpegBlob(canvas)
    return new Promise((resolve, reject) => {
        const fileReader = new FileReader()

        fileReader.addEventListener('loadend', function () {
            if (this.error) {
                reject(this.error)
                return
            }
            resolve(this.result)
        })

        fileReader.readAsArrayBuffer(blob)
    })
}

async function takeScreenshotJpegBlob() {
    const canvas = await takeScreenshotCanvas()
    if (!canvas) {
        return null
    }
    return getJpegBlob(canvas)
}

async function takeScreenshotJpegBytes() {
    const canvas = await takeScreenshotCanvas()
    if (!canvas) {
        return null
    }
    return getJpegBytes(canvas)
}

function blobToCanvas(blob, maxWidth, maxHeight) {
    return new Promise((resolve, reject) => {
        const img = new Image()
        img.onload = function () {
            const canvas = document.createElement('canvas')
            const scale = Math.min(
                1,
                maxWidth ? maxWidth / img.width : 1,
                maxHeight ? maxHeight / img.height : 1,
            )
            canvas.width = img.width * scale
            canvas.height = img.height * scale
            const ctx = canvas.getContext('2d')
            ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height)
            resolve(canvas)
        }
        img.onerror = () => {
            reject(new Error('Error load blob to Image'))
        }
        img.src = URL.createObjectURL(blob)
    })
}

DEMO:

// take the screenshot
var screenshotJpegBlob = await takeScreenshotJpegBlob()

// show preview with max size 300 x 300 px
var previewCanvas = await blobToCanvas(screenshotJpegBlob, 300, 300)
previewCanvas.style.position = 'fixed'
document.body.appendChild(previewCanvas)

// send it to the server
let formdata = new FormData()
formdata.append("screenshot", screenshotJpegBlob)
await fetch('https://your-web-site.com/', {
    method: 'POST',
    body: formdata,
    'Content-Type' : "multipart/form-data",
})
Николай Махонин
источник
Интересно, почему это было только 1 upvote, это оказалось действительно полезным!
Джей Дадхания
Пожалуйста, как это работает? Можете ли вы предоставить демо для новичков, как я? Thx
kabrice
@kabrice Я добавил демо. Просто поместите код в консоль Chrome. Если вам нужна поддержка старых браузеров, используйте: babeljs.io/en/repl
Николай Махонин
8

Вот пример использования: getDisplayMedia

document.body.innerHTML = '<video style="width: 100%; height: 100%; border: 1px black solid;"/>';

navigator.mediaDevices.getDisplayMedia()
.then( mediaStream => {
  const video = document.querySelector('video');
  video.srcObject = mediaStream;
  video.onloadedmetadata = e => {
    video.play();
    video.pause();
  };
})
.catch( err => console.log(`${err.name}: ${err.message}`));

Также стоит ознакомиться с документами по Screen Capture API .

JSON C11
источник