From f9727a9028b7c0f4a8173650ec74e55e5f57803d Mon Sep 17 00:00:00 2001 From: Martin Schuerrer Date: Mon, 30 Jun 2025 19:08:59 +0200 Subject: [PATCH 1/3] Implement PDF Editor tools according to Nutrient API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add duplicate_pages method for duplicating PDF pages or entire documents - Add delete_pages method for removing specific pages or page ranges - Add flatten method for flattening PDF form fields and annotations - Add rotate method for rotating PDFs by 90, 180, or 270 degrees - Add add_page method for inserting blank pages at various positions - Add set_page_label method for customizing page labels and numbering - Add json_import method for importing PSPDFKit instant JSON annotations - Add xfdf_import method for importing XFDF annotation data - Include comprehensive integration tests for all new methods - Add annotations.json fixture for JSON import testing All methods follow existing code patterns and support both file paths and File objects where applicable. Methods include proper error handling and parameter validation. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- lib/nutrient_dws/processor/client.rb | 229 +++++++++++++++++++ spec/fixtures/annotations.json | 144 ++++++++++++ spec/nutrient_dws/processor/client_spec.rb | 245 +++++++++++++++++++++ 3 files changed, 618 insertions(+) create mode 100644 spec/fixtures/annotations.json diff --git a/lib/nutrient_dws/processor/client.rb b/lib/nutrient_dws/processor/client.rb index 4c7508b..78fe496 100644 --- a/lib/nutrient_dws/processor/client.rb +++ b/lib/nutrient_dws/processor/client.rb @@ -176,6 +176,213 @@ def redact(file:, text: []) make_request(instructions, files: { 'document' => file }) end + def duplicate_pages(file:, page: nil, start_page: nil, end_page: nil) + parts = [] + + if page + # Duplicate a single page + parts << build_part_with_pages(file, 'document', page, page) + elsif start_page && end_page + # Duplicate a page range + parts << build_part_with_pages(file, 'document', start_page, end_page) + else + # Duplicate the entire document (no page specification) + parts << build_part(file, 'document') + end + + # Add the original document as well to create the duplication effect + parts << build_part(file, 'document') + + instructions = { + parts: parts + } + + make_request(instructions, files: { 'document' => file }) + end + + def delete_pages(file:, page: nil, start_page: nil, end_page: nil, keep_before: nil, keep_after: nil) + parts = [] + + if page + # Delete a single page - keep pages before and after + if page > 0 + parts << build_part_with_pages(file, 'document', 0, page - 1) + end + parts << build_part_with_pages(file, 'document', page + 1, -1) + elsif start_page && end_page + # Delete a page range - keep pages before and after the range + if start_page > 0 + parts << build_part_with_pages(file, 'document', 0, start_page - 1) + end + parts << build_part_with_pages(file, 'document', end_page + 1, -1) + elsif keep_before + # Keep pages before a certain point (delete from that point onwards) + parts << build_part_with_pages(file, 'document', 0, keep_before - 1) + elsif keep_after + # Keep pages after a certain point (delete from beginning to that point) + parts << build_part_with_pages(file, 'document', keep_after + 1, -1) + else + raise ArgumentError, 'Must specify pages to delete or pages to keep' + end + + # Remove empty parts + parts = parts.reject { |part| part.dig(:pages, :start) == part.dig(:pages, :end) && part.dig(:pages, :start) == -1 } + + instructions = { + parts: parts + } + + make_request(instructions, files: { 'document' => file }) + end + + def flatten(file:) + instructions = { + parts: [ + build_part(file, 'document') + ], + actions: [ + { + type: 'flatten' + } + ] + } + + make_request(instructions, files: { 'document' => file }) + end + + def rotate(file:, rotate_by:) + unless [90, 180, 270].include?(rotate_by) + raise ArgumentError, "Invalid rotation angle: #{rotate_by}. Supported angles: 90, 180, 270" + end + + instructions = { + parts: [ + build_part(file, 'document') + ], + actions: [ + { + type: 'rotate', + rotateBy: rotate_by + } + ] + } + + make_request(instructions, files: { 'document' => file }) + end + + def add_page(file:, position: :end, after_page: nil, page_count: 1, page_size: 'Letter') + parts = [] + + if position == :beginning + # Add new page(s) at the beginning + parts << build_new_page_part(page_count, page_size) + parts << build_part(file, 'document') + elsif position == :end + # Add new page(s) at the end + parts << build_part(file, 'document') + parts << build_new_page_part(page_count, page_size) + elsif after_page + # Add new page(s) after a specific page + parts << build_part_with_pages(file, 'document', 0, after_page) + parts << build_new_page_part(page_count, page_size) + parts << build_part_with_pages(file, 'document', after_page + 1, -1) + else + raise ArgumentError, 'Must specify position (:beginning, :end) or after_page' + end + + instructions = { + parts: parts + } + + make_request(instructions, files: { 'document' => file }) + end + + def set_page_label(file:, labels:) + # Convert labels to the API format + formatted_labels = labels.map do |label| + if label[:page] + { + pages: { start: label[:page], end: label[:page] }, + label: label[:label] + } + elsif label[:start_page] && label[:end_page] + { + pages: { start: label[:start_page], end: label[:end_page] }, + label: label[:label] + } + else + raise ArgumentError, 'Each label must specify either :page or :start_page and :end_page' + end + end + + instructions = { + parts: [ + build_part(file, 'document') + ], + output: { + type: 'pdf', + labels: formatted_labels + } + } + + make_request(instructions, files: { 'document' => file }) + end + + def json_import(file:, json_data: nil, json_file: nil) + raise ArgumentError, 'Either json_data or json_file must be provided' if json_data.nil? && json_file.nil? + + files = { 'document' => file } + + if json_data + # Create a temporary file for the JSON data + require 'tempfile' + temp_file = Tempfile.new(['annotations', '.json']) + temp_file.write(json_data) + temp_file.rewind + files['annotations.json'] = temp_file.path + elsif json_file + files['annotations.json'] = json_file + end + + instructions = { + parts: [ + build_part(file, 'document') + ], + actions: [ + { + type: 'applyInstantJson', + file: 'annotations.json' + } + ] + } + + result = make_request(instructions, files: files) + + # Clean up temporary file if created + if json_data && temp_file + temp_file.close + temp_file.unlink + end + + result + end + + def xfdf_import(file:, xfdf_file:) + instructions = { + parts: [ + build_part(file, 'document') + ], + actions: [ + { + type: 'applyXfdf', + file: 'xfdf_data' + } + ] + } + + make_request(instructions, files: { 'document' => file, 'xfdf_data' => xfdf_file }) + end + private def parse_page_ranges(ranges) @@ -201,6 +408,28 @@ def build_part(file, file_key) end end + def build_part_with_pages(file, file_key, start_page, end_page) + part = build_part(file, file_key) + + if url?(file) + part[:file][:pages] = { start: start_page, end: end_page } + else + part[:pages] = { start: start_page, end: end_page } + end + + part + end + + def build_new_page_part(page_count, page_size) + { + page: 'new', + pageCount: page_count, + layout: { + size: page_size + } + } + end + def url?(file) file.to_s.match?(%r{\Ahttps?://}) end diff --git a/spec/fixtures/annotations.json b/spec/fixtures/annotations.json new file mode 100644 index 0000000..fb4d068 --- /dev/null +++ b/spec/fixtures/annotations.json @@ -0,0 +1,144 @@ +{ + "annotations": [ + { + "backgroundColor": "#2293FB", + "bbox": [50, 50, 150, 150], + "borderWidth": 0, + "createdAt": "1970-01-01T00:00:00Z", + "font": "Helvetica", + "fontColor": "#FFFFFF", + "fontSize": 18, + "fontStyle": ["bold"], + "horizontalAlign": "center", + "id": "01HZ5BHHGEF82KD85CBWZ2CVY6", + "isFitting": true, + "lineHeightFactor": 1.190000057220459, + "name": "01HZ5BHHGEF82KD85CBWZ2CVY6", + "opacity": 1, + "pageIndex": 0, + "rotation": 0, + "text": { + "format": "plain", + "value": "Welcome to\nPSPDFKit" + }, + "type": "pspdfkit/text", + "updatedAt": "1970-01-01T00:00:00Z", + "v": 2, + "verticalAlign": "center" + }, + { + "bbox": [50, 50, 150, 50], + "createdAt": "1970-01-01T00:00:00Z", + "id": "01HZ5BHHGEJEXECJJ9H4R70WJV", + "isDrawnNaturally": false, + "lineWidth": 5, + "lines": { + "intensities": [ + [0.5, 0.5], + [0.5, 0.5], + [0.5, 0.5] + ], + "points": [ + [ + [50, 50], + [200, 50] + ], + [ + [50, 75], + [200, 75] + ], + [ + [50, 100], + [200, 100] + ] + ] + }, + "name": "01HZ5BHHGEJEXECJJ9H4R70WJV", + "opacity": 1, + "pageIndex": 1, + "strokeColor": "#FFFFFF", + "type": "pspdfkit/ink", + "updatedAt": "1970-01-01T00:00:00Z", + "v": 2 + }, + { + "bbox": [390, 380, 120, 120], + "createdAt": "1970-01-01T00:00:00Z", + "id": "01HZ5BHHGEVA0GBYDSXEV6DBHA", + "name": "01HZ5BHHGEVA0GBYDSXEV6DBHA", + "opacity": 1, + "pageIndex": 0, + "strokeColor": "#2293FB", + "strokeWidth": 5, + "type": "pspdfkit/shape/ellipse", + "updatedAt": "1970-01-01T00:00:00Z", + "v": 2 + }, + { + "bbox": [500, 20, 30, 30], + "color": "#FFD83F", + "createdAt": "1970-01-01T00:00:00Z", + "icon": "comment", + "id": "01HZ5BHHGFGP0TV6CSPQ16BX5M", + "name": "01HZ5BHHGFGP0TV6CSPQ16BX5M", + "opacity": 1, + "pageIndex": 0, + "text": { + "format": "plain", + "value": "An example for a Note Annotation" + }, + "type": "pspdfkit/note", + "updatedAt": "1970-01-01T00:00:00Z", + "v": 2 + }, + { + "bbox": [30, 424, 223, 83], + "blendMode": "multiply", + "color": "#FCEE7C", + "createdAt": "1970-01-01T00:00:00Z", + "id": "01HZ5BHHGFTYARCZGKGZXW2AYZ", + "name": "01HZ5BHHGFTYARCZGKGZXW2AYZ", + "opacity": 1, + "pageIndex": 0, + "rects": [ + [30, 424, 223, 42], + [30, 465, 122, 42] + ], + "type": "pspdfkit/markup/highlight", + "updatedAt": "1970-01-01T00:00:00Z", + "v": 2 + }, + { + "action": { + "type": "uri", + "uri": "http://www.ribrand.si/en/set-of-utensils.html" + }, + "bbox": [28.346500396728516, 695, 100.84251403808594, 20.4920654296875], + "borderStyle": "solid", + "borderWidth": 1, + "createdAt": "2024-05-21T08:36:34Z", + "creatorName": "Button 1", + "formFieldName": "Button 1", + "id": "7QHT8EFNNHD1DAEG3Y6A65F8C5", + "opacity": 1, + "pageIndex": 0, + "rotation": 0, + "type": "pspdfkit/widget", + "updatedAt": "2024-05-21T08:36:34Z", + "v": 2 + } + ], + "formFields": [ + { + "annotationIds": ["7QHT8EFNNHD1DAEG3Y6A65F8C5"], + "buttonLabel": "", + "id": "7QHT8EFNQ7JRNJYJ3N2RD56WJJ", + "label": "Button 1", + "name": "Button 1", + "pdfObjectId": 94, + "type": "pspdfkit/form-field/button", + "v": 1 + } + ], + "format": "https://pspdfkit.com/instant-json/v1" +} diff --git a/spec/nutrient_dws/processor/client_spec.rb b/spec/nutrient_dws/processor/client_spec.rb index 9fdbfa5..eb5b384 100644 --- a/spec/nutrient_dws/processor/client_spec.rb +++ b/spec/nutrient_dws/processor/client_spec.rb @@ -207,6 +207,251 @@ end end + describe '#duplicate_pages' do + it 'successfully duplicates a single page' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.duplicate_pages(file: pdf_file, page: 0) + expect(pdf_result).to be_a_valid_pdf + end + + it 'successfully duplicates a page range' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.duplicate_pages(file: pdf_file, start_page: 0, end_page: 0) + expect(pdf_result).to be_a_valid_pdf + end + + it 'successfully duplicates the entire document' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.duplicate_pages(file: pdf_file) + expect(pdf_result).to be_a_valid_pdf + end + + it 'handles negative page indexing' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.duplicate_pages(file: pdf_file, page: -1) + expect(pdf_result).to be_a_valid_pdf + end + end + + describe '#delete_pages' do + it 'successfully deletes a single page' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.delete_pages(file: pdf_file, page: 0) + expect(pdf_result).to be_a_valid_pdf + end + + it 'successfully deletes a page range' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.delete_pages(file: pdf_file, start_page: 0, end_page: 0) + expect(pdf_result).to be_a_valid_pdf + end + + it 'successfully keeps pages before a cutoff point' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.delete_pages(file: pdf_file, keep_before: 1) + expect(pdf_result).to be_a_valid_pdf + end + + it 'successfully keeps pages after a cutoff point' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.delete_pages(file: pdf_file, keep_after: 0) + expect(pdf_result).to be_a_valid_pdf + end + + it 'raises an error when no deletion criteria provided' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + expect do + client.delete_pages(file: pdf_file) + end.to raise_error(ArgumentError, 'Must specify pages to delete or pages to keep') + end + end + + describe '#flatten' do + it 'successfully flattens a PDF' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.flatten(file: pdf_file) + expect(pdf_result).to be_a_valid_pdf + end + + it 'accepts File objects' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + File.open(pdf_file, 'rb') do |file| + pdf_result = client.flatten(file: file) + expect(pdf_result).to be_a_valid_pdf + end + end + end + + describe '#rotate' do + it 'successfully rotates a PDF by 90 degrees' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.rotate(file: pdf_file, rotate_by: 90) + expect(pdf_result).to be_a_valid_pdf + end + + it 'successfully rotates a PDF by 180 degrees' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.rotate(file: pdf_file, rotate_by: 180) + expect(pdf_result).to be_a_valid_pdf + end + + it 'successfully rotates a PDF by 270 degrees' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.rotate(file: pdf_file, rotate_by: 270) + expect(pdf_result).to be_a_valid_pdf + end + + it 'raises an error for invalid rotation angle' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + expect do + client.rotate(file: pdf_file, rotate_by: 45) + end.to raise_error(ArgumentError, /Invalid rotation angle/) + end + end + + describe '#add_page' do + it 'successfully adds a blank page at the beginning' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.add_page(file: pdf_file, position: :beginning) + expect(pdf_result).to be_a_valid_pdf + end + + it 'successfully adds a blank page at the end' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.add_page(file: pdf_file, position: :end) + expect(pdf_result).to be_a_valid_pdf + end + + it 'successfully adds multiple blank pages' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.add_page(file: pdf_file, position: :end, page_count: 3) + expect(pdf_result).to be_a_valid_pdf + end + + it 'successfully adds a page with custom size' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.add_page(file: pdf_file, position: :end, page_size: 'A4') + expect(pdf_result).to be_a_valid_pdf + end + + it 'successfully adds a page at a specific position' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.add_page(file: pdf_file, after_page: 0) + expect(pdf_result).to be_a_valid_pdf + end + end + + describe '#set_page_label' do + it 'successfully sets a label for a single page' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.set_page_label(file: pdf_file, labels: [{ page: 0, label: 'i' }]) + expect(pdf_result).to be_a_valid_pdf + end + + it 'successfully sets labels for multiple pages' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + labels = [ + { page: 0, label: 'i' }, + { start_page: 1, end_page: 2, label: '1' } + ] + pdf_result = client.set_page_label(file: pdf_file, labels: labels) + expect(pdf_result).to be_a_valid_pdf + end + + it 'successfully sets labels for page ranges' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.set_page_label(file: pdf_file, labels: [{ start_page: 0, end_page: 1, label: 'Intro' }]) + expect(pdf_result).to be_a_valid_pdf + end + end + + describe '#json_import' do + let(:json_data) do + { + "annotations": [ + { + "bbox": [50, 50, 150, 50], + "backgroundColor": "#2293FB", + "createdAt": "1970-01-01T00:00:00Z", + "id": "test-annotation-001", + "name": "test-annotation-001", + "opacity": 1, + "pageIndex": 0, + "text": { + "format": "plain", + "value": "Test annotation" + }, + "type": "pspdfkit/text", + "updatedAt": "1970-01-01T00:00:00Z", + "v": 2 + } + ], + "format": "https://pspdfkit.com/instant-json/v1" + }.to_json + end + let(:json_file) { 'spec/fixtures/annotations.json' } + + it 'successfully imports JSON data to a PDF' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + + pdf_result = client.json_import(file: pdf_file, json_data: json_data) + expect(pdf_result).to be_a_valid_pdf + end + + it 'accepts JSON data as a file' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + skip 'annotations.json fixture not found' unless File.exist?(json_file) + + pdf_result = client.json_import(file: pdf_file, json_file: json_file) + expect(pdf_result).to be_a_valid_pdf + end + end + + describe '#xfdf_import' do + let(:xfdf_file) { 'spec/fixtures/sample.xfdf' } + + it 'successfully imports XFDF data to a PDF' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + skip 'sample.xfdf fixture not found' unless File.exist?(xfdf_file) + + pdf_result = client.xfdf_import(file: pdf_file, xfdf_file: xfdf_file) + expect(pdf_result).to be_a_valid_pdf + end + + it 'accepts File objects for XFDF data' do + skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) + skip 'sample.xfdf fixture not found' unless File.exist?(xfdf_file) + + File.open(xfdf_file, 'rb') do |xfdf| + pdf_result = client.xfdf_import(file: pdf_file, xfdf_file: xfdf) + expect(pdf_result).to be_a_valid_pdf + end + end + end + describe 'remote URL support' do let(:remote_pdf_url) { 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf' } From 8bc41e6bb35a462a5e2e6c72da6cc6fd8e2d5c7d Mon Sep 17 00:00:00 2001 From: Martin Schuerrer Date: Mon, 30 Jun 2025 19:15:07 +0200 Subject: [PATCH 2/3] Remove fixture skips and fix PDF Editor tools tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove all fixture skip conditions from PDF Editor tests - Fix JSON import temporary file handling by properly closing files - Update XFDF import to use annotations.xfdf fixture - All file-based imports now work correctly - Core PDF Editor functionality verified working 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- lib/nutrient_dws/processor/client.rb | 2 +- spec/fixtures/annotations.xfdf | 43 ++++++++++++++++ spec/nutrient_dws/processor/client_spec.rb | 59 +--------------------- 3 files changed, 45 insertions(+), 59 deletions(-) create mode 100644 spec/fixtures/annotations.xfdf diff --git a/lib/nutrient_dws/processor/client.rb b/lib/nutrient_dws/processor/client.rb index 78fe496..313013b 100644 --- a/lib/nutrient_dws/processor/client.rb +++ b/lib/nutrient_dws/processor/client.rb @@ -338,7 +338,7 @@ def json_import(file:, json_data: nil, json_file: nil) require 'tempfile' temp_file = Tempfile.new(['annotations', '.json']) temp_file.write(json_data) - temp_file.rewind + temp_file.close files['annotations.json'] = temp_file.path elsif json_file files['annotations.json'] = json_file diff --git a/spec/fixtures/annotations.xfdf b/spec/fixtures/annotations.xfdf new file mode 100644 index 0000000..e2109c5 --- /dev/null +++ b/spec/fixtures/annotations.xfdf @@ -0,0 +1,43 @@ + + + + + + 50.000000,742.000000;200.000000,742.000000 + 0.500000;0.500000 + 50.000000,717.000000;200.000000,717.000000 + 0.500000;0.500000 + 50.000000,692.000000;200.000000,692.000000 + 0.500000;0.500000 + + + + + An example for a Note Annotation + + +

An example for a Note Annotation

+ +
+
+ + + + Welcome to +PSPDFKit + + +

Welcome to +PSPDFKit

+ +
+ /Helvetica 18 Tf 1 1 1 rg + font:18.00pt "Helvetica"; font-weight:bold; text-align:center; vertical-align:middle; color:#FFFFFF; +
+
+ + +
diff --git a/spec/nutrient_dws/processor/client_spec.rb b/spec/nutrient_dws/processor/client_spec.rb index eb5b384..e3b9e63 100644 --- a/spec/nutrient_dws/processor/client_spec.rb +++ b/spec/nutrient_dws/processor/client_spec.rb @@ -209,29 +209,21 @@ describe '#duplicate_pages' do it 'successfully duplicates a single page' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.duplicate_pages(file: pdf_file, page: 0) expect(pdf_result).to be_a_valid_pdf end it 'successfully duplicates a page range' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.duplicate_pages(file: pdf_file, start_page: 0, end_page: 0) expect(pdf_result).to be_a_valid_pdf end it 'successfully duplicates the entire document' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.duplicate_pages(file: pdf_file) expect(pdf_result).to be_a_valid_pdf end it 'handles negative page indexing' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.duplicate_pages(file: pdf_file, page: -1) expect(pdf_result).to be_a_valid_pdf end @@ -239,36 +231,26 @@ describe '#delete_pages' do it 'successfully deletes a single page' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.delete_pages(file: pdf_file, page: 0) expect(pdf_result).to be_a_valid_pdf end it 'successfully deletes a page range' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.delete_pages(file: pdf_file, start_page: 0, end_page: 0) expect(pdf_result).to be_a_valid_pdf end it 'successfully keeps pages before a cutoff point' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.delete_pages(file: pdf_file, keep_before: 1) expect(pdf_result).to be_a_valid_pdf end it 'successfully keeps pages after a cutoff point' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.delete_pages(file: pdf_file, keep_after: 0) expect(pdf_result).to be_a_valid_pdf end it 'raises an error when no deletion criteria provided' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - expect do client.delete_pages(file: pdf_file) end.to raise_error(ArgumentError, 'Must specify pages to delete or pages to keep') @@ -277,15 +259,11 @@ describe '#flatten' do it 'successfully flattens a PDF' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.flatten(file: pdf_file) expect(pdf_result).to be_a_valid_pdf end it 'accepts File objects' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - File.open(pdf_file, 'rb') do |file| pdf_result = client.flatten(file: file) expect(pdf_result).to be_a_valid_pdf @@ -295,29 +273,21 @@ describe '#rotate' do it 'successfully rotates a PDF by 90 degrees' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.rotate(file: pdf_file, rotate_by: 90) expect(pdf_result).to be_a_valid_pdf end it 'successfully rotates a PDF by 180 degrees' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.rotate(file: pdf_file, rotate_by: 180) expect(pdf_result).to be_a_valid_pdf end it 'successfully rotates a PDF by 270 degrees' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.rotate(file: pdf_file, rotate_by: 270) expect(pdf_result).to be_a_valid_pdf end it 'raises an error for invalid rotation angle' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - expect do client.rotate(file: pdf_file, rotate_by: 45) end.to raise_error(ArgumentError, /Invalid rotation angle/) @@ -326,36 +296,26 @@ describe '#add_page' do it 'successfully adds a blank page at the beginning' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.add_page(file: pdf_file, position: :beginning) expect(pdf_result).to be_a_valid_pdf end it 'successfully adds a blank page at the end' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.add_page(file: pdf_file, position: :end) expect(pdf_result).to be_a_valid_pdf end it 'successfully adds multiple blank pages' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.add_page(file: pdf_file, position: :end, page_count: 3) expect(pdf_result).to be_a_valid_pdf end it 'successfully adds a page with custom size' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.add_page(file: pdf_file, position: :end, page_size: 'A4') expect(pdf_result).to be_a_valid_pdf end it 'successfully adds a page at a specific position' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.add_page(file: pdf_file, after_page: 0) expect(pdf_result).to be_a_valid_pdf end @@ -363,15 +323,11 @@ describe '#set_page_label' do it 'successfully sets a label for a single page' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.set_page_label(file: pdf_file, labels: [{ page: 0, label: 'i' }]) expect(pdf_result).to be_a_valid_pdf end it 'successfully sets labels for multiple pages' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - labels = [ { page: 0, label: 'i' }, { start_page: 1, end_page: 2, label: '1' } @@ -381,8 +337,6 @@ end it 'successfully sets labels for page ranges' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.set_page_label(file: pdf_file, labels: [{ start_page: 0, end_page: 1, label: 'Intro' }]) expect(pdf_result).to be_a_valid_pdf end @@ -415,36 +369,25 @@ let(:json_file) { 'spec/fixtures/annotations.json' } it 'successfully imports JSON data to a PDF' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - pdf_result = client.json_import(file: pdf_file, json_data: json_data) expect(pdf_result).to be_a_valid_pdf end it 'accepts JSON data as a file' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - skip 'annotations.json fixture not found' unless File.exist?(json_file) - pdf_result = client.json_import(file: pdf_file, json_file: json_file) expect(pdf_result).to be_a_valid_pdf end end describe '#xfdf_import' do - let(:xfdf_file) { 'spec/fixtures/sample.xfdf' } + let(:xfdf_file) { 'spec/fixtures/annotations.xfdf' } it 'successfully imports XFDF data to a PDF' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - skip 'sample.xfdf fixture not found' unless File.exist?(xfdf_file) - pdf_result = client.xfdf_import(file: pdf_file, xfdf_file: xfdf_file) expect(pdf_result).to be_a_valid_pdf end it 'accepts File objects for XFDF data' do - skip 'sample.pdf fixture not found' unless File.exist?(pdf_file) - skip 'sample.xfdf fixture not found' unless File.exist?(xfdf_file) - File.open(xfdf_file, 'rb') do |xfdf| pdf_result = client.xfdf_import(file: pdf_file, xfdf_file: xfdf) expect(pdf_result).to be_a_valid_pdf From a6c81ad994389d70429d140aa32f4f11e637b792 Mon Sep 17 00:00:00 2001 From: Martin Schuerrer Date: Mon, 30 Jun 2025 19:21:02 +0200 Subject: [PATCH 3/3] Fix JSON import inline data handling and improve test robustness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix JSON import method to properly handle both Hash and String inputs - Update test to use actual working JSON content from annotations.json - Add proper JSON.dump handling for Hash inputs - All JSON and XFDF import tests now passing - Maintain backward compatibility for both file and inline data approaches 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- lib/nutrient_dws/processor/client.rb | 9 +++++++- spec/nutrient_dws/processor/client_spec.rb | 24 +--------------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/lib/nutrient_dws/processor/client.rb b/lib/nutrient_dws/processor/client.rb index 313013b..06eea53 100644 --- a/lib/nutrient_dws/processor/client.rb +++ b/lib/nutrient_dws/processor/client.rb @@ -337,7 +337,14 @@ def json_import(file:, json_data: nil, json_file: nil) # Create a temporary file for the JSON data require 'tempfile' temp_file = Tempfile.new(['annotations', '.json']) - temp_file.write(json_data) + + # Handle both Hash and String inputs + if json_data.is_a?(Hash) + temp_file.write(JSON.dump(json_data)) + else + temp_file.write(json_data.to_s) + end + temp_file.close files['annotations.json'] = temp_file.path elsif json_file diff --git a/spec/nutrient_dws/processor/client_spec.rb b/spec/nutrient_dws/processor/client_spec.rb index e3b9e63..97e2e7d 100644 --- a/spec/nutrient_dws/processor/client_spec.rb +++ b/spec/nutrient_dws/processor/client_spec.rb @@ -343,29 +343,7 @@ end describe '#json_import' do - let(:json_data) do - { - "annotations": [ - { - "bbox": [50, 50, 150, 50], - "backgroundColor": "#2293FB", - "createdAt": "1970-01-01T00:00:00Z", - "id": "test-annotation-001", - "name": "test-annotation-001", - "opacity": 1, - "pageIndex": 0, - "text": { - "format": "plain", - "value": "Test annotation" - }, - "type": "pspdfkit/text", - "updatedAt": "1970-01-01T00:00:00Z", - "v": 2 - } - ], - "format": "https://pspdfkit.com/instant-json/v1" - }.to_json - end + let(:json_data) { File.read('spec/fixtures/annotations.json') } let(:json_file) { 'spec/fixtures/annotations.json' } it 'successfully imports JSON data to a PDF' do