poml 0.0.6 → 0.0.7
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.
- checksums.yaml +4 -4
- data/docs/tutorial/advanced/performance.md +695 -0
- data/docs/tutorial/advanced/tool-registration.md +776 -0
- data/docs/tutorial/basic-usage.md +351 -0
- data/docs/tutorial/components/chat-components.md +552 -0
- data/docs/tutorial/components/formatting.md +623 -0
- data/docs/tutorial/components/index.md +366 -0
- data/docs/tutorial/components/media-components.md +259 -0
- data/docs/tutorial/components/schema-components.md +668 -0
- data/docs/tutorial/index.md +184 -0
- data/docs/tutorial/output-formats.md +688 -0
- data/docs/tutorial/quickstart.md +30 -0
- data/docs/tutorial/template-engine.md +540 -0
- data/lib/poml/components/base.rb +146 -4
- data/lib/poml/components/content.rb +10 -3
- data/lib/poml/components/data.rb +539 -19
- data/lib/poml/components/examples.rb +235 -1
- data/lib/poml/components/formatting.rb +184 -18
- data/lib/poml/components/layout.rb +7 -2
- data/lib/poml/components/lists.rb +69 -35
- data/lib/poml/components/meta.rb +134 -5
- data/lib/poml/components/output_schema.rb +19 -1
- data/lib/poml/components/template.rb +72 -61
- data/lib/poml/components/text.rb +30 -1
- data/lib/poml/components/tool.rb +81 -0
- data/lib/poml/components/tool_definition.rb +339 -10
- data/lib/poml/components/tools.rb +14 -0
- data/lib/poml/components/utilities.rb +34 -18
- data/lib/poml/components.rb +19 -0
- data/lib/poml/context.rb +19 -4
- data/lib/poml/parser.rb +88 -63
- data/lib/poml/renderer.rb +191 -9
- data/lib/poml/template_engine.rb +138 -13
- data/lib/poml/version.rb +1 -1
- data/lib/poml.rb +16 -1
- data/readme.md +154 -27
- metadata +31 -4
- data/TUTORIAL.md +0 -987
data/lib/poml/components/data.rb
CHANGED
@@ -9,6 +9,7 @@ module Poml
|
|
9
9
|
|
10
10
|
src = get_attribute('src')
|
11
11
|
records_attr = get_attribute('records')
|
12
|
+
data_attr = get_attribute('data') # Support 'data' attribute as alias for 'records'
|
12
13
|
_columns_attr = get_attribute('columns') # Not used but may be needed for future features
|
13
14
|
parser = get_attribute('parser', 'auto')
|
14
15
|
syntax = get_attribute('syntax')
|
@@ -22,9 +23,43 @@ module Poml
|
|
22
23
|
load_table_data(src, parser)
|
23
24
|
elsif records_attr
|
24
25
|
parse_records_attribute(records_attr)
|
25
|
-
elsif
|
26
|
+
elsif data_attr
|
27
|
+
parse_records_attribute(data_attr)
|
28
|
+
elsif @element.children.any? { |child| child.tag_name == :tr || child.tag_name == :thead || child.tag_name == :tbody || child.tag_name == :tfoot }
|
26
29
|
# Handle HTML-style table markup
|
27
|
-
|
30
|
+
|
31
|
+
# Check if any children contain for loops - if so, use normal rendering
|
32
|
+
has_for_loops = @element.children.any? { |child| contains_for_loops?(child) }
|
33
|
+
|
34
|
+
if has_for_loops
|
35
|
+
# When for loops are present, use normal child rendering instead of table parsing
|
36
|
+
# This allows for loops to be processed normally
|
37
|
+
# Set output format to HTML temporarily so tr/td elements render as HTML
|
38
|
+
original_format = @context.output_format
|
39
|
+
@context.output_format = 'html'
|
40
|
+
|
41
|
+
children_content = render_children
|
42
|
+
|
43
|
+
# Restore original format
|
44
|
+
@context.output_format = original_format
|
45
|
+
|
46
|
+
# For raw format, convert HTML table to markdown table
|
47
|
+
if original_format == 'raw'
|
48
|
+
return convert_html_table_to_markdown(children_content)
|
49
|
+
else
|
50
|
+
return children_content
|
51
|
+
end
|
52
|
+
elsif @context.output_format == 'html' || xml_mode?
|
53
|
+
# In HTML or XML mode, render children directly to preserve nested components
|
54
|
+
children_content = render_children
|
55
|
+
if xml_mode?
|
56
|
+
return "<table>\n#{children_content}</table>\n"
|
57
|
+
else
|
58
|
+
return children_content
|
59
|
+
end
|
60
|
+
else
|
61
|
+
parse_html_table_children
|
62
|
+
end
|
28
63
|
else
|
29
64
|
{ records: [], columns: [] }
|
30
65
|
end
|
@@ -32,14 +67,23 @@ module Poml
|
|
32
67
|
# Apply column and record selection
|
33
68
|
data = apply_selection(data, selected_columns, selected_records, max_records, max_columns)
|
34
69
|
|
35
|
-
# Check syntax preference
|
36
|
-
if syntax == 'tsv' || syntax == 'csv'
|
70
|
+
# Check syntax preference and output format
|
71
|
+
result = if syntax == 'tsv' || syntax == 'csv'
|
37
72
|
render_table_raw(data, syntax)
|
38
73
|
elsif xml_mode?
|
39
74
|
render_table_xml(data)
|
75
|
+
elsif @context.output_format == 'html'
|
76
|
+
render_table_html(data)
|
40
77
|
else
|
41
78
|
render_table_markdown(data)
|
42
79
|
end
|
80
|
+
|
81
|
+
# Apply inline rendering if requested
|
82
|
+
if inline? && !xml_mode?
|
83
|
+
result.strip
|
84
|
+
else
|
85
|
+
result
|
86
|
+
end
|
43
87
|
end
|
44
88
|
|
45
89
|
private
|
@@ -104,7 +148,7 @@ module Poml
|
|
104
148
|
end
|
105
149
|
|
106
150
|
def parse_json_file(file_path)
|
107
|
-
content =
|
151
|
+
content = read_file_with_encoding(file_path)
|
108
152
|
records = JSON.parse(content)
|
109
153
|
|
110
154
|
# Extract columns from first record if it's an array of objects
|
@@ -119,7 +163,7 @@ module Poml
|
|
119
163
|
|
120
164
|
def parse_jsonl_file(file_path)
|
121
165
|
records = []
|
122
|
-
|
166
|
+
read_file_lines_with_encoding(file_path).each do |line|
|
123
167
|
records << JSON.parse(line.strip) unless line.strip.empty?
|
124
168
|
end
|
125
169
|
|
@@ -191,12 +235,10 @@ module Poml
|
|
191
235
|
records = []
|
192
236
|
columns = []
|
193
237
|
|
194
|
-
# Extract rows from tr children
|
195
|
-
@element.
|
196
|
-
next unless child.tag_name == :tr
|
197
|
-
|
238
|
+
# Extract rows from tr children (including nested in thead/tbody)
|
239
|
+
find_tr_elements(@element).each do |tr_element|
|
198
240
|
row_data = {}
|
199
|
-
|
241
|
+
tr_element.children.each_with_index do |cell, index|
|
200
242
|
next unless cell.tag_name == :td || cell.tag_name == :th
|
201
243
|
|
202
244
|
# Get cell content (render children to get text)
|
@@ -220,6 +262,68 @@ module Poml
|
|
220
262
|
{ records: records, columns: columns }
|
221
263
|
end
|
222
264
|
|
265
|
+
def find_tr_elements(element)
|
266
|
+
tr_elements = []
|
267
|
+
|
268
|
+
element.children.each do |child|
|
269
|
+
if child.tag_name == :tr
|
270
|
+
tr_elements << child
|
271
|
+
elsif child.tag_name == :thead || child.tag_name == :tbody || child.tag_name == :tfoot
|
272
|
+
# Recursively find tr elements in table sections
|
273
|
+
tr_elements.concat(find_tr_elements(child))
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
tr_elements
|
278
|
+
end
|
279
|
+
|
280
|
+
def contains_for_loops?(element)
|
281
|
+
# Check if this element or any of its children contain for loops
|
282
|
+
return true if element.tag_name == :for
|
283
|
+
|
284
|
+
element.children.any? { |child| contains_for_loops?(child) }
|
285
|
+
end
|
286
|
+
|
287
|
+
def convert_html_table_to_markdown(html_content)
|
288
|
+
# Simple HTML table to markdown conversion
|
289
|
+
# This is a basic implementation - can be enhanced as needed
|
290
|
+
|
291
|
+
# Extract table rows from HTML
|
292
|
+
rows = []
|
293
|
+
html_content.scan(/<tr[^>]*>(.*?)<\/tr>/m) do |row_match|
|
294
|
+
row_content = row_match[0]
|
295
|
+
cells = []
|
296
|
+
row_content.scan(/<t[hd][^>]*>(.*?)<\/t[hd]>/m) do |cell_match|
|
297
|
+
cell_text = cell_match[0].strip
|
298
|
+
# Remove HTML tags from cell content
|
299
|
+
cell_text = cell_text.gsub(/<[^>]+>/, '')
|
300
|
+
cells << cell_text
|
301
|
+
end
|
302
|
+
rows << cells unless cells.empty?
|
303
|
+
end
|
304
|
+
|
305
|
+
return '' if rows.empty?
|
306
|
+
|
307
|
+
# Convert to markdown table
|
308
|
+
if rows.length == 1
|
309
|
+
# Only data rows, create a simple header
|
310
|
+
header_row = rows[0].map.with_index { |_, i| "Column #{i + 1}" }
|
311
|
+
rows.unshift(header_row)
|
312
|
+
end
|
313
|
+
|
314
|
+
# Build markdown table
|
315
|
+
result = []
|
316
|
+
rows.each_with_index do |row, index|
|
317
|
+
result << "| #{row.join(' | ')} |"
|
318
|
+
if index == 0
|
319
|
+
# Add separator row after header
|
320
|
+
result << "| #{row.map { '---' }.join(' | ')} |"
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
result.join("\n")
|
325
|
+
end
|
326
|
+
|
223
327
|
def apply_selection(data, selected_columns, selected_records, max_records, max_columns)
|
224
328
|
records = data[:records]
|
225
329
|
columns = data[:columns]
|
@@ -400,6 +504,50 @@ module Poml
|
|
400
504
|
def escape_xml(text)
|
401
505
|
text.to_s.gsub('&', '&').gsub('<', '<').gsub('>', '>')
|
402
506
|
end
|
507
|
+
|
508
|
+
def render_table_html(data)
|
509
|
+
records = data[:records]
|
510
|
+
columns = data[:columns]
|
511
|
+
|
512
|
+
return '' if records.empty?
|
513
|
+
|
514
|
+
# If no columns specified, infer from first record
|
515
|
+
if columns.empty? && records.first.is_a?(Hash)
|
516
|
+
columns = records.first.keys.map { |key| { field: key, header: key } }
|
517
|
+
end
|
518
|
+
|
519
|
+
return '' if columns.empty?
|
520
|
+
|
521
|
+
# Build HTML table structure
|
522
|
+
result = []
|
523
|
+
result << '<table>'
|
524
|
+
result << ' <thead>'
|
525
|
+
result << ' <tr>'
|
526
|
+
columns.each do |col|
|
527
|
+
result << " <th>#{escape_html(col[:header] || col[:field])}</th>"
|
528
|
+
end
|
529
|
+
result << ' </tr>'
|
530
|
+
result << ' </thead>'
|
531
|
+
result << ' <tbody>'
|
532
|
+
|
533
|
+
records.each do |record|
|
534
|
+
result << ' <tr>'
|
535
|
+
columns.each do |col|
|
536
|
+
value = record[col[:field]]
|
537
|
+
result << " <td>#{escape_html(value.nil? ? '' : value.to_s)}</td>"
|
538
|
+
end
|
539
|
+
result << ' </tr>'
|
540
|
+
end
|
541
|
+
|
542
|
+
result << ' </tbody>'
|
543
|
+
result << '</table>'
|
544
|
+
|
545
|
+
result.join("\n")
|
546
|
+
end
|
547
|
+
|
548
|
+
def escape_html(text)
|
549
|
+
text.to_s.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
550
|
+
end
|
403
551
|
end
|
404
552
|
|
405
553
|
# Object component for displaying structured data
|
@@ -410,15 +558,27 @@ module Poml
|
|
410
558
|
def render
|
411
559
|
apply_stylesheet
|
412
560
|
|
413
|
-
|
561
|
+
data_attr = get_attribute('data')
|
414
562
|
syntax = get_attribute('syntax', 'json')
|
415
563
|
|
416
|
-
return '' unless
|
564
|
+
return '' unless data_attr
|
565
|
+
|
566
|
+
# Parse data if it's a JSON string
|
567
|
+
data = if data_attr.is_a?(String) && data_attr.start_with?('{', '[')
|
568
|
+
begin
|
569
|
+
JSON.parse(data_attr)
|
570
|
+
rescue JSON::ParserError
|
571
|
+
data_attr # Use as-is if parsing fails
|
572
|
+
end
|
573
|
+
else
|
574
|
+
data_attr
|
575
|
+
end
|
417
576
|
|
418
577
|
if xml_mode?
|
419
578
|
render_as_xml('obj', serialize_data(data, syntax))
|
420
579
|
else
|
421
|
-
serialize_data(data, syntax)
|
580
|
+
result = serialize_data(data, syntax)
|
581
|
+
inline? ? result.strip : result
|
422
582
|
end
|
423
583
|
end
|
424
584
|
|
@@ -543,7 +703,7 @@ module Poml
|
|
543
703
|
return "[Webpage: File not found: #{file_path}]"
|
544
704
|
end
|
545
705
|
|
546
|
-
html_content =
|
706
|
+
html_content = read_file_with_encoding(full_path)
|
547
707
|
process_html_content(html_content, selector, extract_text)
|
548
708
|
rescue => e
|
549
709
|
"[Webpage: Error reading file #{file_path}: #{e.message}]"
|
@@ -631,13 +791,373 @@ module Poml
|
|
631
791
|
apply_stylesheet
|
632
792
|
|
633
793
|
src = get_attribute('src')
|
794
|
+
base64 = get_attribute('base64')
|
634
795
|
alt = get_attribute('alt', '')
|
635
|
-
syntax = get_attribute('syntax', '
|
796
|
+
syntax = get_attribute('syntax', 'multimedia')
|
797
|
+
max_width = get_attribute('maxWidth')
|
798
|
+
max_height = get_attribute('maxHeight')
|
799
|
+
resize = get_attribute('resize')
|
800
|
+
image_type = get_attribute('type')
|
801
|
+
position = get_attribute('position', 'here')
|
802
|
+
|
803
|
+
# Handle missing src and base64
|
804
|
+
unless src || base64
|
805
|
+
return handle_error("no src or base64 specified")
|
806
|
+
end
|
807
|
+
|
808
|
+
begin
|
809
|
+
# Process the image
|
810
|
+
if src
|
811
|
+
if url?(src)
|
812
|
+
content = fetch_image_from_url(src, max_width, max_height, resize, image_type)
|
813
|
+
else
|
814
|
+
content = read_local_image(src, max_width, max_height, resize, image_type)
|
815
|
+
end
|
816
|
+
elsif base64
|
817
|
+
content = process_base64_image(base64, max_width, max_height, resize, image_type)
|
818
|
+
end
|
819
|
+
|
820
|
+
# Render based on syntax and XML mode
|
821
|
+
if xml_mode?
|
822
|
+
attributes = {}
|
823
|
+
attributes[:src] = src if src
|
824
|
+
attributes[:base64] = base64 if base64
|
825
|
+
attributes[:alt] = alt unless alt.empty?
|
826
|
+
attributes[:type] = image_type if image_type
|
827
|
+
attributes[:position] = position
|
828
|
+
render_as_xml('img', content || '', attributes)
|
829
|
+
else
|
830
|
+
if syntax == 'multimedia'
|
831
|
+
# Show image reference with content info
|
832
|
+
image_ref = src || '[embedded image]'
|
833
|
+
result = "[Image: #{image_ref}]"
|
834
|
+
result += " (#{alt})" unless alt.empty?
|
835
|
+
result += "\n#{content}" if content && content.is_a?(String) && content.start_with?('data:')
|
836
|
+
result
|
837
|
+
else
|
838
|
+
# Text mode - show alt text or simple reference
|
839
|
+
alt.empty? ? "[Image: #{src || 'embedded'}]" : alt
|
840
|
+
end
|
841
|
+
end
|
842
|
+
rescue => e
|
843
|
+
handle_error("error processing image: #{e.message}")
|
844
|
+
end
|
845
|
+
end
|
846
|
+
|
847
|
+
private
|
848
|
+
|
849
|
+
def url?(src)
|
850
|
+
src && src.match?(/^https?:\/\//i)
|
851
|
+
end
|
852
|
+
|
853
|
+
def fetch_image_from_url(url, max_width = nil, max_height = nil, resize = nil, image_type = nil)
|
854
|
+
require 'net/http'
|
855
|
+
require 'uri'
|
856
|
+
require 'base64'
|
857
|
+
|
858
|
+
uri = URI.parse(url)
|
859
|
+
|
860
|
+
# Configure HTTP client
|
861
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
862
|
+
http.use_ssl = true if uri.scheme == 'https'
|
863
|
+
http.read_timeout = 10
|
864
|
+
http.open_timeout = 5
|
865
|
+
|
866
|
+
# Create request with appropriate headers
|
867
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
868
|
+
request['User-Agent'] = 'POML Ruby/1.0'
|
869
|
+
request['Accept'] = 'image/*'
|
870
|
+
|
871
|
+
# Fetch the image
|
872
|
+
response = http.request(request)
|
873
|
+
|
874
|
+
unless response.code == '200'
|
875
|
+
raise "HTTP #{response.code}: #{response.message}"
|
876
|
+
end
|
877
|
+
|
878
|
+
# Process the image data
|
879
|
+
image_data = response.body
|
880
|
+
content_type = response['content-type'] || detect_image_type(image_data)
|
881
|
+
|
882
|
+
# Apply processing if requested
|
883
|
+
if max_width || max_height || resize || image_type
|
884
|
+
image_data = process_image_data(image_data, content_type, max_width, max_height, resize, image_type)
|
885
|
+
end
|
886
|
+
|
887
|
+
# Return base64 encoded data URL
|
888
|
+
base64_data = Base64.strict_encode64(image_data)
|
889
|
+
mime_type = image_type ? "image/#{image_type}" : content_type
|
890
|
+
"data:#{mime_type};base64,#{base64_data}"
|
891
|
+
rescue => e
|
892
|
+
raise "Failed to fetch image from URL #{url}: #{e.message}"
|
893
|
+
end
|
894
|
+
|
895
|
+
def read_local_image(file_path, max_width = nil, max_height = nil, resize = nil, image_type = nil)
|
896
|
+
require 'base64'
|
897
|
+
|
898
|
+
# Resolve file path
|
899
|
+
full_path = if file_path.start_with?('/')
|
900
|
+
file_path
|
901
|
+
else
|
902
|
+
base_path = @context.source_path ? File.dirname(@context.source_path) : Dir.pwd
|
903
|
+
File.join(base_path, file_path)
|
904
|
+
end
|
905
|
+
|
906
|
+
unless File.exist?(full_path)
|
907
|
+
raise "File not found: #{file_path}"
|
908
|
+
end
|
909
|
+
|
910
|
+
# Read image data
|
911
|
+
image_data = File.read(full_path, mode: 'rb')
|
912
|
+
content_type = detect_image_type_from_extension(full_path) || detect_image_type(image_data)
|
913
|
+
|
914
|
+
# Apply processing if requested
|
915
|
+
if max_width || max_height || resize || image_type
|
916
|
+
image_data = process_image_data(image_data, content_type, max_width, max_height, resize, image_type)
|
917
|
+
end
|
918
|
+
|
919
|
+
# Return base64 encoded data URL
|
920
|
+
base64_data = Base64.strict_encode64(image_data)
|
921
|
+
mime_type = image_type ? "image/#{image_type}" : content_type
|
922
|
+
"data:#{mime_type};base64,#{base64_data}"
|
923
|
+
rescue => e
|
924
|
+
raise "Failed to read local image #{file_path}: #{e.message}"
|
925
|
+
end
|
926
|
+
|
927
|
+
def process_base64_image(base64_data, max_width = nil, max_height = nil, resize = nil, image_type = nil)
|
928
|
+
require 'base64'
|
929
|
+
|
930
|
+
# Decode base64 data
|
931
|
+
image_data = Base64.decode64(base64_data)
|
932
|
+
content_type = detect_image_type(image_data)
|
933
|
+
|
934
|
+
# Apply processing if requested
|
935
|
+
if max_width || max_height || resize || image_type
|
936
|
+
image_data = process_image_data(image_data, content_type, max_width, max_height, resize, image_type)
|
937
|
+
end
|
938
|
+
|
939
|
+
# Return base64 encoded data URL
|
940
|
+
processed_base64 = Base64.strict_encode64(image_data)
|
941
|
+
mime_type = image_type ? "image/#{image_type}" : content_type
|
942
|
+
"data:#{mime_type};base64,#{processed_base64}"
|
943
|
+
rescue => e
|
944
|
+
raise "Failed to process base64 image: #{e.message}"
|
945
|
+
end
|
946
|
+
|
947
|
+
def detect_image_type_from_extension(file_path)
|
948
|
+
ext = File.extname(file_path).downcase
|
949
|
+
case ext
|
950
|
+
when '.jpg', '.jpeg' then 'image/jpeg'
|
951
|
+
when '.png' then 'image/png'
|
952
|
+
when '.gif' then 'image/gif'
|
953
|
+
when '.webp' then 'image/webp'
|
954
|
+
when '.svg' then 'image/svg+xml'
|
955
|
+
when '.bmp' then 'image/bmp'
|
956
|
+
when '.tiff', '.tif' then 'image/tiff'
|
957
|
+
else nil
|
958
|
+
end
|
959
|
+
end
|
960
|
+
|
961
|
+
def detect_image_type(image_data)
|
962
|
+
# Check magic bytes to detect image type
|
963
|
+
return 'image/jpeg' if image_data[0..1] == "\xFF\xD8".b
|
964
|
+
return 'image/png' if image_data[0..7] == "\x89PNG\r\n\x1A\n".b
|
965
|
+
return 'image/gif' if image_data[0..5] == "GIF87a".b || image_data[0..5] == "GIF89a".b
|
966
|
+
return 'image/webp' if image_data[0..3] == "RIFF".b && image_data[8..11] == "WEBP".b
|
967
|
+
return 'image/bmp' if image_data[0..1] == "BM".b
|
968
|
+
return 'image/svg+xml' if image_data.include?('<svg')
|
969
|
+
|
970
|
+
# Default fallback
|
971
|
+
'image/octet-stream'
|
972
|
+
end
|
973
|
+
|
974
|
+
def process_image_data(image_data, content_type, max_width, max_height, resize, new_type)
|
975
|
+
begin
|
976
|
+
require 'vips'
|
977
|
+
rescue LoadError
|
978
|
+
# Fallback to basic implementation if vips is not available
|
979
|
+
warn "Warning: ruby-vips gem not found. Image processing features are limited. Install ruby-vips gem for full image processing support."
|
980
|
+
return image_data
|
981
|
+
end
|
982
|
+
|
983
|
+
begin
|
984
|
+
# Load image from memory
|
985
|
+
image = Vips::Image.new_from_buffer(image_data, "")
|
986
|
+
|
987
|
+
# Apply resizing if requested
|
988
|
+
if max_width || max_height || resize
|
989
|
+
image = resize_image(image, max_width, max_height, resize)
|
990
|
+
end
|
636
991
|
|
637
|
-
|
638
|
-
|
992
|
+
# Convert format if requested
|
993
|
+
if new_type && !content_type.include?(new_type)
|
994
|
+
# Map new_type to vips format
|
995
|
+
format_suffix = case new_type.downcase
|
996
|
+
when 'jpeg', 'jpg' then '.jpg'
|
997
|
+
when 'png' then '.png'
|
998
|
+
when 'webp' then '.webp'
|
999
|
+
when 'tiff', 'tif' then '.tiff'
|
1000
|
+
when 'gif' then '.gif'
|
1001
|
+
else '.jpg' # default fallback
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
# Convert image to new format
|
1005
|
+
image = convert_image_format(image, format_suffix)
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
# Write image back to buffer
|
1009
|
+
image.write_to_buffer(get_vips_format_string(new_type || extract_format_from_content_type(content_type)))
|
1010
|
+
rescue => e
|
1011
|
+
warn "Warning: Image processing failed (#{e.message}). Returning original image data."
|
1012
|
+
image_data
|
1013
|
+
end
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
private
|
1017
|
+
|
1018
|
+
def resize_image(image, max_width, max_height, resize_mode)
|
1019
|
+
current_width = image.width
|
1020
|
+
current_height = image.height
|
1021
|
+
|
1022
|
+
# Convert parameters to integers if they are strings
|
1023
|
+
max_width = max_width.to_i if max_width && max_width != 0
|
1024
|
+
max_height = max_height.to_i if max_height && max_height != 0
|
1025
|
+
|
1026
|
+
# Determine target dimensions
|
1027
|
+
if resize_mode == 'fit'
|
1028
|
+
# Fit within bounds while preserving aspect ratio
|
1029
|
+
if max_width && max_height
|
1030
|
+
scale_x = max_width.to_f / current_width
|
1031
|
+
scale_y = max_height.to_f / current_height
|
1032
|
+
scale = [scale_x, scale_y].min
|
1033
|
+
|
1034
|
+
if scale < 1.0 # Only resize if image is larger than target
|
1035
|
+
image = image.resize(scale)
|
1036
|
+
end
|
1037
|
+
elsif max_width
|
1038
|
+
scale = max_width.to_f / current_width
|
1039
|
+
if scale < 1.0
|
1040
|
+
image = image.resize(scale)
|
1041
|
+
end
|
1042
|
+
elsif max_height
|
1043
|
+
scale = max_height.to_f / current_height
|
1044
|
+
if scale < 1.0
|
1045
|
+
image = image.resize(scale)
|
1046
|
+
end
|
1047
|
+
end
|
1048
|
+
elsif resize_mode == 'fill'
|
1049
|
+
# Fill the entire area, potentially cropping
|
1050
|
+
if max_width && max_height
|
1051
|
+
scale_x = max_width.to_f / current_width
|
1052
|
+
scale_y = max_height.to_f / current_height
|
1053
|
+
scale = [scale_x, scale_y].max
|
1054
|
+
|
1055
|
+
# Resize to fill
|
1056
|
+
image = image.resize(scale)
|
1057
|
+
|
1058
|
+
# Crop to exact dimensions if needed
|
1059
|
+
if image.width > max_width || image.height > max_height
|
1060
|
+
left = [(image.width - max_width) / 2, 0].max
|
1061
|
+
top = [(image.height - max_height) / 2, 0].max
|
1062
|
+
image = image.crop(left, top, max_width, max_height)
|
1063
|
+
end
|
1064
|
+
end
|
1065
|
+
elsif resize_mode == 'stretch'
|
1066
|
+
# Stretch to exact dimensions (may distort aspect ratio)
|
1067
|
+
if max_width && max_height
|
1068
|
+
scale_x = max_width.to_f / current_width
|
1069
|
+
scale_y = max_height.to_f / current_height
|
1070
|
+
image = image.resize(scale_x, vscale: scale_y)
|
1071
|
+
end
|
1072
|
+
else
|
1073
|
+
# Default behavior: fit within bounds
|
1074
|
+
if max_width && current_width > max_width
|
1075
|
+
scale = max_width.to_f / current_width
|
1076
|
+
image = image.resize(scale)
|
1077
|
+
end
|
1078
|
+
if max_height && image.height > max_height
|
1079
|
+
scale = max_height.to_f / image.height
|
1080
|
+
image = image.resize(scale)
|
1081
|
+
end
|
1082
|
+
end
|
1083
|
+
|
1084
|
+
image
|
1085
|
+
end
|
1086
|
+
|
1087
|
+
def convert_image_format(image, format_suffix)
|
1088
|
+
case format_suffix.downcase
|
1089
|
+
when '.jpg', '.jpeg'
|
1090
|
+
# For JPEG, ensure RGB colorspace and add quality setting
|
1091
|
+
image = image.colourspace(:srgb) if image.bands >= 3
|
1092
|
+
image
|
1093
|
+
when '.png'
|
1094
|
+
# PNG supports transparency, no special handling needed
|
1095
|
+
image
|
1096
|
+
when '.webp'
|
1097
|
+
# WebP format, good compression
|
1098
|
+
image
|
1099
|
+
when '.tiff'
|
1100
|
+
# TIFF format
|
1101
|
+
image
|
1102
|
+
when '.gif'
|
1103
|
+
# For GIF, we may need to handle transparency and color reduction
|
1104
|
+
# Note: libvips has limited GIF write support, might need special handling
|
1105
|
+
image
|
1106
|
+
else
|
1107
|
+
image
|
1108
|
+
end
|
1109
|
+
end
|
1110
|
+
|
1111
|
+
def get_vips_format_string(image_type)
|
1112
|
+
case image_type&.downcase
|
1113
|
+
when 'jpeg', 'jpg'
|
1114
|
+
'.jpg[Q=85]' # JPEG with 85% quality
|
1115
|
+
when 'png'
|
1116
|
+
'.png'
|
1117
|
+
when 'webp'
|
1118
|
+
'.webp'
|
1119
|
+
when 'tiff', 'tif'
|
1120
|
+
'.tiff'
|
1121
|
+
when 'gif'
|
1122
|
+
'.gif'
|
1123
|
+
else
|
1124
|
+
'.jpg[Q=85]' # Default to JPEG
|
1125
|
+
end
|
1126
|
+
end
|
1127
|
+
|
1128
|
+
def extract_format_from_content_type(content_type)
|
1129
|
+
return nil unless content_type&.start_with?('image/')
|
1130
|
+
|
1131
|
+
format = content_type.split('/').last
|
1132
|
+
case format
|
1133
|
+
when 'jpeg' then 'jpg'
|
1134
|
+
else format
|
1135
|
+
end
|
1136
|
+
end
|
1137
|
+
|
1138
|
+
public
|
1139
|
+
|
1140
|
+
def process_image_data_old(image_data, content_type, max_width, max_height, resize, new_type)
|
1141
|
+
# Note: This is a basic implementation that doesn't actually resize images
|
1142
|
+
# For full image processing, consider using mini_magick or image_processing gems
|
1143
|
+
|
1144
|
+
# If type conversion is requested and it's different from current type
|
1145
|
+
if new_type && !content_type.include?(new_type)
|
1146
|
+
# For now, we'll just change the MIME type
|
1147
|
+
# Real implementation would use an image processing library
|
1148
|
+
# Note: Basic image format conversion available. Install mini_magick gem for enhanced image processing support.
|
1149
|
+
end
|
1150
|
+
|
1151
|
+
# Legacy implementation - kept for compatibility
|
1152
|
+
# Use process_image_data method above for full image processing with libvips
|
1153
|
+
image_data
|
1154
|
+
end
|
1155
|
+
|
1156
|
+
def handle_error(message)
|
1157
|
+
if xml_mode?
|
1158
|
+
render_as_xml('img', "[Error: #{message}]")
|
639
1159
|
else
|
640
|
-
|
1160
|
+
"[Image Error: #{message}]"
|
641
1161
|
end
|
642
1162
|
end
|
643
1163
|
end
|