feat(image): 支持多角度图片旋转及方向判断

- 在 PhotoProcessContext 中新增 imageRotation 字段用于存储旋转角度
- 修改 ConditionalRotateStage 支持 90、180、270 度旋转
- 优化 ImageOrientationStage 综合判断图片方向逻辑
- 新增 NoOpStage 作为空操作阶段占位符
- 解除 DeviceVideoContinuityCheckTask 的生产环境限制
- 添加完整的单元测试覆盖各种旋转场景和边界情况
This commit is contained in:
2025-11-26 14:34:17 +08:00
parent d2846e6d8e
commit 90efc908c5
7 changed files with 424 additions and 18 deletions

View File

@@ -92,6 +92,13 @@ public class PhotoProcessContext {
private File processedFile;
private boolean isLandscape = true;
private boolean needRotation = false;
/**
* 图像需要旋转的角度(用于后续Stage使用)
* 由 ImageOrientationStage 从 Crop.rotation 中提取并设置
*/
private Integer imageRotation;
private String resultUrl;
private IStorageAdapter storageAdapter;
private WatermarkInfo watermarkInfo;

View File

@@ -13,13 +13,13 @@ import java.io.File;
/**
* 条件旋转Stage
* 如果是竖图,旋转90度变成横图(便于后续水印处理)
* 根据图片需要旋转的角度进行旋转(便于后续水印处理
*/
@Slf4j
@StageConfig(
stageId = "rotate",
optionalMode = StageOptionalMode.UNSUPPORT,
description = "竖图旋转90度"
description = "根据需要旋转图片"
)
public class ConditionalRotateStage extends AbstractPipelineStage<PhotoProcessContext> {
@@ -43,11 +43,18 @@ public class ConditionalRotateStage extends AbstractPipelineStage<PhotoProcessCo
return StageResult.failed("当前文件不存在");
}
// 获取需要旋转的角度
Integer rotation = context.getImageRotation();
if (rotation == null || rotation == 0) {
return StageResult.skipped("无需旋转");
}
File rotatedFile = context.getTempFileManager()
.createTempFile("rotated", ".jpg");
log.debug("竖图旋转90度: {} -> {}", currentFile.getName(), rotatedFile.getName());
ImageUtils.rotateImage90(currentFile, rotatedFile);
// 根据实际角度进行旋转
log.debug("旋转图片{}度: {} -> {}", rotation, currentFile.getName(), rotatedFile.getName());
rotateByDegree(currentFile, rotatedFile, rotation);
if (!rotatedFile.exists()) {
return StageResult.failed("旋转后的文件未生成");
@@ -57,12 +64,31 @@ public class ConditionalRotateStage extends AbstractPipelineStage<PhotoProcessCo
context.setNeedRotation(true);
context.setOffsetLeft(OFFSET_LEFT_FOR_PORTRAIT);
log.info("图已旋转90度, offsetLeft={}", OFFSET_LEFT_FOR_PORTRAIT);
return StageResult.success("已旋转90");
log.info("已旋转{}度, offsetLeft={}", rotation, OFFSET_LEFT_FOR_PORTRAIT);
return StageResult.success("已旋转" + rotation + "");
} catch (Exception e) {
log.error("图片旋转失败", e);
return StageResult.failed("旋转失败: " + e.getMessage(), e);
}
}
/**
* 根据角度旋转图片
*/
private void rotateByDegree(File input, File output, int degree) throws Exception {
switch (degree) {
case 90:
ImageUtils.rotateImage90(input, output);
break;
case 180:
ImageUtils.rotateImage180(input, output);
break;
case 270:
ImageUtils.rotateImage270(input, output);
break;
default:
throw new IllegalArgumentException("不支持的旋转角度: " + degree);
}
}
}

View File

@@ -1,5 +1,6 @@
package com.ycwl.basic.image.pipeline.stages;
import com.ycwl.basic.image.pipeline.enums.ImageSource;
import com.ycwl.basic.model.Crop;
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
@@ -45,6 +46,11 @@ public class ImageOrientationStage extends AbstractPipelineStage<PhotoProcessCon
boolean isLandscape = detectOrientation(currentFile, context.getCrop());
context.setLandscape(isLandscape);
// 保存 rotation 信息到 context,方便后续 Stage 使用
if (context.getCrop() != null && context.getCrop().getRotation() != null) {
context.setImageRotation(context.getCrop().getRotation());
}
String orientation = isLandscape ? "横图" : "竖图";
log.info("图片方向检测: {}", orientation);
@@ -58,20 +64,28 @@ public class ImageOrientationStage extends AbstractPipelineStage<PhotoProcessCon
/**
* 检测图片方向
* 优先使用Crop的rotation字段,其次使用图片宽高判断
* 综合考虑物理分辨率和rotation字段
* rotation表示"需要旋转多少度照片才能正确显示"
*/
private boolean detectOrientation(File imageFile, Crop crop) {
if (crop != null && crop.getRotation() != null) {
int rotation = crop.getRotation();
boolean isLandscape = (rotation == 90 || rotation == 270);
log.debug("从Crop.rotation判断方向: rotation={}, isLandscape={}", rotation, isLandscape);
return isLandscape;
}
try {
boolean isLandscape = ImageUtils.isLandscape(imageFile);
log.debug("从图片尺寸判断方向: isLandscape={}", isLandscape);
return isLandscape;
// 先获取物理分辨率方向
boolean physicalLandscape = ImageUtils.isLandscape(imageFile);
// 如果有rotation信息,需要综合判断
if (crop != null && crop.getRotation() != null) {
int rotation = crop.getRotation();
// rotation=90/270 会翻转方向
boolean isLandscape = (rotation == 90 || rotation == 270) ? !physicalLandscape : physicalLandscape;
log.debug("综合判断方向: physicalLandscape={}, rotation={}, finalLandscape={}",
physicalLandscape, rotation, isLandscape);
return isLandscape;
}
// 没有rotation信息,直接使用物理分辨率
log.debug("从图片尺寸判断方向: isLandscape={}", physicalLandscape);
return physicalLandscape;
} catch (Exception e) {
log.warn("从图片尺寸判断方向失败,默认为横图: {}", e.getMessage());
return true;

View File

@@ -0,0 +1,17 @@
package com.ycwl.basic.image.pipeline.stages;
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
import com.ycwl.basic.image.pipeline.core.StageResult;
public class NoOpStage extends AbstractPipelineStage<PhotoProcessContext> {
@Override
protected StageResult doExecute(PhotoProcessContext context) {
return StageResult.skipped("无操作");
}
@Override
public String getName() {
return "NoOpStage";
}
}

View File

@@ -36,7 +36,7 @@ import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@EnableScheduling
@Profile("prod")
//@Profile("prod")
public class DeviceVideoContinuityCheckTask {
private static final String REDIS_KEY_PREFIX = "device:video:continuity:";