"""
This class mangaes PDF creation via LaTeX.
"""
import os
import subprocess
import math
import numpy as np
# ==================================================
[docs]
def wrap_string(a, left="", right=""):
"""
Wrap array elementwise with left and right strings.
Args:
a (array-like): array.
left (str, optional): left string to wrap.
right (str, optional): right string to wrap.
Returns:
- (array-like) -- wrapped array.
"""
return np.frompyfunc(lambda i: left + i + right, 1, 1)(np.array(a, dtype=str))
# ==================================================
[docs]
def regularize_table(lst2d, padding=None):
"""
Regularize an irregular table.
Args:
lst2d (list): 2d list with different number of columns.
padding (any, optional): padding value.
Returns:
- (list) -- regularized 2d list.
"""
if type(lst2d) != list:
raise KeyError(f"non list type ({type(lst2d)}) is given.")
col = max(map(len, lst2d))
tbl = []
for r in lst2d:
p = [padding] * col
p[: len(r)] = r
tbl.append(p)
return tbl
# ==================================================
[docs]
def list_to_table(lst1d, col, p=None):
"""
Convert from list to table.
Args:
lst1d (list): 1d list.
col (int): number of columns.
p (any, optional): padding value (no padding for None).
Returns:
- (list) -- 2d list.
"""
if type(lst1d) != list:
raise KeyError(f"non list type ({type(lst1d)}) is given.")
n = len(lst1d)
row = int(math.ceil(n / col))
tbl = []
for i in range(row):
tbl.append(lst1d[col * i : col * (i + 1)])
d = row * col - n
if p is not None and d != 0:
tbl[-1].extend([p] * d)
return tbl
# ==================================================
[docs]
class PDFviaLaTeX:
"""
PDF creation via LaTeX.
"""
# Attributes:
# __content (list): text in each line [str]
# __package (list): package [(name, option)]
# __twice (bool): compile twice ?
# __fname (str): file name without ".tex"
# __dir (str): output directory
# __opt (dict): option
# __replace (list): replace string at creation
# __pos (dict): position string
#
# ==================================================
def __init__(self, fname, package=[], style="normal", pt=10, no_page=False, landscape=False, english=False, dir=""):
"""
PDF creator class.
Args:
fname (str): file name without ".tex".
package (list, optional): package [(name,option)].
style (str, optional): margin style (narrowest/narrow/normal/wide/widest).
pt (int, optional): default point (8/9/10/11/12/14).
no_page (bool, optional): no page number ?
landscape (bool, optional): landscape ?
english (bool, optional): english mode ?
dir (str, optional): output directory.
"""
self.__content = []
self.__twice = False
self.__fname = fname
self.__dir = dir if dir else "."
self.__opt = {"style": style, "pt": pt, "no_page": no_page, "landscape": landscape, "english": english}
self.__package = [
("color", "usenames"),
("amsmath", ""),
("breqn", ""),
("graphicx", ""),
("amssymb", ""),
("bm", ""),
("enumitem", ""),
("braket", ""),
("multirow", ""),
("booktabs", ""),
("longtable", ""),
("caption", ""),
] #: LaTeX package (name, option)
self.__package += package
self.__replace = [
(r"\left[\begin{matrix}", r"\begin{pmatrix}"),
(r"\end{matrix}\right]", r"\end{pmatrix}"),
] #: replace string (target, replace)
self.__pos = {"c": "center", "l": "flushleft", "r": "flushright"}
# ==================================================
[docs]
def build(self):
"""
Create PDF.
"""
txt = self._create_source(content=self.__content, package=self.__package, **self.__opt)
for x in self.__replace:
txt = txt.replace(*x)
pdfdir = self.__dir
cwd = os.getcwd()
os.chdir(pdfdir)
f = open(self.__fname + ".tex", mode="wt")
f.write(txt)
f.close()
cmd = f"ptex2pdf -l -ot -synctex=0 -halt-on-error {self.__fname}.tex" # for texlive.
rm_file = [self.__fname + ext for ext in [".aux", ".log"]]
try:
subprocess.run(cmd.split(), capture_output=True, check=True, cwd=pdfdir)
if self.__twice:
subprocess.run(cmd.split(), capture_output=True, check=True, cwd=pdfdir)
except subprocess.CalledProcessError:
raise Exception(f"LaTeX compile error. See, {self.__fname}.log")
for rm in rm_file:
if os.path.exists(rm):
os.remove(rm)
if pdfdir:
os.chdir(cwd)
# ==================================================
def _create_source(self, content, package=[], style="normal", pt=10, no_page=False, landscape=False, english=False):
"""
Create tex source.
Args:
content (str or list): content of document.
package (list, optional): package [(name,option)].
style (str, optional): margin style (narrowest/narrow/normal/wide/widest).
pt (int, optional): default point size (8/9/10/11/12/14).
no_page (bool, optional): no page number ?
landscape (bool, optional): landscape ?
english (bool, optional): english mode ? (use US letter size).
Returns:
- (str) -- tex source.
"""
px, py = (210, 297) # A4
st = "jsarticle"
foot = 7
layout_dict = {
"narrowest": (-15, -15),
"narrow": (-5, -5),
"normal": (0, 0),
"wide": (5, 5),
"widest": (10, 10),
} # margin (width,height)
scale_dict = {8: 9.6, 9: 9.82, 10: 10.0, 11: 10.0, 12: 10.0, 14: 9.7}
mx, my = layout_dict[style]
if landscape:
px, py = py, px
px *= scale_dict[pt] / pt
py *= scale_dict[pt] / pt
tx, ty = (px - 25.4 * 2 - 2 * mx, py - 25.4 * 2 - 2 * my - foot)
package_txt = []
if package:
package_txt += ["\n%%% package\n"]
for p, o in package:
txt = r"\usepackage"
if o:
txt += "[" + o + "]"
txt += "{" + p + "}"
package_txt.append(txt)
ls = ",landscape" if landscape else ""
head = [
r"\documentclass[fleqn," + str(pt) + "pt" + ls + "]{" + st + "}",
]
if landscape:
head.append(r"\special{papersize=\the\paperwidth,\the\paperheight}")
preamble = [
"\n%%% layout setting\n",
f"\\paperwidth={px}truemm",
f"\\paperheight={py}truemm",
f"\\textwidth={tx}truemm",
f"\\textheight={ty}truemm",
f"\\oddsidemargin={mx}truemm",
f"\\topmargin={my}truemm",
f"\\footskip={foot}truemm",
"\n%%% fixed\n",
r"\hoffset=0truemm",
r"\voffset=0truemm",
r"\headheight=0truemm",
r"\headsep=0truemm",
r"\marginparsep=0truemm",
r"\marginparwidth=0truemm",
"\n%%% main text\n",
r"\begin{document}" + "\n",
r"\setcounter{MaxMatrixCols}{32}" + "\n",
]
if english:
preamble += [
r"\renewcommand{\tablename}{Table~}",
r"\renewcommand{\figurename}{Figure~}",
]
if no_page:
preamble.append(r"\pagestyle{empty}" + "\n")
postamble = ["\n" + r"\end{document}" + "\n"]
txt = "\n".join(head + package_txt + preamble + content + postamble)
return txt
# ==================================================
def _create_table(self, tbl, cols=None, math=False):
"""
Create table content.
Args:
tbl (list): content.
cols (int, optional): number of columns.
math (bool, optional): math mode ?
Returns:
- (list) -- 2d table with given cols [[str]].
- (list) -- number of divisions in each rows [int].
"""
if type(tbl) != list:
raise KeyError("non-list type is given for table.")
if len(tbl) == 0:
raise KeyError("empty list is given for table.")
if type(tbl[0]) != list: # in case of 1d list
tbl = [tbl]
nmax = max(map(len, tbl))
if cols is None or nmax <= cols:
n = [1] * len(tbl)
t = tbl
else:
n = []
t = []
for i in tbl:
t1 = list_to_table(i, cols, "")
t.extend(t1)
n.append(len(t1))
t = regularize_table(t, "")
if math:
t = wrap_string(t, "$ ", " $")
return t, n
# ==================================================
[docs]
def title(self, title, size="LARGE", pos="c"):
"""
Add title.
Args:
title (str): title.
size (str, optional): font size (large/Large/LARGE/Huge).
pos (str, optional): position (c/l/r) (center/left/right).
"""
pos = self.__pos[pos]
txt = [r"\begin{" + pos + "}", f"\\{size}", title, r"\end{" + pos + "}"]
self.text(txt)
# ==================================================
[docs]
def text(self, content=""):
"""
Add text.
Args:
content (str or list, optional): text.
"""
if type(content) == list:
self.__content += content
else:
self.__content += [content]
# ==================================================
[docs]
def equation(self, eqs, num=False, long=False):
"""
Add equation.
Args:
eqs (str or list): equation or list of eqs.
num (bool, optional): with eq. number ?
long (bool, optional): long eq. ? (single eq. only).
"""
n = "" if num else "*"
if type(eqs) == list:
if long:
raise KeyError("long equation is unsupported.")
if not eqs:
return
txt = (
[r"\begin{align" + n + "}"] + ["& " + i + r" \\" for i in eqs[:-1]] + ["& " + eqs[-1]] + [r"\end{align" + n + "}"]
)
else:
env = "dmath" if long else "align"
txt = [r"\begin{" + env + n + "}", eqs, r"\end{" + env + n + "}"]
self.text(txt)
# ==================================================
# ==================================================
[docs]
def simple_table(self, tbl, cols=None, cpos=None, tmath=False):
"""
Add simple table.
Args:
tbl (list): content of table (1d or 2d).
cols (int, optional): number of columns.
cpos (str, optional): position of each cols. default is center.
tmath (bool, optional): math mode ?
"""
t, _ = self._create_table(tbl, cols, tmath)
if cpos is None:
cpos = "c" * len(t[0])
txt = [r"\begin{tabular}{" + cpos + "}"]
for i in t[:-1]:
txt.append(" & ".join(i) + r" \\")
txt.append(" & ".join(t[-1]))
txt.append(r"\end{tabular}")
self.text(txt)
# ==================================================
def _table(
self,
tbl,
row,
col,
rc="",
hl=[],
caption="",
stretch=1.0,
cpos=None,
rcmath=False,
rmath=False,
cmath=False,
tmath=False,
center=True,
):
"""
Add table (reshaped by using number of col).
Args:
same as self.table() except "long".
"""
cr = r" \\"
hr = r" \hline"
if rcmath and rc != "":
rc = "$ " + rc + " $"
if rmath:
row = wrap_string(row, "$ ", " $")
if cmath:
col = wrap_string(col, "$ ", " $")
txt = []
if caption:
txt.append(r"\begin{table}[ht!]")
if center:
txt.append(r"\begin{center}")
else:
txt.append(r"\captionsetup{justification=raggedright, singlelinecheck=off}")
txt.append(r"\caption{" + caption + "}")
txt.append(r"\renewcommand{\arraystretch}{" + str(stretch) + "}")
# tabular
t, n = self._create_table(tbl, len(col), tmath)
if cpos is None:
cpos = "c" * (len(col) + 1)
head = rc + " & " + " & ".join(col) + cr + hr
txt.append(r"\begin{tabular}{" + cpos + "}" + hr + hr)
txt.append(head)
ti = 0
for i in range(len(row)):
last = hr if i in hl else ""
if n[i] == 1:
txt.append(str(row[i]) + " & " + " & ".join(t[ti]) + cr + last)
else:
txt.append(str(row[i]) + " & " + " & ".join(t[ti]) + cr)
for j in range(1, n[i] - 1):
txt.append("& " + " & ".join(t[ti + j]) + cr)
txt.append("& " + " & ".join(t[ti + n[i] - 1]) + cr + last)
ti += n[i]
txt.append(hr + hr)
txt.append(r"\end{tabular}")
# end tabular
if caption:
if center:
txt.append(r"\end{center}")
txt.append(r"\end{table}")
self.text(txt)
# ==================================================
def _long_table(
self,
tbl,
row,
col,
rc="",
hl=[],
caption="",
stretch=1.0,
cpos=None,
rcmath=False,
rmath=False,
cmath=False,
tmath=False,
center=True,
):
"""
Add table (reshaped by using number of col).
Args:
same as self.table() except "long".
"""
cr = r" \\"
hr = r" \hline"
if rcmath and rc != "":
rc = "$ " + rc + " $"
if rmath:
row = wrap_string(row, "$ ", " $")
if cmath:
col = wrap_string(col, "$ ", " $")
t, n = self._create_table(tbl, len(col), tmath)
if cpos is None:
cpos = "c" * (len(col) + 1)
mc = r"\multicolumn{" + str(len(col)) + "}"
head = rc + " & " + " & ".join(col) + cr + hr
txt = []
cnt = "" if center else "[l]"
txt.append(r"\renewcommand{\arraystretch}{" + str(stretch) + "}")
txt.append(r"\begin{longtable}" + cnt + "{" + cpos + "}")
if caption:
if not center:
txt.append(r"\captionsetup{justification=raggedright, singlelinecheck=off}")
txt.append(r"\caption{" + caption + "}")
txt.append(cr)
txt.append(hr + hr)
txt.append(head + r" \endfirsthead" + "\n")
txt.append(mc + r"{l}{\tablename\ \thetable{}}" + cr)
txt.append(hr + hr)
txt.append(head + r" \endhead" + "\n")
txt.append(hr + hr)
txt.append(mc + r"{r}{\footnotesize\it continued ...}" + cr + r" \endfoot" + "\n")
txt.append(hr + hr)
txt.append(mc + "{r}{}" + cr + r" \endlastfoot" + "\n")
ti = 0
for i in range(len(row)):
last = hr if i in hl else ""
if n[i] == 1:
txt.append(str(row[i]) + " & " + " & ".join(t[ti]) + cr + last)
else:
txt.append(str(row[i]) + " & " + " & ".join(t[ti]) + cr)
for j in range(1, n[i] - 1):
txt.append("& " + " & ".join(t[ti + j]) + cr)
txt.append("& " + " & ".join(t[ti + n[i] - 1]) + cr + last)
ti += n[i]
txt.append(r"\end{longtable}")
self.text(txt)
self.__twice = True
# ==================================================
[docs]
def table(
self,
tbl,
row,
col,
rc="",
hl=[],
caption="",
stretch=1.0,
cpos=None,
rcmath=False,
rmath=False,
cmath=False,
tmath=False,
long=False,
center=True,
):
"""
Add table (reshaped by using number of col).
Args:
tbl (list): content of table [[str]].
row (list): row name [str].
col (list): column name [str].
rc (str, optional): row-col name.
hl (list, optional): position with horizontal line [int]. if True, use horizontal lines for all lines.
caption (str, optional): table caption.
stretch (float, optional): baseline stretch (ratio).
cpos (str, optional): column position. default is center.
rcmath (bool, optional): math mode for row-col name ?
rmath (bool, optional): math mode for row name ?
cmath (bool, optional): math mode for column name ?
tmath (bool, optional): math mode for table ?
long (bool, optional): long table ?
center (bool, optional): centering ?
"""
if hl is True:
hl = list(range(len(row) - 1))
if long:
self._long_table(tbl, row, col, rc, hl, caption, stretch, cpos, rcmath, rmath, cmath, tmath, center)
else:
self._table(tbl, row, col, rc, hl, caption, stretch, cpos, rcmath, rmath, cmath, tmath, center)