page_print 0.1.9 → 0.1.11

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.
@@ -0,0 +1,148 @@
1
+ # Production guide
2
+
3
+ PagePrint is pre-1.0 software. Treat adoption as an application-specific engineering decision: validate the native package, representative documents, throughput, memory use, fonts, and failure behavior on the same platform used in production.
4
+
5
+ ## Production readiness checklist
6
+
7
+ Before deploying PagePrint:
8
+
9
+ - [ ] Confirm the deployment platform has a native gem or can build PlutoBook from source.
10
+ - [ ] Render representative short, long, image-heavy, and multilingual documents in CI.
11
+ - [ ] Visually compare pagination, fonts, images, headers, and footers against approved fixtures.
12
+ - [ ] Verify every stylesheet, image, and font is available through the Rails integration or an allowlisted resource fetcher.
13
+ - [ ] Confirm templates do not depend on JavaScript, CSS Grid, or other unsupported browser features.
14
+ - [ ] Load-test with production-sized documents and the same worker/thread configuration used in deployment.
15
+ - [ ] Set application-level job timeouts and monitor render latency, failures, and output size.
16
+ - [ ] Prefer `render_to_file` for large PDFs, and clean up temporary output in both success and failure paths.
17
+ - [ ] Pin the gem version and test upgrades before rollout; renderer upgrades can alter layout and pagination.
18
+
19
+ ## Platform support
20
+
21
+ PagePrint requires Ruby 3.2 or newer.
22
+
23
+ | Platform | Installation |
24
+ | --- | --- |
25
+ | `x86_64-linux` | Native gem with vendored PlutoBook libraries; the Ruby extension is compiled against the local Ruby |
26
+ | `arm64-darwin` | Native gem with vendored PlutoBook libraries; the Ruby extension is compiled against the local Ruby |
27
+ | Other platforms | Source build using an installed PlutoBook |
28
+
29
+ The published native gems disable PlutoBook's optional curl, TurboJPEG, and WebP features to keep the bundled dependency set smaller. PagePrint uses its own Ruby resource fetcher boundary and does not expose PlutoBook's optional network fetcher.
30
+
31
+ Your production image still needs a compiler and Ruby development headers because the small PagePrint extension is compiled during installation.
32
+
33
+ ### Installing on other platforms
34
+
35
+ Install PlutoBook development headers and libraries before installing the gem. On macOS with Homebrew:
36
+
37
+ ```sh
38
+ brew install plutobook pkg-config
39
+ bundle install
40
+ ```
41
+
42
+ If `pkg-config` cannot find PlutoBook, provide explicit paths:
43
+
44
+ ```sh
45
+ gem install page_print -- \
46
+ --with-plutobook-include=/path/to/include \
47
+ --with-plutobook-lib=/path/to/lib
48
+ ```
49
+
50
+ Use equivalent Bundler build configuration in repeatable deployments:
51
+
52
+ ```sh
53
+ bundle config set --local build.page_print \
54
+ "--with-plutobook-include=/path/to/include --with-plutobook-lib=/path/to/lib"
55
+ bundle install
56
+ ```
57
+
58
+ Run a render smoke test while building the deployment artifact, rather than only checking that the gem can be required:
59
+
60
+ ```sh
61
+ bundle exec ruby -e '
62
+ require "page_print"
63
+ pdf = PagePrint.render("<h1>Deployment check</h1>")
64
+ abort "invalid PDF" unless pdf.start_with?("%PDF")
65
+ '
66
+ ```
67
+
68
+ ## Security
69
+
70
+ PagePrint's resource policy is deny-by-default: an unresolved URL is not fetched over the network. Preserve that property in custom fetchers.
71
+
72
+ - Do not build a fetcher that blindly downloads arbitrary URLs. Doing so can introduce server-side request forgery (SSRF), access to cloud metadata endpoints, and unbounded downloads.
73
+ - Prefer a fixed asset map, an application-owned URI scheme, or strict allowlists for scheme, host, path, content type, and size.
74
+ - Do not treat PagePrint as a sandbox for hostile HTML. It parses complex input in native code and should run with the minimum filesystem and process permissions needed by the application.
75
+ - Never place secrets in HTML, asset URLs, metadata, or error logs.
76
+ - Validate and limit user-controlled content before inserting it into a trusted template.
77
+
78
+ PagePrint's disabled network fallback improves predictability and reduces accidental outbound access, but it is not a complete security boundary.
79
+
80
+ ## Template compatibility
81
+
82
+ PagePrint is intended for static, server-rendered documents. It does not execute JavaScript. PlutoBook supports much of CSS 3 and selected CSS 4 functionality, but important gaps include CSS Grid and some advanced visual features.
83
+
84
+ Before migrating from PDFKit, `wkhtmltopdf`, or a browser renderer:
85
+
86
+ 1. Inventory JavaScript, layout modules, fonts, SVG, and remote assets used by the templates.
87
+ 2. Check the [PlutoBook feature matrix](https://github.com/plutoprint/plutobook/blob/main/FEATURES.md).
88
+ 3. Render a fixture set with both engines.
89
+ 4. Compare page breaks and visual output, not only whether a PDF was produced.
90
+
91
+ Use explicit `@page`, print styles, and embedded or controlled fonts. Font substitution is a common cause of different line wrapping and pagination across development and production.
92
+
93
+ ## Assets and networking
94
+
95
+ For deterministic output, package document assets with the application and return them from a resource fetcher. Avoid making rendering depend on the availability or latency of another service.
96
+
97
+ Rails applications using Propshaft get a default fetcher for `/assets/...`. Outside Rails, configure a fetcher once at process startup:
98
+
99
+ ```ruby
100
+ PagePrint.configure do |config|
101
+ config.resource_fetcher = ProductionPdfAssets.new
102
+ end
103
+ ```
104
+
105
+ See the [resource fetcher reference](configuration.md#resource-fetchers) for the callable contract.
106
+
107
+ ## Capacity and concurrency
108
+
109
+ HTML parsing, layout, and PDF writing are native CPU- and memory-intensive work. The extension releases Ruby's Global VM Lock during PlutoBook loading and writing, but Ruby callbacks such as a custom resource fetcher run with the lock.
110
+
111
+ Do not size workers from the repository benchmark alone. Measure:
112
+
113
+ - latency percentiles, not only averages;
114
+ - peak resident memory with concurrent renders;
115
+ - the largest realistic HTML, image, and PDF sizes;
116
+ - application behavior when a render raises or a job times out;
117
+ - both `render` and `render_to_file` if you expect large outputs.
118
+
119
+ Use bounded job concurrency. For background processing, let the job system retry only failures known to be transient; invalid templates and unsupported resources will fail repeatedly.
120
+
121
+ ## Failure handling
122
+
123
+ PagePrint validates option types and raises Ruby exceptions for invalid input, resource fetcher failures, HTML loading failures, and PDF write failures. Treat a successful method return—and, where appropriate, a `%PDF` signature and non-zero output size—as the completion signal.
124
+
125
+ When writing temporary files:
126
+
127
+ ```ruby
128
+ require "tempfile"
129
+
130
+ Tempfile.create(["invoice", ".pdf"]) do |file|
131
+ PagePrint.render_to_file(html, file.path)
132
+ deliver(file.path)
133
+ end
134
+ ```
135
+
136
+ Do not expose native error text directly to end users. Record enough context to identify the template and render options, without logging document contents that may contain personal or confidential data.
137
+
138
+ ## Upgrade strategy
139
+
140
+ Pin PagePrint in the application lockfile. Before upgrading PagePrint or PlutoBook:
141
+
142
+ 1. Build on every production platform.
143
+ 2. Run the complete PDF fixture suite.
144
+ 3. Compare visual output and page count.
145
+ 4. Repeat a focused load test.
146
+ 5. Roll out gradually while watching latency and render failures.
147
+
148
+ [Back to the README](../README.md)
@@ -2,7 +2,9 @@
2
2
  #include "ruby/encoding.h"
3
3
  #include "ruby/thread.h"
4
4
  #include <limits.h>
5
+ #include <math.h>
5
6
  #include <stdint.h>
7
+ #include <string.h>
6
8
  #include <plutobook/plutobook.h>
7
9
 
8
10
  #if defined(__GNUC__) || defined(__clang__)
@@ -55,6 +57,7 @@ typedef struct {
55
57
 
56
58
  typedef struct {
57
59
  VALUE object;
60
+ VALUE error;
58
61
  int state;
59
62
  } pageprint_resource_fetcher_t;
60
63
 
@@ -66,10 +69,12 @@ typedef struct {
66
69
 
67
70
  typedef struct {
68
71
  VALUE output;
72
+ VALUE error;
69
73
  int state;
70
74
  } pageprint_pdf_string_output_t;
71
75
 
72
76
  typedef struct {
77
+ pageprint_pdf_string_output_t *owner;
73
78
  VALUE output;
74
79
  const char *data;
75
80
  unsigned int length;
@@ -102,10 +107,49 @@ typedef struct {
102
107
  VALUE metadata;
103
108
  } pageprint_apply_metadata_args_t;
104
109
 
110
+ typedef struct {
111
+ plutobook_t *book;
112
+ VALUE html;
113
+ VALUE base_url;
114
+ VALUE resource_fetcher;
115
+ VALUE metadata;
116
+ pageprint_resource_fetcher_t resource_fetcher_state;
117
+ } pageprint_book_context_t;
118
+
119
+ typedef struct {
120
+ pageprint_book_context_t book_context;
121
+ VALUE html;
122
+ VALUE path;
123
+ VALUE options;
124
+ } pageprint_render_to_file_context_t;
125
+
126
+ typedef struct {
127
+ pageprint_book_context_t book_context;
128
+ VALUE html;
129
+ VALUE options;
130
+ pageprint_pdf_string_output_t output;
131
+ } pageprint_render_context_t;
132
+
105
133
  static VALUE pageprint_append_pdf_string(VALUE value);
106
134
  static plutobook_stream_status_t pageprint_write_pdf_string(void *closure, const char *data, unsigned int length);
107
135
  static plutobook_resource_data_t *pageprint_fetch_resource(void *closure, const char *url);
108
136
 
137
+ static void pageprint_reject_embedded_nul(VALUE value, const char *name)
138
+ {
139
+ if (memchr(RSTRING_PTR(value), '\0', (size_t)RSTRING_LEN(value))) {
140
+ rb_raise(rb_eArgError, "%s must not contain NUL bytes", name);
141
+ }
142
+ }
143
+
144
+ static VALUE pageprint_frozen_string_copy(VALUE value)
145
+ {
146
+ VALUE copy = rb_str_dup(value);
147
+
148
+ rb_obj_freeze(copy);
149
+
150
+ return copy;
151
+ }
152
+
109
153
  static void PAGEPRINT_NORETURN pageprint_raise_plutobook_error(VALUE error_class, const char *message)
110
154
  {
111
155
  const char *error_message = plutobook_get_error_message();
@@ -128,6 +172,12 @@ static void PAGEPRINT_NORETURN pageprint_raise_plutobook_error_with_path(VALUE e
128
172
  rb_raise(error_class, "%s %s", message, path);
129
173
  }
130
174
 
175
+ static void PAGEPRINT_NORETURN pageprint_jump_callback_error(int state, VALUE error)
176
+ {
177
+ rb_set_errinfo(error);
178
+ rb_jump_tag(state);
179
+ }
180
+
131
181
  static double pageprint_unit_factor_from_value(VALUE value, const char *name)
132
182
  {
133
183
  ID value_id;
@@ -175,6 +225,14 @@ static plutobook_page_size_t pageprint_page_size_from_value(VALUE value)
175
225
  width_number = NUM2DBL(width);
176
226
  height_number = NUM2DBL(height);
177
227
 
228
+ if (!isfinite(width_number)) {
229
+ rb_raise(rb_eArgError, "page_size width must be finite");
230
+ }
231
+
232
+ if (!isfinite(height_number)) {
233
+ rb_raise(rb_eArgError, "page_size height must be finite");
234
+ }
235
+
178
236
  if (width_number <= 0) {
179
237
  rb_raise(rb_eArgError, "page_size width must be greater than 0");
180
238
  }
@@ -247,6 +305,10 @@ static plutobook_page_margins_t pageprint_margins_from_value(VALUE value)
247
305
  bottom_number = NUM2DBL(bottom);
248
306
  left_number = NUM2DBL(left);
249
307
 
308
+ if (!isfinite(top_number) || !isfinite(right_number) || !isfinite(bottom_number) || !isfinite(left_number)) {
309
+ rb_raise(rb_eArgError, "margins values must be finite");
310
+ }
311
+
250
312
  if (top_number < 0 || right_number < 0 || bottom_number < 0 || left_number < 0) {
251
313
  rb_raise(rb_eArgError, "margins values must be greater than or equal to 0");
252
314
  }
@@ -410,11 +472,17 @@ static void *pageprint_write_pdf_without_gvl(void *ptr)
410
472
  static void *pageprint_append_pdf_string_with_gvl(void *ptr)
411
473
  {
412
474
  pageprint_pdf_string_append_t *append = ptr;
475
+ pageprint_pdf_string_output_t *output = append->owner;
413
476
  int state = 0;
414
477
 
415
478
  rb_protect(pageprint_append_pdf_string, (VALUE)append, &state);
416
479
 
417
- return (void *)(intptr_t)state;
480
+ if (state && !output->state) {
481
+ output->state = state;
482
+ output->error = rb_errinfo();
483
+ }
484
+
485
+ return NULL;
418
486
  }
419
487
 
420
488
  static void *pageprint_write_pdf_stream_without_gvl(void *ptr)
@@ -492,24 +560,31 @@ static void *pageprint_call_resource_fetcher_with_gvl(void *ptr)
492
560
 
493
561
  rb_protect(pageprint_call_resource_fetcher, (VALUE)args, &state);
494
562
 
495
- return (void *)(intptr_t)state;
563
+ if (state && !args->fetcher->state) {
564
+ args->fetcher->state = state;
565
+ args->fetcher->error = rb_errinfo();
566
+ }
567
+
568
+ return NULL;
496
569
  }
497
570
 
498
571
  static plutobook_resource_data_t *pageprint_fetch_resource(void *closure, const char *url)
499
572
  {
500
573
  pageprint_resource_fetcher_t *fetcher = closure;
501
574
  pageprint_resource_fetch_args_t args;
502
- int state;
575
+
576
+ if (fetcher->state) {
577
+ return NULL;
578
+ }
503
579
 
504
580
  if (!NIL_P(fetcher->object)) {
505
581
  args.fetcher = fetcher;
506
582
  args.url = url;
507
583
  args.resource = NULL;
508
584
 
509
- state = (int)(intptr_t)rb_thread_call_with_gvl(pageprint_call_resource_fetcher_with_gvl, &args);
585
+ rb_thread_call_with_gvl(pageprint_call_resource_fetcher_with_gvl, &args);
510
586
 
511
- if (state) {
512
- fetcher->state = state;
587
+ if (fetcher->state) {
513
588
  plutobook_set_error_message("failed to fetch URL '%s'", url);
514
589
  return NULL;
515
590
  }
@@ -608,15 +683,11 @@ static pageprint_options_t pageprint_options_from_value(VALUE options)
608
683
  return result;
609
684
  }
610
685
 
611
- static plutobook_t *pageprint_create_book_from_html(VALUE html, VALUE options, pageprint_resource_fetcher_t *resource_fetcher)
686
+ static void pageprint_create_book_from_html(VALUE html, VALUE options, pageprint_book_context_t *context)
612
687
  {
613
688
  pageprint_options_t pageprint_options;
614
-
615
- const char *html_str;
616
689
  const char *base_url_str;
617
690
  long html_len;
618
-
619
- plutobook_t *book;
620
691
  pageprint_load_html_args_t load_args;
621
692
 
622
693
  if (!RB_TYPE_P(html, T_STRING)) {
@@ -631,45 +702,49 @@ static plutobook_t *pageprint_create_book_from_html(VALUE html, VALUE options, p
631
702
  rb_raise(rb_eArgError, "html is too large");
632
703
  }
633
704
 
705
+ pageprint_reject_embedded_nul(html, "html");
634
706
  pageprint_options = pageprint_options_from_value(options);
707
+ context->html = pageprint_frozen_string_copy(html);
708
+ context->base_url = Qnil;
709
+ context->resource_fetcher = pageprint_options.resource_fetcher;
710
+ context->metadata = pageprint_options.metadata;
711
+
712
+ if (!NIL_P(pageprint_options.base_url)) {
713
+ pageprint_reject_embedded_nul(pageprint_options.base_url, "base_url");
714
+ context->base_url = pageprint_frozen_string_copy(pageprint_options.base_url);
715
+ }
635
716
 
636
- html_str = RSTRING_PTR(html);
637
- html_len = RSTRING_LEN(html);
717
+ html_len = RSTRING_LEN(context->html);
638
718
 
639
719
  /* 1. Create */
640
- book = plutobook_create(pageprint_options.page_size, pageprint_options.margins, pageprint_options.media);
720
+ context->book = plutobook_create(pageprint_options.page_size, pageprint_options.margins, pageprint_options.media);
641
721
 
642
- if (!book) {
722
+ if (!context->book) {
643
723
  rb_raise(rb_eRuntimeError, "failed to create plutobook");
644
724
  }
645
725
 
646
- resource_fetcher->object = pageprint_options.resource_fetcher;
647
- resource_fetcher->state = 0;
726
+ context->resource_fetcher_state.object = context->resource_fetcher;
727
+ context->resource_fetcher_state.error = Qnil;
728
+ context->resource_fetcher_state.state = 0;
648
729
 
649
- plutobook_set_custom_resource_fetcher(book, pageprint_fetch_resource, resource_fetcher);
730
+ plutobook_set_custom_resource_fetcher(context->book, pageprint_fetch_resource, &context->resource_fetcher_state);
650
731
 
651
732
  /* 2. Load HTML */
652
733
  plutobook_clear_error_message();
653
734
 
654
735
  base_url_str = "";
655
- if (!NIL_P(pageprint_options.base_url)) {
656
- base_url_str = StringValueCStr(pageprint_options.base_url);
736
+ if (!NIL_P(context->base_url)) {
737
+ base_url_str = RSTRING_PTR(context->base_url);
657
738
  }
658
739
 
659
- load_args.book = book;
660
- load_args.html = html_str;
740
+ load_args.book = context->book;
741
+ load_args.html = RSTRING_PTR(context->html);
661
742
  load_args.length = (int)html_len;
662
743
  load_args.user_style = "";
663
744
  load_args.user_script = "";
664
745
  load_args.base_url = base_url_str;
665
746
  load_args.ok = 0;
666
747
 
667
- RB_GC_GUARD(html);
668
- RB_GC_GUARD(options);
669
- RB_GC_GUARD(pageprint_options.base_url);
670
- RB_GC_GUARD(pageprint_options.resource_fetcher);
671
- RB_GC_GUARD(pageprint_options.metadata);
672
-
673
748
  rb_thread_call_without_gvl(
674
749
  pageprint_load_html_without_gvl,
675
750
  &load_args,
@@ -677,37 +752,41 @@ static plutobook_t *pageprint_create_book_from_html(VALUE html, VALUE options, p
677
752
  NULL
678
753
  );
679
754
 
680
- if (!load_args.ok) {
681
- plutobook_destroy(book);
755
+ RB_GC_GUARD(context->html);
756
+ RB_GC_GUARD(context->base_url);
682
757
 
683
- if (resource_fetcher->state) {
684
- rb_jump_tag(resource_fetcher->state);
758
+ if (!load_args.ok) {
759
+ if (context->resource_fetcher_state.state) {
760
+ pageprint_jump_callback_error(
761
+ context->resource_fetcher_state.state,
762
+ context->resource_fetcher_state.error
763
+ );
685
764
  }
686
765
 
687
766
  pageprint_raise_plutobook_error(rb_eRuntimeError, "failed to load HTML into plutobook");
688
767
  }
689
768
 
690
- if (resource_fetcher->state) {
691
- plutobook_destroy(book);
692
- rb_jump_tag(resource_fetcher->state);
769
+ if (context->resource_fetcher_state.state) {
770
+ pageprint_jump_callback_error(
771
+ context->resource_fetcher_state.state,
772
+ context->resource_fetcher_state.error
773
+ );
693
774
  }
694
775
 
695
776
  {
696
777
  pageprint_apply_metadata_args_t metadata_args;
697
778
  int metadata_state = 0;
698
779
 
699
- metadata_args.book = book;
700
- metadata_args.metadata = pageprint_options.metadata;
780
+ metadata_args.book = context->book;
781
+ metadata_args.metadata = context->metadata;
701
782
 
702
783
  rb_protect(pageprint_apply_metadata_with_gvl, (VALUE)&metadata_args, &metadata_state);
784
+ RB_GC_GUARD(context->metadata);
703
785
 
704
786
  if (metadata_state) {
705
- plutobook_destroy(book);
706
787
  rb_jump_tag(metadata_state);
707
788
  }
708
789
  }
709
-
710
- return book;
711
790
  }
712
791
 
713
792
  static VALUE pageprint_append_pdf_string(VALUE value)
@@ -723,62 +802,54 @@ static plutobook_stream_status_t pageprint_write_pdf_string(void *closure, const
723
802
  {
724
803
  pageprint_pdf_string_output_t *output = closure;
725
804
  pageprint_pdf_string_append_t append;
726
- int state;
727
805
 
806
+ if (output->state) {
807
+ return PLUTOBOOK_STREAM_STATUS_WRITE_ERROR;
808
+ }
809
+
810
+ append.owner = output;
728
811
  append.output = output->output;
729
812
  append.data = data;
730
813
  append.length = length;
731
814
 
732
- state = (int)(intptr_t)rb_thread_call_with_gvl(pageprint_append_pdf_string_with_gvl, &append);
815
+ rb_thread_call_with_gvl(pageprint_append_pdf_string_with_gvl, &append);
733
816
 
734
- if (state) {
735
- output->state = state;
817
+ if (output->state) {
736
818
  return PLUTOBOOK_STREAM_STATUS_WRITE_ERROR;
737
819
  }
738
820
 
739
821
  return PLUTOBOOK_STREAM_STATUS_SUCCESS;
740
822
  }
741
823
 
742
- static VALUE pageprint_render_to_file(int argc, VALUE *argv, VALUE self) {
743
- VALUE html;
744
- VALUE path;
745
- VALUE options;
746
-
747
- const char *path_str;
748
-
749
- plutobook_t *book;
750
- pageprint_resource_fetcher_t resource_fetcher;
751
- pageprint_write_pdf_args_t write_args;
752
-
753
- rb_check_arity(argc, 2, 3);
824
+ static VALUE pageprint_destroy_book(VALUE value)
825
+ {
826
+ pageprint_book_context_t *context = (pageprint_book_context_t *)value;
827
+ plutobook_t *book = context->book;
754
828
 
755
- html = argv[0];
756
- path = argv[1];
757
- options = argc == 3 ? argv[2] : Qnil;
829
+ context->book = NULL;
758
830
 
759
- if (!RB_TYPE_P(path, T_STRING)) {
760
- rb_raise(rb_eTypeError, "path must be a String");
831
+ if (book) {
832
+ plutobook_destroy(book);
761
833
  }
762
834
 
763
- if (RSTRING_LEN(path) == 0) {
764
- rb_raise(rb_eArgError, "path must not be empty");
765
- }
835
+ return Qnil;
836
+ }
766
837
 
767
- path_str = StringValueCStr(path);
838
+ static VALUE pageprint_render_to_file_body(VALUE value)
839
+ {
840
+ pageprint_render_to_file_context_t *context = (pageprint_render_to_file_context_t *)value;
841
+ const char *path_str;
842
+ pageprint_write_pdf_args_t write_args;
768
843
 
769
- book = pageprint_create_book_from_html(html, options, &resource_fetcher);
844
+ pageprint_create_book_from_html(context->html, context->options, &context->book_context);
845
+ path_str = RSTRING_PTR(context->path);
770
846
 
771
847
  plutobook_clear_error_message();
772
848
 
773
- write_args.book = book;
849
+ write_args.book = context->book_context.book;
774
850
  write_args.path = path_str;
775
851
  write_args.ok = 0;
776
852
 
777
- RB_GC_GUARD(html);
778
- RB_GC_GUARD(options);
779
- RB_GC_GUARD(resource_fetcher.object);
780
- RB_GC_GUARD(path);
781
-
782
853
  rb_thread_call_without_gvl(
783
854
  pageprint_write_pdf_without_gvl,
784
855
  &write_args,
@@ -786,10 +857,17 @@ static VALUE pageprint_render_to_file(int argc, VALUE *argv, VALUE self) {
786
857
  NULL
787
858
  );
788
859
 
789
- plutobook_destroy(book);
860
+ RB_GC_GUARD(context->path);
861
+ RB_GC_GUARD(context->book_context.resource_fetcher);
862
+ RB_GC_GUARD(context->book_context.resource_fetcher_state.error);
863
+ RB_GC_GUARD(context->html);
864
+ RB_GC_GUARD(context->options);
790
865
 
791
- if (resource_fetcher.state) {
792
- rb_jump_tag(resource_fetcher.state);
866
+ if (context->book_context.resource_fetcher_state.state) {
867
+ pageprint_jump_callback_error(
868
+ context->book_context.resource_fetcher_state.state,
869
+ context->book_context.resource_fetcher_state.error
870
+ );
793
871
  }
794
872
 
795
873
  if (!write_args.ok) {
@@ -799,36 +877,42 @@ static VALUE pageprint_render_to_file(int argc, VALUE *argv, VALUE self) {
799
877
  return Qtrue;
800
878
  }
801
879
 
802
- static VALUE pageprint_render(int argc, VALUE *argv, VALUE self) {
803
- VALUE html;
804
- VALUE options;
805
- pageprint_pdf_string_output_t output;
880
+ static VALUE pageprint_render_to_file(int argc, VALUE *argv, VALUE self)
881
+ {
882
+ pageprint_render_to_file_context_t context = { 0 };
806
883
 
807
- plutobook_t *book;
808
- pageprint_resource_fetcher_t resource_fetcher;
809
- pageprint_write_pdf_stream_args_t write_args;
884
+ rb_check_arity(argc, 2, 3);
810
885
 
811
- rb_check_arity(argc, 1, 2);
886
+ context.html = argv[0];
887
+ context.options = argc == 3 ? argv[2] : Qnil;
888
+
889
+ if (!RB_TYPE_P(argv[1], T_STRING)) {
890
+ rb_raise(rb_eTypeError, "path must be a String");
891
+ }
892
+
893
+ if (RSTRING_LEN(argv[1]) == 0) {
894
+ rb_raise(rb_eArgError, "path must not be empty");
895
+ }
896
+
897
+ pageprint_reject_embedded_nul(argv[1], "path");
898
+ context.path = pageprint_frozen_string_copy(argv[1]);
812
899
 
813
- html = argv[0];
814
- options = argc == 2 ? argv[1] : Qnil;
815
- output.output = rb_str_new(NULL, 0);
816
- output.state = 0;
817
- rb_enc_associate_index(output.output, rb_ascii8bit_encindex());
900
+ return rb_ensure(pageprint_render_to_file_body, (VALUE)&context, pageprint_destroy_book, (VALUE)&context.book_context);
901
+ }
902
+
903
+ static VALUE pageprint_render_body(VALUE value)
904
+ {
905
+ pageprint_render_context_t *context = (pageprint_render_context_t *)value;
906
+ pageprint_write_pdf_stream_args_t write_args;
818
907
 
819
- book = pageprint_create_book_from_html(html, options, &resource_fetcher);
908
+ pageprint_create_book_from_html(context->html, context->options, &context->book_context);
820
909
 
821
910
  plutobook_clear_error_message();
822
911
 
823
- write_args.book = book;
824
- write_args.output = &output;
912
+ write_args.book = context->book_context.book;
913
+ write_args.output = &context->output;
825
914
  write_args.ok = 0;
826
915
 
827
- RB_GC_GUARD(html);
828
- RB_GC_GUARD(options);
829
- RB_GC_GUARD(resource_fetcher.object);
830
- RB_GC_GUARD(output.output);
831
-
832
916
  rb_thread_call_without_gvl(
833
917
  pageprint_write_pdf_stream_without_gvl,
834
918
  &write_args,
@@ -836,21 +920,45 @@ static VALUE pageprint_render(int argc, VALUE *argv, VALUE self) {
836
920
  NULL
837
921
  );
838
922
 
839
- plutobook_destroy(book);
840
-
841
- if (resource_fetcher.state) {
842
- rb_jump_tag(resource_fetcher.state);
923
+ RB_GC_GUARD(context->book_context.resource_fetcher);
924
+ RB_GC_GUARD(context->book_context.resource_fetcher_state.error);
925
+ RB_GC_GUARD(context->output.output);
926
+ RB_GC_GUARD(context->output.error);
927
+ RB_GC_GUARD(context->html);
928
+ RB_GC_GUARD(context->options);
929
+
930
+ if (context->book_context.resource_fetcher_state.state) {
931
+ pageprint_jump_callback_error(
932
+ context->book_context.resource_fetcher_state.state,
933
+ context->book_context.resource_fetcher_state.error
934
+ );
843
935
  }
844
936
 
845
- if (output.state) {
846
- rb_jump_tag(output.state);
937
+ if (context->output.state) {
938
+ pageprint_jump_callback_error(context->output.state, context->output.error);
847
939
  }
848
940
 
849
941
  if (!write_args.ok) {
850
942
  pageprint_raise_plutobook_error(rb_eRuntimeError, "failed to write PDF to string");
851
943
  }
852
944
 
853
- return output.output;
945
+ return context->output.output;
946
+ }
947
+
948
+ static VALUE pageprint_render(int argc, VALUE *argv, VALUE self)
949
+ {
950
+ pageprint_render_context_t context = { 0 };
951
+
952
+ rb_check_arity(argc, 1, 2);
953
+
954
+ context.html = argv[0];
955
+ context.options = argc == 2 ? argv[1] : Qnil;
956
+ context.output.output = rb_str_new(NULL, 0);
957
+ context.output.error = Qnil;
958
+ context.output.state = 0;
959
+ rb_enc_associate_index(context.output.output, rb_ascii8bit_encindex());
960
+
961
+ return rb_ensure(pageprint_render_body, (VALUE)&context, pageprint_destroy_book, (VALUE)&context.book_context);
854
962
  }
855
963
 
856
964
  void Init_page_print(void) {
@@ -888,6 +996,6 @@ void Init_page_print(void) {
888
996
  id_mm = rb_intern_const("mm");
889
997
  id_px = rb_intern_const("px");
890
998
 
891
- rb_define_singleton_method(mPagePrint, "render_to_file", RUBY_METHOD_FUNC(pageprint_render_to_file), -1);
892
- rb_define_singleton_method(mPagePrint, "render", RUBY_METHOD_FUNC(pageprint_render), -1);
999
+ rb_define_singleton_method(mPagePrint, "render_to_file", pageprint_render_to_file, -1);
1000
+ rb_define_singleton_method(mPagePrint, "render", pageprint_render, -1);
893
1001
  }