На сегодня мне
известны две такие библиотеки (обе представлены 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";
// 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);
// Create relative uri for image
var relativeUri = PackUriHelper.GetRelativeUri(mainPartUri,
imagePartUri);
mainPart.CreateRelationship(relativeUri, TargetMode.Internal,
imagePartRelationshipType,
Большая часть
примера – это объявление всевозможных констант: типов контента, типов
отношений, имен компонент, …
В принципе,
все довольно прозрачно. Единственное, на что хочется обратить внимание – две
строки добавления отношения от главной части документа к картинке. Вот они:
var
relativeUri = PackUriHelper.GetRelativeUri(mainPartUri, imagePartUri);
mainPart.CreateRelationship(relativeUri, TargetMode.Internal, imagePartRelationshipType,
imageRelationshipId);
В первой
строке мы вычисляем относительный адрес между двумя именами компонент.
Вообще-то делать это не обязательно: если мы знаем имя конечной компоненты, то
в качестве targetUri, можно сразу указывать его. У относительного пути есть
один плюс – если перемещать некую часть контейнера (например, в отдельную
ветку), а все связи внутри окажутся заданы относительными путями, то их менять
не придется. Так что, это больше дело вкуса.
Вторая строка
интересна тем, что при добавлении связи между главной частью документа и
картинкой мы явно задаем Id отношения. Этот параметр не обязателен – id в любом
случае будет присвоено. Но в нашем случае автоматическая генерация Id не удобна
тем, что на отношение есть ссылка из главной части документа, а значит ее
пришлось бы менять.
Чтение
пакета
Теперь
выполним обратную работу – найдем в документе изображения и сохраним их. В
случае Word-документа (как, впрочем, и любых офисных) наиболее предпочтительный
способ навигации – искать связи определенного типа и двигаться по ним. Увы,
такого подхода придерживаются далеко не все (например, в формате XPS все
завязано на правила именования компонентов).
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.