from __future__ import with_statement from __future__ import division import tempfile, os, re import mimetypes import subprocess from . import formats from .dependencies import Dependencies class Intermed(object): def __init__(self,metadata={}): self.md = metadata self.path = None self.extension = None self.data = None self.deps = Dependencies() self.cached = False def isPath(self): return self.path is not None def asPath(self): if self.path is None: assert self.extension.startswith('.'),self.extension fdesc,self.path = tempfile.mkstemp(suffix=self.extension) os.write(fdesc,self.data) os.close(fdesc) return self.path def asData(self): if self.data is None: with open(self.path) as f: self.data = f.read() self.nix() return self.data def getExtension(self): return self.extension def nix(self): if self.path is not None: os.remove(self.path) self.path = None def metadata(self): return self.md def addPropvalDep(self, elm, propname): self.deps.addPropvalDep(elm, propname) def addDeps(self,deps): self.deps.update(deps) def getDeps(self): return self.deps def setData(self,data,extension): self.nix() self.extension = unicode(extension) self.data = data self.path = None return self def setPath(self,path): self.nix() self.path = path self.extension = u'.'+path.rsplit('.',1)[-1] self.data = None PIXELS_PER_IN = 96 class Length(object): def __init__(self, length, unit): self.length = length self.unit = unit def to_px(self): if self.unit == 'in': return int(PIXELS_PER_IN * self.length) else: assert False, (self.length, self.unit) def to_str(self): return "%s%s" % (self.length, self.unit) def to_dim(s): if s == '?': return None elif s.endswith('in'): return Length(float(s[:-2]), 'in') else: return int(s) def parse_dimension(arg): if arg.startswith('!'): arg = arg[1:] force=True else: force=False if 'x' in arg: w,h = (to_dim(b) for b in arg.split('x')) else: w = h = int(arg) return w,h,force def parse_dimension_px(arg): w, h, force = parse_dimension(arg) if hasattr(w, 'to_px'): w = w.to_px() if hasattr(h, 'to_px'): h = h.to_px() return w, h, force def rsvg_convert(im,format='.png'): args = [] force = False if 'd' in im.metadata()['filters']: w,h,force = parse_dimension_px(im.metadata()['filters']['d']) del im.metadata()['filters']['d'] # This takes width and height in pt, where 1pt=1.25px, and as integers. # But only when converting to .svg, to be confusing. if format == '.svg': mw = int(w/1.25) mh = int(h/1.25) else: mw = w mh = h if force: args.append('-w') args.append(str(mw)) args.append('-h') args.append(str(mh)) else: ow, oh = formats.rsvg_dimensions(im) # We need to pass just one -w or -h. If we pass both, it makes # it at least that large with -a, but we want at most that large. args.append('-a') if ow / oh > w / h: args.append('-w') args.append(str(mw)) h = int(oh / ow * w) else: args.append('-h') args.append(str(mh)) w = int(ow / oh * h) else: w = h = None args.append('-f') args.append(format[1:]) if format == '.pdf': # For PDF we have to output to a file and then convert to PDF 1.3, # otherwise XeTeX barfs. fdesc, pdftmp = tempfile.mkstemp(suffix='.pdf') os.close(fdesc) args.append('-o') args.append(pdftmp) p=subprocess.Popen(['rsvg-convert']+args+[im.asPath()], stdout=subprocess.PIPE,stderr=subprocess.PIPE) out,err = p.communicate() if p.returncode != 0: # Error messages get to stdout, which is dumb. raise OSError( "rsvg-convert failed with status %s: %s" % (p.returncode, out+err)) if format == '.pdf': fdesc, pdf13tmp = tempfile.mkstemp(suffix='.pdf') os.close(fdesc) # Works to convert pdfs, too. p2 = subprocess.Popen(['ps2pdf13', pdftmp, pdf13tmp], stderr=subprocess.PIPE) out, err = p2.communicate() if p2.returncode != 0: raise OSError("ps2pdf13 failed with status %s: %s" % (p2.returncode, err)) os.unlink(pdftmp) im.setPath(pdf13tmp) else: im.setData(out,format) if w is not None: im.metadata()['width'] = w if h is not None: im.metadata()['height'] = h FORMAT_TO_PSTOEDIT = {'.svg' : 'plot-svg', '.eps' : 'ps', '.epdf': 'gs:pdfwrite', } def pstoedit_convert(im, format): assert format in FORMAT_TO_PSTOEDIT cmd = ['pstoedit', '-f', FORMAT_TO_PSTOEDIT[format], '-pti', im.asPath()] p=subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() if p.returncode != 0: raise OSError(p.returncode, err) im.setData(out, format) def get_pdfinfo(im): from pyPdf import PdfFileReader pdf = PdfFileReader(open(im.asPath(), 'rb')) im.setData("%s pages" % pdf.getNumPages(), 'pdfinfo') # TRANSLATORS maps dest to a map that maps source to function. # So, if TRANSLATORS['.pdf']['.tex'] is func, then func takes # an Intermed that refers to '.tex' formatted data and sets # that Intermed with '.pdf' formatted data or a appropriate path. # # Formats can start with a '.', in which case they represent complete # files, or without a '.', in which case they represent incomplete # fragments. # # A translator should either call asData or asPath, whichever is more # convenient, but only once. # # Anything that calls asPath but doesn't call setData or setPath # should call nix when it's done with the file at that path. TRANSLATORS = {'.txt': {'txt':lambda im: im.setData(im.data.encode('utf-8'), '.txt')}, '.png': {'.svg':lambda im:rsvg_convert(im, '.png')}, '.epdf': {'.svg':lambda im:rsvg_convert(im, '.pdf')}, '.eps': {'.svg':lambda im:rsvg_convert(im, '.eps')}, 'pdfinfo': {'.pdf': get_pdfinfo}, } # Need a helper function b/c python doesn't freeze the environment of a lambda def _trans(func, format): def translator(im): return func(im, format) return translator for to_f in FORMAT_TO_PSTOEDIT: for from_f in formats.PSTOEDIT_READABLE_FORMATS: if to_f != from_f: if from_f not in TRANSLATORS.setdefault(to_f, {}): TRANSLATORS[to_f][from_f] = _trans(pstoedit_convert, to_f) # Overrides for mimetypes.guess_extension MIME_MAP = {"image/svg+xml": ".svg", "text/plain": ".txt", "application/x-empty": ".txt", "text/x-c": ".txt", "text/html": ".html", "image/jpeg": ".jpg"} # Overrides for mimetypes.guess_type EXT_MAP = {'.svg': 'image/svg+xml', '.tex': 'text/plain'} def guess_extension(mime): if mime in MIME_MAP: return MIME_MAP[mime] else: ext = mimetypes.guess_extension(mime) #assert ext is not None,mime if ext is None: return ".dat" return ext def guess_type(filename): ext = '.' + filename.rsplit('.', 1)[-1] if ext in EXT_MAP: return EXT_MAP[ext], None return mimetypes.guess_type(filename, strict=False)