Skip to content

Instantly share code, notes, and snippets.

@Ke-
Forked from jasonhuck/image.lasso
Last active August 6, 2019 22:18
Show Gist options
  • Save Ke-/4fc6d3baedc760b076860d96cb102101 to your computer and use it in GitHub Desktop.
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.
[//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