import base64 import errno import io import json import logging import os import shutil import subprocess import uuid from django.conf import settings from django.utils.translation import gettext logger = logging.getLogger("judge.problem.pdf") HAS_SELENIUM = False if settings.USE_SELENIUM: try: from selenium import webdriver from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait HAS_SELENIUM = True except ImportError: logger.warning("Failed to import Selenium", exc_info=True) HAS_PHANTOMJS = os.access(settings.PHANTOMJS, os.X_OK) HAS_SLIMERJS = os.access(settings.SLIMERJS, os.X_OK) NODE_PATH = settings.NODEJS PUPPETEER_MODULE = settings.PUPPETEER_MODULE HAS_PUPPETEER = os.access(NODE_PATH, os.X_OK) and os.path.isdir(PUPPETEER_MODULE) HAS_PDF = os.path.isdir(settings.DMOJ_PDF_PROBLEM_CACHE) and ( HAS_PHANTOMJS or HAS_SLIMERJS or HAS_PUPPETEER or HAS_SELENIUM ) EXIFTOOL = settings.EXIFTOOL HAS_EXIFTOOL = os.access(EXIFTOOL, os.X_OK) class BasePdfMaker(object): math_engine = "jax" title = None def __init__(self, dir=None, clean_up=True): self.dir = dir or os.path.join( settings.DMOJ_PDF_PROBLEM_TEMP_DIR, str(uuid.uuid1()) ) self.proc = None self.log = None self.htmlfile = os.path.join(self.dir, "input.html") self.pdffile = os.path.join(self.dir, "output.pdf") self.clean_up = clean_up def load(self, file, source): with open(os.path.join(self.dir, file), "w") as target, open(source) as source: target.write(source.read()) def make(self, debug=False): self._make(debug) if self.title and HAS_EXIFTOOL: try: subprocess.check_output( [EXIFTOOL, "-Title=%s" % (self.title,), self.pdffile] ) except subprocess.CalledProcessError as e: logger.error( "Failed to run exiftool to set title for: %s\n%s", self.title, e.output, ) def _make(self, debug): raise NotImplementedError() @property def html(self): with io.open(self.htmlfile, encoding="utf-8") as f: return f.read() @html.setter def html(self, data): with io.open(self.htmlfile, "w", encoding="utf-8") as f: f.write(data) @property def success(self): return self.proc.returncode == 0 @property def created(self): return os.path.exists(self.pdffile) def __enter__(self): try: os.makedirs(self.dir) except OSError as e: if e.errno != errno.EEXIST: raise return self def __exit__(self, exc_type, exc_val, exc_tb): if self.clean_up: shutil.rmtree(self.dir, ignore_errors=True) class PhantomJSPdfMaker(BasePdfMaker): template = """\ "use strict"; var page = require('webpage').create(); var param = {params}; page.paperSize = { format: param.paper, orientation: 'portrait', margin: '1cm', footer: { height: '1cm', contents: phantom.callback(function(num, pages) { return ('
' + param.footer.replace('[page]', num).replace('[topage]', pages) + '
'); }) } }; page.onCallback = function (data) { if (data.action === 'snapshot') { page.render(param.output); phantom.exit(); } } page.open(param.input, function (status) { if (status !== 'success') { console.log('Unable to load the address!'); phantom.exit(1); } else { page.evaluate(function (zoom) { document.documentElement.style.zoom = zoom; }, param.zoom); window.setTimeout(function () { page.render(param.output); phantom.exit(); }, param.timeout); } }); """ def get_render_script(self): return self.template.replace( "{params}", json.dumps( { "zoom": settings.PHANTOMJS_PDF_ZOOM, "timeout": int(settings.PHANTOMJS_PDF_TIMEOUT * 1000), "input": "input.html", "output": "output.pdf", "paper": settings.PHANTOMJS_PAPER_SIZE, "footer": gettext("Page [page] of [topage]"), } ), ) def _make(self, debug): with io.open(os.path.join(self.dir, "_render.js"), "w", encoding="utf-8") as f: f.write(self.get_render_script()) cmdline = [settings.PHANTOMJS, "_render.js"] self.proc = subprocess.Popen( cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir ) self.log = self.proc.communicate()[0] class SlimerJSPdfMaker(BasePdfMaker): math_engine = "mml" template = """\ "use strict"; try { var param = {params}; var {Cc, Ci} = require('chrome'); var prefs = Cc['@mozilla.org/preferences-service;1'].getService(Ci.nsIPrefService); // Changing the serif font so that printed footers show up as Segoe UI. var branch = prefs.getBranch('font.name.serif.'); branch.setCharPref('x-western', 'Segoe UI'); var page = require('webpage').create(); page.paperSize = { format: param.paper, orientation: 'portrait', margin: '1cm', edge: '0.5cm', footerStr: { left: '', right: '', center: param.footer } }; page.open(param.input, function (status) { if (status !== 'success') { console.log('Unable to load the address!'); slimer.exit(1); } else { page.render(param.output, { ratio: param.zoom }); slimer.exit(); } }); } catch (e) { console.error(e); slimer.exit(1); } """ def get_render_script(self): return self.template.replace( "{params}", json.dumps( { "zoom": settings.SLIMERJS_PDF_ZOOM, "input": "input.html", "output": "output.pdf", "paper": settings.SLIMERJS_PAPER_SIZE, "footer": gettext("Page [page] of [topage]") .replace("[page]", "&P") .replace("[topage]", "&L"), } ), ) def _make(self, debug): with io.open(os.path.join(self.dir, "_render.js"), "w", encoding="utf-8") as f: f.write(self.get_render_script()) env = None firefox = settings.SLIMERJS_FIREFOX_PATH if firefox: env = os.environ.copy() env["SLIMERJSLAUNCHER"] = firefox cmdline = [settings.SLIMERJS, "--headless", "_render.js"] self.proc = subprocess.Popen( cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir, env=env, ) self.log = self.proc.communicate()[0] class PuppeteerPDFRender(BasePdfMaker): template = """\ "use strict"; const param = {params}; const puppeteer = require('puppeteer'); puppeteer.launch().then(browser => Promise.resolve() .then(async () => { const page = await browser.newPage(); await page.goto(param.input, { waitUntil: 'load' }); await page.waitForSelector('.math-loaded', { timeout: 15000 }); await page.pdf({ path: param.output, format: param.paper, margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm', }, printBackground: true, displayHeaderFooter: true, headerTemplate: '
', footerTemplate: '
' + param.footer.replace('[page]', '') .replace('[topage]', '') + '
', }); await browser.close(); }) .catch(e => browser.close().then(() => {throw e})) ).catch(e => { console.error(e); process.exit(1); }); """ def get_render_script(self): return self.template.replace( "{params}", json.dumps( { "input": "file://%s" % self.htmlfile, "output": self.pdffile, "paper": settings.PUPPETEER_PAPER_SIZE, "footer": gettext("Page [page] of [topage]"), } ), ) def _make(self, debug): with io.open(os.path.join(self.dir, "_render.js"), "w", encoding="utf-8") as f: f.write(self.get_render_script()) env = os.environ.copy() env["NODE_PATH"] = os.path.dirname(PUPPETEER_MODULE) cmdline = [NODE_PATH, "_render.js"] self.proc = subprocess.Popen( cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir, env=env, ) self.log = self.proc.communicate()[0] class SeleniumPDFRender(BasePdfMaker): success = False template = { "printBackground": True, "displayHeaderFooter": True, "headerTemplate": "
", "footerTemplate": '
' + gettext("Page %s of %s") % ('', '') + "
", } def get_log(self, driver): return "\n".join(map(str, driver.get_log("driver") + driver.get_log("browser"))) def _make(self, debug): options = webdriver.ChromeOptions() options.add_argument("--headless") options.add_argument("--no-sandbox") # for root options.binary_location = settings.SELENIUM_CUSTOM_CHROME_PATH browser = webdriver.Chrome(settings.SELENIUM_CHROMEDRIVER_PATH, options=options) browser.get("file://%s" % self.htmlfile) self.log = self.get_log(browser) try: WebDriverWait(browser, 15).until( EC.presence_of_element_located((By.CLASS_NAME, "math-loaded")) ) except TimeoutException: logger.error("PDF math rendering timed out") self.log = self.get_log(browser) + "\nPDF math rendering timed out" browser.quit() return response = browser.execute_cdp_cmd("Page.printToPDF", self.template) self.log = self.get_log(browser) if not response: browser.quit() return with open(self.pdffile, "wb") as f: f.write(base64.b64decode(response["data"])) self.success = True browser.quit() if HAS_PUPPETEER: DefaultPdfMaker = PuppeteerPDFRender elif HAS_SELENIUM: DefaultPdfMaker = SeleniumPDFRender elif HAS_SLIMERJS: DefaultPdfMaker = SlimerJSPdfMaker elif HAS_PHANTOMJS: DefaultPdfMaker = PhantomJSPdfMaker else: DefaultPdfMaker = None