浏览代码

image-utils工具增加图片拼接后的布局规则保存,增加自适应规则下的大图切割逻辑;
评卷页面所有轨迹标记,根据拼接布局额外计算所属裁切图的序号与坐标,并在数据库保存;
修改轨迹还原逻辑,使用轨迹标记所属的裁切图序号坐标计算所属的原图序号与坐标;
轨迹还原兼容裁切坐标为空,或者裁切坐标不包含裁切图序号的自适应情况;

luoshi 4 年之前
父节点
当前提交
181acb5d88

+ 21 - 11
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/exam/service/impl/ExamStudentServiceImpl.java

@@ -768,9 +768,10 @@ public class ExamStudentServiceImpl extends BaseQueryService<ExamStudent> implem
         // 构造评卷标记信息
         Exam exam = examService.findById(student.getExamId());
         ExamSubject subject = subjectService.find(student.getExamId(), student.getSubjectCode());
-        List<PictureConfigItem> sliceConfig = subject.getSliceConfigList().isEmpty() ?
-                exam.getSliceConfigList() :
-                subject.getSliceConfigList();
+        List<PictureConfigItem> sliceConfig = subject.getSliceConfigList();
+        if (sliceConfig.isEmpty()) {
+            sliceConfig = exam.getSliceConfigList();
+        }
         if (!sliceConfig.isEmpty()) {
             List<PictureTag> tags = PictureConfigTransform.process(sliceConfig, getSliceTags(student)).get(index);
             if (tags != null) {
@@ -788,9 +789,10 @@ public class ExamStudentServiceImpl extends BaseQueryService<ExamStudent> implem
         Map<MarkGroup, List<OriginTag>> tagMap = new HashMap<MarkGroup, List<OriginTag>>();
         Exam exam = examService.findById(student.getExamId());
         ExamSubject subject = subjectService.find(student.getExamId(), student.getSubjectCode());
-        List<PictureConfigItem> sliceConfig = subject.getSliceConfigList().isEmpty() ?
-                exam.getSliceConfigList() :
-                subject.getSliceConfigList();
+        List<PictureConfigItem> sliceConfig = subject.getSliceConfigList();
+        if (sliceConfig.isEmpty()) {
+            sliceConfig = exam.getSliceConfigList();
+        }
         if (!sliceConfig.isEmpty()) {
             // 有裁切图配置时才需要获取原始评卷标记信息
             tagMap = getSliceTags(student);
@@ -907,7 +909,15 @@ public class ExamStudentServiceImpl extends BaseQueryService<ExamStudent> implem
         // 原图中需要显示的内容列表
         List<OriginTag> originTags = new LinkedList<>();
         // 首先添加本大题总得分
-        originTags.add(new OriginTag(format.format(score), 0, 0));
+        List<PictureConfigItem> configList = group.getPictureConfigList();
+        if (configList.isEmpty()) {
+            //未设置评卷区域,则自动取第一张裁切图的固定位置
+            originTags.add(new OriginTag(format.format(score), 1, 10, 10));
+        } else {
+            //取第一个显示区域相对裁切图的位置
+            PictureConfigItem config = configList.get(0);
+            originTags.add(new OriginTag(format.format(score), config.getI(), config.getX(), config.getY()));
+        }
         // 检测应该使用哪个评卷任务的轨迹记录
         MarkLibrary selected = null;
         List<MarkLibrary> libraries = libraryService.findByStudentAndGroup(student.getId(), group.getNumber());
@@ -921,14 +931,14 @@ public class ExamStudentServiceImpl extends BaseQueryService<ExamStudent> implem
             // 添加轨迹分
             List<MarkTrack> tracks = trackService.findByLibraryId(selected.getId());
             for (MarkTrack markTrack : tracks) {
-                originTags.add(new OriginTag(format.format(markTrack.getScore()), markTrack.getPositionX(),
-                        markTrack.getPositionY()));
+                originTags.add(new OriginTag(format.format(markTrack.getScore()), markTrack.getOffsetIndex(),
+                        markTrack.getOffsetX(), markTrack.getOffsetY()));
             }
             // 添加特殊标记
             List<MarkSpecialTag> specialTags = specialTagService.findByLibraryId(selected.getId());
             for (MarkSpecialTag markSpecialTag : specialTags) {
-                originTags.add(new OriginTag(markSpecialTag.getTagName(), markSpecialTag.getPositionX(),
-                        markSpecialTag.getPositionY()));
+                originTags.add(new OriginTag(markSpecialTag.getTagName(), markSpecialTag.getOffsetIndex(),
+                        markSpecialTag.getOffsetX(), markSpecialTag.getOffsetY()));
             }
         }
         return originTags;

+ 43 - 5
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/mark/model/MarkSpecialTag.java

@@ -1,10 +1,6 @@
 package cn.com.qmth.stmms.biz.mark.model;
 
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.Id;
-import javax.persistence.Table;
+import javax.persistence.*;
 
 @Entity
 @Table(name = "m_special_tag")
@@ -26,6 +22,24 @@ public class MarkSpecialTag {
     @Column(name = "position_y", nullable = false)
     private Double positionY;
 
+    /**
+     * 所在裁切图序号
+     */
+    @Column(name = "offset_index", nullable = false)
+    private Integer offsetIndex;
+
+    /**
+     * 相对裁切图X轴坐标
+     */
+    @Column(name = "offset_x", nullable = false)
+    private Integer offsetX;
+
+    /**
+     * 相对裁切图Y轴坐标
+     */
+    @Column(name = "offset_y", nullable = false)
+    private Integer offsetY;
+
     public MarkSpecialTag() {
     }
 
@@ -68,4 +82,28 @@ public class MarkSpecialTag {
     public void setPositionY(Double positionY) {
         this.positionY = positionY;
     }
+
+    public Integer getOffsetIndex() {
+        return offsetIndex;
+    }
+
+    public void setOffsetIndex(Integer offsetIndex) {
+        this.offsetIndex = offsetIndex;
+    }
+
+    public Integer getOffsetX() {
+        return offsetX;
+    }
+
+    public void setOffsetX(Integer offsetX) {
+        this.offsetX = offsetX;
+    }
+
+    public Integer getOffsetY() {
+        return offsetY;
+    }
+
+    public void setOffsetY(Integer offsetY) {
+        this.offsetY = offsetY;
+    }
 }

+ 42 - 2
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/mark/model/MarkTrack.java

@@ -7,9 +7,8 @@ import javax.persistence.Table;
 
 /**
  * 阅卷轨迹
- * 
+ *
  * @author luoshi
- * 
  */
 @Entity
 @Table(name = "m_track")
@@ -51,6 +50,24 @@ public class MarkTrack {
     @Column(name = "position_y", nullable = false)
     private Double positionY;
 
+    /**
+     * 所在裁切图序号
+     */
+    @Column(name = "offset_index", nullable = false)
+    private Integer offsetIndex;
+
+    /**
+     * 相对裁切图X轴坐标
+     */
+    @Column(name = "offset_x", nullable = false)
+    private Integer offsetX;
+
+    /**
+     * 相对裁切图Y轴坐标
+     */
+    @Column(name = "offset_y", nullable = false)
+    private Integer offsetY;
+
     public MarkTrack() {
         this.pk = new MarkTrackPK();
     }
@@ -143,4 +160,27 @@ public class MarkTrack {
         this.subjectCode = subjectCode;
     }
 
+    public Integer getOffsetIndex() {
+        return offsetIndex;
+    }
+
+    public void setOffsetIndex(Integer offsetIndex) {
+        this.offsetIndex = offsetIndex;
+    }
+
+    public Integer getOffsetX() {
+        return offsetX;
+    }
+
+    public void setOffsetX(Integer offsetX) {
+        this.offsetX = offsetX;
+    }
+
+    public Integer getOffsetY() {
+        return offsetY;
+    }
+
+    public void setOffsetY(Integer offsetY) {
+        this.offsetY = offsetY;
+    }
 }

+ 36 - 0
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/mark/model/SpecialTagDTO.java

@@ -12,6 +12,12 @@ public class SpecialTagDTO implements Serializable {
 
     private Double positionY;
 
+    private Integer offsetIndex;
+
+    private Integer offsetX;
+
+    private Integer offsetY;
+
     public SpecialTagDTO() {
 
     }
@@ -22,6 +28,9 @@ public class SpecialTagDTO implements Serializable {
         markSpecialTag.setTagName(tagName);
         markSpecialTag.setPositionX(positionX);
         markSpecialTag.setPositionY(positionY);
+        markSpecialTag.setOffsetIndex(offsetIndex);
+        markSpecialTag.setOffsetX(offsetX);
+        markSpecialTag.setOffsetY(offsetY);
         return markSpecialTag;
     }
 
@@ -32,6 +41,9 @@ public class SpecialTagDTO implements Serializable {
         tag.setContent(tagName);
         tag.setPositionX(positionX);
         tag.setPositionY(positionY);
+        tag.setOffsetIndex(offsetIndex);
+        tag.setOffsetX(offsetX);
+        tag.setOffsetY(offsetY);
         return tag;
     }
 
@@ -58,4 +70,28 @@ public class SpecialTagDTO implements Serializable {
     public void setPositionY(Double positionY) {
         this.positionY = positionY;
     }
+
+    public Integer getOffsetIndex() {
+        return offsetIndex;
+    }
+
+    public void setOffsetIndex(Integer offsetIndex) {
+        this.offsetIndex = offsetIndex;
+    }
+
+    public void setOffsetX(Integer offsetX) {
+        this.offsetX = offsetX;
+    }
+
+    public Integer getOffsetX() {
+        return offsetX;
+    }
+
+    public Integer getOffsetY() {
+        return offsetY;
+    }
+
+    public void setOffsetY(Integer offsetY) {
+        this.offsetY = offsetY;
+    }
 }

+ 12 - 4
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/mark/model/Task.java

@@ -10,19 +10,16 @@ public class Task extends MarkResult implements Serializable {
 
     /**
      * 评卷状态
-     * 
      */
     private String statusName;
 
     /**
      * 显示任务编号
-     * 
      */
     private String taskNumber;
 
     /**
      * 显示考生编号
-     * 
      */
     private String studentNumber;
 
@@ -36,6 +33,11 @@ public class Task extends MarkResult implements Serializable {
      */
     private List<PictureConfigItem> pictureConfig;
 
+    /**
+     * 大图默认切割配置
+     */
+    private List<PictureConfigItem> splitConfig;
+
     /**
      * 题卡原图地址
      */
@@ -116,7 +118,6 @@ public class Task extends MarkResult implements Serializable {
 
     /**
      * 显示考生名称
-     * 
      */
     private String studentName;
 
@@ -330,4 +331,11 @@ public class Task extends MarkResult implements Serializable {
         this.jsonUrl = jsonUrl;
     }
 
+    public List<PictureConfigItem> getSplitConfig() {
+        return splitConfig;
+    }
+
+    public void setSplitConfig(List<PictureConfigItem> splitConfig) {
+        this.splitConfig = splitConfig;
+    }
 }

+ 44 - 4
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/mark/model/TrackDTO.java

@@ -1,14 +1,13 @@
 package cn.com.qmth.stmms.biz.mark.model;
 
-import java.io.Serializable;
-
 import cn.com.qmth.stmms.biz.exam.model.Marker;
 
+import java.io.Serializable;
+
 /**
  * 阅卷轨迹交换类
- * 
+ *
  * @author luoshi
- * 
  */
 public class TrackDTO implements Serializable {
 
@@ -24,6 +23,12 @@ public class TrackDTO implements Serializable {
 
     private double positionY;
 
+    private int offsetIndex;
+
+    private int offsetX;
+
+    private int offsetY;
+
     public TrackDTO() {
 
     }
@@ -34,6 +39,9 @@ public class TrackDTO implements Serializable {
         setScore(track.getScore());
         setPositionX(track.getPositionX());
         setPositionY(track.getPositionY());
+        setOffsetIndex(track.getOffsetIndex());
+        setOffsetX(track.getOffsetX());
+        setOffsetY(track.getOffsetY());
     }
 
     public TrackDTO(TrialTrack track) {
@@ -42,6 +50,9 @@ public class TrackDTO implements Serializable {
         setScore(track.getScore());
         setPositionX(track.getPositionX());
         setPositionY(track.getPositionY());
+        setOffsetIndex(track.getOffsetIndex());
+        setOffsetX(track.getOffsetX());
+        setOffsetY(track.getOffsetY());
     }
 
     public MarkTrack transform(MarkLibrary library, Marker marker) {
@@ -57,6 +68,9 @@ public class TrackDTO implements Serializable {
         track.setScore(getScore());
         track.setPositionX(getPositionX());
         track.setPositionY(getPositionY());
+        track.setOffsetIndex(getOffsetIndex());
+        track.setOffsetX(getOffsetX());
+        track.setOffsetY(getOffsetY());
         return track;
     }
 
@@ -74,6 +88,9 @@ public class TrackDTO implements Serializable {
         track.setScore(getScore());
         track.setPositionX(getPositionX());
         track.setPositionY(getPositionY());
+        track.setOffsetIndex(getOffsetIndex());
+        track.setOffsetX(getOffsetX());
+        track.setOffsetY(getOffsetY());
         return track;
     }
 
@@ -117,4 +134,27 @@ public class TrackDTO implements Serializable {
         this.positionY = positionY;
     }
 
+    public int getOffsetIndex() {
+        return offsetIndex;
+    }
+
+    public void setOffsetIndex(int offsetIndex) {
+        this.offsetIndex = offsetIndex;
+    }
+
+    public int getOffsetX() {
+        return offsetX;
+    }
+
+    public void setOffsetX(int offsetX) {
+        this.offsetX = offsetX;
+    }
+
+    public int getOffsetY() {
+        return offsetY;
+    }
+
+    public void setOffsetY(int offsetY) {
+        this.offsetY = offsetY;
+    }
 }

+ 44 - 8
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/mark/model/TrialTag.java

@@ -1,18 +1,12 @@
 package cn.com.qmth.stmms.biz.mark.model;
 
+import javax.persistence.*;
 import java.io.Serializable;
 
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.Id;
-import javax.persistence.Table;
-
 /**
  * 试评特殊标记
- * 
- * @author luoshi
  *
+ * @author luoshi
  */
 @Entity
 @Table(name = "m_trial_tag")
@@ -39,6 +33,24 @@ public class TrialTag implements Serializable {
     @Column(name = "position_y", nullable = false)
     private Double positionY;
 
+    /**
+     * 所在裁切图序号
+     */
+    @Column(name = "offset_index", nullable = false)
+    private Integer offsetIndex;
+
+    /**
+     * 相对裁切图X轴坐标
+     */
+    @Column(name = "offset_x", nullable = false)
+    private Integer offsetX;
+
+    /**
+     * 相对裁切图Y轴坐标
+     */
+    @Column(name = "offset_y", nullable = false)
+    private Integer offsetY;
+
     public Integer getId() {
         return id;
     }
@@ -86,4 +98,28 @@ public class TrialTag implements Serializable {
     public void setPositionY(Double positionY) {
         this.positionY = positionY;
     }
+
+    public Integer getOffsetIndex() {
+        return offsetIndex;
+    }
+
+    public void setOffsetIndex(Integer offsetIndex) {
+        this.offsetIndex = offsetIndex;
+    }
+
+    public Integer getOffsetX() {
+        return offsetX;
+    }
+
+    public void setOffsetX(Integer offsetX) {
+        this.offsetX = offsetX;
+    }
+
+    public Integer getOffsetY() {
+        return offsetY;
+    }
+
+    public void setOffsetY(Integer offsetY) {
+        this.offsetY = offsetY;
+    }
 }

+ 42 - 2
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/mark/model/TrialTrack.java

@@ -7,9 +7,8 @@ import javax.persistence.Table;
 
 /**
  * 试评阅卷轨迹
- * 
+ *
  * @author luoshi
- * 
  */
 @Entity
 @Table(name = "m_trial_track")
@@ -48,6 +47,24 @@ public class TrialTrack {
     @Column(name = "position_y", nullable = false)
     private Double positionY;
 
+    /**
+     * 所在裁切图序号
+     */
+    @Column(name = "offset_index", nullable = false)
+    private Integer offsetIndex;
+
+    /**
+     * 相对裁切图X轴坐标
+     */
+    @Column(name = "offset_x", nullable = false)
+    private Integer offsetX;
+
+    /**
+     * 相对裁切图Y轴坐标
+     */
+    @Column(name = "offset_y", nullable = false)
+    private Integer offsetY;
+
     public TrialTrack() {
         this.pk = new TrialTrackPK();
     }
@@ -140,4 +157,27 @@ public class TrialTrack {
         this.subjectCode = subjectCode;
     }
 
+    public Integer getOffsetIndex() {
+        return offsetIndex;
+    }
+
+    public void setOffsetIndex(Integer offsetIndex) {
+        this.offsetIndex = offsetIndex;
+    }
+
+    public Integer getOffsetX() {
+        return offsetX;
+    }
+
+    public void setOffsetX(Integer offsetX) {
+        this.offsetX = offsetX;
+    }
+
+    public Integer getOffsetY() {
+        return offsetY;
+    }
+
+    public void setOffsetY(Integer offsetY) {
+        this.offsetY = offsetY;
+    }
 }

+ 23 - 13
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/utils/OriginTag.java

@@ -4,18 +4,21 @@ public class OriginTag {
 
     private String content;
 
-    private double positionX;
+    private int offsetIndex;
 
-    private double positionY;
+    private int offsetX;
+
+    private int offsetY;
 
     public OriginTag() {
 
     }
 
-    public OriginTag(String content, double positionX, double positionY) {
+    public OriginTag(String content, int offsetIndex, int offsetX, int offsetY) {
         this.content = content;
-        this.positionX = positionX;
-        this.positionY = positionY;
+        this.offsetIndex = offsetIndex;
+        this.offsetX = offsetX;
+        this.offsetY = offsetY;
     }
 
     public String getContent() {
@@ -26,20 +29,27 @@ public class OriginTag {
         this.content = content;
     }
 
-    public double getPositionX() {
-        return positionX;
+    public int getOffsetIndex() {
+        return offsetIndex;
+    }
+
+    public void setOffsetIndex(int offsetIndex) {
+        this.offsetIndex = offsetIndex;
     }
 
-    public void setPositionX(double positionX) {
-        this.positionX = positionX;
+    public int getOffsetX() {
+        return offsetX;
     }
 
-    public double getPositionY() {
-        return positionY;
+    public void setOffsetX(int offsetX) {
+        this.offsetX = offsetX;
     }
 
-    public void setPositionY(double positionY) {
-        this.positionY = positionY;
+    public int getOffsetY() {
+        return offsetY;
     }
 
+    public void setOffsetY(int offsetY) {
+        this.offsetY = offsetY;
+    }
 }

+ 30 - 85
stmms-biz/src/main/java/cn/com/qmth/stmms/biz/utils/PictureConfigTransform.java

@@ -1,81 +1,44 @@
 package cn.com.qmth.stmms.biz.utils;
 
+import cn.com.qmth.stmms.biz.exam.model.MarkGroup;
+import cn.com.qmth.stmms.biz.mark.model.PictureConfigItem;
+
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 
-import cn.com.qmth.stmms.biz.exam.model.MarkGroup;
-import cn.com.qmth.stmms.biz.mark.model.PictureConfigItem;
-
 /**
  * 根据裁切图配置与评卷分组拼接配置,将各显示元素坐标转换为原图坐标
- * 
- * @author luoshi
  *
+ * @author luoshi
  */
 public class PictureConfigTransform {
 
     /**
      * 转换方法
-     * 
-     * @param sliceConfigs
-     *            - 裁切图配置
-     * @param groups
-     *            - 评卷分组与原始元素列表
+     *
+     * @param sliceConfigs - 裁切图配置
+     * @param groups       - 评卷分组与原始元素列表
      * @return 原图序号与新的显示元素列表,序号从1开始
      */
     public static Map<Integer, List<PictureTag>> process(List<PictureConfigItem> sliceConfigs,
             Map<MarkGroup, List<OriginTag>> groups) {
         Map<Integer, List<PictureTag>> map = new HashMap<>();
-        if (sliceConfigs.isEmpty()) {
-            return map;
-        }
+        //不判断裁切坐标是否空白,兼容新的原图遮盖模式
+        int sliceCount = sliceConfigs.size();
         for (Entry<MarkGroup, List<OriginTag>> entry : groups.entrySet()) {
-            MarkGroup group = entry.getKey();
-            List<OriginTag> tags = entry.getValue();
-            List<PictureConfigItem> groupConfigs = group.getPictureConfigList();
-            if (tags.isEmpty() || groupConfigs.isEmpty()) {
-                continue;
-            }
-
-            int maxWidth = 0;
-            int totalHeight = 0;
-            // 计算评卷分组拼接图的总高度与最大宽度
-            for (PictureConfigItem config : groupConfigs) {
-                if (config.getW() == 0 || config.getH() == 0) {
-                    // 兼容老数据,宽高未设置时,表示直接使用整张裁切图
-                    PictureConfigItem sliceConfig = sliceConfigs.size() >= config.getI()
-                            ? sliceConfigs.get(config.getI() - 1)
-                            : null;
-                    if (sliceConfig != null) {
-                        // 直接使用裁切图的宽高
-                        config.setW(sliceConfig.getW());
-                        config.setH(sliceConfig.getH());
-                    }
-                }
-                if (config.getW() > 0 && config.getH() > 0) {
-                    maxWidth = Math.max(maxWidth, config.getW());
-                    totalHeight += config.getH();
-                }
-            }
             // 遍历所有显示元素
-            for (OriginTag tag : tags) {
-                // 计算显示元素在拼接图内的绝对位置
-                int left = (int) (maxWidth * tag.getPositionX());
-                int top = (int) (totalHeight * tag.getPositionY());
-                int start = 0;
-                for (PictureConfigItem config : groupConfigs) {
-                    if (config.getW() > 0 && config.getH() > 0) {
-                        if (top <= (config.getH() + start)) {
-                            // 根据绝对高度判断显示元素是否落在当前拼接块
-                            buildTag(map, sliceConfigs, config, tag.getContent(), left, top - start);
-                            break;
-                        } else {
-                            start += config.getH();
-                        }
-                    }
+            for (OriginTag tag : entry.getValue()) {
+                if (tag.getOffsetIndex() > sliceCount) {
+                    //元素所属裁切图不在裁切坐标范围内,表示裁切图直接使用的原图
+                    buildTag(map, tag.getContent(), tag.getOffsetIndex(), tag.getOffsetX(), tag.getOffsetY());
+                } else {
+                    //有裁切坐标时,原图坐标=元素相对坐标+裁切坐标
+                    PictureConfigItem config = sliceConfigs.get(tag.getOffsetIndex() - 1);
+                    buildTag(map, tag.getContent(), config.getI(), config.getX() + tag.getOffsetX(),
+                            config.getY() + tag.getOffsetY());
                 }
             }
         }
@@ -84,37 +47,19 @@ public class PictureConfigTransform {
 
     /**
      * 根据计算出的拼接块,构造新的原图显示元素
-     * 
-     * @param tags
-     *            - 原图显示元素集合
-     * @param sliceConfigs
-     *            - 裁切图配置
-     * @param config
-     *            - 当前拼接块配置
-     * @param content
-     *            - 显示元素内容
-     * @param left
-     *            - 在拼接块内的左偏移
-     * @param top
-     *            - 在拼接块内的上偏移
+     *
+     * @param tags    - 原图显示元素集合
+     * @param content - 显示元素内容
+     * @param index   - 所属原图序号
+     * @param left    - 在原图内的左偏移
+     * @param top     - 在原图内的上偏移
      */
-    private static void buildTag(Map<Integer, List<PictureTag>> tags, List<PictureConfigItem> sliceConfigs,
-            PictureConfigItem config, String content, int left, int top) {
-        if (sliceConfigs.size() >= config.getI()) {
-            PictureConfigItem sliceConfig = sliceConfigs.get(config.getI() - 1);
-            PictureTag tag = new PictureTag();
-            tag.setContent(content);
-            // 原图内偏移=拼接块内偏移+拼接块在裁切图内偏移+裁切图在原图内偏移
-            tag.setLeft(left + config.getX() + sliceConfig.getX());
-            tag.setTop(top + config.getY() + sliceConfig.getY());
-
-            List<PictureTag> list = tags.get(sliceConfig.getI());
-            if (list == null) {
-                list = new LinkedList<>();
-                tags.put(sliceConfig.getI(), list);
-            }
-            list.add(tag);
-        }
+    private static void buildTag(Map<Integer, List<PictureTag>> tags, String content, int index, int left, int top) {
+        PictureTag tag = new PictureTag();
+        tag.setContent(content);
+        tag.setLeft(left);
+        tag.setTop(top);
+        tags.computeIfAbsent(index, k -> new LinkedList<>()).add(tag);
     }
 
 }

+ 3 - 1
stmms-web/src/main/webapp/WEB-INF/views/include/trialDetail.jsp

@@ -55,7 +55,9 @@
                     }
                     if (data.fileServer != undefined && data.pictureConfig != undefined) {
                         new ImageLoader({
-                            server: data.fileServer
+                            server: data.fileServer,
+                            flush: true,
+                            strict: false
                         }).merge(data.urls, data.pictureConfig, function (image) {
                             trialDetailModal.setContent($('#trial-detail-content'));
                             $(image).width($('#trial-left-div').width() * 0.95);

+ 3 - 1
stmms-web/src/main/webapp/static/mark-new/js/modules/image-builder.js

@@ -21,9 +21,11 @@ ImageBuilder.prototype.build = function (task, callback) {
     new ImageLoader({
         server: this.markControl.context.imageServer,
         flush: true,
-        strict: false
+        strict: false,
+        split: task.splitConfig
     }).merge(task.pictureUrls, this.canvas, task.pictureConfig, function (image) {
         task.imageData = image
+        task.imageLayout = image.layout
         callback()
     }, function (error) {
         callback('image load error: ' + error)

+ 44 - 27
stmms-web/src/main/webapp/static/mark-new/js/modules/single-image-view.js

@@ -1,5 +1,5 @@
 //简单多张图片排列显示模块
-var single_image_view = function(option, success) {
+var single_image_view = function (option, success) {
     var object = new SingleImageView(option);
     success();
     return object;
@@ -8,52 +8,52 @@ var single_image_view = function(option, success) {
 function SingleImageView(option) {
     this.markControl = option.markControl;
     this.init();
-    this.markControl.on('center.width.change', this, function(event, context, eventObject) {
+    this.markControl.on('center.width.change', this, function (event, context, eventObject) {
         this.reloadImage();
         this.markControl.trigger('image.reload.event');
     });
-    this.markControl.on('step.board.show', this, function(event, context, eventObject) {
+    this.markControl.on('step.board.show', this, function (event, context, eventObject) {
         this.container.removeClass('span12');
         this.container.addClass('span10');
         this.reloadImage();
         this.markControl.trigger('image.reload.event');
     });
-    this.markControl.on('step.board.hide', this, function(event, context, eventObject) {
+    this.markControl.on('step.board.hide', this, function (event, context, eventObject) {
         this.container.removeClass('span10');
         this.container.addClass('span12');
         this.reloadImage();
         this.markControl.trigger('image.reload.event');
     });
-    this.markControl.on('task.get.before', this, function(event, context, eventObject) {
+    this.markControl.on('task.get.before', this, function (event, context, eventObject) {
         this.task = undefined;
         this.image = undefined;
         this.render();
     });
-    this.markControl.on('task.get.success', this, function(event, context, eventObject) {
+    this.markControl.on('task.get.success', this, function (event, context, eventObject) {
         this.task = context.task;
         this.image = undefined;
         this.render();
     });
-    this.markControl.on('task.get.none', this, function(event, context, eventObject) {
+    this.markControl.on('task.get.none', this, function (event, context, eventObject) {
         this.task = undefined;
         this.image = undefined;
         this.render();
     });
-    this.markControl.on('mark.tag.show', this, function(event, context, tag) {
+    this.markControl.on('mark.tag.show', this, function (event, context, tag) {
         if (this.task != undefined && tag != undefined) {
             this.drawTag(tag);
         }
     });
-    this.markControl.on('mark.tag.clear', this, function(event, context, track) {
+    this.markControl.on('mark.tag.clear', this, function (event, context, track) {
         this.reloadImage();
         this.markControl.trigger('image.reload.event');
     });
-    this.markControl.on('image.position.change', this, function(event, context, topPercent) {
+    this.markControl.on('image.position.change', this, function (event, context, topPercent) {
         if (this.task != undefined) {
             this.updateScrollTop(topPercent);
         }
     });
-    this.markControl.on('mark.setting.init', this, function(event, context, setting) {
+    this.markControl.on('mark.setting.init', this, function (event, context, setting) {
         var scale = setting['image.view.scale'];
         if (scale != undefined) {
             this.scale = Number(scale);
@@ -61,7 +61,7 @@ function SingleImageView(option) {
     });
 }
 
-SingleImageView.prototype.init = function() {
+SingleImageView.prototype.init = function () {
     var self = this;
     this.container = this.markControl.container.imageContent;
     this.container.height(this.markControl.container.centerContent.height());
@@ -72,38 +72,55 @@ SingleImageView.prototype.init = function() {
     this.ctx.fillStyle = "#FFFFFF";
     this.scale = 1.0;
 
-    $(this.canvas).click(function(event) {
+    $(this.canvas).click(function (event) {
         if (self.task != undefined) {
+            let left = event.pageX - $(self.canvas).offset().left
+            let top = event.pageY - $(self.canvas).offset().top
+            let offsetX = left
+            let offsetY = top
+            let offsetIndex = 1
+            for (let i = 0; i < self.task.imageLayout.length; i++) {
+                let layout = self.task.imageLayout[i]
+                if (top > layout.top && top <= layout.bottom) {
+                    offsetIndex = layout.config.i
+                    offsetX = left / layout.ratio + layout.config.x
+                    offsetY = (top - layout.top) / layout.ratio + layout.config.y
+                    break
+                }
+            }
             self.markControl.trigger('image.click.event', {
-                positionX: ((event.pageX - $(self.canvas).offset().left) / $(self.canvas).width()).toFixed(3),
-                positionY: ((event.pageY - $(self.canvas).offset().top) / $(self.canvas).height()).toFixed(3)
+                positionX: (left / $(self.canvas).width()).toFixed(3),
+                positionY: (top / $(self.canvas).height()).toFixed(3),
+                offsetIndex: offsetIndex,
+                offsetX: offsetX,
+                offsetY: offsetY
             });
         }
     });
 
     this.imageControl = getDom(this.image_control_dom, this.markControl).insertBefore(this.markControl.container.assistantButton.parent());
-    this.imageControl.find('.zoom-in-button').click(this, function(event) {
+    this.imageControl.find('.zoom-in-button').click(this, function (event) {
         self.reloadImage(0.2);
         self.markControl.trigger('image.reload.event');
     });
-    this.imageControl.find('.zoom-out-button').click(this, function(event) {
+    this.imageControl.find('.zoom-out-button').click(this, function (event) {
         self.reloadImage(-0.2);
         self.markControl.trigger('image.reload.event');
     });
-    this.imageControl.find('.zoom-fit-button').click(this, function(event) {
+    this.imageControl.find('.zoom-fit-button').click(this, function (event) {
         self.reloadImage('fit');
         self.markControl.trigger('image.reload.event');
     });
 }
 
-SingleImageView.prototype.render = function() {
+SingleImageView.prototype.render = function () {
     var self = this;
     $(this.canvas).hide();
     if (this.task != undefined && this.task.imageData != undefined) {
         //设置画布大小及背景颜色
         this.image = new Image();
         this.image.src = this.task.imageData;
-        this.image.onload = function() {
+        this.image.onload = function () {
             self.reloadImage();
             $(self.canvas).show();
 
@@ -112,14 +129,14 @@ SingleImageView.prototype.render = function() {
     }
 }
 
-SingleImageView.prototype.reloadImage = function(scaleAddon) {
+SingleImageView.prototype.reloadImage = function (scaleAddon) {
     if (this.image != undefined) {
         var scale = this.scale;
         if (scaleAddon != undefined) {
-        	if (scaleAddon=='fit') {
-        		scale = 1.0;
-        	} else {
-            	scale += scaleAddon;
+            if (scaleAddon == 'fit') {
+                scale = 1.0;
+            } else {
+                scale += scaleAddon;
             }
         }
         if (scale < 0.2) {
@@ -140,7 +157,7 @@ SingleImageView.prototype.reloadImage = function(scaleAddon) {
     }
 }
 
-SingleImageView.prototype.drawTag = function(tag) {
+SingleImageView.prototype.drawTag = function (tag) {
     if (tag != undefined && tag.positionX > 0 && tag.positionY > 0 && this.image != undefined) {
         this.ctx.font = parseInt(60 * this.scale) + "px Arial";
         this.ctx.fillStyle = 'red';
@@ -148,7 +165,7 @@ SingleImageView.prototype.drawTag = function(tag) {
     }
 }
 
-SingleImageView.prototype.updateScrollTop = function(scrollTopPercent) {
+SingleImageView.prototype.updateScrollTop = function (scrollTopPercent) {
     var height = this.canvas.height;
     var minHeight = this.container.height();
     if (scrollTopPercent != undefined && scrollTopPercent >= 0 && scrollTopPercent <= 1 && height > minHeight) {

+ 55 - 52
stmms-web/src/main/webapp/static/mark-new/js/modules/specialTag.js

@@ -1,5 +1,5 @@
 //特殊标识模块
-var specialTag = function(option, success) {
+var specialTag = function (option, success) {
     var object = new SpecialTag(option);
     success();
     return object;
@@ -11,50 +11,53 @@ function SpecialTag(option) {
     this.show = false;
     this.tagName = undefined;
     this.init();
-    this.markControl.on('task.get.before', this, function(event, context, eventObject) {
+    this.markControl.on('task.get.before', this, function (event, context, eventObject) {
         this.task = undefined;
         this.reset();
     });
-    this.markControl.on('task.get.success', this, function(event, context, eventObject) {
+    this.markControl.on('task.get.success', this, function (event, context, eventObject) {
         this.task = context.task;
-        if(this.task.tagList==undefined){
-        	this.task.tagList=[];
+        if (this.task.tagList == undefined) {
+            this.task.tagList = [];
         }
         this.markControl.trigger('special.tag.disable');
     });
-    this.markControl.on('task.get.none', this, function(event, context, eventObject) {
+    this.markControl.on('task.get.none', this, function (event, context, eventObject) {
         this.task = undefined;
     });
-    this.markControl.on('image.click.event', this, function(event, context, eventObject) {
-        if(this.task!=undefined && this.tagName!=undefined && this.show==true){
-        	var specialTag = {
-        		tagName : this.tagName,
-        		positionX : eventObject.positionX,
-        		positionY : eventObject.positionY
-        	}
+    this.markControl.on('image.click.event', this, function (event, context, eventObject) {
+        if (this.task != undefined && this.tagName != undefined && this.show == true) {
+            var specialTag = {
+                tagName: this.tagName,
+                positionX: eventObject.positionX,
+                positionY: eventObject.positionY,
+                offsetIndex: eventObject.offsetIndex,
+                offsetX: eventObject.offsetX,
+                offsetY: eventObject.offsetY
+            }
             this.task.tagList.push(specialTag);
             this.markControl.trigger('mark.tag.show', {
-                	content: specialTag.tagName,
-                	positionX: specialTag.positionX,
-                	positionY: specialTag.positionY
+                content: specialTag.tagName,
+                positionX: specialTag.positionX,
+                positionY: specialTag.positionY
             });
         }
     })
     //图片重新加载后,恢复显示所有标记
-    this.markControl.on('image.reload.event', this, function(event, context, eventObject) {
-        if(this.task != undefined){
-            for(var i = 0; i <this.task.tagList.length; i++) {
+    this.markControl.on('image.reload.event', this, function (event, context, eventObject) {
+        if (this.task != undefined) {
+            for (var i = 0; i < this.task.tagList.length; i++) {
                 this.markControl.trigger('mark.tag.show', {
-                	content: this.task.tagList[i].tagName,
-                	positionX: this.task.tagList[i].positionX,
-                	positionY: this.task.tagList[i].positionY
+                    content: this.task.tagList[i].tagName,
+                    positionX: this.task.tagList[i].positionX,
+                    positionY: this.task.tagList[i].positionY
                 });
             }
         }
     });
 }
 
-SpecialTag.prototype.init = function() {
+SpecialTag.prototype.init = function () {
     var self = this;
     this.container = getDom(this.container_dom, this.markControl).appendTo(this.markControl.container);
     this.container.width(this.maxWidth);
@@ -76,51 +79,51 @@ SpecialTag.prototype.init = function() {
 
     this.onclickList.find('a').click(function () {
         self.tagName = undefined;
-        if(self.task!=undefined){
-        	if($(this).hasClass('selected')){
-        		self.tagName = undefined;
-        		$(this).removeClass('selected');
-        	}else{
-        		self.tagName = $(this).attr('value');
-        		self.onclickList.find('a').removeClass('selected');
-        		$(this).addClass('selected');
-        	}
+        if (self.task != undefined) {
+            if ($(this).hasClass('selected')) {
+                self.tagName = undefined;
+                $(this).removeClass('selected');
+            } else {
+                self.tagName = $(this).attr('value');
+                self.onclickList.find('a').removeClass('selected');
+                $(this).addClass('selected');
+            }
         }
-        if(self.tagName != undefined){
-        	self.markControl.trigger('special.tag.enable');
-        }else{
-        	self.markControl.trigger('special.tag.disable');
+        if (self.tagName != undefined) {
+            self.markControl.trigger('special.tag.enable');
+        } else {
+            self.markControl.trigger('special.tag.disable');
         }
-        
+
     });
-    this.container.find('#undo-button').click(function(){
-    	if(self.task!=undefined){
-    		self.task.tagList.pop();
-    		self.markControl.trigger('mark.tag.clear');
-    	}
+    this.container.find('#undo-button').click(function () {
+        if (self.task != undefined) {
+            self.task.tagList.pop();
+            self.markControl.trigger('mark.tag.clear');
+        }
     });
-    this.container.find('#clear-button').click(function(){
-    	if(self.task!=undefined){
-    		self.task.tagList=[];
-    		self.markControl.trigger('mark.tag.clear');
-    	}
+    this.container.find('#clear-button').click(function () {
+        if (self.task != undefined) {
+            self.task.tagList = [];
+            self.markControl.trigger('mark.tag.clear');
+        }
     });
-    this.container.header.find('#close-button').click(function() {
+    this.container.header.find('#close-button').click(function () {
         self.toggle(false);
     });
 
     this.control = getDom(this.control_dom, this.markControl).appendTo(this.markControl.container.assistant);
-    this.control.find('#show-SpecialTag-button').click(function() {
+    this.control.find('#show-SpecialTag-button').click(function () {
         self.markControl.container.assistant.hide();
         self.toggle(true);
     });
-    this.control.find('#hide-SpecialTag-button').click(function() {
+    this.control.find('#hide-SpecialTag-button').click(function () {
         self.markControl.container.assistant.hide();
         self.toggle(false);
     });
 }
 
-SpecialTag.prototype.toggle = function(show) {
+SpecialTag.prototype.toggle = function (show) {
     if (show == true) {
         this.show = true;
         this.container.show();
@@ -132,7 +135,7 @@ SpecialTag.prototype.toggle = function(show) {
     }
 }
 
-SpecialTag.prototype.reset = function() {
+SpecialTag.prototype.reset = function () {
     this.tagName = $(this).attr('value');
     this.onclickList.find('a').removeClass('selected');
 }

+ 41 - 24
stmms-web/src/main/webapp/static/mark-track/js/modules/single-image-view.js

@@ -1,5 +1,5 @@
 //简单多张图片排列显示模块
-var single_image_view = function(option, success) {
+var single_image_view = function (option, success) {
     var object = new SingleImageView(option);
     success();
     return object;
@@ -8,34 +8,34 @@ var single_image_view = function(option, success) {
 function SingleImageView(option) {
     this.markControl = option.markControl;
     this.init();
-    this.markControl.on('center.width.change', this, function(event, context, eventObject) {
+    this.markControl.on('center.width.change', this, function (event, context, eventObject) {
         //this.container.perfectScrollbar('update');
     });
-    this.markControl.on('task.get.before', this, function(event, context, eventObject) {
+    this.markControl.on('task.get.before', this, function (event, context, eventObject) {
         this.task = undefined;
         this.image = undefined;
         this.render();
     });
-    this.markControl.on('task.get.success', this, function(event, context, eventObject) {
+    this.markControl.on('task.get.success', this, function (event, context, eventObject) {
         this.task = context.task;
         this.image = undefined;
         this.render();
     });
-    this.markControl.on('task.get.none', this, function(event, context, eventObject) {
+    this.markControl.on('task.get.none', this, function (event, context, eventObject) {
         this.task = undefined;
         this.image = undefined;
         this.render();
     });
-    this.markControl.on('mark.tag.show', this, function(event, context, tag) {
+    this.markControl.on('mark.tag.show', this, function (event, context, tag) {
         if (this.task != undefined && tag != undefined) {
             this.drawTag(tag);
         }
     });
-    this.markControl.on('mark.tag.clear', this, function(event, context, track) {
+    this.markControl.on('mark.tag.clear', this, function (event, context, track) {
         this.reloadImage();
         this.markControl.trigger('image.reload.event');
     });
-    this.markControl.on('mark.setting.init', this, function(event, context, setting) {
+    this.markControl.on('mark.setting.init', this, function (event, context, setting) {
         var scale = setting['image.view.scale'];
         if (scale != undefined) {
             this.scale = Number(scale);
@@ -43,7 +43,7 @@ function SingleImageView(option) {
     });
 }
 
-SingleImageView.prototype.init = function() {
+SingleImageView.prototype.init = function () {
     var self = this;
     this.container = getDom(this.container_dom, this.markControl).appendTo(this.markControl.container.centerContent);
     this.container.height(this.markControl.container.centerContent.height());
@@ -57,38 +57,55 @@ SingleImageView.prototype.init = function() {
     this.ctx.fillStyle = "#FFFFFF";
     this.scale = 1.0;
 
-    $(this.canvas).click(function(event) {
+    $(this.canvas).click(function (event) {
         if (self.task != undefined) {
+            let left = event.pageX - $(self.canvas).offset().left
+            let top = event.pageY - $(self.canvas).offset().top
+            let offsetX = left
+            let offsetY = top
+            let offsetIndex = 1
+            for (let i = 0; i < self.task.imageLayout.length; i++) {
+                let layout = self.task.imageLayout[i]
+                if (top > layout.top && top <= layout.bottom) {
+                    offsetIndex = layout.config.i
+                    offsetX = left / layout.ratio + layout.config.x
+                    offsetY = (top - layout.top) / layout.ratio + layout.config.y
+                    break
+                }
+            }
             self.markControl.trigger('image.click.event', {
-                positionX: ((event.pageX - $(self.canvas).offset().left) / $(self.canvas).width()).toFixed(3),
-                positionY: ((event.pageY - $(self.canvas).offset().top) / $(self.canvas).height()).toFixed(3)
+                positionX: (left / $(self.canvas).width()).toFixed(3),
+                positionY: (top / $(self.canvas).height()).toFixed(3),
+                offsetIndex: offsetIndex,
+                offsetX: offsetX,
+                offsetY: offsetY
             });
         }
     });
 
     this.imageControl = getDom(this.image_control_dom, this.markControl).insertBefore(this.markControl.container.assistantButton.parent());
-    this.imageControl.find('.zoom-in-button').click(this, function(event) {
+    this.imageControl.find('.zoom-in-button').click(this, function (event) {
         self.reloadImage(0.2);
         self.markControl.trigger('image.reload.event');
     });
-    this.imageControl.find('.zoom-out-button').click(this, function(event) {
+    this.imageControl.find('.zoom-out-button').click(this, function (event) {
         self.reloadImage(-0.2);
         self.markControl.trigger('image.reload.event');
     });
-    this.imageControl.find('.zoom-fit-button').click(this, function(event) {
+    this.imageControl.find('.zoom-fit-button').click(this, function (event) {
         self.reloadImage('fit');
         self.markControl.trigger('image.reload.event');
     });
 }
 
-SingleImageView.prototype.render = function() {
+SingleImageView.prototype.render = function () {
     var self = this;
     $(this.canvas).hide();
     if (this.task != undefined && this.task.imageData != undefined) {
         //设置画布大小及背景颜色
         this.image = new Image();
         this.image.src = this.task.imageData;
-        this.image.onload = function() {
+        this.image.onload = function () {
             self.reloadImage();
             $(self.canvas).show();
 
@@ -97,15 +114,15 @@ SingleImageView.prototype.render = function() {
     }
 }
 
-SingleImageView.prototype.reloadImage = function(scaleAddon) {
+SingleImageView.prototype.reloadImage = function (scaleAddon) {
     if (this.image != undefined) {
         var scale = this.scale;
         if (scaleAddon != undefined) {
-        	if (scaleAddon == 'fit') {
-        		scale = 1.0;
-        	} else {
-        		scale += scaleAddon;
-        	}
+            if (scaleAddon == 'fit') {
+                scale = 1.0;
+            } else {
+                scale += scaleAddon;
+            }
         }
         if (scale < 0.2) {
             scale = 0.2;
@@ -125,7 +142,7 @@ SingleImageView.prototype.reloadImage = function(scaleAddon) {
     }
 }
 
-SingleImageView.prototype.drawTag = function(tag) {
+SingleImageView.prototype.drawTag = function (tag) {
     if (tag != undefined && tag.positionX > 0 && tag.positionY > 0 && this.image != undefined) {
         this.ctx.font = parseInt(60 * this.scale) + "px Arial";
         this.ctx.fillStyle = 'red';

+ 26 - 26
stmms-web/src/main/webapp/static/utils/image-utils.js

@@ -20,14 +20,18 @@ function getPixelRatio(context) {
  * 1. 支持指定服务器地址、是否强制刷新、是否严格模式
  * 2. 支持按云阅卷规则筛选实际需要加载的图片
  * 3. 支持动态使用画布,按云阅卷规则垂直拼接出新的图片
+ * 4. 支持指定切割规则,用于默认模式下大图的切割坐标
  *
- * @param {{server: *, flush: boolean, strict: boolean}} option
+ * @param {{server: *, flush: boolean, strict: boolean, split: array}} option
  */
 function ImageLoader(option) {
     option = option || {}
     this.server = option.server || ''
     this.flush = option.flush === true
     this.strict = option.strict === true
+    this.split = Array.isArray(option.split) && option.split.length > 0 ? option.split : [
+        {left: 0, width: 0.55}, {left: 0.45, width: 0.55}
+    ]
 }
 
 ImageLoader.prototype.load = function (urls, config, onSuccess, onError) {
@@ -140,6 +144,7 @@ ImageLoader.prototype.combine = function (urls, canvas, config, onSuccess, onErr
     })
     urls = Array.isArray(urls) ? urls : []
     config = Array.isArray(config) ? config : []
+    let self = this
     this.load(urls, config, function (images) {
         if (config.length == 0) {
             //不指定拼接配置时,默认按顺序显示所有图片
@@ -148,21 +153,16 @@ ImageLoader.prototype.combine = function (urls, canvas, config, onSuccess, onErr
                 let image = images[i]
                 if (image.loaded === true) {
                     if (image.wide === true) {
-                        //大图一分为二
-                        config.push({
-                            i: i + 1,
-                            x: 0,
-                            y: 0,
-                            w: image.width * 0.55,
-                            h: image.height
-                        })
-                        config.push({
-                            i: i + 1,
-                            x: image.width * 0.45,
-                            y: 0,
-                            w: image.width * 0.55,
-                            h: image.height
-                        })
+                        //大图按切割规则
+                        for (let j = 0; j < self.split.length; j++) {
+                            config.push({
+                                i: i + 1,
+                                x: image.width * self.split[j].x,
+                                y: image.height * self.split[j].y,
+                                w: image.width * self.split[j].w,
+                                h: image.height * self.split[j].h
+                            })
+                        }
                     } else {
                         //小图原样显示
                         config.push({
@@ -206,27 +206,27 @@ ImageLoader.prototype.combine = function (urls, canvas, config, onSuccess, onErr
         context.fillRect(0, 0, canvas.width, canvas.height);
         //绘画到画布
         //高度位置与图片配置对应关系
-        let array = []
-        let top = 0
+        let layout = []
         let height = 0
         for (let i = 0; i < config.length; i++) {
             let image = images[config[i] - 1]
-            if (image != undefined && image.loaded === true) {
+            if (image.loaded === true) {
                 context.drawImage(image, config[i].x, config[i].y, config[i].w, config[i].h, 0, height, config[i].w * ratio, config[i].h * ratio)
-                height += config[i].h * ratio
-                top += config[i].h
-                array.push({
-                    offsetY: top,
+                layout.push({
+                    ratio: ratio,
+                    top: height,
+                    bottom: height + config[i].h * ratio,
                     config: config[i]
                 })
+                height += config[i].h * ratio
             }
         }
-        onSuccess(array)
+        onSuccess(layout)
     }, onError)
 }
 
 ImageLoader.prototype.merge = function (urls, canvas, config, onSuccess, onError) {
-    this.combine(urls, canvas, config, function (array) {
+    this.combine(urls, canvas, config, function (layout) {
         let image = new Image()
         image.onload = function () {
             onSuccess(image)
@@ -234,7 +234,7 @@ ImageLoader.prototype.merge = function (urls, canvas, config, onSuccess, onError
         image.onerror = function () {
             onError('image merge error')
         }
-        image.config = array
+        image.layout = layout
         image.src = canvas.toDataURL()
     }, null)
 }