Escrito por

Software Architect at Visum
Artigo Yuri Marx · Mar. 23, 2022 18m read

Introdução ao Embbeded Python - Um processador de imagens como exemplo

A partir da versão 2021.2 do InterSystems IRIS é possível desenvolver serviços de backend, de integração e procedures de bancos de dados utilizando Python. A grande vantagem desta possibilidade é a redução na curva de aprendizado e a utilização de programadores especialistas na linguagem de programação que mais cresce no mundo. O propósito deste artigo é de demonstrar que os projetos criados em InterSystems IRIS podem ser desenvolvidos com Python, ou mesmo com Python e ObjectScript (linguagem de programação proprietária da InterSystems) juntos, para atender a quaisquer requisitos e necessidades do negócio. O desafio com este artigo é processar imagens de forma geral usando Python, uma vez que o ObjectScript não conta com funcionalidades nativas para isto.  

Para ilustrar na prática este casamento perfeito do Python e do InterSystems IRIS, foi criada uma aplicação de exemplo, o iris-image-editor. Esta aplicação possui as seguintes funcionalidades:

  • Criação de thumbnail (miniatura) na imagem;
  • Criação de marca d´agua na imagem;
  • Criação de filtros na imagem, como filtro blur, dentre outros.

Esta aplicação utiliza a biblioteca da comunidade Python Pillow (https://pillow.readthedocs.io/en/stable/). Ela permite realizar desde de operações básicas (leitura, gravação, corte, ampliação, redução, etc.), até as mais avançadas em imagens (aplicação de filtros em geral).

Para obter e executar o iris-image-editor, siga os passos abaixo:

  1. Clone o repositório para um diretório local à sua escolha
$ git clone https://github.com/yurimarx/iris-image-editor.git
  1. Abra o terminal do docker no diretório escolhido e execute:
$ docker-compose build
  1. Execute o container do IRIS:
$ docker-compose up -d 
  1. Para criar thumbnails (miniaturas): Vá no seu Postman (ou cliente REST similar) e configure a requisição como na imagem a seguir, envie e veja a resposta:

Request for Thumbnail images

  1. Para fazer a marca d´agua: Vá no seu Postman (ou outro cliente REST similar) e configure a requisição como na figura, envie e veja o resultado:

Request for watermark images

  • Method: POST
  • URL: http://localhost:52773/iris-image-editor/watermark
  • Body: form-data
  • Key: file (o nome do campo file deve ser file) e o tipo File
  • Value: arquivo do seu computador
  • Key: watermark (texto a ser escrito dentro da imagem) e o type é text
  • Value: I'm a cat (ou outro valor que você queira)
  1. Para fazer filtros: Vá no seu Postman (ou outro cliente REST) e configurar a requisição como na imagem, envie e veja a resposta:

Request for filter images

  • Method: POST
  • URL: http://localhost:52773/iris-image-editor/filter
  • Body: form-data
  • Key: file (o nome do campo deve ser file) e o type File
  • Value: arquivo do seu computador
  • Key: filter (deve ser este nome filter) e o tipo text
  • Value: BLUR, CONTOUR, DETAIL, EDGE_ENHANCE, EDGE_ENHANCE_MORE, EMBOSS, FIND_EDGES, SMOOTH, SMOOTH_MORE ou SHARPEN

Como o Python foi utilizado dentro do IRIS neste exemplo:

A primeira ação a ser tomada é ter o Python instalado junto ao IRIS, no nosso exemplo, utilizamos Docker para instalar e executar o IRIS. Sendo assim, incluimos a instalação do Python e da biblioteca Pillow no arquivo Dockerfile, veja:

 

Trecho de código do Dockerfile responsável por instalar o Python, o PIP e o Pillow

# install libraries required by Pillow to process images
RUNapt-get-yupdate\
    &&apt-get-yinstallapt-utils\
    &&apt-getinstall-ybuild-essentialunzippkg-configwget\
    &&apt-getinstall-ypython3-pip
   
# use pip3 (the python zpm) to install Pillow dependencies
RUNpip3install--upgradepipsetuptoolswheel
RUNpip3install--target/usr/irissys/mgr/pythonPillow

O apt-get install da biblioteca do Linux python3-pip, realiza a instalação do Python e do gerenciador de pacotes/bibliotecas do Python (como o ZPM do ObjectScript ou o Maven do Java), o PyPI (pip3). A partir do pip3 é possível instalar qualquer biblioteca conhecida do mundo Python, inclusive o Pillow.

O comando pip3 install --target /usr/irissys/mgr/python Pillow instala o Pillow e suas dependências dentro do diretório no qual o IRIS procura bibliotecas Python para utilizar (quando no ambiente Linux).

Uma vez que temos o Python e as bibliotecas necessárias instaladas, podemos agora criar nossos métodos de classe em linguagem Python, ao invés de utilizar ObjectScript. Neste exemplo criamos três métodos, todos na classe dc.imageeditor.ImageEditorService, detalhados abaixo:

 

Método de Classe em Python para gerar thumbnails para imagens

ClassMethodProcessThumbnail(imageName)[Language=python]
{
        #ImportrequiredImagelibrary
        fromPILimportImage

 

        # setfoldertoreceivetheimagetobeprocessed
        input_path ="/opt/irisbuild/input/" +imageName
        # setfoldertostorestheresults (imageprocessed)
        output_path ="/opt/irisbuild/output/" +imageName

 

        # opentheoriginalimage
        image =Image.open(input_path)
        # reduceimagesize
        image.thumbnail((90,90))
        # savethenewimage
        image.save(output_path)
}

O primeiro passo é declarar Language = python na declaração do método, assim o IRIS irá utilizar Python, ao invés de ObjectScript, que a linguagem padrão. A seguir, basta escrever todo o conteúdo do método em Python, como seria feito dentro de um arquivo .py.

Este método importa o Pillow com o from PIL import Image. Em seguida são definidos os diretórios de origem da imagem e de destino da imagem processada. O Image.open, obtêm a imagem a ser processada. O image.thumbnail reduz o tamanho da imagem para as dimensões 90x90. O image.save grava o processamento no diretório de destino.

 

Método de Classe em Python que escreve um texto como marca d'agua da imagem

ClassMethodProcessWartermarker(imageName,watermark)[Language=python]
{
        #ImportrequiredImagelibrary
        fromPILimportImage,ImageDraw,ImageFont

 

        # setfoldertoreceivetheimagetobeprocessed
        input_path ="/opt/irisbuild/input/" +imageName
        # setfoldertostorestheresults (imageprocessed)
        output_path ="/opt/irisbuild/output/" +imageName

 

        # opentheoriginalimage
        im =Image.open(input_path)
       
        # extractimagedimensions
        width,height =im.size

 

        # getdrawer
        draw =ImageDraw.Draw(im)
       
        # gettextwriter
        font =ImageFont.truetype("DejaVuSans.ttf",32)
        textwidth,textheight =draw.textsize(watermark,font)

 

        # calculatethex,ycoordinatesofthetext
        margin =10
        x =width -textwidth -margin
        y =height -textheight -margin
       
        # drawwatermarkinthebottomrightcorner
        draw.text((x,y),watermark,font=font)
       
        # savethenewimage
        im.save(output_path)
}

O Image.open() abre a imagem de origem. O im.size obtêm as dimensões da imagem. O ImageDraw.Draw obtem uma referência chamada draw que irá permitir a operação de escrita na imagem. O ImageFont.truetype() escolhe a fonte e tamanho do texto a ser escrito. É calculada a posição x,y do texto na image e o draw.text escreve o texto na fonte escolhida. A imagem final é gravada no diretório de destino.

 

Método de Classe em Python para aplicar filtros avançados na imagem

ClassMethodProcessFilter(imageName,filter)[Language=python]
{
        #ImportrequiredImagelibrary
        fromPILimportImage,ImageFilter

 

        #Importalltheenhancementfilterfrompillow
        fromPIL.ImageFilterimport (
        BLUR,CONTOUR,DETAIL,EDGE_ENHANCE,EDGE_ENHANCE_MORE,
        EMBOSS,FIND_EDGES,SMOOTH,SMOOTH_MORE,SHARPEN
        )

 

        # setfoldertoreceivetheimagetobeprocessed
        input_path ="/opt/irisbuild/input/" +imageName
        # setfoldertostorestheresults (imageprocessed)
        output_path ="/opt/irisbuild/output/" +imageName

 

        # opentheoriginalimage
        im =Image.open(input_path)
       
        iffilter =="BLUR":
                imFiltered =im.filter(BLUR)
        eliffilter =="CONTOUR":
                imFiltered =im.filter(CONTOUR)
        eliffilter =="DETAIL":
                imFiltered =im.filter(DETAIL)
        eliffilter =="EDGE_ENHANCE":
                imFiltered =im.filter(CONTOUR)
        eliffilter =="EDGE_ENHANCE_MORE":
                imFiltered =im.filter(EDGE_ENHANCE_MORE)
        eliffilter =="EMBOSS":
                imFiltered =im.filter(EMBOSS)
        eliffilter =="FIND_EDGES":
                imFiltered =im.filter(FIND_EDGES)
        eliffilter =="SHARPEN":
                imFiltered =im.filter(SHARPEN)
        eliffilter =="SMOOTH":
                imFiltered =im.filter(SMOOTH)
        eliffilter =="SMOOTH_MORE":
                imFiltered =im.filter(SMOOTH_MORE)
        else:  
                imFiltered =im
        # savethenewimage
        imFiltered.save(output_path)
}

O Image.open() abre a imagem de origem. O im.filter(NOME DO FILTRO) aplica o filtro e retorna uma refêrencia para a imagem processada. A imagem com o filtro aplicado é gravada no diretório de destino com imFiltered.save().

A interação entre o ObjectScript e o Python

O exemplo também demonstra como é fácil para o ObjectScript realizar uma chamada para um método escrito em Python. É da mesma forma como seria chamado qualquer outro método. Veja este trecho de exemplo:

ClassMethodDoThumbnail(ImageAs%String)As%Status
{
        Do..ProcessThumbnail(Image)
        Quit$$$OK
}

O método de classe escrito em Python está na mesma classe deste método de classe em ObjectScript, então basta utilizar .. seguido do nome do método e seus argumentos de chamada. O IRIS internamente realiza as conversões de tipos na ida e na volta para os argumentos.

Se o método estiver em outra classe, basta utilizar Do ##class(pacote.NomeClasse).NomeMetodoPython(<argumentos>).

Expondo as funcionalidades como API REST

O IRIS possui extrema facilidade em expor métodos de classe como API REST, basta ter uma classe que herde de %CSP.REST. Nesta aplicação de exemplo isto foi feito a partir da classe dc.imageeditor.ImageEditorRESTApp. Veja:

 

Classe responsável por expor as funcionalidades como API REST

Classdc.imageeditor.ImageEditorRESTAppExtends%CSP.REST
{

 

ParameterCHARSET="utf-8";

 

ParameterCONVERTINPUTSTREAM=1;

 

ParameterCONTENTTYPE="application/json";

 

ParameterVersion="1.0.0";

 

ParameterHandleCorsRequest=1;

 

XDataUrlMap[XMLNamespace="http://www.intersystems.com/urlmap"]
{
<Routes>
<!--ServerInfo-->
<RouteUrl="/"Method="GET"Call="GetInfo"Cors="true"/>
<!--Swaggerspecs-->
<RouteUrl="/_spec"Method="GET"Call="SwaggerSpec"/>

 

<!--doathumbnail-->
<RouteUrl="/thumbnail"Method="POST"Call="DoThumbnail"/>

 

<!--doawatermark-->
<RouteUrl="/watermark"Method="POST"Call="DoWatermark"/>

 

<!--doaimagefilter-->
<RouteUrl="/filter"Method="POST"Call="DoFilter"/>

 

</Routes>
}

 

///Do thumbnail
ClassMethodDoThumbnail()As%Status
{
    SettSC=$$$OK
   
    try{
        // get the file from the multipart request
        Setsource=%request.GetMimeData("file")
       
        // save the file to the input folder, to be processed with imageai
        Setdestination=##class(%Stream.FileBinary).%New()
        Setdestination.Filename="/opt/irisbuild/input/"_source.FileName
        settSC=destination.CopyFrom(source)//reader open the file
        setresult=destination.%Save()
       
        //call embedded python classmethod to thumbnail the image
        Do##class(dc.imageeditor.ImageEditorService).DoThumbnail(source.FileName)

 

        If($FIND(source.FileName,"jpg")>0)||($FIND(source.FileName,"jpeg")>0){
          Set%response.ContentType="image/jpeg"
        }ElseIf($FIND(source.FileName,"png")>0){
          Set%response.ContentType="image/png"
        }Else{
          Set%response.ContentType="application/octet-stream"
        }

 

        Do%response.SetHeader("Content-Disposition","attachment;filename="""_source.FileName_"""")
        Set%response.NoCharSetConvert=1
        Set%response.Headers("Access-Control-Allow-Origin")="*"

 

        Setstream=##class(%Stream.FileBinary).%New()
        Setsc=stream.LinkToFile("/opt/irisbuild/output/"_source.FileName)
        Dostream.OutputToDevice()
         
        SettSC=$$$OK
   
    //returns error message to the user
    }catche{
        SettSC=e.AsStatus()
        SetpOutput=tSC
    }

 

    QuittSC
}

 

///Do Watermark
ClassMethodDoWatermark()As%Status
{
    SettSC=$$$OK
   
    try{
        // get the file from the multipart request
        Setsource=%request.GetMimeData("file")

 

        Setwatermark=$Get(%request.Data(("watermark"),1))
       
        // save the file to the input folder, to be processed with image editor
        Setdestination=##class(%Stream.FileBinary).%New()
        Setdestination.Filename="/opt/irisbuild/input/"_source.FileName
        settSC=destination.CopyFrom(source)//reader open the file
        setresult=destination.%Save()
       
        //call embedded python classmethod to thumbnail the image
        Do##class(dc.imageeditor.ImageEditorService).DoWatermark(source.FileName,watermark)

 

        If($FIND(source.FileName,"jpg")>0)||($FIND(source.FileName,"jpeg")>0){
          Set%response.ContentType="image/jpeg"
        }ElseIf($FIND(source.FileName,"png")>0){
          Set%response.ContentType="image/png"
        }Else{
          Set%response.ContentType="application/octet-stream"
        }

 

        Do%response.SetHeader("Content-Disposition","attachment;filename="""_source.FileName_"""")
        Set%response.NoCharSetConvert=1
        Set%response.Headers("Access-Control-Allow-Origin")="*"

 

        Setstream=##class(%Stream.FileBinary).%New()
        Setsc=stream.LinkToFile("/opt/irisbuild/output/"_source.FileName)
        Dostream.OutputToDevice()
         
        SettSC=$$$OK
   
    //returns error message to the user
    }catche{
        SettSC=e.AsStatus()
        SetpOutput=tSC
    }

 

    QuittSC
}

 

///Do filter
ClassMethodDoFilter()As%Status
{
    SettSC=$$$OK
   
    try{
        // get the file from the multipart request
        Setsource=%request.GetMimeData("file")

 

        Setfilter=$Get(%request.Data(("filter"),1))
       
        // save the file to the input folder, to be processed with image editor
        Setdestination=##class(%Stream.FileBinary).%New()
        Setdestination.Filename="/opt/irisbuild/input/"_source.FileName
        settSC=destination.CopyFrom(source)//reader open the file
        setresult=destination.%Save()
       
        //call embedded python classmethod to thumbnail the image
        Do##class(dc.imageeditor.ImageEditorService).DoFilter(source.FileName,filter)

 

        If($FIND(source.FileName,"jpg")>0)||($FIND(source.FileName,"jpeg")>0){
          Set%response.ContentType="image/jpeg"
        }ElseIf($FIND(source.FileName,"png")>0){
          Set%response.ContentType="image/png"
        }Else{
          Set%response.ContentType="application/octet-stream"
        }

 

        Do%response.SetHeader("Content-Disposition","attachment;filename="""_source.FileName_"""")
        Set%response.NoCharSetConvert=1
        Set%response.Headers("Access-Control-Allow-Origin")="*"

 

        Setstream=##class(%Stream.FileBinary).%New()
        Setsc=stream.LinkToFile("/opt/irisbuild/output/"_source.FileName)
        Dostream.OutputToDevice()
         
        SettSC=$$$OK
   
    //returns error message to the user
    }catche{
        SettSC=e.AsStatus()
        SetpOutput=tSC
    }

 

    QuittSC
}

 

///General information
ClassMethodGetInfo()As%Status
{
  SETversion=..#Version
  SETfmt=##class(%SYS.NLS.Format).%New("ptbw")
 
  SETinfo={
    "Service":"Image Editor API",
    "version":(version),
    "Developer":"Yuri Gomes",
    "Status":"Ok",
    "Date":($ZDATETIME($HOROLOG))
  }
  Set%response.ContentType=..#CONTENTTYPEJSON
  Set%response.Headers("Access-Control-Allow-Origin")="*"

 

  Writeinfo.%ToJSON()
  Quit$$$OK
}

 

ClassMethodSwaggerSpec()As%Status
{
  SettSC=##class(%REST.API).GetWebRESTApplication($NAMESPACE,%request.Application,.swagger)
  Doswagger.info.%Remove("x-ISC_Namespace")
  Setswagger.basePath="/iris-tts"
  Setswagger.info.title="TTS Service API"
  Setswagger.info.version="1.0"
  Setswagger.host="localhost:52773"
  Return..%ProcessResult($$$OK,swagger)
}

 

ClassMethod%ProcessResult(pStatusAs%Status={$$$OK},pResultAs%DynamicObject="")As%Status[Internal]
{
  #dim%responseAs%CSP.Response
  SETtSC=$$$OK
  IF$$$ISERR(pStatus){
    SET%response.Status=500
    SETtSC=..StatusToJSON(pStatus,.tJSON)
    IF$isobject(tJSON){
      SETpResult=tJSON
    }ELSE{
      SETpResult={"errors":[{"error":"Unknown error parsing status code"}]}
    }
  }
  ELSEIFpStatus=1{
    IF'$isobject(pResult){
      SETpResult={
      }
    }
  }
  ELSE{
    SET%response.Status=pStatus
    SETerror=$PIECE(pStatus," ",2,*)
    SETpResult={
      "error":(error)
    }
  }
 
  IFpResult.%Extends("%Library.DynamicAbstractObject"){
    WRITEpResult.%ToJSON()
  }
  ELSEIFpResult.%Extends("%JSON.Adaptor"){
    DOpResult.%JSONExport()
  }
  ELSEIFpResult.%Extends("%Stream.Object"){
    DOpResult.OutputToDevice()
  }
 
  QUITtSC
}

 

}

A classe extende de %CSP.REST e possui uma seção <Routes> responsável por criar as rotas (URL) HTTP para utilizar as funcionalidades. Na tag <Route> é definida a propriedade Url como o caminho HTTP utilizado para chamar o método de classe indicado na propriedade Call. O verbo HTTP é escolhido na propriedade Method.

A chamada %request.GetMimeData() é responsável por receber os valores de input da requisição. O %response.ContentType define o tipo de retorno da requisição e a chamada %Stream.FileBinary.%New() permite obter uma referência para um arquivo, basta requisitar LinkToFile(), indicando o caminho para o arquivo. Por último é executado o OutputToDevice() para escrever o conteúdo do arquivo na resposta HTTP.

Outras formas de utilizar o Python e IRIS juntos (fonte: https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cl…)

A partir do terminal do IRIS, é possível acionar o ambiente de execução do Python (shell):

USER>do ##class(%SYS.Python).Shell()

A partir de um programa Python é possível chamar Classes e Métodos ObjectScript

# import the iris module and show the classes in this namespace
import iris
print('\nInterSystems IRIS classes in this namespace:')
status = iris.cls('%SYSTEM.OBJ').ShowClasses()
print(status)

Ou

>>> import iris
>>> status = iris.cls('User.EmbeddedPython').Test()
Fibonacci series:
0 1 1 2 3 5 8
InterSystems IRIS classes in this namespace:
User.Company
User.EmbeddedPython
User.Person
>>> print(status)
1

Em funções e Stored Procedures SQL:

CREATE FUNCTION tzconvert(dt DATETIME, tzfrom VARCHAR, tzto VARCHAR)
    RETURNS DATETIME
    LANGUAGE PYTHON
{
    from datetime import datetime
    from dateutil import parser, tz
    d = parser.parse(dt)
    if (tzfrom is not None):
        tzf = tz.gettz(tzfrom)
        d = d.replace(tzinfo = tzf)
    return d.astimezone(tz.gettz(tzto)).strftime("%Y-%m-%d %H:%M:%S")
}

A partir da classe utilitária %SYS.Python

Class Demo.PDF
{

ClassMethod CreateSamplePDF(fileloc As %String) As %Status
{
    set canvaslib = ##class(%SYS.Python).Import("reportlab.pdfgen.canvas")
    set canvas = canvaslib.Canvas(fileloc)
    do canvas.drawImage("C:\Sample\isc.png", 150, 600)
    do canvas.drawImage("C:\Sample\python.png", 150, 200)
    do canvas.setFont("Helvetica-Bold", 24)
    do canvas.drawString(25, 450, "InterSystems IRIS & Python. Perfect Together.")
    do canvas.save()
}

}

Usando o Python Buitin Functions

set builtins = ##class(%SYS.Python).Import("builtins")
USER>do builtins.print("hello world!")
hello world!

Saiba mais: