Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(extension): ensure dpr>=1 when exporting images(#1222) #1274

Merged
merged 1 commit into from
Aug 14, 2023

Conversation

wbccb
Copy link
Contributor

@wbccb wbccb commented Aug 11, 2023

close #1222

问题发生的原因

svg基础知识

SVG坐标系有两个重要的属性:viewportviewBox

<div style="width: 200px; height: 200px">
    <div>没有使用viewBox进行缩放:svg的viewport < 内部元素的宽高</div>
    <svg width="100%" height="100%" style="background:#889cee">
        <circle cx="100" cy="100" r="200" fill="#ff0036"></circle>
    </svg>
</div>

<div style="width: 200px; height: 200px">
    <div>没有使用viewBox进行缩放:svg的viewport > 内部元素的宽高 </div>
    <svg width="100%" height="100%" style="background:#889cee">
        <circle cx="50" cy="50" r="50" fill="#ff0036"></circle>
    </svg>
</div>

<div style="width: 200px; height: 200px">
    <div>使用内部元素的viewBox进行缩放:svg的viewport > 内部元素的宽高</div>
    <svg width="100%" height="100%" style="background:#889cee" viewBox="0 0 100 100">
        <circle cx="50" cy="50" r="50" fill="#ff0036"></circle>
    </svg>
</div>

当运行上面的代码时,可以发现,如果没有viewBox,那么svg内部的元素是不会根据svg的宽高进行自适应的

test.png

drawImage

导出图片流程分析

回到这个问题中,最终导出图片的步骤是:

  1. 将目前的svg拼接然后放入到new Image()中,即
var img = new Image();
var svg2Img = "data:image/svg+xml;charset=utf-8," + new XMLSerializer().serializeToString(copy);
var imgSrc = svg2Img
    .replace(/\n/g, '')
    .replace(/\t/g, '')
    .replace(/#/g, '%23');
img.src = imgSrc;
  1. new Image()绘制到canvas中
ctx.drawImage(img, 0, 0);
  1. 使用canvas的API导出图片
var imgURI = canvas
    .toDataURL('image/png')
    .replace('image/png', 'image/octet-stream')

问题关键点分析

从上面的分析我们可以知道,本质就是绘制一个svg到canvas上面

当我们调试svg,我们可以看到如下的结构

<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" name="canvas-overlay" class="lf-canvas-overlay lf-drag-able">
    <g>...</g>
    <foreignobject>...</foreignobject>
</svg>

这里我们可以看出,是没有使用viewBox进行缩放,换句话说,由于svg的viewport设置的是width="100%" height="100%",因此如果绘制时装载svg的容器小于svg内部的元素,也就是canvas(因为我们要进行drawImage)的大小无法保证canvas.width/canvas.height>svg内部元素宽/高,那么绘制将会是缺失的svg图形

而缩放浏览器比例时,经过调试可以发现,window.devicePixelRatio也会随着变小,最终导致
canvas.width < canvas.style.width

canvas.style.width = `${bboxWidth}px`;
canvas.style.height = `${bboxHeight}px`;
canvas.width = bboxWidth * dpr + 80;
canvas.height = bboxHeight * dpr + 80;

那svg内部元素的大小是多少呢?

从下面代码可以看出,svg内部元素的大小就是bboxWidth/bboxHeight

const base = this.lf.graphModel.rootEl.querySelector('.lf-base');
const bbox = (base as Element).getBoundingClientRect();
const layout = document
    .querySelector('.lf-canvas-overlay')
    .getBoundingClientRect();
const offsetX = bbox.x - layout.x;
const offsetY = bbox.y - layout.y;
const { graphModel } = this.lf;
const { transformModel } = graphModel;
const { SCALE_X, SCALE_Y, TRANSLATE_X, TRANSLATE_Y } = transformModel;
// offset值加10,保证图形不会紧贴着下载图片的左边和上边
(copy.lastChild as SVGGElement).style.transform = `matrix(1, 0, 0, 1, ${(-offsetX + TRANSLATE_X) * (1 / SCALE_X) + 10
    }, ${(-offsetY + TRANSLATE_Y) * (1 / SCALE_Y) + 10})`;
const bboxWidth = Math.ceil(bbox.width / SCALE_X);
const bboxHeight = Math.ceil(bbox.height / SCALE_Y);

因此,当window.devicePixelRatio变小到window.devicePixelRatio <1时,会导致canvas.width < canvas.style.width=svg内部元素的大小,从而导致svg绘制到canvas上的图形缺失

问题非关键点分析

按照上面svg基础知识的分析,由于没有设置viewBox,因此当canvas.width远远大于canvas.style.width=svg内部元素的大小时,会导致svg绘制到canvas上的图形占的比例非常小,那么为什么放大浏览器比例后(window.devicePixelRatio变得非常大)也仍然能导出非常合适宽高的图片呢?

这是因为当canvas.widthcanvas.style.width不相同时,会进行缩放

在导出图片代码中,为了解决绘制模糊问题,进行了canvas.width = bboxWidth * dpr配合ctx.scale(dpr, dpr)的逻辑

比如dpr=3时,计算出来的svg内部元素的大小为

  • svg内部元素宽 = canvas.style.width = 600
  • svg内部元素高 = canvas.style.height = 300

那么对应的

  • canvas.width = 1800
  • canvas.height = 900
  • ctx.scale(3, 3)

当没有使用ctx.scale(3, 3)时,svg内部元素绘制到canvas只占了1/3空间(svg元素全部都能绘制出来),由于canvas.widthcanvas.style.width不相同时,会进行缩放,因此整体canvas会缩小为原来的1/3大小,此时绘制上去的svg为:

  • svg内部元素宽 = 200
  • svg内部元素高 = 100
  • canvas展示出来的宽 = canvas.style.width = 600
  • canvas展示出来的高 = canvas.style.height = 300

此时触发了ctx.scale(3, 3),会将绘制的svg放大,此时绘制上去的svg为:

  • svg内部元素宽 = 600
  • svg内部元素高 = 300

刚好适配上目前canvas展示出来的宽和高

既然最终svg绘制的宽高都会适配目前canvas展示出来的宽和高,那为什么dpr<1时就会缺失呢?

那是因为当dpr<1时,canvas.width < svg内部元素的宽,此时绘制上去的就是缺失的svg,这个时候再按照上面流程,触发

  • canvas.widthcanvas.style.width不相同时,会进行等比例的放大(因为dpr<1
  • ctx.scale(dpr, dpr)进行内部元素的缩小(因为dpr<1

也是把绘制上去的缺失的svg部分等比例的放大+scale缩小,简单点说,就是缺少一半的图形,再怎么放大或者缩小,缺失还是缺失,无法改变

解决方法

从上面的分析中,我们已经明白了问题的关键点,就是canvas.width不能小于svg内部元素的大小,那么就有对应的两种解决方法:

  • 方法1: 强制dpr>=1,这样就能永远保证canvas.width > canvas.style.width = svg内部元素的大小
  • 方法2: 不管dpr,使用viewBox进行svg内部元素的缩放,去自适应适配canvas.width的大小

方法2

直接对svg元素设置属性viewBox

缺点:当window.devicePixelRatio如果非常非常非常小的时候,canvas.width也会变得非常小,为了适配canvas.width,最终导出的图片有效内容占据的地方也会非常小

    var bboxWidth = Math.ceil(bbox.width / SCALE_X);
    var bboxHeight = Math.ceil(bbox.height / SCALE_Y);
    // width,height 值加40,保证图形不会紧贴着下载图片的右边和下边
    canvas.style.width = bboxWidth + "px";
    canvas.style.height = bboxHeight + "px";
    window.bboxWidth = bboxWidth;
    window.bboxHeight = bboxHeight;
    canvas.width = bboxWidth * dpr + 80;
    canvas.height = bboxHeight * dpr + 80;
// +80是为了保证图形不会紧贴着下载图片的右边和下边
+   copy.setAttribute("viewBox", `0 0 ${bboxWidth+80} ${bboxHeight+80}`); 

方法1

直接判断是否小于1,强制转化为1

let dpr = window.devicePixelRatio || 1;
if (dpr < 1) {
    dpr = 1;
}

总结

最终我采用了方法1进行代码的提交,主要有两点:

  • window.devicePixelRatio如果非常非常非常小的时候,canvas.width也会变得非常小,为了适配canvas.width,最终导出的图片有效内容占据的地方也会非常小
  • canvas.width = bboxWidth * dpr配合ctx.scale(dpr, dpr)是为了解决绘制模糊,本质是为了解决dpr>1高分辨率导致的绘制模糊问题,当dpr<1时并不存在这种绘制模糊问题,因此没必要进行这种canvas.width = bboxWidth * dpr配合ctx.scale(dpr, dpr)逻辑,即当dpr<1时直接就dpr=1

@wumail wumail merged commit ac3a774 into didi:master Aug 14, 2023
@wbccb wbccb deleted the fix-1222 branch August 14, 2023 05:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug report] 缩放浏览器到80%, 通过导出base64/下载图片, 图片会出现显示不全的情况
2 participants