Verwenden von HTML5 / Canvas / JavaScript zum Erstellen von Screenshots im Browser

924

Mit "Fehler melden" oder "Feedback-Tool" von Google können Sie einen Bereich Ihres Browserfensters auswählen, um einen Screenshot zu erstellen, der mit Ihrem Feedback zu einem Fehler übermittelt wird.

Screenshot des Google Feedback-Tools Screenshot von Jason Small, in einer doppelten Frage veröffentlicht .

Wie machen sie das? Googles JavaScript - Feedback - API geladen wird von hier und ihre Übersicht über den Feedback - Modul wird die Screenshot Fähigkeit demonstrieren.

Joelvh
quelle
2
Elliott Sprehn schrieb vor ein paar Tagen in einem Tweet :> @CatChen Dieser Stackoverflow-Beitrag ist nicht korrekt. Der Screenshot von Google Feedback wird vollständig clientseitig erstellt. :)
Goran Rakic
1
Dies ist logisch, da sie genau erfassen möchten, wie der Browser des Benutzers eine Seite rendert, und nicht, wie sie sie mit ihrer Engine auf der Serverseite rendern würden. Wenn Sie nur das aktuelle Seiten-DOM an den Server senden, werden Inkonsistenzen beim Rendern des HTML-Codes durch den Browser übersehen. Dies bedeutet nicht, dass Chens Antwort für das Aufnehmen von Screenshots falsch ist. Es sieht nur so aus, als würde Google dies anders machen.
Goran Rakic
Elliott erwähnte Jan Kuča heute und ich fand diesen Link in Jan's Tweet: jankuca.tumblr.com/post/7391640769/…
Cat Chen
Ich werde später darauf eingehen und sehen, wie dies mit der clientseitigen Rendering-Engine gemacht werden kann, und prüfen, ob Google dies tatsächlich auf diese Weise tut.
Cat Chen
Ich sehe die Verwendung von compareDocumentPosition, getBoxObjectFor, toDataURL, drawImage, Tracking Padding und ähnlichen Dingen. Es sind Tausende von Zeilen verschleierten Codes, die verschleiert und durchgesehen werden müssen. Ich würde gerne eine Open Source lizenzierte Version davon sehen, ich habe Elliott Sprehn kontaktiert!
Luke Stanley

Antworten:

1154

JavaScript kann das DOM lesen und eine ziemlich genaue Darstellung davon mit rendern canvas. Ich habe an einem Skript gearbeitet, das HTML in ein Canvas-Bild konvertiert. Ich habe heute beschlossen, eine Implementierung davon durchzuführen, um Feedbacks zu senden, wie Sie es beschrieben haben.

Mit dem Skript können Sie Feedback-Formulare erstellen, die einen Screenshot enthalten, der im Browser des Kunden zusammen mit dem Formular erstellt wurde. Der Screenshot basiert auf dem DOM und ist daher möglicherweise nicht 100% genau für die tatsächliche Darstellung, da kein tatsächlicher Screenshot erstellt wird, sondern der Screenshot auf der Grundlage der auf der Seite verfügbaren Informationen erstellt wird.

Es ist kein Rendern vom Server erforderlich , da das gesamte Bild im Browser des Clients erstellt wird. Das HTML2Canvas-Skript selbst befindet sich noch in einem sehr experimentellen Zustand, da es nicht annähernd so viele CSS3-Attribute analysiert, wie ich es gerne hätte, und es auch keine Unterstützung zum Laden von CORS-Images bietet, selbst wenn ein Proxy verfügbar war.

Immer noch recht eingeschränkte Browserkompatibilität (nicht, weil mehr nicht unterstützt werden konnten, ich hatte nur keine Zeit, es browserübergreifender zu unterstützen).

Weitere Informationen finden Sie in den Beispielen hier:

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

edit Das Skript html2canvas ist jetzt hier separat und einige Beispiele hier verfügbar .

edit 2 Eine weitere Bestätigung, dass Google eine sehr ähnliche Methode verwendet (tatsächlich ist der einzige wesentliche Unterschied, basierend auf der Dokumentation, die asynchrone Methode zum Durchlaufen / Zeichnen), findet sich in dieser Präsentation von Elliott Sprehn vom Google Feedback-Team: http: //www.elliottsprehn.com/preso/fluentconf/

Niklas
quelle
1
Sehr cool, Sikuli oder Selen können gut sein, um zu verschiedenen Sites zu gehen und eine Aufnahme der Site vom Test-Tool mit dem von html2canvas.js gerenderten Bild in Bezug auf Pixelähnlichkeit zu vergleichen! Ich frage mich, ob Sie Teile des DOM automatisch mit einem sehr einfachen Formellöser durchlaufen können, um herauszufinden, wie alternative Datenquellen für Browser analysiert werden können, in denen getBoundingClientRect nicht verfügbar ist. Ich würde dies wahrscheinlich verwenden, wenn es Open Source wäre, und darüber nachdenken, selbst damit zu spielen. Gute Arbeit Niklas!
Luke Stanley
1
@ Luke Stanley Ich werde die Quelle höchstwahrscheinlich an diesem Wochenende auf Github werfen, noch einige kleinere Aufräumarbeiten und Änderungen, die ich vorher vornehmen möchte, sowie die unnötige jQuery-Abhängigkeit, die es derzeit hat, loswerden.
Niklas
43
Der Quellcode ist jetzt unter github.com/niklasvh/html2canvas verfügbar , einige Beispiele für das dort verwendete Skript html2canvas.hertzen.com . Es müssen noch viele Fehler behoben werden, daher würde ich die Verwendung des Skripts in einer Live-Umgebung noch nicht empfehlen.
Niklas
2
Jede Lösung, damit es für SVG funktioniert, ist eine große Hilfe. Es funktioniert nicht mit highcharts.com
Jagdeep
3
@Niklas Ich sehe, dein Beispiel hat sich zu einem echten Projekt entwickelt. Aktualisieren Sie möglicherweise Ihren am besten bewerteten Kommentar zum experimentellen Charakter des Projekts. Nach fast 900 Commits würde ich denken, dass es zu diesem Zeitpunkt etwas mehr als ein Experiment ist
;-)
70

Ihre Web-App kann jetzt einen "nativen" Screenshot des gesamten Desktops des Clients erstellen, indem Sie Folgendes verwenden getUserMedia():

Schauen Sie sich dieses Beispiel an:

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

Der Client muss (vorerst) Chrome verwenden und die Unterstützung für die Bildschirmaufnahme unter chrome: // flags aktivieren.

Matt Sinclair
quelle
2
Ich kann keine Demos finden, die nur einen Screenshot machen - alles dreht sich um das Teilen von Bildschirmen. muss es versuchen.
JWL
8
@XMight können Sie auswählen, ob dies zugelassen werden soll, indem Sie das Flag zur Unterstützung der Bildschirmaufnahme aktivieren.
Matt Sinclair
19
@XMight Bitte denk nicht so. Webbrowser sollten in der Lage sein, viele Dinge zu tun, aber leider stimmen sie nicht mit ihren Implementierungen überein. Es ist absolut in Ordnung, wenn ein Browser über solche Funktionen verfügt, solange der Benutzer gefragt wird. Ohne Ihre Aufmerksamkeit kann niemand einen Screenshot machen. Aber zu viel Angst führt zu schlechten Implementierungen, wie der API der Zwischenablage, die vollständig deaktiviert wurde, und stattdessen Bestätigungsdialoge wie für Webcams, Mikrofone, Screenshot-Funktionen usw. erstellt
StanE
3
Dies war veraltet und wird laut developer.mozilla.org/en-US/docs/Web/API/Navigator/getUserMedia
Agustin Cautin
7
@AgustinCautin Navigator.getUserMedia()ist veraltet, aber direkt darunter steht "... Bitte verwenden Sie den neueren navigator.mediaDevices.getUserMedia () ", dh es wurde gerade durch eine neuere API ersetzt.
Levant pied
37

Wie Niklas bereits erwähnt hat, können Sie mit der html2canvas- Bibliothek einen Screenshot mit JS im Browser erstellen . Ich werde seine Antwort an dieser Stelle durch ein Beispiel für die Erstellung eines Screenshots mit dieser Bibliothek erweitern:

In report()Funktion in onrenderednach wie Daten URI bekommen Bild , das Sie es dem Benutzer zeigen kann , und ihm erlauben , „Bug - Region“ mit der Maus zu ziehen und dann einen Screenshot und Region Koordinaten an den Server senden.

In diesem Beispiel wurde async/await Version gemacht: mit schöner makeScreenshot()Funktion .

AKTUALISIEREN

Ein einfaches Beispiel, mit dem Sie einen Screenshot machen, eine Region auswählen, einen Fehler beschreiben und eine POST-Anfrage senden können ( hier jsfiddle ) (die Hauptfunktion ist report()).

Kamil Kiełczewski
quelle
10
Wenn Sie Minuspunkt geben
möchten
Ich denke, der Grund, warum Sie herabgestimmt werden, ist höchstwahrscheinlich, dass die html2canvas-Bibliothek seine Bibliothek ist, kein Werkzeug, auf das er einfach hingewiesen hat.
zfrisch
Es ist in Ordnung, wenn Sie keine Nachbearbeitungseffekte (als Unschärfefilter) erfassen möchten.
vintproykt
Einschränkungen Alle vom Skript verwendeten Bilder müssen sich unter demselben Ursprung befinden, damit sie ohne Unterstützung eines Proxys gelesen werden können. Wenn Sie andere Canvas-Elemente auf der Seite haben, die mit Ursprungsinhalten belastet sind, werden diese ebenfalls schmutzig und können von html2canvas nicht mehr gelesen werden.
Aravind3
13

Screenshot als Canvas oder Jpeg Blob / ArrayBuffer mit getDisplayMedia API abrufen :

// 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",
})
Nikolay Makhonin
quelle
Ich frage mich, warum dies nur eine positive Bewertung hatte. Dies erwies sich als sehr hilfreich!
Jay Dadhania
Bitte wie funktioniert es? Können Sie Neulingen wie mir eine Demo anbieten? Thx
kabrice
@kabrice Ich habe eine Demo hinzugefügt. Geben Sie einfach den Code in die Chrome-Konsole ein. Wenn Sie Unterstützung für alte Browser benötigen, verwenden Sie: babeljs.io/en/repl
Nikolay Makhonin
8

Hier ist ein Beispiel mit: 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}`));

Ebenfalls einen Besuch wert sind die Screen Capture API- Dokumente.

JSON C11
quelle