在浏览器上显示TIF图像

在实际的业务中,我们需要在浏览器中对 TIF 文件的某个区域进行选择,所以需要展示 TIF 文件在浏览器中。不过浏览器并不能直接支持 TIF 格式,所以我们需要做相应的处理。

# 寻找合适的解析库

图像处理是一个很复杂的事情,如果别人有实现就别自己造轮子了。这里我推荐UTIF.js (opens new window),这位作者实现了网页版图片编辑器。所以质量上还是很高的。

# 导入图片二进制数据

我们需要把图片导入成二进制数据,我们使用fetch来获取图片二进制数据,fetch 本身就可以返回二进制数据,大致代码如下:

  fetchImage() {
    fetch("bali.tif")
      .then(response => {
        if (!response.ok) {
          throw new Error("Network response was not ok");
        }
        return response.arrayBuffer();
      })
      .then(data => {
        const ifds = UTIF.decode(data);
        UTIF.decodeImage(data, ifds[0]);
        const rgba = UTIF.toRGBA8(ifds[0]);
        const widh = ifds[0].width;
        const height = ifds[0].height;
        const canvas = document.createElement("canvas");
        canvas.width = widh;
        canvas.height = height;
        this.width = widh;
        this.height = height;
        const ctx = canvas.getContext("2d");
        const imgData = new ImageData(
          new Uint8ClampedArray(rgba.buffer),
          widh,
          height
        );
        ctx?.putImageData(imgData, 0, 0);
        canvas.toBlob(blob => {
          this.imgData = URL.createObjectURL(blob);
        });
      });
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

导入的二进制数据,使用UTIF去解码二进制数据。拿到图片的基本信息之后,创建相应的 canvas 画布,把图片数据加载进 canvas 之后可以创建ObjectURL就可以使用img tag 来显示。

# 总结

处理 TIF 文件本身并不是很复杂,在于找到合适的方法。完整的演示代码在vue-tif (opens new window)

# 参考

UTIF.js (opens new window)

vue-tif (opens new window)

简单多边形的判定

在实际工作中,需要在照片上选定一个范围,这个范围是个多边形,并且是个简单多边形,我们需要判定是否是个合法的简单多边形,主要判定的是任意两边不能交叉。

# 判断两条线段相交

我们可以借助向量的知识来判断两个线段是否相交。二维向量的叉乘(cross product)的几何意义是以两向量为邻边的平行四边形的面积。此外,定义两个向量 a, b。 当 aXb < 0, b 对应的线段,在 a 的顺时针方向。当 aXb = 0时, a 与 b 共线。当 aXb > 0,b 在 a 的逆时针方向。 如果两条线段相交,那必然一条线段的终点和起点,在另外一条线段的两侧。

# 判断多边形两边是否相交

简单并且暴力的方法是检测任意两边是否有交点,在复杂度不高的情况下可以使用这种方法。但是显然是有更优解的,目前比较有名的两个算法是 The Bentley-Ottmann AlgorithmThe Shamos-Hoey Algorithm。算法的细节请查看下方的参考,我就不详细描述了。

# 简单多边形判定的实现

export interface Point {
  x: number;
  y: number;
}

export interface Line {
  start: Point;
  end: Point;
}

function samePoint(p1: Point, p2: Point) {
  if (p1.x === p2.x && p1.y === p2.y) {
    return true;
  } else {
    return false;
  }
}

function signedArea(p1: Point, p2: Point, p3: Point) {
  return (p1.x - p3.x) * (p2.y - p3.y) > (p2.x - p3.x) * (p1.y - p3.y);
}

function intersectLine(l1: Line, l2: Line) {
  // consecutive edge return false
  if (
    samePoint(l1.start, l2.start) ||
    samePoint(l1.start, l2.end) ||
    samePoint(l1.end, l2.start) ||
    samePoint(l1.end, l2.end)
  ) {
    return false;
  }
  return (
    signedArea(l1.start, l2.start, l2.end) !==
      signedArea(l1.end, l2.start, l2.end) &&
    signedArea(l1.start, l1.end, l2.start) !==
      signedArea(l1.start, l1.end, l2.end)
  );
}

export function intersectPolygon(points: Array<Point>) {
  const len = points.length;
  for (let i = 0; i < len - 1; i++) {
    for (let j = i + 1; j < len; j++) {
      const l1: Line = {
        start: points[i],
        end: points[(i + 1) % len]
      };
      const l2: Line = {
        start: points[j],
        end: points[(j + 1) % len]
      };

      if (intersectLine(l1, l2)) {
        return true;
      }
    }
  }

  return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

# 总结

简单多边形判定的本质是任意两边是否相交,如果是邻边的话就直接跳过。在复杂度不高的情况下,可以直接使用遍历的方法来实现。

# 参考

Intersections of a Set of Segments (opens new window)

Line Segment Intersection Algorithm (opens new window)

shamos-hoey (opens new window)

Vue中组件的设计

Vue 中组件设计经常遇到复合组件的问题,例如下拉菜单,下拉菜单中我们不仅要实现select元素的功能,还要实现option元素的功能,这样才是一个完整的下拉菜单组件。

# 复合组件中的数据相互访问

复合组件中的数据访问是设计组件的一个重要议题,设计不好会严重影响组件的灵活性。我们拿 Element UI 的组件来举个例子:

<el-select v-model="value" placeholder="请选择">
  <el-option
    v-for="item in options"
    :key="item.value"
    :label="item.label"
    :value="item.value"
  >
  </el-option>
</el-select>
1
2
3
4
5
6
7
8
9

这是一个经典的例子,但是实际使用过程中,我们可能会二次封装组件,例如我们需要给el-option加一些事件或者样式,那我们的代码会变成这样:

<el-select v-model="value" placeholder="请选择">
  <styled-el-option
    v-for="item in options"
    :key="item.value"
    :label="item.label"
    :value="item.value"
  >
  </styled-el-option>
</el-select>
1
2
3
4
5
6
7
8
9

styled-el-option 是我们二次封装的组件,里面包含了el-option组件。如果我们使用$parentel-option组件中访问el-select组件的话,二次封装的情况下就会出错。 因为el-option的父组件不再是el-select,而是styled-el-option。正确的数据访问方式是使用provideinject

# 子组件访问父组件

首先我们需要在父附件中这样定义:

provide: function () {
  return {
    'select': this
  }
}
1
2
3
4
5

然后在子组件中使用inject:

inject: ['select'];
1

这样子组件就可以访问父组件了,使用provide的好处是不用担心复合组件中的再封装问题,Vue 会自动向上查找。

# 父组件访问子组件

Vue 中并没有提供非常方便的查找子组件的功能, 所以目前的方法是在父组件中存储子组件的实例。

父组件中定义数据:

data: function () {
  return {
    options: []
  }
}
1
2
3
4
5

子组件通过inject可以访问父组件,所以:

created() {
  this.select.options.push(this);
}
1
2
3

在创建组件的时候同时传入父组件的data字段中。

# 总结

灵活的设计组件才能在实际开发中更方便的使用,一些第三方组件开发的时候没有考虑到二次封装的可能性,直接通过$parent$children进行数据的更新。导致可用性大大降低。

# 参考

Dependency Injection (opens new window)