Use canvas and Data URLs to download a picture safely

Ordinary users only need one "right click Save as" operation to download pictures, but when we do online editor and the whole UI is customized, how to give users the ability to safely download pictures in the page?

 

0. Use the < a > tag to download resources

The simplest way, of course, is to use the < a > tag. According to the description of "MDN", the < a > tag has an attribute called "download", which indicates that the browser downloads the URL instead of navigating to it, so the user will be prompted to save it as a local file. If we assign another value to this attribute, this value will be used as the pre populated file name during download and saving.

Therefore, we can attach the required resource link to a < a > tag with "download" attribute to realize the download function, for example:

<a 
    href="http://hijiangtao.github.io/README.md"
    download="default"
>
    download README
</a>

However, it should be noted that this attribute is only applicable to homologous URL s. If we insert a cross domain image into the < a > tag, the effect of clicking in chrome will be to open and display the image in a page without downloading. So what should we do when facing cross domain image resources?

As we all know, < img > is not subject to cross domain restrictions when loading image resources, and the canvas canvas can draw any image resources and convert itself into Data URLs. Yes, according to this idea, we will solve the problem step by step.

 

1. Parse DOM to get picture links

First, find the < img > tag from the DOM and extract the picture resource link. If you can directly get the < img > object through the selector, you can directly get the src attribute, for example:

const {src} = document.getElementById("hijiangtao");

If you get a string of {html} strings, you will use the following regular expression to match the < img > tag and extract the src content:

// @Input - rawhtml
const re = /<img\s.*?src=(?:'|")([^'">]+)(?:'|")/gi;
const matchArray = re.exec(rawHTML);
const src = matchArray && matchArray[1]) || '';

The < img > tag has two forms of discussion: < img > and < img / >. This article will not discuss it. For details, you can move to < StackOverflow.

 

2. Split link processing

After getting src, i.e. image link, we will discuss the processing logic in different cases. In this paper, Data URLs specifically refers to base64 image URL, which will not be explained further below:

  1. Images in the same domain or Data URLs are returned directly
  2. Cross domain picture to Data URLs return

Therefore, our code should grow like this. Considering that the img tag needs a callback when downloading resources, we wrap the function result with a Promise:

/**
 * Get the picture address that can be downloaded safely
 * @param src
 */
export const getDownloadSafeImgSrc = (src: string): Promise<string> => {
    return new Promise(resolve => {
        // 0. Invalid src returned directly
        if (!src) {
            resolve(src);
        }

        // 1. Direct return of src in the same domain or base64 form
        if (isValidDataUrl(src) || isSameOrigin(src)) {
            resolve(src);
        }

        // 2. Cross domain picture transferred to base64 return
        getImgToBase64(src, resolve);
    });
};

As for the encoding and decoding of base64 format, this article will not explain too much. Web APIs already has a method to encode and decode base64. For details, please go to Base64 encoding and decoding.

 

3. Improve the function codes of various tools

In the above example, we have added many processing functions. Here we implement them one by one. First, let's see the function implementation to judge whether the image is in base64 format.

base64 format is a kind of data URLs. Data URLs, that is, URLs prefixed with {data: protocol, which allows content creators to embed small files into documents. It consists of four parts: prefix data:, MIME type indicating data type, optional base64 tag if it is not text, and data itself:

data:[<mediatype>][;base64],<data>

The tag part is optional, and the prefix and data are required. We will continue to introduce MIME later. Then, knowing the composition of Data URLs, we can write the regular matching method to judge whether the URL is valid Data URLs as follows:

/**
 * Determine whether the given URL is Data URLs
 * @param s
 */
export const isValidDataUrl = (s: string): boolean => {
    const rg = /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*?)\s*$/i;
    return rg.test(s);
};

As for the cross domain problem, I have described it in more detail in the article "summary of front-end cross domain request solutions". Here we directly use an imperfect but basically available string method to solve the cross domain judgment:

/**
 * Determine whether the given URL is homologous with the current page
 * @param s
 */
export const isSameOrigin = (s: string): boolean => {
    return s.includes(location.origin)
}

Here, let's talk about mime, which will be used when we improve the method of converting canvas to Data URLs. Mime, the full name of which is Multipurpose Internet Mail Extensions. The MIME type we usually call is also called media type. It is a standard used to represent the nature and format of documents, files or byte streams.

For image resources, MIME types widely supported in Web pages include the following:

MIME type Picture type
image/gif GIF picture (replaced by PNG in lossless compression)
image/jpeg JPEG picture
image/png PNG picture
image/svg+xml SVG picture (vector picture)

If we want to extract MIME format from a resource URL without considering webp, icon and other formats, we can do this:

/**
 * Get MIME type according to resource link address
 * 'image/png' is returned by default
 * @param src
 */
export const getImgMIMEType = (src: string): string => {
    const PNG_MIME = 'image/png';

    // File suffix found
    let type = src.replace(/.+\./, '').toLowerCase();

    // Handle special MIME relationships
    type = type.replace(/jpg/i, 'jpeg').replace(/svg/i, 'svg+xml');

    if (!type) {
        return PNG_MIME;
    } else {
        const matchedFix = type.match(/png|jpeg|bmp|gif|svg\+xml/);
        return matchedFix ? `image/${matchedFix[0]}` : PNG_MIME;
    }
};

 

4. Draw pictures with canvas and transfer to Data URLs

We won't talk more about canvas here. First, we create a canvas canvas, and then we load the image resources we want by creating a new Image() object, and call the # CTX of the 2D context of the canvas DrawImage () API to draw pictures, and then we call {canvas Todataurl() converts the resource into Data URLs and returns.

Among them, the MIME mentioned earlier is passed as a parameter to {canvas Todataurl(), the default input parameter is' image/png '.

function convertImgToBase64(url: string, callback: Function, mime?: string) {
    // Canvas canvas
    const canvas: HTMLCanvasElement = document.createElement('CANVAS'),
    ctx = canvas.getContext('2d'),
    img = new Image();
    img.crossOrigin = 'Anonymous';
    img.onload = function() {
        canvas.height = img.height;
        canvas.width = img.width;

        // draw
        ctx!.drawImage(img, 0, 0);

        // Generate Data URLs
        const dataURL = canvas.toDataURL(mime || 'image/png');
        callback.call(this, dataURL);
        canvas = null;
    };
    
    if (/http[s]{0,1}/.test(url)) {
        // Solve cross domain problems
        img.src = url + '?random=' + Date.now();
    } else {
        img.src = url;
    }
}

Finally, we will change the previous call to getImgToBase64(src, resolve) to getImgToBase64(src, resolve, getImgMIMEType(src)), and the module will be completed.

vi designhttp://www.maiqicn.com Complete collection of office resources websiteshttps://www.wode007.com

5. Actual use

Take Angular as an example, our code may grow like this:

// 1. HTML part
<a 
    *ngIf="downloadImageUrl" 
    href="" 
    download="image" 
    class="context-menu-link"
>
    Save picture to local
</a>

// 2. TypeScript
// The implementation of getDownloadSafeImgSrc is introduced
import { getDownloadSafeImgSrc } from './utils.ts';

// ...

// Method notified by a stream update request
function updateDownloadImgState(editors: any[]) {
    // Suppose there are all kinds of selected DOM HTML in editors
    const rawHTML = editors.getSelectionInnerHTML(); 
    const re = /<img\s.*?src=(?:'|")([^'">]+)(?:'|")/gi;
    const matchArray = re.exec(rawHTML);
    this.downloadImageUrl = await getDownloadSafeImgSrc((matchArray && matchArray[1]) || '');
}

So far, no matter whether the picture resources are cross domain or not, we can download them safely by using < a > + canvas , and keep the original format of the picture. This involves many web APIs and concepts, including @ canvas, < a > Download attribute, MIME and popular regular expressions. These are all aspects that can be explored in detail. Welcome to further study.

Tags: Front-end

Posted by slushpuppie on Sat, 14 May 2022 10:44:51 +0300