Конвертер: одна строка в самом простом виде, любая среда в самой гибкой форме

Пакет converter из Aspose.Cells for Go via C++ Toolkits сводит каждую необходимость “преобразовать электронную таблицу в другой формат” к одной функции входа:

converter.Convert(source, saveOption, sink) error

У него только три параметра, и каждый отвечает ровно на один вопрос:

ПараметрТипВопрос, на который отвечаетТипичные значения
sourcedatasource.DataSourceОткуда поступает ввод?FilePathSource(path), BytesSource(data), NewReaderSource(r)
saveOptionsaveoptions.SaveOptionВывод в каком виде, как настроено?formats.Get("pdf") для значений по умолчанию по имени, или pdf.New(pdf.WithOnePagePerSheet(true)) для тонкой настройки
sinkdatasource.DataSinkКуда направляется вывод?FilePathSink(path), &BytesSink{}, NewWriterSink(w)

Для самого лаконичного стиля используйте значения по умолчанию по всем трем осям; для самого гибкого — регулируйте каждый параметр независимо. Поскольку три оси независимы, один и тот же Convert естественно подходит для файловых, оперативных, HTTP и пакетных сред. (Существует также устаревший однострочник ConvertSpreadsheetToFile для простого случая файл→файл — сравнение в разделе 1 ниже.) Остальная часть этой статьи переходит от самого лаконичного к самому гибкому, постепенно расширяя предыдущий фрагмент на каждом шаге, а не переключаясь на другой API.

1. Самый простой: один вызов для одного формата

1.1 Встроенный однострочник file→file: ConvertSpreadsheetToFile

Пакет также поставляется со старой, ещё более короткой записью для классического случая “конвертировать этот файл в тот файл”. Всё, что нужно — два пути:

package main

import (
	"log"
	"github.com/aspose-cells/aspose-cells-go-cpp-toolkits/v26/core"
	"github.com/aspose-cells/aspose-cells-go-cpp-toolkits/v26/converter"
	_ "github.com/aspose-cells/aspose-cells-go-cpp-toolkits/v26/register" // registers every output format
)

func main() {
	defer core.Cleanup()
	// Both ends are files; the output format is inferred from the extension.
	if err := converter.ConvertSpreadsheetToFile("input.xlsx", "output.pdf"); err != nil {
		log.Fatal(err)
	}
}

Устарело. ConvertSpreadsheetToFile — это тонкая оболочка, сохраняемая для обратной совместимости; новый код следует направлять к converter.Convert ниже. Для случая file→file оба эквивалентны — это всё, что делает ConvertSpreadsheetToFile.

opt := formats.Get(strings.TrimPrefix(filepath.Ext("output.pdf"), ".")) // "pdf"
converter.Convert(
	datasource.FilePathSource("input.xlsx"), // input: a file
	opt,                                     // format: taken from the extension
	datasource.FilePathSink("output.pdf"),   // output: a file
)

ConvertSpreadsheetToFile и форма Convert, приведённые выше, дают одинаковые результаты; различие заключается в сценарии, как показано далее.

1.2 Рекомендуемый однострочник: Convert со значениями по умолчанию

package main

import (
	"log"
	"github.com/aspose-cells/aspose-cells-go-cpp-toolkits/v26/core"
	"github.com/aspose-cells/aspose-cells-go-cpp-toolkits/v26/converter"
	"github.com/aspose-cells/aspose-cells-go-cpp-toolkits/v26/datasource"
	"github.com/aspose-cells/aspose-cells-go-cpp-toolkits/v26/saveoptions/pdf"
)

func main() {
	defer core.Cleanup()
	if err := converter.Convert(
		datasource.FilePathSource("input.xlsx"), // input: a file
		pdf.New(),                               // format: PDF, all defaults
		datasource.FilePathSink("output.pdf"),   // output: a file
	); err != nil {
		log.Fatal(err)
	}
}

Все три параметра используют значения по умолчанию: input = файл, format = pdf.New() без какой‑либо конфигурации, output = файл. Это “самый лаконичный” вариант — без ручного os.Open/os.Create, без выделения объектов, без закрытия дескрипторов; источник/приёмник управляют ресурсами за вас.

1.3 Сравнение двух форм file→file

ConvertSpreadsheetToFile(in, out)converter.Convert(source, opt, sink)
Вводтолько путь к файлулюбой DataSource: путь, []byte, или reader
Выводтолько путь к файлулюбой DataSink: путь, байты или io.Writer
Формат выводавсегда определяется по расширению, только значения по умолчаниюлюбой saveOption: значение по умолчанию или настройка, специфичная для формата
Дополнительный импорттребуется регистрация формата (_ \"…/register\", или соответствующий подпакет saveoptions/*)нет, если передать конструктор, например pdf.New()
Состояниеустаревшая тонкая обёртка (стабильный, оставлен для совместимости)задокументированная единственная точка входа для нового кода

Один и тот же файл → результат файла, два разных сценария:

  • ConvertSpreadsheetToFile — у вас буквально два пути, и вы довольны настройками по умолчанию движка для целевого формата. Идеально подходит для одноразовых скриптов конвертации, миграционных пакетов и тестовых вспомогательных средств. Его цена: ввод, вывод и настройки никогда не могут выйти за рамки формы «file path + defaults» — и он устарел.
  • converter.Convert — вы хотите тот же однострочник, но любой из трёх аргументов может быть заменён: завтра ввод будет []byte загрузки, цель потребует pdf.WithOnePagePerSheet(true), или вывод должен сразу попасть в HTTP‑ответ. Это именно сценарии разделов 3 и 4 — напишите один раз с Convert, и каждое изменение будет правкой одного аргумента.

2. Самый простой × N: одна функция для всех 32 форматов

Следующий уровень лаконичности — использование одной и той же функции независимо от того, какой формат вы выбираете. Поскольку saveOption можно вычислить из расширения выходного файла, вынесение этого вывода наружу дает «универсальный файловый конвертер»:

package main

import (
	"errors"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
	"github.com/aspose-cells/aspose-cells-go-cpp-toolkits/v26/core"
	"github.com/aspose-cells/aspose-cells-go-cpp-toolkits/v26/converter"
	"github.com/aspose-cells/aspose-cells-go-cpp-toolkits/v26/datasource"
	toolkiterrors "github.com/aspose-cells/aspose-cells-go-cpp-toolkits/v26/errors"
	"github.com/aspose-cells/aspose-cells-go-cpp-toolkits/v26/formats"
	_ "github.com/aspose-cells/aspose-cells-go-cpp-toolkits/v26/register" // registers every output format
)

// convert re-saves in as out; the output format is read from out's extension.
// The same code handles every built-in format (pdf/csv/html/md/xlsx/png/…).
// It is exactly what the deprecated ConvertSpreadsheetToFile does internally.
func convert(in, out string) error {
	opt := formats.Get(strings.TrimPrefix(filepath.Ext(out), ".")) // ".pdf" -> "pdf"
	if opt == nil {
		return fmt.Errorf("%w: %q", toolkiterrors.ErrUnsupportedFormat, filepath.Ext(out))
	}
	return converter.Convert(
		datasource.FilePathSource(in), // where the input comes from
		opt,                           // what to produce: decided by the extension
		datasource.FilePathSink(out),  // where the output goes
	)
}

func main() {
	defer core.Cleanup()
	if len(os.Args) != 3 {
		log.Fatalf("usage: %s <input.xlsx> <output.ext>", os.Args[0])
	}
	if err := convert(os.Args[1], os.Args[2]); err != nil {
		if errors.Is(err, toolkiterrors.ErrUnsupportedFormat) {
			log.Printf("no registered output format for %q", filepath.Ext(os.Args[2]))
		}
		log.Fatal(err)
	}
	fmt.Println("done")
}

Использование остается идентичным — меняется только расширение выходного файла:

$ go run . input.xlsx report.pdf     # to PDF
$ go run . input.xlsx report.csv     # to CSV
$ go run . input.xlsx report.html    # to HTML
$ go run . input.xlsx notes.md       # to Markdown
$ go run . input.xlsx sheet.png      # to a PNG image
$ go run . input.xlsx legacy.xls     # to legacy Excel

Что это за пустой импорт _ "…/register"?

Существует два способа получить saveOption; это единственная концепция, которую необходимо понять стилю «самый лаконичный».

  • Найдите его в реестре по имени формата (formats.Get): расширение должно присутствовать в реестре formats. Каждый подпакет saveoptions/* регистрирует себя в собственном init(), но только при импорте. Импортируйте необходимые подпакеты по одному (например, только pdf, если вы конвертируете в PDF), или blank-import …/register один раз, который регистрирует все 32 формата сразу — вышеупомянутый «универсальный конвертер» опирается на эту единственную строку.
  • Вызовите конструктор напрямую (pdf.New(), markdown.New()): обходится без реестра, импорт register не требуется; стоимость — одна строка импорта на каждый используемый формат.

Вкратце: используйте _ "…/register" только когда вы разрешаете через formats.Get + расширение вывода; передача конструктора напрямую ничего не требует. Пустой импорт безвреден и всегда присутствует в официальных примерах.

3. Самый гибкий: регулируйте каждый параметр независимо

Когда потребность выходит за рамки «файл в файл с настройками по умолчанию», вы не меняете API — три параметра Convert можно менять независимо друг от друга.

1. Ввод (источник): из файла, в память, в любой поток

datasource.FilePathSource("input.xlsx") // a file on disk
datasource.BytesSource(data)            // []byte already in memory (downloads, cached reuse)
datasource.NewReaderSource(r.Body)      // any io.Reader (HTTP body, compressed stream, …)

Примечание: базовый движок требует произвольного доступа ко всему содержимому, поэтому ReaderSource полностью буферизует поток при первом доступе; каждый последующий Open предоставляет копию этого буфера. Используйте его уверенно для HTTP, но имейте в виду, что это не настоящий “конвертировать во время потоковой передачи”.

2. Формат (saveOption): по умолчанию, или настроенный

СценарийСтиль записиПочему
Some format with default settingsformats.Get("pdf")поиск по расширению; без учёта регистра; nil, если не зарегистрирован
A format with format-specific optionspdf.New(pdf.WithOnePagePerSheet(true))функциональные опции
Default settings, explicit formatooxml.New() / csv.New() / …один конструктор без аргументов для каждого формата
A custom private formatformats.Register("fmt", factory)подключить к реестру — см. docs/register.md

Функциональные параметры появляются только в тех форматах, которые их поддерживают: одна страница на лист в PDF — WithOnePagePerSheet; встроенные изображения в HTML — WithExportImagesAsBase64; TSV — formats.Get("tsv"). Параметры имеют семантику указателя: опускание = значение по умолчанию движка; явная передача false также считается.

3. Вывод (sink): файл, байты или io.Writer

datasource.FilePathSink("output.pdf") // to disk (parent dirs are created)
&out                                 // into memory; read it back with out.Bytes()
datasource.NewWriterSink(w)          // straight into any io.Writer (http.ResponseWriter, buffer, socket)

BytesSink и WriterSink — два ключа к «наиболее гибкому»: они позволяют результату конвертации полностью обходить диск и направлять его напрямую в HTTP‑ответ, объектное хранилище, zip‑архив и т.д. Все три являются разными реализациями одного и того же параметра sink, поэтому любой объект, принимающий []byte или io.Writer, может быть подключён к конвертеру.

4. Примеры из реальной среды: один Convert, много фигур

A. Конвертация в памяти; конечная точка загрузки передаёт PDF в виде потока

Обычный сценарий на стороне сервера — пользователь загружает xlsx, эндпоинт возвращает PDF, без временных файлов:

func handleExcelToPDF(w http.ResponseWriter, r *http.Request) {
	var out datasource.BytesSink
	if err := converter.Convert(
		datasource.NewReaderSource(r.Body),    // input: the request body
		pdf.New(pdf.WithOnePagePerSheet(true)), // format: PDF, one page per sheet
		&out,                                   // output: memory
	); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	w.Header().Set("Content-Type", "application/pdf")
	w.Header().Set("Content-Disposition", `attachment; filename="report.pdf"`)
	_, _ = w.Write(out.Bytes())
}

Чтобы выдавать CSV / Excel / HTML вместо этого, замените только saveOption (например formats.Get("csv")); больше ничего не меняется.

B. Прочитать один раз, пакетный экспорт в различные форматы

Прочитайте ввод один раз, затем переиспользуйте его через BytesSource для любого количества выводов — идеально для «экспортировать каждый отчет»:

data, err := os.ReadFile("input.xlsx")
if err != nil {
	log.Fatal(err)
}

for _, ext := range []string{"pdf", "csv", "html", "json", "md"} {
	var out datasource.BytesSink
	if err := converter.Convert(datasource.BytesSource(data), formats.Get(ext), &out); err != nil {
		log.Fatalf("%s: %v", ext, err)
	}
	if err := os.WriteFile("output."+ext, out.Bytes(), 0o644); err != nil {
		log.Fatal(err)
	}
}

C. “Сохранить как” в семействе электронных таблиц (миграция / совместимость)

Миграция форматов внутри семейства электронных таблиц, например xlsx → xls, xlsx → xlsb, xlsx → ods, и шаблоны xltx/xltm — всё ещё один Convert, только saveOption меняется:

src := datasource.FilePathSource("input.xlsx")
converter.Convert(src, formats.Get("xls"),  datasource.FilePathSink("legacy.xls")) // legacy Excel
converter.Convert(src, formats.Get("xlsb"), datasource.FilePathSink("binary.xlsb")) // binary Excel
converter.Convert(src, formats.Get("ods"),  datasource.FilePathSink("open.ods"))    // OpenDocument
converter.Convert(src, formats.Get("xltx"), datasource.FilePathSink("template.xltx")) // template

D. Настройка параметров для каждого формата, только при необходимости

Функциональные опции появляются, когда требуется тонкий контроль — обратите внимание, каждый вызов ниже является реальным и правильно типизированным:

// PDF: one page per worksheet + a compliance level
pdf.New(
	pdf.WithOnePagePerSheet(true),
	pdf.WithCompliance("pdf15"),
	pdf.WithProducer("my-report-app"),
)

// HTML: single file, images inlined as base64, CSS prefix (avoids style clashes)
html.New(
	html.WithSaveAsSingleFile(true),
	html.WithExportImagesAsBase64(true),
	html.WithCellCssPrefix("tbl_"),
)

// CSV: custom separator and encoding
// (the separator is a byte, and the encoding is a name — matched case- and
// punctuation-insensitively, so "UTF-8", "utf8" and "UTF8" are one name)
csv.New(
	csv.WithSeparator(';'),
	csv.WithEncoding("UTF-8"),
)

// Image: pick the type (the only switch the image package currently exposes)
image.New(image.WithImageType("png")) // or "jpg" / "svg" / "gif" / "tif" / …

Совет: для однофайловых выводов, таких как CSV/HTML/изображение, движок экспортирует только лист, который он считает «активным» по умолчанию. Чтобы управлять областью, используйте собственные параметры формата — например html.WithExportActiveWorksheetOnly(true) (только активный лист?), html.WithShowAllSheets(true) или csv.WithExportAllSheets(true). Пиксельное качество изображения, точный выбор листов для рендеринга и другие глубокие настройки всё ещё доступны на уровне движка, но текущий пакет image раскрывает только переключатель WithImageType — обратитесь к нижележащей привязке, если нужно больше; со временем набор опций будет расширяться.

E. Пакетное преобразование каталога

Повседневная задача «конвертировать эту папку Excel в PDF» становится очень короткой благодаря определению расширения:

func convertAll(inputDir, outputDir string) error {
	if err := os.MkdirAll(outputDir, 0o755); err != nil {
		return err
	}
	return filepath.Walk(inputDir, func(path string, info os.FileInfo, err error) error {
		if err != nil || info.IsDir() {
			return err
		}
		ext := strings.ToLower(filepath.Ext(path))
		if ext != ".xlsx" && ext != ".xls" && ext != ".xlsm" && ext != ".csv" {
			return nil
		}
		out := filepath.Join(outputDir, strings.TrimSuffix(filepath.Base(path), ext)+".pdf")
		if err := convert(path, out); err != nil { // reuse the convert from section 2
			return fmt.Errorf("convert %s: %w", path, err)
		}
		fmt.Printf("done: %s -> %s\n", path, out)
		return nil
	})
}

F. Классификация ошибок

Ошибки конвертера являются классифицируемыми sentinel‑ошибками; используйте errors.Is для точной обработки:

err := convert("in.xlsx", "out.xyz")
switch {
case errors.Is(err, toolkiterrors.ErrUnsupportedFormat):
	log.Println("this extension has no registered output format")
case errors.Is(err, os.ErrNotExist):
	log.Println("the input file does not exist")
default:
	log.Printf("conversion failed: %v", err)
}

formats.List() возвращает все текущие зарегистрированные расширения (отсортированные, потокобезопасные) — удобно для создания списка “поддерживаемых форматов” или подсказки об ошибке.

5. Поддерживаемые форматы в обзоре (32 зарегистрировано по умолчанию)

СемействоВыходные расширенияПакет настройки
Excelxlsx xlsm xltx xltm xls xlsbsaveoptions/ooxml, xls, xlsb
OpenDocumentods xml dif dbfsaveoptions/ods, xml, dif, dbf
Текст / данныеcsv txt tsv json sqlsaveoptions/csv, txt, json, sqlScript
Документы / макетpdf docx pptx xps pcl epubsaveoptions/pdf, docx, pptx, xps, pcl, ebook
Вебhtml mdsaveoptions/html, markdown
Изображенияpng jpg jpeg gif bmp svg emf tif tiffsaveoptions/image

Чтобы бинарный файл оставался небольшим, импортируйте только те подпакеты, которые используете (например, pdf); для удобства или общего диспетчерования используйте пустой импорт …/register. Импорт подпакета инициирует его регистрацию; поиск в реестре нечувствителен к регистру; неподдерживаемое расширение возвращает nil (не ошибку), оставляя решение о резервном варианте за вызывающим кодом.

6. Лучшие практики и предостережения

  1. Одна точка входа — не изобретайте её заново. File-to-file использует Convert(FilePathSource, formats.Get(ext)/constructor, FilePathSink); переключайте sink’ы только когда нужны байты или поток. Не реализуйте вручную os.Open → ReadAll → … → os.WriteFile; source/sink управляют ресурсами и создают родительские каталоги за вас.
  2. Параллелизм и лицензирование. Подлежащий движок нативный: не делитесь одним объектом SaveOption/workbook между goroutine’ами в одном процессе; для массовой обработки обрабатывайте файлы последовательно или распараллеливайте их между процессами. Оценочная сборка ограничивает количество загрузок (примерно сто загрузок NewWorkbook_Stream на процесс) — для больших тестов перезапускайте процесс, чтобы сбросить счётчик.
  3. Прочитайте ввод один раз, а затем переиспользуйте его. Чтобы вывести несколько форматов из одного ввода, сначала os.ReadFile его в []byte и многократно конвертируйте с помощью BytesSource, избегая повторных чтений с диска и перезагрузок.
  4. Потоки буферизуются. NewReaderSource буферизует данные при первом доступе, поэтому подходит для одноразовых вводов, таких как тело HTTP‑запроса, а не для реального потокового чтения очень больших файлов (для этого используйте FilePathSource, указывающий напрямую на диск).
  5. Знайте, кто определяет структуру вывода. Convert — это «один ввод → один файл вывода». Если вам нужен один файл на лист или zip, используйте manipulator.Split с FolderSink/ZipSink — не принуждайте это через Convert.