Scilim

blog

About

Imaginate

2023-06-20

Table of contents

Introduction

Imaginate is a language designed to facilitate the creation of collages through images and code. The philosophy is based on providing entropy to the image generation process and thus help the programmer/designer choose the best design from different images. This objective is represented through the languages designed with elements like the optional methods (prefixed with the ? symbol), which are applied or not randomly.

The main goal of this language is to facilitate the creative process of creating images through the simple alteration of filters and overlay of images using code.

Compilation Phases

Figure 1: Compilation phases

As can be seen in the Figure 1, first we have the code as the input to the compiler, which is parsed by Flex and matched against the language's grammar through Bison. These are two libraries implemented in C that allow us to (1) separate the input into the tokens that compose the text (this is Flex's job) and (2) match the hierarchy that those token appear to the grammar that we have defined for the language (this is Bison's job).

The result of this first part (formally it's referenced in the literature as the front-end of the compiler), is an Abstract Syntax Tree or AST for short. Here the idea is simple, if you remember your trees from discrete math or DSA class. As we traverse the input we create a tree that will represent the input and make use of different Abstract Data Types or ADTs for short, to fill in our definitions of data types and function definitions.

Finally, we enter into the generation step in which we traverse the AST and recursively create the output of the compiler. Because we are generating images under the hood we generate a python file, which can then be run to generate the images.

Project structure

.
├── backend
│   ├── code-generation
│   │   ├── generator.c
│   │   ├── generator.h
│   │   └── python
│   │       └── generateImage.py
│   ├── domain-specific
│   │   ├── def-execution.c
│   │   └── def-execution.h
│   ├── errors
│   │   ├── error-list.c
│   │   └── error-list.h
│   ├── semantic-analysis
│   │   ├── README.md
│   │   ├── abstract-syntax-tree.h
│   │   ├── adapters.c
│   │   └── adapters.h
│   ├── support
│   │   ├── garbage-collector.c
│   │   ├── garbage-collector.h
│   │   ├── lib.c
│   │   ├── lib.h
│   │   ├── logger.c
│   │   ├── logger.h
│   │   └── shared.h
│   └── symbols-table
│       ├── definitions
│       │   ├── hashmap_defs.c
│       │   └── hashmap_defs.h
│       └── values
│           ├── hashmap_test.c
│           ├── hashmap_val.c
│           └── hashmap_val.h
├── frontend
│   ├── lexical-analysis
│   │   ├── flex-actions.c
│   │   ├── flex-actions.h
│   │   └── flex-patterns.l
│   └── syntactic-analysis
│       ├── bison-actions.c
│       ├── bison-actions.h
│       └── bison-grammar.y
└── main.c

The project is divided into two main strucutures front end for creating the AST and back end for using the AST and managing the logic of user defined values and defs through symbols table.

Difficulties encountered

Flex-Bison

At the beginning, it was difficult to understand the example provided by the chair due to its modularization to be scalable. Therefore, reference was made to the Flex & Bison book by John Levine to understand its operation. After this, it was not only possible to write a grammar, but also to make a grammar that communicates clearly the syntax of the language.

After taking the trouble to develop the grammar, there were no inconveniences in transferring it to Bison, however, a couple of minor modifications had to be made for the correct operation of the compiler. First, modifications were made on ambiguities in it and minor errors committed in its design. On the other hand, as the work was developed, the need arose to make corrections for a better interpretation of the language (For example, real numbers with point as decimal separator were not contemplated).

ADTs

Different ADTs or Abstract Data Types are used throughout the compiler. (Note that they are not exactly ADTs as the internals can be accesed, but for the purposes of the projects they represent the encapsulation found in ADTs). On the one hand, to handle memory leaks, a rudimentary garbage collector was created that stores in a linked list all the portions of memory that it assigns to then free them all at the end of the program. Then, a hashmap ADT was implemented to handle the symbol table of functions and values (being C, it literally made a copy of this for each table). Finally, another linked list was implemented to be able to store the compilation errors and to be able to show all the errors to the user through standard output and not just one at a time.

Code generation and Python

The last step of the work was the generation of code by traversing the AST tree. In terms of efficiency, the traversal of the same has been simplified thanks to the development of the syntax and grammar of the language. As the definitions (variables and methods) are exclusively at the beginning of the code, it is only necessary to make a single traversal of the tree. For the generation of images, the Python Pillow library was used, the use of which was quite straightforward. The main difficulty was in modeling the python code that would be generated when traversing the AST tree, several approaches have been made until finding a structure that adapts to the AST tree.

Future extensions and modifications

In terms of possible extensions, the main one is to add functionality to the image handling. A number that is considered sufficient of functions to modify the images has been implemented but there is functionality that we decided not to implement due to an extension of the project. Also, elements of the language could be added to modify the flow of execution, even from the innate decisions of the compiler. For example, if a certain focus was chosen then certain filters would be applied over others.

On the other hand, a possible future modification is the handling of the size and position of the images that are incorporated into the final render, since the library used allows this easily. However, due to the extension of the project and the necessary restructuring of the syntax, it has not been achieved.

Conclusion

In conclusion, Imagine stands out not only for its ability to facilitate the creation of images programmatically, but also for its innovative way of incorporating randomness into the design process, allowing users to explore a multitude of possibilities and select those that best fit their needs.

Appendix

Grammar

Below is a simplified version of the grammar. It does not include terminal symbols, but includes tokens that represent a set of terminal symbols. Lowercase words represent non-terminal symbols and uppercase terminal symbols.

program -> assignments definitions imaginate

assignments -> assignment assignments
             | /* empty */

assignment -> VAL variableIdentifier COLON value
            | assignmentObject

assignmentObject -> VAL variableIdentifier COLON object

variableIdentifier -> IDENTIFIER

value -> STRING_IDENTIFIER
       | INTEGER

definitions -> definition definitions
             | /* empty */

definition -> DEF_KEYWORD IDENTIFIER argumentsBlock COLON methodChain

emptyParams -> OPEN_PARENTHESES CLOSE_PARENTHESES

imaginate -> IMAGINATE focus methodChain render
           | IMAGINATE foreachFocus methodChain render

focus -> DOT ADDFOCUS paramsBlock

foreachFocus -> DOT FOREACHFOCUS paramsBlock

methodChain -> method methodChain
             | /* empty */

method -> DOT optional methodIdentifier paramsBlock

paramsBlock -> OPEN_PARENTHESES params CLOSE_PARENTHESES

argumentsBlock -> OPEN_PARENTHESES arguments CLOSE_PARENTHESES

arguments -> argument
           | argument COMMA arguments
           | /* empty */

argument -> IDENTIFIER

optional -> QUESTION_SIGN
          | /* empty */

params -> param
        | param COMMA params
        | /* empty */

param -> STRING_IDENTIFIER
       | INTEGER
       | variableIdentifier
       | objectElement

objectElement -> variableIdentifier DOT variableIdentifier

render -> RENDER emptyParams
        | RENDER_ALL emptyParams

methodIdentifier -> ADDBACKGROUND
                  | ADDFLAVOUR
                  | PICKFLAVOUR
                  | ADDGRAYSCALE
                  | ADDBLACKANDWHITE
                  | ADDCONTRAST
                  | IDENTIFIER

object -> OPEN_CURLY_BRACE objectContent CLOSE_CURLY_BRACE

objectContent -> objectAssignment COMMA objectContent
               | objectAssignment
               | /* empty */

objectAssignment -> variableIdentifier COLON value

Features

  • You will be able to export an image (or several) per program, where the exported image will be the result of applying the mentioned operations (background, filter, etc) in the order established in the same program.

  • You can set a focus (.addFoco(...)) to which all operations will be applied or a set of focuses (.foreachFoco(..., ..., ...)) to which the operations will be applied generating multiple combinations.

  • A series of "flavours" (textures, backgrounds, etc) will be provided that can be identified by looking at the documentation.

  • The addFlavour(...) method will add a flavour to the final render.

  • The pickFlavour(..., ..., ...) method will receive a set of flavours, which can be added or not to the final render, as decided by imaginate and the user in the final rendering.

  • Additionally, backgrounds can be added for the images.

  • There will be a certain amount of filters (blur, contrast, etc) that can be applied in the order desired and as many times as wanted. And said filter will be applied over the previous elements (see examples).

  • The "?" symbol will be provided before a method to tell it that it can be included or not in the final rendering, thus leaving to imaginate to choose whether to include it or not.

  • The render method will be provided, which will be the one that ends the code and renders a single version of the specified imaginate.

  • The renderAll method will be provided, which will be the one that ends the code and renders all versions of the specified imaginate. That is, it generates all the combinations of the options defined through pickFlavour and the "?" methods or symbol.

  • The background can be either a determined image or a solid color.

  • Definitions can be added, which can parameterize the application of certain operations. They must be declared prior to Imaginate and can take parameters.

  • Values can be declared, to be able to parameterize in the operations to apply. The objective is to avoid magic numbers.

Examples

Here we have the simplest example (simple in terms of modularity) for creating a number of images that depict a night sky. It applies the foco, styles, and render all in the Imaginate module

Imaginate
          .addFoco(“./moon.png”)
          .addBackground(“./darkblue.png”)
          .addFlavour(“./clouds.png”)
          .pickFlavour(“./stars.png”, “./thunders.png”, “./rain.png”)
          .addFlavour(“./fog.png)
          .?addContrast(20)
          .addGrayScale(20)
          .renderAll()

In this example we can already see the use of val and def to improve upon the modularity of the code to be able to more easily maintain the code.

val sky: "./sky.png"
val moon: "./moon.png"
val stars1: "./starsExploding.png"
val stars2: "./starsFew.png"
val clouds: "./clouds.png"
val city: "./city.png"
val rain: "./rain.png"

def applyMoonAndStars():
    .addBackground(moon)
    .pickFlavour(stars1, stars2)

def applyCloudsAndRain():
    .addFlavour(clouds)
    .addFlavour(rain)
    .addGrayscale(15)
    .addContrast(5, 10)

def createNightCityScene():
    .applyMoonAndStars()
    .applyCloudsAndRain()

Imaginate
    .addFocus(sky)
    .addBackground(city)
    .createNightCityScene()
    .render()

Finally we can also see the use of objects to further encapsulate common settings used in the code

val sky: "./sky.png"
val moon: "./moon.png"
val stars: {exploding: "./starsExploding.png", few: "./starsFew.png"}
val clouds: {img: "./clouds.png", gryScale: 15}
val city: "./city.png"
val rain: "./rain.png"

def applyMoonAndStars():
    .addBackground(moon)
    .pickFlavour(stars.exploding, stars.few)

def applyCloudsAndRain():
    .addFlavour(clouds.img)
    .addFlavour(rain)
    .addGrayscale(clouds.gryScale)
    .addContrast(5, 10)

def createNightCityScene():
    .applyMoonAndStars()
    .applyCloudsAndRain()

Imaginate
    .addFocus(sky)
    .addBackground(city)
    .createNightCityScene()
    .render()