设备图片

This commit is contained in:
caozehui
2026-07-23 08:39:02 +08:00
parent 23c0855794
commit dde876eb26
12 changed files with 636 additions and 162 deletions

View File

@@ -100,11 +100,26 @@ public class PqDevController extends BaseController {
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DOWNLOAD)
@GetMapping("/image/download")
@GetMapping("/image/preview")
@ApiOperation("下载设备图片")
@ApiImplicitParam(name = "id", value = "被检设备id", required = true)
public void downloadImage(@RequestParam("id") String id, HttpServletResponse response) {
pqDevService.downloadImage(id, response);
public void previewImage(@RequestParam("id") String id,
@RequestParam("fileName") String fileName,
HttpServletResponse response) {
pqDevService.previewImage(id, fileName, response);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DOWNLOAD)
@GetMapping("/image/download")
@ApiOperation("download device image")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "id", required = true),
@ApiImplicitParam(name = "fileName", value = "fileName", required = true)
})
public void downloadImage(@RequestParam("id") String id,
@RequestParam("fileName") String fileName,
HttpServletResponse response) {
pqDevService.downloadImage(id, fileName, response);
}
@OperateInfo(operateType = OperateType.DELETE)

View File

@@ -154,7 +154,7 @@
</select>
<select id="selectByDevId" resultType="com.njcn.gather.device.pojo.vo.PqDevVO">
SELECT dev.*, dev_sub.*
SELECT <include refid="DevColumnsNoImage"/>, dev_sub.*
FROM pq_dev dev
left JOIN pq_dev_sub dev_sub ON dev.Id = dev_sub.Dev_Id
WHERE dev.Id = #{devId}

View File

@@ -138,7 +138,7 @@ public class PqDevParam {
private Integer importFlag;
@ApiModelProperty("设备图片")
private MultipartFile image;
private List<MultipartFile> images;
/**
* 更新操作实体
@@ -151,6 +151,9 @@ public class PqDevParam {
@NotBlank(message = DetectionValidMessage.ID_NOT_BLANK)
@Pattern(regexp = PatternRegex.SYSTEM_ID, message = DetectionValidMessage.ID_FORMAT_ERROR)
private String id;
@ApiModelProperty("deleteImageNames")
private List<String> deleteImageNames;
}
/**

View File

@@ -3,7 +3,6 @@ package com.njcn.gather.device.pojo.po;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@@ -185,9 +184,6 @@ public class PqDev extends BaseEntity implements Serializable {
*/
private Integer importFlag;
@JsonIgnore
private byte[] image;
/**
* 状态0-删除 1-正常
*/

View File

@@ -0,0 +1,17 @@
package com.njcn.gather.device.pojo.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PqDevImageVO {
private String fileName;
private String previewUrl;
private String downloadUrl;
}

View File

@@ -150,7 +150,5 @@ public class PqDevVO extends PqDev {
private Boolean hasImage;
private String imageBase64;
private String imageContentType;
private List<PqDevImageVO> images;
}

View File

@@ -102,7 +102,9 @@ public interface IPqDevService extends IService<PqDev> {
* @param id 设备id
* @param response 响应
*/
void downloadImage(String id, HttpServletResponse response);
void previewImage(String id, String fileName, HttpServletResponse response);
void downloadImage(String id, String fileName, HttpServletResponse response);
/**
* 获取装置信息和装置下监测点信息

View File

@@ -0,0 +1,288 @@
package com.njcn.gather.device.service;
import com.njcn.gather.device.pojo.vo.PqDevImageVO;
import com.njcn.gather.device.util.PqDevImageSupport;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@Service
public class PqDevImageFileService {
public static final int MAX_IMAGE_COUNT = 5;
private static final Pattern UUID_JPG_FILE_NAME = Pattern.compile(
"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\.jpg$"
);
private static final Pattern DEVICE_ID_PATTERN = Pattern.compile("^[A-Za-z0-9_-]{1,64}$");
private static final String INVALID_IMAGE_NAME = "图片文件名无效";
private static final String INVALID_IMAGE_PATH = "图片路径无效";
private static final String INVALID_DEVICE_ID = "设备ID无效";
private static final String IMAGE_NOT_EXISTS = "设备图片不存在";
private static final String IMAGE_COUNT_EXCEEDED = "设备图片最多上传5张";
private static final String READ_IMAGE_FAILED = "读取设备图片失败";
private static final String PREVIEW_URL = "/pqDev/image/preview?id=%s&fileName=%s";
private static final String DOWNLOAD_URL = "/pqDev/image/download?id=%s&fileName=%s";
private final String imageDir;
public PqDevImageFileService(@Value("${device.image.dir:}") String imageDir) {
this.imageDir = imageDir;
}
public List<PqDevImageVO> listImages(String devId) {
Path deviceDir = deviceDir(devId);
if (!Files.isDirectory(deviceDir)) {
return Collections.emptyList();
}
List<Path> imagePaths = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(deviceDir)) {
for (Path path : stream) {
if (Files.isRegularFile(path) && isValidImageFileName(path.getFileName().toString())) {
imagePaths.add(path);
}
}
} catch (IOException ex) {
throw new IllegalStateException(READ_IMAGE_FAILED, ex);
}
Collections.sort(imagePaths, new Comparator<Path>() {
@Override
public int compare(Path left, Path right) {
int modifiedCompare = Long.compare(lastModified(left), lastModified(right));
if (modifiedCompare != 0) {
return modifiedCompare;
}
return left.getFileName().toString().compareTo(right.getFileName().toString());
}
});
List<PqDevImageVO> images = new ArrayList<>();
for (Path imagePath : imagePaths) {
images.add(buildImageVO(devId, imagePath.getFileName().toString()));
}
return images;
}
public void validateAddCount(List<MultipartFile> newImages) {
if (effectiveNewImageCount(newImages) > MAX_IMAGE_COUNT) {
throw new IllegalArgumentException(IMAGE_COUNT_EXCEEDED);
}
}
public void validateFinalCount(String devId, List<String> deleteImageNames, List<MultipartFile> newImages) {
validateAddCount(newImages);
Set<String> existingNames = currentImageNameSet(devId);
validateExistingImageNames(devId, deleteImageNames);
int finalCount = existingNames.size() - uniqueCount(deleteImageNames) + effectiveNewImageCount(newImages);
if (finalCount > MAX_IMAGE_COUNT) {
throw new IllegalArgumentException(IMAGE_COUNT_EXCEEDED);
}
}
public List<PqDevImageVO> saveImages(String devId, List<MultipartFile> newImages) throws IOException {
validateAddCount(newImages);
if (newImages == null || newImages.isEmpty()) {
return Collections.emptyList();
}
Files.createDirectories(deviceDir(devId));
List<PqDevImageVO> savedImages = new ArrayList<>();
for (MultipartFile image : newImages) {
if (image == null || image.isEmpty()) {
continue;
}
String fileName = UUID.randomUUID().toString().toLowerCase(Locale.ROOT) + ".jpg";
Path path = imagePath(devId, fileName);
writeImage(path, PqDevImageSupport.processUploadToJpeg(image));
savedImages.add(buildImageVO(devId, fileName));
}
return savedImages;
}
public void deleteImages(String devId, List<String> deleteImageNames) throws IOException {
validateExistingImageNames(devId, deleteImageNames);
if (deleteImageNames == null || deleteImageNames.isEmpty()) {
return;
}
Set<String> deletedNames = new HashSet<>();
for (String fileName : deleteImageNames) {
if (deletedNames.add(fileName)) {
Files.delete(imagePath(devId, fileName));
}
}
}
public void validateImageNames(List<String> imageNames) {
if (imageNames == null) {
return;
}
for (String imageName : imageNames) {
if (!isValidImageFileName(imageName)) {
throw new IllegalArgumentException(INVALID_IMAGE_NAME);
}
}
}
public void validateExistingImageNames(String devId, List<String> imageNames) {
validateImageNames(imageNames);
if (imageNames == null || imageNames.isEmpty()) {
return;
}
Set<String> existingNames = currentImageNameSet(devId);
for (String imageName : imageNames) {
if (!existingNames.contains(imageName)) {
throw new IllegalArgumentException(IMAGE_NOT_EXISTS);
}
}
}
public Path imagePath(String devId, String fileName) {
if (!isValidImageFileName(fileName)) {
throw new IllegalArgumentException(INVALID_IMAGE_NAME);
}
Path deviceDir = deviceDir(devId);
Path path = deviceDir.resolve(fileName).normalize();
if (!path.startsWith(deviceDir)) {
throw new IllegalArgumentException(INVALID_IMAGE_PATH);
}
return path;
}
public long imageSize(String devId, String fileName) throws IOException {
Path path = imagePath(devId, fileName);
if (!Files.isRegularFile(path)) {
throw new IllegalArgumentException(IMAGE_NOT_EXISTS);
}
return Files.size(path);
}
public void writeImage(String devId, String fileName, OutputStream outputStream) throws IOException {
Path path = imagePath(devId, fileName);
if (!Files.isRegularFile(path)) {
throw new IllegalArgumentException(IMAGE_NOT_EXISTS);
}
Files.copy(path, outputStream);
}
public void deleteDeviceDirectoryQuietly(String devId) {
Path deviceDir = deviceDir(devId);
if (!Files.exists(deviceDir)) {
return;
}
try (Stream<Path> stream = Files.walk(deviceDir)) {
List<Path> paths = new ArrayList<>();
Iterator<Path> iterator = stream.iterator();
while (iterator.hasNext()) {
paths.add(iterator.next());
}
Collections.sort(paths, new Comparator<Path>() {
@Override
public int compare(Path left, Path right) {
return right.compareTo(left);
}
});
for (Path path : paths) {
Files.deleteIfExists(path);
}
} catch (IOException ignored) {
}
}
private void writeImage(Path imagePath, byte[] imageBytes) throws IOException {
Files.createDirectories(imagePath.getParent());
Files.write(imagePath, imageBytes, StandardOpenOption.CREATE_NEW);
}
private Path rootDir() {
Path root;
if (StringUtils.hasText(imageDir)) {
root = Paths.get(imageDir);
} else {
root = Paths.get(System.getProperty("user.dir"), "data", "device-images");
}
return root.toAbsolutePath().normalize();
}
private Path deviceDir(String devId) {
if (!StringUtils.hasText(devId) || !DEVICE_ID_PATTERN.matcher(devId).matches()) {
throw new IllegalArgumentException(INVALID_DEVICE_ID);
}
Path rootDir = rootDir();
Path deviceDir = rootDir.resolve(devId).normalize();
if (!deviceDir.startsWith(rootDir)) {
throw new IllegalArgumentException(INVALID_IMAGE_PATH);
}
return deviceDir;
}
private PqDevImageVO buildImageVO(String devId, String fileName) {
return new PqDevImageVO(
fileName,
String.format(PREVIEW_URL, devId, fileName),
String.format(DOWNLOAD_URL, devId, fileName)
);
}
private Set<String> currentImageNameSet(String devId) {
Set<String> names = new HashSet<>();
for (PqDevImageVO image : listImages(devId)) {
names.add(image.getFileName());
}
return names;
}
private int effectiveNewImageCount(List<MultipartFile> newImages) {
if (newImages == null || newImages.isEmpty()) {
return 0;
}
int count = 0;
for (MultipartFile image : newImages) {
if (image != null && !image.isEmpty()) {
count++;
}
}
return count;
}
private int uniqueCount(List<String> names) {
if (names == null || names.isEmpty()) {
return 0;
}
return new HashSet<>(names).size();
}
private boolean isValidImageFileName(String fileName) {
return fileName != null && UUID_JPG_FILE_NAME.matcher(fileName).matches();
}
private long lastModified(Path path) {
try {
return Files.getLastModifiedTime(path).toMillis();
} catch (IOException ex) {
return 0L;
}
}
}

View File

@@ -38,7 +38,7 @@ import com.njcn.gather.device.pojo.po.PqDevSub;
import com.njcn.gather.device.pojo.vo.*;
import com.njcn.gather.device.service.IPqDevService;
import com.njcn.gather.device.service.IPqDevSubService;
import com.njcn.gather.device.util.PqDevImageSupport;
import com.njcn.gather.device.service.PqDevImageFileService;
import com.njcn.gather.monitor.pojo.po.PqMonitor;
import com.njcn.gather.monitor.pojo.vo.PqMonitorExcel;
import com.njcn.gather.monitor.service.IPqMonitorService;
@@ -100,6 +100,7 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
private final AdPlanMapper adPlanMapper;
private final IPqDevCheckHistoryService pqDevCheckHistoryService;
private final RestTemplateUtil restTemplateUtil;
private final PqDevImageFileService pqDevImageFileService;
@Override
public Page<PqDevVO> listPqDevs(PqDevParam.QueryParam queryParam) {
@@ -139,10 +140,10 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
pqDevParam.setName(pqDevParam.getName().trim());
pqDevParam.setCreateId(pqDevParam.getCreateId().trim());
this.checkRepeat(pqDevParam, false);
validateAddImages(pqDevParam.getImages());
PqDev pqDev = new PqDev();
BeanUtil.copyProperties(pqDevParam, pqDev, CopyOptions.create().setIgnoreProperties("image"));
fillImage(pqDev, pqDevParam);
BeanUtil.copyProperties(pqDevParam, pqDev, CopyOptions.create().setIgnoreProperties("images"));
String currrentScene = sysTestConfigService.getCurrrentScene();
this.checkParams(pqDev, currrentScene);
@@ -170,19 +171,49 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
pqDev.setImportFlag(0);
pqDev.setState(DataStateEnum.ENABLE.getCode());
pqDevSubService.save(pqDevSub);
return this.save(pqDev);
boolean saved = this.save(pqDev);
if (saved) {
saveAddImages(id, pqDevParam.getImages());
}
return saved;
}
private void fillImage(PqDev pqDev, PqDevParam pqDevParam) {
private void validateAddImages(List<MultipartFile> images) {
try {
byte[] image = PqDevImageSupport.processUpload(pqDevParam.getImage());
if (image != null) {
pqDev.setImage(image);
}
pqDevImageFileService.validateAddCount(images);
} catch (IllegalArgumentException e) {
throw new BusinessException(CommonResponseEnum.FAIL, e.getMessage());
}
}
private void saveAddImages(String id, List<MultipartFile> images) {
try {
pqDevImageFileService.saveImages(id, images);
} catch (IllegalArgumentException e) {
pqDevImageFileService.deleteDeviceDirectoryQuietly(id);
throw new BusinessException(CommonResponseEnum.FAIL, e.getMessage());
} catch (IOException e) {
pqDevImageFileService.deleteDeviceDirectoryQuietly(id);
throw new BusinessException(CommonResponseEnum.FAIL, "device image save failed");
}
}
private void validateUpdateImages(PqDevParam.UpdateParam updateParam) {
try {
pqDevImageFileService.validateFinalCount(updateParam.getId(), updateParam.getDeleteImageNames(), updateParam.getImages());
} catch (IllegalArgumentException e) {
throw new BusinessException(CommonResponseEnum.FAIL, e.getMessage());
}
}
private void saveUpdateImages(PqDevParam.UpdateParam updateParam) {
try {
pqDevImageFileService.saveImages(updateParam.getId(), updateParam.getImages());
pqDevImageFileService.deleteImages(updateParam.getId(), updateParam.getDeleteImageNames());
} catch (IllegalArgumentException e) {
throw new BusinessException(CommonResponseEnum.FAIL, e.getMessage());
} catch (IOException e) {
throw new BusinessException(CommonResponseEnum.FAIL, "图片处理失败");
throw new BusinessException(CommonResponseEnum.FAIL, "device image save failed");
}
}
@@ -226,10 +257,10 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
updateParam.setName(updateParam.getName().trim());
updateParam.setCreateId(updateParam.getCreateId().trim());
this.checkRepeat(updateParam, true);
validateUpdateImages(updateParam);
PqDev pqDev = new PqDev();
BeanUtil.copyProperties(updateParam, pqDev, CopyOptions.create().setIgnoreProperties("image"));
fillImage(pqDev, updateParam);
BeanUtil.copyProperties(updateParam, pqDev, CopyOptions.create().setIgnoreProperties("images", "deleteImageNames"));
String currrentScene = sysTestConfigService.getCurrrentScene();
this.checkParams(pqDev, currrentScene);
@@ -237,7 +268,11 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
if (PatternEnum.CONTRAST.getValue().equals(dictDataService.getDictDataById(updateParam.getPattern()).getCode())) {
pqMonitorService.updatePqMonitorByDevId(updateParam.getId(), updateParam.getMonitorList());
}
return this.updateById(pqDev);
boolean updated = this.updateById(pqDev);
if (updated) {
saveUpdateImages(updateParam);
}
return updated;
}
@Override
@@ -282,7 +317,13 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
throw new BusinessException(DetectionResponseEnum.DEVICE_DELETE, str.toString());
}
pqDevSubService.remove(new QueryWrapper<PqDevSub>().lambda().in(PqDevSub::getDevId, param.getIds()));
return this.lambdaUpdate().set(PqDev::getState, DataStateEnum.DELETED.getCode()).in(PqDev::getId, param.getIds()).update();
boolean deleted = this.lambdaUpdate().set(PqDev::getState, DataStateEnum.DELETED.getCode()).in(PqDev::getId, param.getIds()).update();
if (deleted) {
for (String id : param.getIds()) {
pqDevImageFileService.deleteDeviceDirectoryQuietly(id);
}
}
return deleted;
}
return true;
}
@@ -399,7 +440,7 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
if (ObjectUtil.isNull(pqDevVO)) {
return null;
}
setImageEchoFields(pqDevVO);
setImageListFields(pqDevVO);
if (StrUtil.isNotBlank(pqDevVO.getSeries())) {
pqDevVO.setSeries(EncryptionUtil.decoderString(1, pqDevVO.getSeries()));
}
@@ -429,36 +470,38 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
}
@Override
public void downloadImage(String id, HttpServletResponse response) {
PqDev pqDev = this.getById(id);
if (ObjectUtil.isNull(pqDev) || pqDev.getImage() == null || pqDev.getImage().length == 0) {
throw new BusinessException(CommonResponseEnum.FAIL, "设备图片不存在");
}
String contentType = PqDevImageSupport.detectContentType(pqDev.getImage());
String filename = "device-image-" + id + "." + PqDevImageSupport.getExtension(contentType);
response.setContentType(contentType);
response.setContentLength(pqDev.getImage().length);
response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
try {
response.getOutputStream().write(pqDev.getImage());
response.flushBuffer();
} catch (IOException e) {
throw new BusinessException(CommonResponseEnum.FAIL, "设备图片下载失败");
}
public void previewImage(String id, String fileName, HttpServletResponse response) {
writeDeviceImage(id, fileName, response, "inline");
}
private void setImageEchoFields(PqDevVO pqDevVO) {
@Override
public void downloadImage(String id, String fileName, HttpServletResponse response) {
writeDeviceImage(id, fileName, response, "attachment");
}
private void writeDeviceImage(String id, String fileName, HttpServletResponse response, String disposition) {
PqDev pqDev = this.getById(id);
if (ObjectUtil.isNull(pqDev)) {
throw new BusinessException(CommonResponseEnum.FAIL, "device not found");
}
try {
PqDevImageSupport.ImageEchoPayload payload = PqDevImageSupport.buildEchoPayload(pqDevVO.getImage());
pqDevVO.setHasImage(payload.isHasImage());
pqDevVO.setImageBase64(payload.getImageBase64());
pqDevVO.setImageContentType(payload.getImageContentType());
pqDevVO.setImage(null);
response.setContentType("image/jpeg");
response.setContentLength((int) pqDevImageFileService.imageSize(id, fileName));
response.setHeader("Content-Disposition", disposition + "; filename=\"" + fileName + "\"");
pqDevImageFileService.writeImage(id, fileName, response.getOutputStream());
response.flushBuffer();
} catch (IllegalArgumentException e) {
throw new BusinessException(CommonResponseEnum.FAIL, e.getMessage());
} catch (IOException e) {
throw new BusinessException(CommonResponseEnum.FAIL, "device image output failed");
}
}
private void setImageListFields(PqDevVO pqDevVO) {
List<PqDevImageVO> images = pqDevImageFileService.listImages(pqDevVO.getId());
pqDevVO.setImages(images);
pqDevVO.setHasImage(CollUtil.isNotEmpty(images));
}
/**
* 获取查询条件wrapper

View File

@@ -1,7 +1,5 @@
package com.njcn.gather.device.util;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.IIOImage;
@@ -9,35 +7,53 @@ import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import java.util.Iterator;
import java.util.Locale;
public final class PqDevImageSupport {
private static final long MAX_FILE_SIZE = 10L * 1024L * 1024L;
private static final int MAX_EDGE = 1600;
public static final long MAX_FILE_SIZE = 10L * 1024L * 1024L;
public static final int MAX_EDGE = 1600;
private static final String INVALID_IMAGE_CONTENT = "图片内容无效";
private static final String UNSUPPORTED_IMAGE_TYPE = "仅支持 JPG、JPEG、PNG 图片";
private static final String IMAGE_TOO_LARGE = "图片大小不能超过 10MB";
private static final float JPEG_QUALITY = 0.82f;
private PqDevImageSupport() {
}
public static ImageEchoPayload buildEchoPayload(byte[] imageBytes) {
if (imageBytes == null || imageBytes.length == 0) {
return new ImageEchoPayload(false, null, null);
public static byte[] processUploadToJpeg(MultipartFile image) throws IOException {
if (image == null || image.isEmpty()) {
throw new IllegalArgumentException(INVALID_IMAGE_CONTENT);
}
return new ImageEchoPayload(true, Base64.getEncoder().encodeToString(imageBytes), detectContentType(imageBytes));
if (image.getSize() > MAX_FILE_SIZE) {
throw new IllegalArgumentException(IMAGE_TOO_LARGE);
}
if (!hasAllowedDeclaredType(image)) {
throw new IllegalArgumentException(UNSUPPORTED_IMAGE_TYPE);
}
byte[] originalBytes = image.getBytes();
detectContentType(originalBytes);
BufferedImage source = ImageIO.read(new ByteArrayInputStream(originalBytes));
if (source == null) {
throw new IllegalArgumentException(INVALID_IMAGE_CONTENT);
}
return writeJpeg(scaleIfNeeded(source));
}
public static String detectContentType(byte[] imageBytes) {
if (imageBytes == null || imageBytes.length < 4) {
throw new IllegalArgumentException("图片内容无效");
throw new IllegalArgumentException(INVALID_IMAGE_CONTENT);
}
if (isPng(imageBytes)) {
return "image/png";
@@ -45,47 +61,31 @@ public final class PqDevImageSupport {
if (isJpeg(imageBytes)) {
return "image/jpeg";
}
throw new IllegalArgumentException("仅支持 JPG、PNG 图片");
throw new IllegalArgumentException(UNSUPPORTED_IMAGE_TYPE);
}
public static byte[] processUpload(MultipartFile image) throws IOException {
if (image == null || image.isEmpty()) {
return null;
}
if (image.getSize() > MAX_FILE_SIZE) {
throw new IllegalArgumentException("图片大小不能超过 10MB");
}
private static boolean hasAllowedDeclaredType(MultipartFile image) {
String normalizedName = normalize(image.getOriginalFilename());
String normalizedContentType = normalize(image.getContentType());
boolean allowedType = normalizedContentType.equals("image/jpeg")
|| normalizedContentType.equals("image/png")
|| normalizedName.endsWith(".jpg")
boolean hasContentType = !normalizedContentType.isEmpty();
boolean hasKnownExtension = normalizedName.endsWith(".jpg")
|| normalizedName.endsWith(".jpeg")
|| normalizedName.endsWith(".png")
|| normalizedName.matches(".*\\.[^.]+$");
boolean contentTypeAllowed = normalizedContentType.equals("image/jpeg")
|| normalizedContentType.equals("image/jpg")
|| normalizedContentType.equals("image/png");
boolean extensionAllowed = normalizedName.endsWith(".jpg")
|| normalizedName.endsWith(".jpeg")
|| normalizedName.endsWith(".png");
if (!allowedType) {
throw new IllegalArgumentException("仅支持 JPG、PNG 图片");
}
byte[] originalBytes = image.getBytes();
String contentType = detectContentType(originalBytes);
BufferedImage source;
try (InputStream inputStream = image.getInputStream()) {
source = ImageIO.read(inputStream);
if (hasContentType && !contentTypeAllowed) {
return false;
}
if (source == null) {
throw new IllegalArgumentException("图片内容无效");
if (hasKnownExtension && !extensionAllowed) {
return false;
}
BufferedImage target = scaleIfNeeded(source);
if ("image/png".equals(contentType)) {
return writePng(target);
}
return writeJpeg(target);
}
public static String getExtension(String contentType) {
return "image/png".equals(contentType) ? "png" : "jpg";
return contentTypeAllowed || extensionAllowed;
}
private static BufferedImage scaleIfNeeded(BufferedImage source) {
@@ -99,8 +99,7 @@ public final class PqDevImageSupport {
double scale = (double) MAX_EDGE / max;
int targetWidth = Math.max(1, (int) Math.round(width * scale));
int targetHeight = Math.max(1, (int) Math.round(height * scale));
int type = source.getType() == BufferedImage.TYPE_CUSTOM ? BufferedImage.TYPE_INT_ARGB : source.getType();
BufferedImage target = new BufferedImage(targetWidth, targetHeight, type);
BufferedImage target = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = target.createGraphics();
try {
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
@@ -113,16 +112,12 @@ public final class PqDevImageSupport {
return target;
}
private static byte[] writePng(BufferedImage image) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "png", outputStream);
return outputStream.toByteArray();
}
private static byte[] writeJpeg(BufferedImage image) throws IOException {
BufferedImage rgbImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = rgbImage.createGraphics();
try {
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
graphics.drawImage(image, 0, 0, null);
} finally {
graphics.dispose();
@@ -130,7 +125,7 @@ public final class PqDevImageSupport {
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
if (!writers.hasNext()) {
throw new IOException("未找到 JPG 图片编码器");
throw new IOException("JPG image writer not found");
}
ImageWriter writer = writers.next();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
@@ -170,12 +165,4 @@ public final class PqDevImageSupport {
private static String normalize(String value) {
return value == null ? "" : value.toLowerCase(Locale.ROOT);
}
@Data
@AllArgsConstructor
public static class ImageEchoPayload {
private boolean hasImage;
private String imageBase64;
private String imageContentType;
}
}

View File

@@ -1,55 +1,203 @@
package com.njcn.gather.device.util;
import org.junit.Test;
import org.springframework.mock.web.MockMultipartFile;
import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class PqDevImageSupportTest {
@Test
public void buildEchoPayloadReturnsEmptyFieldsWhenImageMissing() {
PqDevImageSupport.ImageEchoPayload payload = PqDevImageSupport.buildEchoPayload(null);
private static final String INVALID_IMAGE_CONTENT = "图片内容无效";
private static final String UNSUPPORTED_IMAGE_TYPE = "仅支持 JPG、JPEG、PNG 图片";
private static final String IMAGE_TOO_LARGE = "图片大小不能超过 10MB";
assertFalse(payload.isHasImage());
assertNull(payload.getImageBase64());
assertNull(payload.getImageContentType());
@Test
public void processUploadConvertsPngToJpeg() throws Exception {
byte[] pngBytes = createImageBytes("png", 16, 10, BufferedImage.TYPE_INT_ARGB);
MockMultipartFile image = new MockMultipartFile("image", "test.png", "image/png", pngBytes);
byte[] result = PqDevImageSupport.processUploadToJpeg(image);
assertEquals("image/jpeg", PqDevImageSupport.detectContentType(result));
BufferedImage decoded = ImageIO.read(new ByteArrayInputStream(result));
assertNotNull(decoded);
assertEquals(16, decoded.getWidth());
assertEquals(10, decoded.getHeight());
}
@Test
public void buildEchoPayloadDetectsPngAndCreatesBase64() throws Exception {
byte[] imageBytes = createImageBytes("png");
public void processUploadConvertsTransparentPngToWhiteJpegBackground() throws Exception {
byte[] pngBytes = createTransparentImageBytes();
MockMultipartFile image = new MockMultipartFile("image", "transparent.png", "image/png", pngBytes);
PqDevImageSupport.ImageEchoPayload payload = PqDevImageSupport.buildEchoPayload(imageBytes);
byte[] result = PqDevImageSupport.processUploadToJpeg(image);
assertTrue(payload.isHasImage());
assertEquals("image/png", payload.getImageContentType());
assertNotNull(payload.getImageBase64());
assertFalse(payload.getImageBase64().isEmpty());
BufferedImage decoded = ImageIO.read(new ByteArrayInputStream(result));
assertNotNull(decoded);
Color background = new Color(decoded.getRGB(0, 0));
assertTrue(background.getRed() >= 245);
assertTrue(background.getGreen() >= 245);
assertTrue(background.getBlue() >= 245);
}
@Test(expected = IllegalArgumentException.class)
@Test
public void processUploadKeepsJpegReadable() throws Exception {
byte[] jpegBytes = createImageBytes("jpg", 12, 8, BufferedImage.TYPE_INT_RGB);
MockMultipartFile image = new MockMultipartFile("image", "test.jpeg", "image/jpeg", jpegBytes);
byte[] result = PqDevImageSupport.processUploadToJpeg(image);
assertEquals("image/jpeg", PqDevImageSupport.detectContentType(result));
BufferedImage decoded = ImageIO.read(new ByteArrayInputStream(result));
assertNotNull(decoded);
assertEquals(12, decoded.getWidth());
assertEquals(8, decoded.getHeight());
}
@Test
public void processUploadRejectsInvalidBytes() throws Exception {
MockMultipartFile image = new MockMultipartFile("image", "test.png", "image/png", new byte[]{1, 2, 3});
assertIllegalArgumentMessage(INVALID_IMAGE_CONTENT, () -> PqDevImageSupport.processUploadToJpeg(image));
}
@Test
public void processUploadRejectsNullImage() {
assertIllegalArgumentMessage(INVALID_IMAGE_CONTENT, () -> PqDevImageSupport.processUploadToJpeg(null));
}
@Test
public void processUploadRejectsEmptyImage() {
MockMultipartFile image = new MockMultipartFile("image", "empty.png", "image/png", new byte[0]);
assertIllegalArgumentMessage(INVALID_IMAGE_CONTENT, () -> PqDevImageSupport.processUploadToJpeg(image));
}
@Test
public void processUploadRejectsOversizedImage() {
MockMultipartFile image = new MockMultipartFile(
"image",
"large.jpg",
"image/jpeg",
new byte[(int) PqDevImageSupport.MAX_FILE_SIZE + 1]
);
assertIllegalArgumentMessage(IMAGE_TOO_LARGE, () -> PqDevImageSupport.processUploadToJpeg(image));
}
@Test
public void processUploadRejectsUnsupportedType() throws Exception {
byte[] pngBytes = createImageBytes("png", 2, 2, BufferedImage.TYPE_INT_RGB);
MockMultipartFile image = new MockMultipartFile("image", "test.gif", "image/gif", pngBytes);
assertIllegalArgumentMessage(UNSUPPORTED_IMAGE_TYPE, () -> PqDevImageSupport.processUploadToJpeg(image));
}
@Test
public void processUploadRejectsUnsupportedContentTypeEvenWithAllowedFileName() throws Exception {
byte[] pngBytes = createImageBytes("png", 2, 2, BufferedImage.TYPE_INT_RGB);
MockMultipartFile image = new MockMultipartFile("image", "test.png", "image/gif", pngBytes);
assertIllegalArgumentMessage(UNSUPPORTED_IMAGE_TYPE, () -> PqDevImageSupport.processUploadToJpeg(image));
}
@Test
public void processUploadRejectsUnsupportedFileNameEvenWithAllowedContentType() throws Exception {
byte[] pngBytes = createImageBytes("png", 2, 2, BufferedImage.TYPE_INT_RGB);
MockMultipartFile image = new MockMultipartFile("image", "test.gif", "image/png", pngBytes);
assertIllegalArgumentMessage(UNSUPPORTED_IMAGE_TYPE, () -> PqDevImageSupport.processUploadToJpeg(image));
}
@Test
public void processUploadRejectsUnsupportedImageBytes() throws Exception {
byte[] gifBytes = new byte[]{0x47, 0x49, 0x46, 0x38, 0x39, 0x61};
MockMultipartFile image = new MockMultipartFile("image", "test.png", "image/png", gifBytes);
assertIllegalArgumentMessage(UNSUPPORTED_IMAGE_TYPE, () -> PqDevImageSupport.processUploadToJpeg(image));
}
@Test
public void processUploadScalesLargeImage() throws Exception {
byte[] jpegBytes = createImageBytes("jpg", 3200, 1200, BufferedImage.TYPE_INT_RGB);
MockMultipartFile image = new MockMultipartFile("image", "large.jpg", "image/jpeg", jpegBytes);
byte[] result = PqDevImageSupport.processUploadToJpeg(image);
BufferedImage decoded = ImageIO.read(new ByteArrayInputStream(result));
assertNotNull(decoded);
assertEquals(PqDevImageSupport.MAX_EDGE, Math.max(decoded.getWidth(), decoded.getHeight()));
assertEquals(1600, decoded.getWidth());
assertEquals(600, decoded.getHeight());
}
@Test
public void detectContentTypeRejectsInvalidImageBytes() {
PqDevImageSupport.detectContentType(new byte[]{1, 2, 3, 4});
assertIllegalArgumentMessage(INVALID_IMAGE_CONTENT, () -> PqDevImageSupport.detectContentType(new byte[]{1, 2, 3}));
}
private byte[] createImageBytes(String format) throws Exception {
BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB);
image.setRGB(0, 0, Color.RED.getRGB());
image.setRGB(1, 0, Color.GREEN.getRGB());
image.setRGB(0, 1, Color.BLUE.getRGB());
image.setRGB(1, 1, Color.WHITE.getRGB());
@Test
public void detectContentTypeRejectsUnsupportedImageBytes() {
assertIllegalArgumentMessage(UNSUPPORTED_IMAGE_TYPE, () -> PqDevImageSupport.detectContentType(new byte[]{1, 2, 3, 4}));
}
private byte[] createImageBytes(String format, int width, int height, int type) throws Exception {
BufferedImage image = new BufferedImage(width, height, type);
Graphics2D graphics = image.createGraphics();
try {
graphics.setColor(new Color(255, 0, 0, type == BufferedImage.TYPE_INT_ARGB ? 120 : 255));
graphics.fillRect(0, 0, width, height);
graphics.setColor(Color.BLUE);
graphics.fillRect(width / 2, height / 2, Math.max(1, width / 2), Math.max(1, height / 2));
} finally {
graphics.dispose();
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, format, outputStream);
assertTrue(ImageIO.write(image, format, outputStream));
return outputStream.toByteArray();
}
private byte[] createTransparentImageBytes() throws Exception {
BufferedImage image = new BufferedImage(4, 4, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = image.createGraphics();
try {
graphics.setComposite(java.awt.AlphaComposite.Clear);
graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
graphics.setComposite(java.awt.AlphaComposite.SrcOver);
graphics.setColor(new Color(0, 0, 255, 128));
graphics.fillRect(2, 2, 2, 2);
} finally {
graphics.dispose();
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
assertTrue(ImageIO.write(image, "png", outputStream));
return outputStream.toByteArray();
}
private void assertIllegalArgumentMessage(String expectedMessage, ThrowingRunnable runnable) {
try {
runnable.run();
} catch (IllegalArgumentException ex) {
assertEquals(expectedMessage, ex.getMessage());
return;
} catch (Exception ex) {
throw new AssertionError("Expected IllegalArgumentException", ex);
}
throw new AssertionError("Expected IllegalArgumentException");
}
private interface ThrowingRunnable {
void run() throws Exception;
}
}