Image Factories
Image factories provide custom image loading from sources beyond the file system and HTTP URLs. Use image factories to load images from:
- Databases (binary blobs)
- Cloud storage (S3, Azure Blob, etc.)
- Content management systems
- In-memory caches
- Custom protocols
Architecture
┌──────────────────────────────────────────────────────┐
│ PDF Template │
│ <img src='db://product/12345' /> │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Image Factory Registration (scrybersettings.json) │
│ Factories: [ DatabaseImageFactory, ... ] │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Factory Matching (Regex) │
│ Factory.ShouldMatch("db://...") → true │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Image Loading │
│ Factory.LoadImageData("db://...", document) │
│ Returns: byte[] of image data │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Image Rendering │
│ Scryber renders loaded image data to PDF │
└──────────────────────────────────────────────────────┘
IPDFImageDataFactory Interface
public interface IPDFImageDataFactory
{
// Regex pattern to match image sources this factory handles
string FactoryKey { get; }
// Test if this factory should handle the given source
bool ShouldMatch(string source);
// Load image data as byte array
byte[] LoadImageData(IDocument document, IComponent component, string source);
}
Creating a Custom Image Factory
1. Implement IPDFImageDataFactory
Example: Database image factory
using System;
using System.Data.SqlClient;
using System.Text.RegularExpressions;
using Scryber;
using Scryber.Components;
using Scryber.Imaging;
namespace MyCompany.Imaging
{
public class DatabaseImageFactory : IPDFImageDataFactory
{
private readonly string _connectionString;
private readonly Regex _pattern;
// Pattern: db://tablename/id or db://tablename/columnname/id
public string FactoryKey => @"^db://[\w]+/[\w]+(/[\w]+)?$";
public DatabaseImageFactory(string connectionString)
{
_connectionString = connectionString;
_pattern = new Regex(FactoryKey, RegexOptions.IgnoreCase);
}
public bool ShouldMatch(string source)
{
if (string.IsNullOrEmpty(source))
return false;
return _pattern.IsMatch(source);
}
public byte[] LoadImageData(IDocument document, IComponent component, string source)
{
// Parse source: db://tablename/id or db://tablename/columnname/id
var uri = new Uri(source);
var parts = uri.AbsolutePath.Trim('/').Split('/');
string tableName;
string columnName = "ImageData"; // Default column
string id;
if (parts.Length == 2)
{
tableName = parts[0];
id = parts[1];
}
else if (parts.Length == 3)
{
tableName = parts[0];
columnName = parts[1];
id = parts[2];
}
else
{
throw new ArgumentException($"Invalid database image path: {source}");
}
// Load from database
return LoadFromDatabase(tableName, columnName, id);
}
private byte[] LoadFromDatabase(string tableName, string columnName, string id)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
var query = $"SELECT [{columnName}] FROM [{tableName}] WHERE Id = @Id";
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Id", id);
var result = command.ExecuteScalar();
if (result == null || result is DBNull)
throw new FileNotFoundException($"Image not found: {tableName}/{id}");
return (byte[])result;
}
}
}
}
}
2. Register Factory
scrybersettings.json:
{
"Scryber": {
"Imaging": {
"Factories": [
{
"Key": "database",
"Type": "MyCompany.Imaging.DatabaseImageFactory, MyCompany.Imaging",
"Config": {
"ConnectionString": "Server=localhost;Database=ProductCatalog;..."
}
}
]
}
}
}
3. Use in Templates
<html xmlns='http://www.w3.org/1999/xhtml'>
<body>
<main>
<!-- Load from database: Products table, row 12345 -->
<img src='db://Products/12345' />
<!-- Specify column: Products table, Thumbnail column, row 67890 -->
<img src='db://Products/Thumbnail/67890' />
</main>
</body>
</html>
Factory Examples
Azure Blob Storage Factory
using Azure.Storage.Blobs;
using System.Text.RegularExpressions;
using Scryber.Imaging;
namespace MyCompany.Imaging
{
public class AzureBlobImageFactory : IPDFImageDataFactory
{
private readonly BlobServiceClient _client;
private readonly Regex _pattern;
// Pattern: azblob://container/blobname
public string FactoryKey => @"^azblob://[\w-]+/[\w/.]+$";
public AzureBlobImageFactory(string connectionString)
{
_client = new BlobServiceClient(connectionString);
_pattern = new Regex(FactoryKey, RegexOptions.IgnoreCase);
}
public bool ShouldMatch(string source)
{
return !string.IsNullOrEmpty(source) && _pattern.IsMatch(source);
}
public byte[] LoadImageData(IDocument document, IComponent component, string source)
{
// Parse: azblob://container/blob/path
var uri = new Uri(source);
var containerName = uri.Host;
var blobName = uri.AbsolutePath.TrimStart('/');
// Get container
var container = _client.GetBlobContainerClient(containerName);
// Download blob
var blobClient = container.GetBlobClient(blobName);
using (var memoryStream = new MemoryStream())
{
blobClient.DownloadTo(memoryStream);
return memoryStream.ToArray();
}
}
}
}
Base64 Embedded Image Factory
using System;
using System.Text.RegularExpressions;
using Scryber.Imaging;
namespace MyCompany.Imaging
{
public class Base64ImageFactory : IPDFImageDataFactory
{
private readonly Regex _pattern;
// Pattern: data:image/png;base64,iVBORw0KGgo...
public string FactoryKey => @"^data:image/[\w]+;base64,";
public Base64ImageFactory()
{
_pattern = new Regex(FactoryKey, RegexOptions.IgnoreCase);
}
public bool ShouldMatch(string source)
{
return !string.IsNullOrEmpty(source) && _pattern.IsMatch(source);
}
public byte[] LoadImageData(IDocument document, IComponent component, string source)
{
// Extract base64 content after "base64,"
var index = source.IndexOf("base64,") + 7;
var base64 = source.Substring(index);
return Convert.FromBase64String(base64);
}
}
}
CMS Image Factory
using System.Net.Http;
using System.Text.RegularExpressions;
using Scryber.Imaging;
namespace MyCompany.Imaging
{
public class CMSImageFactory : IPDFImageDataFactory
{
private readonly HttpClient _httpClient;
private readonly string _cmsApiUrl;
private readonly string _apiKey;
private readonly Regex _pattern;
// Pattern: cms://12345 or cms://images/12345
public string FactoryKey => @"^cms://([\w/]+)$";
public CMSImageFactory(string cmsApiUrl, string apiKey)
{
_cmsApiUrl = cmsApiUrl;
_apiKey = apiKey;
_httpClient = new HttpClient();
_pattern = new Regex(FactoryKey, RegexOptions.IgnoreCase);
}
public bool ShouldMatch(string source)
{
return !string.IsNullOrEmpty(source) && _pattern.IsMatch(source);
}
public byte[] LoadImageData(IDocument document, IComponent component, string source)
{
// Parse: cms://12345 or cms://images/12345
var assetId = source.Substring("cms://".Length);
// Build API request
var requestUrl = $"{_cmsApiUrl}/assets/{assetId}";
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
// Download image
var response = _httpClient.GetAsync(requestUrl).Result;
response.EnsureSuccessStatusCode();
return response.Content.ReadAsByteArrayAsync().Result;
}
}
}
Cached Image Factory
Wrap another factory with caching:
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using Scryber.Imaging;
namespace MyCompany.Imaging
{
public class CachedImageFactory : IPDFImageDataFactory
{
private readonly IPDFImageDataFactory _innerFactory;
private readonly ConcurrentDictionary<string, byte[]> _cache;
private readonly int _maxCacheSize;
public string FactoryKey => _innerFactory.FactoryKey;
public CachedImageFactory(IPDFImageDataFactory innerFactory, int maxCacheSize = 100)
{
_innerFactory = innerFactory;
_cache = new ConcurrentDictionary<string, byte[]>();
_maxCacheSize = maxCacheSize;
}
public bool ShouldMatch(string source)
{
return _innerFactory.ShouldMatch(source);
}
public byte[] LoadImageData(IDocument document, IComponent component, string source)
{
// Check cache
var cacheKey = GetCacheKey(source);
if (_cache.TryGetValue(cacheKey, out byte[] cached))
{
return cached;
}
// Load from inner factory
var data = _innerFactory.LoadImageData(document, component, source);
// Cache result (with size limit)
if (_cache.Count < _maxCacheSize)
{
_cache.TryAdd(cacheKey, data);
}
return data;
}
private string GetCacheKey(string source)
{
using (var sha256 = SHA256.Create())
{
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(source));
return Convert.ToBase64String(hash);
}
}
}
}
Configuration
Factory Registration
scrybersettings.json:
{
"Scryber": {
"Imaging": {
"Factories": [
{
"Key": "database",
"Type": "MyCompany.Imaging.DatabaseImageFactory, MyCompany.Imaging",
"Config": {
"ConnectionString": "Server=localhost;Database=MyDB;..."
}
},
{
"Key": "azblob",
"Type": "MyCompany.Imaging.AzureBlobImageFactory, MyCompany.Imaging",
"Config": {
"ConnectionString": "DefaultEndpointsProtocol=https;..."
}
},
{
"Key": "base64",
"Type": "MyCompany.Imaging.Base64ImageFactory, MyCompany.Imaging"
}
]
}
}
}
Programmatic Registration
using Microsoft.Extensions.DependencyInjection;
using Scryber;
using Scryber.Imaging;
var services = new ServiceCollection();
// Add Scryber with custom image factory
services.AddScryber(config =>
{
config.ImagingOptions.Register.Add(new DatabaseImageFactory(connectionString));
config.ImagingOptions.Register.Add(new AzureBlobImageFactory(azureConnectionString));
config.ImagingOptions.Register.Add(new Base64ImageFactory());
});
var provider = services.BuildServiceProvider();
Factory Ordering
Custom factories are checked BEFORE standard factories:
// From Scryber.Extensions/ImageOptionExtensions.cs
public static IEnumerable<IPDFImageDataFactory> GetConfiguredFactories(
this ImagingOptions options)
{
List<IPDFImageDataFactory> all = new List<IPDFImageDataFactory>();
// 1. Custom factories from configuration (checked first)
if (null != options.Register)
{
foreach (var one in options.Register)
all.Add(one);
}
// 2. Standard factories (file system, HTTP, etc.)
var standard = GetStandardImageFactories();
all.AddRange(standard);
return all;
}
This allows custom factories to override standard behavior for specific patterns.
Complete Example
Factory Implementation
using System.Data.SqlClient;
using System.Text.RegularExpressions;
using Scryber;
using Scryber.Components;
using Scryber.Imaging;
namespace ProductCatalog.Imaging
{
public class ProductImageFactory : IPDFImageDataFactory
{
private readonly string _connectionString;
private readonly Regex _pattern;
public string FactoryKey => @"^product://[\w-]+$";
public ProductImageFactory(string connectionString)
{
_connectionString = connectionString;
_pattern = new Regex(FactoryKey, RegexOptions.IgnoreCase);
}
public bool ShouldMatch(string source)
{
return !string.IsNullOrEmpty(source) && _pattern.IsMatch(source);
}
public byte[] LoadImageData(IDocument document, IComponent component, string source)
{
// Parse: product://SKU-12345
var productSku = source.Substring("product://".Length);
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
var query = @"
SELECT ImageData
FROM ProductImages
WHERE ProductSKU = @SKU AND IsPrimary = 1";
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@SKU", productSku);
var result = command.ExecuteScalar();
if (result == null)
throw new FileNotFoundException($"Product image not found: {productSku}");
return (byte[])result;
}
}
}
}
}
Configuration
{
"ConnectionStrings": {
"ProductCatalog": "Server=localhost;Database=ProductCatalog;Integrated Security=true;"
},
"Scryber": {
"Imaging": {
"Factories": [
{
"Key": "product-images",
"Type": "ProductCatalog.Imaging.ProductImageFactory, ProductCatalog",
"Config": {
"ConnectionString": "Server=localhost;Database=ProductCatalog;..."
}
}
]
}
}
}
Template
<?xml version='1.0' encoding='utf-8' ?>
<html xmlns='http://www.w3.org/1999/xhtml'>
<body>
<main>
<div>
<span>Product: Widget Pro</span>
<!-- Load product image from database -->
<img src='product://WIDGET-PRO-001'
width='200'
height='200' />
<span>Price: $99.99</span>
</div>
<!-- Load another product -->
<div>
<span>Product: Gadget Plus</span>
<img src='product://GADGET-PLUS-002'
width='200'
height='200' />
</div>
</main>
</body>
</html>
Usage
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Scryber;
using Scryber.Components;
using ProductCatalog.Imaging;
// Build configuration
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
// Setup dependency injection
var services = new ServiceCollection();
// Add Scryber with custom factory
services.AddScryber(options =>
{
var connectionString = config.GetConnectionString("ProductCatalog");
options.ImagingOptions.Register.Add(new ProductImageFactory(connectionString));
});
var provider = services.BuildServiceProvider();
// Generate document
using (var reader = new StreamReader("ProductCatalog.pdfx"))
{
var doc = Document.ParseDocument(reader, ParseSourceType.DynamicContent);
doc.ProcessDocument("ProductCatalog.pdf");
}
Best Practices
Factory Design
- Single Responsibility: Each factory handles one type of source
- Fast Pattern Matching: Use compiled regex for
ShouldMatch - Error Handling: Throw descriptive exceptions for missing images
- Resource Management: Dispose connections, streams properly
- Thread Safety: Factories may be called from multiple threads
Pattern Design
- Use specific patterns:
^myprotocol://not.* - Avoid overlapping patterns between factories
- Document pattern format in FactoryKey property
- Test patterns thoroughly
Performance
- Cache loaded images when appropriate
- Use connection pooling for database factories
- Async operations where possible (though interface is synchronous)
- Monitor factory performance in production
Configuration
- Externalize connection strings and API keys
- Use dependency injection for testability
- Register factories in order of specificity (most specific first)
- Document required configuration
Troubleshooting
“No factory matched source”
- Check factory pattern matches source exactly
- Verify factory is registered in configuration
- Test pattern with regex tester
- Check factory assembly is loaded
“Could not load image data”
- Verify source path/ID is correct
- Check database/API connectivity
- Ensure image data exists
- Add logging to factory implementation
Factory not called
- Verify
ShouldMatchreturns true for source - Check factory is registered before standard factories
- Ensure pattern regex is correct
- Check for exceptions in
ShouldMatch
Performance issues
- Add caching layer
- Use connection pooling
- Monitor database query performance
- Consider pre-loading images
Related Documentation
- Font Configuration - Custom font loading
- Custom Components - Using images in custom components
- Integration Example - Complete example with image factories
- Best Practices - Factory design patterns