前書き こんにちは、スマートファクトリー向け制御ソフトウェア開発チームの高石( @ksk_taka )です。 本記事では、アパレル業界や製造業界など、CADを取り扱う業界で広く使われているdxfファイルを 一括で画像ファイルに変換する 手法について記載します。 dxfファイルとは そもそも dxfファイルとは何ぞや? という方のために簡単に説明をします。 dxfファイルは CAD間を仲介する中間ファイル として使うことを目的としたファイルです。 例えば機械設計をする際に3D-CADが利用されますが、よく使われる3D-CADソフトとして、以下のものがあります。 CATIA SolidWorks Creo Parametric これらのそれぞれのソフトで作られる図面データは 別々のファイル形式(拡張子) を持っており、基本的に互換性がありません。 アパレル業界で用いられるCADソフトも同様で、以下の様な異なるCADソフトにて生成される図面データには互換性がありません。 クレアコンポ AGMS このままでは一方のCADソフトで生成した図面データを、別のCADソフトを持った人が開こうとすると「開かない!」という状況になってしまいます。 そんなの困る! ということで、必要となるのが 中間ファイル という存在です。 dxfファイルは恐らく 現在最も広く使われている であろう中間ファイルで、 使用するCADソフトに関わらず開くことができる図面データ になります。 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の終了 今回はひとまず、使用頻度の高い POLYLINE と LINE の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: デニム左前身頃 画像2: デニム右前身頃 画像3: デニム左後身頃 画像4: デニム右後身頃 画像5: デニム後ポケット(左) その他、デニムパーツ多数出力(数が多いので省略) 想定通り、複数の図形データを持つdxfファイルを、画像に一括で変換できていることが確認できます。 最後に 今回は「POLYLINE」「LINE」という2つの図形に絞って画像化する機能を実装しました。 恐らく他にもdxfファイルに利用されている図形はあると思いますので、気になる方は是非続きを実装してみて下さい。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。一緒に楽しく仕事しましょう! www.wantedly.com