diff --git a/pdfgen.c b/pdfgen.c index ec212ff..f3674db 100644 --- a/pdfgen.c +++ b/pdfgen.c @@ -240,6 +240,7 @@ enum { OBJ_pages, OBJ_image, OBJ_link, + OBJ_form_field, OBJ_count, }; @@ -299,6 +300,25 @@ struct pdf_object { float target_x; /* Target location */ float target_y; } link; + struct { + int field_type; /* PDF_FIELD_TYPE_* */ + struct pdf_object + *page; /* Page containing widget (NULL for radio group) */ + float x, y, width, height; /* Widget rectangle */ + char name[64]; /* /T field name or radio option value */ + char + value[128]; /* /V default text value or selected radio name */ + float font_size; /* Font size for text field /DA string */ + uint32_t font_colour; /* Font colour for text field /DA string */ + int checked; /* Initial state for checkbox/radio button */ + struct pdf_object + *parent; /* For radio kids: parent group field */ + struct flexarray kids; /* For radio groups: kid widget fields */ + struct pdf_object + *on_appearance; /* Appearance XObject for on/yes state */ + struct pdf_object + *off_appearance; /* Appearance XObject for off/no state */ + } form_field; }; }; @@ -312,6 +332,9 @@ struct pdf_doc { struct pdf_object *current_font; + struct pdf_object + *form_font; /* Helvetica font for AcroForm /DR resources */ + struct pdf_object *last_objects[OBJ_count]; struct pdf_object *first_objects[OBJ_count]; }; @@ -675,6 +698,9 @@ static void pdf_object_destroy(struct pdf_object *object) case OBJ_bookmark: flexarray_clear(&object->bookmark.children); break; + case OBJ_form_field: + flexarray_clear(&object->form_field.kids); + break; } free(object); } @@ -1184,6 +1210,8 @@ static int pdf_save_object(struct pdf_doc *pdf, FILE *fp, int index) case OBJ_catalog: { struct pdf_object *outline = pdf_find_first_object(pdf, OBJ_outline); struct pdf_object *pages = pdf_find_first_object(pdf, OBJ_pages); + struct pdf_object *first_field = + pdf_find_first_object(pdf, OBJ_form_field); fprintf(fp, "<<\r\n" " /Type /Catalog\r\n"); @@ -1192,10 +1220,27 @@ static int pdf_save_object(struct pdf_doc *pdf, FILE *fp, int index) " /Outlines %d 0 R\r\n" " /PageMode /UseOutlines\r\n", outline->index); - fprintf(fp, - " /Pages %d 0 R\r\n" - ">>\r\n", - pages->index); + fprintf(fp, " /Pages %d 0 R\r\n", pages->index); + + if (first_field) { + fprintf(fp, " /AcroForm <<\r\n" + " /Fields ["); + for (struct pdf_object *f = first_field; f; f = f->next) { + /* Only top-level fields (not radio button kids) */ + if (f->form_field.field_type != PDF_FIELD_TYPE_RADIO_BUTTON) + fprintf(fp, "%d 0 R ", f->index); + } + fprintf(fp, "]\r\n"); + if (pdf->form_font) { + fprintf(fp, + " /DR << /Font << /Helv %d 0 R >> >>\r\n" + " /DA (/Helv 12 Tf 0 g)\r\n", + pdf->form_font->index); + } + fprintf(fp, " >>\r\n"); + } + + fprintf(fp, ">>\r\n"); break; } @@ -1214,6 +1259,125 @@ static int pdf_save_object(struct pdf_doc *pdf, FILE *fp, int index) break; } + case OBJ_form_field: { + switch (object->form_field.field_type) { + case PDF_FIELD_TYPE_TEXT: + fprintf(fp, "<<\r\n" + " /Type /Annot\r\n" + " /Subtype /Widget\r\n" + " /FT /Tx\r\n" + " /T ("); + pdf_print_escaped_string(fp, object->form_field.name); + fprintf(fp, ")\r\n"); + if (object->form_field.value[0]) { + fprintf(fp, " /V ("); + pdf_print_escaped_string(fp, object->form_field.value); + fprintf(fp, ")\r\n"); + } + fprintf(fp, + " /Rect [%f %f %f %f]\r\n" + " /DA (/Helv %f Tf %f %f %f rg)\r\n" + " /P %d 0 R\r\n" + ">>\r\n", + object->form_field.x, object->form_field.y, + object->form_field.x + object->form_field.width, + object->form_field.y + object->form_field.height, + object->form_field.font_size, + PDF_RGB_R(object->form_field.font_colour), + PDF_RGB_G(object->form_field.font_colour), + PDF_RGB_B(object->form_field.font_colour), + object->form_field.page->index); + break; + + case PDF_FIELD_TYPE_CHECKBOX: + fprintf(fp, "<<\r\n" + " /Type /Annot\r\n" + " /Subtype /Widget\r\n" + " /FT /Btn\r\n" + " /T ("); + pdf_print_escaped_string(fp, object->form_field.name); + fprintf(fp, + ")\r\n" + " /V /%s\r\n" + " /AS /%s\r\n" + " /Rect [%f %f %f %f]\r\n", + object->form_field.checked ? "Yes" : "Off", + object->form_field.checked ? "Yes" : "Off", + object->form_field.x, object->form_field.y, + object->form_field.x + object->form_field.width, + object->form_field.y + object->form_field.height); + if (object->form_field.on_appearance && + object->form_field.off_appearance) { + fprintf(fp, + " /AP << /N << /Yes %d 0 R /Off %d 0 R >> >>\r\n", + object->form_field.on_appearance->index, + object->form_field.off_appearance->index); + } + fprintf(fp, + " /P %d 0 R\r\n" + ">>\r\n", + object->form_field.page->index); + break; + + case PDF_FIELD_TYPE_RADIO: { + /* Radio button group (parent field, not a widget annotation) */ + fprintf(fp, "<<\r\n" + " /FT /Btn\r\n" + " /Ff 32768\r\n" + " /T ("); + pdf_print_escaped_string(fp, object->form_field.name); + fprintf(fp, ")\r\n"); + if (object->form_field.value[0]) + fprintf(fp, " /V /%s\r\n", object->form_field.value); + else + fprintf(fp, " /V /Off\r\n"); + fprintf(fp, " /Kids ["); + for (int i = 0; i < flexarray_size(&object->form_field.kids); + i++) { + struct pdf_object *kid = (struct pdf_object *)flexarray_get( + &object->form_field.kids, i); + fprintf(fp, "%d 0 R ", kid->index); + } + fprintf(fp, "]\r\n" + ">>\r\n"); + break; + } + + case PDF_FIELD_TYPE_RADIO_BUTTON: + /* Radio button widget (kid of a radio group) */ + fprintf(fp, + "<<\r\n" + " /Type /Annot\r\n" + " /Subtype /Widget\r\n" + " /Parent %d 0 R\r\n" + " /Rect [%f %f %f %f]\r\n" + " /AS /%s\r\n", + object->form_field.parent->index, object->form_field.x, + object->form_field.y, + object->form_field.x + object->form_field.width, + object->form_field.y + object->form_field.height, + object->form_field.checked ? object->form_field.name + : "Off"); + if (object->form_field.on_appearance && + object->form_field.off_appearance) { + fprintf(fp, " /AP << /N << /%s %d 0 R /Off %d 0 R >> >>\r\n", + object->form_field.name, + object->form_field.on_appearance->index, + object->form_field.off_appearance->index); + } + fprintf(fp, + " /P %d 0 R\r\n" + ">>\r\n", + object->form_field.page->index); + break; + + default: + return pdf_set_err(pdf, -EINVAL, "Invalid form field type %d", + object->form_field.field_type); + } + break; + } + default: return pdf_set_err(pdf, -EINVAL, "Invalid PDF object type %d", object->type); @@ -4068,8 +4232,7 @@ static int pdf_add_bmp_data(struct pdf_doc *pdf, struct pdf_object *page, if (header->bfOffBits >= len) return pdf_set_err(pdf, -EINVAL, "Invalid BMP image offset"); - if (len - header->bfOffBits < - (size_t)height * stride) + if (len - header->bfOffBits < (size_t)height * stride) return pdf_set_err(pdf, -EINVAL, "Wrong BMP image size"); if (bpp == 3) { @@ -4079,8 +4242,8 @@ static int pdf_add_bmp_data(struct pdf_doc *pdf, struct pdf_object *page, return pdf_set_err(pdf, -ENOMEM, "Insufficient memory for bitmap"); for (uint32_t pos = 0; pos < width * height; pos++) { - uint32_t src_pos = - header->bfOffBits + (pos / width) * stride + (pos % width) * 3; + uint32_t src_pos = header->bfOffBits + (pos / width) * stride + + (pos % width) * 3; bmp_data[pos * 3] = data[src_pos + 2]; bmp_data[pos * 3 + 1] = data[src_pos + 1]; @@ -4230,3 +4393,279 @@ int pdf_add_image_file(struct pdf_doc *pdf, struct pdf_object *page, float x, free(data); return ret; } + +/** + * Create a Form XObject (appearance stream) for checkbox/radio button states. + * Returns the newly created stream object, or NULL on error. + * The caller must NOT add this object to any page's children – it is + * referenced only through the widget's /AP dictionary. + */ +static struct pdf_object *pdf_create_form_xobject(struct pdf_doc *pdf, + float width, float height, + const char *content) +{ + struct pdf_object *obj = pdf_add_object(pdf, OBJ_stream); + if (!obj) + return NULL; + + size_t len = strlen(content); + dstr_printf(&obj->stream.stream, + "<< /Type /XObject /Subtype /Form" + " /BBox [0 0 %f %f] /Resources << >> /Length %zu >>\r\n" + "stream\r\n", + width, height, len); + dstr_append_data(&obj->stream.stream, content, len); + dstr_append(&obj->stream.stream, "\r\nendstream\r\n"); + /* Leave stream.page as NULL – appearance XObjects are not page children + */ + return obj; +} + +/** + * Find or create the Helvetica font used as the AcroForm default font. + * Records the result in pdf->form_font for use when writing the catalog. + */ +static struct pdf_object *pdf_get_form_font(struct pdf_doc *pdf) +{ + if (pdf->form_font) + return pdf->form_font; + + int last_index = 0; + for (struct pdf_object *obj = pdf_find_first_object(pdf, OBJ_font); obj; + obj = obj->next) { + if (strcmp(obj->font.name, "Helvetica") == 0) { + pdf->form_font = obj; + return obj; + } + last_index = obj->font.index; + } + + struct pdf_object *obj = pdf_add_object(pdf, OBJ_font); + if (!obj) + return NULL; + strncpy(obj->font.name, "Helvetica", sizeof(obj->font.name) - 1); + obj->font.name[sizeof(obj->font.name) - 1] = '\0'; + obj->font.index = last_index + 1; + pdf->form_font = obj; + return obj; +} + +int pdf_add_text_field(struct pdf_doc *pdf, struct pdf_object *page, float x, + float y, float width, float height, const char *name, + const char *value, float font_size, uint32_t colour) +{ + if (!page) + page = pdf_find_last_object(pdf, OBJ_page); + if (!page) + return pdf_set_err(pdf, -EINVAL, + "Unable to add text field, no pages available"); + if (!name || !name[0]) + return pdf_set_err(pdf, -EINVAL, "Text field name must not be empty"); + + if (!pdf_get_form_font(pdf)) + return pdf->errval; + + struct pdf_object *obj = pdf_add_object(pdf, OBJ_form_field); + if (!obj) + return pdf->errval; + + obj->form_field.field_type = PDF_FIELD_TYPE_TEXT; + obj->form_field.page = page; + obj->form_field.x = x; + obj->form_field.y = y; + obj->form_field.width = width; + obj->form_field.height = height; + strncpy(obj->form_field.name, name, sizeof(obj->form_field.name) - 1); + obj->form_field.name[sizeof(obj->form_field.name) - 1] = '\0'; + if (value) { + strncpy(obj->form_field.value, value, + sizeof(obj->form_field.value) - 1); + obj->form_field.value[sizeof(obj->form_field.value) - 1] = '\0'; + } + obj->form_field.font_size = (font_size > 0) ? font_size : 12.0f; + obj->form_field.font_colour = colour; + + if (flexarray_append(&page->page.annotations, obj) < 0) + return pdf_set_err(pdf, -ENOMEM, + "Unable to add text field annotation"); + return obj->index; +} + +int pdf_add_checkbox(struct pdf_doc *pdf, struct pdf_object *page, float x, + float y, float width, float height, const char *name, + int checked) +{ + if (!page) + page = pdf_find_last_object(pdf, OBJ_page); + if (!page) + return pdf_set_err(pdf, -EINVAL, + "Unable to add checkbox, no pages available"); + if (!name || !name[0]) + return pdf_set_err(pdf, -EINVAL, "Checkbox name must not be empty"); + + /* Checked appearance: draw a checkmark path */ + char on_content[256]; + snprintf(on_content, sizeof(on_content), + "q\n" + "0.5 w\n" + "%.4f %.4f m\n" + "%.4f %.4f l\n" + "%.4f %.4f l\n" + "S\n" + "Q\n", + width * 0.2f, height * 0.5f, width * 0.4f, height * 0.2f, + width * 0.8f, height * 0.8f); + + /* Unchecked appearance: empty form XObject */ + const char *off_content = "q Q\n"; + + struct pdf_object *on_ap = + pdf_create_form_xobject(pdf, width, height, on_content); + if (!on_ap) + return pdf->errval; + + struct pdf_object *off_ap = + pdf_create_form_xobject(pdf, width, height, off_content); + if (!off_ap) + return pdf->errval; + + struct pdf_object *obj = pdf_add_object(pdf, OBJ_form_field); + if (!obj) + return pdf->errval; + + obj->form_field.field_type = PDF_FIELD_TYPE_CHECKBOX; + obj->form_field.page = page; + obj->form_field.x = x; + obj->form_field.y = y; + obj->form_field.width = width; + obj->form_field.height = height; + strncpy(obj->form_field.name, name, sizeof(obj->form_field.name) - 1); + obj->form_field.name[sizeof(obj->form_field.name) - 1] = '\0'; + obj->form_field.checked = checked ? 1 : 0; + obj->form_field.on_appearance = on_ap; + obj->form_field.off_appearance = off_ap; + + if (flexarray_append(&page->page.annotations, obj) < 0) + return pdf_set_err(pdf, -ENOMEM, "Unable to add checkbox annotation"); + return obj->index; +} + +int pdf_add_radio_button(struct pdf_doc *pdf, struct pdf_object *page, + float x, float y, float width, float height, + const char *radio_group_name, + const char *option_name, int selected) +{ + if (!page) + page = pdf_find_last_object(pdf, OBJ_page); + if (!page) + return pdf_set_err(pdf, -EINVAL, + "Unable to add radio button, no pages available"); + if (!radio_group_name || !radio_group_name[0]) + return pdf_set_err(pdf, -EINVAL, + "Radio button group name must not be empty"); + if (!option_name || !option_name[0]) + return pdf_set_err(pdf, -EINVAL, + "Radio button option name must not be empty"); + + /* Find or create the parent radio group field */ + struct pdf_object *group = NULL; + for (struct pdf_object *f = pdf_find_first_object(pdf, OBJ_form_field); f; + f = f->next) { + if (f->form_field.field_type == PDF_FIELD_TYPE_RADIO && + strcmp(f->form_field.name, radio_group_name) == 0) { + group = f; + break; + } + } + if (!group) { + group = pdf_add_object(pdf, OBJ_form_field); + if (!group) + return pdf->errval; + group->form_field.field_type = PDF_FIELD_TYPE_RADIO; + strncpy(group->form_field.name, radio_group_name, + sizeof(group->form_field.name) - 1); + group->form_field.name[sizeof(group->form_field.name) - 1] = '\0'; + } + + /* Selected appearance: filled circle (approximated with Bezier curves). + * 0.5523 is the Bezier control-point distance constant (≈ 4/3*(√2-1)) + * that makes four cubic segments approximate a full circle. */ + char on_content[512]; + float cx = width / 2.0f; + float cy = height / 2.0f; + float r = (width < height ? width : height) * 0.3f; + float k = 0.5523f * r; + snprintf(on_content, sizeof(on_content), + "q\n" + "0 g\n" + "%.4f %.4f m\n" + "%.4f %.4f %.4f %.4f %.4f %.4f c\n" + "%.4f %.4f %.4f %.4f %.4f %.4f c\n" + "%.4f %.4f %.4f %.4f %.4f %.4f c\n" + "%.4f %.4f %.4f %.4f %.4f %.4f c\n" + "f\n" + "Q\n", + cx - r, cy, cx - r, cy + k, cx - k, cy + r, cx, cy + r, cx + k, + cy + r, cx + r, cy + k, cx + r, cy, cx + r, cy - k, cx + k, + cy - r, cx, cy - r, cx - k, cy - r, cx - r, cy - k, cx - r, cy); + + /* Unselected appearance: empty */ + const char *off_content = "q Q\n"; + + struct pdf_object *on_ap = + pdf_create_form_xobject(pdf, width, height, on_content); + if (!on_ap) + return pdf->errval; + + struct pdf_object *off_ap = + pdf_create_form_xobject(pdf, width, height, off_content); + if (!off_ap) + return pdf->errval; + + /* Create the kid widget */ + struct pdf_object *kid = pdf_add_object(pdf, OBJ_form_field); + if (!kid) + return pdf->errval; + + kid->form_field.field_type = PDF_FIELD_TYPE_RADIO_BUTTON; + kid->form_field.page = page; + kid->form_field.x = x; + kid->form_field.y = y; + kid->form_field.width = width; + kid->form_field.height = height; + strncpy(kid->form_field.name, option_name, + sizeof(kid->form_field.name) - 1); + kid->form_field.name[sizeof(kid->form_field.name) - 1] = '\0'; + kid->form_field.checked = selected ? 1 : 0; + kid->form_field.parent = group; + kid->form_field.on_appearance = on_ap; + kid->form_field.off_appearance = off_ap; + + /* If this button is selected, ensure no other button in the group + * is already selected (enforce single-selection invariant) and + * update the group's current value. */ + if (selected) { + for (int i = 0; i < flexarray_size(&group->form_field.kids); i++) { + struct pdf_object *existing = (struct pdf_object *)flexarray_get( + &group->form_field.kids, i); + if (existing->form_field.checked) + return pdf_set_err( + pdf, -EINVAL, + "Radio group '%s' already has a selected button", + radio_group_name); + } + strncpy(group->form_field.value, option_name, + sizeof(group->form_field.value) - 1); + group->form_field.value[sizeof(group->form_field.value) - 1] = '\0'; + } + + if (flexarray_append(&group->form_field.kids, kid) < 0) + return pdf_set_err(pdf, -ENOMEM, + "Unable to add radio button to group"); + + if (flexarray_append(&page->page.annotations, kid) < 0) + return pdf_set_err(pdf, -ENOMEM, + "Unable to add radio button annotation"); + + return kid->index; +} diff --git a/pdfgen.h b/pdfgen.h index 69e41f8..df34ded 100644 --- a/pdfgen.h +++ b/pdfgen.h @@ -205,13 +205,13 @@ struct pdf_path_operation { * Convert a value in inches into a number of points. * @param inch inches value to convert to points */ -#define PDF_INCH_TO_POINT(inch) ((float)((inch)*72.0f)) +#define PDF_INCH_TO_POINT(inch) ((float)((inch) * 72.0f)) /** * Convert a value in milli-meters into a number of points. * @param mm millimeter value to convert to points */ -#define PDF_MM_TO_POINT(mm) ((float)((mm)*72.0f / 25.4f)) +#define PDF_MM_TO_POINT(mm) ((float)((mm) * 72.0f / 25.4f)) /*! Point width of a standard US-Letter page */ #define PDF_LETTER_WIDTH PDF_INCH_TO_POINT(8.5f) @@ -237,7 +237,7 @@ struct pdf_path_operation { * in PDFGen */ #define PDF_RGB(r, g, b) \ - (uint32_t)((((r)&0xff) << 16) | (((g)&0xff) << 8) | (((b)&0xff))) + (uint32_t)((((r) & 0xff) << 16) | (((g) & 0xff) << 8) | (((b) & 0xff))) /** * Convert four 8-bit ARGB values into a single packed 32-bit @@ -246,8 +246,8 @@ struct pdf_path_operation { * (transparent) */ #define PDF_ARGB(a, r, g, b) \ - (uint32_t)(((uint32_t)((a)&0xff) << 24) | (((r)&0xff) << 16) | \ - (((g)&0xff) << 8) | (((b)&0xff))) + (uint32_t)(((uint32_t)((a) & 0xff) << 24) | (((r) & 0xff) << 16) | \ + (((g) & 0xff) << 8) | (((b) & 0xff))) /*! Utility macro to provide bright red */ #define PDF_RED PDF_RGB(0xff, 0, 0) @@ -793,6 +793,75 @@ int pdf_parse_image_header(struct pdf_img_info *info, const uint8_t *data, size_t length, char *err_msg, size_t err_msg_length); +/** + * List of PDF form field types supported by pdf_add_text_field, + * pdf_add_checkbox and pdf_add_radio_button. + */ +enum { + PDF_FIELD_TYPE_TEXT, //!< Single-line text input field + PDF_FIELD_TYPE_CHECKBOX, //!< Boolean checkbox + PDF_FIELD_TYPE_RADIO, //!< Radio button group (internal use) + PDF_FIELD_TYPE_RADIO_BUTTON, //!< Individual radio button (internal use) + PDF_FIELD_TYPE_PUSH_BUTTON, //!< Push button (reserved for future use) +}; + +/** + * Add a text input form field to the document. + * The field can be filled in by the user when the PDF is opened in a + * form-capable viewer. + * @param pdf PDF document to add to + * @param page Page to add the field to (NULL => most recently added page) + * @param x X coordinate of the bottom-left corner of the field + * @param y Y coordinate of the bottom-left corner of the field + * @param width Width of the field + * @param height Height of the field + * @param name Field name (used to identify the field; must not be empty) + * @param value Initial/default text value, or NULL for an empty field + * @param font_size Font size for the field text (0 => default 12pt) + * @param colour Text colour + * @return < 0 on failure, object index on success + */ +int pdf_add_text_field(struct pdf_doc *pdf, struct pdf_object *page, float x, + float y, float width, float height, const char *name, + const char *value, float font_size, uint32_t colour); + +/** + * Add a checkbox form field to the document. + * @param pdf PDF document to add to + * @param page Page to add the field to (NULL => most recently added page) + * @param x X coordinate of the bottom-left corner of the checkbox + * @param y Y coordinate of the bottom-left corner of the checkbox + * @param width Width of the checkbox + * @param height Height of the checkbox + * @param name Field name (must not be empty) + * @param checked Non-zero for initially checked, zero for unchecked + * @return < 0 on failure, object index on success + */ +int pdf_add_checkbox(struct pdf_doc *pdf, struct pdf_object *page, float x, + float y, float width, float height, const char *name, + int checked); + +/** + * Add a radio button to a named radio button group. + * All radio buttons sharing the same @p radio_group_name belong to the + * same group; at most one can be selected at a time. + * Call this function once for each option in the group. + * @param pdf PDF document to add to + * @param page Page to add the button to (NULL => most recently added page) + * @param x X coordinate of the bottom-left corner of the button + * @param y Y coordinate of the bottom-left corner of the button + * @param width Width of the button + * @param height Height of the button + * @param radio_group_name Name of the radio button group (must not be empty) + * @param option_name Value name for this specific button (must not be empty) + * @param selected Non-zero if this button should be initially selected + * @return < 0 on failure, object index on success + */ +int pdf_add_radio_button(struct pdf_doc *pdf, struct pdf_object *page, + float x, float y, float width, float height, + const char *radio_group_name, + const char *option_name, int selected); + #ifdef __cplusplus } #endif diff --git a/tests/main.c b/tests/main.c index 8d9887b..a57f002 100644 --- a/tests/main.c +++ b/tests/main.c @@ -285,6 +285,68 @@ int main(int argc, char *argv[]) } pdf_add_rgb24(pdf, NULL, 72, 72, 288, 144, data_rgb, 16, 8); + /* Page 6: Form fields */ + pdf_append_page(pdf); + pdf_add_bookmark(pdf, NULL, -1, "Form Page"); + + pdf_add_text(pdf, NULL, "PDF Forms Demo", 16, 50, 780, PDF_BLACK); + + /* Text field */ + pdf_add_text(pdf, NULL, "Name:", 12, 50, 740, PDF_BLACK); + err = pdf_add_text_field(pdf, NULL, 120, 730, 200, 20, "name", + "Enter name here", 12, PDF_BLACK); + if (err < 0) { + fprintf(stderr, "Failed to add text field: %d\n", err); + pdf_destroy(pdf); + return -1; + } + + /* Checkbox - initially checked */ + pdf_add_text(pdf, NULL, "Agree to terms:", 12, 50, 700, PDF_BLACK); + err = pdf_add_checkbox(pdf, NULL, 180, 698, 14, 14, "agree", 1); + if (err < 0) { + fprintf(stderr, "Failed to add checkbox: %d\n", err); + pdf_destroy(pdf); + return -1; + } + + /* Checkbox - initially unchecked */ + pdf_add_text(pdf, NULL, "Subscribe to newsletter:", 12, 50, 670, + PDF_BLACK); + err = pdf_add_checkbox(pdf, NULL, 230, 668, 14, 14, "subscribe", 0); + if (err < 0) { + fprintf(stderr, "Failed to add checkbox: %d\n", err); + pdf_destroy(pdf); + return -1; + } + + /* Radio buttons */ + pdf_add_text(pdf, NULL, "Favourite colour:", 12, 50, 640, PDF_BLACK); + pdf_add_text(pdf, NULL, "Red", 12, 200, 640, PDF_BLACK); + err = + pdf_add_radio_button(pdf, NULL, 190, 638, 14, 14, "colour", "red", 1); + if (err < 0) { + fprintf(stderr, "Failed to add radio button: %d\n", err); + pdf_destroy(pdf); + return -1; + } + pdf_add_text(pdf, NULL, "Green", 12, 260, 640, PDF_BLACK); + err = pdf_add_radio_button(pdf, NULL, 250, 638, 14, 14, "colour", "green", + 0); + if (err < 0) { + fprintf(stderr, "Failed to add radio button: %d\n", err); + pdf_destroy(pdf); + return -1; + } + pdf_add_text(pdf, NULL, "Blue", 12, 330, 640, PDF_BLACK); + err = pdf_add_radio_button(pdf, NULL, 320, 638, 14, 14, "colour", "blue", + 0); + if (err < 0) { + fprintf(stderr, "Failed to add radio button: %d\n", err); + pdf_destroy(pdf); + return -1; + } + pdf_save(pdf, "output.pdf"); const char *err_str = pdf_get_err(pdf, &err); diff --git a/tests/massive-file b/tests/massive-file new file mode 100755 index 0000000..848ce0e Binary files /dev/null and b/tests/massive-file differ diff --git a/tests/tests.sh b/tests/tests.sh index 5cb8252..dfc7d4e 100755 --- a/tests/tests.sh +++ b/tests/tests.sh @@ -53,7 +53,7 @@ run "check barcode Code 128" grep -q "CODE-128:Code128" output-barcodes.txt run "check barcode CODE 39" grep -q "CODE-39:CODE39" output-barcodes.txt # Check for pdftk meta data -run "check page count" grep -q "NumberOfPages: 5$" output.pdftk +run "check page count" grep -q "NumberOfPages: 6$" output.pdftk run "check bookmarks" grep -q "BookmarkTitle: First page$" output.pdftk run "check for subject" grep -q "InfoValue: My subject$" output.pdftk