pdfcrate 0.1.0

A Rust library for creating and manipulating PDF documents
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
"""
Tests for pdfcrate rich text (formatted text) features.
"""

import pytest
from pdfcrate import Document, Margin, Color, TextFragment, SpanBuilder


class TestTextFragment:
    """Test TextFragment class."""

    def test_create_simple_fragment(self):
        """Test creating simple text fragment."""
        frag = TextFragment("Hello")
        assert frag is not None

    def test_fragment_with_bold(self):
        """Test creating bold fragment."""
        frag = TextFragment("Bold", bold=True)
        assert frag is not None

    def test_fragment_with_italic(self):
        """Test creating italic fragment."""
        frag = TextFragment("Italic", italic=True)
        assert frag is not None

    def test_fragment_with_color(self):
        """Test creating colored fragment."""
        frag = TextFragment("Red", color=Color.red())
        assert frag is not None

    def test_fragment_with_size(self):
        """Test creating fragment with custom size."""
        frag = TextFragment("Large", size=24)
        assert frag is not None

    def test_fragment_with_font(self):
        """Test creating fragment with custom font."""
        frag = TextFragment("Courier", font="Courier")
        assert frag is not None

    def test_fragment_with_all_options(self):
        """Test creating fragment with all options."""
        frag = TextFragment(
            "Styled",
            bold=True,
            italic=True,
            color=Color.blue(),
            size=18,
            font="Times-Roman"
        )
        assert frag is not None

    def test_fragment_with_underline(self):
        """Test creating fragment with underline."""
        frag = TextFragment("Underlined", underline=True)
        assert frag is not None

    def test_fragment_with_strikethrough(self):
        """Test creating fragment with strikethrough."""
        frag = TextFragment("Struck", strikethrough=True)
        assert frag is not None

    def test_fragment_with_superscript(self):
        """Test creating fragment with superscript."""
        frag = TextFragment("2", superscript=True)
        assert frag is not None

    def test_fragment_with_subscript(self):
        """Test creating fragment with subscript."""
        frag = TextFragment("2", subscript=True)
        assert frag is not None

    def test_fragment_with_link(self):
        """Test creating fragment with link."""
        frag = TextFragment("click", link="https://example.com")
        assert frag is not None

    def test_fragment_with_all_new_options(self):
        """Test creating fragment with all new inline options."""
        frag = TextFragment(
            "Full",
            bold=True,
            italic=True,
            color=Color.red(),
            size=14,
            font="Courier",
            underline=True,
            strikethrough=True,
            superscript=False,
            subscript=False,
            link="https://example.com",
        )
        assert frag is not None


class TestSpanBuilder:
    """Test SpanBuilder fluent API."""

    def test_create_span(self):
        """Test creating a span."""
        span = SpanBuilder("Hello")
        assert span is not None

    def test_span_bold(self):
        """Test bold span."""
        span = SpanBuilder("Bold").bold()
        assert span is not None

    def test_span_italic(self):
        """Test italic span."""
        span = SpanBuilder("Italic").italic()
        assert span is not None

    def test_span_color(self):
        """Test colored span."""
        span = SpanBuilder("Red").color(Color.red())
        assert span is not None

    def test_span_size(self):
        """Test span with size."""
        span = SpanBuilder("Large").size(24)
        assert span is not None

    def test_span_font(self):
        """Test span with font."""
        span = SpanBuilder("Mono").font("Courier")
        assert span is not None

    def test_span_chaining(self):
        """Test chaining multiple span methods."""
        span = SpanBuilder("Styled").bold().italic().color(Color.blue()).size(18)
        assert span is not None

    def test_span_end(self):
        """Test span.end() returns TextFragment."""
        frag = SpanBuilder("Text").bold().end()
        assert isinstance(frag, TextFragment)

    def test_document_span_shortcut(self):
        """Test Document.span() static method."""
        frag = Document.span("Hello").bold().end()
        assert isinstance(frag, TextFragment)

    def test_span_underline(self):
        """Test underline span."""
        span = SpanBuilder("text").underline()
        assert span is not None
        frag = span.end()
        assert isinstance(frag, TextFragment)

    def test_span_strikethrough(self):
        """Test strikethrough span."""
        frag = SpanBuilder("text").strikethrough().end()
        assert isinstance(frag, TextFragment)

    def test_span_superscript(self):
        """Test superscript span."""
        frag = SpanBuilder("2").superscript().end()
        assert isinstance(frag, TextFragment)

    def test_span_subscript(self):
        """Test subscript span."""
        frag = SpanBuilder("2").subscript().end()
        assert isinstance(frag, TextFragment)

    def test_span_link(self):
        """Test link span."""
        frag = SpanBuilder("click").link("https://example.com").end()
        assert isinstance(frag, TextFragment)

    def test_span_all_new_methods_chained(self):
        """Test chaining all new span methods."""
        frag = (
            SpanBuilder("text")
            .bold()
            .underline()
            .strikethrough()
            .link("https://example.com")
            .end()
        )
        assert isinstance(frag, TextFragment)


class TestFormattedText:
    """Test formatted_text method."""

    def test_formatted_text_requires_margin(self):
        """Test that formatted_text requires margin."""
        doc = Document()
        with pytest.raises(RuntimeError):
            doc.formatted_text([TextFragment("Hello")])

    def test_formatted_text_single_fragment(self):
        """Test formatted_text with single fragment."""
        doc = Document(margin=Margin.all(72))
        result = doc.formatted_text([TextFragment("Hello")])
        assert result is doc

    def test_formatted_text_multiple_fragments(self):
        """Test formatted_text with multiple fragments."""
        doc = Document(margin=Margin.all(72))
        result = doc.formatted_text([
            TextFragment("Hello "),
            TextFragment("World", bold=True),
        ])
        assert result is doc

    def test_formatted_text_with_span_builder(self):
        """Test formatted_text with SpanBuilder."""
        doc = Document(margin=Margin.all(72))
        result = doc.formatted_text([
            Document.span("Hello ").end(),
            Document.span("World").bold().end(),
        ])
        assert result is doc

    def test_formatted_text_mixed_styles(self):
        """Test formatted_text with various styles."""
        doc = Document(margin=Margin.all(72))
        result = doc.formatted_text([
            TextFragment("Normal "),
            TextFragment("bold", bold=True),
            TextFragment(", "),
            TextFragment("italic", italic=True),
            TextFragment(", and "),
            TextFragment("red", color=Color.red()),
            TextFragment(" text."),
        ])
        assert result is doc

    def test_formatted_text_with_fonts(self):
        """Test formatted_text with different fonts."""
        doc = Document(margin=Margin.all(72))
        result = doc.formatted_text([
            TextFragment("Helvetica "),
            TextFragment("Times", font="Times-Roman"),
            TextFragment(" and "),
            TextFragment("Courier", font="Courier"),
        ])
        assert result is doc

    def test_formatted_text_with_new_fields(self):
        """Test formatted_text with underline, strikethrough, etc."""
        doc = Document(margin=Margin.all(72))
        result = doc.formatted_text([
            TextFragment("Normal "),
            TextFragment("underlined", underline=True),
            TextFragment(" and "),
            TextFragment("struck", strikethrough=True),
            TextFragment(" text."),
        ])
        assert result is doc


class TestTextInline:
    """Test text_inline method (HTML-like inline formatting)."""

    def test_text_inline_requires_margin(self):
        """Test that text_inline requires margin."""
        doc = Document()
        with pytest.raises(RuntimeError):
            doc.text_inline("Hello <b>world</b>")

    def test_text_inline_plain_text(self):
        """Test text_inline with plain text (no tags)."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline("Hello world")
        assert result is doc

    def test_text_inline_bold(self):
        """Test text_inline with bold tag."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline("Hello <b>bold</b> world")
        assert result is doc

    def test_text_inline_italic(self):
        """Test text_inline with italic tag."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline("Hello <i>italic</i> world")
        assert result is doc

    def test_text_inline_strong_em(self):
        """Test text_inline with strong/em tags."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline("<strong>strong</strong> and <em>emphasis</em>")
        assert result is doc

    def test_text_inline_underline(self):
        """Test text_inline with underline tag."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline("Hello <u>underlined</u> world")
        assert result is doc

    def test_text_inline_strikethrough(self):
        """Test text_inline with strikethrough tag."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline("Hello <strikethrough>struck</strikethrough> world")
        assert result is doc

    def test_text_inline_superscript(self):
        """Test text_inline with superscript tag."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline("E = mc<sup>2</sup>")
        assert result is doc

    def test_text_inline_subscript(self):
        """Test text_inline with subscript tag."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline("H<sub>2</sub>O")
        assert result is doc

    def test_text_inline_color(self):
        """Test text_inline with color tag."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline('Normal <color rgb="#FF0000">red</color> normal')
        assert result is doc

    def test_text_inline_font(self):
        """Test text_inline with font tag."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline('Default <font name="Courier">mono</font> default')
        assert result is doc

    def test_text_inline_link(self):
        """Test text_inline with link tag."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline('Click <a href="https://example.com">here</a>')
        assert result is doc

    def test_text_inline_br(self):
        """Test text_inline with br tag."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline("Line 1<br>Line 2")
        assert result is doc

    def test_text_inline_entities(self):
        """Test text_inline with HTML entities."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline("a &amp; b &lt; c &gt; d")
        assert result is doc

    def test_text_inline_nested_tags(self):
        """Test text_inline with nested tags."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline("<b>bold <i>bold-italic</i> bold</b>")
        assert result is doc

    def test_text_inline_complex(self):
        """Test text_inline with complex mixed markup."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_inline(
            '<b>Bold</b>, <i>italic</i>, <u>underline</u>, '
            '<color rgb="#FF0000">red</color>, '
            '<font name="Courier">mono</font>, '
            '<a href="https://example.com">link</a>'
        )
        assert result is doc

    def test_text_inline_empty(self):
        """Test text_inline with empty string."""
        doc = Document(margin=Margin.all(72))
        cursor_before = doc.cursor()
        doc.text_inline("")
        cursor_after = doc.cursor()
        assert abs(cursor_after - cursor_before) < 0.1

    def test_text_inline_advances_cursor(self):
        """Test that text_inline advances the cursor."""
        doc = Document(margin=Margin.all(72))
        cursor_before = doc.cursor()
        doc.text_inline("Hello <b>world</b>")
        cursor_after = doc.cursor()
        assert cursor_after < cursor_before


class TestTextWrapInline:
    """Test text_wrap_inline method (HTML-like inline formatting with wrapping)."""

    def test_text_wrap_inline_requires_margin(self):
        """Test that text_wrap_inline requires margin."""
        doc = Document()
        with pytest.raises(RuntimeError):
            doc.text_wrap_inline("Hello <b>world</b>")

    def test_text_wrap_inline_plain_text(self):
        """Test text_wrap_inline with plain text."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_wrap_inline("Hello world")
        assert result is doc

    def test_text_wrap_inline_with_tags(self):
        """Test text_wrap_inline with formatting tags."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_wrap_inline(
            "This is a <b>long</b> paragraph with <i>mixed</i> styles "
            "that should <u>wrap automatically</u> within the margins."
        )
        assert result is doc

    def test_text_wrap_inline_empty(self):
        """Test text_wrap_inline with empty string."""
        doc = Document(margin=Margin.all(72))
        cursor_before = doc.cursor()
        doc.text_wrap_inline("")
        cursor_after = doc.cursor()
        assert abs(cursor_after - cursor_before) < 0.1

    def test_text_wrap_inline_long_wraps_multiple_lines(self):
        """Test that long text actually wraps to multiple lines."""
        doc = Document(margin=Margin.all(72))
        cursor_before = doc.cursor()
        doc.text("Short")
        single_line_drop = cursor_before - doc.cursor()

        cursor_before_wrap = doc.cursor()
        doc.text_wrap_inline(
            "This is a <b>very long piece of text</b> that contains <i>many words</i> "
            "and should definitely <u>wrap across multiple lines</u> when rendered "
            "within the default page margins of the layout document."
        )
        multi_line_drop = cursor_before_wrap - doc.cursor()

        # Multi-line should drop more than single line
        assert multi_line_drop > single_line_drop * 1.5

    def test_text_wrap_inline_with_br(self):
        """Test text_wrap_inline with explicit line breaks."""
        doc = Document(margin=Margin.all(72))
        result = doc.text_wrap_inline("Line 1<br>Line 2<br/>Line 3")
        assert result is doc

    def test_text_wrap_inline_renders_pdf(self):
        """Test that text_wrap_inline produces valid PDF output."""
        doc = Document(margin=Margin.all(72))
        doc.text_inline("Hello <b>bold</b> and <i>italic</i>")
        doc.text_wrap_inline(
            "Wrapped <u>underline</u> text that goes on for a while."
        )
        pdf_bytes = doc.render()
        assert len(pdf_bytes) > 100
        assert pdf_bytes[:5] == b"%PDF-"