PySSTV 0.5.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env python
2
+ # -*- encoding: utf-8 -*-
3
+
4
+ # copy to ~/.gimp-2.8/plug-ins/
5
+ # dependencies: GIMP 2.8, python-imaging-tk, python-pyaudio
6
+
7
+ from gimpfu import register, main, pdb, PF_BOOL, PF_STRING, PF_RADIO, CLIP_TO_IMAGE
8
+ from PIL import Image, ImageTk
9
+ from tkinter import Tk, Canvas, Button, Checkbutton, IntVar, Frame, LEFT, NW
10
+ from pysstv import __main__ as pysstv_main
11
+ from pysstv.examples.pyaudio_sstv import PyAudioSSTV
12
+ from pysstv.sstv import SSTV
13
+ from itertools import repeat
14
+ from threading import Thread
15
+ from queue import Queue, Empty
16
+ from time import sleep
17
+ import gimp, os
18
+
19
+ MODULE_MAP = pysstv_main.build_module_map()
20
+
21
+ class AudioThread(Thread):
22
+ def __init__(self, sstv, parent):
23
+ Thread.__init__(self)
24
+ self.pas = PyAudioSSTV(sstv)
25
+ self.parent = parent
26
+
27
+ def run(self):
28
+ self.pas.execute()
29
+ self.parent.audio_thread_ended()
30
+
31
+ def stop(self):
32
+ if self.pas is not None:
33
+ self.pas.sampler = []
34
+ self.pas = None
35
+
36
+
37
+ class Sine1750(SSTV):
38
+ encode_line = None
39
+
40
+ def gen_freq_bits(self):
41
+ return repeat((1750, 1000))
42
+
43
+
44
+ class Transmitter(object):
45
+ def __init__(self, sstv, root, progress, set_ptt_pin, ptt_state):
46
+ def encode_line_hooked(line):
47
+ progress.update_image(line)
48
+ return self.original_encode_line(line)
49
+ self.progress = progress
50
+ self.sstv = sstv
51
+ self.original_encode_line = sstv.encode_line
52
+ sstv.encode_line = encode_line_hooked
53
+ self.root = root
54
+ self.tx_enabled = IntVar()
55
+ self.audio_thread = None
56
+ self.stopping = False
57
+ self.set_ptt_pin = set_ptt_pin
58
+ self.ptt_state = ptt_state
59
+
60
+ def set_ptt(self, state):
61
+ if self.set_ptt_pin is None:
62
+ return
63
+ if not state:
64
+ sleep(0.2)
65
+ self.set_ptt_pin(state != self.ptt_state)
66
+ if state:
67
+ sleep(0.2)
68
+
69
+ def start_stop_tx(self):
70
+ if self.tx_enabled.get():
71
+ self.stopping = False
72
+ self.audio_thread = AudioThread(self.sstv, self)
73
+ self.set_ptt(True)
74
+ self.audio_thread.start()
75
+ else:
76
+ self.stop()
77
+ if self.progress is not None:
78
+ self.progress.update_image()
79
+
80
+ def stop(self):
81
+ if self.audio_thread is not None:
82
+ self.stopping = True
83
+ self.audio_thread.stop()
84
+ self.set_ptt(False)
85
+
86
+ def audio_thread_ended(self):
87
+ if not self.stopping:
88
+ self.set_ptt(False)
89
+ self.tx_enabled.set(0)
90
+
91
+ def close(self):
92
+ self.root.destroy()
93
+
94
+
95
+ class CanvasUpdater(Thread):
96
+ def __init__(self, progress):
97
+ Thread.__init__(self)
98
+ self.progress = progress
99
+ self.queue = Queue()
100
+ self.should_run = True
101
+
102
+ def stop(self):
103
+ self.should_run = False
104
+
105
+ def update_image(self, line=None):
106
+ self.queue.put(line)
107
+
108
+ def run(self):
109
+ while self.should_run:
110
+ try:
111
+ self.progress.update_image(self.queue.get(timeout=0.5))
112
+ except Empty:
113
+ pass
114
+
115
+
116
+ class ProgressCanvas(Canvas):
117
+ def __init__(self, master, image):
118
+ self.height_ratio = 1
119
+ width, height = image.size
120
+ pixels = image.load()
121
+ RED, GREEN, BLUE = list(range(3))
122
+ self.colors = ['#{0:02x}{1:02x}{2:02x}'.format(
123
+ contrast(sum(pixels[x, y][RED] for x in range(width)) / width),
124
+ contrast(sum(pixels[x, y][GREEN] for x in range(width)) / width),
125
+ contrast(sum(pixels[x, y][BLUE] for x in range(width)) / width))
126
+ for y in range(height)]
127
+ if height / float(width) > 1.5:
128
+ width *= 2
129
+ elif width < 200:
130
+ width *= 2
131
+ height *= 2
132
+ self.height_ratio = 2
133
+ if (width, height) != image.size:
134
+ image = image.resize((width, height))
135
+ Canvas.__init__(self, master, width=width, height=height)
136
+ self.tk_img = ImageTk.PhotoImage(image)
137
+ self.update_image()
138
+ self.pack()
139
+
140
+ def update_image(self, line=None):
141
+ image = self.tk_img
142
+ self.create_image(0, 0, anchor=NW, image=image)
143
+ if line is not None:
144
+ fill = self.colors[line]
145
+ line *= self.height_ratio
146
+ self.create_line(0, line, image.width(), line, fill=fill)
147
+
148
+
149
+ def contrast(value):
150
+ if 80 < value < 160:
151
+ return value + (80 if value < 120 else -80)
152
+ else:
153
+ return 255 - value
154
+
155
+ def transmit_current_image(image, drawable, mode, vox, fskid, ptt_port, ptt_pin, ptt_state):
156
+ sstv = MODULE_MAP[mode]
157
+ if ptt_port is not None:
158
+ from serial import Serial
159
+ set_ptt_pin = getattr(Serial(ptt_port), 'set' + ptt_pin)
160
+ set_ptt_pin(ptt_state)
161
+ else:
162
+ set_ptt_pin = None
163
+ pil_img = match_image_with_sstv_mode(image_gimp_to_pil(image), sstv)
164
+ root = Tk()
165
+ cu = CanvasUpdater(ProgressCanvas(root, pil_img))
166
+ cu.start()
167
+ tm = Transmitter(init_sstv(sstv, pil_img, vox, fskid), root, cu, set_ptt_pin, ptt_state)
168
+ tm1750 = Transmitter(Sine1750(None, 44100, 16), None, None, set_ptt_pin, ptt_state)
169
+ buttons = Frame(root)
170
+ for text, tram in (('TX', tm), ('1750 Hz', tm1750)):
171
+ Checkbutton(buttons, text=text, indicatoron=False, padx=5, pady=5,
172
+ variable=tram.tx_enabled, command=tram.start_stop_tx).pack(side=LEFT)
173
+ Button(buttons, text="Close", command=tm.close).pack(side=LEFT)
174
+ buttons.pack()
175
+ root.mainloop()
176
+ for obj in (tm, tm1750, cu):
177
+ obj.stop()
178
+
179
+ def image_gimp_to_pil(image):
180
+ try:
181
+ sandbox = image.duplicate()
182
+ for layer in sandbox.layers:
183
+ if not layer.visible:
184
+ sandbox.remove_layer(layer)
185
+ sandbox.merge_visible_layers(CLIP_TO_IMAGE)
186
+ layer = sandbox.layers[0]
187
+ if not layer.is_rgb:
188
+ pdb.gimp_image_convert_rgb(sandbox)
189
+ if layer.has_alpha:
190
+ pdb.gimp_layer_flatten(layer)
191
+ w, h = layer.width, layer.height
192
+ pixels = layer.get_pixel_rgn(0, 0, w, h)[:, :] # all pixels
193
+ return Image.frombuffer('RGB', (w, h), pixels, 'raw', 'RGB', 0, 0)
194
+ finally:
195
+ gimp.delete(sandbox)
196
+
197
+ def match_image_with_sstv_mode(image, mode):
198
+ mode_size = mode.WIDTH, mode.HEIGHT
199
+ if image.size != mode_size:
200
+ image = image.resize(mode_size, Image.ANTIALIAS)
201
+ if 'grayscale' in mode.__module__:
202
+ image = image.convert('LA').convert('RGB')
203
+ return image
204
+
205
+ def init_sstv(mode, image, vox, fskid):
206
+ s = mode(image, 44100, 16)
207
+ s.vox_enabled = vox
208
+ if fskid:
209
+ s.add_fskid_text(fskid)
210
+ return s
211
+
212
+ def get_serial_ports():
213
+ try:
214
+ if os.name == 'nt':
215
+ from serial.tools.list_ports_windows import comports
216
+ elif os.name == 'posix':
217
+ from serial.tools.list_ports_posix import comports
218
+ else:
219
+ raise ImportError("Sorry: no implementation for your"
220
+ "platform ('%s') available" % (os.name,))
221
+ except ImportError:
222
+ yield "(couldn't import PySerial)", None
223
+ else:
224
+ yield "(disabled)", None
225
+ for port, desc, _ in comports():
226
+ yield '{0} ({1})'.format(port, desc.strip()), port
227
+
228
+
229
+ register(
230
+ "pysstv_for_gimp",
231
+ "PySSTV for GIMP",
232
+ "Transmits the current image using PySSTV",
233
+ "Andras Veres-Szentkiralyi",
234
+ "Andras Veres-Szentkiralyi",
235
+ "November 2013",
236
+ "<Image>/PySSTV/Transmit...",
237
+ "*",
238
+ [
239
+ (PF_RADIO, "mode", "SSTV mode", "MartinM1",
240
+ tuple((n, n) for n in sorted(MODULE_MAP.keys()))),
241
+ (PF_BOOL, "vox", "Include VOX tones", True),
242
+ (PF_STRING, "fskid", "FSK ID", ""),
243
+ (PF_RADIO, "ptt_port", "PTT port", None,
244
+ tuple(get_serial_ports())),
245
+ (PF_RADIO, "ptt_pin", "PTT pin", "RTS",
246
+ tuple((n, n) for n in ("RTS", "DTR"))),
247
+ (PF_BOOL, "ptt_state", "PTT inversion", False),
248
+ ],
249
+ [],
250
+ transmit_current_image
251
+ )
252
+
253
+ main()
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env python
2
+
3
+ """
4
+ Demonstrates adding text overlay (callsign, RSV, etc.) using PIL
5
+ See example output received by slowrx at the following URL:
6
+ http://vsza.hu/c6fa52b2c7b20320bdab2da15877f0efbd466e67d37c8d124a557367de9380da.png
7
+ """
8
+
9
+ from PIL import Image, ImageFont, ImageDraw
10
+ from pysstv.grayscale import Robot8BW
11
+
12
+ img = Image.open("160x120bw.png")
13
+ font = ImageFont.load_default()
14
+ draw = ImageDraw.Draw(img)
15
+ draw.text((0, 0), "HA5VSA", (255,255,255), font=font)
16
+ sstv = Robot8BW(img, 44100, 16)
17
+ sstv.write_wav("overlay.wav")
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env python
2
+
3
+ """
4
+ Demonstrates playing the generated samples directly using PyAudio
5
+ Tested on PyAudio 0.2.7 http://people.csail.mit.edu/hubert/pyaudio/
6
+ """
7
+
8
+
9
+ from pysstv.sstv import SSTV
10
+ from time import sleep
11
+ from itertools import islice
12
+ import struct, pyaudio
13
+
14
+ class PyAudioSSTV(object):
15
+ def __init__(self, sstv):
16
+ self.pa = pyaudio.PyAudio()
17
+ self.sstv = sstv
18
+ self.fmt = '<' + SSTV.BITS_TO_STRUCT[sstv.bits]
19
+
20
+ def __del__(self):
21
+ self.pa.terminate()
22
+
23
+ def execute(self):
24
+ self.sampler = self.sstv.gen_samples()
25
+ stream = self.pa.open(
26
+ format=self.pa.get_format_from_width(self.sstv.bits // 8),
27
+ channels=1, rate=self.sstv.samples_per_sec, output=True,
28
+ stream_callback=self.callback)
29
+ stream.start_stream()
30
+ while stream.is_active():
31
+ sleep(0.5)
32
+ stream.stop_stream()
33
+ stream.close()
34
+
35
+ def callback(self, in_data, frame_count, time_info, status):
36
+ frames = ''.join(struct.pack(self.fmt, b)
37
+ for b in islice(self.sampler, frame_count))
38
+ return frames, pyaudio.paContinue
39
+
40
+
41
+ def main():
42
+ from PIL import Image
43
+ from pysstv.grayscale import Robot8BW
44
+
45
+ img = Image.open("160x120bw.png")
46
+ sstv = Robot8BW(img, 44100, 16)
47
+ sstv.vox_enabled = True
48
+ PyAudioSSTV(sstv).execute()
49
+
50
+ if __name__ == '__main__':
51
+ main()
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env python
2
+
3
+ """
4
+ Simple repeater that monitors a single directory using inotify, and if
5
+ an image appears, it tries to repeat it on using the PyAudio example,
6
+ trying to match the mode used for receiving it. It can be tested by
7
+ simply copying/linking images to the directory or suing an SSTV
8
+ receiver such as slowrx or QSSTV.
9
+ """
10
+
11
+
12
+ from pyinotify import WatchManager, Notifier, ProcessEvent, IN_CREATE
13
+ from .pyaudio_sstv import PyAudioSSTV
14
+ from pysstv.color import MartinM1, MartinM2, ScottieS1, ScottieS2
15
+ from pysstv.grayscale import Robot8BW, Robot24BW
16
+ from PIL import Image
17
+ from os import path
18
+
19
+ # matches the abbreviations used by slowrx and QSSTV
20
+ MODE_MAP = {
21
+ 'M1': MartinM1,
22
+ 'M2': MartinM2,
23
+ 'S1': ScottieS1,
24
+ 'S2': ScottieS2,
25
+ 'R8BW': Robot8BW,
26
+ 'R24BW': Robot24BW,
27
+ }
28
+
29
+ class EventHandler(ProcessEvent):
30
+ def process_IN_CREATE(self, event):
31
+ filename = event.pathname
32
+ print('Found image', filename)
33
+ mode = get_module_for_filename(filename)
34
+ img = Image.open(filename)
35
+ if mode is None:
36
+ mode = get_module_for_image(img)
37
+ if mode is None:
38
+ print('No suitable mode found to repeat', filename)
39
+ return
40
+ print('Repeating image using', mode.__name__)
41
+ sstv = mode(img, 44100, 16)
42
+ sstv.vox_enabled = True
43
+ PyAudioSSTV(sstv).execute()
44
+
45
+ def get_module_for_filename(filename):
46
+ basename, _ = path.splitext(path.basename(filename))
47
+ for mode, module in MODE_MAP.items():
48
+ if mode in basename:
49
+ return module
50
+
51
+ def get_module_for_image(image):
52
+ size = image.size
53
+ for mode in MODE_MAP.values():
54
+ if all(i >= m for i, m in zip(size, (mode.WIDTH, mode.HEIGHT))):
55
+ return mode
56
+
57
+ def main():
58
+ from sys import argv, stderr
59
+ try:
60
+ directory = argv[1]
61
+ except IndexError:
62
+ print("Usage: {0} <directory>".format(argv[0]), file=stderr)
63
+ else:
64
+ watch(directory)
65
+
66
+ def watch(directory):
67
+ wm = WatchManager()
68
+ notifier = Notifier(wm, EventHandler())
69
+ wm.add_watch(directory, IN_CREATE)
70
+ notifier.loop()
71
+
72
+ if __name__ == '__main__':
73
+ main()
pysstv/grayscale.py ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env python
2
+
3
+ from __future__ import division
4
+ from pysstv.sstv import SSTV, byte_to_freq
5
+
6
+
7
+ class GrayscaleSSTV(SSTV):
8
+ def on_init(self):
9
+ self.pixels = self.image.convert('LA').load()
10
+
11
+ def gen_image_tuples(self):
12
+ for line in range(self.HEIGHT):
13
+ yield from self.horizontal_sync()
14
+ yield from self.encode_line(line)
15
+
16
+ def encode_line(self, line):
17
+ msec_pixel = self.SCAN / self.WIDTH
18
+ image = self.pixels
19
+ for col in range(self.WIDTH):
20
+ pixel = image[col, line]
21
+ freq_pixel = byte_to_freq(pixel[0])
22
+ yield freq_pixel, msec_pixel
23
+
24
+
25
+ class Robot8BW(GrayscaleSSTV):
26
+ VIS_CODE = 0x02
27
+ WIDTH = 160
28
+ HEIGHT = 120
29
+ SYNC = 7
30
+ SCAN = 60
31
+
32
+
33
+ class Robot24BW(GrayscaleSSTV):
34
+ VIS_CODE = 0x0A
35
+ WIDTH = 320
36
+ HEIGHT = 240
37
+ SYNC = 7
38
+ SCAN = 93
39
+
40
+ MODES = (Robot8BW, Robot24BW)
pysstv/sstv.py ADDED
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env python
2
+
3
+ from __future__ import division, with_statement
4
+ from math import sin, pi
5
+ from random import random
6
+ from contextlib import closing
7
+ from itertools import cycle, chain
8
+ from array import array
9
+ import wave
10
+
11
+ FREQ_VIS_BIT1 = 1100
12
+ FREQ_SYNC = 1200
13
+ FREQ_VIS_BIT0 = 1300
14
+ FREQ_BLACK = 1500
15
+ FREQ_VIS_START = 1900
16
+ FREQ_WHITE = 2300
17
+ FREQ_RANGE = FREQ_WHITE - FREQ_BLACK
18
+ FREQ_FSKID_BIT1 = 1900
19
+ FREQ_FSKID_BIT0 = 2100
20
+
21
+ MSEC_VIS_START = 300
22
+ MSEC_VIS_SYNC = 10
23
+ MSEC_VIS_BIT = 30
24
+ MSEC_FSKID_BIT = 22
25
+
26
+
27
+ class SSTV(object):
28
+
29
+ def __init__(self, image, samples_per_sec, bits):
30
+ self.image = image
31
+ self.samples_per_sec = samples_per_sec
32
+ self.bits = bits
33
+ self.vox_enabled = False
34
+ self.fskid_payload = ''
35
+ self.nchannels = 1
36
+ self.on_init()
37
+
38
+ def on_init(self):
39
+ pass
40
+
41
+ BITS_TO_STRUCT = {8: 'b', 16: 'h'}
42
+
43
+ def write_wav(self, filename):
44
+ """writes the whole image to a Microsoft WAV file"""
45
+ fmt = self.BITS_TO_STRUCT[self.bits]
46
+ data = array(fmt, self.gen_samples())
47
+ if self.nchannels != 1:
48
+ data = array(fmt, chain.from_iterable(
49
+ zip(*([data] * self.nchannels))))
50
+ with closing(wave.open(filename, 'wb')) as wav:
51
+ wav.setnchannels(self.nchannels)
52
+ wav.setsampwidth(self.bits // 8)
53
+ wav.setframerate(self.samples_per_sec)
54
+ wav.writeframes(data)
55
+
56
+ def gen_samples(self):
57
+ """generates discrete samples from gen_values()
58
+
59
+ performs quantization according to
60
+ the bits per sample value given during construction
61
+ """
62
+ max_value = 2 ** self.bits
63
+ alias = 1 / max_value
64
+ amp = max_value // 2
65
+ lowest = -amp
66
+ highest = amp - 1
67
+ alias_cycle = cycle((alias * (random() - 0.5) for _ in range(1024)))
68
+ for value, alias_item in zip(self.gen_values(), alias_cycle):
69
+ sample = int(value * amp + alias_item)
70
+ yield (lowest if sample <= lowest else
71
+ sample if sample <= highest else highest)
72
+
73
+ def gen_values(self):
74
+ """generates samples between -1 and +1 from gen_freq_bits()
75
+
76
+ performs sampling according to
77
+ the samples per second value given during construction
78
+ """
79
+ spms = self.samples_per_sec / 1000
80
+ offset = 0
81
+ samples = 0
82
+ factor = 2 * pi / self.samples_per_sec
83
+ sample = 0
84
+ for freq, msec in self.gen_freq_bits():
85
+ samples += spms * msec
86
+ tx = int(samples)
87
+ freq_factor = freq * factor
88
+ for sample in range(tx):
89
+ yield sin(sample * freq_factor + offset)
90
+ offset += (sample + 1) * freq_factor
91
+ samples -= tx
92
+
93
+ def gen_freq_bits(self):
94
+ """generates tuples (freq, msec) that describe a sine wave segment
95
+
96
+ frequency "freq" in Hz and duration "msec" in ms
97
+ """
98
+ if self.vox_enabled:
99
+ for freq in (1900, 1500, 1900, 1500, 2300, 1500, 2300, 1500):
100
+ yield freq, 100
101
+ yield FREQ_VIS_START, MSEC_VIS_START
102
+ yield FREQ_SYNC, MSEC_VIS_SYNC
103
+ yield FREQ_VIS_START, MSEC_VIS_START
104
+ yield FREQ_SYNC, MSEC_VIS_BIT # start bit
105
+ vis = self.VIS_CODE
106
+ num_ones = 0
107
+ for _ in range(7):
108
+ bit = vis & 1
109
+ vis >>= 1
110
+ num_ones += bit
111
+ bit_freq = FREQ_VIS_BIT1 if bit == 1 else FREQ_VIS_BIT0
112
+ yield bit_freq, MSEC_VIS_BIT
113
+ parity_freq = FREQ_VIS_BIT1 if num_ones % 2 == 1 else FREQ_VIS_BIT0
114
+ yield parity_freq, MSEC_VIS_BIT
115
+ yield FREQ_SYNC, MSEC_VIS_BIT # stop bit
116
+ yield from self.gen_image_tuples()
117
+ for fskid_byte in map(ord, self.fskid_payload):
118
+ for _ in range(6):
119
+ bit = fskid_byte & 1
120
+ fskid_byte >>= 1
121
+ bit_freq = FREQ_FSKID_BIT1 if bit == 1 else FREQ_FSKID_BIT0
122
+ yield bit_freq, MSEC_FSKID_BIT
123
+
124
+ def gen_image_tuples(self):
125
+ return []
126
+
127
+ def add_fskid_text(self, text):
128
+ self.fskid_payload += '\x20\x2a{0}\x01'.format(
129
+ ''.join(chr(ord(c) - 0x20) for c in text))
130
+
131
+ def horizontal_sync(self):
132
+ yield FREQ_SYNC, self.SYNC
133
+
134
+
135
+ def byte_to_freq(value):
136
+ return FREQ_BLACK + FREQ_RANGE * value / 255
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env python
2
+
3
+ __all__ = ['common', 'test_color', 'test_sstv']
pysstv/tests/common.py ADDED
@@ -0,0 +1,9 @@
1
+ from os import path
2
+ import pickle
3
+
4
+ def get_asset_filename(filename):
5
+ return path.join(path.dirname(__file__), 'assets', filename)
6
+
7
+ def load_pickled_asset(filename):
8
+ with open(get_asset_filename(filename + '.p'), 'rb') as f:
9
+ return pickle.load(f)
@@ -0,0 +1,44 @@
1
+ import unittest
2
+ from itertools import islice
3
+
4
+ from PIL import Image
5
+
6
+ from pysstv import color
7
+ from pysstv.tests.common import get_asset_filename, load_pickled_asset
8
+
9
+
10
+ class TestMartinM1(unittest.TestCase):
11
+
12
+ def setUp(self):
13
+ self.image = Image.new('RGB', (320, 256))
14
+ self.s = color.MartinM1(self.image, 48000, 16)
15
+ lena = Image.open(get_asset_filename('320x256.png'))
16
+ self.lena = color.MartinM1(lena, 48000, 16)
17
+
18
+ def test_gen_freq_bits(self):
19
+ expected = load_pickled_asset("MartinM1_freq_bits")
20
+ actual = list(islice(self.s.gen_freq_bits(), 0, 1000))
21
+ self.assertEqual(expected, actual)
22
+
23
+ def test_gen_freq_bits_lena(self):
24
+ expected = load_pickled_asset("MartinM1_freq_bits_lena")
25
+ actual = list(islice(self.lena.gen_freq_bits(), 0, 1000))
26
+ self.assertEqual(expected, actual)
27
+
28
+ def test_encode_line(self):
29
+ zeroth = list(self.s.encode_line(0))
30
+ first = list(self.s.encode_line(1))
31
+ tenth = list(self.s.encode_line(10))
32
+ eleventh = list(self.s.encode_line(11))
33
+
34
+ self.assertEqual(zeroth, first)
35
+ self.assertEqual(tenth, eleventh)
36
+ self.assertEqual(zeroth, eleventh)
37
+
38
+ def test_encode_line_lena(self):
39
+ self.maxDiff = None
40
+ line_numbers = [1, 10, 100]
41
+ for line in line_numbers:
42
+ expected = load_pickled_asset("MartinM1_encode_line_lena{0}".format(line))
43
+ actual = list(self.lena.encode_line(line))
44
+ self.assertEqual(expected, actual)