Mając aplikację napisaną w office możemy bardzo łatwo przeczytać własności pliku – zarówno te wbudowane jak i te stworzone przez nas – te dodatkowe. Wystarczy kilka linijek i niezależnie od formatu pliku otwartego mamy dostęp do keywords, author i innych własności. Do tego własne własności, ale o tym, kiedy indziej. Wszystko byłoby fajnie, gdyby nie to, że do tych własności nie zawsze chcemy się dostać z poziomu Office. Czasami chcemy przeczytać to w innej, osobnej aplikacji. I co wtedy?

Przy dostępie do własności plików bardzo dużo znaczenie ma format pliku – czy to jest coś związanego z starym office i wersjami doc, xls itp., czy z nowym i z docx, xlsx itp. Jeden jest zamkniętym formatem pliku, drugi jest plikiem zip zawierającym pliki XML. Jednego możemy przeczytać wykorzystując parsery XMLa, drugi wymaga wyższej szkoły jazdy i System32 api.

Różnica w plikach nie tylko jest w tym jak się do nich dostajemy, ale także co zobaczymy w UI w explorerze.

DOCX własności pliku w Windows Explorer
DOCX własności pliku w Windows Explorer
DOCX własności pliku w Windows Explorer
DOCX własności pliku w Windows Explorer

By pobrać własności z DOCX wystarczy wykorzystać poniższy kod by dobrać się do słów kluczowych/tagów:

private string get_keywords_from_office_x_files(string path)
{
    using (var package = Package.Open(path, FileMode.Open, FileAccess.ReadWrite))
    {
        var corePart = package.GetPart(new Uri("/docProps/core.xml", UriKind.Relative));
        XDocument settings = null;

        using (TextReader tr = new StreamReader(corePart.GetStream()))
        {
            settings = XDocument.Load(tr);
        }

        XNamespace cp = "http://schemas.openxmlformats.org/package/2006/metadata/core-properties";

        var root = settings.Root;
        var tags = root?.Element(cp + "keywords");
        return tags?.Value;
    }
}

Jest to też jedyny rozsądny (nie wymagający niczego innego – paczek, skomplikowanego kodu itp.) sposób pobierania danych. Ale nie zadziała on na własnościach plików doc. Tutaj musimy napisać inny kod. Najlepsze jest to, że ten nowy kod nie działa na plikach docx. Więc nie ma jednego unikatowego rozwiązania dostępu do danych pliku. Wręcz mamy dwa zupełnie różne sposoby.

Doc używa tak zwanego Structured Storage – Compound File Binary Format. Który zapisuje własności pliku w pliku. To co możemy zrobić to wykorzystać Shell32 w celu pobrania informacji o pliku:

private string get_keywords_from_old_office_files(string path, string fileName)
{
    Object shell = null;
    Shell32.Folder folder = null;
    Shell32.FolderItem file = null;

    try
    {
        // Shell32.Shell shell = new Shell32.Shell(); does not work everywhere!!!!
        Type shellAppType = Type.GetTypeFromProgID("Shell.Application");

        shell = Activator.CreateInstance(shellAppType);
        folder = (Shell32.Folder)shellAppType.InvokeMember("NameSpace", BindingFlags.InvokeMethod, null, shell, new object[] { folder });

        folder = get_shell32_name_space_folder(path);
        file = folder.ParseName(fileName);

        // https://stackoverflow.com/questions/22382010/what-options-are-available-for-shell32-folder-getdetailsof        string keywords = folder.GetDetailsOf(file, 18);
        return keywords;
    }
    catch(Exception ex)
    {
        _log.Error(ex, "get_keywords_from_old_office_files|");
    }
    finally
    {
        file.Release();
        folder.Release();
        shell.Release();
    }
    return null;
}

private Shell32.Folder get_shell32_name_space_folder(Object folder)
{
    Type shellAppType = Type.GetTypeFromProgID("Shell.Application");

    Object shell = Activator.CreateInstance(shellAppType);
    return (Shell32.Folder)shellAppType.InvokeMember("NameSpace", BindingFlags.InvokeMethod, null, shell, new[] { folder });
}

Jeżeli jednak chcemy zrobić to jeszcze szybciej to możemy wykorzystać bibliotekę nuget WindowsAPICodePack-Shell która to znacznie ułatwia:

string docx = @"DOCX.docx";
string doc = @"DOC.doc";

var docxFile = ShellFile.FromFilePath(docx);
var docFile = ShellFile.FromFilePath(doc);

var docxKeywords =  docxFile.Properties.System.Keywords;
var docKeywords = docFile.Properties.System.Keywords;

var docxValue = docxKeywords.Value;
var docValue = docKeywords.Value;

WindowsAPICodePack różni się od Shell32 tym, że odwołuje się on do plików za pomocą property hendler w windows property system. Czyli trochę magii Windows API – dla maniaków super sprawa, dla normalnych ludzi…. czarna dziura. Ale ogólnie dla chętnych to polecam poczytać o IPropertyStore.

Jak widać WindowsAPICodePack na razie najlepiej się sprawuje. Zamiast dwóch różnych metod mamy jedną która nam pobiera to co trzeba. Wymaga ona jednak zainstalowania osobnej biblioteki, która kiedyś była rozwijana przez MS a teraz jest przez osobą trzecią. Jeżeli nie chcemy skorzystać z bibliotek i tylko sami chcemy zrobić to co robi WindowsAPICodePack to musimy się przygotować na trudny orzech do zgryzienia.

Ale nawet wykorzystanie IPropertyStore czy też WindowsAPICodePack nie gwarantuje, że odczytamy wszystkie własności wszystkich plików…. ale o tym innym razem.