From 78624641e435798b14cefc2f07d2928f87747391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=91=D0=B0?= =?UTF-8?q?=D0=B4=D1=8F=D0=B5=D0=B2?= Date: Sun, 9 Apr 2023 21:08:35 +0700 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=81=D0=B5=20=D1=82=D1=80=D0=B5=D0=B1?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=80=D0=B5=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20=D0=B1=D0=B5=D0=B7?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D1=8F=20=D0=BC=D0=BE=D0=B4=D1=83?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D0=BC=D0=B8=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8.=20(#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR closes #1 Reviewed-on: https://git.mousesoft.ru/ms/drawio-export/pulls/3 --- cmd/drawio-export/main.go | 28 ++- cmd/drawio-export/options.go | 75 ++++++++ pkg/drawio/export.go | 125 ++++++++++++- pkg/drawio/options.go | 350 +++++++++++++++++++++++++++++++++++ 4 files changed, 575 insertions(+), 3 deletions(-) create mode 100644 cmd/drawio-export/options.go create mode 100644 pkg/drawio/options.go diff --git a/cmd/drawio-export/main.go b/cmd/drawio-export/main.go index 99fc8a2..a129101 100644 --- a/cmd/drawio-export/main.go +++ b/cmd/drawio-export/main.go @@ -1,7 +1,31 @@ package main -import "fmt" +import ( + "flag" + "fmt" + "os" + + "git.mousesoft.ru/ms/drawio-exporter/pkg/drawio" +) + +var version string // Версия приложения func main() { - fmt.Println("Draw.io Exporter") + flag.Parse() + if flagHelp { + flag.Usage() + os.Exit(0) + } + if flagVersion { + fmt.Println("Draw.io Export cli util", version) + os.Exit(0) + } + if flag.NArg() < 1 { + flag.Usage() + os.Exit(1) + } + exporter := drawio.NewWithOptions(&opts) + if err := exporter.Export(flag.Args()...); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + } } diff --git a/cmd/drawio-export/options.go b/cmd/drawio-export/options.go new file mode 100644 index 0000000..0d34320 --- /dev/null +++ b/cmd/drawio-export/options.go @@ -0,0 +1,75 @@ +package main + +import ( + "flag" + + "git.mousesoft.ru/ms/drawio-exporter/pkg/drawio" +) + +var ( + flagHelp bool // Вывести справку о приложении и выйти + flagVersion bool // Вывести информацию о версии приложения и выйти + opts = drawio.Options{} // Аргументы командной строки приложения +) + +func init() { + // version + flag.BoolVar(&flagVersion, "V", false, "Prints version information") + flag.BoolVar(&flagVersion, "version", false, "Prints version information") + // Application + flag.StringVar(&opts.Application, "A", "", "Draw.io Desktop Application") + flag.StringVar(&opts.Application, "application", "", "Draw.io Desktop Application") + // Output + flag.StringVar(&opts.Output, "o", "", "Exported folder name [default: export]") + flag.StringVar(&opts.Output, "output", "", "Exported folder name [default: export]") + // Format + flag.Var(&opts.Format, "f", + "Exported format [default: pdf] [possible values: pdf, png, jpg, svg, vsdx, xml]") + flag.Var(&opts.Format, "format", + "Exported format [default: pdf] [possible values: pdf, png, jpg, svg, vsdx, xml]") + // Recursive + flag.BoolVar(&opts.Recursive, "r", false, + "For a folder input, recursively convert all files in sub-folders also") + flag.BoolVar(&opts.Recursive, "recursive", false, + "For a folder input, recursively convert all files in sub-folders also") + // RemovePageSuffix + flag.BoolVar(&opts.RemovePageSuffix, "remove-page-suffix", false, + "Remove page suffix when possible (in case of single page file") + // Quality + flag.UintVar(&opts.Quality, "q", 0, "Output image quality for JPEG [default: 90]") + flag.UintVar(&opts.Quality, "quality", 0, "Output image quality for JPEG [default: 90]") + // Transparent + flag.BoolVar(&opts.Transparent, "t", false, "Set transparent background for PNG") + flag.BoolVar(&opts.Transparent, "transparent", false, "Set transparent background for PNG") + // EmbedDiagram + flag.BoolVar(&opts.EmbedDiagram, "e", false, + "Includes a copy of the diagram for PDF, PNG, or SVG") + flag.BoolVar(&opts.EmbedDiagram, "embed-diagram", false, + "Includes a copy of the diagram for PDF, PNG, or SVG") + // EmbedSvgImages + flag.BoolVar(&opts.EmbedSvgImages, "embed-svg-images", false, "Embed Images in SVG file") + // Border + flag.UintVar(&opts.Border, "b", 0, + "Sets the border width around the diagram [default: 0]") + flag.UintVar(&opts.Border, "border", 0, + "Sets the border width around the diagram [default: 0]") + // Scale + flag.UintVar(&opts.Scale, "s", 0, "Scales the diagram size") + flag.UintVar(&opts.Scale, "scale", 0, "Scales the diagram size") + // Width + flag.UintVar(&opts.Width, "width", 0, + "Fits the generated image/pdf into the specified width, preserves aspect ratio") + // Height + flag.UintVar(&opts.Height, "height", 0, + "Fits the generated image/pdf into the specified height, preserves aspect ratio") + // Crop + flag.BoolVar(&opts.Crop, "crop", false, "Crops PDF to diagram size") + // Uncompressed + flag.BoolVar(&opts.Uncompressed, "u", false, "Uncompressed XML output") + flag.BoolVar(&opts.Uncompressed, "uncompressed", false, "Uncompressed XML output") + // EnablePlugins + flag.BoolVar(&opts.EnablePlugins, "enable-plugins", false, "Enable Plugins") + // help + flag.BoolVar(&flagHelp, "h", false, "Prints help information") + flag.BoolVar(&flagHelp, "help", false, "Prints help information") +} diff --git a/pkg/drawio/export.go b/pkg/drawio/export.go index 1e48b04..bf3e699 100644 --- a/pkg/drawio/export.go +++ b/pkg/drawio/export.go @@ -2,12 +2,67 @@ package drawio import ( "bufio" + "errors" "fmt" "io" + "io/fs" + "os" + "os/exec" + "path" + "strconv" + "strings" xmlparser "github.com/tamerh/xml-stream-parser" ) +// Создать нового экспортёра диаграмм +func New(opt ...Option) Exporter { + options := &Options{ + Application: "drawio", + Output: "export", + Format: Format("pdf"), + } + for _, opt := range opt { + opt.apply(options) + } + return NewWithOptions(options) +} + +// Создать нового экспортёра с параметрами +func NewWithOptions(opts *Options) Exporter { + return Exporter{opts: opts} +} + +// Экспортёр диаграмм +type Exporter struct { + opts *Options +} + +// Экспорт диаграмм из указанный файлов или папок +func (exp Exporter) Export(fileOrDir ...string) error { + var ( + err error + errs = []error{} + args = exp.opts.Args() + info fs.FileInfo + ) + for _, item := range fileOrDir { + if info, err = os.Stat(item); err != nil { + errs = append(errs, err) + continue + } + if info.IsDir() { + err = exp.exportDir(item, args) + } else { + err = exp.exportFile(item, args) + } + if err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + // Разбирает данные и возвращает срез имён диаграмм в них func Diagrams(reader io.Reader) ([]string, error) { var ( @@ -21,7 +76,6 @@ func Diagrams(reader io.Reader) ([]string, error) { if xml.Err != nil { return result, xml.Err } - fmt.Println(xml.Name) if items, ok := xml.Childs["diagram"]; ok { for _, item := range items { result = append(result, item.Attrs["name"]) @@ -30,3 +84,72 @@ func Diagrams(reader io.Reader) ([]string, error) { } return result, nil } + +// Экспорт диаграмм из всех файлов в папке +func (exp Exporter) exportDir(dirPath string, args []string, subDir ...string) error { + var ( + entries []fs.DirEntry + err error + errs []error + ) + if entries, err = os.ReadDir(dirPath); err != nil { + return err + } + for _, entry := range entries { + name := entry.Name() + outDir := make([]string, len(subDir), len(subDir)+1) + outDir = append(outDir, name) + if entry.IsDir() && exp.opts.Recursive { + err = exp.exportDir(path.Join(dirPath, name), args, outDir...) + } else { + err = exp.exportFile(path.Join(dirPath, name), args, outDir...) + } + if err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + +// Экспорт файла с диаграммами +func (exp Exporter) exportFile(filePath string, args []string, subDir ...string) error { + var ( + file *os.File + diagrams []string + err error + ) + if file, err = os.Open(filePath); err != nil { + return err + } + defer file.Close() + if diagrams, err = Diagrams(file); err != nil { + return err + } + outDirs := make([]string, 1, len(subDir)+1) + outDirs[0] = exp.opts.Output + outDirs = append(outDirs, subDir...) + output := path.Join(outDirs...) + errs := []error{} + for i, name := range diagrams { + outName := strings.TrimSuffix(path.Base(filePath), path.Ext(filePath)) + if !exp.opts.RemovePageSuffix || len(diagrams) > 1 { + outName += "-" + name + } + outName += exp.opts.OutExt() + drawioArgs := make([]string, len(args), len(args)+4) + copy(drawioArgs, args) + drawioArgs = append(drawioArgs, + "--page-index", strconv.Itoa(i), + "--output", path.Join(output, outName), + "--export", filePath, + ) + cmd := exec.Command(exp.opts.App(), drawioArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + fmt.Println("Run command:", cmd.Path, cmd.Args) + if err = cmd.Run(); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} diff --git a/pkg/drawio/options.go b/pkg/drawio/options.go new file mode 100644 index 0000000..2249cf0 --- /dev/null +++ b/pkg/drawio/options.go @@ -0,0 +1,350 @@ +package drawio + +import ( + "flag" + "fmt" + "strconv" +) + +// Интерфейс параметра экспортёра диаграмм +type Option interface { + apply(opts *Options) // Применить параметр к хранилищу параметров +} + +// Ошибка "неподдерживаемый формат" +type ErrUnsupportedFormat struct { + Format string +} + +// Описание ошибки +func (e ErrUnsupportedFormat) Error() string { + return fmt.Sprintf("unsupported format: '%s'", e.Format) +} + +var _ error = (*ErrUnsupportedFormat)(nil) // Проверка реализации интерфейса + +type Format string // Формат экспортированного файла + +// Строковое представление формата +func (f Format) String() string { + return string(f) +} + +// Загрузка формата из строки +func (f *Format) Set(str string) error { + for _, fmt := range SupportedFormats() { + if str == string(fmt) { + *f = Format(str) + return nil + } + } + return &ErrUnsupportedFormat{str} +} + +var _ flag.Value = (*Format)(nil) // Поверка реализации интерфейса + +// Расширение файла заданного формата +func (f Format) ext() string { + return "." + string(f) +} + +// Параметры экспортёра диаграмм +type Options struct { + Application string // Путь к приложению drawio + Output string // Путь к папке с экспортированными файлами + Format Format // Формат экспортированных файлов + Recursive bool // Рекурсивно сканировать вложенные папки с файлами + RemovePageSuffix bool // Удалять суффикс страницы, если это возможно + Quality uint // Качество экспортированного изображения (только для JPEG) + Transparent bool // Прозрачный фона для PNG + // Включать копию диаграммы в экспортированный файл для PDF, PNG и SVG + EmbedDiagram bool + EmbedSvgImages bool // Встраивать изображения в файл формата SVG + Border uint // Ширина рамки вокруг диаграмм + Scale uint // Масштаб в процентах размера экспортированных диаграмм + Width uint // Ширина экспортированной диаграммы с сохранением масштаба + Height uint // Высота экспортированной диаграммы с сохранением масштаба + Crop bool // Обрезать результирующий PDF до размера диаграммы + Uncompressed bool // Выводить несжатый XML + EnablePlugins bool // Включить подключаемые модули +} + +// Путь к приложению drawio +func (opts Options) App() string { + app := opts.Application + if len(app) == 0 { + app = "drawio" + } + return app +} + +// Путь к папке с экспортированными файлами +func (opts Options) OutDir() string { + out := opts.Output + if len(out) == 0 { + out = "export" + } + return out +} + +// Расширение экспортированных файлов +func (opts Options) OutExt() string { + fmt := opts.Format + if len(fmt) == 0 { + fmt = Format("pdf") + } + return fmt.ext() +} + +// Формирование аргументов командной строки +func (opts Options) Args() []string { + args := []string{} + if len(opts.Format) > 0 { + args = append(args, "--format", string(opts.Format)) + } + if opts.Quality != 0 { + args = append(args, "--quality", strconv.Itoa(int(opts.Quality))) + } + if opts.Transparent { + args = append(args, "--transparent") + } + if opts.EmbedDiagram { + args = append(args, "--embed-diagram") + } + if opts.EmbedSvgImages { + args = append(args, "--embed-svg-images") + } + if opts.Border != 0 { + args = append(args, "--border", strconv.Itoa(int(opts.Border))) + } + if opts.Scale != 0 { + args = append(args, "--scale", strconv.Itoa(int(opts.Scale))) + } + if opts.Width != 0 { + args = append(args, "--width", strconv.Itoa(int(opts.Width))) + } + if opts.Height != 0 { + args = append(args, "--height", strconv.Itoa(int(opts.Height))) + } + if opts.Crop { + args = append(args, "--crop") + } + if opts.Uncompressed { + args = append(args, "--uncompressed") + } + if opts.EnablePlugins { + args = append(args, "--enable-plugins") + } + return args +} + +// Путь к приложению drawio +func WithAppPath(path string) Option { + return optionApplication(path) +} + +type optionApplication string + +var _ Option = (*optionApplication)(nil) // Проверка реализации интерфейса + +func (opt optionApplication) apply(opts *Options) { + opts.Application = string(opt) +} + +// Путь к папке с экспортированными файлами +func WithOutput(path string) Option { + return optionOutput(path) +} + +type optionOutput string + +var _ Option = (*optionOutput)(nil) // Проверка реализации интерфейса + +func (opt optionOutput) apply(opts *Options) { + opts.Output = string(opt) +} + +// Формат экспортированных файлов +func WithFormat(format Format) Option { + return format +} + +var _ Option = (*Format)(nil) // Проверка реализации интерфейса + +func (opt Format) apply(opts *Options) { + opts.Format = opt +} + +// Рекурсивно сканировать вложенные папки с файлами +func WithRecursive() Option { + return optionRecursive{} +} + +type optionRecursive struct{} + +var _ Option = optionRecursive{} // Проверка реализации интерфейса + +func (opt optionRecursive) apply(opts *Options) { + opts.Recursive = true +} + +// Удалять суффикс страницы, если это возможно +func WithRemovePageSuffix() Option { + return optionRemovePageSuffix{} +} + +type optionRemovePageSuffix struct{} + +var _ Option = optionRemovePageSuffix{} // Проверка реализации интерфейса + +func (opt optionRemovePageSuffix) apply(opts *Options) { + opts.RemovePageSuffix = true +} + +// Качество экспортированного изображения (только для JPEG) +func WithQuality(q int) Option { + return optionQuality(q) +} + +type optionQuality uint + +var _ Option = (*optionQuality)(nil) // Проверка реализации интерфейса + +func (opt optionQuality) apply(opts *Options) { + opts.Quality = uint(opt) +} + +// Прозрачный фона для PNG +func WithTransparent() Option { + return optionTransparent{} +} + +type optionTransparent struct{} + +var _ Option = optionTransparent{} // Проверка реализации интерфейса + +func (opt optionTransparent) apply(opts *Options) { + opts.Transparent = true +} + +// Включать копию диаграммы в экспортированный файл для PDF, PNG и SVG +func WithEmbedDiagram() Option { + return optionEmbedDiagram{} +} + +type optionEmbedDiagram struct{} + +var _ Option = optionEmbedDiagram{} // Проверка реализации интерфейса + +func (opt optionEmbedDiagram) apply(opts *Options) { + opts.EmbedDiagram = true +} + +// Встраивать изображения в файл формата SVG +func WithEmbedSvgImages() Option { + return optionEmbedSvgImages{} +} + +type optionEmbedSvgImages struct{} + +var _ Option = optionEmbedSvgImages{} // Проверка реализации интерфейса + +func (opt optionEmbedSvgImages) apply(opts *Options) { + opts.EmbedSvgImages = true +} + +// Ширина рамки вокруг диаграмм +func WithBorder(border int) Option { + return optionBorder(border) +} + +type optionBorder uint + +var _ Option = (*optionBorder)(nil) // Проверка реализации интерфейса + +func (opt optionBorder) apply(opts *Options) { + opts.Border = uint(opt) +} + +// Масштаб в процентах размера экспортированных диаграмм +func WithScale(scale int) Option { + return optionScale(scale) +} + +type optionScale uint + +var _ Option = (*optionScale)(nil) // Проверка реализации интерфейса + +func (opt optionScale) apply(opts *Options) { + opts.Scale = uint(opt) +} + +// Ширина экспортированной диаграммы с сохранением масштаба +func WithWidth(width int) Option { + return optionWidth(width) +} + +type optionWidth uint + +var _ Option = (*optionWidth)(nil) // Проверка реализации интерфейса + +func (opt optionWidth) apply(opts *Options) { + opts.Width = uint(opt) +} + +// Высота экспортированной диаграммы с сохранением масштаба +func WithHeight(height int) Option { + return optionHeight(height) +} + +type optionHeight uint + +var _ Option = (*optionHeight)(nil) // Проверка реализации интерфейса + +func (opt optionHeight) apply(opts *Options) { + opts.Height = uint(opt) +} + +// Обрезать результирующий PDF до размера диаграммы +func WithCrop() Option { + return optionCrop{} +} + +type optionCrop struct{} + +var _ Option = optionCrop{} // Проверка реализации интерфейса + +func (opt optionCrop) apply(opts *Options) { + opts.Crop = true +} + +// Выводить несжатый XML +func WithUncompressed() Option { + return optionUncompressed{} +} + +type optionUncompressed struct{} + +var _ Option = optionUncompressed{} // Проверка реализации интерфейса + +func (opt optionUncompressed) apply(opts *Options) { + opts.Uncompressed = true +} + +// Включить подключаемые модули +func WithEnablePlugins() Option { + return optionEnablePlugins{} +} + +type optionEnablePlugins struct{} + +var _ Option = optionEnablePlugins{} // Проверка реализации интерфейса + +func (opt optionEnablePlugins) apply(opts *Options) { + opts.EnablePlugins = true +} + +// Список всех поддерживаемых форматов экспорта +func SupportedFormats() []Format { + return []Format{"pdf", "png", "jpg", "svg", "vsdx", "xml"} +}