A Simple XPS Decoder in C++

If you write programs in C#, Windows Presentation Foundation in .Net 3.0 provides quite nice API to write, generate and manipulate XPS documents. You can get the same feature if you work with managed C++ in .Net 3.0. Even if you work with .Net 2.0, you can get the basic ZIP stream decoding/encoding support.

But if you want to write code in unmamanged world, especially outside of the XPS filter pipeline, there is not enough sample code there. I'm going to write a simple XPS decoder in C++, based on the zlib compression library (https://www.zlib.net/zlib123-dll.zip).

#include <windows.h>

#include <stdio.h>

#include <tchar.h>

#include "MemFile.h"

#include "zip.h"

#include "zlib.h"

#pragma comment(lib, "zdll.lib")

HANDLE CreateDirFile(char * pName, DWORD len, HRESULT & hr);

class CXps : CMemoryMappedFile

{

    const CEndCentralDir * m_pEndCentralDir;

    const CCentralFileHeader * m_pFileHeader;

public:

    HRESULT LoadXps(const wchar_t * pFileName);

    HRESULT DecodePiece(const BYTE * pCur, DWORD compressedSize);

    HRESULT ExtractAllStreams();

};

The code uses zlib.h and zdll.lib for decompression. It opens up an XPS container as a memory-mapped file, wrapped in the CXps class. The CXps class has a pointer to end central directory, and a pointer to the first central file header.

HRESULT CXps::LoadXps(const wchar_t * pFileName)

{

    m_pEndCentralDir = NULL;

    m_pFileHeader;

    if (! Open(pFileName))

    {

        return E_INVALIDARG;

    }

    const BYTE * pCur = m_View + m_nFileSize - (sizeof(CEndCentralDir) - 1);

    while (pCur > m_View)

    {

        DWORD signature = * (DWORD *) pCur;

        if (signature == sig_EndCentralDir)

        {

            m_pEndCentralDir = (const CEndCentralDir *) pCur;

            m_pFileHeader = (const CCentralFileHeader *) (m_View + m_pEndCentralDir->m_offsetCentralDic);

            return S_OK;

        }

       

        pCur --;

    }

    return E_FAIL;

}

The LoadXps method opens up an XPS file and locates its end central directory by reading backward from the end of the ZIP file.

HRESULT CXps::ExtractAllStreams()

{

    HRESULT hr = S_OK;

    const CCentralFileHeader * pFile = m_pFileHeader;

    for (int i = 0; SUCCEEDED(hr) && (i < m_pEndCentralDir->m_entryCentralDic); i ++)

    {

        hr = DecodePiece(m_View + pFile->m_offsetLocalHeader, pFile->m_compressedSize);

        pFile = (const CCentralFileHeader *) (pFile->m_Data + pFile->m_nFileName + pFile->m_nExtraField + pFile->m_nFileComment);

    }

  

    return hr;

}

int XpsExtract(const wchar_t * pFileName)

{

    CXps xpsFile;

    HRESULT hr = xpsFile.LoadXps(pFileName);

    if (SUCCEEDED(hr))

    {

        hr = xpsFile.ExtractAllStreams();

    }

    printf("\nhr = %x\n", hr);

    return 0;

}

The ExtractAllStreams method tries to extract every pieces in the XPS container to an file on the disk by calling DecodePiece method. The main routine XpsExtract is responsible for calling the loading routine and the main extracting routine.

The most complicated routine here is DecodePiece:

HRESULT CXps::DecodePiece(const BYTE * pCur, DWORD compressedSize)

{

    HRESULT hr = S_OK;

    const CLocalFileHeader * pHeader = (const CLocalFileHeader *) pCur;

    char * pName = new char[pHeader->m_nFileName + 1];

    if (pName == NULL)

    {

        return E_OUTOFMEMORY;

    }

    memcpy(pName, pHeader->m_Data, pHeader->m_nFileName);

    pName[pHeader->m_nFileName] = 0;

    printf("\n%8d %8d %-60s", pHeader->m_uncompressedSize, compressedSize, pName);

    HANDLE hFile = CreateDirFile(pName, pHeader->m_nFileName, hr);

    delete pName;

    if (FAILED(hr))

    {

        printf("\nUnable to open file\n");

        return hr;

    }

    pCur = pHeader->m_Data + pHeader->m_nFileName + pHeader->m_nExtraField;

   

    if (pHeader->m_compression != 0)

    {

        BYTE * pOutput = new BYTE[pHeader->m_uncompressedSize];

        if (pOutput == NULL)

        {

            return E_OUTOFMEMORY;

        }

        z_stream stream;

        memset(&stream, 0, sizeof(stream));

        stream.avail_in = compressedSize;

        stream.avail_out = pHeader->m_uncompressedSize;

        stream.next_in = (Bytef *) pCur;

        stream.next_out = pOutput;

        int result = inflateInit2(& stream, -MAX_WBITS);

        if (result == Z_OK)

        {

            result = inflate(& stream, Z_SYNC_FLUSH);

        }

        DWORD dwWritten;

        WriteFile(hFile, pOutput, pHeader->m_uncompressedSize, & dwWritten, NULL);

        delete [] pOutput;

    }

    else

    {

        DWORD dwWritten;

        WriteFile(hFile, pCur, pHeader->m_uncompressedSize, & dwWritten, NULL);

    }

    CloseHandle(hFile);

    return hr;

}

DecocePiece has two parameters: a pointer to local file header structure, and compressed size. Note the compressed size may not be stored in the local file header, so it's better to pass it in from the end of the container. From the header structure, we can get its name and call CreateDirFile to create a corresponding file on the disk, making sure the proper directory path is created. The ZLIB decompression routine inflate, together with its preparation routine inflateInit2 are called when the piece is actually compressed. The result uncompressed streams are then written to disk.

The structures not shown here can be easily created by following ZIP specification at https://www.pkware.com/documents/casestudies/APPNOTE.TXT. The CreateDirFile routine is a simple wrapper around CreateFile and CreateDirectory.

The program is implicitly linked with ZLIB1 ZDLL.