Pattern

This module provides classes that describe embroidery patterns and can display them and information associated with them.

Copyright (C) 2016 David Boddie <david@boddie.org.uk>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

Imports

We import a number of standard Java classes to help with input/output from files and managing data structures. The Android classes we use are concerned with accessing resources, rendering graphics, performing background tasks and presenting a user interface.

from java.io import BufferedInputStream, File, FileInputStream, InputStream, \
                    IOException
from java.lang import Byte, Math, Object, Runnable, String
from java.text import DateFormat
from java.util import List, Map, Queue

from android.content import Context
from android.content.res import Resources
from android.graphics import Bitmap, Canvas, Color, Paint, Rect
from android.os import AsyncTask, Handler
from android.view import View, ViewGroup
from android.widget import Adapter, BaseAdapter, ImageView, TextView

The remaining imports are from two application modules.

The common module provides various classes to describe elements of embroidery patterns as well as implementations of streams used to read data from JEF files.

from common import LittleStream, Rectangle, Stitch, Threads

The jef_colours module provides a class that is used to obtain the colour definitions for threads used in a pattern.

from jef_colours import ColourInfo

Pattern

We define a class to represent a pattern with a number of fields that will be accessed by other components.

class Pattern(Object):

    __fields__ = {"date": String,
                  "hoop_code": int,
                  "rectangles": List(Rectangle),
                  "colours": List(int),
                  "thread_types": List(int),
                  "threads": List(List(Stitch))}
    
    def __init__(self):
    
        Object.__init__(self)

The following method fills in the fields using data supplied by an InputStream. This allows us to separate the task of opening a JEF file from the task of decoding its contents.

    @args(void, [InputStream])
    def read(self, input):
    
        stream = LittleStream(input)
        start = stream.readInt()
        has_date = (stream.readInt() & 1) != 0
        
        if has_date:
            dateFormat = DateFormat.getDateTimeInstance()
            self.date = String(stream.readBytes(14), "ASCII")
        else:
            stream.skipBytes(14)
            self.date = ""
        
        stream.skipBytes(2)
        threads = stream.readInt()
        data_length = stream.readInt() * 2
        self.hoop_code = stream.readInt()
        
        # Read bounding rectangles.
        self.rectangles = []
        
        while stream.position < 0x74:
            x1 = stream.readInt()
            y1 = stream.readInt()
            x2 = stream.readInt()
            y2 = stream.readInt()
            if x1 != -1 and y1 != -1 and x2 != -1 and y2 != -1:
                self.rectangles.add(Rectangle(-x1, -y1, x2, y2))
        
        self.colours = []
        i = 0
        while i < threads:
            self.colours.add(stream.readInt())
            i += 1
        
        self.thread_types = []
        i = 0
        while i < threads:
            self.thread_types.add(stream.readInt())
            i += 1
        
        #stream.skipBytes(start - stream.position)
        self.read_threads(stream)

Since the task of reading the thread data is a fair amount of code in itself, this is split into a separate method that continues to use the stream object created in the previous method.

    @args(void, [LittleStream])
    def read_threads(self, stream):
    
        self.threads = []
        x = y = 0
        
        stitches = []
        first = True
        command = ""
        i = 0
        
        while True:
        
            try:
                bx = Byte(stream.readByte()).intValue()
                by = Byte(stream.readByte()).intValue()
            except IOException:
                break
            
            if bx == -128 and by == 0x01:
                # Record the coordinates already read and skip the next two bytes.
                if len(stitches) > 0:
                    self.threads.add(stitches)
                
                stitches = []
                first = True
                stream.skipBytes(2)
                continue
            
            elif bx == -128 and by == 0x02:
                command = "move"
                first = True
                x += Byte(stream.readByte()).intValue()
                y += Byte(stream.readByte()).intValue()
            
            elif bx == -128 and by == 0x10:
                if len(stitches) > 0:
                    self.threads.add(stitches)
                break
            else:
                command = "stitch"
                x += bx
                y += by
            
            if command == "move":
                stitches.add(Stitch(command, x, y))
            elif first:
                stitches.add(Stitch("move", x, y))
                first = False
            else:
                stitches.add(Stitch(command, x, y))

PatternRenderer

Rendering is performed by the following class which is used in conjunction with an instance of the PatternViewAdapter class. Since we want to perform rendering of each pattern as a background task, the class is derived from the standard AsyncTask template class.

We need to specify which concrete types our class uses for the input parameters, progress value and result value. We do this by defining the __item_types__ attribute which indicates that an instances of the class will process an array of integers, publish a Bitmap to indicate progress, and produce a Bitmap as the result.

class PatternRenderer(AsyncTask):

    __item_types__ = [int, Bitmap, Bitmap]

The __init__ method accepts the file containing the pattern to render, the colour information object, the ImageView used to display the resulting bitmap, a Map that contains cached bitmaps for patterns already rendered, and a queue of keys for bitmaps in the cache.

    @args(void, [File, ColourInfo, ImageView, Map(int, Bitmap), Queue(int)])
    def __init__(self, file, colourInfo, imageView, cache, queue):
    
        AsyncTask.__init__(self)
        
        self.file = file
        self.colourInfo = colourInfo
        self.imageView = imageView
        self.cache = cache
        self.queue = queue
        
        self.background = Color.argb(255, 64, 64, 64)

We define a method to conveniently create new empty bitmaps since we need to create these in a couple of places in this module.

    @static
    @args(Bitmap, [int, int])
    def emptyBitmap(width, height):
    
        bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
        return bitmap

The following method performs work in a background thread. It accepts an array of the Params type, which we defined above as int, so it will receive an array of integers which describe the width and height of each bitmap to create, as well as the position of the bitmap in the adapter that uses the PatternRenderer. The position is used as a key into the Map we use as a cache.

    @args(Result, [[Params]])
    def doInBackground(self, params):
    
        width, height, self.position = params
        
        stream = BufferedInputStream(FileInputStream(self.file))
        pattern = Pattern()
        pattern.read(stream)

We read the file containing the pattern using a stream, passing it to a newly-created Pattern object. The bounding boxes of the threads that make up the pattern are combined to produce an overall bounding box for the pattern.

        x1 = y1 = x2 = y2 = 0
        
        for thread in pattern.threads:
        
            for stitch in thread:
            
                x1 = Math.min(x1, stitch.x)
                x2 = Math.max(x2, stitch.x)
                y1 = Math.min(y1, stitch.y)
                y2 = Math.max(y2, stitch.y)
        
        bbox_width = x2 - x1
        bbox_height = y2 - y1

We create a Bitmap of the desired size and fill it with a predefined background colour before using a Canvas to draw the pattern in the bitmap. We use the bounding box size to scale the drawing so that it fits inside the bitmap.

        bitmap = self.emptyBitmap(width, height)
        bitmap.eraseColor(self.background)
        
        canvas = Canvas(bitmap)
        
        ox = float(width/2)
        oy = float(height/2)
        xscale = float(width)/bbox_width
        yscale = float(height)/bbox_height
        scale = Math.min(xscale, yscale)

We combine the colours and threads as we iterate over them, using each colour to create a Paint that we apply to a series of calls to the Canvas.drawLine method. Each "move" in the thread sets the current position using the absolute coordinate values defined by the stitch. Each "stitch" causes a line to be drawn from the current position to the new position before updating the current position.

        colour_it = pattern.colours.iterator()
        thread_it = pattern.threads.iterator()
        
        while colour_it.hasNext() and thread_it.hasNext():
        
            colour_index = colour_it.next()
            thread = thread_it.next()
            
            paint = Paint()
            paint.setColor(self.colourInfo.getColour(colour_index))
            
            x = ox
            y = oy
            
            for stitch in thread:
            
                sx = float(stitch.x)
                sy = float(stitch.y)
                
                if stitch.command == "move":
                    x = sx
                    y = sy
                
                elif stitch.command == "stitch":
                    canvas.drawLine(ox + (x * scale), oy - (y * scale),
                                    ox + (sx * scale), oy - (sy * scale),
                                    paint)
                x = sx
                y = sy
            
            self.publishProgress(array([bitmap]))
        
        return bitmap

As each thread is drawn, the current bitmap is published to show the progress made. When all threads have been drawn, the final bitmap is returned.

The following method handles each publication of the progress made while rendering, updating the ImageView that displays the bitmap in the application's main UI thread.

    @args(void, [[Progress]])
    def onProgressUpdate(self, progress):
    
        bitmap = progress[0]
        self.imageView.setImageBitmap(bitmap)

When all processing has finished, the following method is called by the application framework to allow the final result to be handled in the main UI thread. We update the bitmap cache with the new bitmap and add its position to the queue of keys to the cache. Then we update the ImageView to show the finished bitmap.

    @args(void, [Result])
    def onPostExecute(self, result):
    
        self.cache[self.position] = result
        self.queue.add(self.position)
        self.imageView.setImageBitmap(result)

PatternInfo

This class is currently unused. It provides a custom TextView subclass that displays basic information about a pattern.

class PatternInfo(TextView):

    @args(void, [Context, Pattern])
    def __init__(self, context, pattern):
    
        TextView.__init__(self, context)
        
        text = "Date: " + str(pattern.date) + "\n"
        text += "Hoop: " + str(pattern.hoop_code) + "\n"
        
        text += "Rectangles:\n"
        for r in pattern.rectangles:
            text += "(" + str(r.x1) + "," + str(r.y1) + "), "
            text += "(" + str(r.x2) + "," + str(r.y2) + ")\n"
        
        text += "Colours: "
        for colour in pattern.colours:
            text += str(colour) + " "
        
        text += "\n"
        
        text += "Thread types: "
        for thread_type in pattern.thread_types:
            text += str(thread_type) + " "
        
        text += "\n"
        self.setText(text)

PatternViewAdapter

This adapter class exposes a list of JEF files to ListView classes and other View collections, providing information about the underlying data structure and creating View instances for display. In this case, the adapter creates ImageView instances to show the bitmaps that represent the contents of the JEF files.

The adapter obtains the bitmaps for each item in the list using instances of the PatternRenderer class which renders each bitmap in a background thread. The adapter also maintains a cache of bitmaps that have already been created in order to avoid redoing work, but limits the number of bitmaps in the cache to avoid using too much memory.

The class implements the Runnable interface so that we can implement a method that allows us to postpone events and perform them later.

class PatternViewAdapter(BaseAdapter):

    __interfaces__ = [Runnable]
    
    __fields__ = {
        "cache": Map(int, Bitmap),
        "positions": Queue(int),
        "size": int,
        "pending": Queue(WorkItem)
        }

The __init__ method accepts a File that represents the directory containing the JEF files and the application's Resources object. We obtain a ColourInfo object using the application's resources and define the default size of the bitmaps that will be produced. We obtain a list of files to read before initialising the cache.

    @args(void, [File, Resources])
    def __init__(self, directory, resources):
    
        BaseAdapter.__init__(self)
        self.directory = directory
        self.colourInfo = ColourInfo(resources)
        self.size = 128
        
        self.items = []
        files = array(File, 0)
        
        if directory.exists():
            files = directory.listFiles()
        
        i = 0
        while i < len(files):
        
            f = files[i]
            if f.isFile() and f.getName().endsWith(".jef"):
                self.items.add(f)
            
            i += 1
        
        self.cache = {}
        self.positions = []
        self.handler = Handler()

The following methods implement the standard adapter API. Only the getCount method needs to return a valid value, returning the number of items in the list.

    @args(int, [])
    def getCount(self):
        return len(self.items)
    
    @args(Object, [int])
    def getItem(self, position):
        return None
    
    @args(long, [int])
    def getItemId(self, position):
        return long(0)

The final method returns a View for display by any container that uses this adapter, based on the position of the item in the list and the parent ViewGroup that represents the container. The ImageView that is returned is created using the application context provided by the parent.

    @args(View, [int, View, ViewGroup])
    def getView(self, position, convertView, parent):
    
        imageView = ImageView(parent.getContext())

If the position of the item is in the cache then we can reuse a bitmap that has already been created. We set the bitmap in the ImageView. If there are more than 20 items in the cache then we discard the oldest one from the positions queue.

        if self.cache.containsKey(position):
            bitmap = self.cache[position]
            imageView.setImageBitmap(bitmap)
            
            if len(self.positions) > 20:
                self.cache.remove(self.positions.remove())
        
        else:
            # Create a placeholder bitmap to put into the view.
            bitmap = PatternRenderer.emptyBitmap(self.size, self.size)
            bitmap.eraseColor(Color.argb(255, 32, 32, 32))
            imageView.setImageBitmap(bitmap)
            
            # Schedule the rendering process.
            self.scheduleRender(WorkItem(position, imageView))
        
        return imageView

If the position was not in the cache then we create an empty bitmap as a placeholder and set it in the ImageView before calling the scheduleRender method to try and start the pattern rendering process.

Since we either obtain an existing bitmap from the cache or a placeholder that can be displayed while rendering occurs, we return an ImageView immediately. If the bitmap was a placeholder, the view will be updated as the rendering is performed.

In the following method, we obtain the file to read from the underlying list and pass it to a new PatternRenderer instance along with the other information it needs to perform its task.

We try to start the renderer by calling its execute method with the width and height of the bitmap we want as well as the item position it will use to update the cache. This call returns immediately because rendering should occur in a background thread. However, if rendering could not be started, we schedule the class's run method to be called at a later time so that we can try again.

    @args(void, [WorkItem])
    def scheduleRender(self, work):
    
        f = self.items[work.position]
        renderer = PatternRenderer(f, self.colourInfo, work.view,
                                   self.cache, self.positions)
        try:
            # Create a list then convert it to an array. The initial list
            # creation causes the items to be wrapped in Integer objects.
            renderer.execute(array([self.size, self.size, work.position]))
        
        except:
            # The pattern couldn't be rendered immediately. Add this item of
            # work to a queue and schedule an event for later. This will cause
            # the run method to be called.
            self.pending.add(work)
            self.handler.postDelayed(self, long(250)) # 0.25s

This method is called when a pattern render scheduled for later needs to be performed. We simply check that there is at least one item of work in the queue and try to schedule it again.

    def run(self):
    
        if not self.pending.isEmpty():
            work = self.pending.remove()
            self.scheduleRender(work)


class WorkItem(Object):

    __fields__ = {"position": int, "view": ImageView}
    
    @args(void, [int, ImageView])
    def __init__(self, position, view):
    
        Object.__init__(self)
        self.position = position
        self.view = view

Files