Rightmove requires Energy Performance Certificate (EPC) rating graphs in specific formats for property listings. These graphs display colour-coded energy efficiency bands (A through G), highlight current and potential ratings, and follow official government styling standards. Creating these graphics manually for hundreds of properties is impractical; agencies need automated generation that produces portal-ready images instantly.
Week 38 implemented automatic EPC image generation that transforms certificate data into professional rating graphics. This article explores how we built this system using SVG templates, ImageMagick conversion, and background job processing—ensuring every property has portal-compliant EPC graphics without manual intervention.
What Your Team Will Notice
When EPC data is imported from the government database or manually uploaded, rating graphics generate automatically within seconds. The property EPC card displays a professional energy efficiency chart showing:
- Colour-coded rating bands (green for A/B/C, yellow for D, orange for E/F, red for G)
- Current rating indicated with a filled marker
- Potential rating shown with an outlined marker
- Numeric scores alongside letter ratings
- Government-compliant styling matching official EPC certificates
These graphics sync automatically to Rightmove and other portals when properties advertise. Updates to EPC data (new certificates, rating improvements) trigger automatic image regeneration, keeping portal listings current without manual graphic creation.
For properties with manually uploaded certificates, the same automatic generation occurs—ensuring consistent visual presentation regardless of whether EPC data came from the government API or manual entry.
Under the Bonnet: SVG Template Generation
Rather than using complex image manipulation libraries or pre-rendered graphics, we generate EPC images dynamically using SVG (Scalable Vector Graphics). SVG provides crisp rendering at any resolution, easy programmatic manipulation, and straightforward conversion to PNG format required by property portals.
Template Structure
The EPC rating graphic consists of stacked horizontal bands representing efficiency ratings:
class EpcImageService
def initialize(epc_certificate)
@epc = epc_certificate
end
def generate_image
svg = build_svg_template
png = convert_to_png(svg)
attach_to_epc(png)
end
private
def build_svg_template
# SVG dimensions optimized for portal display
width = 600
height = 400
band_height = 50
svg = <<~SVG
<svg xmlns="http://www.w3.org/2000/svg" width="#{width}" height="#{height}">
<defs>
<!-- Gradient definitions for rating bands -->
</defs>
<!-- Rating scale bands (A through G) -->
#{build_rating_bands(band_height)}
<!-- Current rating marker -->
#{build_current_marker}
<!-- Potential rating marker -->
#{build_potential_marker}
<!-- Score labels -->
#{build_score_labels}
</svg>
SVG
svg
end
end
Colour-Coded Rating Bands
Each efficiency rating has specific colours matching government standards:
def build_rating_bands(band_height)
bands = [
{ rating: 'A', color: '#008054', range: '92-100' },
{ rating: 'B', color: '#19b459', range: '81-91' },
{ rating: 'C', color: '#8dce46', range: '69-80' },
{ rating: 'D', color: '#ffd500', range: '55-68' },
{ rating: 'E', color: '#fcaa65', range: '39-54' },
{ rating: 'F', color: '#ef8023', range: '21-38' },
{ rating: 'G', color: '#e9153b', range: '1-20' }
]
bands.map.with_index do |band, index|
y_position = index * band_height
<<~BAND
<rect x="0" y="#{y_position}" width="500" height="#{band_height}"
fill="#{band[:color]}" opacity="0.9" />
<text x="520" y="#{y_position + 35}" font-size="28" font-weight="bold"
fill="#333">
#{band[:rating]}
</text>
<text x="560" y="#{y_position + 30}" font-size="14" fill="#666">
#{band[:range]}
</text>
BAND
end.join
end
This produces a vertical stack of coloured bands with ratings and score ranges labelled clearly.
Rating Markers
Current and potential ratings display as markers overlaying the appropriate bands:
def build_current_marker
rating = @epc.current_energy_rating
score = @epc.current_energy_efficiency
y_position = calculate_y_position(rating)
<<~MARKER
<g>
<!-- Filled marker for current rating -->
<polygon points="#{marker_points(y_position)}"
fill="#1e40af" stroke="#1e40af" stroke-width="2" />
<text x="480" y="#{y_position + 8}" font-size="20" font-weight="bold"
fill="white" text-anchor="end">
#{score}
</text>
<text x="20" y="#{y_position - 10}" font-size="16" fill="#1e40af">
Current: #{rating} (#{score})
</text>
</g>
MARKER
end
def build_potential_marker
rating = @epc.potential_energy_rating
score = @epc.potential_energy_efficiency
y_position = calculate_y_position(rating)
<<~MARKER
<g>
<!-- Outlined marker for potential rating -->
<polygon points="#{marker_points(y_position)}"
fill="none" stroke="#059669" stroke-width="3"
stroke-dasharray="5,5" />
<text x="480" y="#{y_position + 8}" font-size="18"
fill="#059669" text-anchor="end">
#{score}
</text>
<text x="20" y="#{y_position + 30}" font-size="16" fill="#059669">
Potential: #{rating} (#{score})
</text>
</g>
MARKER
end
def calculate_y_position(rating)
# Maps rating letter to vertical position on chart
rating_positions = { 'A' => 25, 'B' => 75, 'C' => 125, 'D' => 175,
'E' => 225, 'F' => 275, 'G' => 325 }
rating_positions[rating] || 175
end
The filled marker shows the current rating; the dashed outline shows achievable potential after recommended improvements.
ImageMagick Conversion
SVG provides resolution independence and easy manipulation, but property portals require PNG format. We use ImageMagick for conversion:
def convert_to_png(svg_content)
require 'tempfile'
require 'open3'
# Write SVG to temporary file
svg_file = Tempfile.new(['epc', '.svg'])
svg_file.write(svg_content)
svg_file.close
# Convert to PNG using ImageMagick
png_file = Tempfile.new(['epc', '.png'])
command = [
'convert',
'-density', '300', # High DPI for quality
'-background', 'white', # White background
'-flatten', # Remove transparency
svg_file.path,
'-resize', '600x400!', # Fixed dimensions for portals
'-quality', '95', # High quality compression
png_file.path
]
stdout, stderr, status = Open3.capture3(*command)
unless status.success?
raise "ImageMagick conversion failed: #{stderr}"
end
# Read PNG binary data
png_data = File.binread(png_file.path)
# Clean up temporary files
svg_file.unlink
png_file.unlink
png_data
end
This conversion produces crisp PNG images at 600×400 pixels—optimal for property portal display whilst keeping file sizes manageable.
Background Job Processing
Image generation involves SVG building and ImageMagick conversion—operations taking several seconds. Processing synchronously would delay EPC import workflows. Background jobs handle generation asynchronously:
class EpcImageGenerationJob < ApplicationJob
queue_as :default
def perform(epc_id)
epc = EnergyPerformanceCertificate.find(epc_id)
# Generate PNG from EPC data
service = EpcImageService.new(epc)
image_data = service.generate_image
# Attach to EPC record using ActiveStorage
epc.rating_image.attach(
io: StringIO.new(image_data),
filename: "epc-rating-#{epc.certificate_number}.png",
content_type: 'image/png'
)
end
end
The job queues automatically when EPC records are created or updated:
class EnergyPerformanceCertificate
has_one_attached :rating_image
after_commit :queue_image_generation, on: [:create, :update], if: :needs_image?
private
def needs_image?
# Generate if no image exists or ratings changed
!rating_image.attached? || saved_change_to_current_energy_rating?
end
def queue_image_generation
EpcImageGenerationJob.perform_later(id)
end
end
This ensures images generate for all EPCs without blocking user workflows.
ActiveStorage Integration
Generated images attach using ActiveStorage, providing cloud storage integration and flexible URL generation:
class EnergyPerformanceCertificate
has_one_attached :rating_image
def rating_image_url
return nil unless rating_image.attached?
# Generate URL with proper extension for portal compatibility
rating_image.url
end
end
ActiveStorage handles S3 uploads, URL signing, and CDN integration automatically. The URLs work across development (local storage) and production (S3) without code changes.
Ensuring File Extensions for Portal Compatibility
Property portals often require image URLs with explicit file extensions (.png, .jpg). ActiveStorage blob keys don't include extensions by default. We created a background job ensuring all image blobs have proper extensions:
class EnsureBlobKeyExtensionJob < ApplicationJob
queue_as :low
def perform(blob_id)
blob = ActiveStorage::Blob.find(blob_id)
# Check if key already has extension
return if blob.key.include?('.')
# Determine extension from content type
extension = extension_for_content_type(blob.content_type)
return unless extension
# Generate new key with extension
new_key = "#{blob.key}#{extension}"
# Copy blob to new key in cloud storage
copy_blob_to_new_key(blob, new_key)
# Update blob record
blob.update_column(:key, new_key)
end
private
def extension_for_content_type(content_type)
case content_type
when 'image/png' then '.png'
when 'image/jpeg' then '.jpg'
when 'image/gif' then '.gif'
else nil
end
end
end
This job triggers via ActiveStorage initializer:
# config/initializers/active_storage_extensions.rb
Rails.application.config.after_initialize do
ActiveSupport.on_load(:active_storage_blob) do
after_create_commit do
EnsureBlobKeyExtensionJob.perform_later(id) if image?
end
end
end
Now all images—including EPC rating graphics—have URLs ending in proper extensions, ensuring portal compatibility.
Rightmove Integration
Generated EPC images include automatically in Rightmove property payloads:
class RightmovePropertySerializer
def build_media_section
media = []
# Property photos
media += property_photo_urls
# EPC rating graph
if @property.current_epc&.rating_image&.attached?
media << {
url: @property.current_epc.rating_image_url,
caption: "Energy Performance Certificate",
type: 11 # Rightmove code for EPC graphs
}
end
media
end
end
This ensures properties advertised on Rightmove include EPC graphics automatically, improving listing quality and tenant engagement.
Testing Image Generation
Testing image generation requires both unit tests (SVG building logic) and integration tests (full generation pipeline):
RSpec.describe EpcImageService do
it "generates valid SVG with rating bands" do
epc = create(:epc, current_energy_rating: 'C', current_energy_efficiency: 72)
service = described_class.new(epc)
svg = service.build_svg_template
expect(svg).to include('<svg xmlns="http://www.w3.org/2000/svg"')
expect(svg).to include('fill="#8dce46"') # C rating colour
end
it "positions markers correctly based on ratings" do
epc = create(:epc, current_energy_rating: 'B', potential_energy_rating: 'A')
service = described_class.new(epc)
svg = service.build_svg_template
expect(svg).to include('y="75"') # B rating position
expect(svg).to include('y="25"') # A rating position
end
end
RSpec.describe EpcImageGenerationJob do
it "generates and attaches PNG image" do
epc = create(:epc, current_energy_rating: 'C')
expect {
described_class.perform_now(epc.id)
}.to change { epc.reload.rating_image.attached? }.from(false).to(true)
expect(epc.rating_image.content_type).to eq('image/png')
end
it "includes proper file extension in URL" do
epc = create(:epc_with_image)
url = epc.rating_image_url
expect(url).to end_with('.png')
end
end
These tests verify both SVG generation logic and the complete pipeline from data to portal-ready images.
What's Next
The EPC image generation foundation enables several enhancements: customisable branding (agency logos on EPC graphics), multi-language support (Welsh translations for Welsh properties), and responsive sizing (different dimensions for different portals).
Future improvements might include animated graphics showing rating improvement potential, interactive versions for agency websites where tenants can explore recommendations, and comparison graphics showing how a property's efficiency compares to similar properties in the area.
By automating EPC graphic generation, LetAdmin ensures every property has professional energy efficiency visualizations without manual design work—improving portal listing quality whilst reducing administrative overhead.
