金相世界 - 怎么优雅地处理用户上传的图片

金相世界 - 怎么优雅地处理用户上传的图片

黄鹏宇 427 2022-11-24

一、现有的问题

  1. 现在图库和资料库直接使用腾讯云的图片压缩,这样不安全:
    1. 暴露用户信息
    2. 可以直接查看原图
?imageMogr2/crop/152x152/gravity/center/format/png/interlace/1/quality/17
  1. 对于tif文件的显示问题,采取的是在前端处理,导致每个页面都需要加载tif.min.js很麻烦
  2. 那么如果后端处理,怎么优雅一些?
  3. 对于头像、回复、帖子这类图片,采取前端压缩的方式。

二、流程

  1. 在上传图库或者资料库中,如果含有图片资源,则只在数据库中添加origin_url字段
  2. 向消息队列中传入process_image,width,url,table_name,id
  3. 由消费者(python),去补全thumb_url,以及tif的转化
    image-1669232322365

三、最终效果

  1. 缩略图:
    image-1669291189930
  2. 原图
    image-1669291219024

四、具体代码

  1. 根据url下载图片
import shutil
import time
import requests

# 获取后缀名
def getSuffix(url):
	return url.split(".")[-1]
# 获取整形时间戳
def getTimestamp():
	return str(int(time.time()))
# 获取随机文件名
def getRandomFileName(path):
	fileName = '{}.{}'.format(getTimestamp(),getSuffix(url)) 
	return fileName
# 下载图片
def downloadImage(url):
	response = requests.get(url, stream=True)
	with open(getRandomFileName(url), 'wb') as out_file:
		shutil.copyfileobj(response.raw, out_file)
		del response
  1. 图像压缩处理 image-process-core.py
from PIL import Image

def compressImage(width,path):
    im = Image.open(path)
    (x, y) = im.size  # 读取图片尺寸(像素)
    height = int(y * width / x)  # 计算缩小后的高度
    out = im.resize((width, height), Image.Resampling.LANCZOS)  # 改变尺寸,保持图片高品质
    if out.mode=='RGBA':
        #转化为rgb格式
        out=out.convert('RGB')
    fileSuffix = path.split(".")[-1]
    fileName = '{}.{}'.format(getTimestamp(),fileSuffix) 
    out.save(fileName)
  1. tif转png
def tif2jpg(filePathName):
    if filePathName[-3:] == "tif" or filePathName[-3:] == "bmp" or filePathName[-4:] == "tiff":
       outfile = filePathName[:-3] + "jpeg"
       im = Image.open(filePathName)
       out = im.convert("RGB")
       out.save(outfile, "JPEG", quality=100)
  1. 将转好的文件,上传cdn
# yyyymmdd
def getTimePrefix():
	return time.strftime("%Y%m%d",time.localtime())

# return: transform/{yyyymmdd}/{业务名}/{类型}/{时间戳}.{后缀名}
# imageType: full_size,compress,thumb
# bussinessType: avatar,bbs-file,doc-file,picture-file
def genTencentCOSKey(url,imageType,bussinessType):	
	return "transform/{}/{}/{}/{}.{}".format(getTimePrefix(),bussinessType,imageType,getTimestamp(),getSuffix(url))

def upload2TencentCos(fileName):	
	response = client.upload_file(
	    Bucket='metal-1254798469',
	    Key=genTencentCOSKey(fileName,"thumb","avatar"),
	    LocalFilePath=fileName,
	    EnableMD5=False,
	    progress_callback=None
	)
  1. 更新数据库
import mysql.connector
import json
from mysql.connector.pooling import MySQLConnectionPool

# 主表
DB_BASE_NAME = "metal"
DB_URL = ""
DB_PASSWORD = ""

baseMysqlPool = MySQLConnectionPool(
        host = DB_URL,
        user ="root",        
        port = 59958,
        passwd = DB_PASSWORD,
        database = DB_BASE_NAME,
        charset ='utf8',
        autocommit = True,
        pool_size = 2,
        auth_plugin = 'mysql_native_password'
)


def updatePictureAppendix(id,fullSizeUrl,fullSizeFileSize,thumbUrl,thumbSizeFileSize,compressUrl,compressSizeFileSize):
    conn = baseMysqlPool.get_connection()
    cursor = conn.cursor()
    sql = "UPDATE pic_appendix SET full_size_url='%s',full_size='%s',thumb_url='%s',thumb_size='%s',compress_url='%s',compress_size='%s',has_process=1 WHERE appendix_id= '%s';"%(fullSizeUrl,fullSizeFileSize,thumbUrl,thumbSizeFileSize,compressUrl,compressSizeFileSize,id)
    print(sql)
    cursor.execute(sql) 
    conn.commit()
    cursor.close()
    conn.close()
  1. 消息队列消费者
import pika
import json
from core import transformImage
HOST = ""
EXCHANGE_NAME = "PIC_PROCESS_DIRECT_EXCHANGE"
QUEUE_NAME = "py_81"

credentials = pika.PlainCredentials('', '') 

connection = pika.BlockingConnection(pika.ConnectionParameters(host=HOST,port=5672,virtual_host='/metal',credentials=credentials))
channel = connection.channel()

#
#  Integer width;
#  String tableName;
#  Integer appendixID;
#  String url;
#
def callback(ch, method, properties, body):
    # ⼿动发送确认消息
    ch.basic_ack(delivery_tag=method.delivery_tag)
    print(body)
    msg = body.decode()
    dto = json.loads(msg)
    transformImage(dto['appendixID'],dto['width'],dto['tableName'],dto['url'])

channel.exchange_declare(exchange=EXCHANGE_NAME, exchange_type='direct',durable=True)
result = channel.queue_declare(queue=QUEUE_NAME, exclusive=False)
channel.queue_bind(exchange=EXCHANGE_NAME, queue=QUEUE_NAME,routing_key="")

#channel.basicQos(10); 
channel.basic_qos(prefetch_count=1)
channel.basic_consume(queue=QUEUE_NAME,on_message_callback=callback,auto_ack=False)

channel.start_consuming()
  1. 完整代码
# 做两件事
# 1. 图片压缩,生成缩略图

# 152x152

'''
实现图片压缩
1.保持图片大小比例不变
2.使用Image里面的resize进行

'''

# -*- coding=utf-8
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
from qcloud_cos.cos_exception import CosClientError, CosServiceError
import sys
import logging
import shutil
import time
from PIL import Image
import os
import requests
from db import updatePictureAppendix
# 正常情况日志级别使用INFO,需要定位时可以修改为DEBUG,此时SDK会打印和服务端的通信信息
logging.basicConfig(level=logging.INFO, stream=sys.stdout)

secret_id = 'SECRETID'
secret_key = 'SECRETKEY'   
region = 'ap-beijing'								
token = None			   
scheme = 'https'		   
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Token=token, Scheme=scheme)
client = CosS3Client(config)


def getSuffix(url):
	return url.split(".")[-1]

def getTimestamp():
	return str(time.time())

def getRandomFileName(url):
	fileName = '{}.{}'.format(getTimestamp(),getSuffix(url)) 
	return fileName

def downloadImage(url):
	response = requests.get(url, stream=True)
	fileName = getRandomFileName(url)
	with open(fileName, 'wb') as out_file:
		shutil.copyfileobj(response.raw, out_file)
		del response
		return fileName

def compressImage(width,path):
	im = Image.open(path)
	(x, y) = im.size  # 读取图片尺寸(像素)
	height = int(y * width / x)  # 计算缩小后的高度
	out = im.resize((width, height), Image.Resampling.LANCZOS)  # 改变尺寸,保持图片高品质
	if out.mode=='RGBA':
		#转化为rgb格式
		out=out.convert('RGB')
	fileSuffix = path.split(".")[-1]
	fileName = '{}.{}'.format(getTimestamp(),fileSuffix) 
	out.save(fileName)
	return fileName


# yyyymmdd
def getTimePrefix():
	return time.strftime("%Y%m%d",time.localtime())

# return: transform/{yyyymmdd}/{业务名}/{类型}/{时间戳}.{后缀名}
# imageType: full_size,compress,thumb
# bussinessType: avatar,bbs-file,doc-file,picture-file
def genTencentCOSKey(url,imageType,bussinessType):	
	return "transform/{}/{}/{}/{}.{}".format(getTimePrefix(),bussinessType,imageType,getTimestamp(),getSuffix(url))

def upload2TencentCosAndDeleteLocalFile(fileName,imageType,bussinessType):
	key = genTencentCOSKey(fileName,imageType,bussinessType)
	response = client.upload_file(
		Bucket='metal-1254798469',
		Key=key,
		LocalFilePath=fileName,
		EnableMD5=False,
		progress_callback=None
	)

	# 删除本地文件
	os.remove(fileName)
	return "https://cdn.jinxiangshijie.com/"+key

def updateDB(id,tableName,fullSizeUrl,fullSizeFileSize,thumbUrl,thumbSizeFileSize,compressUrl,compressSizeFileSize):
	pass

def isImageFile(url):
	return True

def tif2jpg(filePathName):
	if filePathName[-3:] == "tif" or filePathName[-3:] == "bmp" or filePathName[-4:] == "tiff":
		outfile = filePathName[:-3] + "jpeg"
		im = Image.open(filePathName)
		out = im.convert("RGB")
		out.save(outfile, "JPEG", quality=100)
		return outfile
	else:
		return filePathName

def getFileSize(filePath):
	return os.path.getsize(filePath)

# 根据fullSizeUrl,进行下载、转换、压缩操作
def transformImage(id,width,tableName,fullSizeUrl):
	if(tableName!='pic_appendix' and tableName!='document_appendix'):
		return
	if(tableName=='pic_appendix'):
		bussinessType = 'picture-file'
	else:
		bussinessType = 'doc-file'

	# 不做参数校验了
	# todo: 根据后缀名,先判断是不是图片
	if(not isImageFile(fullSizeUrl)):
		return
	localFileName = downloadImage(fullSizeUrl)

	# 判断是不是tif,如果是,则需要转化
	localFileName = tif2jpg(localFileName)
	
	# 生成预览图
	thumbImageName = compressImage(width,localFileName)

	# 生成压缩图
	compressImageName = compressImage(300,localFileName)
	# 获取大小
	fullSizeFileSize =  getFileSize(localFileName)
	thumbSizeFileSize =  getFileSize(thumbImageName)
	compressSizeFileSize = getFileSize(compressImageName)
	# 上传三种图片,并删除本地文件
	fullSizeCdnUrl = upload2TencentCosAndDeleteLocalFile(localFileName,"full_size",bussinessType)
	thumbSizeCdnUrl = upload2TencentCosAndDeleteLocalFile(thumbImageName,"thumb",bussinessType)
	compressSizeCdnUrl = upload2TencentCosAndDeleteLocalFile(compressImageName,"compress",bussinessType)

	updatePictureAppendix(id,fullSizeCdnUrl,fullSizeFileSize,thumbSizeCdnUrl,thumbSizeFileSize,compressSizeCdnUrl,compressSizeFileSize)
	# todo: 修改数据库
	print(id,width,tableName,fullSizeCdnUrl)

	# todo: 删除本地文件