Go言語を用いてdxfファイルを一括で画像化する

f:id:vasilyjp:20190129102917j:plain

前書き

こんにちは、スマートファクトリー向け制御ソフトウェア開発チームの高石(@ksk_taka)です。 本記事では、アパレル業界や製造業界など、CADを取り扱う業界で広く使われているdxfファイルを 一括で画像ファイルに変換する手法について記載します。

dxfファイルとは

そもそも dxfファイルとは何ぞや? という方のために簡単に説明をします。
dxfファイルはCAD間を仲介する中間ファイルとして使うことを目的としたファイルです。

例えば機械設計をする際に3D-CADが利用されますが、よく使われる3D-CADソフトとして、以下のものがあります。

  • CATIA
  • SolidWorks
  • Creo Parametric

これらのそれぞれのソフトで作られる図面データは別々のファイル形式(拡張子) を持っており、基本的に互換性がありません。

アパレル業界で用いられるCADソフトも同様で、以下の様な異なるCADソフトにて生成される図面データには互換性がありません。

  • クレアコンポ
  • AGMS

このままでは一方のCADソフトで生成した図面データを、別のCADソフトを持った人が開こうとすると「開かない!」という状況になってしまいます。

そんなの困る! ということで、必要となるのが中間ファイルという存在です。

dxfファイルは恐らく現在最も広く使われているであろう中間ファイルで、使用するCADソフトに関わらず開くことができる図面データになります。

f:id:vasilyjp:20190130164205p:plain

dxfファイルを用いることで、異なるCADソフトを利用している人同士でも、図形データのやり取りが可能になります。
※但し、「各社独自のCADオブジェクト」などはdxfでは再現できません。やり取りの際には注意が必要です。

dxfファイルを画像化する目的

上記のように便利なdxfファイルですが、万能なわけではありません。 例えば以下のような状況では、もっと汎用的なデータ形式の方が望ましいと言えます。

  • CADソフトを持たない人に図面データを渡したい場合
  • Web上や社内のドキュメントファイルなどに図面(絵柄)を貼付したい場合

そのような状況に対応するため、dxfファイルに含まれる図面データを一括で画像化するソフトをGo言語で実装しました。

dxfファイルの構造

dxfファイルはテキスト形式のファイルです。その為、テキストエディタで簡単に内容を確認できます。
ファイル内のテキストは2行で1組となっており、1行目はグループコード、2行目はグループコードに応じて文字列数値などが入ります。
dxfファイルの例を以下に記載します。

  0         
SECTION     //SECTION開始
  2
BLOCKS      //BLOCKS SECTION開始
  0
BLOCK       //1個目のBLOCKの開始
  8
1           //1個目のBLOCKの階層
  2
1_BlockName //1個目のBLOCKの名前
 10 
0           //1個目のBLOCKのX座標
 20
0           //1個目のBLOCKのY座標
  0
POLYLINE      //1個目のBLOCK内の初めのENTITYデータ(POLYLINE:連続した頂点で描画される図形)
・
・
・
  0
LINE       //1個目のBLOCK内の2個目のENTITYデータ(LINE:2点を結ぶ線分で描画される図形) 
・
・
・
  0
ENDBLK      //1個目のBLOCK項目の終了
  0
BLOCK       //2個目のBLOCK項目の開始
・
・
・
  0
ENDBLK      //n個目のBLOCK項目の終了
  0
ENDSEC      //BLOCKS SECTION終了
  0
EOF         //FILEの終了

今回はひとまず、使用頻度の高い POLYLINELINE の2つの図形を描画する機能を実装します。

実装方法

ここからは具体的な実装について記載していきます。 今回、ソフトウェアを実装する上で肝となるのは以下の機能です。

  • dxfファイルを1行ずつ読み取る機能
  • 読み取ったデータを構造体に格納する機能
  • 構造体の格納データに応じて画像を描画する機能

1つずつ見ていきましょう。

事前準備

まず、事前準備から。 目標の機能を実装するにあたり、Go言語のパッケージは以下のものを使用します。

import (
    "bufio"
    "image"
    "io/ioutil"
    "os"
    "path/filepath"
    "regexp"
    "runtime"
    "strconv"
    "sync"
    "time"

    "github.com/llgcode/draw2d/draw2dimg"

    "image/color"

    "fmt"

    "golang.org/x/text/encoding/japanese"
    "golang.org/x/text/transform"
)

更に、読み取ったdxfファイルから各要素を格納していくための構造体を準備しておきましょう。

// Section is a top level group.
type Section struct {
    Blocks   []Block
    Entities []Entity
    Headers  [][]string
    Tables   [][]string
}

// Block is a second level group in Section.
type Block struct {
    Name      string
    LayerName string
    BlockType string
    X         float64
    Y         float64
    Entity    []Entity
}

// Entity is a third level group in Section or Entities.
type Entity struct {
    TYPE string
    Name string
    FULL [][]string
}

dxfファイルを1行ずつ読み取る

まずはdxfファイルを1行ずつ読み取り、Sliceデータとして返す関数を実装します。

func getFileStream(inputpath string, fileName string) (data [][][]string) {
    var row [][]string
    var scangroup [][][]string

    input, err := os.Open(filepath.Join(inputpath, fileName))
    if err != nil {
        // Openエラー処理
        panic(err)
    }
    defer input.Close()

    scangroup = nil
    //dxfファイル ロード開始
    sc := bufio.NewScanner(transform.NewReader(input, japanese.ShiftJIS.NewDecoder()))

    for i := 0; sc.Scan(); i++ {
        if err := sc.Err(); err != nil {
            // エラー処理
            break
        }

        if i != 0 && sc.Text() == "  0" {
            scangroup = append(scangroup, row) //区切り文字で塊を作る
            row = [][]string{}                 //塊を作ったら初期化
        }

        //2行で1要素分なので判別しやすいようにまとめてSlice化
        gkey := sc.Text()
        sc.Scan()
        gvalue := sc.Text()
        row = append(row, []string{gkey, gvalue})
    }

    scangroup = append(scangroup, row)
    row = [][]string{}

    return scangroup
}

ファイルパスファイル名 の文字列が与えられると、該当ファイルを1行ずつ読み込む関数です。
上でお伝えした通り、dxfファイルは2行で1組となっているため、1組分をまとめてSliceにしています。

データを構造体に格納する

続いて、読み取ったデータを各種構造体に格納する処理を実装していきましょう。

func makeSection(data [][][]string) (sec Section) {
    var isBlocks bool
    var isEntities bool

    var blk Block
    var ent Entity
    var copyData [][][]string
    var copyGroup [][]string
    var copyRows []string

    for _, g := range data {
        for _, r := range g {
            copyRows = make([]string, 2)
            copy(copyRows, r)
            copyGroup = append(copyGroup, copyRows)
        }

        copyData = append(copyData, copyGroup)
        copyGroup = [][]string{}
    }

    sec = Section{}
    for _, group := range copyData {
        if group[0][0] == "  0" {
            switch group[0][1] {
            case "SECTION":
                //SECTIONの処理
                for _, rows := range group {
                    if rows[0] == "  2" {
                        switch rows[1] {
                        case "BLOCKS":
                            isBlocks = true
                            isEntities = false
                        case "ENTITIES":
                            isBlocks = false
                            isEntities = true
                        case "HEADER":
                            isBlocks = false
                            isEntities = false
                            sec.Headers = group
                        case "TABLES":
                            isBlocks = false
                            isEntities = false
                            sec.Tables = group
                        default:
                        }
                    }
                }
            case "VERTEX", "LINE", "POLYLINE", "SEQEND", "TEXT": //図形データ
                ent = Entity{}
                ent.TYPE = group[0][1]
                ent.FULL = group
                if isBlocks {
                    blk.Entity = append(blk.Entity, ent)
                } else if isEntities {
                    sec.Entities = append(sec.Entities, ent)
                }
            case "BLOCK"://画像は1BLOCKにつき1枚
                blk = Block{}
                //BLOCKの処理
                for _, rows := range group {
                    switch rows[0] {
                    case "  8":
                        blk.LayerName = rows[1]
                    case "  2":
                        blk.Name = rows[1] //file名に使用
                    case " 70":
                        blk.BlockType = rows[1]
                    case " 10":
                        blk.X, _ = strconv.ParseFloat(rows[1], 64)
                    case " 20":
                        blk.Y, _ = strconv.ParseFloat(rows[1], 64)
                    default:
                    }
                }
            case "ENDBLK": //BLOCKの終わりを示す
                sec.Blocks = append(sec.Blocks, blk)
            case "EOF": //fileの終わりを示す
            default:
            }
        }
    }
    return sec
}

先ほど作成したデータを入力として、各構造体にデータを格納していくコードです。
最終的に全てのSECTIONをまとめたものがSliceで返されます。

画像化する

続いて、構造体に格納されたデータを用いて画像を描画する機能を実装します。

func exportPNGperBlk(sec Section, dir string) {
    const PrefixSEQEND = -1
    var vertexX, vertexY float64
    var lstartX, lendX, lstartY, lendY float64
    var maxX, maxY, minX, minY float64
    var exportFlag bool
    var vertexs [][]float64
    var lines [][]float64
    var name string
    var i int
    exportFlag = false

    for _, bl := range sec.Blocks {
        //file名にはディレクトリ名とブロック名を使用する
        name = dir + "-" + bl.Name
        exportFlag = true

        for _, en := range bl.Entity {
            if en.TYPE == "SEQEND" { //線の切れ目
                vertexs = append(vertexs, []float64{PrefixSEQEND, PrefixSEQEND})
                continue
            }
            if en.TYPE == "VERTEX" { //POLYLINEで頂点を繋ぐ線分を描画する際に使用
                for _, rows := range en.FULL {
                    switch rows[0] {
                    case " 10": //頂点のX座標
                        vertexX, _ = strconv.ParseFloat(rows[1], 64)
                        if vertexX > maxX {
                            maxX = vertexX
                        }
                        if vertexX < minX {
                            maxX = vertexX
                        }
                    case " 20": //頂点のY座標
                        vertexY, _ = strconv.ParseFloat(rows[1], 64)
                        if vertexY > maxY {
                            maxY = vertexY
                        }
                        if vertexY < minY {
                            minY = vertexY
                        }
                    }
                }
                //"vertexs"に、頂点のXY座標を格納
                vertexs = append(vertexs, []float64{vertexX, vertexY})
            }
            if en.TYPE == "LINE" { //LINEで線分描画する際に使用
                for _, rows := range en.FULL {
                    switch rows[0] {
                    case " 10": //開始地点のX座標
                        lstartX, _ = strconv.ParseFloat(rows[1], 64)
                        if lstartX > maxX {
                            maxX = lstartX
                        }
                        if lstartX < minX {
                            minX = lstartX
                        }
                    case " 11": //開始地点のY座標
                        lendX, _ = strconv.ParseFloat(rows[1], 64)
                        if lendX > maxX {
                            maxX = lendX
                        }
                        if lendX < minX {
                            minX = lendX
                        }
                    case " 20": //終了地点のX座標
                        lstartY, _ = strconv.ParseFloat(rows[1], 64)
                        if lstartY > maxY {
                            maxY = lstartY
                        }
                        if lstartY < minY {
                            minY = lstartY
                        }
                    case " 21": //終了地点のY座標
                        lendY, _ = strconv.ParseFloat(rows[1], 64)
                        if lendY > maxY {
                            maxY = lendY
                        }
                        if lendY < minY {
                            minY = lendY
                        }
                    }
                }
                //"lines"に、開始-終了地点のXY座標を格納
                lines = append(lines, []float64{lstartX, lstartY, lendX, lendY})
            }
        }

        if exportFlag {
            //画像サイズがPartsごとに異なるので毎回準備する
            img := image.NewRGBA(image.Rect(int(minX), int(minY), int(maxX+1), int(maxY+1)))
            gc := draw2dimg.NewGraphicContext(img)
            gc.SetFillColor(color.White)
            gc.SetStrokeColor(color.RGBA{0, 0, 255, 255})
            gc.Fill()

            //POLYLINE描画
            for _, vertex := range vertexs {}
                i++
                fmt.Println(len(vertexs), vertex, i)
                if vertex[0] == PrefixSEQEND && vertex[1] == PrefixSEQEND {
                    if i < len(vertexs) {
                        //線の切れ目では描画をせず座標移動だけ実施
                        gc.MoveTo(vertexs[i][0], maxY-vertexs[i][1])
                    }
                    continue
                }
                //線を描画。画像とdxfでY座標の方向が反転する点に注意
                gc.LineTo(vertex[0], maxY-vertex[1])
            }

            //LINE描画
            for _, line := range lines {
                gc.MoveTo(line[0], maxY-line[1])
                gc.LineTo(line[2], maxY-line[3])
            }
            gc.Stroke()
            gc.Close()

            //出力フォルダを作成する
            outputpath, err := filepath.Abs(filepath.Join(".", "file", "output", dir))
            if err != nil {
                panic(err)
            }
            os.Mkdir(outputpath, 0777)

            //出力フォルダを作成する
            outputpath, err = filepath.Abs(filepath.Join(".", "file", "output", dir, "png"))
            if err != nil {
                panic(err)
            }
            os.Mkdir(outputpath, 0777)

            //png画像として保存
            draw2dimg.SaveToPngFile(filepath.Join(outputpath, name+".png"), img)

            //各変数を初期化
            exportFlag = false
            vertexs = [][]float64{}
            lines = [][]float64{}
            maxX = 0
            maxY = 0
            minX = 0
            minY = 0
            i = 0
        } else {
            fmt.Println("画像化対象のパーツが見つかりません。", "パーツ名[", name, "]")
        }
    }

}

最後に、main関数を実装します。

func main() {
    // 正規表現を使って対象ファイルを設定
    rep := regexp.MustCompile("[A-Z]*[a-z]*[0-9]*.dxf")

    // Inputディレクトリがあるかどうか確認
    inputparentpath, err := filepath.Abs("./file/input/")
    if err != nil {
        panic(err)
    }

    dirs, err := ioutil.ReadDir(inputparentpath)
    if err != nil {
        panic(err)
    }

    var wg sync.WaitGroup
    cpus := runtime.NumCPU() // CPUの数
    limit := make(chan struct{}, cpus)

    // Inputディレクトリ配下の全ファイルを読み込み
    for _, dir := range dirs {
        dirName := dir.Name()
        if dir.IsDir() != true {
            continue
        }
        inputpath, err := filepath.Abs(filepath.Join(inputparentpath, dirName))
        if err != nil {
            panic(err)
        }
        files, err := ioutil.ReadDir(inputpath)
        if err != nil {
            panic(err)
        }

        // 各ディレクトリ配下の全ファイルを読み込み
        for i, file := range files {
            fileName := file.Name()
            // dxfファイルが見つからない場合
            if !rep.MatchString(fileName) {
                continue
            }

            if file.IsDir() {
                // ディレクトリはスキップ
                continue
            }

            // ファイル読み込み〜出力までは並列処理で実行
            wg.Add(1)
            go func(i int) {
                defer wg.Done()
                limit <- struct{}{}
                fmt.Println("処理開始", "[", i, "]", dirName, fileName)

                //dxfファイル読み取り
                scangroup := getFileStream(inputpath, fileName)

                //データを構造体に格納
                sec := makeSection(scangroup)

                //画像ファイル生成
                exportPNGperBlk(sec, dirName)

                fmt.Println("処理済", "[", i, "]", dirName, fileName)
                <-limit
            }(i)

            wg.Wait()
        }
    }

    // 全ての処理が完了するまで待機
    wg.Wait()

    fmt.Println("全ての処理が完了しました。キーを押すと終了します。")
    fmt.Scanln()
}

実行結果

実際に、冒頭で紹介したdxfファイル(デニムパターン)に対して本機能を適用してみました。

実行した結果出力された画像は以下の通り。
画像1:

f:id:vasilyjp:20190130162457p:plain
デニム左前身頃

画像2:

f:id:vasilyjp:20190130162518p:plain
デニム右前身頃

画像3:

f:id:vasilyjp:20190130162155p:plain
デニム左後身頃
画像4:
f:id:vasilyjp:20190130162237p:plain
デニム右後身頃

画像5:

f:id:vasilyjp:20190130162728p:plain
デニム後ポケット(左)
その他、デニムパーツ多数出力(数が多いので省略)

想定通り、複数の図形データを持つdxfファイルを、画像に一括で変換できていることが確認できます。

最後に

今回は「POLYLINE」「LINE」という2つの図形に絞って画像化する機能を実装しました。 恐らく他にもdxfファイルに利用されている図形はあると思いますので、気になる方は是非続きを実装してみて下さい。

ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。一緒に楽しく仕事しましょう!

www.wantedly.com

カテゴリー