Open Packaging Conventions #3. Немного об API

Ну что ж, теперь, когда мы получили общее представление о структуре пакетов в OPC, а также применили эти знания на практике, имеет смысл слегка познакомиться с API, которое имеется для манипулирования пакетами OPC.

На сегодня мне известны две такие библиотеки (обе представлены Microsoft)

  • Packaging API – нативная COM-based библиотека. По всей видимости, основная её задача исходно была – служить базисом для XPS Document API
  • System.IO.Packaging – managed библиотека, входящая в состав .Net начиная с версии 3.0.

Я бы хотел сегодня немного поговорить о второй: рассказать о базовом функционале для манипулирования OPC пакетами (создание/чтение компонент и управление отношениями), а также обратить внимание на несколько не очень очевидных, с моей точки зрения, моментов.

Базовые классы пространства System.IO.Packaging

Практически все задачи манипулирования OPC-пакетами решаются следующими 4-я классами:

  • Package – через него идет открытие/создание пакета, создание/удаление/получение компонент и отношений уровня пакета.
  • PackagePart – это компонент. Через этот класс можно узнать тип контента, прочесть содержимое компонента, а также получить список его отношений
  • PackageRelationship – отношение. Этот класс содержит только тип отношения и Uri цели.
  • PackUriHelper – этот класс содержит набор вспомогательных методов для манипулирования Uri отношений. Он позволяет, например, легко решить такие задачи:
    • Зная имя исходного компонента и относительную ссылку в одном из отношений, получить имя конечного компонента
    • Или наоборот, зная имена 2-х компонент, получит относительную ссылку от одного к другому

Да, все классы объявлены в сборке WindowsBase.

Создание пакетов

Думаю, особо расписывать что-то здесь нет смысла, достаточно привести пример. В качестве такового мы возьмем созданный в предыдущей статье документ с котом Улыбка.

Вот так выглядит программный вариант его создания:

public void CreateWordDocumentWithTextAndImage()
{
// Part Names
const string mainDocumentPartName = "/main.xml";
const string imagePartName = "/cat.jpeg";

// Content types
const string mainDocumentPartContentType =
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml";
const string imagePartContentType =
System.Net.Mime.MediaTypeNames.Image.Jpeg;

// Relationsip Types
const string mainDocumentPartRelationshipType =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
const string imagePartRelationshipType =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";

// Image relationship Id
const string imageRelationshipId = "rId1";

// Create new Word document
using (var document = Package.Open("result.docx", System.IO.FileMode.Create))
{
// Add main part
var mainPartUri = new Uri(mainDocumentPartName, UriKind.Relative);
var mainPart = document.CreatePart(mainPartUri, mainDocumentPartContentType);

using (var mainPartStream = mainPart.GetStream())
{
mainPartStream.Write(Properties.Resources.main, 0, Properties.Resources.main.Length);
mainPartStream.Close();
}

document.CreateRelationship(mainPartUri, TargetMode.Internal, mainDocumentPartRelationshipType);

// Add image
var imagePartUri = new Uri(imagePartName, UriKind.Relative);
var imagePart = document.CreatePart(imagePartUri, imagePartContentType);

using (var imagePartStream = imagePart.GetStream())
{
imagePartStream.Write(Properties.Resources.cat, 0, Properties.Resources.cat.Length);
imagePartStream.Close();
}

// Create relative uri for image
var relativeUri = PackUriHelper.GetRelativeUri(mainPartUri, imagePartUri);
mainPart.CreateRelationship(relativeUri, TargetMode.Internal, imagePartRelationshipType,

imageRelationshipId);

document.Close();
}

System.Diagnostics.Process.Start("result.docx");
}

Большая часть примера – это объявление всевозможных констант: типов контента, типов отношений, имен компонент, …

В принципе, все довольно прозрачно. Единственное, на что хочется обратить внимание – две строки добавления отношения от главной части документа к картинке. Вот они:

var relativeUri = PackUriHelper.GetRelativeUri(mainPartUri, imagePartUri);
mainPart.CreateRelationship(relativeUri, TargetMode.Internal, imagePartRelationshipType,
imageRelationshipId);

В первой строке мы вычисляем относительный адрес между двумя именами компонент. Вообще-то делать это не обязательно: если мы знаем имя конечной компоненты, то в качестве targetUri, можно сразу указывать его. У относительного пути есть один плюс – если перемещать некую часть контейнера (например, в отдельную ветку), а все связи внутри окажутся заданы относительными путями, то их менять не придется. Так что, это больше дело вкуса.

Вторая строка интересна тем, что при добавлении связи между главной частью документа и картинкой мы явно задаем Id отношения. Этот параметр не обязателен – id в любом случае будет присвоено. Но в нашем случае автоматическая генерация Id не удобна тем, что на отношение есть ссылка из главной части документа, а значит ее пришлось бы менять.

Чтение пакета

Теперь выполним обратную работу – найдем в документе изображения и сохраним их. В случае Word-документа (как, впрочем, и любых офисных) наиболее предпочтительный способ навигации – искать связи определенного типа и двигаться по ним. Увы, такого подхода придерживаются далеко не все (например, в формате XPS все завязано на правила именования компонентов).

Вот этот пример:

public void ReadWordDocumentImages()
{
// Relationsip Types
const string mainDocumentPartRelationshipType =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
const string imagePartRelationshipType =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";

using (var document = Package.Open("result.docx", System.IO.FileMode.Open))
{
var mainPartRelationship = document.GetRelationshipsByType(mainDocumentPartRelationshipType).Single();

var mainPartName = PackUriHelper.ResolvePartUri(new Uri("/", UriKind.Relative), mainPartRelationship.TargetUri);
var mainPart = document.GetPart(mainPartName);

foreach (var imageRelationship in mainPart.GetRelationshipsByType(imagePartRelationshipType))
{
var imagePartName = PackUriHelper.ResolvePartUri(mainPartName, imageRelationship.TargetUri);
var imagePart = document.GetPart(imagePartName);

var fileName = Path.GetFileName(imagePartName.OriginalString);

using (var file = new FileStream(fileName, FileMode.Create))
{
var imageStream = imagePart.GetStream();
imageStream.CopyTo(file);

file.Close();
imageStream.Close();
}
}
}

}

Из интересного я бы отметил 2 вещи:

  • Метод GetRelationshipsByType(), который присутствует, как у пакета, так и у компонента и возвращает все связи пакета/компонента требуемого типа.
  • Для того, чтобы получить имя компонента из пути, который хранится в отношении используется метод ResolvePartUri(). Причем первый параметр у него
    • либо имя компонента от которого выходит отношение
    • либо “/” – если мы смотрим на отношение уровня пакета

Ну и наконец, смена содержимого

Редактирование содержимого компонентов

Пример еще проще предыдущих:

public void ChangeWordDocumentMainPart()
{
const string mainDocumentPartRelationshipType =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";

using (var document = Package.Open("result.docx", System.IO.FileMode.Open))
{
var mainPartRelationship = document.GetRelationshipsByType(mainDocumentPartRelationshipType).Single();

var mainPartName = PackUriHelper.ResolvePartUri(new Uri("/", UriKind.Relative), mainPartRelationship.TargetUri);
var mainPart = document.GetPart(mainPartName);

using (var mainPartStream = mainPart.GetStream())
{
mainPartStream.SetLength(0);
mainPartStream.Write(Properties.Resources.main2, 0, Properties.Resources.main2.Length);

mainPartStream.Close();
}

document.Close();
}

}

Из важного, только 1 строчка:

mainPartStream.SetLength(0);

– которая отбрасывает предыдущее содержимое.

Почему это важно? По умолчанию поток компонента производит запись поверх уже имеющегося контента. И если предыдущее содержимое было больше, чем новое, то в хвосте компонента у вас окажется всякий мусор. Как правило, с точки зрения потребителя, такой пакет будет безнадежно испорченным.

В заключение

Если вы внимательно посмотрели на пространство System.IO.Packaging, то уже обнаружили там много других интересных возможностей. О них мы обязательно поговорим, но чуть позже когда узнаем немного о дополнительных возможностях стандарта Open Package Convention.

Все приведенные в статье примеры кода можно найти все там же на https://msosamples.codeplex.com в папке проекта PackagingAPI.

This entry was posted in MS Office and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s