prefig 0.2.15.dev20250514053750__py3-none-any.whl → 0.5.6.dev20260130060411__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. prefig/cli.py +46 -26
  2. prefig/core/CTM.py +18 -3
  3. prefig/core/__init__.py +2 -0
  4. prefig/core/annotations.py +115 -2
  5. prefig/core/area.py +20 -4
  6. prefig/core/arrow.py +9 -3
  7. prefig/core/axes.py +909 -0
  8. prefig/core/circle.py +13 -4
  9. prefig/core/clip.py +1 -1
  10. prefig/core/coordinates.py +31 -4
  11. prefig/core/diagram.py +129 -6
  12. prefig/core/graph.py +236 -23
  13. prefig/core/grid_axes.py +181 -495
  14. prefig/core/image.py +127 -0
  15. prefig/core/label.py +14 -4
  16. prefig/core/legend.py +4 -4
  17. prefig/core/line.py +92 -1
  18. prefig/core/math_utilities.py +155 -0
  19. prefig/core/network.py +2 -2
  20. prefig/core/parametric_curve.py +15 -3
  21. prefig/core/path.py +25 -0
  22. prefig/core/point.py +18 -2
  23. prefig/core/polygon.py +7 -1
  24. prefig/core/read.py +6 -3
  25. prefig/core/rectangle.py +10 -4
  26. prefig/core/repeat.py +37 -3
  27. prefig/core/shape.py +12 -1
  28. prefig/core/slope_field.py +142 -0
  29. prefig/core/tags.py +8 -2
  30. prefig/core/tangent_line.py +36 -12
  31. prefig/core/user_namespace.py +8 -0
  32. prefig/core/utilities.py +9 -1
  33. prefig/engine.py +73 -28
  34. prefig/resources/diagcess/diagcess.js +7 -1
  35. prefig/resources/schema/pf_schema.rnc +89 -6
  36. prefig/resources/schema/pf_schema.rng +321 -5
  37. prefig/scripts/install_mj.py +10 -11
  38. {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/METADATA +12 -16
  39. prefig-0.5.6.dev20260130060411.dist-info/RECORD +68 -0
  40. prefig-0.2.15.dev20250514053750.dist-info/RECORD +0 -66
  41. {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/LICENSE +0 -0
  42. {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/WHEEL +0 -0
  43. {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/entry_points.txt +0 -0
prefig/core/rectangle.py CHANGED
@@ -71,10 +71,15 @@ def rectangle(element, diagram, parent, outline_status):
71
71
  path.set('d', ' '.join(cmds))
72
72
 
73
73
  if diagram.output_format() == 'tactile':
74
- if element.get('stroke') is not None:
74
+ stroke = element.get('stroke')
75
+ fill = element.get('fill')
76
+ if stroke is not None and stroke != 'none':
75
77
  element.set('stroke', 'black')
76
- if element.get('fill') is not None:
77
- element.set('fill', 'lightgray')
78
+ if fill is not None:
79
+ if fill.strip().lower() != 'none':
80
+ element.set('fill', 'lightgray')
81
+ else:
82
+ element.set('fill', 'none')
78
83
  else:
79
84
  util.set_attr(element, 'stroke', 'none')
80
85
  util.set_attr(element, 'fill', 'none')
@@ -97,5 +102,6 @@ def finish_outline(element, diagram, parent):
97
102
  diagram.finish_outline(element,
98
103
  element.get('stroke'),
99
104
  element.get('thickness'),
100
- element.get('fill', 'none'),
105
+ # element.get('fill', 'none'),
106
+ 'none',
101
107
  parent)
prefig/core/repeat.py CHANGED
@@ -2,6 +2,7 @@ import lxml.etree as ET
2
2
  import logging
3
3
  from . import user_namespace as un
4
4
  import copy
5
+ import re
5
6
  import numpy as np
6
7
  from . import group
7
8
  from . import label
@@ -9,6 +10,25 @@ from . import utilities
9
10
 
10
11
  log = logging.getLogger('prefigure')
11
12
 
13
+ # EPUB restricts the characters that can appear in an id to
14
+ # a-z|A-Z|0-9|_|-
15
+ # This is an issue here since <repeat> elements can create id's
16
+ # This regular expression checks characters to see if they are allowed
17
+ epub_check = re.compile(r'[A-Za-z0-9_-]')
18
+
19
+ # We also define substitutions for the most common disallowed characters
20
+ epub_dict = {'(': 'p',
21
+ ')': 'q',
22
+ '[': 'p',
23
+ ']': 'q',
24
+ '{': 'p',
25
+ '}': 'q',
26
+ ',': 'c',
27
+ '.': 'd',
28
+ '=': '_',
29
+ r'#': 'h'}
30
+
31
+
12
32
  # Allows a block of XML to repeat with a changing parameter
13
33
 
14
34
  def repeat(element, diagram, parent, outline_status):
@@ -40,7 +60,10 @@ def repeat(element, diagram, parent, outline_status):
40
60
  if outline is not None:
41
61
  element.set('outline', outline)
42
62
  if id is not None:
63
+ if not id.startswith('pf__'):
64
+ id = 'pf__' + id
43
65
  element.set('id', id)
66
+ element_cp.set('id', id)
44
67
 
45
68
  for num, k in enumerate(iterator):
46
69
  if isinstance(k, np.ndarray):
@@ -48,11 +71,12 @@ def repeat(element, diagram, parent, outline_status):
48
71
  else:
49
72
  k_str = str(k)
50
73
 
51
- un.enter_namespace(k_str, k)
74
+ k_str_clean = epub_clean(k_str)
75
+ # This is a change since we use the syntax "var_str" for the id suffix
52
76
  if count:
53
- suffix_str = var + "=" + k_str
77
+ suffix_str = var + "_" + k_str_clean
54
78
  else:
55
- suffix_str = var + "=" + str(num)
79
+ suffix_str = var + "_" + str(num)
56
80
 
57
81
  definition = ET.SubElement(element, 'definition')
58
82
  definition.text = var + '=' + k_str
@@ -78,3 +102,13 @@ def repeat(element, diagram, parent, outline_status):
78
102
 
79
103
  if annotation is not None:
80
104
  diagram.pop_from_annotation_branch()
105
+
106
+ def epub_clean(s):
107
+ epub_clean = [bool(epub_check.fullmatch(ch)) for ch in s]
108
+ chars = []
109
+ for index, ch in enumerate(s):
110
+ if epub_clean[index]:
111
+ chars.append(ch)
112
+ continue
113
+ chars.append(epub_dict.get(ch, '_'))
114
+ return "".join(chars)
prefig/core/shape.py CHANGED
@@ -36,7 +36,10 @@ def define(element, diagram, parent, outline_status):
36
36
  log.error(f"In <define-shapes>, {child.tag} does not define a shape")
37
37
  continue
38
38
  if child.get('at', None) is not None:
39
- child.set('id', child.get('at'))
39
+ id = child.get('at')
40
+ if not id.startswith('pf__'):
41
+ id = 'pf__' + id
42
+ child.set('id', id)
40
43
  dummy_parent = ET.Element('group')
41
44
  # this is kind of a hack, but we only need to construct the shape
42
45
  # so we stash the format temporarily in case we're building a
@@ -69,6 +72,12 @@ def shape(element, diagram, parent, outline_status):
69
72
  return
70
73
 
71
74
  shape_refs = [r.strip() for r in reference.split(',')]
75
+ shape_edit = []
76
+ for shape_ref in shape_refs:
77
+ if not shape_ref.startswith('pf__'):
78
+ shape_ref = 'pf__' + shape_ref
79
+ shape_edit.append(shape_ref)
80
+ shape_refs = shape_edit
72
81
  shapes = []
73
82
  for ref in shape_refs:
74
83
  shapes.append(diagram.recall_shape(ref))
@@ -82,6 +91,8 @@ def shape(element, diagram, parent, outline_status):
82
91
  operation = 'union'
83
92
  else:
84
93
  path = ET.SubElement(parent, 'use')
94
+ if not reference.startswith('pf__'):
95
+ reference = 'pf__' + reference
85
96
  path.set('href', r'#' + reference)
86
97
 
87
98
  if operation is not None:
@@ -8,8 +8,10 @@ from . import utilities
8
8
  from . import grid_axes
9
9
  from . import group
10
10
  from . import math_utilities as math_util
11
+ from . import calculus
11
12
 
12
13
  log = logging.getLogger('prefigure')
14
+ np.seterr(divide="ignore", invalid="ignore")
13
15
 
14
16
  # Add a graphical element for slope fields
15
17
  def slope_field(element, diagram, parent, outline_status):
@@ -24,6 +26,8 @@ def slope_field(element, diagram, parent, outline_status):
24
26
  return
25
27
  bbox = diagram.bbox()
26
28
 
29
+ if element.get('id', None) is None:
30
+ diagram.add_id(element, None)
27
31
  # We're going to turn this element into a group and add lines to it
28
32
  element.tag = "group"
29
33
  if element.get('outline', 'no') == 'yes':
@@ -112,3 +116,141 @@ def slope_field(element, diagram, parent, outline_status):
112
116
 
113
117
  group.group(element, diagram, parent, outline_status)
114
118
 
119
+ # Add a graphical element for slope fields
120
+ def vector_field(element, diagram, parent, outline_status):
121
+ if outline_status == 'finish_outline':
122
+ finish_outline(element, diagram, parent)
123
+ return
124
+
125
+ try:
126
+ f = un.valid_eval(element.get('function'))
127
+ except:
128
+ log.error(f"Error retrieving slope-field function={element.get('function')}")
129
+ return
130
+ bbox = diagram.bbox()
131
+
132
+ if element.get('id', None) is None:
133
+ diagram.add_id(element, None)
134
+
135
+ # We're going to turn this element into a group and add lines to it
136
+ element.tag = "group"
137
+ if element.get('outline', 'no') == 'yes':
138
+ element.set('outline', 'always')
139
+
140
+ # Now we'll construct a line with all the graphical information
141
+ # and make copies of it
142
+ line_template = ET.Element('line')
143
+
144
+ if diagram.output_format() == 'tactile':
145
+ line_template.set('stroke', 'black')
146
+ else:
147
+ line_template.set('stroke', element.get('stroke', 'blue'))
148
+ line_template.set('thickness', element.get('thickness', '2'))
149
+ line_template.set('arrows', '1')
150
+
151
+ if element.get('arrow-width', None) is not None:
152
+ line_template.set('arrow-width', element.get('arrow-width'))
153
+ if element.get('arrow-angles', None) is not None:
154
+ line_template.set('arrow-angles', element.get('arrow-angles'))
155
+
156
+ field_data = []
157
+ if element.get('curve', None) is not None:
158
+ curve = un.valid_eval(element.get('curve'))
159
+ try:
160
+ domain = un.valid_eval(element.get('domain'))
161
+ except:
162
+ log.error('A @domain is needed if adding a vector field to a curve')
163
+ return
164
+ try:
165
+ N = un.valid_eval(element.get('N'))
166
+ except:
167
+ log.error('A @N is needed if adding a vector field to a curve')
168
+ return
169
+
170
+ t = domain[0]
171
+ # if "f" a function of t or (x,y)?
172
+ one_variable = True
173
+ try:
174
+ f(t)
175
+ except TypeError:
176
+ one_variable = False
177
+
178
+ dt = (domain[1]-domain[0])/(N-1)
179
+ for _ in range(N):
180
+ position = curve(t)
181
+ if one_variable:
182
+ field_data.append([position, f(t)])
183
+ else:
184
+ field_data.append([position, f(*position)])
185
+ t += dt
186
+ scale_factor = un.valid_eval(element.get('scale', '1'))
187
+
188
+ else:
189
+ spacings = element.get('spacings', None)
190
+ if spacings is not None:
191
+ try:
192
+ spacings = un.valid_eval(spacings)
193
+ rx, ry = spacings
194
+ except:
195
+ log.error(f"Error parsing slope-field attribute @spacings={element.get('spacings')}")
196
+ return
197
+ else:
198
+ rx = grid_axes.find_gridspacing((bbox[0], bbox[2]))
199
+ ry = grid_axes.find_gridspacing((bbox[1], bbox[3]))
200
+
201
+ # we will go through and generate the vectors first
202
+ # since we'll need to scale them
203
+ max_scale = 0
204
+ exponent = un.valid_eval(element.get('exponent', '1'))
205
+ x = rx[0]
206
+ while x <= rx[2]:
207
+ y = ry[0]
208
+ while y <= ry[2]:
209
+ f_value = f(x, y)
210
+ if any(np.isnan(f_value)):
211
+ y += ry[1]
212
+ continue
213
+
214
+ try:
215
+ if len(f_value) != 2:
216
+ log.error("Only two-dimensional vector fields are supported")
217
+ return;
218
+ except:
219
+ pass
220
+ norm = math_util.length(f_value)
221
+ if norm < 1e-10:
222
+ f_value = np.array((0,0))
223
+ else:
224
+ # we will scale the length by length**exponent
225
+ # to promote the length of shorter vectors
226
+ f_value = norm**exponent * (1/norm * f_value)
227
+ max_scale = max(max_scale,
228
+ abs((f_value[0])/rx[1]),
229
+ abs((f_value[1])/ry[1]))
230
+ field_data.append([np.array([x,y]), f_value])
231
+ y += ry[1]
232
+ x += rx[1]
233
+
234
+ scale_factor = min(1, 0.75 / max_scale)
235
+ if element.get('scale') is not None:
236
+ scale = un.valid_eval(element.get('scale'))
237
+ scale_factor = scale
238
+
239
+ for datum in field_data:
240
+ p, v = datum
241
+ v = scale_factor * v
242
+ # is this long enough to add?
243
+ tail = p
244
+ tip = p+v
245
+ p0 = diagram.transform(tail)
246
+ p1 = diagram.transform(tip)
247
+ if math_util.distance(p0, p1) < 2:
248
+ continue
249
+
250
+ line_el = copy.deepcopy(line_template)
251
+ line_el.set('p1', utilities.pt2long_str(tail, spacer=','))
252
+ line_el.set('p2', utilities.pt2long_str(tip, spacer=','))
253
+ element.append(line_el)
254
+
255
+ group.group(element, diagram, parent, outline_status)
256
+
prefig/core/tags.py CHANGED
@@ -2,6 +2,7 @@ import logging
2
2
  import lxml.etree as ET
3
3
  from . import annotations
4
4
  from . import area
5
+ from . import axes
5
6
  from . import clip
6
7
  from . import circle
7
8
  from . import coordinates
@@ -10,6 +11,7 @@ from . import definition
10
11
  from . import graph
11
12
  from . import grid_axes
12
13
  from . import group
14
+ from . import image
13
15
  from . import implicit
14
16
  from . import label
15
17
  from . import legend
@@ -36,10 +38,11 @@ tag_dict = {
36
38
  'arc': circle.arc,
37
39
  'area-between-curves': area.area_between_curves,
38
40
  'area-under-curve': area.area_under_curve,
39
- 'axes': grid_axes.axes,
41
+ 'axes': axes.axes,
40
42
  'caption': label.caption,
41
43
  'circle': circle.circle,
42
44
  'clip': clip.clip,
45
+ 'contour': implicit.implicit_curve,
43
46
  'coordinates': coordinates.coordinates,
44
47
  'definition': definition.definition,
45
48
  'derivative': definition.derivative,
@@ -49,6 +52,7 @@ tag_dict = {
49
52
  'grid-axes': grid_axes.grid_axes,
50
53
  'group': group.group,
51
54
  'histogram': statistics.histogram,
55
+ 'image': image.image,
52
56
  'implicit-curve': implicit.implicit_curve,
53
57
  'label': label.label,
54
58
  'legend': legend.legend,
@@ -61,6 +65,7 @@ tag_dict = {
61
65
  'rectangle': rectangle.rectangle,
62
66
  'repeat': repeat.repeat,
63
67
  'riemann-sum': riemann_sum.riemann_sum,
68
+ 'tick-mark': axes.tick_mark,
64
69
  'transform': CTM.transform_group,
65
70
  'rotate': CTM.transform_rotate,
66
71
  'scale': CTM.transform_scale,
@@ -70,7 +75,8 @@ tag_dict = {
70
75
  'tangent-line': tangent_line.tangent,
71
76
  'translate': CTM.transform_translate,
72
77
  'triangle': polygon.triangle,
73
- 'vector': vector.vector
78
+ 'vector': vector.vector,
79
+ 'vector-field': slope_field.vector_field
74
80
  }
75
81
 
76
82
  log = logging.getLogger('prefigure')
@@ -1,4 +1,6 @@
1
1
  import logging
2
+ import lxml.etree as ET
3
+ import numpy as np
2
4
  from . import user_namespace as un
3
5
  from . import utilities as util
4
6
  from . import calculus
@@ -31,6 +33,10 @@ def tangent(element, diagram, parent, outline_status):
31
33
  def tangent(x):
32
34
  return y0 + m*(x-a)
33
35
 
36
+ name = element.get('name', None)
37
+ if name is not None:
38
+ un.enter_namespace(name, tangent)
39
+
34
40
  # determine the interval over which we'll draw the tangent line
35
41
  bbox = diagram.bbox()
36
42
  domain = element.get('domain', None)
@@ -39,19 +45,38 @@ def tangent(element, diagram, parent, outline_status):
39
45
  else:
40
46
  domain = un.valid_eval(domain)
41
47
 
42
- # find the endpoints of the tangent line
48
+ scales = diagram.get_scales()
43
49
  x1, x2 = domain
44
- y1 = tangent(x1)
45
- y2 = tangent(x2)
46
- p1 = (x1, y1)
47
- p2 = (x2, y2)
48
- if element.get('infinite') == 'yes' or element.get('domain') is None:
49
- p1, p2 = line.infinite_line(p1, p2, diagram)
50
- if p1 is None:
51
- return
50
+ if scales[0] == 'linear' and scales[1] == 'linear':
51
+ # find the endpoints of the tangent line
52
+ y1 = tangent(x1)
53
+ y2 = tangent(x2)
54
+ p1 = (x1, y1)
55
+ p2 = (x2, y2)
56
+ if element.get('infinite') == 'yes' or element.get('domain') is None:
57
+ p1, p2 = line.infinite_line(p1, p2, diagram)
58
+ if p1 is None:
59
+ return
52
60
 
53
- # construct the graphical line element from those points and attributes
54
- line_el = line.mk_line(p1, p2, diagram, element.get('id'))
61
+ # construct the graphical line element from those points and attributes
62
+ line_el = line.mk_line(p1, p2, diagram, element.get('id'))
63
+
64
+ else:
65
+ line_el = ET.Element('path')
66
+ if scales[0] == 'log':
67
+ x_positions = np.logspace(np.log10(x1), np.log(x2), 101)
68
+ else:
69
+ x_positions = np.linspace(x1, x2, 101)
70
+ cmds = []
71
+ next_cmd = 'M'
72
+ for x in x_positions:
73
+ y = tangent(x)
74
+ if y < 0 and scales[1] == 'log':
75
+ next_cmd = 'M'
76
+ continue
77
+ cmds += [next_cmd, util.pt2str(diagram.transform((x, y)))]
78
+ next_cmd = 'L'
79
+ line_el.set('d', ' '.join(cmds))
55
80
 
56
81
  if diagram.output_format() == 'tactile':
57
82
  element.set('stroke', 'black')
@@ -60,7 +85,6 @@ def tangent(element, diagram, parent, outline_status):
60
85
  util.set_attr(element, 'thickness', '2')
61
86
 
62
87
  util.add_attr(line_el, util.get_1d_attr(element))
63
- # line_el.set('type', 'tangent line')
64
88
  element.set('cliptobbox', 'yes')
65
89
  util.cliptobbox(line_el, element, diagram)
66
90
 
@@ -24,6 +24,7 @@ __delta_on = False
24
24
  functions = {x for x in dir(math) + dir(math_utilities) if not "__" in x}.difference({'e', 'pi'})
25
25
  functions.add('max')
26
26
  functions.add('min')
27
+ functions.add('round')
27
28
  variables = {'e', 'pi', 'inf'}
28
29
 
29
30
  # Transforms an AST by wrapping any List or Tuple inside a numpy array
@@ -66,6 +67,8 @@ def validate_node(node, args=None):
66
67
  return True
67
68
  if isinstance(node, ast.Expression):
68
69
  return validate_node(node.body, args)
70
+ if isinstance(node, ast.Starred):
71
+ return validate_node(node.value)
69
72
  if isinstance(node, ast.Name):
70
73
  if node.id in variables:
71
74
  return True
@@ -170,6 +173,11 @@ def derivative(f, name):
170
173
  functions.add(name)
171
174
  variables.add(name)
172
175
 
176
+ def enter_function(name, f):
177
+ globals()[name] = f
178
+ functions.add(name)
179
+ variables.add(name)
180
+
173
181
  def enter_namespace(name, value):
174
182
  globals()[name] = value
175
183
  variables.add(name)
prefig/core/utilities.py CHANGED
@@ -2,6 +2,14 @@ import numpy as np
2
2
  from . import user_namespace as un
3
3
  from . import label
4
4
 
5
+ import logging
6
+ logger = logging.getLogger('prefigure')
7
+
8
+ import warnings
9
+ with warnings.catch_warnings():
10
+ warnings.filterwarnings('ignore', 'legacy print')
11
+ np.set_printoptions(legacy="1.25")
12
+
5
13
  colors = {'gray': r'#777', 'lightgray': r'#ccc', 'darkgray': r'#333'}
6
14
 
7
15
  # Some utilities to handle XML elements
@@ -20,7 +28,7 @@ def get_attr(element, attr, default):
20
28
  try:
21
29
  attribute = un.valid_eval(element.get(attr, default))
22
30
  if isinstance(attribute, np.ndarray):
23
- return np.array2string(attribute, separator=',')
31
+ return ','.join([float2longstr(a) for a in attribute])
24
32
  return str(attribute)
25
33
  except (TypeError, SyntaxError): # this is a string that's not in the namespace
26
34
  return element.get(attr, default)
prefig/engine.py CHANGED
@@ -40,7 +40,7 @@ def build(
40
40
  ):
41
41
  pub_requested = not ignore_publication and publication is not None
42
42
  path = Path(filename)
43
- if path.suffix != '.xml':
43
+ if path.suffix == '':
44
44
  filename = str(path.parent / (path.stem + '.xml'))
45
45
 
46
46
  # We're going to look for a publication, possibly in a parent directory
@@ -84,20 +84,40 @@ def build(
84
84
  def build_from_string(format, input_string, environment="pyodide"):
85
85
  tree = ET.fromstring(input_string)
86
86
  log.setLevel(logging.DEBUG)
87
- diagrams = tree.xpath('//diagram')
88
- if len(diagrams) > 0:
89
- output_string = core.parse.mk_diagram(
90
- diagrams[0],
91
- format,
92
- None, # publication file
93
- "prefig", # filename needed for label generation
94
- False, # supress caption
95
- None, # diagram number
96
- environment,
97
- return_string = True
98
- )
99
- return output_string
100
- return ''
87
+ ns = {'pf': 'https://prefigure.org'}
88
+ diagrams_with_ns = tree.xpath('//pf:diagram', namespaces=ns)
89
+ diagrams_without_ns = tree.xpath('//diagram', namespaces=ns)
90
+ diagrams = diagrams_with_ns + diagrams_without_ns
91
+
92
+ try:
93
+ diagram = diagrams[0]
94
+ except:
95
+ return ''
96
+
97
+ # put all elements in default namespace
98
+ for elem in diagram.getiterator():
99
+ # Skip comments and processing instructions,
100
+ # because they do not have names
101
+ if not (
102
+ isinstance(elem, ET._Comment)
103
+ or isinstance(elem, ET._ProcessingInstruction)
104
+ ):
105
+ # Remove a namespace URI in the element's name
106
+ elem.tag = ET.QName(elem).localname
107
+
108
+ core.parse.check_duplicate_handles(diagram, set())
109
+
110
+ output_string = core.parse.mk_diagram(
111
+ diagram,
112
+ format,
113
+ None, # publication file
114
+ "prefig", # filename needed for label generation
115
+ False, # supress caption
116
+ None, # diagram number
117
+ environment,
118
+ return_string = True
119
+ )
120
+ return output_string
101
121
 
102
122
  def pdf(
103
123
  format,
@@ -121,23 +141,35 @@ def pdf(
121
141
  filename = Path(filename)
122
142
 
123
143
  if filename.suffix != '.svg':
124
- filename = filename.parent / (filename.name + '.svg')
144
+ filename = filename.parent / (filename.stem + '.svg')
125
145
 
126
146
  if build_path is None:
127
147
  filename_str = str(filename)
128
- for dir, dirs, files in os.walk(os.getcwd()):
129
- files = set(files)
130
- if filename_str in files:
131
- build_path = dir / filename
148
+
149
+ # fist look in current directory
150
+ cwd_files = os.listdir('.')
151
+ if filename_str in cwd_files:
152
+ build_path = filename
153
+ else:
154
+ if 'output' in cwd_files:
155
+ output_files = os.listdir('output')
156
+ if filename_str in output_files:
157
+ build_path = 'output' / filename
158
+
159
+ if build_path is None:
160
+ for dir, dirs, files in os.walk(os.getcwd()):
161
+ files = set(files)
162
+ if filename_str in files:
163
+ build_path = dir / filename
132
164
  if build_path is None:
133
- log.debug(f"Unable to find {filename}")
165
+ log.error(f"Unable to find {filename}")
134
166
  return
135
167
 
136
168
  dpi = str(dpi)
137
169
  executable = shutil.which('rsvg-convert')
138
170
  if executable is None:
139
- log.debug("rsvg-convert is required to create PDFs.")
140
- log.debug("See the installation instructions at https://prefigure.org")
171
+ log.error("rsvg-convert is required to create PDFs.")
172
+ log.error("See the installation instructions at https://prefigure.org")
141
173
  return
142
174
 
143
175
  log.info(f"Converting {build_path} to PDF")
@@ -184,12 +216,25 @@ def png(
184
216
 
185
217
  if build_path is None:
186
218
  filename_str = str(filename)
187
- for dir, dirs, files in os.walk(os.getcwd()):
188
- files = set(files)
189
- if filename_str in files:
190
- build_path = dir / filename
219
+ # look first in current directory
220
+ cwd_files = os.listdir('.')
221
+ if filename_str in cwd_files:
222
+ build_path = filename
223
+ else:
224
+ # now look in output
225
+ if 'output' in cwd_files:
226
+ output_files = os.listdir('output')
227
+ if filename_str in output_files:
228
+ build_path = 'output' / filename
229
+
230
+ # we still haven't found it so descend in the file system
231
+ if build_path is None:
232
+ for dir, dirs, files in os.walk(os.getcwd()):
233
+ files = set(files)
234
+ if filename_str in files:
235
+ build_path = dir / filename
191
236
  if build_path is None:
192
- log.debug(f"Unable to find {filename}")
237
+ log.error(f"Unable to find {filename}")
193
238
  return
194
239
 
195
240
  log.info(f"Converting {build_path} to PDF")