上传照片类型不对,上传照片说格式不对怎么解决

首页 > 实用技巧 > 作者:YD1662023-11-13 07:46:05

Webp 是一种现代的图像格式,由 Google 开发。它采用了无损和有损的压缩算法,可以显著减小图像文件的大小,同时保持较高的视觉质量。WebP 图像通常比 JPEG 和 PNG 格式的图像更小,并且具有更快的加载速度,这对于 Web 应用程序和网页的性能优化非常有益。此外,WebP 还支持透明度和动画,使其成为一个多功能的图像格式。它已经得到了广泛的支持,包括主流的 Web 浏览器和图像处理软件。

简单理解就是:「WebP编码格式的图片,体积更小,质量不减(肉眼很难看出质量差异),主流浏览器都支持」

据说使用 webp 编码的图片,有利于搜索引擎 SEO。

在 Java 中编码 Webp 图片

WEBP 官方开放了源码,以及编译后的可执行文件(可以通过命令行的形式对图片文件进行编码,解码处理),官方并未提供 Java 的 SDK。

我翻遍了互联网,在网上找到了一个开源的 webp 编码库:https://github.com/sejda-pdf/webp-imageio

<!-- https://mvnrepository.com/artifact/org.sejda.imageio/webp-imageio --> <dependency> <groupId>org.sejda.imageio</groupId> <artifactId>webp-imageio</artifactId> <version>0.1.6</version> </dependency>

它貌似采用了 JNI 技术来调用 webp 的动态库来实现的编码。使用方式及其简单,如下:

import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.io.InputStream; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.FileImageOutputStream; import com.luciad.imageio.webp.WebPWriteParam; public class Webp { /** * 编码为WEBP * * @param in 输入文件 * @param file 输出文件 * @throws IOException */ public void encode(InputStream in, File file) throws IOException { // 读取图片文件 BufferedImage image = ImageIO.read(in); // 获取 WEBP writer ImageWriter writer = ImageIO.getImageWritersByMIMEType("image/webp").next(); WebPWriteParam writeParam = new WebPWriteParam(writer.getLocale()); // 压缩方式,以指定的压缩类型和质量设置进行压缩 writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); // 压缩质量,无损压缩 writeParam.setCompressionType(writeParam.getCompressionTypes()[WebPWriteParam.LOSSLESS_COMPRESSION]); try (FileImageOutputStream outputStream = new FileImageOutputStream(file)) { writer.setOutput(outputStream); writer.write(null, new IIOImage(image, null, null), writeParam); } } }

但是这个库有一个很大的问题就是,「不支持对 GIF 格式的图片进行编码」。如果对 GIF 格式的图片进行编码,只会截取第一帧,动画效果就没了。

所以,我更加推荐直接下载官方所提供的,编译好的可执行文件。通过在应用中启动新的进程,调用可执行程序来对图片资源进行编码。

安装 webp

你可以在 https://developers.google.com/speed/webp/download 下载你操作系统对应的可执行文件。

下载后,解压文件到任意目录。进入解压后目录中的 bin 目录,有如下可执行文件。

anim_diff.exe anim_dump.exe cwebp.exe # WebP 编码器工具 dwebp.exe freeglut.dll get_disto.exe gif2webp.exe # 用于将 GIF 图片转换为 WebP 的工具 img2webp.exe vwebp.exe webp_quality.exe webpinfo.exe webpmux.exe

可执行文件有很多,真正用到的只有2个, cwebp.exe 和 gif2webp.exe。其他的文件,你可以考虑删掉。对于工具详细的使用方法、完整的命令行参数,也你可以从上述页面中找到。篇幅原因,这里只做简单介绍,不详细展开。

cwebp -lossless [源文件] -o [输出文件] # -lossless 参数表示使用无损压缩。

gif2webp [源文件] -o [输出文件] # gif2webp 默认采用无损压缩。

把这个 bin 目录添加到 PATH 环境变量,使其可以在任意命令行中调用。配置好后,执行如下命令验证是否安装成功。

> cwebp -version 1.3.0 libsharpyuv: 0.2.0 > gif2webp -version WebP Encoder version: 1.3.0 WebP Mux version: 1.3.0 在 spring boot 应用中把上传图片编码为 webp

创建任意 spring boot 应用(过程略,非本文重点),在 pom.xml 添加 commons-exec 依赖,如下:

<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-exec</artifactId> <version>1.3</version> </dependency>

由于我们采用了启动外部进程的方式来编码 webp 文件,所以我推荐使用 commons-exec 库。它 Apache Commons 项目的一部分,它提供了一个简单而强大的API,用于执行外部进程并与其进行交互,从而使 Java 应用程序能够方便地调用和控制命令行程序。

FileUploadController

该 Controller 会把用户上传的图片文件编码为 webp 格式,并且返回相对访问路径。

import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.time.LocalDate; import java.util.UUID; import java.util.concurrent.TimeUnit; import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.DefaultExecutor; import org.apache.commons.exec.ExecuteWatchdog; import org.apache.commons.exec.PumpStreamHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import jakarta.servlet.http.HttpServletResponse; @RestController @RequestMapping("/upload") public class FileUploadController { private static final Logger logger = LoggerFactory.getLogger(FileUploadController.class); // 运行程序的目录 public static final String USER_DIR = System.getProperty("user.dir"); // 公共资源访问目录 public static final Path PUBLIC_PATH = Paths.get(USER_DIR, "public"); // /public // 文件上传目录 public static final Path UPLOAD_PATH = PUBLIC_PATH.resolve("files"); // /public/files /** * 文件上传,返回相对路径URI * @param file * @param response * @return * @throws IOException */ @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public String upload (@RequestPart("file") MultipartFile file, HttpServletResponse response) throws IOException { // 上传文件的类型 String contentType = file.getContentType(); // 上传文件的大小 long size = file.getSize(); // 文件名称 String fileName = file.getOriginalFilename(); // 文件后缀 String ext = fileExt(fileName); logger.info("文件上传:contentType={}, size={}, fileName={}", contentType, size, fileName); // 文件必须要有后缀 if (!StringUtils.hasText(ext)) { response.setStatus(HttpStatus.BAD_REQUEST.value()); return null; } // TODO 文件类型的合法性校验 // 按照日期打散目录:/yyyy/MM/dd LocalDate today = LocalDate.now(); Path dir = UPLOAD_PATH.resolve(Path.of(today.getYear() "", String.format("d", today.getMonthValue()), String.format("d", today.getDayOfMonth()) )); // 尝试创建目录 if (!Files.isDirectory(dir)) { Files.createDirectories(dir); } // 使用UUID重命名本地文件 Path localFile = dir.resolve(UUID.randomUUID().toString().replace("-", "") "." ext); logger.info("IO到本地:{}", localFile.toString()); // IO 文件到磁盘 try (InputStream inputStream = file.getInputStream()){ Files.copy(inputStream, localFile, StandardCopyOption.REPLACE_EXISTING); } // 如果上传的是非 webp 类型的图片文件,则尝试编码为 webp if (contentType.toLowerCase().startsWith("image") && !ext.toLowerCase().equals("webp")) { Path webpFile = encode2Webp(localFile); if (webpFile != null) { // 编码成功,删除原文件 Files.delete(localFile); // 响应客户端 webp 文件 localFile = webpFile; } } // 计算文件的相对 public 目录的路径,也就是URI访问路径 String relativizePath = "/" PUBLIC_PATH.relativize(localFile).toString(); // windows 平台下,统一替换为 / if (File.separator.equals("\\")) { // windows relativizePath = relativizePath.replace(File.separator, "/"); } logger.info("文件访问路径:{}", relativizePath); return relativizePath; } // 尝试把文件编码为webp文件 public Path encode2Webp (Path file) { // 创建执行器 DefaultExecutor defaultExecutor = new DefaultExecutor(); defaultExecutor.setWatchdog(new ExecuteWatchdog(TimeUnit.MINUTES.toMillis(10))); // 超时时间,10分钟 defaultExecutor.setStreamHandler(new PumpStreamHandler(System.out, System.err)); // 进程输出到标准输出和标准错误 // 命令行 CommandLine commandLine = null; String ext = fileExt(file.getFileName().toString()).toLowerCase(); // 在同目录下创建 webp 文件 Path webpFile = file.getParent().resolve(UUID.randomUUID().toString().replace("-", "") ".webp"); if (ext.equals("gif")) { // GIF commandLine = new CommandLine("gif2webp"); commandLine.addArgument(file.toString()); // 源文件 commandLine.addArgument("-o"); // 指定输出文件 commandLine.addArgument(webpFile.toString()); // 输出文件 } else { // 其他 commandLine = new CommandLine("cwebp"); commandLine.addArgument("-lossless"); // 无损压缩 commandLine.addArgument(file.toString()); // 源文件 commandLine.addArgument("-o"); // 指定输出文件 commandLine.addArgument(webpFile.toString());// 输出文件 } try { // 同步执行,返回执行结果 defaultExecutor.execute(commandLine); } catch (Throwable e) { logger.warn("WEBP编码异常:{}", e.getMessage()); // webp编码异常,尝试删除文件 try { Files.delete(webpFile); } catch (IOException e1) {} return null; } return webpFile; } // 获取文件的后缀名称,不带 “.” public String fileExt (String filename) { int index = filename.lastIndexOf("."); return index == -1 ? "" : filename.substring(index 1); } }

很简单,200行代码不到,简单解释一下逻辑。

  1. 把程序运行目录下的 public 目录作为静态资源目录,这个目录中的所有资源可以被直接访问(spring boot 特性)。
  2. 在 public 目录中定义上传目录 files,用于存储用户上传的文件。
  3. 把用户上传的图片 IO 到上传目录。
  4. 如果用户上传的是图片文件,且不是 webp 文件,则新启动外部进程调用 webp 可执行文件对图片进行编码(输出到同一个目录)。
  5. 如果编码成功,则删除原文件,仅保留 webp 文件。
  6. 计算出文件相对于 public 的访问路径,响应到客户端。
测试

使用 Postman 把 的 logo 图片(png/19.3 KB)上传到 API,请求日志如下:

POST /upload HTTP/1.1 Origin: http://localhost:8080/ Access-Control-Request-Headers: Foo Access-Control-Request-Method: GET User-Agent: PostmanRuntime/7.29.2 Accept: */* Postman-Token: faa439a4-7f32-45db-9d40-3e8cf68fa9c8 Host: 127.0.0.1:8080 Accept-Encoding: gzip, deflate, br Connection: keep-alive Content-Type: multipart/form-data; boundary=--------------------------234233726664451982101919 Content-Length: 20034 ----------------------------234233726664451982101919 Content-Disposition: form-data; name="file"; filename="springoc.png" <springoc.png> ----------------------------234233726664451982101919-- HTTP/1.1 200 OK Connection: keep-alive Content-Type: text/plain;charset=UTF-8 Content-Length: 55 Date: Wed, 30 Aug 2023 04:16:38 GMT /files/2023/08/30/9a0f950efc6242dfb61d9ef859962df6.webp

上传成功后,查看本地上传目录中的文件。

上传照片类型不对,上传照片说格式不对怎么解决(1)

上传到后台的 webp 文件

编码成 webp 文件后,体积只有 6.57 KB,比原文件小太多了,大大节省了存储空间和加载速度。

尝试用用浏览器访问该文件,一切OK。

上传照片类型不对,上传照片说格式不对怎么解决(2)

用浏览器访问 webp 图片

最后,附上后端服务的日志。

2023-08-30T12:16:38.611 08:00 DEBUG 6828 --- [ XNIO-1 task-2] o.s.web.servlet.DispatcherServlet : POST "/upload", parameters={multipart} 2023-08-30T12:16:38.655 08:00 DEBUG 6828 --- [ XNIO-1 task-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cn.springdoc.demo.controller.FileUploadController#upload(MultipartFile, HttpServletResponse) 2023-08-30T12:16:38.671 08:00 INFO 6828 --- [ XNIO-1 task-2] c.s.d.controller.FileUploadController : 文件上传:contentType=image/png, size=19825, fileName=springoc.png 2023-08-30T12:16:38.672 08:00 INFO 6828 --- [ XNIO-1 task-2] c.s.d.controller.FileUploadController : IO到本地:C:\eclipse\eclipse-jee-2022-09-R-win32-x86_64\project\springdoc-demo\public\files\2023\08\30\6041f1999cb4497584a4560f49aa4641.png Saving file 'C:\eclipse\eclipse-jee-2022-09-R-win32-x86_64\project\springdoc-demo\public\files\2023\08\30\9a0f950efc6242dfb61d9ef859962df6.webp' File: C:\eclipse\eclipse-jee-2022-09-R-win32-x86_64\project\springdoc-demo\public\files\2023\08\30\6041f1999cb4497584a4560f49aa4641.png Dimension: 512 x 512 Output: 6734 bytes (0.21 bpp) Lossless-ARGB compressed size: 6734 bytes * Header size: 292 bytes, image data size: 6417 * Lossless features used: SUBTRACT-GREEN * Precision Bits: histogram=4 transform=4 cache=10 2023-08-30T12:16:38.802 08:00 INFO 6828 --- [ XNIO-1 task-2] c.s.d.controller.FileUploadController : 文件访问路径:/files/2023/08/30/9a0f950efc6242dfb61d9ef859962df6.webp 2023-08-30T12:16:38.815 08:00 DEBUG 6828 --- [ XNIO-1 task-2] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/plain', given [*/*] and supported [text/plain, */*, application/json, application/* json] 2023-08-30T12:16:38.816 08:00 DEBUG 6828 --- [ XNIO-1 task-2] m.m.a.RequestResponseBodyMethodProcessor : Writing ["/files/2023/08/30/9a0f950efc6242dfb61d9ef859962df6.webp"] 2023-08-30T12:16:38.842 08:00 DEBUG 6828 --- [ XNIO-1 task-2] o.s.web.servlet.DispatcherServlet : Completed 200 OK 结语

WEBP 编码是非常值得尝试使用的一个技术,不仅是可以节省存储空间,最重要的是节省带宽,提高加载速度从而提高用户体验。

你也可以考虑使用单独的图片资源服务器,原样存储用户上传的图片资源,然后根据查询参数(/logo.png?format=webp)动态地地把图片资源编码为 webp 响应给客户端(现在大多数云存储服务都提供了这种功能)。这种方式的好处就是不会修改用户上传的资源,同时又可以通过 webp 编码节省带宽。坏处也明显,每次请求都要对图片进行在线编码,会增加响应时间。

本文完,谢谢阅读。


「Spring系列框架的中文文档」:中文互联网上现有的关于spring的文档要么已经多年未更新,要么就是机器直接翻译,质量太次!于是我们花了一些时间,整理翻译出了全网最优质,最新的 spring/spring-boot/spring-data/spring-security/spring-cloud 等框架的官方中文文档。使用 Deepl AI 翻译,人工逐行校验。同官方文档相同布局,无广告,无须登录,无须关注,在线阅读,欢迎访问:「springdoc.cn」

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.