Utilisation de HTML5 / Canvas / JavaScript pour prendre des captures d'écran dans le navigateur

924

"Signaler un bug" ou "Outil de rétroaction" de Google vous permet de sélectionner une zone de la fenêtre de votre navigateur pour créer une capture d'écran qui est soumise avec vos commentaires sur un bug.

Capture d'écran de l'outil de commentaires Google Capture d'écran de Jason Small, publiée dans une question en double .

Comment font-ils cela? L'API de rétroaction JavaScript de Google est chargée à partir d' ici et leur présentation du module de rétroaction montrera la capacité de capture d'écran.

joelvh
la source
2
Elliott Sprehn a écrit dans un Tweet il y a quelques jours:> @CatChen Ce post stackoverflow n'est pas précis. La capture d'écran de Google Feedback se fait entièrement côté client. :)
Goran Rakic
1
Cela semble logique car ils veulent comprendre exactement comment le navigateur de l'utilisateur affiche une page, pas comment ils le rendraient côté serveur à l'aide de leur moteur. Si vous envoyez uniquement la page DOM actuelle au serveur, il manquera toute incohérence dans la façon dont le navigateur rend le code HTML. Cela ne signifie pas que la réponse de Chen est erronée pour prendre des captures d'écran, il semble que Google le fasse d'une manière différente.
Goran Rakic
Elliott a mentionné Jan Kuča aujourd'hui, et j'ai trouvé ce lien dans le tweet de Jan: jankuca.tumblr.com/post/7391640769/…
Cat Chen
Je vais creuser cela plus tard et voir comment cela peut être fait avec le moteur de rendu côté client et vérifier si Google le fait réellement de cette façon.
Cat Chen
Je vois l'utilisation de compareDocumentPosition, getBoxObjectFor, toDataURL, drawImage, le padding de suivi et des choses comme ça. Ce sont des milliers de lignes de code obscurci à désobscurcir et à parcourir. J'adorerais en voir une version sous licence open source, j'ai contacté Elliott Sprehn!
Luke Stanley

Réponses:

1155

JavaScript peut lire le DOM et en rendre une représentation assez précise en utilisant canvas . J'ai travaillé sur un script qui convertit le HTML en une image de canevas. Décidé aujourd'hui d'en faire une implémentation en envoyant des commentaires comme vous l'avez décrit.

Le script vous permet de créer des formulaires de commentaires qui incluent une capture d'écran, créée sur le navigateur du client, avec le formulaire. La capture d'écran est basée sur le DOM et, en tant que telle, peut ne pas être exacte à 100% par rapport à la représentation réelle car elle ne fait pas de capture d'écran réelle, mais crée la capture d'écran en fonction des informations disponibles sur la page.

Il ne nécessite aucun rendu du serveur , car l'image entière est créée sur le navigateur du client. Le script HTML2Canvas lui-même est encore dans un état très expérimental, car il n'analyse pas autant des attributs CSS3 que je le souhaiterais, ni n'a aucun support pour charger des images CORS même si un proxy était disponible.

Compatibilité du navigateur encore assez limitée (pas parce que plus ne pouvait pas être pris en charge, je n'ai tout simplement pas eu le temps de le rendre plus compatible avec tous les navigateurs).

Pour plus d'informations, consultez les exemples ici:

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

modifier Le script html2canvas est maintenant disponible séparément ici et quelques exemples ici .

modifier 2 Une autre confirmation que Google utilise une méthode très similaire (en fait, sur la base de la documentation, la seule différence majeure est leur méthode asynchrone de traversée / dessin) peut être trouvée dans cette présentation par Elliott Sprehn de l'équipe Google Feedback: http: //www.elliottsprehn.com/preso/fluentconf/

Niklas
la source
1
Très cool, Sikuli ou Selenium pourrait être bon pour aller sur différents sites, en comparant une photo du site de l'outil de test à votre image rendue html2canvas.js en termes de similitude de pixels! Je me demande si vous pouvez parcourir automatiquement des parties du DOM avec un solveur de formule très simple pour trouver comment analyser d'autres sources de données pour les navigateurs où getBoundingClientRect n'est pas disponible. J'utiliserais probablement cela si c'était open source, envisageait de jouer avec moi-même. Beau travail Niklas!
Luke Stanley
1
@ Luke Stanley Je vais probablement jeter la source sur github ce week-end, encore quelques nettoyages et changements mineurs que je veux faire avant cela, ainsi que me débarrasser de la dépendance inutile de jQuery dont il dispose actuellement.
Niklas
43
Le code source est maintenant disponible sur github.com/niklasvh/html2canvas , quelques exemples du script utilisé html2canvas.hertzen.com là-bas. Encore beaucoup de bugs à corriger, donc je ne recommanderais pas encore d'utiliser le script dans un environnement live.
Niklas
2
toute solution pour le faire fonctionner pour SVG sera d'une grande aide. Cela ne fonctionne pas avec highcharts.com
Jagdeep
3
@Niklas Je vois que votre exemple est devenu un vrai projet. Peut-être mettre à jour votre commentaire le plus voté sur la nature expérimentale du projet. Après presque 900 commits, je pense que c'est un peu plus qu'une expérience à ce stade ;-)
Jogai
70

Votre application Web peut désormais prendre une capture d'écran «native» de l'ensemble du bureau du client en utilisant getUserMedia():

Jetez un œil à cet exemple:

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

Le client devra utiliser Chrome (pour l'instant) et devra activer la prise en charge de la capture d'écran sous les indicateurs chrome: //.

Matt Sinclair
la source
2
je ne trouve pas de démos de simplement prendre une capture d'écran - tout est question de partage d'écran. devra l'essayer.
jwl
8
@XMight, vous pouvez choisir d'autoriser cela en basculant le drapeau de support de capture d'écran.
Matt Sinclair
19
@XMight Veuillez ne pas penser comme ça. Les navigateurs Web devraient pouvoir faire beaucoup de choses, mais malheureusement ils ne sont pas cohérents avec leurs implémentations. C'est tout à fait correct, si un navigateur possède une telle fonctionnalité, tant que l'utilisateur est invité. Personne ne pourra faire une capture d'écran sans votre attention. Mais trop de peur entraîne de mauvaises implémentations, comme l'API du presse-papiers, qui a été complètement désactivée, créant à la place des boîtes de dialogue de confirmation, comme pour les webcams, les micros, la capacité de capture d'écran, etc.
StanE
3
Cela a été déprécié et sera supprimé de la norme selon developer.mozilla.org/en-US/docs/Web/API/Navigator/getUserMedia
Agustin Cautin
7
@AgustinCautin Navigator.getUserMedia()est obsolète, mais juste en dessous, il est écrit "... Veuillez utiliser le navigateur plus récent navigator.mediaDevices.getUserMedia () ", c'est-à-dire qu'il vient d'être remplacé par une API plus récente.
levant pied
37

Comme Niklas l'a mentionné, vous pouvez utiliser la bibliothèque html2canvas pour prendre une capture d'écran à l'aide de JS dans le navigateur. Je vais étendre sa réponse sur ce point en fournissant un exemple de capture d'écran à l'aide de cette bibliothèque:

En report()fonction onrenderedaprès avoir obtenu l'image sous forme d'URI de données, vous pouvez la montrer à l'utilisateur et lui permettre de dessiner une "région de bogue" avec la souris, puis envoyer une capture d'écran et les coordonnées de la région au serveur.

Dans cet exemple, une async/await version a été créée: avec une makeScreenshot()fonction agréable .

MISE À JOUR

Exemple simple qui vous permet de prendre une capture d'écran, de sélectionner une région, de décrire un bug et d'envoyer une requête POST ( ici jsfiddle ) (la fonction principale est report()).

Kamil Kiełczewski
la source
10
Si vous voulez donner un point négatif, laissez également un commentaire avec explication
Kamil Kiełczewski
Je pense que la raison pour laquelle vous obtenez un vote négatif est très probablement que la bibliothèque html2canvas est sa bibliothèque, pas un outil qu'il a simplement souligné.
zfrisch
C'est très bien si vous ne voulez pas capturer les effets de post-traitement (comme filtre de flou).
vintproykt
Limitations Toutes les images que le script utilise doivent résider sous la même origine pour qu'il puisse les lire sans l'aide d'un proxy. De même, si vous avez d'autres éléments de canevas sur la page, qui ont été entachés de contenu d'origine croisée, ils deviendront sales et ne seront plus lisibles par html2canvas.
aravind3
14

Obtenez une capture d'écran au format Canvas ou Jpeg Blob / ArrayBuffer à l'aide de l' 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)
    })
}

DÉMO:

// 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",
})
Nikolay Makhonin
la source
Je me demande pourquoi cela n'a eu qu'un vote positif, cela s'est avéré très utile!
Jay Dadhania
S'il vous plaît comment ça marche? Pouvez-vous fournir une démo pour des débutants comme moi? Thx
kabrice
@kabrice J'ai ajouté une démo. Mettez simplement le code dans la console Chrome. Si vous avez besoin de la prise en charge d'anciens navigateurs, utilisez: babeljs.io/en/repl
Nikolay Makhonin
8

Voici un exemple en utilisant: 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}`));

Les documents de l' API de capture d'écran méritent également d'être vérifiés .

JSON C11
la source