~ubuntu-branches/ubuntu/karmic/tovid/karmic

« back to all changes in this revision

Viewing changes to libtovid/render/layer.py

  • Committer: Bazaar Package Importer
  • Author(s): Matvey Kozhev
  • Date: 2008-01-24 22:04:40 UTC
  • Revision ID: james.westby@ubuntu.com-20080124220440-x7cheljduf1rdgnq
Tags: upstream-0.31
ImportĀ upstreamĀ versionĀ 0.31

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/env python
 
2
# layer.py
 
3
 
 
4
"""This module provides a Layer class and several derivatives. A Layer
 
5
is a graphical overlay that may be composited onto an image canvas.
 
6
 
 
7
Run this script standalone for a demonstration:
 
8
 
 
9
    $ python libtovid/layer.py
 
10
 
 
11
Layer subclasses may combine graphical elements, including other Layers,
 
12
into a single interface for drawing and customizing those elements. Layers may
 
13
exhibit animation, through the use of keyframed drawing commands, or through
 
14
use of the Effect class (and its subclasses, as defined in libtovid/effect.py).
 
15
 
 
16
Each Layer subclass provides (at least) an __init__ function, and a draw
 
17
function. For more on how to use Layers, see the Layer class definition and
 
18
template example below.
 
19
"""
 
20
 
 
21
__all__ = [\
 
22
    'Layer',
 
23
    'Background',
 
24
    'Text',
 
25
    'ShadedText',
 
26
    'TextBox',
 
27
    'Label',
 
28
    'VideoClip',
 
29
    'Image',
 
30
    'Thumb',
 
31
    'ThumbGrid',
 
32
    'SafeArea',
 
33
    'Scatterplot',
 
34
    'InterpolationGraph',
 
35
    'ColorBars']
 
36
 
 
37
import os
 
38
import sys
 
39
import math
 
40
import commands
 
41
from libtovid.utils import get_file_type
 
42
from libtovid.render.drawing import Drawing, save_image
 
43
from libtovid.render.effect import Effect
 
44
from libtovid.render.animation import Keyframe, Tween
 
45
from libtovid.media import MediaFile
 
46
from libtovid.transcode import rip
 
47
from libtovid import log
 
48
 
 
49
log.level = 'info'
 
50
 
 
51
class Layer:
 
52
    """A visual element, or a composition of visual elements. Conceptually
 
53
    similar to a layer in the GIMP or Photoshop, with support for animation
 
54
    effects and sub-Layers.
 
55
    """
 
56
    def __init__(self):
 
57
        """Initialize the layer. Extend this in derived classes to accept
 
58
        configuration settings for drawing the layer; call this function
 
59
        from any derived __init__ functions."""
 
60
        self.effects = []
 
61
        self.sublayers = []
 
62
        self._parent_flipbook = None
 
63
        self._parent_layer = None
 
64
 
 
65
    ###
 
66
    ### Child-parent initialization
 
67
    ###
 
68
    def _init_childs(self):
 
69
        """Give access to all descendant layers and effects to their parents.
 
70
 
 
71
        In layers, you can access your parent layer (if sublayed) with:
 
72
            layer._parent_layer
 
73
        and to the top Flipbook object with:
 
74
            layer._parent_flipbook
 
75
        """
 
76
        for x in range(0, len(self.effects)):
 
77
            self.effects[x]._init_parent_flipbook(self._parent_flipbook)
 
78
            self.effects[x]._init_parent_layer(self)
 
79
        for x in range(0, len(self.sublayers)):
 
80
            self.sublayers[x][0]._init_parent_flipbook(self._parent_flipbook)
 
81
            self.sublayers[x][0]._init_parent_layer(self)
 
82
            self.sublayers[x][0]._init_childs(self)
 
83
 
 
84
    def _init_parent_flipbook(self, flipbook):
 
85
        self._parent_flipbook = flipbook
 
86
 
 
87
    def _init_parent_layer(self, layer):
 
88
        self._parent_layer = layer
 
89
 
 
90
 
 
91
    ###
 
92
    ### Derived-class interface
 
93
    ###
 
94
    
 
95
    def draw(self, drawing, frame):
 
96
        """Draw the layer and all sublayers onto the given Drawing. Override
 
97
        this function in derived layers."""
 
98
        assert isinstance(drawing, Drawing)
 
99
 
 
100
    ###
 
101
    ### Sublayer and effect interface
 
102
    ###
 
103
    
 
104
    def add_sublayer(self, layer, position=(0, 0)):
 
105
        """Add the given Layer as a sublayer of this one, at the given position.
 
106
        Sublayers are drawn in the order they are added; each sublayer may have
 
107
        its own effects, but the parent Layer's effects apply to all sublayers.
 
108
        """
 
109
        assert isinstance(layer, Layer)
 
110
        self.sublayers.append((layer, position))
 
111
 
 
112
    def draw_sublayers(self, drawing, frame):
 
113
        """Draw all sublayers onto the given Drawing for the given frame."""
 
114
        assert isinstance(drawing, Drawing)
 
115
        for sublayer, position in self.sublayers:
 
116
            drawing.save()
 
117
            drawing.translate(position)
 
118
            sublayer.draw(drawing, frame)
 
119
            drawing.restore()
 
120
 
 
121
    def add_effect(self, effect):
 
122
        """Add the given Effect to this Layer. A Layer may have multiple effects
 
123
        applied to it; all effects apply to the current layer, and all sublayers.
 
124
        """
 
125
        assert isinstance(effect, Effect)
 
126
        self.effects.append(effect)
 
127
 
 
128
    def draw_with_effects(self, drawing, frame):
 
129
        """Render the entire layer, with all effects applied.
 
130
        
 
131
            drawing: A Drawing object to draw the Layer on
 
132
            frame:   The frame number that is being drawn
 
133
            
 
134
        """
 
135
        # Do preliminary effect rendering
 
136
        for effect in self.effects:
 
137
            effect.pre_draw(drawing, frame)
 
138
        # Draw the layer and sublayers
 
139
        self.draw(drawing, frame)
 
140
        # Close out effect rendering, in reverse (nested) order
 
141
        for effect in reversed(self.effects):
 
142
            effect.post_draw(drawing, frame)
 
143
 
 
144
 
 
145
# ============================================================================
 
146
# Layer template
 
147
# ============================================================================
 
148
# Copy and paste the following code to create your own Layer.
 
149
#
 
150
# Layer subclasses should define two things:
 
151
#
 
152
#     __init__():  How to initialize the layer with parameters
 
153
#     draw():   How do draw the layer on a Drawing
 
154
#
 
155
# First, declare the layer class name. Include (Layer) to indicate that your
 
156
# class is a Layer.
 
157
class MyLayer (Layer):
 
158
    """Overlapping semitransparent rectangles.
 
159
    (Modify this documentation string to describe what's in your layer)"""
 
160
 
 
161
    # Here's the class initialization function, __init__. Define here any
 
162
    # parameters that might be used to configure your layer's behavior or
 
163
    # appearance in some way (along with default values, if you like). Here,
 
164
    # we're allowing configuration of the fill and stroke colors, with default
 
165
    # values of 'blue' and 'black', respectively:
 
166
 
 
167
    def __init__(self, fill_color='blue', stroke_color='black'):
 
168
        """Create a MyLayer with the given fill and stroke colors."""
 
169
        # Initialize the base Layer class. Always do this.
 
170
        Layer.__init__(self)
 
171
        # Store the given colors, for later use
 
172
        self.fill_color = fill_color
 
173
        self.stroke_color = stroke_color
 
174
 
 
175
    # The draw() function is responsible for rendering the contents of the
 
176
    # layer onto a Drawing. It will use the configuration given to __init__
 
177
    # (in this case, fill and stroke colors) to render something onto a Drawing
 
178
    # associated with a particular frame number:
 
179
 
 
180
    def draw(self, drawing, frame):
 
181
        """Draw MyLayer contents onto the given drawing, at the given frame
 
182
        number."""
 
183
 
 
184
        # For safety's sake, make sure you really have a Drawing object:
 
185
        assert isinstance(drawing, Drawing)
 
186
 
 
187
        # Save the drawing context. This prevents any upcoming effects or
 
188
        # style changes from messing up surrounding layers in the Drawing.
 
189
        drawing.save()
 
190
 
 
191
        # Get a Cairo pattern source for the fill and stroke colors
 
192
        # (TODO: Make this easier, or use a simpler example)
 
193
        fc = drawing.create_source(self.fill_color, 0.6)
 
194
        sc = drawing.create_source(self.stroke_color)
 
195
 
 
196
        # And a stroke width of 1, say:
 
197
        drawing.stroke_width(1)
 
198
 
 
199
        # Now, draw something. Here, a couple of pretty semitransparent
 
200
        # rectangles, using the fill and stroke color patterns created earlier:
 
201
        drawing.rectangle(0, 0,  50, 20)
 
202
        drawing.fill(fc)
 
203
        drawing.stroke(sc)
 
204
        drawing.rectangle(15, 12,  45, 28)
 
205
        drawing.fill(fc)
 
206
        drawing.stroke(sc)
 
207
 
 
208
        # Be sure to restore the drawing context afterwards:
 
209
        drawing.restore()
 
210
 
 
211
    # That's it! Your layer is ready to use. See the Demo section at the end of
 
212
    # this file for examples on how to create and render Layers using Python.
 
213
 
 
214
# ============================================================================
 
215
# End of Layer template
 
216
# ============================================================================
 
217
 
 
218
 
 
219
class Background (Layer):
 
220
    """A background that fills the frame with a solid color, or an image."""
 
221
    def __init__(self, color='black', filename=''):
 
222
        Layer.__init__(self)
 
223
        self.color = color
 
224
        self.filename = filename
 
225
        
 
226
    def draw(self, drawing, frame):
 
227
        assert isinstance(drawing, Drawing)
 
228
        log.debug("Drawing Background")
 
229
        width, height = drawing.size
 
230
        drawing.save()
 
231
        # Fill drawing with an image
 
232
        if self.filename is not '':
 
233
            drawing.image(0, 0, width, height, self.filename)
 
234
        # Fill drawing with a solid color
 
235
        elif self.color:
 
236
            drawing.rectangle(0, 0, width, height)
 
237
            drawing.fill(self.color)
 
238
        drawing.restore()
 
239
 
 
240
 
 
241
### --------------------------------------------------------------------
 
242
 
 
243
class Image (Layer):
 
244
    """A rectangular image, scaled to the given size.
 
245
 
 
246
    image_source -- can be anything Drawing::image() can accept.
 
247
                    See documentation in render/drawing.py.
 
248
    """
 
249
    def __init__(self, image_source, (x, y), (width, height)):
 
250
        Layer.__init__(self)
 
251
        self.size = (width, height)
 
252
        self.image_source = image_source
 
253
        self.position = (x, y)
 
254
 
 
255
 
 
256
    def draw(self, drawing, frame=1):
 
257
        assert isinstance(drawing, Drawing)
 
258
        log.debug("Drawing Image")
 
259
        drawing.save()
 
260
        # Save the source for future calls to draw, so no further
 
261
        # processing will be necessary. And other effects can be done
 
262
        # without interferring with the original source.
 
263
        self.image_source = drawing.image(self.position[0], self.position[1],
 
264
                                          self.size[0], self.size[1],
 
265
                                          self.image_source)
 
266
        drawing.restore()
 
267
 
 
268
 
 
269
### --------------------------------------------------------------------
 
270
 
 
271
class VideoClip (Layer):
 
272
    """A rectangular video clip, scaled to the given size.
 
273
 
 
274
    TODO: num_frames should accept a range [first, end], an int (1-INT) and
 
275
    rip frames accordingly. For now, it only accepts an INT for the range 1-INT
 
276
    """
 
277
    def __init__(self, filename, (width, height), position=(0,0), num_frames=120):
 
278
        Layer.__init__(self)
 
279
        self.filename = filename
 
280
        self.mediafile = MediaFile(filename)
 
281
        self.size = (width, height)
 
282
        # List of filenames of individual frames
 
283
        self.frame_files = []
 
284
        # TODO: be able to change hardcoded default values
 
285
        self.rip_frames(1, num_frames)
 
286
        # Set (x,y) position
 
287
        assert(isinstance(position, tuple))
 
288
        self.position = position
 
289
 
 
290
    def rip_frames(self, start, end):
 
291
        """Rip frames from the video file, from start to end frames."""
 
292
        log.info("VideoClip: Ripping frames %s to %s" % (start, end))
 
293
        outdir = '/tmp/%s_frames' % self.filename
 
294
        self.frame_files = rip.rip_frames(self.mediafile, outdir,
 
295
                                          [start, end])
 
296
 
 
297
    def draw(self, drawing, frame=1):
 
298
        """Draw ripped video frames to the given drawing. For now, it's
 
299
        necessary to call rip_frames() before calling this function.
 
300
        
 
301
        Video is looped.
 
302
        """
 
303
        assert isinstance(drawing, Drawing)
 
304
        log.debug("Drawing VideoClip")
 
305
        if len(self.frame_files) == 0:
 
306
            log.error("VideoClip: need to call rip_frames() before drawing.")
 
307
            sys.exit(1)
 
308
        drawing.save()
 
309
        # Loop frames (modular arithmetic)
 
310
        if frame >= len(self.frame_files):
 
311
            frame = frame % len(self.frame_files)
 
312
        filename = self.frame_files[frame-1]
 
313
        drawing.image(self.position, self.size, filename)
 
314
        drawing.restore()
 
315
 
 
316
 
 
317
### --------------------------------------------------------------------
 
318
 
 
319
class Text (Layer):
 
320
    """A simple text string, with size, color and font.
 
321
 
 
322
    text -- UTF8 encoded string.
 
323
    """
 
324
    def __init__(self, text, position=(0, 0), color='white', fontsize=20, \
 
325
                 font='Helvetica', align='left'):
 
326
        Layer.__init__(self)
 
327
        self.text = text
 
328
        self.color = color
 
329
        self.fontsize = fontsize
 
330
        self.font = font
 
331
        self.align = align
 
332
        # Set (x,y) position
 
333
        self.position = position
 
334
 
 
335
    # TODO: This is gonna be pretty broken...
 
336
    def extents(self, drawing):
 
337
        """Return the extents of the text as a (x0, y0, x1, y1) tuple."""
 
338
        assert isinstance(drawing, Drawing)
 
339
        drawing.save()
 
340
        drawing.font(self.font)
 
341
        drawing.font_size(self.fontsize)
 
342
        x_bearing, y_bearing, width, height, x_adv, y_adv = \
 
343
                 drawing.text_extents(self.text)
 
344
        drawing.restore()
 
345
        # Add current layer's position to the (x,y) bearing of extents
 
346
        x0 = int(self.position[0] + x_bearing)
 
347
        y0 = int(self.position[1] + y_bearing)
 
348
        x1 = int(x0 + width)
 
349
        y1 = int(y0 + height)
 
350
        return (x0, y0, x1, y1)
 
351
 
 
352
    def draw(self, drawing, frame=1):
 
353
        assert isinstance(drawing, Drawing)
 
354
        log.debug("Drawing Text")
 
355
        # Drop in debugger
 
356
        drawing.save()
 
357
        drawing.font(self.font)
 
358
        drawing.font_size(self.fontsize)
 
359
        if self.color is not None:
 
360
            drawing.set_source(self.color)
 
361
        # TODO: DO something with the align !!
 
362
        drawing.text(self.text, self.position[0], self.position[1], self.align)
 
363
        drawing.restore()
 
364
 
 
365
 
 
366
### --------------------------------------------------------------------
 
367
 
 
368
class ShadedText (Layer):
 
369
    """A simple text string, with size, color and font.
 
370
 
 
371
    text -- UTF8 encoded string.
 
372
    """
 
373
    def __init__(self, text, position=(0, 0), offset=(5, 5),
 
374
                 color='white', shade_color='gray', fontsize=20,
 
375
                 font='Nimbus Sans', align='left'):
 
376
        Layer.__init__(self)
 
377
        shade_position = (position[0] + offset[0],
 
378
                          position[1] + offset[1])
 
379
        self.under = Text(text, shade_position, shade_color,
 
380
                          fontsize, font, align)
 
381
        self.over = Text(text, position, color, fontsize, font, align)
 
382
 
 
383
    def draw(self, drawing, frame=1):
 
384
        assert isinstance(drawing, Drawing)
 
385
        log.debug("Drawing Text")
 
386
        drawing.save()
 
387
        self.under.draw(drawing, frame)
 
388
        self.over.draw(drawing, frame)
 
389
        drawing.restore()
 
390
 
 
391
 
 
392
### --------------------------------------------------------------------
 
393
 
 
394
class Label (Text):
 
395
    """A text string with a rectangular background.
 
396
 
 
397
    You can access Text's extents() function from within here too."""
 
398
    def __init__(self, text, position=(0,0), color='white', bgcolor='#555',
 
399
                 fontsize=20, font='NimbusSans'):
 
400
        Text.__init__(self, text, position, color, fontsize, font)
 
401
        self.bgcolor = bgcolor
 
402
        # Set (x,y) position
 
403
        assert(isinstance(position, tuple))
 
404
        self.position = position
 
405
 
 
406
    def draw(self, drawing, frame=1):
 
407
        assert isinstance(drawing, Drawing)
 
408
        log.debug("Drawing Label")
 
409
        #(dx, dy, w, h, ax, ay) = self.extents(drawing)
 
410
        (x0, y0, x1, y1) = self.extents(drawing)
 
411
        # Save context
 
412
        drawing.save()
 
413
 
 
414
        # Calculate rectangle dimensions from text size/length
 
415
        width = x1 - x0
 
416
        height = y1 - y0
 
417
        # Padding to use around text
 
418
        pad = self.fontsize / 3
 
419
        # Calculate start and end points of background rectangle
 
420
        start = (-pad, -height - pad)
 
421
        end = (width + pad, pad)
 
422
 
 
423
        # Draw a stroked round rectangle
 
424
        drawing.save()
 
425
        drawing.stroke_width(1)
 
426
        drawing.roundrectangle(start[0], start[1], end[0], end[1], pad, pad)
 
427
        drawing.stroke('black')
 
428
        drawing.fill(self.bgcolor, 0.3)
 
429
        drawing.restore()
 
430
 
 
431
        # Call base Text class to draw the text
 
432
        Text.draw(self, drawing, frame)
 
433
 
 
434
        # Restore context
 
435
        drawing.restore()
 
436
 
 
437
 
 
438
### --------------------------------------------------------------------
 
439
 
 
440
class Thumb (Layer):
 
441
    """A thumbnail image or video."""
 
442
    def __init__(self, filename, (width, height), position=(0,0), title=''):
 
443
        Layer.__init__(self)
 
444
        self.filename = filename
 
445
        self.size = (width, height)
 
446
        self.title = title or os.path.basename(filename)
 
447
        # Set (x,y) position
 
448
        assert(isinstance(position, tuple))
 
449
        self.position = position
 
450
 
 
451
        # Determine whether file is a video or image, and create the
 
452
        # appropriate sublayer
 
453
        filetype = get_file_type(filename)
 
454
        if filetype == 'video':
 
455
            self.add_sublayer(VideoClip(filename, self.size, self.position))
 
456
        elif filetype == 'image':
 
457
            self.add_sublayer(Image(filename, self.size, self.position))
 
458
        self.lbl = Label(self.title, fontsize=15)
 
459
        self.add_sublayer(self.lbl, self.position)
 
460
 
 
461
    def draw(self, drawing, frame=1):
 
462
        assert isinstance(drawing, Drawing)
 
463
        log.debug("Drawing Thumb")
 
464
        drawing.save()
 
465
        (x0, y0, x1, y1) = self.lbl.extents(drawing)
 
466
        drawing.translate(0, x1-x0)
 
467
        self.draw_sublayers(drawing, frame)
 
468
        drawing.restore()
 
469
 
 
470
 
 
471
### --------------------------------------------------------------------
 
472
 
 
473
class ThumbGrid (Layer):
 
474
    """A rectangular array of thumbnail images or videos."""
 
475
    def __init__(self, files, titles=None, (width, height)=(600, 400),
 
476
                 (columns, rows)=(0, 0), aspect=(4,3)):
 
477
        """Create a grid of thumbnail images or videos from a list of files,
 
478
        fitting in a space no larger than the given size, with the given number
 
479
        of columns and rows. Use 0 to auto-layout columns or rows, or both 
 
480
        (default).
 
481
        """
 
482
        assert files != []
 
483
        if titles:
 
484
            assert len(files) == len(titles)
 
485
        else:
 
486
            titles = files
 
487
        Layer.__init__(self)
 
488
        self.size = (width, height)
 
489
        # Auto-dimension (using desired rows/columns, if given)
 
490
        self.columns, self.rows = \
 
491
            self._fit_items(len(files), columns, rows, aspect)
 
492
        # Calculate thumbnail size, keeping aspect
 
493
        w = (width - self.columns * 16) / self.columns
 
494
        h = w * aspect[1] / aspect[0]
 
495
        thumbsize = (w, h)
 
496
 
 
497
        # Calculate thumbnail positions
 
498
        positions = []
 
499
        for row in range(self.rows):
 
500
            for column in range(self.columns):
 
501
                x = column * (width / self.columns)
 
502
                y = row * (height / self.rows)
 
503
                positions.append((x, y))
 
504
 
 
505
        # Add Thumb sublayers
 
506
        for file, title, position in zip(files, titles, positions):
 
507
            title = os.path.basename(file)
 
508
            self.add_sublayer(Thumb(file, thumbsize, (0, 0), title), position)
 
509
 
 
510
    def _fit_items(self, num_items, columns, rows, aspect=(4, 3)):
 
511
        # Both fixed, nothing to calculate
 
512
        if columns > 0 and rows > 0:
 
513
            # Make sure num_items will fit (columns, rows)
 
514
            if num_items < columns * rows:
 
515
                return (columns, rows)
 
516
            # Not enough room; auto-dimension both
 
517
            else:
 
518
                log.warning("ThumbGrid: Can't fit %s items" % num_items +\
 
519
                            " in (%s, %s) grid;" % (columns, rows) +\
 
520
                            " doing auto-dimensioning instead.")
 
521
                columns = rows = 0
 
522
        # Auto-dimension to fit num_items
 
523
        if columns == 0 and rows == 0:
 
524
            # TODO: Take aspect ratio into consideration to find an optimal fit
 
525
            root = int(math.floor(math.sqrt(num_items)))
 
526
            return ((1 + num_items / root), root)
 
527
        # Rows fixed; use enough columns to fit num_items
 
528
        if columns == 0 and rows > 0:
 
529
            return ((1 + num_items / rows), rows)
 
530
        # Columns fixed; use enough rows to fit num_items
 
531
        if rows == 0 and columns > 0:
 
532
            return (columns, (1 + num_items / columns))
 
533
 
 
534
    def draw(self, drawing, frame=1):
 
535
        assert isinstance(drawing, Drawing)
 
536
        log.debug("Drawing ThumbGrid")
 
537
        drawing.save()
 
538
        self.draw_sublayers(drawing, frame)
 
539
        drawing.restore()
 
540
 
 
541
 
 
542
### --------------------------------------------------------------------
 
543
 
 
544
class SafeArea (Layer):
 
545
    """Render a safe area box at a given percentage.
 
546
    """
 
547
    def __init__(self, percent, color):
 
548
        self.percent = percent
 
549
        self.color = color
 
550
        
 
551
    def draw(self, drawing, frame=1):
 
552
        assert isinstance(drawing, Drawing)
 
553
        log.debug("Drawing SafeArea")
 
554
        # Calculate rectangle dimensions
 
555
        scale = float(self.percent) / 100.0
 
556
        width, height = drawing.size
 
557
        topleft = ((1.0 - scale) * width / 2,
 
558
                  (1.0 - scale) * height / 2)
 
559
        # Save context
 
560
        drawing.save()
 
561
        drawing.translate(topleft[0], topleft[1])
 
562
        # Safe area box
 
563
        drawing.stroke_width(3)
 
564
        drawing.rectangle(0, 0,  width * scale, height * scale)
 
565
        drawing.stroke(self.color)
 
566
        # Label
 
567
        drawing.font_size(18)
 
568
        drawing.set_source(self.color)
 
569
        drawing.text(u"%s%%" % self.percent, 10, 20)
 
570
        # Restore context
 
571
        drawing.restore()
 
572
 
 
573
 
 
574
### --------------------------------------------------------------------
 
575
 
 
576
class Scatterplot (Layer):
 
577
    """A 2D scatterplot of data.
 
578
 
 
579
    Untested since MVG move.
 
580
    """
 
581
    def __init__(self, xy_dict, width=240, height=80, x_label='', y_label=''):
 
582
        """Create a scatterplot using data in xy_dict, a dictionary of
 
583
        lists of y-values, indexed by x-value."""
 
584
        self.xy_dict = xy_dict
 
585
        self.width, self.height = (width, height)
 
586
        self.x_label = x_label
 
587
        self.y_label = y_label
 
588
 
 
589
    def draw(self, drawing, frame):
 
590
        """Draw the scatterplot."""
 
591
        assert isinstance(drawing, Drawing)
 
592
        log.debug("Drawing Scatterplot")
 
593
        width, height = (self.width, self.height)
 
594
        x_vals = self.xy_dict.keys()
 
595
        max_y = 0
 
596
        for x in x_vals:
 
597
            largest = max(self.xy_dict[x] or [0])
 
598
            if largest > max_y:
 
599
                max_y = largest
 
600
        # For numeric x, scale by maximum x-value
 
601
        x_is_num = isinstance(x_vals[0], int) or isinstance(x_vals[0], float)
 
602
        if x_is_num:
 
603
            x_scale = float(width) / max(x_vals)
 
604
        # For string x, scale by number of x-values
 
605
        else:
 
606
            x_scale = float(width) / len(x_vals)
 
607
        # Scale y according to maximum value
 
608
        y_scale = float(height) / max_y
 
609
 
 
610
        # Save context
 
611
        drawing.save()
 
612
        drawing.rectangle(0, 0, width, height)
 
613
        drawing.fill('white', 0.75)
 
614
        
 
615
        # Draw axes
 
616
        #->comment("Axes of scatterplot")
 
617
        drawing.save()
 
618
        drawing.stroke_width(2)
 
619
        drawing.line(0, 0, 0, height)
 
620
        drawing.stroke('black')
 
621
        drawing.line(0, height, width, height)
 
622
        drawing.stroke('black')
 
623
        drawing.restore()
 
624
 
 
625
        # Axis labels
 
626
        drawing.save()
 
627
        drawing.set_source('blue')
 
628
 
 
629
        drawing.save()
 
630
        for i, x in enumerate(x_vals):
 
631
            drawing.save()
 
632
            if x_is_num:
 
633
                drawing.translate(x * x_scale, height + 15)
 
634
            else:
 
635
                drawing.translate(i * x_scale, height + 15)
 
636
            drawing.rotate(30)
 
637
            drawing.text(x, 0, 0)
 
638
            drawing.restore()
 
639
 
 
640
        drawing.font_size(20)
 
641
        drawing.text(self.x_label, width/2, height+40)
 
642
        drawing.restore()
 
643
 
 
644
        drawing.save()
 
645
        drawing.text(max_y, -30, 0)
 
646
        drawing.translate(-25, height/2)
 
647
        drawing.rotate(90)
 
648
        drawing.text(self.y_label, 0, 0)
 
649
        drawing.restore()
 
650
 
 
651
        drawing.restore()
 
652
 
 
653
        # Plot all y-values for each x (as small circles)
 
654
        #->comment("Scatterplot data")
 
655
        drawing.save()
 
656
        for i, x in enumerate(x_vals):
 
657
            if x_is_num:
 
658
                x_coord = x * x_scale
 
659
            else:
 
660
                x_coord = i * x_scale
 
661
            # Shift x over slightly
 
662
            x_coord += 10
 
663
            # Plot all y-values for this x
 
664
            for y in self.xy_dict[x]:
 
665
                y_coord = height - y * y_scale
 
666
                drawing.circle(x_coord, y_coord, 3)
 
667
                drawing.fill('red', 0.2)
 
668
        drawing.restore()
 
669
        
 
670
        # Restore context
 
671
        drawing.restore()
 
672
 
 
673
 
 
674
### --------------------------------------------------------------------
 
675
 
 
676
class InterpolationGraph (Layer):
 
677
    # TODO: Support graphing of tuple data
 
678
    """A graph of an interpolation curve, defined by a list of Keyframes and
 
679
    an interpolation method."""
 
680
    def __init__(self, keyframes, size=(240, 80), method='linear'):
 
681
        """Create an interpolation graph of the given keyframes, at the given
 
682
        size, using the given interpolation method."""
 
683
        Layer.__init__(self)
 
684
                
 
685
        self.keyframes = keyframes
 
686
        self.size = size
 
687
        self.method = method
 
688
        # Interpolate keyframes
 
689
        self.tween = Tween(keyframes, method)
 
690
 
 
691
    def draw(self, drawing, frame):
 
692
        """Draw the interpolation graph, including frame/value axes,
 
693
        keyframes, and the interpolation curve."""
 
694
        assert isinstance(drawing, Drawing)
 
695
        log.debug("Drawing InterpolationGraph")
 
696
        data = self.tween.data
 
697
        # Calculate maximum extents of the graph
 
698
        width, height = self.size
 
699
        x_scale = float(width) / len(data)
 
700
        y_scale = float(height) / max(data)
 
701
 
 
702
        #->drawing.comment("InterpolationGraph Layer")
 
703
 
 
704
        # Save context
 
705
        drawing.save()
 
706
 
 
707
        # Draw axes
 
708
        #->drawing.comment("Axes of graph")
 
709
        drawing.save()
 
710
        drawing.stroke_width(3)
 
711
        drawing.polyline([(0, 0), (0, height), (width, height)], False)
 
712
        drawing.stroke('#ccc')
 
713
        drawing.restore()
 
714
 
 
715
        # Create a list of (x, y) points to be graphed
 
716
        curve = []
 
717
        x = 1
 
718
        while x <= len(self.tween.data):
 
719
            # y increases downwards; subtract from height to give a standard
 
720
            # Cartesian-oriented graph (so y increases upwards)
 
721
            point = (int(x * x_scale), int(height - data[x-1] * y_scale))
 
722
            curve.append(point)
 
723
            x += 1
 
724
        drawing.save()
 
725
        # Draw the curve
 
726
        drawing.stroke_width(2)
 
727
        drawing.polyline(curve, False)
 
728
        drawing.stroke('blue')
 
729
        drawing.restore()
 
730
 
 
731
        # Draw Keyframes as dotted vertical lines
 
732
        drawing.save()
 
733
        # Vertical dotted lines
 
734
        drawing.set_source('red')
 
735
        drawing.stroke_width(2)
 
736
        for key in self.keyframes:
 
737
            x = int(key.frame * x_scale)
 
738
            drawing.line(x, 0,   x, height)
 
739
            drawing.stroke('red')
 
740
            
 
741
        # Draw Keyframe labels
 
742
        drawing.set_source('white')
 
743
        for key in self.keyframes:
 
744
            x = int(key.frame * x_scale)
 
745
            y = int(height - key.data * y_scale - 3)
 
746
            drawing.text(u"(%s,%s)" % (key.frame, key.data), x, y)
 
747
 
 
748
        drawing.restore()
 
749
 
 
750
        # Draw a yellow dot for current frame
 
751
        #->drawing.comment("Current frame marker")
 
752
        drawing.save()
 
753
        pos = (frame * x_scale, height - data[frame-1] * y_scale)
 
754
        drawing.circle(pos[0], pos[1], 2)
 
755
        drawing.fill('yellow')
 
756
        drawing.restore()
 
757
 
 
758
        # Restore context
 
759
        drawing.restore()
 
760
 
 
761
 
 
762
### --------------------------------------------------------------------
 
763
 
 
764
class ColorBars (Layer):
 
765
    """Standard SMPTE color bars
 
766
    (http://en.wikipedia.org/wiki/SMPTE_color_bars)
 
767
    """
 
768
    def __init__(self, size, position=(0,0)):
 
769
        """Create color bars in a region of the given size and position.
 
770
        """
 
771
        Layer.__init__(self)
 
772
        self.size = size
 
773
        # Set (x,y) position
 
774
        assert(isinstance(position, tuple))
 
775
        self.position = position
 
776
 
 
777
    def draw(self, drawing, frame=1):
 
778
        assert isinstance(drawing, Drawing)
 
779
        log.debug("Drawing ColorBars")
 
780
        x, y = self.position
 
781
        width, height = self.size
 
782
 
 
783
        drawing.save()
 
784
        # Get to the right place and size
 
785
        drawing.translate(x, y)
 
786
        drawing.scale(width, height)
 
787
        # Video-black background
 
788
        drawing.rectangle(0, 0, 1, 1)
 
789
        drawing.fill('rgb(16, 16, 16)')
 
790
 
 
791
        # Top 67% of picture: Color bars at 75% amplitude
 
792
        top = 0
 
793
        bottom = 2.0 / 3
 
794
        seventh = 1.0 / 7
 
795
        size = (seventh, bottom)
 
796
        bars = [(0, top, seventh, bottom, 'rgb(191, 191, 191)'),
 
797
                (seventh, top, seventh, bottom, 'rgb(191, 191, 0)'),
 
798
                (2*seventh, top, seventh, bottom, 'rgb(0, 191, 191)'),
 
799
                (3*seventh, top, seventh, bottom, 'rgb(0, 191, 0)'),
 
800
                (4*seventh, top, seventh, bottom, 'rgb(191, 0, 191)'),
 
801
                (5*seventh, top, seventh, bottom, 'rgb(191, 0, 0)'),
 
802
                (6*seventh, top, seventh, bottom, 'rgb(0, 0, 191)')]
 
803
 
 
804
        # Next 8% of picture: Reverse blue bars
 
805
        top = bottom
 
806
        bottom = 0.75
 
807
        height = bottom - top
 
808
        bars.extend([(0, top, seventh, height, 'rgb(0, 0, 191)'),
 
809
                (seventh, top, seventh, height, 'rgb(16, 16, 16)'),
 
810
                (2*seventh, top, seventh, height, 'rgb(191, 0, 191)'),
 
811
                (3*seventh, top, seventh, height, 'rgb(16, 16, 16)'),
 
812
                (4*seventh, top, seventh, height, 'rgb(0, 191, 191)'),
 
813
                (5*seventh, top, seventh, height, 'rgb(16, 16, 16)'),
 
814
                (6*seventh, top, seventh, height, 'rgb(191, 191, 191)')])
 
815
 
 
816
        # Lower 25%: Pluge signal
 
817
        top = bottom
 
818
        bottom = 1.0
 
819
        sixth = 1.0 / 6
 
820
        height = bottom - top
 
821
        bars.extend([(0, top, 1.0, height, 'rgb(16, 16, 16)'),
 
822
                (0, top, sixth, height, 'rgb(0, 29, 66)'),
 
823
                (sixth, top, sixth, height, 'rgb(255, 255, 255)'),
 
824
                (2*sixth, top, sixth, height, 'rgb(44, 0, 92)'),
 
825
                # Sub- and super- black narrow bars
 
826
                (4*sixth, top, 0.33*sixth, height, 'rgb(7, 7, 7)'),
 
827
                (4.33*sixth, top, 0.33*sixth, height,'rgb(16, 16, 16)'),
 
828
                (4.66*sixth, top, 0.33*sixth, height, 'rgb(24, 24, 24)')])
 
829
 
 
830
        # Draw and fill all bars
 
831
        for x, y, width, height, color in bars:
 
832
            drawing.rectangle(x, y, width, height)
 
833
            drawing.fill(color)
 
834
 
 
835
        drawing.restore()
 
836
 
 
837
 
 
838
 
 
839
if __name__ == '__main__':
 
840
    images = None
 
841
    # Get arguments, if any
 
842
    if len(sys.argv) > 1:
 
843
        # Use all args as image filenames to ThumbGrid
 
844
        images = sys.argv[1:]
 
845
 
 
846
    # A Drawing to render Layer demos to
 
847
    drawing = Drawing(800, 600)
 
848
    
 
849
    # Draw a background layer
 
850
    bgd = Background(color='#7080A0')
 
851
    bgd.draw(drawing, 1)
 
852
 
 
853
    # Draw color bars
 
854
    bars = ColorBars((320, 240), (400, 100))
 
855
    bars.draw(drawing, 1)
 
856
 
 
857
    # Draw a label
 
858
    drawing.save()
 
859
    drawing.translate(460, 200)
 
860
    label = Label("tovid loves Linux")
 
861
    label.draw(drawing, 1)
 
862
    drawing.restore()
 
863
 
 
864
    # Draw a text layer, with position.
 
865
    text = Text("Jackdaws love my big sphinx of quartz",
 
866
                (82, 62), '#bbb')
 
867
    text.draw(drawing, 1)
 
868
 
 
869
    # Draw a text layer
 
870
    drawing.save()
 
871
    drawing.translate(80, 60)
 
872
    text = Text("Jackdaws love my big sphinx of quartz")
 
873
    text.draw(drawing, 1)
 
874
    drawing.restore()
 
875
 
 
876
    # Draw a template layer (overlapping semitransparent rectangles)
 
877
    template = MyLayer('white', 'darkblue')
 
878
    # Scale and translate the layer before drawing it
 
879
    drawing.save()
 
880
    drawing.translate(50, 100)
 
881
    drawing.scale(3.0, 3.0)
 
882
    template.draw(drawing, 1)
 
883
    drawing.restore()
 
884
 
 
885
    # Draw a safe area test (experimental)
 
886
    safe = SafeArea(93, 'yellow')
 
887
    safe.draw(drawing, 1)
 
888
 
 
889
    # Draw a thumbnail grid (if images were provided)
 
890
    if images:
 
891
        drawing.save()
 
892
        drawing.translate(350, 300)
 
893
        thumbs = ThumbGrid(images, (320, 250))
 
894
        thumbs.draw(drawing, 1)
 
895
        drawing.restore()
 
896
 
 
897
    # Draw an interpolation graph
 
898
    drawing.save()
 
899
    drawing.translate(60, 400)
 
900
    # Some random keyframes to graph
 
901
    keys = [Keyframe(1, 25), Keyframe(10, 5), Keyframe(30, 35),
 
902
            Keyframe(40, 35), Keyframe(45, 20), Keyframe(60, 40)]
 
903
    interp = InterpolationGraph(keys, (400, 120), method="cosine")
 
904
    interp.draw(drawing, 25)
 
905
    drawing.restore()
 
906
 
 
907
    # Draw a scatterplot
 
908
    drawing.save()
 
909
    xy_data = {
 
910
        5: [2, 4, 6, 8],
 
911
        10: [3, 5, 7, 9],
 
912
        15: [5, 9, 13, 17],
 
913
        20: [8, 14, 20, 26]}
 
914
    drawing.translate(550, 350)
 
915
    #drawing.scale(200, 200)
 
916
    plot = Scatterplot(xy_data, 200, 200, "Spam", "Eggs")
 
917
    plot.draw(drawing, 1)
 
918
    drawing.restore()
 
919
    
 
920
    log.info("Output to /tmp/my.png")
 
921
    save_image(drawing, '/tmp/my.png', 800, 600)