Buscar..


Observaciones

Este tema trata sobre los distintos tipos de medios y cómo se pueden utilizar con el lienzo en la interfaz 2D.

Los tipos de medios tienen categorías genéricas y específicas de formato.

Tipos de medios

  • Animaciones
  • Videos
  • Imágenes
  • Imagenes HD
  • Imagen vectorial
  • Imagenes animadas

Formatos de medios

  • Jpg / Jpeg
  • Png
  • Gif
  • SVG
  • M-JPEG
  • Webm
  • Webp

Imágenes

Hay una gran variedad de formatos de imagen compatibles con los navegadores, aunque ningún navegador los admite a todos. Si tiene formatos de imagen particulares, desea utilizar los navegadores Wiki y los formatos de imagen compatibles proporciona una buena visión general.

El mejor soporte es para los 3 formatos principales, "jpeg", "png" y "gif" con todos los navegadores principales que brindan soporte.

JPEG

Las imágenes JPEG se adaptan mejor a las fotos y fotos como imágenes. No se prestan a los cuadros, diagramas y texto. Las imágenes JPEG no admiten transparencia.

Canvas puede generar imágenes JPEG a través de canvas.toDataURL y canvas.toBlob y proporciona una configuración de calidad. Como JPEG no admite la transparencia, todos los píxeles transparentes se mezclarán con el negro para la salida final JPG. La imagen resultante no será una copia perfecta del lienzo.

JPEG en wikipedia

Png

Las imágenes PNG son imágenes de la más alta calidad y también pueden incluir un canal alfa para píxeles transparentes. Los datos de la imagen están comprimidos pero no producen artefactos como las imágenes JPG.

Debido a la compresión sin pérdida y el soporte de canal alfa, los PNG se utilizan para juegos, imágenes de componentes de interfaz de usuario, cuadros, diagramas y texto. Cuando se usan para fotos como fotos, su tamaño de archivo puede ser mucho más grande que el de JPEG. .

El formato PNG también proporciona soporte de animación, aunque el soporte del navegador es limitado, y el acceso a la animación para usar en el lienzo solo se puede realizar a través de las API y bibliotecas de Javascript

El lienzo se puede usar para codificar imágenes PNG a través de canvas.toDataURL y canvas.toBlob aunque el formato de salida está limitado a RGBA comprimido de 32 bits. El PNG proporcionará una copia perfecta del pixel del lienzo.

PNG en wikipedia

GIF

Los GIF se usan para animaciones cortas, pero también se pueden usar para proporcionar gráficos, diagramas y texto de alta calidad como imágenes. Los GIF tienen un soporte de color muy limitado con un máximo de 256 colores por fotograma. Con el procesamiento de imágenes cleaver, las imágenes gif pueden producir resultados sorprendentemente buenos, especialmente cuando se animan. Los gifs también proporcionan transparencia, aunque esto está limitado a activado o desactivado.

AS con PNG, las animaciones GIF no son accesibles directamente para su uso en el lienzo y necesitará una API o biblioteca de Javascript para obtener acceso. GIF no se puede guardar a través del lienzo y requerirá una API o una biblioteca para hacerlo.

GIF en wikipedia

Cargando y mostrando una imagen

Para cargar una imagen y colocarla en el lienzo.

var image = new Image();  // see note on creating an image
image.src = "imageURL";
image.onload = function(){
    ctx.drawImage(this,0,0);
}

Creando una imagen

Hay varias formas de crear una imagen.

  • new Image()
  • document.createElement("img")
  • <img src = 'imageUrl' id='myImage'> Como parte del cuerpo HTML y recuperado con document.getElementById('myImage')

La imagen es un HTMLImageElement

Propiedad image.src

La imagen src puede ser cualquier URL de imagen válida o dataURL codificada. Consulte las Observaciones de este tema para obtener más información sobre los formatos de imagen y el soporte.

  • image.src = "http://my.domain.com/images/myImage.jpg"
  • image.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=" *

* El dataURL es una imagen gif de 1 por 1 píxel que contiene negro

Observaciones sobre carga y errores.

La imagen comenzará a cargarse cuando se establezca su propiedad src. La carga es syncriouse pero el evento onload no se llamará hasta que la función o el código haya salido / devuelto.

Si obtiene una imagen de la página (por ejemplo document.getElementById("myImage") ) y su src está configurado puede o no puede haber cargado. Puede verificar el estado de la imagen con HTMLImageElement.complete que será true si está completo. Esto no significa que la imagen se haya cargado, significa que tiene

  • cargado
  • Hubo un error
  • La propiedad src no se ha establecido y es igual a la cadena vacía ""

Si la imagen es de una fuente no confiable y puede no ser accesible por una variedad de razones, generará un evento de error. Cuando esto suceda, la imagen estará en un estado roto. Si intenta dibujarlo en el lienzo, se mostrará el siguiente error.

Uncaught DOMException: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The HTMLImageElement provided is in the 'broken' state.

Al proporcionar el evento image.onerror = myImgErrorHandler , puede tomar las medidas adecuadas para evitar errores.

Dibujando una imagen svg

Para dibujar una imagen SVG vectorial, la operación no es diferente de una imagen rasterizada:
Primero debe cargar su imagen SVG en un elemento HTMLImage, luego usar el método drawImage() .

var image = new Image();
image.onload = function(){
    ctx.drawImage(this, 0,0);
}
image.src = "someFile.SVG";

Las imágenes SVG tienen algunas ventajas sobre las trama, ya que no perderá calidad, independientemente de la escala que dibuje en su lienzo. Pero cuidado, también puede ser un poco más lento que dibujar una imagen rasterizada.

Sin embargo, las imágenes SVG vienen con más restricciones que las imágenes rasterizadas.

  • Por razones de seguridad, no se puede cargar contenido externo desde una imagen SVG a la que se hace referencia en un elemento de imagen HTML ( <img> )
    Sin hoja de estilo externa, sin imagen externa referenciada en los elementos SVGImage ( <image/> ), sin filtro externo o elemento vinculado por el atributo xlink:href ( <use xlink:href="anImage.SVG#anElement"/> ) o la funcIRI ( url() ) método de atributo, etc.
    Además, las hojas de estilo adjuntas en el documento principal no tendrán ningún efecto en el documento SVG una vez referenciado en un elemento HTMLImage.
    Finalmente, no se ejecutará ningún script dentro de la imagen SVG.
    Solución alternativa: deberá adjuntar todos los elementos externos dentro del propio SVG antes de hacer referencia al elemento HTMLImage. (para imágenes o fuentes, debe adjuntar una versión dataURI de sus recursos externos).

  • El elemento raíz ( <svg> ) debe tener sus atributos de ancho y alto establecidos en un valor absoluto.
    Si tuviera que usar la longitud relativa (por ejemplo, % ), entonces el navegador no podrá saber qué es relativo. Algunos navegadores (Blink) intentarán hacer una conjetura, pero la mayoría simplemente ignorará su imagen y no dibujará nada, sin una advertencia.

  • Algunos navegadores mancharán el lienzo cuando se dibuje una imagen SVG.
    Específicamente, Internet-Explorer <Edge en cualquier caso, y Safari 9 cuando un <foreignObject> está presente en la imagen SVG.

Carga básica y reproducción de un video en el lienzo.

El lienzo se puede usar para mostrar videos de una variedad de fuentes. Este ejemplo muestra cómo cargar un video como un recurso de archivo, mostrarlo y agregar un simple clic en la pantalla para reproducir / pausar para alternar.

Esta pregunta de respuesta automática de Stackoverflow ¿Cómo muestro un video utilizando la etiqueta de lienzo HTML5 muestra el siguiente código de ejemplo en acción?

Solo una imagen

Un video es solo una imagen en lo que concierne al lienzo. Puedes dibujarlo como cualquier imagen. La diferencia es que el video puede reproducirse y tiene sonido.

Consigue lienzo y configuración básica.

// It is assumed you know how to add a canvas and correctly size it.
var canvas = document.getElementById("myCanvas"); // get the canvas from the page
var ctx = canvas.getContext("2d");
var videoContainer; // object to hold video and associated info

Creando y cargando el video

var video = document.createElement("video"); // create a video element
video.src = "urlOffVideo.webm"; 
// the video will now begin to load.
// As some additional info is needed we will place the video in a
// containing object for convenience
video.autoPlay = false; // ensure that the video does not auto play
video.loop = true; // set the video to loop.
videoContainer = {  // we will add properties as needed
     video : video,
     ready : false,   
};

A diferencia de los elementos de imágenes, los videos no tienen que estar completamente cargados para mostrarse en el lienzo. Los videos también proporcionan una gran cantidad de eventos adicionales que se pueden usar para monitorear el estado del video.

En este caso, deseamos saber cuándo el video está listo para reproducirse. oncanplay significa que se ha cargado suficiente video para reproducirlo, pero puede que no haya suficiente para reproducirlo hasta el final.

video.oncanplay = readyToPlayVideo; // set the event to the play function that 
                                  // can be found below

Alternativamente, puede usar el oncanplaythrough que se disparará cuando se haya cargado suficiente video para que pueda reproducirse hasta el final.

video.oncanplaythrough = readyToPlayVideo; // set the event to the play function that
                                         // can be found below

Utilice solo uno de los eventos canPlay no ambos.

El evento puede jugar (equivalente a la carga de imágenes)

function readyToPlayVideo(event){ // this is a referance to the video
    // the video may not match the canvas size so find a scale to fit
    videoContainer.scale = Math.min(
                         canvas.width / this.videoWidth, 
                         canvas.height / this.videoHeight); 
    videoContainer.ready = true;
    // the video can be played so hand it off to the display function
    requestAnimationFrame(undateCanvas);
}

Mostrando

El video no se reproducirá en el lienzo. Necesitas dibujarlo para cada nuevo marco. Como es difícil saber la velocidad de fotogramas exacta y cuando se producen, el mejor enfoque es mostrar el video como si se estuviera ejecutando a 60 fps. Si la velocidad de fotogramas es menor, entonces w simplemente renderiza el mismo fotograma dos veces. Si la velocidad de fotogramas es mayor, no hay nada que se pueda hacer para ver los fotogramas adicionales, por lo que simplemente los ignoramos.

El elemento de video es solo un elemento de imagen y puede dibujarse como cualquier imagen, puede escalar, rotar, panoramizar el video, reflejarlo, desvanecerlo, recortarlo y mostrar solo partes, dibujarlo dos veces la segunda vez con un modo compuesto global Para añadir efectos como lighten, screen, etc.

function updateCanvas(){
    ctx.clearRect(0,0,canvas.width,canvas.height); // Though not always needed 
                                                     // you may get bad pixels from 
                                                     // previous videos so clear to be
                                                     // safe
    // only draw if loaded and ready
    if(videoContainer !== undefined && videoContainer.ready){ 
        // find the top left of the video on the canvas
        var scale = videoContainer.scale;
        var vidH = videoContainer.video.videoHeight;
        var vidW = videoContainer.video.videoWidth;
        var top = canvas.height / 2 - (vidH /2 ) * scale;
        var left = canvas.width / 2 - (vidW /2 ) * scale;
        // now just draw the video the correct size
        ctx.drawImage(videoContainer.video, left, top, vidW * scale, vidH * scale);
        if(videoContainer.video.paused){ // if not playing show the paused screen 
            drawPayIcon();
        }
    }
    // all done for display 
    // request the next frame in 1/60th of a second
    requestAnimationFrame(updateCanvas);
}

Control de pausa de juego básico

Ahora tenemos el video cargado y mostramos todo lo que necesitamos es el control de reproducción. Lo haremos como un click toggle play en la pantalla. Cuando el video se está reproduciendo y el usuario hace clic, el video se detiene. Cuando está en pausa, el clic reanuda la reproducción. Agregaremos una función para oscurecer el video y dibujar un ícono de reproducción (triángulo)

function drawPayIcon(){
     ctx.fillStyle = "black";  // darken display
     ctx.globalAlpha = 0.5;
     ctx.fillRect(0,0,canvas.width,canvas.height);
     ctx.fillStyle = "#DDD"; // colour of play icon
     ctx.globalAlpha = 0.75; // partly transparent
     ctx.beginPath(); // create the path for the icon
     var size = (canvas.height / 2) * 0.5;  // the size of the icon
     ctx.moveTo(canvas.width/2 + size/2, canvas.height / 2); // start at the pointy end
     ctx.lineTo(canvas.width/2 - size/2, canvas.height / 2 + size);
     ctx.lineTo(canvas.width/2 - size/2, canvas.height / 2 - size);
     ctx.closePath();
     ctx.fill();
     ctx.globalAlpha = 1; // restore alpha
}    

Ahora el evento de pausa de juego

function playPauseClick(){
     if(videoContainer !== undefined && videoContainer.ready){
          if(videoContainer.video.paused){                                 
                videoContainer.video.play();
          }else{
                videoContainer.video.pause();
          }
     }
}
// register the event
canvas.addEventListener("click",playPauseClick);

Resumen

Reproducir un video es muy fácil usando el lienzo, agregar efecto en tiempo real también es fácil. Sin embargo, existen algunas limitaciones en cuanto a los formatos, cómo puedes jugar y buscar. MDN HTMLMediaElement es el lugar para obtener la referencia completa al objeto de video.

Una vez que la imagen se ha dibujado en el lienzo, puede utilizar ctx.getImageData para acceder a los píxeles que contiene. O puede usar canvas.toDataURL para tomar una canvas.toDataURL y descargarla. (Solo si el video es de una fuente confiable y no mancha el lienzo).

Tenga en cuenta que si el video tiene sonido, si lo reproduce, también reproducirá el sonido.

Feliz videoing

Capturar lienzo y guardar como video webM

Crear un video de WebM desde marcos de lienzo y reproducir en lienzo, o cargar o descargar.

Ejemplo de captura y reproducción de lienzo.

name = "CanvasCapture"; // Placed into the Mux and Write Application Name fields of the WebM header
quality = 0.7; // good quality 1 Best < 0.7 ok to poor
fps = 30; // I have tried all sorts of frame rates and all seem to work
          // Do some test to workout what your machine can handle as there
          // is a lot of variation between machines.
var video = new Groover.Video(fps,quality,name)
function capture(){
    if(video.timecode < 5000){ // 5 seconds
         setTimeout(capture,video.frameDelay);             
    }else{
         var videoElement = document.createElement("video");
         videoElement.src = URL.createObjectURL(video.toBlob());
         document.body.appendChild(videoElement);
         video = undefined; // DeReference as it is memory hungry.
         return;
    }
    // first frame sets the video size
    video.addFrame(canvas); // Add current canvas frame
}
capture(); // start capture

En lugar de hacer un gran esfuerzo solo para ser rechazado, esta es una inserción rápida para ver si es aceptable. Dará los detalles completos si es aceptado. También incluye opciones de captura adicionales para obtener mejores tasas de captura de HD (eliminada de esta versión, puede capturar HD 1080 a 50 fps en buenas máquinas).

Esto fue inspirado por Wammy pero es una completa reescritura con codificación a medida que avanza la metodología, reduciendo en gran medida la memoria requerida durante la captura. Puede capturar más de 30 segundos mejores datos, manejando algoritmos.

Los cuadros de notas están codificados en imágenes webP. Solo Chrome soporta codificación webP canvas. Para otros navegadores (Firefox y Edge), deberá utilizar un codificador webP de terceros, como Libwebp. La codificación de imágenes WebP a través de Javascript es lenta. (Incluirá la adición de soporte de imágenes webp sin formato si es aceptado).

El codificador webM inspirado por Whammy: un Javascript en tiempo real WebM

var Groover = (function(){
    // ensure webp is supported 
    function canEncode(){
        var canvas = document.createElement("canvas");
        canvas.width = 8;
        canvas.height = 8;
        return canvas.toDataURL("image/webp",0.1).indexOf("image/webp") > -1;
    }
    if(!canEncode()){
        return undefined;
    }    
    var webmData = null;
    var clusterTimecode = 0;
    var clusterCounter = 0;
    var CLUSTER_MAX_DURATION = 30000;
    var frameNumber = 0;
    var width;
    var height;
    var frameDelay;
    var quality;
    var name;
    const videoMimeType = "video/webm"; // the only one.
    const frameMimeType = 'image/webp'; // can be no other
    const S = String.fromCharCode;
    const dataTypes = {
        object : function(data){ return toBlob(data);},
        number : function(data){ return stream.num(data);},
        string : function(data){ return stream.str(data);},
        array  : function(data){ return data;}, 
        double2Str : function(num){
            var c = new Uint8Array((new Float64Array([num])).buffer);
            return S(c[7]) + S(c[6]) + S(c[5]) + S(c[4]) + S(c[3]) + S(c[2]) + S(c[1]) + S(c[0]);
        }
    };    
   
    const stream = {
        num : function(num){ // writes int
            var parts = [];
            while(num > 0){ parts.push(num & 0xff); num = num >> 8; }
            return new Uint8Array(parts.reverse());
        },
        str : function(str){ // writes string
            var i, len, arr;
            len = str.length;
            arr = new Uint8Array(len);
            for(i = 0; i < len; i++){arr[i] = str.charCodeAt(i);}
            return arr;
        },
        compInt : function(num){ // could not find full details so bit of a guess
            if(num < 128){       // number is prefixed with a bit (1000 is on byte 0100 two, 0010 three and so on)
                num += 0x80;
                return new Uint8Array([num]);
            }else
            if(num < 0x4000){
                num += 0x4000;
                return new Uint8Array([num>>8, num])
            }else
            if(num < 0x200000){
                num += 0x200000;
                return new Uint8Array([num>>16, num>>8, num])
            }else
            if(num < 0x10000000){
                num += 0x10000000;
                return new Uint8Array([num>>24, num>>16, num>>8, num])
            }            
        }
    }
    const ids = { // header names and values
        videoData          : 0x1a45dfa3, 
        Version            : 0x4286,
        ReadVersion        : 0x42f7,
        MaxIDLength        : 0x42f2,
        MaxSizeLength      : 0x42f3,
        DocType            : 0x4282,
        DocTypeVersion     : 0x4287,
        DocTypeReadVersion : 0x4285,
        Segment            : 0x18538067,
        Info               : 0x1549a966,
        TimecodeScale      : 0x2ad7b1,
        MuxingApp          : 0x4d80,
        WritingApp         : 0x5741,
        Duration           : 0x4489,
        Tracks             : 0x1654ae6b,
        TrackEntry         : 0xae,
        TrackNumber        : 0xd7,
        TrackUID           : 0x63c5,
        FlagLacing         : 0x9c,
        Language           : 0x22b59c,
        CodecID            : 0x86,
        CodecName          : 0x258688,
        TrackType          : 0x83,
        Video              : 0xe0,
        PixelWidth         : 0xb0,
        PixelHeight        : 0xba,
        Cluster            : 0x1f43b675,
        Timecode           : 0xe7,
        Frame              : 0xa3,
        Keyframe           : 0x9d012a,
        FrameBlock         : 0x81,
    };
    const keyframeD64Header = '\x9d\x01\x2a'; //VP8 keyframe header 0x9d012a
    const videoDataPos = 1; // data pos of frame data header
    const defaultDelay = dataTypes.double2Str(1000/25);
    const header = [  // structure of webM header/chunks what ever they are called.
        ids.videoData,[
            ids.Version, 1,
            ids.ReadVersion, 1,
            ids.MaxIDLength, 4,
            ids.MaxSizeLength, 8,
            ids.DocType, 'webm',
            ids.DocTypeVersion, 2,
            ids.DocTypeReadVersion, 2
        ],
        ids.Segment, [
            ids.Info, [
                ids.TimecodeScale, 1000000,
                ids.MuxingApp, 'Groover',
                ids.WritingApp, 'Groover',
                ids.Duration, 0
            ],
            ids.Tracks,[
                ids.TrackEntry,[
                    ids.TrackNumber, 1,
                    ids.TrackUID, 1,
                    ids.FlagLacing, 0,     // always o
                    ids.Language, 'und',   // undefined I think this means
                    ids.CodecID, 'V_VP8',  // These I think must not change
                    ids.CodecName, 'VP8',  // These I think must not change
                    ids.TrackType, 1,
                    ids.Video, [
                        ids.PixelWidth, 0,
                        ids.PixelHeight, 0
                    ]
                ]
            ]
        ]
    ];    
    function getHeader(){
        header[3][2][3] = name;
        header[3][2][5] = name;
        header[3][2][7] =  dataTypes.double2Str(frameDelay);
        header[3][3][1][15][1] =  width;
        header[3][3][1][15][3] =  height;
        function create(dat){
            var i,kv,data;
            data = [];
            for(i = 0; i < dat.length; i += 2){
                kv = {i : dat[i]};
                if(Array.isArray(dat[i + 1])){
                    kv.d = create(dat[i + 1]);
                }else{
                    kv.d = dat[i + 1];
                }
                data.push(kv);
            }
            return data;
        }
        return create(header);
    }
    function addCluster(){
        webmData[videoDataPos].d.push({ i: ids.Cluster,d: [{ i: ids.Timecode, d: Math.round(clusterTimecode)}]}); // Fixed bug with Round
        clusterCounter = 0;
    }
    function addFrame(frame){
        var VP8, kfS,riff;
        riff = getWebPChunks(atob(frame.toDataURL(frameMimeType, quality).slice(23)));
        VP8 = riff.RIFF[0].WEBP[0];
        kfS = VP8.indexOf(keyframeD64Header) + 3;
        frame = {
            width: ((VP8.charCodeAt(kfS + 1) << 8) | VP8.charCodeAt(kfS)) & 0x3FFF,
            height: ((VP8.charCodeAt(kfS + 3) << 8) | VP8.charCodeAt(kfS + 2)) & 0x3FFF,
            data: VP8,
            riff: riff
        };
        if(clusterCounter > CLUSTER_MAX_DURATION){
            addCluster();            
        }
        webmData[videoDataPos].d[webmData[videoDataPos].d.length-1].d.push({
            i: ids.Frame, 
            d: S(ids.FrameBlock) + S( Math.round(clusterCounter) >> 8) +  S( Math.round(clusterCounter) & 0xff) + S(128) + frame.data.slice(4),
        });
        clusterCounter += frameDelay;        
        clusterTimecode += frameDelay;
        webmData[videoDataPos].d[0].d[3].d = dataTypes.double2Str(clusterTimecode);
    }
    function startEncoding(){
        frameNumber = clusterCounter = clusterTimecode = 0;
        webmData  = getHeader();
        addCluster();
    }    
    function toBlob(vidData){
        var data,i,vData, len;
        vData = [];
        for(i = 0; i < vidData.length; i++){
            data = dataTypes[typeof vidData[i].d](vidData[i].d);
            len  = data.size || data.byteLength || data.length;
            vData.push(stream.num(vidData[i].i));
            vData.push(stream.compInt(len));
            vData.push(data)
        }
        return new Blob(vData, {type: videoMimeType});
    }
    function getWebPChunks(str){
        var offset, chunks, id, len, data;
        offset = 0;
        chunks = {};
        while (offset < str.length) {
            id = str.substr(offset, 4);
            // value will have top bit on (bit 32) so not simply a bitwise operation
            // Warning little endian (Will not work on big endian systems)
            len = new Uint32Array(
                new Uint8Array([
                    str.charCodeAt(offset + 7),
                    str.charCodeAt(offset + 6),
                    str.charCodeAt(offset + 5),
                    str.charCodeAt(offset + 4)
                ]).buffer)[0];
            id = str.substr(offset, 4);
            chunks[id] = chunks[id] === undefined ? [] : chunks[id];
            if (id === 'RIFF' || id === 'LIST') {
                chunks[id].push(getWebPChunks(str.substr(offset + 8, len)));
                offset += 8 + len;
            } else if (id === 'WEBP') {
                chunks[id].push(str.substr(offset + 8));
                break;
            } else {
                chunks[id].push(str.substr(offset + 4));
                break;
            }
        }
        return chunks;
    }
    function Encoder(fps, _quality = 0.8, _name = "Groover"){ 
        this.fps = fps;
        this.quality = quality = _quality;
        this.frameDelay = frameDelay = 1000 / fps;
        this.frame = 0;
        this.width = width = null;
        this.timecode = 0;
        this.name = name = _name;
    }
    Encoder.prototype = {
        addFrame : function(frame){
            if('canvas' in frame){
                frame = frame.canvas;    
            }
            if(width === null){
                this.width = width = frame.width,
                this.height = height = frame.height
                startEncoding();
            }else
            if(width !== frame.width || height !== frame.height){
                throw RangeError("Frame size error. Frames must be the same size.");
            }            
            addFrame(frame);   
            this.frame += 1;
            this.timecode = clusterTimecode;
        },        
        toBlob : function(){
            return toBlob(webmData);
        }
    }
    return {
        Video: Encoder,
    }
})()


Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow