1#!/usr/bin/env python3
2# This file is part of Xpra.
3# Copyright (C) 2017-2020 Antoine Martin <antoine@xpra.org>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
6
7import os
8import sys
9from ctypes.wintypes import HDC
10from ctypes import WinDLL, c_void_p, Structure, c_int, c_uint, c_ulong, c_char_p, cast, pointer, POINTER
11
12from xpra.util import ellipsizer
13from xpra.os_util import strtobytes
14from xpra.platform.win32.common import GetDeviceCaps
15from xpra.platform.win32 import win32con
16from xpra.platform.win32.win32_printing import GDIPrinterContext, DOCINFO, StartDocA, EndDoc, LPCSTR
17
18LIBPDFIUMDLL = os.environ.get("XPRA_LIBPDFIUMDLL", "pdfium.dll")
19try:
20    pdfium = WinDLL(LIBPDFIUMDLL, use_last_error=True)
21except WindowsError as e:        #@UndefinedVariable
22    raise ImportError("cannot load %s: %s" % (LIBPDFIUMDLL, e)) from None
23
24class FPDF_LIBRARY_CONFIG(Structure):
25    _fields_ = [
26        ("m_pUserFontPaths",    c_void_p),
27        ("version",                c_int),
28        ("m_pIsolate",            c_void_p),
29        ("m_v8EmbedderSlot",    c_uint),
30        ]
31
32FPDF_DOCUMENT = c_void_p
33FPDF_PAGE = c_void_p
34
35FPDF_DestroyLibrary = pdfium.FPDF_DestroyLibrary
36FPDF_InitLibraryWithConfig = pdfium.FPDF_InitLibraryWithConfig
37FPDF_InitLibraryWithConfig.argtypes = [POINTER(FPDF_LIBRARY_CONFIG)]
38FPDF_GetLastError = pdfium.FPDF_GetLastError
39FPDF_GetLastError.restype = c_ulong
40FPDF_GetPageCount = pdfium.FPDF_GetPageCount
41FPDF_GetPageCount.argtypes = [FPDF_DOCUMENT]
42FPDF_GetPageCount.restype = c_int
43FPDF_LoadPage = pdfium.FPDF_LoadPage
44FPDF_LoadPage.argtypes = [FPDF_DOCUMENT, c_int]
45FPDF_RenderPage = pdfium.FPDF_RenderPage
46FPDF_RenderPage.argtypes = [HDC, FPDF_PAGE, c_int, c_int, c_int, c_int, c_int, c_int]
47FPDF_LoadMemDocument = pdfium.FPDF_LoadMemDocument
48FPDF_LoadMemDocument.restype = FPDF_DOCUMENT
49FPDF_LoadMemDocument.argtypes = [c_void_p, c_int, c_void_p]
50FPDF_CloseDocument = pdfium.FPDF_CloseDocument
51FPDF_CloseDocument.argtypes = [FPDF_DOCUMENT]
52
53FPDF_ERR_SUCCESS = 0    # No error.
54FPDF_ERR_UNKNOWN = 1    # Unknown error.
55FPDF_ERR_FILE = 2         # File not found or could not be opened.
56FPDF_ERR_FORMAT = 3     # File not in PDF format or corrupted.
57FPDF_ERR_PASSWORD = 4   # Password required or incorrect password.
58FPDF_ERR_SECURITY = 5   # Unsupported security scheme.
59FPDF_ERR_PAGE = 6       # Page not found or content error.
60FPDF_ERR_XFALOAD = 7    # Load XFA error.
61FPDF_ERR_XFALAYOUT = 8  # Layout XFA error.
62
63ERROR_STR = {
64    #FPDF_ERR_SUCCESS : No error.
65    FPDF_ERR_UNKNOWN     : "Unknown error",
66    FPDF_ERR_FILE         : "File not found or could not be opened",
67    FPDF_ERR_FORMAT        : "File not in PDF format or corrupted",
68    FPDF_ERR_PASSWORD     : "Password required or incorrect password",
69    FPDF_ERR_SECURITY     : "Unsupported security scheme",
70    FPDF_ERR_PAGE         : "Page not found or content error",
71    FPDF_ERR_XFALOAD     : "Load XFA error",
72    FPDF_ERR_XFALAYOUT     : "Layout XFA error",
73    }
74
75FPDF_ANNOT = 0x01
76FPDF_LCD_TEXT = 0x02
77FPDF_NO_NATIVETEXT = 0x04
78FPDF_GRAYSCALE = 0x08
79FPDF_DEBUG_INFO = 0x80
80FPDF_NO_CATCH = 0x100
81FPDF_RENDER_LIMITEDIMAGECACHE = 0x200
82FPDF_RENDER_FORCEHALFTONE = 0x400
83FPDF_PRINTING = 0x800
84FPDF_RENDER_NO_SMOOTHTEXT = 0x1000
85FPDF_RENDER_NO_SMOOTHIMAGE = 0x2000
86FPDF_RENDER_NO_SMOOTHPATH = 0x4000
87FPDF_REVERSE_BYTE_ORDER = 0x10
88
89def get_error():
90    global ERROR_STR
91    v = FPDF_GetLastError()
92    return ERROR_STR.get(v, v)
93
94def do_print_pdf(hdc, title=b"PDF Print Test", pdf_data=None):
95    assert pdf_data, "no pdf data"
96    from xpra.log import Logger
97    log = Logger("printing", "win32")
98    log("pdfium=%s", pdfium)
99    buf = c_char_p(pdf_data)
100    log("pdf data buffer: %s", ellipsizer(pdf_data))
101    log("FPDF_InitLibraryWithConfig=%s", FPDF_InitLibraryWithConfig)
102    config = FPDF_LIBRARY_CONFIG()
103    config.m_pUserFontPaths = None
104    config.version = 2
105    config.m_pIsolate = None
106    config.m_v8EmbedderSlot = 0
107    FPDF_InitLibraryWithConfig(config)
108    x = 0
109    y = 0
110    w = GetDeviceCaps(hdc, win32con.HORZRES)
111    h = GetDeviceCaps(hdc, win32con.VERTRES)
112    rotate = 0
113    log("printer device size: %ix%i", w, h)
114    flags = FPDF_PRINTING | FPDF_DEBUG_INFO
115    try:
116        doc = FPDF_LoadMemDocument(cast(buf, c_void_p), len(pdf_data), None)
117        if not doc:
118            log.error("Error: FPDF_LoadMemDocument failed, error: %s", get_error())
119            return -1
120        log("FPDF_LoadMemDocument(..)=%s", doc)
121        count = FPDF_GetPageCount(doc)
122        log("FPDF_GetPageCount(%s)=%s", doc, count)
123        docinfo = DOCINFO()
124        docinfo.lpszDocName = LPCSTR(b"%s\0" % title)
125        jobid = StartDocA(hdc, pointer(docinfo))
126        if jobid<0:
127            log.error("Error: StartDocA failed: %i", jobid)
128            return jobid
129        log("StartDocA()=%i", jobid)
130        try:
131            for i in range(count):
132                page = FPDF_LoadPage(doc, i)
133                if not page:
134                    log.error("Error: FPDF_LoadPage failed for page %i, error: %s", i, get_error())
135                    return -2
136                log("FPDF_LoadPage()=%s page %i loaded", page, i)
137                FPDF_RenderPage(hdc, page, x, y, w, h, rotate, flags)
138                log("FPDF_RenderPage page %i rendered", i)
139        finally:
140            EndDoc(hdc)
141    finally:
142        FPDF_DestroyLibrary()
143    return jobid
144
145def print_pdf(printer_name, title, pdf_data):
146    with GDIPrinterContext(printer_name) as hdc:
147        return do_print_pdf(hdc, title, pdf_data)
148
149
150EXIT = False
151JOBS_INFO = {}
152def watch_print_job_status():
153    global JOBS_INFO, EXIT
154    from xpra.log import Logger
155    log = Logger("printing", "win32")
156    log("wait_for_print_job_end()")
157    #log("wait_for_print_job_end(%i)", print_job_id)
158    from xpra.platform.win32.printer_notify import wait_for_print_job_info, job_status
159    while not EXIT:
160        info = wait_for_print_job_info(timeout=1.0)
161        if not info:
162            continue
163        log("wait_for_print_job_info()=%s", info)
164        for nd in info:
165            job_id, key, value = nd
166            if key=='job_status':
167                value = job_status(value)
168            log("job_id=%s, key=%s, value=%s", job_id, key, value)
169            JOBS_INFO.setdefault(job_id, {})[key] = value
170
171
172def main():
173    global JOBS_INFO, EXIT
174    if len(sys.argv) not in (2, 3, 4):
175        print("usage: %s /path/to/document.pdf [printer-name] [document-title]" % sys.argv[0])
176        return -3
177    filename = sys.argv[1]
178    with open(filename, 'rb') as f:
179        pdf_data = f.read()
180
181    if len(sys.argv)==2:
182        from xpra.platform.win32.printing import get_printers
183        printers = get_printers()
184        printer_name = strtobytes(printers.keys()[0])
185    if len(sys.argv) in (3, 4):
186        printer_name = strtobytes(sys.argv[2])
187    if len(sys.argv)==4:
188        title = strtobytes(sys.argv[3])
189    else:
190        title = strtobytes(os.path.basename(filename))
191
192    import time
193    from xpra.util import csv
194    from xpra.log import Logger
195    log = Logger("printing", "win32")
196
197    #start a new thread before submitting the document,
198    #because otherwise the job may complete before we can get its status
199    from threading import Thread
200    t = Thread(target=watch_print_job_status, name="watch print job status")
201    t.daemon = True
202    t.start()
203
204    job_id = print_pdf(printer_name, title, pdf_data)
205    if job_id<0:
206        return job_id
207    #wait for job to end:
208    job_status = None
209    while True:
210        job_info = JOBS_INFO.get(job_id, {})
211        log("job_info[%i]=%s", job_id, job_info)
212        v = job_info.get("job_status")
213        if v!=job_status:
214            log.info("print job status: %s", csv(v))
215            job_status = v
216            if "OFFLINE" in job_status or "DELETING" in job_status:
217                EXIT = True
218                break
219        time.sleep(1.0)
220    return 0
221
222
223if __name__ == "__main__":
224    sys.exit(main())
225