Одной из часто встречающихся в процессе создания приложений задач является генерация различных документов в каком-либо из популярных форматов. Есть несколько привычных путей к желаемому результату – от подключения готовой библиотеки до брутального штудирования спецификации формата с последующим написанием необходимого кода. Но, независимо от выбранного варианта, было бы неплохо проверить, что полученный документ будет хорошо восприниматься и редактироваться стандартными средствами. О некоторых способах такой проверки и пойдёт речь под катом.



Оговорюсь сразу, что под проверкой в данном случае я понимаю проверку на соответствие полученного результата стандарту выбранного формата, позволяющую с достаточно большой вероятностью утверждать, что документ как минимум откроется в Word или Excel без неприятных сообщений о возникших проблемах с предложением попытаться восстановить повреждённый файл. Независимо от того, насколько серьёзным является разрабатываемое приложение и насколько солидным должен быть набор поддерживаемых типов файлов, в него скорее всего будут входить стандартные форматы Office Open XML, более известные как docx и xlsx, а также их двоичные предшественники, doc и xls. О некоторых механизмах работы с ними я и буду рассказывать.

Прогулки с динозаврами


Хотя Microsoft и выложила на своём сайте официальные спецификации форматов doc и xls, они нередко оказываются скупы и немногословны в своих описаниях. Стоит понимать, что со временем форматы претерпевали существенные изменения (с сохранением обратной совместимости). При этом единый подход использовался не только для хранения документов Word, но и для таблиц Excel и презентаций PowerPoint. Проще говоря, даже при наличии мануалов возможностей почувствовать себя Жоржем Кювье, пытающимся восстановить облик загадочной зверюшки по разрозненным костям, предостаточно. К счастью, существует способ локализовать имеющиеся в документе проблемы без необходимости расхлёбывать байтовую кашу в любимом hex-редакторе.

Способ этот заключается в использовании Microsoft Office Binary File Format Validator. Это достаточно простая в использовании утилита командной строки, в комплекте с которой идут три dll-ки (по одной на каждый поддерживаемый формат – doc, xls, ppt). Несмотря на то, что с момента первого официального анонса на сайте так и осталась выложенной бета-версия, инструмент вполне себе работоспособен и справляется со своими задачами. Единственная существенная проблема, с которой я столкнулся за время работы с валидатором – это отсутствие поддержки кириллических имён файлов. Для запуска утилиты достаточно ввести команду

bffvalidator.exe [-l log.xml] filename.ext

где filename.ext — это имя исследуемого файла, а -l log.xml – необязательный параметр, указывающий куда сохранить лог (по умолчанию лог пишется в ту же папку, где лежит проверяемый документ).
Для облегчения жизни и уменьшения количества рутинных действий я использую два сценария работы с валидатором. При проверке отдельного файла удобно пользоваться Far-ом: достаточно завести отдельную папку, например, c:\Temp\Bff, положить туда экзешник валидатора и сопутствующие dll-ли, а потом завести команду через F9-Commands-File Associations:




После этого проверить подозрительный файл можно будет буквально в пару нажатий на клавиатуру. Другой сценарий, который имеет смысл — заставить приложение сгенерировать набор тестовых файлов, и затем при помощи несложного кода прогнать проверку по всему набору, например так:

public class FileFormatValidationFailedException : Exception {
    public FileFormatValidationFailedException(string msg) : base(msg) { }
}
public void RunBFFValidator(string filePath) {
    string fileName = Path.GetFileName(filePath);
    string workingDirectory = Path.GetDirectoryName(filePath);
    string startupPath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
    StageName = String.Format("RUNNING BFFValidator for FILE {0}", fileName);
    outputManager.BeginWriteInfoLine(String.Format("Running BFFValidator for saved file '{0}'", fileName));
    ProcessStartInfo startInfo = new ProcessStartInfo(Path.Combine(startupPath, "BFFValidator.exe"));
    startInfo.Arguments = string.Format("-l bfflog.xml \"{0}\"", Path.Combine(workingDirectory, fileName));
    startInfo.WorkingDirectory = workingDirectory;
    startInfo.WindowStyle = ProcessWindowStyle.Hidden;
    Process validatorProcess = Process.Start(startInfo);
    validatorProcess.WaitForExit();
    if (validatorProcess.ExitCode != 0) {
        using (StreamReader reader = new StreamReader(Path.Combine(workingDirectory, "bfflog.xml"))) {
            string logContent = reader.ReadToEnd();
            throw new FileFormatValidationFailedException(logContent);
        }
    }
}

Вторая часть Мерлезонского балета


Ситуация с Office Open XML существенно проще. Если открыть несколько файлов hex-редактором, то в начале данных можно будет увидеть инициалы Фила Каца:


Это означает, что файлы представляют собой переименованный в docx/xlsx zip-архив, который можно открыть и увидеть вполне читаемую структуру. Однако и в этом случае можно не пытаться вручную искать расхождения с документацией из Редмонда, а поручить анализ файла специально предназначенным для этого инструментам. Для этого качаем Open XML SDK 2.5 и устанавливаем его (нам понадобятся OpenXMLSDKV25.msi и OpenXMLSDKToolV25.msi). После этого можно будет создать приложение для проверки файлов на наличие невалидной разметки (понадобится референс на DocumentFormat.OpenXml.dll). Простейший код для анализа документов выглядит следующим образом:

public void RunOpenXmlValidation(string filePath, string openXmlFormatVersion) {
    string fileName = Path.GetFileName(filePath);
    StageName = String.Format("RUNNING OpenXmlValidation for FILE {0}", fileName);
    outputManager.BeginWriteInfoLine(String.Format("Running OpenXmlValidation for saved file '{0}'", fileName));
    using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(filePath, false)) {
        DocumentFormat.OpenXml.FileFormatVersions formatVersion = DocumentFormat.OpenXml.FileFormatVersions.Office2010;
        if (openXmlFormatVersion == "office2007")
            formatVersion = DocumentFormat.OpenXml.FileFormatVersions.Office2007;
        else if (openXmlFormatVersion == "office2013")
            formatVersion = DocumentFormat.OpenXml.FileFormatVersions.Office2013;
        OpenXmlValidator validator = new OpenXmlValidator(formatVersion);
        var errors = validator.Validate(wordDoc);
        StringBuilder builder = new StringBuilder();
        foreach (ValidationErrorInfo error in errors) {
            string errorMsg = string.Format("{0}: {1}, {2}, {3}", error.ErrorType.ToString(), error.Part.Uri, error.Path.XPath, error.Node.LocalName);
            builder.AppendLine(errorMsg);
            builder.AppendLine(error.Description);
        }
        string logContent = builder.ToString();
        if (!string.IsNullOrEmpty(logContent))
            throw new FileFormatValidationFailedException(logContent);
    }
}

And we need to go deeper…


Рассмотренные выше способы валидации стоит рассматривать как попытку быстрого поиска, где именно могут быть проблемы в документе, а не как абсолютную гарантию того, что всё в порядке. Вполне реальны ситуации, когда валидатор выдаёт сообщение об ошибке, а Word или Excel нормально открывает файл и наоборот – не удаётся открыть прошедший валидацию документ. Поэтому если необходима более надёжная проверка, то не обойтись без использования COM. Это требует установленного Microsoft Office, не является thread-safe, требует дополнительных телодвижений для x64, зато позволяет убедиться в соответствие документа требованиям MS Office и поисследовать его структуру с точки зрения целевой платформы.

Внеклассное чтение


Если понадобится более серьёзный анализ файла с углубленным пониманием его структуры, то можно обратиться к документации по OpenXML SDK и непосредственно форматам.

Также при исследовании внутреннего устройства документов может помочь утилита OffVis.

Несколько полезных ссылок по взаимодействию с офисными приложениями: Primary Interop Assemblies (PIAs), Microsoft.Office.Interop.Excel namespace, Microsoft.Office.Interop.Word namespace.

Надеюсь, что использование полезных инструментов от Microsoft сбережёт ваше время и нервы. Спасибо за внимание!

Комментарии (2)


  1. Mixim333
    07.11.2015 09:40

    Спасибо, было интересно прочесть, но может быть Вы, как знающий человек, избавите меня от головной боли в моем коде для генерации xlsx-файлов на основе xslt-шаблонов: использую класс XslCompiledTransform, получаю валидный документ на основе DataSet, все отлично открывается Excel'ем, но при открытии выскакивает MessageBox от Excel'а со словами вроде: «Вы действительно хотите открыть этот документ… Его разметка может содержать вредоносный код...», при этом, еще раз повторю, сам документ отлично открывается и читается?


    1. catlion
      07.11.2015 14:45

      Если вы генерите просто xml-файл, а расширение ему присваиваете xls[xm]*, то у вас три пути
      1 Перейти на OOXML SDK или сопутствующие либы (как ClosedXML)
      2 Ассоциировать Excel как программу по-умолчанию для xml-файлов и добавить директиву в файл
      3 Переписать xlst на SpreadsheetML