-
-
Save Ke-/4fc6d3baedc760b076860d96cb102101 to your computer and use it in GitHub Desktop.
Replacement [image] Type for Lasso 9.x — updated to use file based approach.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[//lasso | |
/* | |
[sys_image] type for Lasso 9.x https://gist.github.com/Ke-/4fc6d3baedc760b076860d96cb102101 | |
This is a replacement for the native [Image] type in Lasso 9.x. This version requires | |
[sys_process] and calls the ImageMagick command line utilities rather than relying on the | |
low-level libraries. Doing so mimises memory use. | |
Based loosely on the Lasso 8.x replacement by Jason Huck https://gist.github.com/jasonhuck/f281afaa528c82a596a9 | |
Updated to use //tmp and file based approach. Resize and serving the resulting image us twice as fast, handles parallel requests much better. | |
The following methods are not yet implemented: | |
[Image->Composite] | |
*/ | |
define sys_image => type { | |
data public filepath = void | |
data public imagedata = void | |
data public metadata = void | |
data public describe = string | |
data public platform = string | |
data public path = '/usr/local/bin' | |
data public debug = 0 | |
data private working_file = '' | |
public oncreate(filepath::string) => .oncreate(-filepath = #filepath) | |
public oncreate(imagedata::bytes) => .oncreate(-binary = #imagedata) | |
public ascopy => sys_image(.imagedata) | |
// This file is only used on image modification (faster than piping) | |
public working_file => { | |
// Use pipe for non-web requests | |
! web_request | |
? return '-' | |
// Use existing file name | |
.'working_file' | |
? return .'working_file' | |
// Set the file name | |
local(f) = '//tmp/image-' + lasso_uniqueid | |
// Populate the data | |
file_write(#f, .imagedata, -fileoverwrite) | |
// Schedule deletion of file | |
define_atend({ file_delete(#f) }) | |
// Return working file | |
return .'working_file' := #f | |
} | |
public read_path => .working_file | |
public write_path => .working_file | |
public oncreate( | |
-filepath::string='', | |
-binary = void, | |
-base64 = void, | |
-info = void | |
) => { | |
// get image data | |
if(#base64->isa(::string)) => { | |
.imagedata = decode_base64(#base64) | |
else(#binary->isa(::bytes)) | |
.imagedata = #binary | |
else(#filepath) | |
.filepath = #filepath | |
.imagedata = file_read(#filepath) | |
else | |
fail( -9996, 'Image type initializer requires at least one parameter (filepath, image type, or image data)') | |
} | |
.platform = lasso_version(-lassoplatform) | |
if(.platform >> 'Lin') => { | |
.path = '/usr/bin/' | |
else(.platform >> 'Mac') | |
.path = '/usr/local/bin/' | |
else(.platform >> 'Win') | |
local( | |
os = sys_process('cmd', array('/c','where','identify')->asstaticarray), | |
path = string(#os->read) | |
) | |
#os->close | |
#path->trim & removetrailing('identify.exe') | |
.path = #path | |
} | |
} | |
public metadata => { | |
// Reuse meta data | |
.'metadata' ? return .'metadata' | |
// Otherwise identify | |
.identify | |
return .'metadata' | |
} | |
public imagedata => { | |
.'imagedata' ? return .'imagedata' | |
return .'imagedata' := file_read(.working_file) | |
} | |
public metadata(key::string) => .metadata->find(#key) | |
public width => .metadata('width') | |
public height => .metadata('height') | |
public length => .metadata('length') | |
public format => .metadata('format') | |
public comments => .metadata('comment') | |
public data => .imagedata | |
public process( | |
args::array, | |
-cmd = .path + 'convert', | |
-return_only = false, | |
-i = 0 | |
) => { | |
.platform >> 'Win' ? #cmd += '.exe' | |
.debug ? debug('Command: ' + #cmd + ' ' + #args->join(' ')) | |
.debug ? debug('imagedata.type' = .imagedata->type) | |
.debug ? debug('imagedata.size' = .imagedata->size) | |
// #args->insertfirst('-debug') | |
local(os) = sys_process(#cmd, #args->asstaticarray) | |
handle => { | |
#os->close | |
} | |
// Work with pipe | |
if(.write_path == '-' || #return_only) => { | |
#os->closewrite | |
local( | |
response = bytes, | |
read = bytes, | |
error = '' | |
) | |
while(#os->isopen && !#response->size && #i++ < 10) => { | |
#read := #os->read(1024 * 100, -timeout = 3000) | |
! #error | |
? #error = #os->readerror->asstring | |
fail_if(#error, -1, #error) | |
if(#read === void && !#error) => { | |
#os->close | |
return .process(#args, -cmd = #cmd, -return_only = #return_only, -i = #i ) | |
} | |
#read | |
? #response->append( | |
#read | |
) | |
} | |
.debug ? debug('response.size' = #response->size) | |
.debug ? debug('response.type' = #response->type) | |
else | |
#os->wait | |
.debug ? debug('os.readstring' = #os->readstring) | |
.debug ? debug('os.readerror' = #os->readerror) | |
} | |
#return_only | |
? return #response | |
// Clear any caches | |
.imagedata = void | |
.metadata = void | |
} | |
public addcomment( | |
-comment::string = '' | |
) => { | |
local( | |
args = array( | |
.read_path, | |
'-set', | |
'comment', | |
#comment | |
) | |
) | |
#args->insert(.write_path) | |
.process(#args) | |
} | |
public annotate( | |
text::string, | |
-left::integer, | |
-top::integer, | |
-font::string='', | |
-size::integer=0, | |
-color::string='', | |
-aliased = false | |
) => { | |
#left >= 0 ? #left = '+' + #left | |
#top >= 0 ? #top = '+' + #top | |
local( | |
'geometry' = #left + #top, | |
'operation' = '-annotate', | |
'args' = array(.read_path), | |
'cmd' = .path + 'convert' | |
) | |
if(#font->isnota(::void)) => { | |
#args->insert('-font') | |
#args->insert(#font) | |
} | |
if(#size->isnota(::void)) => { | |
#args->insert('-pointsize') | |
#args->insert(#size) | |
} | |
if(#color->isnota(::void)) => { | |
#args->insert('-fill') | |
#args->insert(#color) | |
} | |
#aliased ? #args->insert('-antialias') | |
#args->insert(#operation) | |
#args->insert(#geometry) | |
#args->insert(#text) | |
#args->insert(.write_path) | |
.process(#args) | |
} | |
public blur( | |
-angle = void, | |
-radius = void, | |
-sigma = void, | |
-gaussian = false | |
) => { | |
#angle->isnota(::void) ? #angle = decimal(#angle) | |
#radius->isnota(::void) ? #radius = decimal(#radius) | |
#sigma->isnota(::void) ? #sigma = decimal(#sigma) | |
local( | |
args = array(.read_path), | |
'cmd' = .path + 'convert' | |
) | |
if(#gaussian) => { | |
fail_if(#radius->isa(::void) || #sigma->isa(::void), -9996, '[Image->Blur] requires -Radius and -Sigma values to perform a Gaussian blur.') | |
#args->insert('-blur') | |
#args->insert(#radius + 'x' + #sigma) | |
else | |
// ImageMagick requires radius and sigma for motion blurs, but Lasso's API doesn't, | |
// so for compatibility default values are provided. | |
#radius->isa(::void) ? #radius = 0 | |
#sigma->isa(::void) ? #sigma = 12 | |
#angle >= 0 ? #angle = '+' + #angle | |
local(geometry) = #radius + 'x' + #sigma + #angle | |
#args->insert('-motion-blur') | |
#args->insert(#geometry) | |
} | |
#args->insert(.write_path) | |
.process(#args) | |
} | |
public execute(params::string) => { | |
local( | |
args = #params->split(' '), | |
cmd = #args->get(1), | |
path = .read_path | |
) | |
#args->remove(1) | |
match(#cmd) => { | |
case('mogrify') | |
#cmd = .path + #cmd | |
case('composite') | |
#cmd = .path + #cmd | |
case('montage') | |
#cmd = .path + #cmd | |
case | |
fail(-1, 'Unsupported command ' + #cmd) | |
} | |
#args = ( | |
with arg in #args | |
sum #arg->split('=') | |
) + array(#path) + array(.write_path) | |
.process(#args, -cmd = #cmd) | |
} | |
public contrast(...) => { | |
local( | |
args = array( | |
.read_path, | |
(params && params->first == null ? '+' | '-') + 'contrast', | |
.write_path | |
) | |
) | |
.process(#args) | |
} | |
public convert(format::string, -quality::integer=0) | |
=> .convert(-format = #format, -quality = #quality) | |
public convert( | |
-format::string, | |
-quality::integer=0 | |
) => { | |
fail_if(#quality < 0 || #quality > 1000, -1, 'Value of -quality parameter must be between 0 and 1000.') | |
local( | |
args = array( | |
.read_path, | |
'-format', | |
'"' + #format + '"' | |
) | |
) | |
#quality->isnota(::void) ? #args->insert('-quality')&insert(#quality) | |
#args->insert(.write_path) | |
.process(#args) | |
} | |
public crop( | |
-width::integer, | |
-height::integer, | |
-left::integer, | |
-top::integer | |
) => { | |
#left >= 0 ? #left = '+' + #left | |
#top >= 0 ? #top = '+' + #top | |
local( | |
args = array( | |
.read_path, | |
'-crop', | |
#width + 'x' + #height + #left + #top, | |
'+repage' | |
) | |
) | |
#args->insert(.write_path) | |
.process(#args) | |
} | |
public enhance => { | |
local( | |
args = array(.read_path, '-enhance', .write_path) | |
) | |
.process(#args) | |
} | |
public file => { | |
return(.filepath) | |
} | |
public fliph => { | |
local( | |
args = array(.read_path, '-flop', .write_path) | |
) | |
.process(#args) | |
} | |
public flipv => { | |
local( | |
args = array(.read_path, '-flip', .write_path) | |
) | |
.process(#args) | |
} | |
public histogram => { | |
local( | |
args = array( | |
.read_path, | |
'-format', | |
'%c', | |
'histogram:info:-' | |
) | |
) | |
.process(#args, -return_only) | |
} | |
public identify => { | |
local( | |
cmd = .path + 'identify', | |
args = array('-verbose', .read_path), | |
response = .process(#args, -cmd = #cmd, -return_only)->asstring, | |
lines = #response->split('\n'), | |
metadata = map | |
) | |
.metadata = #metadata | |
.describe = #response | |
with line in #lines | |
where #line | |
do { | |
local( | |
'name' = #line->split(':')->first->trim &, | |
'value' = string(#line)->trim & removeleading(#name + ': ')& | |
) | |
if(#name == 'Geometry') => { | |
local( | |
'width' = integer(#value->split('x')->first), | |
'height' = integer(#value->split('x')->second->split('+')->first) | |
) | |
#metadata->insert('width' = #width) | |
#metadata->insert('height' = #height) | |
} | |
#name && #value ? #metadata->insert(#name = #value) | |
} | |
} | |
public modulate( | |
-brightness::integer, | |
-saturation::integer, | |
-hue::integer | |
) => { | |
local( | |
args = array( | |
.read_path, | |
'-modulate', | |
#brightness + ',' + #saturation + ',' + #hue, | |
.write_path | |
) | |
) | |
.process(#args) | |
} | |
public pixel( | |
-left::integer, | |
-top::integer, | |
-hex = false | |
) => { | |
local( | |
args = array( | |
.read_path + '[1x1+' + #left + '+' + #top + ']', | |
'txt:-' | |
) | |
) | |
local(response) = .process(#args, -return_only) | |
if(#hex) => { | |
local(hex) = string_findregexp( | |
#response, | |
-find='#(?:[0-9a-fA-F]{3}){1,2}', | |
-ignorecase | |
)->first | |
return #hex | |
else | |
local(rgb) = string_findregexp( | |
#response, | |
-find='s?rgb\\(([0-9,]{5,11})\\)', | |
-ignorecase | |
)->second->split(',') | |
return #rgb | |
} | |
} | |
public resolutionh => { | |
return(integer(string(.metadata->find('Resolution'))->split('x')->first)) | |
} | |
public resolutionv => { | |
return(integer(string(.metadata->find('Resolution'))->split('x')->second)) | |
} | |
public rotate( | |
degrees::integer, | |
-bgcolor::string='' | |
) => { | |
fail_if( | |
#degrees < 0 || #degrees > 360, | |
-9996, 'The first argument to [Image->Rotate] must be an integer value between 0 and 360.' | |
) | |
local( | |
args = array(.read_path) | |
) | |
if(#bgcolor) => { | |
#args->insert('-background') | |
#args->insert(#bgcolor) | |
} | |
#args->insert('-rotate') | |
#args->insert(#degrees) | |
#args->insert(.write_path) | |
.process(#args) | |
} | |
public save( | |
-filepath::string, | |
-quality::integer = 0 | |
) => { | |
local(format) = #filepath->split('.')->last | |
#quality ? .convert( -format=#format, -quality=#quality) | .convert( -format=#format) | |
file_write(#filepath, .imagedata, -fileoverwrite) | |
} | |
public scale( | |
-width = void, | |
-height = void, | |
-sample = false, | |
-thumbnail = false | |
) => { | |
fail_if(#width->isa(::void) && #height->isa(::void), -1, '[Image->Scale] requires at least one dimension.') | |
#width->isa(::void) ? local(width) = '' | |
#height->isa(::void) ? local(height) = '' | |
local(geometry) = #width + 'x' + #height | |
#geometry->removetrailing('x') | |
local(operation) = '-scale' | |
#sample ? #operation = '-sample' | |
#thumbnail ? #operation = '-thumbnail' | |
local( | |
args = array( | |
.read_path, | |
#operation, | |
#geometry | |
) | |
) | |
#thumbnail ? #args->insert('-strip') | |
#args->insert(.write_path) | |
.process(#args) | |
} | |
public setcolorspace( | |
-rgb = false, | |
-cmyk = false, | |
-gray = false | |
) => { | |
fail_if(!params->size, -1, '[Image->SetColorSpace] requires at least one parameter, either -rgb, -cmyk, or -gray.') | |
local(colorspace) = '' | |
#rgb ? #colorspace = 'RGB' | |
#cmyk ? #colorspace = 'CMYK' | |
#gray ? #colorspace = 'GRAY' | |
local( | |
args = array( | |
.read_path, | |
'-colorspace', | |
#colorspace, | |
.write_path | |
) | |
) | |
.process(#args) | |
} | |
public sharpen( | |
-radius::integer, | |
-sigma::integer, | |
-amount = void, | |
-threshold = void | |
) => { | |
local(geometry) = #radius + 'x' + #sigma | |
if(#amount->isnota(::void) && #threshold->isnota(::void)) => { | |
#amount = decimal(#amount) | |
#amount >= 0 ? #amount = '+' + #amount | |
#threshold = decimal(#threshold) | |
#threshold >= 0 ? #threshold = '+' + #threshold | |
#geometry += #amount + #threshold | |
} | |
local( | |
args = array( | |
.read_path, | |
'-unsharp', | |
#geometry, | |
.write_path | |
) | |
) | |
.process(#args) | |
} | |
} | |
] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment