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/>.
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
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))
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)
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)
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