How to Enable Image Saving in Mini Programs

Configure Mini Program to Allow Image Saving

Account Settings → Basic Settings → User Privacy Protection Guidelines

Enable Album Write Permission

Code for Saving Images

This article was converted by Jianyue SimpRead; original source: juejin.cn

Generating and Downloading Images from Canvas in UniApp to the Album

Key Points:

  1. When drawing on canvas, numeric values must be converted to strings using + ''; otherwise, they won’t render properly:
    let serviceTime = that.time ? that.time + '' : '10';
  2. When loading multiple images onto the canvas, define a function that creates and returns a Promise for each image load; otherwise, some images may fail to load. Use Promise.all(imagePromises).then(...) to wait for all images to load before drawing, and use wx.nextTick to ensure rendering completes.
  3. To center text horizontally, calculate the text width first, subtract half of it, then draw the text; otherwise, the text will be misaligned.

1. Drawing on Canvas

<template>  
  <view class="container">  
    <!-- Image generated from canvas drawing -->  
    <image style="position: absolute;top: 0;left: 0;"   
            :style="{ width: canvasObj.w + 'rpx', height: canvasObj.h + 'rpx' }"   
            :src="downPic"  
            :show-menu-by-longpress="true">  
    </image>  

    <!-- Canvas -->  
    <canvas   
        type="2d" id="canvas"   
        class="canvas" canvas-id="canvas"  
        :style="{ width: canvasObj.w + 'rpx', height: canvasObj.h + 'rpx' }">  
    </canvas>  

  </view>  
```<button @tap="drawCanvas">Draw image on canvas</button>
</vview>
</template>

<sc```js
export default {
  data() {
    return {
        downPic: "",
        canvasObj: {
            w: 750,
            h: 1333,
        },
    };
  }, 
  methods: {
    async drawCanvas() {
        uni.showLoading({
            title: 'Drawing image on canvas...'
        });
        let that = this;
        await uni.createSelectorQuery()
            .select('#canvas')
            .fields({ node: true, size: true })
            .exec((res) => {
                // Start drawing
                console.log('Retrieved canvas element res', res);
                that.canvas = res[0].node;
                that.canvas.width = that.canvasObj.w;
                that.canvas.height = that.canvasObj.h;

                that.ctx = that.canvas.getContext('2d');
                that.ctx.clearRect(0, 0, that.canvas.width, that.canvas.height);
                console.log('Starting to draw');
                that.canvasDraw().then(res => {
                    console.log('Rendering text', res);
                    // Render text
                    
                    that.$nextTick(() => {
                        that.txt();
                    }); 
                }).then(() => {
                    console.log('Generating image', res);
                    // Generate image from canvas 
                    that.$nextTick(() => {
                        that.canvasImg();
                    });  
                })
            })
    },
    canvasDraw() {
        let that = this;

        // Create an array to store Promises for each image load
        const imagePromises = [];

        // Define a function to create and return a Promise for image loading
        function loadImage(src) {
            return new Promise((resolve, reject) => {
                const img = that.canvas.createImage();
                img.src = src + '?' + new Date().getTime();
                img.onload = () => resolve(img);
                img.onerror = (error) => reject(error);
            });
        }

        // Load all required images
        imagePromises.push(loadImage(that.$util.img('/upload/zyz/annualReport/page-bg7.png')));
        imagePromises.push(loadImage(that.$util.img('/upload/zyz/annualReport/logo7.png')));
        imagePromises.push(loadImage(that.$util.img('/upload/zyz/annualReport/title7.png')));
        imagePromises.push(loadImage(that.$util.img(`/upload/zyz/annualReport/${that.userAttendInfo.userLevel}.png`)));
        imagePromises.push(loadImage(that.$util.img('/upload/zyz/annualReport/code.png'))); 

        // Wait for all images to finish loading
        return Promise.all(imagePromises).then(images => {
            // Draw images in order
            that.ctx.drawImage(images[0], 0, 0, that.canvasObj.w, that.canvasObj.h); // bgImg
            that.ctx.drawImage(images[2], 100, 173, 580, 271); // titleImg
            that.ctx.drawImage(images[1], 33, 69, 336, 66);   // logoImg
            that.ctx.drawImage(images[3], 222, 750, 350, 378); // modelImg
            // that.ctx.drawImage(images[4], 48, 1100, 184, 184);

            let width = 220;
            let height = 220;
            let min = Math.min(width, height);
            let circle = {
                x: Math.floor(min / 2),
                y: Math.floor(min / 2),
                r: Math.floor(min / 2)
            } 
            that.ctx.save();
            that.ctx.beginPath();
            // Begin path to draw circle, apply clipping
            that.ctx.arc(150, 1180, circle.r, 0, Math.PI * 2, false);
            that.ctx.clip();
            that.ctx.drawImage(images[4], 40, 1070, 2 * circle.r, 2 * circle.r) 
            that.ctx.restore(); 

            // Draw white stroke
            that.ctx.save();
            that.ctx.beginPath();
            that.ctx.arc(150, 1180, 114, 0, 2 * Math.PI, false);
            that.ctx.strokeStyle = '#ffffff';
            that.ctx.lineWidth = 10;
            that.ctx.stroke();
            that.ctx.restore();
            that.ctx.draw();

            // Use wx.nextTick to ensure drawing completion
            return new Promise(resolve => {
                if (typeof wx !== 'undefined' && wx.nextTick) {
                    wx.nextTick(() => {
                        resolve(true);
                    });
                } else {
                    // Fallback to requestAnimationFrame if not in WeChat Mini Program environment
                    requestAnimationFrame ? requestAnimationFrame(() => resolve(true)) : setTimeout(() => resolve(true), 0);
                }
            });
        }).catch(error => {
            console.error('Error loading images:', error);
            return false; // Return failure status
        });
    },
    letterspacing(content, x, y) {
        console.log(content);
        
        let that = this;
        let currentX = x;
        for (let i = 0; i < content.length; i++) {
            that.ctx.fillText(content[i], currentX, y);
            currentX += that.ctx.measureText(content[i]).width + 2;
        }
    },
    txt() {
        // Set text font, size, and style
        let that = this;
        let x = 190;
        let y = 480; 

        that.ctx.font = '26px youziFont';
        that.ctx.fillStyle = 'black';
        that.ctx.textAlign = 'center';
        that.ctx.textBaseline = 'top';
        
        // Line 1 text
        that.ctx.fillStyle = '#F27C25'; 
        let userName = `${that.userAttendInfo.userName}:`; 
        const userNameWidth = that.ctx.measureText(userName).width + 20; 
        that.letterspacing(userName, x, y);

        // Line 2 text 
        that.ctx.fillStyle = 'black'; 
        let content1 = "2024,"; 
        // Calculate total width of the entire text
        const totalWidth2 = that.ctx.measureText(content1).width;
        // Calculate center position
        const centerX2 = (that.canvasObj.w - totalWidth2) / 2;
        // Redraw text
        that.letterspacing(content1, centerX2, y + 50);

        // Line 3 text  
        that.ctx.fillStyle = 'black'; 
        let content8 = "You embarked on a volunteer journey in the name of youth."; 
        // Calculate total width of the entire text
        const totalWidth8 = that.ctx.measureText(content8).width;
        // Calculate center position
        const centerX8 = (that.canvasObj.w - totalWidth8) / 2;
        // Redraw text
        that.letterspacing(content8, centerX8, y + 50 * 2);

        // Line 4 text 
        let content2 = "Your most frequent participation was in ";
        let serviceType = that.userAttendInfo.serviceName ? that.userAttendInfo.serviceName : '';
        let content3 = ' volunteer service.';
        // Calculate width of each segment
        const content2Width = that.ctx.measureText(content2).width + 20;
        const serviceTypeWidth = that.ctx.measureText(serviceType).width + 20;  
        // Calculate total width of the entire text
        const totalWidth4 = that.ctx.measureText(content2).width + that.ctx.measureText(serviceType).width + that.ctx.measureText(content3).width;
        // Calculate center position
        const centerX4 = (that.canvasObj.w - totalWidth4) / 2;
        // Redraw text
        that.letterspacing(content2, centerX4, y + 50 * 3);
        that.ctx.fillStyle = '#F27C25'; // Set color to #F27C25
        that.letterspacing(serviceType, centerX4 + content2Width, y + 50 * 3);
        that.ctx.fillStyle = 'black'; // Set color to black
        that.letterspacing(content3, centerX4 + content2Width + serviceTypeWidth, y + 50 * 3); 

        // Line 5 text 
        let content4 = "You have been awarded the "; 
        let serviceTitle = `“${that.userAttendInfo.serviceName} Expert”`;
        let content5 = ' title!';
        const content4Width = that.ctx.measureText(content4).width + 4; 
        const serviceTitleWidth = that.ctx.measureText(serviceTitle).width + 20; 
        const totalWidth5 = that.ctx.measureText(content4).width + that.ctx.measureText(serviceTitle).width + that.ctx.measureText(content5).width;
        // Calculate center position
        const centerX5 = (that.canvasObj.w - totalWidth5) / 2;
        // Redraw text
        that.ctx.fillStyle = 'black';
        that.letterspacing(content4, centerX5, y + 50 * 4);
        that.ctx.fillStyle = '#F27C25'; // Set color to #F27C25
        that.letterspacing(serviceTitle, centerX5 + content4Width, y + 50 * 4);
        that.ctx.fillStyle = 'black';
        that.letterspacing(content5, centerX5 + content4Width + serviceTitleWidth, y + 50 * 4);
    }
  }
}
```// Line 6 text
			that.ctx.fillStyle = 'black';
			let content6 = "Total service duration: ";
			let serviceTime = that.time ? that.time + '' : '10';
			let content7 = ' hours, awarded ';
			// Calculate the width of each content segment
			const content6Width = that.ctx.measureText(content6).width + 4;
			const serviceTimeWidth = that.ctx.measureText(serviceTime).width + 10;
			// Calculate the total width of the entire text
			const totalWidth7 = that.ctx.measureText(content6).width + that.ctx.measureText(serviceTime).width + that.ctx.measureText(content7).width;
			// Calculate the center position
			const centerX7 = (that.canvasObj.w - totalWidth7) / 2;
			// Redraw the text
			that.letterspacing(content6, centerX7, y + 50 * 5);
			that.ctx.fillStyle = '#F27C25'; // Set color to #F27C25
			that.letterspacing(serviceTime, centerX7 + content6Width, y + 50 * 5);
			that.ctx.fillStyle = 'black'; // Set color to black
			that.letterspacing(content7, centerX7 + content6Width + serviceTimeWidth, y + 50 * 5);
		},
		canvasImg() {
			// Generate image from canvas
			let that = this;
			uni.canvasToTempFilePath({
				canvas: that.canvas,
				success: function (res) {
					console.log('Image generation succeeded:')
					console.log(res)
					uni.hideLoading();
					that.downPic = res.tempFilePath
					setTimeout(function () {
					    that.showReport = true
					},1000)
					// that.saveImage()
				},
				fail: function (err) {
					console.log('Image generation failed:')
					console.log(err)
				}
			})
		},
  },
};

</script>

<style>
.container {
  width: 100%;
  height: 100%;
}
.canvas {
	// display: none; causes failure to generate on iOS
	// Position the canvas outside the visible screen area
	left: 9000px;
	width: 1200px;
	height: 1500px;
	position: fixed;
}
</style>

2. After generating the image above, obtain user album permissions and save the image to the device’s photo album

  1. After generating the image above, obtain user album permissions and save the image to the device’s photo album
<template>
  <view class="container">
     <!-- Image generated by canvas drawing -->
    <image  style="position: absolute;top: 0;left: 0;" 
            :style="{ width: canvasObj.w + 'rpx', height: canvasObj.h + 'rpx' }" 
            :src="downPic"
            :show-menu-by-longpress="true">
    </image>

    <!-- canvas -->
    <canvas 
        type="2d" id="canvas" 
        class="canvas" canvas-id="canvas"
        :style="{ width: canvasObj.w + 'rpx', height: canvasObj.h + 'rpx' }">
    </canvas> 
    <button @click="saveImage">Save Image</button>
  </view>
</template>

<script>
export default { 
  methods: {
        saveImage() {
		   var _this = this;
                uni.saveImageToPhotosAlbum({
                    filePath: _this.downPic,
                    success() {
                        uni.showToast({
                            title: "Image saved to album",
                            icon: 'none',
                            duration: 2000
                        })
                    },
                    fail() {
                        uni.hideLoading()
                        uni.showModal({
                            content: 'It appears you have not enabled the permission for accessing photos. Would you like to go to settings to enable it?',
                            confirmText: "Confirm",
                            cancelText: 'Cancel',
                            success: (res) => {
                                if (res.confirm) {
                                    uni.openSetting({
                                        success: (res) => {
                                            console.log(res);
                                            uni.showToast({
                                                title: "Please click 'Save Image' again.",
                                                icon: "none"
                                            });
                                        }
                                    })
                                } else {
                                    uni.showToast({
                                        title: "Save failed. Please enable required permissions and try again.",
                                        icon: "none"
                                    });
                                }
                            }
                        })
                    }
                })
            },
        },
  },
};
</script>

<style>
.canvas-container {
  width: 100%;
  height: 100%;
}
</style>

3. Summary:

In this example, we first create a canvas context, then use the drawCanvas method to draw a rectangle with a white background. Next, we use the saveImage method to convert the canvas content into an image and save it to the photo album. Finally, we add a button to the page that invokes the saveImage method when clicked.
In this example, we first create a canvas context, then use the drawCanvas method to draw a rectangle with a white background. Next, we use the saveImage method to convert the canvas content into an image and save it to the photo album. Finally, we add a button to the page that invokes the saveImage method when clicked.

4. Final result, similar to the image below (some details are omitted for convenience, but the entire project process has already been implemented), including text, background, and colors (these are less important; what matters most is ensuring no images are missed during rendering of multiple images):

  1. Final effect, similar to the image below (some details are omitted for convenience, but the entire project process has already been implemented), including text, background, and colors (these are less important; what matters most is ensuring no images are missed during rendering of multiple images):