pdftron

How to convert .xod file to .pdf/.xps PDFTron


I've use docpub to create a xod file with a pass to view in my PDFTron WebViewer application, but I ended up losing the original pdf file and now I would like to have it back. I have the xod encrypted file and the password. Can I convert it back to a pdf?

I tried to use the webviewer with react, but seems not possible to save the pdf file and docpub only has:
./DocPub –f xod --xod_encrypt_password secret license.pdf

Any ideas?


Solution

  • Yes, you can decrypt it manually and use the PDFNetPython library to convert it back to PDF:

    import sys
    import os
    import zlib
    import binascii
    import struct
    import argparse
    
    from PDFNetPython3 import *
    from Crypto.Cipher import AES
    
    YOUR_PDFTRON_LICENSE_KEY = "<INSERT_LICENSE_KEY>"
    
    def main():
        parser = argparse.ArgumentParser(description='Converts password-protected XOD files to PDF files')
        parser.add_argument('xod_file', type=str, help='path to the encrypted XOD file')
        parser.add_argument('password', type=str, help='the password to decrypt the file...')
    
        args = parser.parse_args()
    
        # alright.
    
        fileHandler = open(args.xod_file,"rb")
        password = args.password
    
        file = bytearray(fileHandler.read())
    
        new_file = bytearray()
        new_file[:] = file
        centeral_directory_end = file[-22:]
        centeral_directory_location = to_dword(centeral_directory_end[16:16+4])
        centeral_directory_size = to_dword(centeral_directory_end[12:12+4])
        centeral_directory = file[centeral_directory_location : centeral_directory_location + centeral_directory_size]
    
        print("[+] file loaded. starting to decrypt the file...")
        while len(centeral_directory) >= 42:
            file_name = centeral_directory[46:46 + to_short(centeral_directory[28:28+2])]
            start = to_dword(centeral_directory[42:42+4]) + 30 + len(file_name) + to_dword(centeral_directory[30:30+4])
            size = to_dword(centeral_directory[20:20+4])
    
            key, iv, encrypted_data = key_and_iv_from_password(bytearray(new_file[start:start+size]), file_name.decode("utf-8"), password)
            aes = AES.new(key, AES.MODE_CBC, iv)
            print("[+] decrypting part " + file_name.decode("utf-8") + " with key " + toHex(key))
            decrypted_data = aes.decrypt(encrypted_data)
            
            new_centeral_directory_index = fix_offsets(new_file, file_name, len(decrypted_data))
            del new_file[start:start+size]
            new_file[start:start] = bytearray(decrypted_data)
            centeral_directory = new_file[new_centeral_directory_index:]
            if centeral_directory[0] != 0x50:
                print("problem")
                break
            file_header_size = 46 + len(file_name) + to_short(centeral_directory[30:30+2]) + to_short(centeral_directory[32:32+2])
            centeral_directory = centeral_directory[file_header_size:]
    
        print("[+] everything decrypted. converting to pdf...")
        new_file_handler = open("new.xod","wb")
        new_file_handler.write(new_file)
        new_file_handler.close()
        toPdf("new.xod","result.pdf")
        os.remove("new.xod")
        print("[+] Done!")
    
    # some helper functions
    
    def toPdf(xodFilename, outputPdfName):
        PDFNet.Initialize(YOUR_PDFTRON_LICENSE_KEY)
        pdf_doc = PDFDoc()
        Convert.ToPdf(pdf_doc, xodFilename)
        pdf_doc.Save(outputPdfName, SDFDoc.e_remove_unused)
        pdf_doc.Close()
    
    def toXod(pdfFilename, outputXodName):
        pdf_doc = PDFDoc()
        Convert.ToXod(pdfFilename, outputXodName)
    
    def toHex(byte_array):
        if byte_array is str:
            byte_array = bytearray(byte_array)
        hex = str(binascii.hexlify(byte_array))
        formatted_hex = ':'.join(hex[i:i+2] for i in range(0, len(hex), 2))
        return formatted_hex
    
    def to_dword(byte_array):
        return struct.unpack('I', byte_array)[0]
    
    def to_short(byte_array):
        return struct.unpack('H', byte_array)[0]
    
    def key_and_iv_from_password(encrypted_data, filename, password):
        key = bytearray([0] * 16)
        for i in range(16):
            key[i] = i
            if i < len(password):
                key[i] |= ord(password[i])
            g = len(filename) + i - 16
            if 0 <= g:
                key[i] |= ord(filename[g])
        
        iv = []
        for i in range(16):
            iv.append(encrypted_data[i])
        encrypted_data = encrypted_data[16:]
    
        return (key, bytearray(iv), encrypted_data)
    
    def fix_offsets(array, alterted_filename, new_size):
        centeral_directory_end = array[-22:]
        centeral_directory_location = to_dword(centeral_directory_end[16:16+4])
        centeral_directory_size = to_dword(centeral_directory_end[12:12+4])
        file_offset_from_centeral_directory = 0
        index = 0
        offset_fix_delta = 0
    
        # fixing the alterted-file's local header
        while index < centeral_directory_location:   
            file_name = array[index + 30: index + 30 + to_short(array[index+26:index+26+2])]
            if file_name == alterted_filename:
                offset_fix_delta = new_size - to_dword(array[index + 18:index+18+4])
                array[index + 18:index+18+4] = struct.pack('I', new_size)
                break
            index += 30 + len(file_name) + to_short(array[index + 28:index+28+2]) + to_dword(array[index+18:index+18+4])
    
        # fixing the centeral directory
        index = centeral_directory_location
        should_fix_offset = False
        while index < centeral_directory_location + centeral_directory_size:  
            file_name = array[index + 46:index + 46 + to_short(array[index+28:index+28+2])]
            if file_name == alterted_filename:
                should_fix_offset = True
                array[index+20:index+20+4] = struct.pack('I', new_size)
                file_offset_from_centeral_directory = index-centeral_directory_location
            elif should_fix_offset:
                array[index +42:index+42+4] = struct.pack('I', to_dword(array[index +42:index+42+4]) + offset_fix_delta)
            file_header_size = 46 + len(file_name) + to_short(array[index+30:index+30+2]) + to_short(array[index+32:index+32+2])
            index += file_header_size
    
        array[-6:-6+4]= struct.pack('I', to_dword(array[-6:-6+4]) + offset_fix_delta)
        return to_dword(array[-6:-6+4]) + file_offset_from_centeral_directory
    
    if __name__ == "__main__":
        main()
    

    Make sure to install dependencies first:

    pip3 install pycryptodome PDFNetPython3

    And then just use it like so:

    python decrypt_xod.py some_xod_file.xod MySecretPassword