前端使用 Konva 實現可視化設計器(9)- 另存爲SVG

請大家動動小手,給我一個免費的 Star 吧~

大家如果發現了 Bug,歡迎來提 Issue 喲~

github源碼

gitee源碼

示例地址

另存爲SVG

這一章增強了另存爲的能力,實現“另存爲SVG”,大概是全網唯一的實例分享了吧。

靈感來源:react-konva-custom-context-canvas-for-use-with-canvas2svg

大神提到了 canvas2svg,表達了可以通過創建一個 canvas2svg 的實例,作爲 CanvasRenderingContext2D 替換了 Konva 原有 canvas 的 CanvasRenderingContext2D,並使其 layer 重繪,canvas2svg 的實例藉此監聽 canvas 的動作,轉換成 Svg 動作,最終生成 svg 內容。

不過,大神的例子,並沒有說明如何處理並導出圖片節點

通過測試大神的例子,並觀察導出的 svg xml 特點,以下是基本實現思路和注意事項:
1、必須通過替換 layer 的 context 實現,通過 stage 是無效的。
2、導出的 svg xml,圖片節點將以 svg 的 image 節點存在。
3、svg 圖片素材節點的 xlink:href 以 blob: 鏈接定義。
4、其它圖片素材節點的 xlink:href 是以一般路徑鏈接定義。
5、通過正則表達式提取圖片素材節點鏈接。
6、fetch svg 圖片素材節點鏈接,獲得 svg xml 文本。
7、fetch 其它圖片素材節點,獲得 blob 後,轉換爲 base64 鏈接。
8、替換 canvas2svg 導出的 svg xml 內的 svg 圖片素材節點爲內嵌 svg 節點(xml)。
9、替換 canvas2svg 導出的 svg xml 內的其它圖片素材節點的 xlink:href 爲 base64 鏈接
10、導出替換完成的 svg xml。

關鍵邏輯

功能入口

主要是 canvas2svg 的使用,獲得原始的 rawSvg xml 內容:

  // 獲取Svg
  async getSvg() {
    // 獲取可視節點和 layer
    const copy = this.getView()
    // 獲取 main layer
    const main = copy.children[0] as Konva.Layer
    // 獲取 layer 的 canvas context
    const ctx = main.canvas.context._context

    if (ctx) {
      // 創建 canvas2svg
      const c2s = new C2S({ ctx, ...main.size() })
      // 替換 layer 的 canvas context
      main.canvas.context._context = c2s
      // 重繪
      main.draw()

      // 獲得 svg
      const rawSvg = c2s.getSerializedSvg()
      // 替換 image 鏈接
      const svg = await this.parseImage(rawSvg)

      // 輸出 svg
      return svg
    }
    return Promise.resolve(
      `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="720"></svg>`
    )
  }

替換 image 鏈接方法

根據 xlink:href 鏈接的特點,通過正則表達式提取,用於後續處理:


  // 替換 image 鏈接
  parseImage(xml: string): Promise<string> {
    return new Promise((resolve) => {
      // 找出 blob:http 圖片鏈接(目前發現只有 svg 是)
      const svgs = xml.match(/(?<=xlink:href=")blob:https?:\/\/[^"]+(?=")/g) ?? []
      // 其他圖片轉爲 base64
      const imgs = xml.match(/(?<=xlink:href=")(?<!blob:)[^"]+(?=")/g) ?? []

      Promise.all([this.parseSvgImage(svgs), this.parseOtherImage(imgs)]).then(
        ([svgXmls, imgUrls]) => {
          // svg xml
          svgs.forEach((svg, idx) => {
            // 替換
            xml = xml.replace(
              new RegExp(`<image[^><]* xlink:href="${svg}"[^><]*/>`),
              svgXmls[idx].match(/<svg[^><]*>.*<\/svg>/)?.[0] ?? '' // 僅保留 svg 結構
            )
          })

          // base64
          imgs.forEach((img, idx) => {
            // 替換
            xml = xml.replace(`"${img}"`, `"${imgUrls[idx]}"`)
          })

          // 替換完成
          resolve(xml)
        }
      )
    })
  }

替換 svg blob: 鏈接

批量 fetch svg blob: 鏈接,獲得 xml 內容:

  // 替換 svg blob: 鏈接
  parseSvgImage(urls: string[]): Promise<string[]> {
    return new Promise((resolve) => {
      if (urls.length > 0) {
        Promise.all(urls.map((o) => fetch(o))).then((rs: Response[]) => {
          // fetch

          // 替換爲 svg 嵌套
          Promise.all(rs.map((o) => o.text())).then((xmls: string[]) => {
            // svg xml
            resolve(xmls)
          })
        })
      } else {
        resolve([])
      }
    })
  }

替換其他 image 鏈接

批量 fetch 圖片鏈接,獲得 base64 鏈接:

  // blob to base64 url
  blobToBase64(blob: Blob, type: string): Promise<string> {
    return new Promise((resolve) => {
      const file = new File([blob], 'image', { type })
      const fileReader = new FileReader()
      fileReader.readAsDataURL(file)
      fileReader.onload = function () {
        resolve((this.result as string) ?? '')
      }
    })
  }

  // 替換其他 image 鏈接
  parseOtherImage(urls: string[]): Promise<string[]> {
    return new Promise((resolve) => {
      if (urls.length > 0) {
        Promise.all(urls.map((o) => fetch(o))).then((rs: Response[]) => {
          // fetch

          // 替換爲 base64 url image
          Promise.all(rs.map((o) => o.blob())).then((bs: Blob[]) => {
            // blob
            Promise.all(bs.map((o) => this.blobToBase64(o, 'image/*'))).then((urls: string[]) => {
              // base64
              resolve(urls)
            })
          })
        })
      } else {
        resolve([])
      }
    })
  }

過程示例

通過 canvas2svg 獲得原始的 rawSvg xml 內容:

image

<?xml version="1.0" encoding="utf-8"?>

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="428" height="448">
  <defs/>
  <g>
    <rect fill="#FFFFFF" stroke="none" x="0" y="0" width="428" height="448"/>
    <g transform="matrix(1,0,0,1,69,80)">
      <!-- gif 圖片 -->
      <image width="200" height="200" preserveAspectRatio="none" xlink:href="data:image/png;base64,略..."/>
      <g transform="translate(0,0)"/>
    </g>
    <g transform="matrix(1,0,0,1,17,22)">
      <!-- png 圖片 -->
      <image width="64" height="64" preserveAspectRatio="none" xlink:href="/src/assets/img/png/2.png"/>
      <g transform="translate(0,0)"/>
    </g>
    <g transform="matrix(1,0,0,1,228,232)">
      <!-- svg 圖片 -->
      <image width="200" height="200" preserveAspectRatio="none" xlink:href="blob:http://localhost:5173/da9ddae7-2ac7-47fb-99c0-e7171aa41655"/>
      <g transform="translate(0,0)"/>
    </g>
  </g>
</svg>

替換之後:

<?xml version="1.0" encoding="utf-8"?>

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="428" height="448">
  <defs/>
  <g>
    <rect fill="#FFFFFF" stroke="none" x="0" y="0" width="428" height="448"/>
    <g transform="matrix(1,0,0,1,69,80)">
      <!-- gif 圖片 base64 -->
      <image width="200" height="200" preserveAspectRatio="none" xlink:href="data:image/*;base64,略..."/>
      <g transform="translate(0,0)"/>
    </g>
    <g transform="matrix(1,0,0,1,17,22)">
      <!-- png 圖片 base64 -->
      <image width="64" height="64" preserveAspectRatio="none" xlink:href="data:image/*;base64,略..."/>
      <g transform="translate(0,0)"/>
    </g>
    <g transform="matrix(1,0,0,1,228,232)">
      <!-- svg 內嵌 -->
      <svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1">
        <path d="M512 0c282.763636 0 512 229.236364 512 512S794.763636 1024 512 1024 0 794.763636 0 512 229.236364 0 512 0z m0 11.636364C235.659636 11.636364 11.636364 235.659636 11.636364 512s224.023273 500.363636 500.363636 500.363636 500.363636-224.023273 500.363636-500.363636S788.340364 11.636364 512 11.636364z m-114.781091 683.927272c38.388364 6.632727 63.767273 22.853818 103.133091 61.556364l7.563636 7.528727 19.502546 19.921455c4.736 4.770909 9.262545 9.216 13.637818 13.370182l6.434909 6.004363c1.047273 0.965818 2.094545 1.908364 3.141818 2.839273l6.132364 5.352727c30.196364 25.728 53.946182 35.735273 87.226182 36.398546 69.992727 1.361455 119.936-22.027636 150.621091-70.272l2.094545-3.397818 9.972364 6.004363c-32.756364 54.318545-87.354182 80.779636-162.909091 79.290182-41.262545-0.814545-68.817455-14.650182-107.333818-50.583273l-6.714182-6.376727-6.946909-6.818909-7.226182-7.272727-15.709091-16.069819-7.284364-7.26109c-37.922909-37.329455-61.777455-52.596364-97.314909-58.740364-67.397818-11.659636-122.705455 10.24-166.725818 66.106182l-2.792727 3.607272-9.262546-7.028363c47.045818-61.940364 107.578182-86.807273 180.759273-74.158546z"/>
      </svg>
      <g transform="translate(0,0)"/>
    </g>
  </g>
</svg>

關於 gif,實測內嵌於 svg 中是無法顯示的,現在除了 svg 圖片素材節點,其它圖片素材統一按靜態圖片處理。

image

磁貼

增加了對 stage 邏輯邊界的磁貼:
image

其它調整

staget 邏輯區域

原來 stage 的邏輯區域和比例尺的區域是重疊一致的(大小一致,默認根據比例尺大小對齊 0 點而已),實在有點變扭,可能會讓人產生疑惑。
現已經調整 stage 的邏輯區域即爲默認可視區域(區別可以觀察紅色虛線框的改變)。
順便使得預覽框的交互優化的更符合直覺。

官方的 API 的 Bug

Bug: 恢復 JSON 時候,如果存在已經被放大縮小點元素,點擊選擇無效
原因不詳,Hack 了一下,暫時可以消除影響。

接下來,計劃實現下面這些功能:

  • 對齊效果
  • 連接線
  • 等等。。。

是不是值得更多的 Star 呢?勾勾手指~

源碼

gitee源碼

示例地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章