poml 0.0.6 → 0.0.8

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/docs/tutorial/advanced/performance.md +695 -0
  3. data/docs/tutorial/advanced/tool-registration.md +776 -0
  4. data/docs/tutorial/basic-usage.md +351 -0
  5. data/docs/tutorial/components/chat-components.md +552 -0
  6. data/docs/tutorial/components/formatting.md +623 -0
  7. data/docs/tutorial/components/index.md +366 -0
  8. data/docs/tutorial/components/media-components.md +259 -0
  9. data/docs/tutorial/components/schema-components.md +668 -0
  10. data/docs/tutorial/index.md +185 -0
  11. data/docs/tutorial/output-formats.md +689 -0
  12. data/docs/tutorial/quickstart.md +30 -0
  13. data/docs/tutorial/template-engine.md +540 -0
  14. data/lib/poml/components/base.rb +146 -4
  15. data/lib/poml/components/content.rb +10 -3
  16. data/lib/poml/components/data.rb +539 -19
  17. data/lib/poml/components/examples.rb +235 -1
  18. data/lib/poml/components/formatting.rb +184 -18
  19. data/lib/poml/components/layout.rb +7 -2
  20. data/lib/poml/components/lists.rb +69 -35
  21. data/lib/poml/components/meta.rb +134 -5
  22. data/lib/poml/components/output_schema.rb +19 -1
  23. data/lib/poml/components/template.rb +72 -61
  24. data/lib/poml/components/text.rb +30 -1
  25. data/lib/poml/components/tool.rb +81 -0
  26. data/lib/poml/components/tool_definition.rb +339 -10
  27. data/lib/poml/components/tools.rb +14 -0
  28. data/lib/poml/components/utilities.rb +34 -18
  29. data/lib/poml/components.rb +19 -0
  30. data/lib/poml/context.rb +19 -4
  31. data/lib/poml/parser.rb +88 -63
  32. data/lib/poml/renderer.rb +191 -9
  33. data/lib/poml/template_engine.rb +138 -13
  34. data/lib/poml/version.rb +1 -1
  35. data/lib/poml.rb +16 -1
  36. data/readme.md +157 -30
  37. metadata +87 -4
  38. data/TUTORIAL.md +0 -987
@@ -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 @element.children.any? { |child| child.tag_name == :tr }
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
- parse_html_table_children
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 = File.read(file_path)
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
- File.readlines(file_path).each do |line|
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.children.each do |child|
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
- child.children.each_with_index do |cell, index|
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('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;')
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('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
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
- data = get_attribute('data')
561
+ data_attr = get_attribute('data')
414
562
  syntax = get_attribute('syntax', 'json')
415
563
 
416
- return '' unless data
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 = File.read(full_path)
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', 'text')
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
- if syntax == 'multimedia'
638
- "[Image: #{src}]#{alt.empty? ? '' : " (#{alt})"}"
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
- alt.empty? ? "[Image: #{src}]" : alt
1160
+ "[Image Error: #{message}]"
641
1161
  end
642
1162
  end
643
1163
  end