From 52a3d454abb9b38b1185f63104151e243507e3f0 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Andr=C3=A9=20Wobst?= Date: Sat, 14 Jul 2018 20:50:07 +0000 Subject: [PATCH] add UnicodeEngine (MultiEngineText and axis texters returning MultiEngineText), texter exponential and mixed combined into default texter, by joergl and wobsta in the course of several years git-svn-id: http://svn.code.sf.net/p/pyx/code/trunk/pyx@3688 a4f5e268-e194-4f32-bce1-d30804cbbcc5 --- examples/axis/log.py | 5 +- manual/axis.rst | 63 ++++----- pyx/box.py | 6 +- pyx/config.py | 3 + pyx/dvi/mapfile.py | 2 +- pyx/font/t1file.py | 180 ++++++++++++------------ pyx/graph/axis/axis.py | 2 +- pyx/graph/axis/texter.py | 277 +++++++++++++++++-------------------- pyx/text.py | 256 +++++++++++++++++++++++++++------- pyx/utils.py | 33 +++++ test/functional/Makefile | 2 +- test/functional/test_axis.py | 4 +- test/functional/test_timeaxis.py | 2 +- test/unit/test_texmessageparser.py | 2 +- test/unit/test_texter.py | 44 +++--- www/mkipynb.py | 2 +- 16 files changed, 523 insertions(+), 360 deletions(-) create mode 100644 pyx/utils.py diff --git a/examples/axis/log.py b/examples/axis/log.py index dbd15b88..bccf8276 100644 --- a/examples/axis/log.py +++ b/examples/axis/log.py @@ -1,12 +1,11 @@ -import math +import fractions, math from pyx import * from pyx.graph import axis # we here use parters and texters which are explained in the examples below log2parter = axis.parter.log([axis.parter.preexp([axis.tick.rational(1)], 4), axis.parter.preexp([axis.tick.rational(1)], 2)]) -log2texter = axis.texter.exponential(nomantissaexp=r"{2^{%s}}", - mantissamax=axis.tick.rational(2)) +log2texter = axis.texter.default(base=fractions.Fraction(2)) g = graph.graphxy(width=10, x=axis.log(min=1, max=1024), diff --git a/manual/axis.rst b/manual/axis.rst index b2f03ddd..781f48d3 100644 --- a/manual/axis.rst +++ b/manual/axis.rst @@ -499,12 +499,16 @@ label, but for which no label text has been specified so far. A typical case are ticks created by partitioners described above. -.. class:: decimal(prefix="", infix="", suffix="", equalprecision=0, decimalsep=".", thousandsep="", thousandthpartsep="", plus="", minus="-", period=r"\\overline{%s}", labelattrs=[text.mathmode]) +.. class:: decimal(prefix="", infix="", suffix="", equalprecision=False, decimalsep=".", thousandsep="", thousandthpartsep="", plus="", minus="-", period=r"\\overline{%s}", labelattrs=[text.mathmode]) Instances of this class create decimal formatted labels. The strings *prefix*, *infix*, and *suffix* are added to the label at the beginning, immediately after the plus or minus, and at the end, respectively. + + *equalprecision* forces the same number of digits after *decimalsep*, even + when the tailing digits are zero. + *decimalsep*, *thousandsep*, and *thousandthpartsep* are strings used to separate integer from fractional part and three-digit groups in the integer and fractional part. The strings *plus* and *minus* are inserted in front of the @@ -518,55 +522,40 @@ ticks created by partitioners described above. Text format options like ``text.size`` should instead be set at the painter. -.. class:: exponential(plus="", minus="-", mantissaexp=r"{{%s}\\cdot10^{%s}}", skipexp0=r"{%s}", skipexp1=None, nomantissaexp=r"{10^{%s}}", minusnomantissaexp=r"{-10^{%s}}", mantissamin=tick.rational((1, 1)), mantissamax=tick.rational((10L, 1)), skipmantissa1=0, skipallmantissa1=1, mantissatexter=decimal()) +.. class:: default(multiplication_tex=r"\cdot{}", multiplication_unicode="·", base=Fraction(10), skipmantissaunity=skipmantissaunity.all, minusunity="-", minexponent=4, minnegexponent=None, uniformexponent=True, mantissatexter=decimal(), basetexter=decimal(), exponenttexter=decimal(), labelattrs=[text.mathmode]) Instances of this class create decimal formatted labels with an exponential. - The strings *plus* and *minus* are inserted in front of the unsigned value of - the exponent. - - The format string *mantissaexp* should generate the exponent. It must contain - two string insert operators ``%s``, the first for the mantissa and the second - for the exponent. An alternative to the default is ``r"{{%s}{\rm e}{%s}}"``. + multiplication_tex and multiplication_unicode are the strings to indicate + the multiplication between the mantissa and the base number for the + TexEngine and the UnicodeEngine, respecitvely - The format string *skipexp0* is used to skip exponent ``0`` and must contain one - string insert operator ``%s`` for the mantissa. ``None`` turns off the special - handling of exponent ``0``. The format string *skipexp1* is similar to - *skipexp0*, but for exponent ``1``. + base is the number of the base of the exponent - The format string *nomantissaexp* is used to skip the mantissa ``1`` and must - contain one string insert operator ``%s`` for the exponent. ``None`` turns off - the special handling of mantissa ``1``. The format string *minusnomantissaexp* - is similar to *nomantissaexp*, but for mantissa ``-1``. + skipmantissaunity is either skipmantissaunity.never (never skip the unity + mantissa), skipmantissaunity.each (skip the unity mantissa whenever it occurs + for each label separately), or skipmantissaunity.all (skip the unity mantissa + whenever if all labels happen to be mantissafixed with unity) - The :class:`tick.rational` instances *mantissamin*< *mantissamax* are minimum - (including) and maximum (excluding) of the mantissa. + minusunity is used as the output of -unity for the mantissa - The boolean *skipmantissa1* enables the skipping of any mantissa equals ``1`` - and ``-1``, when *minusnomantissaexp* is set. When the boolean - *skipallmantissa1* is set, a mantissa equals ``1`` is skipped only, when all - mantissa values are ``1``. Skipping of a mantissa is stronger than the skipping - of an exponent. + minexponent is the minimal positive exponent value to be printed by exponential + notation - *mantissatexter* is a texter instance for the mantissa. + minnegexponent is the minimal negative exponent value to be printed by + exponential notation, for None it is considered to be equal to minexponent + uniformexponent forces all numbers to be written in exponential notation when at + least one label excets the limits for non-exponential notiation -.. class:: mixed(smallestdecimal=tick.rational((1, 1000)), biggestdecimal=tick.rational((9999, 1)), equaldecision=1, decimal=decimal(), exponential=exponential()) + mantissatexter, basetexter, and exponenttexter generate the texts for the + mantissa, basetexter, and exponenttexter - Instances of this class create decimal formatted labels with an exponential, - when the unsigned values are small or large compared to *1*. - - The rational instances *smallestdecimal* and *biggestdecimal* are the smallest - and biggest decimal values, where the decimal texter should be used. The sign of - the value is ignored here. For a tick at zero the decimal texter is considered - best as well. *equaldecision* is a boolean to indicate whether the decision for - the decimal or exponential texter should be done globally for all ticks. - - *decimal* and *exponential* are a decimal and an exponential texter instance, - respectively. + labelattrs is a list of attributes to be added to the label attributes given + in the painter""" -.. class:: rational(prefix="", infix="", suffix="", numprefix="", numinfix="", numsuffix="", denomprefix="", denominfix="", denomsuffix="", plus="", minus="-", minuspos=0, over=r"%s\\over%s", equaldenom=0, skip1=1, skipnum0=1, skipnum1=1, skipdenom1=1, labelattrs=[text.mathmode]) +.. class:: rational(prefix="", infix="", suffix="", numprefix="", numinfix="", numsuffix="", denomprefix="", denominfix="", denomsuffix="", plus="", minus="-", minuspos=0, over=r"%s\\over%s", equaldenom=False, skip1=True, skipnum0=True, skipnum1=True, skipdenom1=True, labelattrs=[text.mathmode]) Instances of this class create labels formated as fractions. diff --git a/pyx/box.py b/pyx/box.py index 13bec71e..2fb77e58 100644 --- a/pyx/box.py +++ b/pyx/box.py @@ -22,6 +22,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +### TODO: rename center to anchor + import types, math from . import bbox, path, unit, trafo @@ -58,9 +60,9 @@ class polygon_pt: pathitems.append(path.closepath()) return path.path(*pathitems) - def transform(self, *trafos): + def transform(self, *trafos, keep_anchor=False): for trafo in trafos: - if self.center is not None: + if self.center is not None and not keep_anchor: self.center = trafo.apply_pt(*self.center) self.corners = [trafo.apply_pt(*point) for point in self.corners] diff --git a/pyx/config.py b/pyx/config.py index a3611743..1a2b27ec 100644 --- a/pyx/config.py +++ b/pyx/config.py @@ -396,4 +396,7 @@ format.tex_ps_header = format("PostScript header", [".pro"]) format.type1 = format("type1 fonts", [".pfa", ".pfb"]) format.vf = format("vf", [".vf"]) format.dvips_config = format("dvips config", []) +format.ttf = format("truetype fonts", [".ttf", ".ttc", ".TTF", ".TTC", ".dfont"]) +format.t42 = format("type42 fonts", [".t42", ".T42"]) +format.otf = format("opentype fonts", [".otf"]) diff --git a/pyx/dvi/mapfile.py b/pyx/dvi/mapfile.py index 6d593081..ab6880ac 100644 --- a/pyx/dvi/mapfile.py +++ b/pyx/dvi/mapfile.py @@ -124,7 +124,7 @@ class MAPline: if self._font is None: if self.fontfilename is not None: with config.open(self.fontfilename, [config.format.type1]) as fontfile: - t1font = t1file.from_PF_bytes(fontfile.read()) + t1font = t1file.T1File.from_PF_bytes(fontfile.read()) assert self.basepsname == t1font.name, "corrupt MAP file" try: with config.open(os.path.splitext(self.fontfilename)[0], [config.format.afm], ascii=True) as metricfile: diff --git a/pyx/font/t1file.py b/pyx/font/t1file.py index bc12b674..4bd5e4e6 100644 --- a/pyx/font/t1file.py +++ b/pyx/font/t1file.py @@ -631,7 +631,10 @@ T1setcurrentpoint = _T1setcurrentpoint() ###################################################################### -class T1file: +class FontFormatError(Exception): + pass + +class T1File: eexecr = 55665 charstringr = 4330 @@ -1000,7 +1003,7 @@ class T1file: # when UniqueID is commented out (as in modern latin), prepare to remove the comment character as well def getstrippedfont(self, glyphs, charcodes): - """create a T1file instance containing only certain glyphs + """create a T1File instance containing only certain glyphs glyphs is a set of the glyph names. It might be modified *in place*! """ @@ -1038,7 +1041,7 @@ class T1file: data3 = self.newlinepattern.subn("\n", self.data3)[0] # create and return the new font instance - return T1file(data1.rstrip() + "\n", self._eexecencode(data2), data3.rstrip() + "\n") + return T1File(data1.rstrip() + "\n", self._eexecencode(data2), data3.rstrip() + "\n") # The following two methods, writePDFfontinfo and getglyphinfo, # extract informtion which should better be taken from the afm file. @@ -1085,7 +1088,7 @@ class T1file: return context.wx, context.wy, bbox.llx_pt, bbox.lly_pt, bbox.urx_pt, bbox.ury_pt def outputPFA(self, file, remove_UniqueID_lookup=False): - """output the T1file in PFA format""" + """output the T1File in PFA format""" data1 = self.data1 data3 = self.data3 if remove_UniqueID_lookup: @@ -1106,7 +1109,7 @@ class T1file: file.write(data3) def outputPFB(self, file): - """output the T1file in PFB format""" + """output the T1File in PFB format""" data2eexec = self.getdata2eexec() def pfblength(data): l = len(data) @@ -1126,7 +1129,7 @@ class T1file: file.write("\200\3") def outputPS(self, file, writer): - """output the PostScript code for the T1file to the file file""" + """output the PostScript code for the T1File to the file file""" self.outputPFA(file, remove_UniqueID_lookup=True) def outputPDF(self, file, writer): @@ -1156,86 +1159,87 @@ class T1file: file.write("\n" "endstream\n") -# factory functions - -class FontFormatError(Exception): - pass - -def from_PFA_bytes(bytes): - """create a T1file instance from a string of bytes corresponding to a PFA file""" - try: - m1 = bytes.index("eexec") + 6 - m2 = bytes.index("0"*40) - except ValueError: - raise FontFormatError - - data1 = bytes[:m1].decode("ascii", errors="surrogateescape") - data2eexec = binascii.a2b_hex(bytes[m1: m2].replace(" ", "").replace("\r", "").replace("\n", "")) - data3 = bytes[m2:].decode("ascii", errors="surrogateescape") - return T1file(data1, data2eexec, data3) - -def from_PFA_filename(filename): - """create a T1file instance from PFA font file of given name""" - with open(filename, "rb") as file: - t1file = from_PFA_bytes(file.read()) - return t1file - -def from_PFB_bytes(bytes): - """create a T1file instance from a string of bytes corresponding to a PFB file""" - - def pfblength(s): - if len(s) != 4: - raise ValueError("invalid string length") - return (s[0] + - s[1]*256 + - s[2]*256*256 + - s[3]*256*256*256) - class consumer: - def __init__(self, bytes): - self.bytes = bytes - self.pos = 0 - def __call__(self, n): - result = self.bytes[self.pos:self.pos+n] - self.pos += n - return result - - consume = consumer(bytes) - mark = consume(2) - if mark != b"\200\1": - raise FontFormatError - data1 = consume(pfblength(consume(4))).decode("ascii", errors="surrogateescape") - mark = consume(2) - if mark != b"\200\2": - raise FontFormatError - data2eexec = b"" - while mark == b"\200\2": - data2eexec = data2eexec + consume(pfblength(consume(4))) + @classmethod + def from_PFA_bytes(cls, bytes): + """create a T1File instance from a string of bytes corresponding to a PFA file""" + try: + m1 = bytes.index("eexec") + 6 + m2 = bytes.index("0"*40) + except ValueError: + raise FontFormatError + + data1 = bytes[:m1].decode("ascii", errors="surrogateescape") + data2eexec = binascii.a2b_hex(bytes[m1: m2].replace(" ", "").replace("\r", "").replace("\n", "")) + data3 = bytes[m2:].decode("ascii", errors="surrogateescape") + return cls(data1, data2eexec, data3) + + @classmethod + def from_PFA_filename(cls, filename): + """create a T1File instance from PFA font file of given name""" + with open(filename, "rb") as file: + t1file = cls.from_PFA_bytes(file.read()) + return t1file + + @classmethod + def from_PFB_bytes(cls, bytes): + """create a T1File instance from a string of bytes corresponding to a PFB file""" + + def pfblength(s): + if len(s) != 4: + raise ValueError("invalid string length") + return (s[0] + + s[1]*256 + + s[2]*256*256 + + s[3]*256*256*256) + class consumer: + def __init__(self, bytes): + self.bytes = bytes + self.pos = 0 + def __call__(self, n): + result = self.bytes[self.pos:self.pos+n] + self.pos += n + return result + + consume = consumer(bytes) + mark = consume(2) + if mark != b"\200\1": + raise FontFormatError + data1 = consume(pfblength(consume(4))).decode("ascii", errors="surrogateescape") mark = consume(2) - if mark != b"\200\1": - raise FontFormatError - data3 = consume(pfblength(consume(4))).decode("ascii", errors="surrogateescape") - mark = consume(2) - if mark != b"\200\3": - raise FontFormatError - if consume(1): - raise FontFormatError - - return T1file(data1, data2eexec, data3) - -def from_PFB_filename(filename): - """create a T1file instance from PFB font file of given name""" - with open(filename, "rb") as file: - t1file = from_PFB_bytes(file.read()) - return t1file - -def from_PF_bytes(bytes): - try: - return from_PFB_bytes(bytes) - except FontFormatError: - return from_PFA_bytes(bytes) - -def from_PF_filename(filename): - """create a T1file instance from PFA or PFB font file of given name""" - with open(filename, "rb") as file: - t1file = from_PF_bytes(file.read()) - return t1file + if mark != b"\200\2": + raise FontFormatError + data2eexec = b"" + while mark == b"\200\2": + data2eexec = data2eexec + consume(pfblength(consume(4))) + mark = consume(2) + if mark != b"\200\1": + raise FontFormatError + data3 = consume(pfblength(consume(4))).decode("ascii", errors="surrogateescape") + mark = consume(2) + if mark != b"\200\3": + raise FontFormatError + if consume(1): + raise FontFormatError + + return cls(data1, data2eexec, data3) + + @classmethod + def from_PFB_filename(cls, filename): + """create a T1File instance from PFB font file of given name""" + with open(filename, "rb") as file: + t1file = cls.from_PFB_bytes(file.read()) + return t1file + + @classmethod + def from_PF_bytes(cls, bytes): + try: + return cls.from_PFB_bytes(bytes) + except FontFormatError: + return cls.from_PFA_bytes(bytes) + + @classmethod + def from_PF_filename(cls, filename): + """create a T1File instance from PFA or PFB font file of given name""" + with open(filename, "rb") as file: + t1file = cls.from_PF_bytes(file.read()) + return t1file diff --git a/pyx/graph/axis/axis.py b/pyx/graph/axis/axis.py index 7c4fd1ba..ce52caed 100644 --- a/pyx/graph/axis/axis.py +++ b/pyx/graph/axis/axis.py @@ -63,7 +63,7 @@ class _regularaxis(_axis): logarithmic axes, time axes etc.""" def __init__(self, min=None, max=None, reverse=0, divisor=None, title=None, - painter=painter.regular(), texter=texter.mixed(), linkpainter=painter.linked(), + painter=painter.regular(), texter=texter.default(), linkpainter=painter.linked(), density=1, maxworse=2, manualticks=[], fallbackrange=None): if min is not None and max is not None and min > max: min, max, reverse = max, min, not reverse diff --git a/pyx/graph/axis/texter.py b/pyx/graph/axis/texter.py index ad1a5a69..f9fe4c49 100644 --- a/pyx/graph/axis/texter.py +++ b/pyx/graph/axis/texter.py @@ -21,31 +21,30 @@ # along with PyX; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +from fractions import Fraction -from pyx import text -from pyx.graph.axis import tick +from pyx import text, utils +from pyx.graph.axis.tick import tick as Tick -class _Itexter: - +class _texter: def labels(self, ticks): """fill the label attribute of ticks - ticks is a list of instances of tick - for each element of ticks the value of the attribute label is set to - a string appropriate to the attributes num and denom of that tick - instance + a string or MultiEngineText instance appropriate to the attributes + num and denom of that tick instance - label attributes of the tick instances are just kept, whenever they are not equal to None - the method might modify the labelattrs attribute of the ticks; be sure to not modify it in-place!""" + raise NotImplementedError -class decimal: +class decimal(_texter): "a texter creating decimal labels (e.g. '1.234' or even '0.\overline{3}')" - __implements__ = _Itexter - - def __init__(self, prefix="", infix="", suffix="", equalprecision=0, + def __init__(self, prefix="", infix="", suffix="", equalprecision=False, decimalsep=".", thousandsep="", thousandthpartsep="", plus="", minus="-", period=r"\overline{%s}", labelattrs=[text.mathmode]): @@ -53,6 +52,8 @@ class decimal: - prefix, infix, and suffix (strings) are added at the begin, immediately after the minus, and at the end of the label, respectively + - equalprecision forces the same number of digits after decimalsep, + even when the tailing digits are zero - decimalsep, thousandsep, and thousandthpartsep (strings) are used as separators - plus or minus (string) is inserted for non-negative or negative numbers @@ -133,169 +134,141 @@ class decimal: # del tick.temp_decprecision # we've inserted this temporary variable ... and do not care any longer about it -class exponential: - "a texter creating labels with exponentials (e.g. '2\cdot10^5')" - __implements__ = _Itexter +class skipmantissaunity: + pass + +skipmantissaunity.never = 0 +skipmantissaunity.each = 1 +skipmantissaunity.all = 2 + - def __init__(self, plus="", minus="-", - mantissaexp=r"{{%s}\cdot10^{%s}}", - skipexp0=r"{%s}", - skipexp1=None, - nomantissaexp=r"{10^{%s}}", - minusnomantissaexp=r"{-10^{%s}}", - mantissamin=tick.rational((1, 1)), mantissamax=tick.rational((10, 1)), - skipmantissa1=0, skipallmantissa1=1, - mantissatexter=decimal()): +class default(_texter): + + "a texter creating regular (e.g. '2') and exponential (e.g. '2\cdot10^5') labels" + + def __init__(self, multiplication_tex=r"\cdot{}", multiplication_unicode="·", base=Fraction(10), + skipmantissaunity=skipmantissaunity.all, minusunity="-", + minexponent=4, minnegexponent=None, uniformexponent=True, + mantissatexter=decimal(), basetexter=decimal(), exponenttexter=decimal(), + labelattrs=[text.mathmode]): + # , **kwargs): # future r"""initializes the instance - - plus or minus (string) is inserted for non-negative or negative exponents - - mantissaexp (string) is taken as a format string generating the exponent; - it has to contain exactly two string insert operators "%s" -- - the first for the mantissa and the second for the exponent; - examples are r"{{%s}\cdot10^{%s}}" and r"{{%s}{\rm e}{%s}}" - - skipexp0 (string) is taken as a format string used for exponent 0; - exactly one string insert operators "%s" for the mantissa; - None turns off the special handling of exponent 0; - an example is r"{%s}" - - skipexp1 (string) is taken as a format string used for exponent 1; - exactly one string insert operators "%s" for the mantissa; - None turns off the special handling of exponent 1; - an example is r"{{%s}\cdot10}" - - nomantissaexp (string) is taken as a format string generating the exponent - when the mantissa is one and should be skipped; it has to contain - exactly one string insert operators "%s" for the exponent; - an examples is r"{10^{%s}}" - - minusnomantissaexp (string) is taken as a format string generating the exponent - when the mantissa is minus one and should be skipped; it has to contain - exactly one string insert operators "%s" for the exponent; - None turns off the special handling of mantissa -1; - an examples is r"{-10^{%s}}" - - mantissamin and mantissamax are the minimum and maximum of the mantissa; - they are rational instances greater than zero and mantissamin < mantissamax; - the sign of the tick is ignored here - - skipmantissa1 (boolean) turns on skipping of any mantissa equals one - (and minus when minusnomantissaexp is set) - - skipallmantissa1 (boolean) as above, but all mantissas must be 1 (or -1) - - mantissatexter is the texter for the mantissa - - the skipping of a mantissa is stronger than the skipping of an exponent""" - self.plus = plus - self.minus = minus - self.mantissaexp = mantissaexp - self.skipexp0 = skipexp0 - self.skipexp1 = skipexp1 - self.nomantissaexp = nomantissaexp - self.minusnomantissaexp = minusnomantissaexp - self.mantissamin = mantissamin - self.mantissamax = mantissamax - self.mantissamindivmax = self.mantissamin / self.mantissamax - self.mantissamaxdivmin = self.mantissamax / self.mantissamin - self.skipmantissa1 = skipmantissa1 - self.skipallmantissa1 = skipallmantissa1 + - multiplication_tex and multiplication_unicode are the strings to + indicate the multiplication between the mantissa and the base + number for the TexEngine and the UnicodeEngine, respecitvely + - base is the number of the base of the exponent + - skipmantissaunity is either skipmantissaunity.never (never skip the + unity mantissa), skipmantissaunity.each (skip the unity mantissa + whenever it occurs for each label separately), or skipmantissaunity.all + (skip the unity mantissa whenever if all labels happen to be + mantissafixed with unity) + - minusunity is used as the output of -unity for the mantissa + - minexponent is the minimal positive exponent value to be printed by + exponential notation + - minnegexponent is the minimal negative exponent value to be printed by + exponential notation, for None it is considered to be equal to minexponent + - uniformexponent forces all numbers to be written in exponential notation + when at least one label excets the limits for non-exponential + notiation + - mantissatexter, basetexter, and exponenttexter generate the texts + for the mantissa, basetexter, and exponenttexter + - labelattrs is a list of attributes to be added to the label attributes + given in the painter""" + self.multiplication_tex = multiplication_tex + self.multiplication_unicode = multiplication_unicode + self.base = base + self.skipmantissaunity = skipmantissaunity + self.minusunity = minusunity + self.minexponent = minexponent + self.minnegexponent = minnegexponent if minnegexponent is not None else minexponent + self.uniformexponent = uniformexponent self.mantissatexter = mantissatexter + self.basetexter = basetexter + self.exponenttexter = exponenttexter + self.labelattrs = labelattrs + + # future: + # kwargs = utils.kwsplit(kwargs, ['mantissatexter', 'basetexter', 'exponenttexter']) + # self.mantissatexter = mantissatexter(a=1, **kwargs['mantissatexter']) + # self.basetexter = basetexter(**kwargs['basetexter']) + # self.exponenttexter = exponenttexter(**kwargs['exponenttexter']) def labels(self, ticks): labeledticks = [] for tick in ticks: if tick.label is None and tick.labellevel is not None: - tick.temp_orgnum, tick.temp_orgdenom = tick.num, tick.denom labeledticks.append(tick) - tick.temp_exp = 0 + + tick.labelattrs = tick.labelattrs + self.labelattrs + if tick.num: - while abs(tick) >= self.mantissamax: - tick.temp_exp += 1 - x = tick * self.mantissamindivmax - tick.num, tick.denom = x.num, x.denom - while abs(tick) < self.mantissamin: - tick.temp_exp -= 1 - x = tick * self.mantissamaxdivmin - tick.num, tick.denom = x.num, x.denom - if tick.temp_exp < 0: - tick.temp_exp = "%s%i" % (self.minus, -tick.temp_exp) + # express tick = tick.temp_sign * tick.temp_mantissa * self.base ** tick.temp_exponent with 1 <= temp_mantissa < self.base + # and decide whether a tick is to be written in exponential notation + tick.temp_sign = 1 if tick >= 0 else -1 + tick.temp_mantissa = abs(Fraction(tick.num, tick.denom)) + tick.temp_exponent = 0 + while tick.temp_mantissa >= self.base: + tick.temp_exponent += 1 + tick.temp_mantissa /= self.base + while tick.temp_mantissa < 1: + tick.temp_exponent -= 1 + tick.temp_mantissa *= self.base + tick.temp_wantexponent = not (-self.minnegexponent < tick.temp_exponent < self.minexponent) else: - tick.temp_exp = "%s%i" % (self.plus, tick.temp_exp) - self.mantissatexter.labels(labeledticks) - if self.minusnomantissaexp is not None: - allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if abs(tick.num) == abs(tick.denom)]) - else: - allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if tick.num == tick.denom]) + tick.temp_mantissa = tick.temp_exponent = 0 + tick.temp_sign = 1 + tick.temp_wantexponent = not (-self.minnegexponent < 0 < self.minexponent) + + # make decision on exponential notation uniform if requested + if self.uniformexponent and any(tick.temp_wantexponent for tick in labeledticks): + for tick in labeledticks: + if tick.num: + tick.temp_wantexponent = True + + # mark mantissa == 1 to be not labeled + if self.skipmantissaunity == skipmantissaunity.each: + for tick in labeledticks: + if tick.temp_wantexponent and tick.temp_mantissa == 1: + tick.temp_mantissa = None + elif self.skipmantissaunity == skipmantissaunity.all and all(tick.temp_mantissa == 1 for tick in labeledticks if tick.temp_wantexponent): + for tick in labeledticks: + if tick.temp_wantexponent: + tick.temp_mantissa = None + + # construct labels + basetick = Tick(self.base, labellevel=0) + self.basetexter.labels([basetick]) for tick in labeledticks: - if (self.skipallmantissa1 and allmantissa1 or - (self.skipmantissa1 and (tick.num == tick.denom or - (tick.num == -tick.denom and self.minusnomantissaexp is not None)))): - if tick.num == tick.denom: - tick.label = self.nomantissaexp % tick.temp_exp - else: - tick.label = self.minusnomantissaexp % tick.temp_exp + if tick.temp_wantexponent: + if tick.temp_mantissa is not None: + tick.temp_mantissatick = Tick(tick.temp_sign * tick.temp_mantissa, labellevel=0) + tick.temp_exponenttick = Tick(tick.temp_exponent, labellevel=0) else: - if tick.temp_exp == "0" and self.skipexp0 is not None: - tick.label = self.skipexp0 % tick.label - elif tick.temp_exp == "1" and self.skipexp1 is not None: - tick.label = self.skipexp1 % tick.label - else: - tick.label = self.mantissaexp % (tick.label, tick.temp_exp) - tick.num, tick.denom = tick.temp_orgnum, tick.temp_orgdenom - - # del tick.temp_orgnum # we've inserted those temporary variables ... and do not care any longer about them - # del tick.temp_orgdenom - # del tick.temp_exp - - -class mixed: - "a texter creating decimal or exponential labels" - - __implements__ = _Itexter - - def __init__(self, smallestdecimal=tick.rational((1, 1000)), - biggestdecimal=tick.rational((9999, 1)), - equaldecision=1, - decimal=decimal(), - exponential=exponential()): - """initializes the instance - - smallestdecimal and biggestdecimal are the smallest and - biggest decimal values, where the decimal texter should be used; - they are rational instances; the sign of the tick is ignored here; - a tick at zero is considered for the decimal texter as well - - equaldecision (boolean) uses decimal texter or exponential texter - globaly (set) or for each tick separately (unset) - - decimal and exponential are texters to be used""" - self.smallestdecimal = smallestdecimal - self.biggestdecimal = biggestdecimal - self.equaldecision = equaldecision - self.decimal = decimal - self.exponential = exponential + tick.temp_mantissatick = tick - def labels(self, ticks): - decticks = [] - expticks = [] - for tick in ticks: - if tick.label is None and tick.labellevel is not None: - if not tick.num or (abs(tick) >= self.smallestdecimal and abs(tick) <= self.biggestdecimal): - decticks.append(tick) + self.mantissatexter.labels([tick.temp_mantissatick for tick in labeledticks if tick.temp_mantissa is not None]) + self.exponenttexter.labels([tick.temp_exponenttick for tick in labeledticks if tick.temp_wantexponent]) + for tick in labeledticks: + if tick.temp_wantexponent: + if tick.temp_mantissa is not None: + mantissalabel_tex = tick.temp_mantissatick.label + self.multiplication_tex + mantissalabel_unicode = tick.temp_mantissatick.label + self.multiplication_unicode else: - expticks.append(tick) - if self.equaldecision: - if len(expticks): - self.exponential.labels(ticks) - else: - self.decimal.labels(ticks) - else: - for tick in decticks: - self.decimal.labels([tick]) - for tick in expticks: - self.exponential.labels([tick]) + mantissalabel_tex = self.minusunity if tick.temp_sign == -1 else "" + mantissalabel_unicode = self.minusunity if tick.temp_sign == -1 else "" + tick.label = text.MultiEngineText("%s%s^{%s}" % (mantissalabel_tex, basetick.label, tick.temp_exponenttick.label), [mantissalabel_unicode + basetick.label, text.Text(tick.temp_exponenttick.label, scale=0.8, shift=0.5)]) -class rational: +class rational(_texter): "a texter creating rational labels (e.g. 'a/b' or even 'a \over b')" - # XXX: we use divmod here to be more expicit - - __implements__ = _Itexter + # we use divmod here to be more explicit def __init__(self, prefix="", infix="", suffix="", numprefix="", numinfix="", numsuffix="", denomprefix="", denominfix="", denomsuffix="", plus="", minus="-", minuspos=0, over=r"{{%s}\over{%s}}", - equaldenom=0, skip1=1, skipnum0=1, skipnum1=1, skipdenom1=1, + equaldenom=False, skip1=True, skipnum0=True, skipnum1=True, skipdenom1=True, labelattrs=[text.mathmode]): r"""initializes the instance - prefix, infix, and suffix (strings) are added at the begin, @@ -436,11 +409,11 @@ class rational: else: tick.temp_rationalnum = "%s%s%s%i%s" % (self.numprefix, rationalnumminus, self.numinfix, tick.temp_rationalnum, self.numsuffix) if self.skipdenom1 and tick.temp_rationaldenom == 1 and not len(rationaldenomminus) and not len(self.denomprefix) and not len(self.denominfix) and not len(self.denomsuffix): - rational = tick.temp_rationalnum + tick.label = "%s%s%s%s%s" % (self.prefix, rationalminus, self.infix, tick.temp_rationalnum, self.suffix) else: tick.temp_rationaldenom = "%s%s%s%i%s" % (self.denomprefix, rationaldenomminus, self.denominfix, tick.temp_rationaldenom, self.denomsuffix) - rational = self.over % (tick.temp_rationalnum, tick.temp_rationaldenom) - tick.label = "%s%s%s%s%s" % (self.prefix, rationalminus, self.infix, rational, self.suffix) + tick.label = text.MultiEngineText("%s%s%s%s%s" % (self.prefix, rationalminus, self.infix, self.over % (tick.temp_rationalnum, tick.temp_rationaldenom), self.suffix), + ["%s%s%s" % (self.prefix, rationalminus, self.infix)] + [text.StackedText([text.Text(tick.temp_rationalnum, shift=0.3), text.Text(tick.temp_rationaldenom, shift=-0.9)], frac=True, align=0.5)] + [self.suffix]) tick.labelattrs = tick.labelattrs + self.labelattrs # del tick.temp_rationalnum # we've inserted those temporary variables ... and do not care any longer about them diff --git a/pyx/text.py b/pyx/text.py index 641180f2..fc301b52 100644 --- a/pyx/text.py +++ b/pyx/text.py @@ -21,7 +21,7 @@ import atexit, errno, functools, glob, inspect, io, itertools, logging, os import queue, re, shutil, sys, tempfile, textwrap, threading -from pyx import config, unit, box, baseclasses, trafo, version, attr, style, path +from pyx import config, unit, box, baseclasses, trafo, version, attr, style, path, canvas from pyx import bbox as bboxmodule from pyx.dvi import dvifile @@ -451,6 +451,9 @@ class halign(boxhalign, flushhalign, _localattr): def apply(self, expr): return r"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.boxhalign, self.flushhalign, expr) + def apply_trafo(self, textbox): + return textbox.transform(trafo.translate_pt(-self.boxhalign*textbox.bbox().width_pt(), 0), keep_anchor=True) + halign.left = halign(0, 0) halign.center = halign(0.5, 0.5) halign.right = halign(1, 1) @@ -463,12 +466,18 @@ halign.flushcenter = halign.raggedcenter = flushhalign.center halign.flushright = halign.raggedleft = flushhalign.right -class _mathmode(attr.attr, textattr, _localattr): +class _mathmode(attr.exclusiveattr, textattr, _localattr): "math mode" + def __init__(self): + attr.exclusiveattr.__init__(self, _mathmode) + def apply(self, expr): return r"$\displaystyle{%s}$" % expr + def apply_trafo(self, textbox): + pass + mathmode = _mathmode() clearmathmode = attr.clearclass(_mathmode) @@ -560,6 +569,9 @@ class _vshiftmathaxis(_vshift): def setheightexpr(self): return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0" + def apply_trafo(self, textbox): + return textbox.transform(trafo.translate_pt(0, -textbox.mathaxis_pt*unit.scale["x"]), keep_anchor=True) + vshift.bottomzero = vshift(0) vshift.middlezero = vshift(0.5) @@ -828,17 +840,15 @@ class textextbox_pt(textbox_pt): self.fillstyles = fillstyles self.texttrafo = trafo.scale(unit.scale["x"]).translated_pt(x_pt, y_pt) -# box.rect_pt.__init__(self, x_pt - left_pt*unit.scale["x"], y_pt - depth_pt*unit.scale["x"], -# (left_pt + right_pt)*unit.scale["x"], -# (depth_pt + height_pt)*unit.scale["x"], -# abscenter_pt = (left_pt*unit.scale["x"], depth_pt*unit.scale["x"])) - box.rect.__init__(self, -self.left, -self.depth, self.left+self.right, self.depth+self.height, abscenter = (self.left, self.depth)) - box.rect.transform(self, self.texttrafo) + box.rect_pt.__init__(self, x_pt - left_pt*unit.scale["x"], y_pt - depth_pt*unit.scale["x"], + (left_pt + right_pt)*unit.scale["x"], + (depth_pt + height_pt)*unit.scale["x"], + abscenter_pt = (left_pt*unit.scale["x"], depth_pt*unit.scale["x"])) self._dvicanvas = None - def transform(self, *trafos): - box.rect.transform(self, *trafos) + def transform(self, *trafos, keep_anchor=False): + box.rect.transform(self, *trafos, keep_anchor=keep_anchor) for trafo in trafos: self.texttrafo = trafo * self.texttrafo if self._dvicanvas is not None: @@ -1262,7 +1272,8 @@ class SingleRunner: :param float x_pt: x position in pts :param float y_pt: y position in pts - :param str expr: text to be typeset + :param expr: text to be typeset + :type expr: str or :class:`MultiEngineText` :param textattrs: styles and attributes to be applied to the text :type textattrs: list of :class:`textattr, :class:`trafo.trafo_pt`, and :class:`style.fillstyle` @@ -1285,6 +1296,8 @@ class SingleRunner: trafos = attr.getattrs(textattrs, [trafo.trafo_pt]) fillstyles = attr.getattrs(textattrs, [style.fillstyle]) textattrs = attr.getattrs(textattrs, [textattr]) + if isinstance(expr, MultiEngineText): + expr = expr.tex for ta in textattrs[::-1]: expr = ta.apply(expr) first = self.state < STATE_TYPESET @@ -1494,7 +1507,7 @@ class MultiRunner: self.preambles = [] -class TexRunner(MultiRunner): +class TexEngine(MultiRunner): def __init__(self, *args, **kwargs): """A restartable :class:`SingleTexRunner` class @@ -1506,7 +1519,7 @@ class TexRunner(MultiRunner): super().__init__(SingleTexRunner, *args, **kwargs) -class LatexRunner(MultiRunner): +class LatexEngine(MultiRunner): def __init__(self, *args, **kwargs): """A restartable :class:`SingleLatexRunner` class @@ -1518,60 +1531,158 @@ class LatexRunner(MultiRunner): super().__init__(SingleLatexRunner, *args, **kwargs) +from pyx import deco from pyx.font import T1font -from pyx.font.t1file import from_PF_bytes +from pyx.font.t1file import T1File from pyx.font.afmfile import AFMfile -class unicodetextbox_pt(textbox_pt): - def __init__(self, x_pt, y_pt, text, font, size): +class MultiEngineText: + + def __init__(self, tex, unicode): + self.tex = tex + self.unicode = unicode + + +class Text: + + def __init__(self, text, scale=1, shift=0): self.text = text + self.scale = scale + self.shift = shift + + +class StackedText: + + def __init__(self, texts, frac=False, align=0): + assert not frac or len(texts) == 2 + self.texts = texts + self.frac = frac + self.align = align + + +class unicodetextbox_pt(textbox_pt): + + def __init__(self, x_pt, y_pt, texts, font, size, mathmode=False): self.font = font self.size = size + self.canvas = canvas.canvas() self.texttrafo = trafo.scale(unit.scale["x"]).translated_pt(x_pt, y_pt) - bbox = self.font.text_pt(0, 0, text, size).bbox() - box.rect_pt.__init__(self, -bbox.llx_pt, -bbox.lly_pt, -bbox.llx_pt+bbox.urx_pt, -bbox.lly_pt+bbox.ury_pt, abscenter_pt = (-bbox.llx_pt, -bbox.lly_pt)) + + if isinstance(texts, (str, Text, StackedText)): + texts = [texts] + + x_pt = 0 + for text in texts: + if isinstance(text, str): + text = Text(text) + if isinstance(text, Text): + if mathmode: + text_fragments = text.text.split('-') + else: + text_fragments = [text.text] + for i, text_fragment in enumerate(text_fragments): + if i: + self.canvas.fill(path.rect_pt(x_pt+0.5*(self.minuswidth_pt-self.minuslength_pt), self.mathaxis_pt-0.5*self.minusthickness_pt, self.minuslength_pt, self.minusthickness_pt)) + x_pt += self.minuswidth_pt + if text_fragment: + try: + t = self.font.text_pt(x_pt, text.shift*self.size, text_fragment, text.scale*self.size) + x_pt += t.bbox().width_pt() + except: + assert '·' in text_fragment + t = self.font.text_pt(x_pt, text.shift*self.size, text_fragment.replace('·', 'x'), text.scale*self.size) + x_pt += t.bbox().width_pt() + self.canvas.insert(t) + else: + assert isinstance(text, StackedText) + shift = self.mathaxis_pt if text.frac else 0 + ts = [self.font.text_pt(x_pt, text.shift*self.size+shift, text.text, text.scale*self.size) + for text in text.texts] + width_pt = max(t.bbox().width_pt() for t in ts) + if text.frac: + self.canvas.fill(path.rect_pt(x_pt, self.mathaxis_pt-0.5*self.minusthickness_pt, width_pt, self.minusthickness_pt)) + for t in ts: + self.canvas.insert(t, [trafo.translate_pt(text.align*(width_pt-t.bbox().width_pt()), 0)] if text.align else []) + x_pt += width_pt + + bbox = self.canvas.bbox() + bbox.includepoint_pt(0, 0) + bbox.includepoint_pt(x_pt, 0) + box.rect_pt.__init__(self, bbox.llx_pt, bbox.lly_pt, bbox.urx_pt-bbox.llx_pt, bbox.ury_pt-bbox.lly_pt, abscenter_pt = (0, 0)) box.rect.transform(self, self.texttrafo) - def transform(self, *trafos): - box.rect.transform(self, *trafos) + def _extract_minus_properties(self): + minus_path = self.font.text_pt(0, 0, '=', 1).textpath().normpath() + minus_path.normsubpaths = [normsubpath for normsubpath in minus_path.normsubpaths if normsubpath] + self.font.minusthickness_pt = max(normsubpath.bbox().height_pt() for normsubpath in minus_path.normsubpaths) + self.font.halfminuswidth_pt, self.font.mathaxis_pt = minus_path.bbox().center_pt() + self.font.minuslength_pt = minus_path.bbox().width_pt() + + @property + def mathaxis_pt(self): + if not hasattr(self.font, "mathaxis_pt"): + self._extract_minus_properties() + return self.font.mathaxis_pt*self.size + + @property + def minuswidth_pt(self): + if not hasattr(self.font, "halfminuswidth_pt"): + self._extract_minus_properties() + return 2*self.font.halfminuswidth_pt*self.size + + @property + def minuslength_pt(self): + if not hasattr(self.font, "minuslength_pt"): + self._extract_minus_properties() + return self.font.minuslength_pt*self.size + + @property + def minusthickness_pt(self): + if not hasattr(self.font, "minusthickness_pt"): + self._extract_minus_properties() + return self.font.minusthickness_pt*self.size + + def transform(self, *trafos, keep_anchor=False): + box.rect.transform(self, *trafos, keep_anchor=keep_anchor) for trafo in trafos: self.texttrafo = trafo * self.texttrafo def bbox(self): - scale, x_pt, y_pt = self.homothety() - return self.font.text_pt(x_pt, y_pt, self.text, scale*self.size).bbox() - - def homothety(self): - assert self.texttrafo.matrix[0][0] == self.texttrafo.matrix[1][1] - assert self.texttrafo.matrix[0][1] == 0 - assert self.texttrafo.matrix[1][0] == 0 - return self.texttrafo.matrix[0][0], self.texttrafo.vector[0], self.texttrafo.vector[1] + return self.canvas.bbox().transformed(self.texttrafo) def textpath(self): - scale, x_pt, y_pt = self.homothety() - return self.font.text_pt(x_pt, y_pt, self.text, scale*self.size).textpath() - - def requiretextregion(self): - return True + r = path.path() + for item in self.canvas.items: + if isinstance(item, canvas.canvas): + for subitem in item.items: + r += subitem.textpath().transformed(item.trafo) + elif isinstance(item, deco.decoratedpath): + r += item.path + else: + r += item.textpath() + return r.transformed(self.texttrafo) def processPS(self, file, writer, context, registry, bbox): - scale, x_pt, y_pt = self.homothety() - self.font.text_pt(x_pt, y_pt, self.text, scale*self.size).processPS(file, writer, context, registry, bbox) + c = canvas.canvas([self.texttrafo]) + c.insert(self.canvas) + c.processPS(file, writer, context, registry, bbox) def processPDF(self, file, writer, context, registry, bbox): - scale, x_pt, y_pt = self.homothety() - self.font.text_pt(x_pt, y_pt, self.text, scale*self.size).processPDF(file, writer, context, registry, bbox) + c = canvas.canvas([self.texttrafo]) + c.insert(self.canvas) + c.processPDF(file, writer, context, registry, bbox) def processSVG(self, xml, writer, context, registry, bbox): - scale, x_pt, y_pt = self.homothety() - self.font.text_pt(x_pt, y_pt, self.text, scale*self.size).processSVG(xml, writer, context, registry, bbox) + c = canvas.canvas([self.texttrafo]) + c.insert(self.canvas) + c.processSVG(xml, writer, context, registry, bbox) -class UnicodeText: +class UnicodeEngine: def __init__(self, fontname="cmr10", size=10): - self.font = T1font(from_PF_bytes(config.open(fontname, [config.format.type1]).read()), + self.font = T1font(T1File.from_PF_bytes(config.open(fontname, [config.format.type1]).read()), AFMfile(config.open(fontname, [config.format.afm], ascii=True))) self.size = size @@ -1582,16 +1693,51 @@ class UnicodeText: raise NotImplemented() def text_pt(self, x_pt, y_pt, text, textattrs=[], texmessages=[], fontmap=None, singlecharmode=False): - # def text_pt(self, x_pt, y_pt, text, *args, **kwargs): - return unicodetextbox_pt(x_pt, y_pt, text, self.font, self.size) + + textattrs = attr.mergeattrs(textattrs) # perform cleans + attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle]) + trafos = attr.getattrs(textattrs, [trafo.trafo_pt]) + fillstyles = attr.getattrs(textattrs, [style.fillstyle]) + textattrs = attr.getattrs(textattrs, [textattr]) + mathmode = bool(attr.getattrs(textattrs, [_mathmode])) + + if isinstance(text, MultiEngineText): + text = text.unicode + output = unicodetextbox_pt(x_pt, y_pt, text, self.font, self.size, mathmode=mathmode) + for ta in textattrs: # reverse?! + ta.apply_trafo(output) + + return output def text(self, x, y, *args, **kwargs): return self.text_pt(unit.topt(x), unit.topt(y), *args, **kwargs) +# from pyx.font.otffile import OpenTypeFont +# +# class OTFUnicodeText: +# +# def __init__(self, fontname, size=10): +# self.font = OpenTypeFont(config.open(fontname, [config.format.ttf])) +# self.size = size +# +# def preamble(self): +# raise NotImplemented() +# +# def reset(self): +# raise NotImplemented() +# +# def text_pt(self, x_pt, y_pt, text, textattrs=[], texmessages=[], fontmap=None, singlecharmode=False): +# # def text_pt(self, x_pt, y_pt, text, *args, **kwargs): +# return unicodetextbox_pt(x_pt, y_pt, text, self.font, self.size) +# +# def text(self, x, y, *args, **kwargs): +# return self.text_pt(unit.topt(x), unit.topt(y), *args, **kwargs) + + # old, deprecated names: -texrunner = TexRunner -latexrunner = LatexRunner +texrunner = TexRunner = TexEngine +latexrunner = LatexRunner = LatexEngine # module level interface documentation for autodoc # the actual values are setup by the set function @@ -1611,7 +1757,7 @@ text = None #: default_runner.reset (bound method) reset = None -def set(cls=TexRunner, mode=None, *args, **kwargs): +def set(engine=None, cls=None, mode=None, *args, **kwargs): """Setup a new module level :class:`MultiRunner` :param cls: the module level :class:`MultiRunner` to be used, i.e. @@ -1627,19 +1773,25 @@ def set(cls=TexRunner, mode=None, *args, **kwargs): :param dict kwargs: keyword args at at class instantiation """ - # note: defaulttexrunner is deprecated - global default_runner, defaulttexrunner, reset, preamble, text, text_pt + # note: default_runner and defaulttexrunner are deprecated + global default_engine, default_runner, defaulttexrunner, reset, preamble, text, text_pt if mode is not None: - logger.warning("mode setting is deprecated, use the cls argument instead") - cls = {"tex": TexRunner, "latex": LatexRunner}[mode.lower()] - default_runner = defaulttexrunner = cls(*args, **kwargs) + logger.warning("mode setting is deprecated, use the engine argument instead") + assert cls is None + assert engine is None + engine = {"tex": TexEngine, "latex": LatexEngine}[mode.lower()] + elif cls is not None: + logger.warning("cls setting is deprecated, use the engine argument instead") + assert not engine + engine = cls + default_engine = default_runner = defaulttexrunner = engine(*args, **kwargs) preamble = default_runner.preamble text_pt = default_runner.text_pt text = default_runner.text reset = default_runner.reset # initialize default_runner -set() +set({"TexEngine": TexEngine, "LatexEngine": LatexEngine, "UnicodeEngine": UnicodeEngine}[config.get('text', 'default_engine', 'TexEngine')]) def escapestring(s, replace={" ": "~", diff --git a/pyx/utils.py b/pyx/utils.py new file mode 100644 index 00000000..00c060e6 --- /dev/null +++ b/pyx/utils.py @@ -0,0 +1,33 @@ +# -*- encoding: utf-8 -*- +# +# +# Copyright (C) 2017 Jörg Lehmann +# Copyright (C) 2017 André Wobst +# +# This file is part of PyX (http://pyx.sourceforge.net/). +# +# PyX is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# PyX is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PyX; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +def kwsplit(kwargs, parts): + # TODO: document by example + # >>> kwsplit({'aaa_bbb': 'ccc'}, ['aaa']) + # {'aaa': {'bbb': 'ccc'}} + result = {part: {} for part in parts} + for key, value in kwargs.items(): + part, subkey = key.split('_', 1) + if part not in parts: + raise ValueError('invalid part name %s in kwargs' % part) + result[part][subkey] = value + return result diff --git a/test/functional/Makefile b/test/functional/Makefile index f9747b4a..d0ce8292 100644 --- a/test/functional/Makefile +++ b/test/functional/Makefile @@ -1,7 +1,7 @@ PYTHON ?= python3 pyfiles = $(wildcard *.py) -# pyfiles = $(filter-out test_connector.py test_deformer.py,$(wildcard *.py)) +pyfiles = $(filter-out test_connector.py test_deformer.py,$(wildcard *.py)) epsfiles = $(patsubst %.py, %.eps, $(pyfiles)) all: diff --git a/test/functional/test_axis.py b/test/functional/test_axis.py index eee506d2..93c28a71 100755 --- a/test/functional/test_axis.py +++ b/test/functional/test_axis.py @@ -5,7 +5,7 @@ import math from pyx import * from pyx.graph.axis.parter import linear as linparter from pyx.graph.axis.painter import regular, ticklength, rotatetext -from pyx.graph.axis.texter import rational, exponential +from pyx.graph.axis.texter import rational, default from pyx.graph.axis.axis import lin, pathaxis c = canvas.canvas() @@ -27,7 +27,7 @@ c.insert(pathaxis(path.path(path.moveto(11, 0), path.lineto(11, 8)), c.insert(pathaxis(path.path(path.moveto(12, 0), path.lineto(12, 8)), lin(painter=regular(tickattrs=[attr.changelist([None, color.rgb.green])]), **lintest))) c.insert(pathaxis(path.path(path.moveto(16, 0), path.lineto(16, 8)), - lin(texter=exponential(), **lintest), + lin(texter=default(), **lintest), direction=-1)) c.insert(pathaxis(path.path(path.moveto(18, 0), path.lineto(18, 8)), lin(texter=rational(), **lintest), diff --git a/test/functional/test_timeaxis.py b/test/functional/test_timeaxis.py index 4aa6ec02..70e04d85 100755 --- a/test/functional/test_timeaxis.py +++ b/test/functional/test_timeaxis.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import sys; sys.path[:0] = ["../.."] -import time +import time, datetime from pyx import * from pyx.graph.axis import timeaxis from pyx.graph import data diff --git a/test/unit/test_texmessageparser.py b/test/unit/test_texmessageparser.py index 08d11103..02da2c61 100644 --- a/test/unit/test_texmessageparser.py +++ b/test/unit/test_texmessageparser.py @@ -84,7 +84,7 @@ Underfull \vbox (badness 10000) detected at line 0""")) os.remove(testfilename + ".eps") def setUp(self): - text.set(mode="latex") + text.set(engine=text.LatexEngine) text.preamble(r"\usepackage{graphicx}") if __name__ == "__main__": diff --git a/test/unit/test_texter.py b/test/unit/test_texter.py index 74cd7750..d10e9a79 100644 --- a/test/unit/test_texter.py +++ b/test/unit/test_texter.py @@ -5,8 +5,15 @@ if sys.path[0] != "../..": import unittest from pyx import * +from pyx.text import MultiEngineText from pyx.graph.axis.tick import tick -from pyx.graph.axis.texter import rational, decimal, exponential, mixed +from pyx.graph.axis.texter import rational, decimal, default, skipmantissaunity + + +def tex_expr(label): + if isinstance(label, MultiEngineText): + return label.tex + return label class TexterTestCase(unittest.TestCase): @@ -14,13 +21,13 @@ class TexterTestCase(unittest.TestCase): def testFrac(self): ticks = [tick((1, 4), labellevel=0), tick((2, 4), labellevel=0)] rational(numsuffix=r"\pi").labels(ticks) - self.assertEqual([t.label for t in ticks], [r"{{\pi}\over{4}}", r"{{\pi}\over{2}}"]) + self.assertEqual([tex_expr(t.label) for t in ticks], [r"{{\pi}\over{4}}", r"{{\pi}\over{2}}"]) ticks = [tick((0, 3), labellevel=0), tick((3, 3), labellevel=0), tick((6, 3), labellevel=0)] rational(numsuffix=r"\pi").labels(ticks) - self.assertEqual([t.label for t in ticks], ["0", r"\pi", r"2\pi"]) + self.assertEqual([tex_expr(t.label) for t in ticks], ["0", r"\pi", r"2\pi"]) ticks = [tick((2, 3), labellevel=0), tick((4, 5), labellevel=0)] rational(numsuffix=r"\pi", equaldenom=1).labels(ticks) - self.assertEqual([t.label for t in ticks], [r"{{10\pi}\over{15}}", r"{{12\pi}\over{15}}"]) + self.assertEqual([tex_expr(t.label) for t in ticks], [r"{{10\pi}\over{15}}", r"{{12\pi}\over{15}}"]) def testDec(self): ticks = [tick((1, 4), labellevel=0), tick((2, 4), labellevel=0)] @@ -39,24 +46,25 @@ class TexterTestCase(unittest.TestCase): decimal(thousandsep=",").labels(ticks) self.assertEqual([t.label for t in ticks], ["1,000,000", "10,000,000", "100,000,000"]) - def testExp(self): - ticks = [tick((-1, 10), labellevel=0), tick((1, 1), labellevel=0), tick((10, 1), labellevel=0)] - exponential().labels(ticks) - self.assertEqual([t.label for t in ticks], [r"{-10^{-1}}", r"{10^{0}}", r"{10^{1}}"]) - ticks = [tick((0, 1), labellevel=0), tick((1, -10), labellevel=0), tick((15, 100), labellevel=0)] - exponential(mantissatexter=decimal(equalprecision=1)).labels(ticks) - self.assertEqual([t.label for t in ticks], [r"{0.0}", r"{{-1.0}\cdot10^{-1}}", r"{{1.5}\cdot10^{-1}}"]) - def testDefault(self): ticks = [tick((0, 10), labellevel=0), tick((1, 10), labellevel=0), tick((1, 1), labellevel=0), tick((10, 1), labellevel=0)] - mixed().labels(ticks) - self.assertEqual([t.label for t in ticks], ["0", "0.1", "1", "10"]) + default().labels(ticks) + self.assertEqual([tex_expr(t.label) for t in ticks], ["0", "0.1", "1", "10"]) ticks = [tick((0, 10), labellevel=0), tick((1, 10), labellevel=0), tick((1, 1), labellevel=0), tick((10000, 1), labellevel=0)] - mixed().labels(ticks) - self.assertEqual([t.label for t in ticks], [r"{0}", r"{{1}\cdot10^{-1}}", r"{1}", r"{{1}\cdot10^{4}}"]) + default(minexp=1, skipmantissaunity=skipmantissaunity.never, uniformexp=False).labels(ticks) + self.assertEqual([tex_expr(t.label) for t in ticks], [r"0", r"1\cdot{}10^{-1}", r"1", r"1\cdot{}10^{4}"]) ticks = [tick((0, 10), labellevel=0), tick((1, 10), labellevel=0), tick((1, 1), labellevel=0), tick((10000, 1), labellevel=0)] - mixed(equaldecision=0).labels(ticks) - self.assertEqual([t.label for t in ticks], ["0", "0.1", "1", r"{10^{4}}"]) + default(minexp=2, uniformexp=False).labels(ticks) + self.assertEqual([tex_expr(t.label) for t in ticks], ["0", "0.1", "1", r"10^{4}"]) + ticks = [tick((-1, 10), labellevel=0), tick((1, 1), labellevel=0), tick((10, 1), labellevel=0)] + default(minexp=0).labels(ticks) + self.assertEqual([tex_expr(t.label) for t in ticks], [r"-10^{-1}", r"10^{0}", r"10^{1}"]) + ticks = [tick((0, 1), labellevel=0), tick((1, -10), labellevel=0), tick((15, 100), labellevel=0)] + default(minnegexp=0, mantissatexter=decimal(equalprecision=True)).labels(ticks) + self.assertEqual([tex_expr(t.label) for t in ticks], [r"0.0\cdot{}10^{0}", r"-1.0\cdot{}10^{-1}", r"1.5\cdot{}10^{-1}"]) + ticks = [tick((0, 1), labellevel=0), tick((1, -10), labellevel=0), tick((15, 100), labellevel=0)] + default(minnegexp=1, mantissatexter=decimal(equalprecision=True)).labels(ticks) + self.assertEqual([tex_expr(t.label) for t in ticks], [r"0.0", r"-1.0\cdot{}10^{-1}", r"1.5\cdot{}10^{-1}"]) if __name__ == "__main__": diff --git a/www/mkipynb.py b/www/mkipynb.py index e738c474..9bc94193 100644 --- a/www/mkipynb.py +++ b/www/mkipynb.py @@ -1,5 +1,5 @@ import base64, os, re, sys -import IPython.nbformat.v4 as nbf +import nbformat.v4 as nbf filename = os.path.splitext(sys.argv[1])[0] -- 2.11.4.GIT