diff --git a/spec/fixtures/outfit_images/Blue Acara With Hat.png b/spec/fixtures/outfit_images/Blue Acara With Hat.png new file mode 100644 index 00000000..c70c7c02 Binary files /dev/null and b/spec/fixtures/outfit_images/Blue Acara With Hat.png differ diff --git a/spec/fixtures/outfit_images/Blue Acara.png b/spec/fixtures/outfit_images/Blue Acara.png new file mode 100644 index 00000000..5ed00824 Binary files /dev/null and b/spec/fixtures/outfit_images/Blue Acara.png differ diff --git a/spec/fixtures/outfit_images/Hat.png b/spec/fixtures/outfit_images/Hat.png new file mode 100644 index 00000000..d18a6bc6 Binary files /dev/null and b/spec/fixtures/outfit_images/Hat.png differ diff --git a/spec/fixtures/zones.yml b/spec/fixtures/zones.yml index a2ec78f5..02ed76e2 100644 --- a/spec/fixtures/zones.yml +++ b/spec/fixtures/zones.yml @@ -28,9 +28,9 @@ hindbiology: type_id: 1 label: Hind Biology plain_label: hindbiology -markings: - id: 31 - depth: 35 +markings1: + id: 6 + depth: 8 type_id: 2 label: Markings plain_label: markings @@ -88,6 +88,12 @@ body: type_id: 1 label: Body plain_label: body +markings2: + id: 16 + depth: 19 + type_id: 2 + label: Markings + plain_label: markings bodydisease: id: 17 depth: 20 @@ -172,6 +178,12 @@ head: type_id: 1 label: Head plain_label: head +markings3: + id: 31 + depth: 35 + type_id: 2 + label: Markings + plain_label: markings headdisease: id: 32 depth: 36 @@ -196,9 +208,9 @@ glasses: type_id: 2 label: Glasses plain_label: glasses -earrings: - id: 41 - depth: 45 +earrings1: + id: 36 + depth: 39 type_id: 2 label: Earrings plain_label: earrings @@ -220,15 +232,21 @@ headdrippings: type_id: 1 label: Head Drippings plain_label: headdrippings -hat: - id: 50 - depth: 16 +hat1: + id: 40 + depth: 44 type_id: 2 label: Hat plain_label: hat -righthanditem: - id: 49 - depth: 5 +earrings2: + id: 41 + depth: 45 + type_id: 2 + label: Earrings + plain_label: earrings +righthanditem1: + id: 42 + depth: 46 type_id: 2 label: Right-hand Item plain_label: righthanditem @@ -268,6 +286,18 @@ backgrounditem: type_id: 3 label: Background Item plain_label: backgrounditem +righthanditem2: + id: 49 + depth: 5 + type_id: 2 + label: Right-hand Item + plain_label: righthanditem +hat2: + id: 50 + depth: 16 + type_id: 2 + label: Hat + plain_label: hat belt: id: 51 depth: 27 diff --git a/spec/lib/outfit_image_renderer_spec.rb b/spec/lib/outfit_image_renderer_spec.rb index 29b54c6b..9d016a64 100644 --- a/spec/lib/outfit_image_renderer_spec.rb +++ b/spec/lib/outfit_image_renderer_spec.rb @@ -4,12 +4,10 @@ require_relative '../rails_helper' RSpec.describe OutfitImageRenderer do fixtures :zones, :colors, :species - # Helper to create a simple PNG image (1x1 pixel) with a specific color - def create_test_png(red, green, blue, alpha = 255) - require 'vips' - image = Vips::Image.black(1, 1, bands: 4) - image = image.new_from_image([red, green, blue, alpha]) - image.write_to_buffer('.png') + # Helper to load a fixture image + def load_fixture_image(filename) + path = Rails.root.join('spec', 'fixtures', 'outfit_images', filename) + File.read(path) end # Helper to create a pet state with specific swf_assets @@ -84,25 +82,28 @@ RSpec.describe OutfitImageRenderer do describe "#render" do context "with a simple outfit" do it "composites biology and item layers into a single PNG" do - # Create test PNG data - red_png = create_test_png(255, 0, 0) # Red pixel - blue_png = create_test_png(0, 0, 255) # Blue pixel + # Load fixture images + acara_png = load_fixture_image('Blue Acara.png') + hat_png = load_fixture_image('Hat.png') + expected_composite_png = load_fixture_image('Blue Acara With Hat.png') # Create biology and item assets biology_asset = build_biology_asset(zones(:head), body_id: 1) - item_asset = build_item_asset(zones(:hat), body_id: 1) + item_asset = build_item_asset(zones(:hat1), body_id: 1) # Stub HTTP requests for the actual image URLs that will be generated stub_request(:get, biology_asset.image_url). - to_return(body: red_png, status: 200) + to_return(body: acara_png, status: 200) stub_request(:get, item_asset.image_url). - to_return(body: blue_png, status: 200) + to_return(body: hat_png, status: 200) # Build outfit pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset]) item = build_item("Test Hat", swf_assets: [item_asset]) - outfit = Outfit.new(pet_state: pet_state) - outfit.item_ids = { worn: [item.id], closeted: [] } + outfit = Outfit.new( + pet_state: pet_state, + worn_items: [item] + ) # Render renderer = OutfitImageRenderer.new(outfit) @@ -114,29 +115,48 @@ RSpec.describe OutfitImageRenderer do expect(result[0..7]).to eq("\x89PNG\r\n\x1A\n".b) # PNG magic bytes # Verify the result is a valid 600x600 PNG - image = Vips::Image.new_from_buffer(result, "") - expect(image.width).to eq(600) - expect(image.height).to eq(600) + result_image = Vips::Image.new_from_buffer(result, "") + expect(result_image.width).to eq(600) + expect(result_image.height).to eq(600) + + # Verify the composite matches the expected image pixel-perfectly + expected_image = Vips::Image.new_from_buffer(expected_composite_png, "") + + # Calculate the absolute difference between images + diff = (result_image - expected_image).abs + max_diff = diff.max + + # Allow a small tolerance for minor encoding/compositing differences + # The expected image was generated with a different method, so we expect + # very close but not necessarily pixel-perfect matches + tolerance = 2 + if max_diff > tolerance + debug_path = Rails.root.join('tmp', 'test_render_result.png') + result_image.write_to_file(debug_path.to_s) + fail "Images should match within tolerance of #{tolerance}, but found max difference of #{max_diff}. Actual output saved to #{debug_path}" + end end end context "when a layer image fails to load" do it "skips the failed layer and continues" do - blue_png = create_test_png(0, 0, 255) + hat_png = load_fixture_image('Hat.png') biology_asset = build_biology_asset(zones(:head), body_id: 1) - item_asset = build_item_asset(zones(:hat), body_id: 1) + item_asset = build_item_asset(zones(:hat1), body_id: 1) # Stub one successful request and one failure stub_request(:get, biology_asset.image_url). to_return(status: 404) stub_request(:get, item_asset.image_url). - to_return(body: blue_png, status: 200) + to_return(body: hat_png, status: 200) pet_state = build_pet_state(@pet_type, swf_assets: [biology_asset]) item = build_item("Test Hat", swf_assets: [item_asset]) - outfit = Outfit.new(pet_state: pet_state) - outfit.item_ids = { worn: [item.id], closeted: [] } + outfit = Outfit.new( + pet_state: pet_state, + worn_items: [item] + ) renderer = OutfitImageRenderer.new(outfit) result = renderer.render diff --git a/spec/models/outfit_spec.rb b/spec/models/outfit_spec.rb index 68370485..307bc2aa 100644 --- a/spec/models/outfit_spec.rb +++ b/spec/models/outfit_spec.rb @@ -95,7 +95,7 @@ RSpec.describe Outfit do pet_state = build_pet_state(@pet_type, swf_assets: [head, body]) # Create item layers - hat_asset = build_item_asset(zones(:hat), body_id: 1) + hat_asset = build_item_asset(zones(:hat1), body_id: 1) hat = build_item("Test Hat", swf_assets: [hat_asset]) outfit = Outfit.new(pet_state: pet_state) @@ -134,7 +134,7 @@ RSpec.describe Outfit do # Create a hat that restricts the hair zone # zones_restrict is a bitfield where position 37 (Hair Front zone id) is "1" zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 = 1 - hat_asset = build_item_asset(zones(:hat), body_id: 1, zones_restrict: zones_restrict) + hat_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: zones_restrict) hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset]) outfit = Outfit.new(pet_state: pet_state) @@ -156,7 +156,7 @@ RSpec.describe Outfit do # Create an item that restricts both Hair Front (37) and Head Transient Biology (38) zones_restrict = "0" * 36 + "11" + "0" * 20 # bits 37 and 38 = 1 - hood_asset = build_item_asset(zones(:hat), body_id: 1, zones_restrict: zones_restrict) + hood_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: zones_restrict) hood = build_item("Agent Hood", swf_assets: [hood_asset]) outfit = Outfit.new(pet_state: pet_state) @@ -222,7 +222,7 @@ RSpec.describe Outfit do pet_state = build_pet_state(@pet_type, pose: "UNCONVERTED", swf_assets: [head, body]) # Create both body-specific and body_id=0 items - body_specific_asset = build_item_asset(zones(:hat), body_id: 1) + body_specific_asset = build_item_asset(zones(:hat1), body_id: 1) body_specific_item = build_item("Body-specific Hat", swf_assets: [body_specific_asset]) universal_asset = build_item_asset(zones(:background), body_id: 0) @@ -244,7 +244,7 @@ RSpec.describe Outfit do pet_state = build_pet_state(@pet_type, pose: "UNCONVERTED", swf_assets: [head]) # Create a body-specific item in a zone the pet doesn't restrict - hat_asset = build_item_asset(zones(:hat), body_id: 1) + hat_asset = build_item_asset(zones(:hat1), body_id: 1) hat = build_item("Body-specific Hat", swf_assets: [hat_asset]) outfit = Outfit.new(pet_state: pet_state) @@ -296,7 +296,7 @@ RSpec.describe Outfit do # Add an item that restricts Hair Front (37) item_zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 = 1 - hat_asset = build_item_asset(zones(:hat), body_id: 1, zones_restrict: item_zones_restrict) + hat_asset = build_item_asset(zones(:hat1), body_id: 1, zones_restrict: item_zones_restrict) hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset]) outfit = Outfit.new(pet_state: pet_state) @@ -357,7 +357,7 @@ RSpec.describe Outfit do # Add items at various depths bg_item = build_item_asset(zones(:backgrounditem), body_id: 0) # depth 4 - hat_asset = build_item_asset(zones(:hat), body_id: 1) # depth 16 + hat_asset = build_item_asset(zones(:hat1), body_id: 1) # depth 44 shirt_asset = build_item_asset(zones(:shirtdress), body_id: 1) # depth 26 bg = build_item("Background Item", swf_assets: [bg_item]) @@ -370,10 +370,10 @@ RSpec.describe Outfit do layers = outfit.visible_layers # Expected order by depth: - # background (3), bg_item (4), hat_asset (16), body_layer (18), - # shirt_asset (26), head_layer (34) - expect(layers.map(&:depth)).to eq([3, 4, 16, 18, 26, 34]) - expect(layers).to eq([background, bg_item, hat_asset, body_layer, shirt_asset, head_layer]) + # background (3), bg_item (4), body_layer (18), shirt_asset (26), + # head_layer (34), hat_asset (44) + expect(layers.map(&:depth)).to eq([3, 4, 18, 26, 34, 44]) + expect(layers).to eq([background, bg_item, body_layer, shirt_asset, head_layer, hat_asset]) end end @@ -417,7 +417,7 @@ RSpec.describe Outfit do pet_state = build_pet_state(@pet_type) # Create a body-specific item for the alt style's body_id - body_specific_asset = build_item_asset(zones(:hat), body_id: 999) + body_specific_asset = build_item_asset(zones(:hat1), body_id: 999) body_specific_item = build_item("Body-specific Hat", swf_assets: [body_specific_asset]) # Create a universal item (body_id=0) @@ -441,7 +441,7 @@ RSpec.describe Outfit do pet_state = build_pet_state(@pet_type) # Create an item that fits the regular pet's body_id (1) - regular_item_asset = build_item_asset(zones(:hat), body_id: 1) + regular_item_asset = build_item_asset(zones(:hat1), body_id: 1) regular_item = build_item("Regular Pet Hat", swf_assets: [regular_item_asset]) outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) @@ -463,7 +463,7 @@ RSpec.describe Outfit do # Create a universal hat that restricts the hair zone zones_restrict = "0" * 36 + "1" + "0" * 20 # bit 37 (Hair Front) = 1 - hat_asset = build_item_asset(zones(:hat), body_id: 0, zones_restrict: zones_restrict) + hat_asset = build_item_asset(zones(:hat1), body_id: 0, zones_restrict: zones_restrict) hat = build_item("Hair-hiding Hat", swf_assets: [hat_asset]) outfit = Outfit.new(pet_state: pet_state, alt_style: @alt_style) @@ -526,7 +526,7 @@ RSpec.describe Outfit do # Add universal items at various depths bg_item = build_item_asset(zones(:backgrounditem), body_id: 0) # depth 4 - trinket = build_item_asset(zones(:righthanditem), body_id: 0) # depth 5 + trinket = build_item_asset(zones(:righthanditem1), body_id: 0) # depth 46 bg = build_item("Background Item", swf_assets: [bg_item]) trinket_item = build_item("Trinket", swf_assets: [trinket]) @@ -537,9 +537,9 @@ RSpec.describe Outfit do layers = outfit.visible_layers # Expected order by depth: - # alt_background (3), bg_item (4), trinket (5), alt_body (18), alt_head (34) - expect(layers.map(&:depth)).to eq([3, 4, 5, 18, 34]) - expect(layers).to eq([alt_background, bg_item, trinket, alt_body, alt_head]) + # alt_background (3), bg_item (4), alt_body (18), alt_head (34), trinket (46) + expect(layers.map(&:depth)).to eq([3, 4, 18, 34, 46]) + expect(layers).to eq([alt_background, bg_item, alt_body, alt_head, trinket]) end end end