pycaption 2.2.7__tar.gz → 2.2.8__tar.gz

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.
Files changed (58) hide show
  1. {pycaption-2.2.7/pycaption.egg-info → pycaption-2.2.8}/PKG-INFO +1 -1
  2. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/scc/__init__.py +47 -24
  3. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/scc/constants.py +6 -0
  4. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/scc/specialized_collections.py +57 -33
  5. {pycaption-2.2.7 → pycaption-2.2.8/pycaption.egg-info}/PKG-INFO +1 -1
  6. {pycaption-2.2.7 → pycaption-2.2.8}/setup.py +1 -1
  7. {pycaption-2.2.7 → pycaption-2.2.8}/tests/fixtures/dfxp.py +2 -2
  8. {pycaption-2.2.7 → pycaption-2.2.8}/tests/fixtures/scc.py +4 -2
  9. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_scc.py +48 -41
  10. {pycaption-2.2.7 → pycaption-2.2.8}/LICENSE +0 -0
  11. {pycaption-2.2.7 → pycaption-2.2.8}/MANIFEST.in +0 -0
  12. {pycaption-2.2.7 → pycaption-2.2.8}/README.rst +0 -0
  13. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/__init__.py +0 -0
  14. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/base.py +0 -0
  15. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/dfxp/__init__.py +0 -0
  16. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/dfxp/base.py +0 -0
  17. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/dfxp/extras.py +0 -0
  18. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/english.pickle +0 -0
  19. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/exceptions.py +0 -0
  20. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/geometry.py +0 -0
  21. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/microdvd.py +0 -0
  22. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/sami.py +0 -0
  23. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/scc/state_machines.py +0 -0
  24. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/scc/translator.py +0 -0
  25. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/srt.py +0 -0
  26. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/transcript.py +0 -0
  27. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/utils.py +0 -0
  28. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption/webvtt.py +0 -0
  29. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption.egg-info/SOURCES.txt +0 -0
  30. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption.egg-info/dependency_links.txt +0 -0
  31. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption.egg-info/requires.txt +0 -0
  32. {pycaption-2.2.7 → pycaption-2.2.8}/pycaption.egg-info/top_level.txt +0 -0
  33. {pycaption-2.2.7 → pycaption-2.2.8}/setup.cfg +0 -0
  34. {pycaption-2.2.7 → pycaption-2.2.8}/tests/__init__.py +0 -0
  35. {pycaption-2.2.7 → pycaption-2.2.8}/tests/conftest.py +0 -0
  36. {pycaption-2.2.7 → pycaption-2.2.8}/tests/fixtures/__init__.py +0 -0
  37. {pycaption-2.2.7 → pycaption-2.2.8}/tests/fixtures/microdvd.py +0 -0
  38. {pycaption-2.2.7 → pycaption-2.2.8}/tests/fixtures/sami.py +0 -0
  39. {pycaption-2.2.7 → pycaption-2.2.8}/tests/fixtures/srt.py +0 -0
  40. {pycaption-2.2.7 → pycaption-2.2.8}/tests/fixtures/translated_scc.py +0 -0
  41. {pycaption-2.2.7 → pycaption-2.2.8}/tests/fixtures/webvtt.py +0 -0
  42. {pycaption-2.2.7 → pycaption-2.2.8}/tests/mixins.py +0 -0
  43. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_base.py +0 -0
  44. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_dfxp.py +0 -0
  45. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_dfxp_conversion.py +0 -0
  46. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_dfxp_extras.py +0 -0
  47. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_functions.py +0 -0
  48. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_geometry.py +0 -0
  49. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_microdvd.py +0 -0
  50. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_microdvd_conversion.py +0 -0
  51. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_sami.py +0 -0
  52. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_sami_conversion.py +0 -0
  53. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_scc_conversion.py +0 -0
  54. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_scc_translator.py +0 -0
  55. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_srt.py +0 -0
  56. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_srt_conversion.py +0 -0
  57. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_webvtt.py +0 -0
  58. {pycaption-2.2.7 → pycaption-2.2.8}/tests/test_webvtt_conversion.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycaption
3
- Version: 2.2.7
3
+ Version: 2.2.8
4
4
  Summary: Closed caption converter
5
5
  Author: Joe Norton
6
6
  Author-email: joey@nortoncrew.com
@@ -308,15 +308,21 @@ class SCCReader(BaseReader):
308
308
 
309
309
  self.time_translator.start_at(parts[0][0])
310
310
 
311
- # loop through each word
312
- for word in parts[0][2].split(' '):
311
+ word_list = parts[0][2].split(' ')
312
+ pacs_are_doubled = len(word_list) > 1 and word_list[0] == word_list[1]
313
+ for idx, word in enumerate(word_list):
313
314
  # ignore empty results or invalid commands
314
315
  word = word.strip()
316
+ previous_is_pac = idx > 0 and _is_pac_command(word_list[idx-1])
315
317
  if len(word) == 4:
316
- self._translate_word(word)
318
+ self._translate_word(
319
+ word=word,
320
+ previous_is_pac=previous_is_pac,
321
+ pacs_are_doubled=pacs_are_doubled
322
+ )
317
323
 
318
- def _translate_word(self, word):
319
- if self._handle_double_command(word):
324
+ def _translate_word(self, word, previous_is_pac, pacs_are_doubled):
325
+ if self._handle_double_command(word, pacs_are_doubled):
320
326
  # count frames for timing
321
327
  self.time_translator.increment_frames()
322
328
  return
@@ -324,7 +330,7 @@ class SCCReader(BaseReader):
324
330
  # TODO - check that all the positioning commands are here, or use
325
331
  # some other strategy to determine if the word is a command.
326
332
  if word in COMMANDS or _is_pac_command(word):
327
- self._translate_command(word)
333
+ self._translate_command(word=word, previous_is_pac=previous_is_pac)
328
334
 
329
335
  # second, check if word is a special character
330
336
  elif word in SPECIAL_CHARS:
@@ -340,42 +346,56 @@ class SCCReader(BaseReader):
340
346
  # count frames for timing only after processing a command
341
347
  self.time_translator.increment_frames()
342
348
 
343
- def _handle_double_command(self, word):
349
+ def _handle_double_command(self, word, pacs_are_doubled):
344
350
  # If the caption is to be broadcast, each of the commands are doubled
345
351
  # up for redundancy in case the signal is garbled in transmission.
346
352
  # The decoder is programmed to ignore a second command when it is the
347
353
  # same as the first.
348
- # Also like codes, Special Characters are always doubled up,
354
+ # If we have doubled commands we're skipping also
355
+ # doubled special characters and doubled extended characters
349
356
  # with only one member of each pair being displayed.
350
- if word in COMMANDS or _is_pac_command(word) or word in SPECIAL_CHARS or word in EXTENDED_CHARS:
351
- if word == self.last_command:
352
- self.last_command = ''
353
- return True
357
+ actionable_commands = {
358
+ key: COMMANDS[key] for key in COMMANDS.keys() if key != "94a1"
359
+ }
360
+ doubled_types = word in actionable_commands or _is_pac_command(word)
361
+ if pacs_are_doubled:
362
+ doubled_types = doubled_types or word in SPECIAL_CHARS or word in EXTENDED_CHARS
363
+
364
+ if doubled_types and word == self.last_command:
365
+ return True
354
366
  # Fix for the <position> <tab offset> <position> <tab offset>
355
367
  # repetition
356
- elif _is_pac_command(word) and word in self.last_command:
357
- self.last_command = ''
368
+ elif _is_pac_command(word) and word in self.last_command:
369
+ self.last_command = ''
370
+ return True
371
+ elif word in PAC_TAB_OFFSET_COMMANDS:
372
+ if _is_pac_command(self.last_command):
373
+ self.last_command += f" {word}"
374
+ return False
375
+ else:
358
376
  return True
359
- elif word in PAC_TAB_OFFSET_COMMANDS:
360
- if _is_pac_command(self.last_command):
361
- self.last_command += f" {word}"
362
- return False
363
- else:
364
- return True
365
377
 
366
378
  self.last_command = word
367
379
  return False
368
380
 
369
381
  def _translate_special_char(self, word):
382
+ self.buffer.handle_backspace(word)
383
+ # add to buffer
370
384
  self.buffer.add_chars(SPECIAL_CHARS[word])
371
385
 
372
386
  def _translate_extended_char(self, word):
373
- self.buffer.remove_ascii_duplicate(EXTENDED_CHARS[word])
374
-
387
+ """
388
+ Each of the 64 Extended Characters incorporates an automatic BS.
389
+ When an Extended Character is received, the cursor moves to the
390
+ left one column position (unless the Extended Character is the first
391
+ character on a row), erasing any character which may be in that location,
392
+ then displays the Extended Character.
393
+ """
394
+ self.buffer.handle_backspace(word)
375
395
  # add to buffer
376
396
  self.buffer.add_chars(EXTENDED_CHARS[word])
377
397
 
378
- def _translate_command(self, word):
398
+ def _translate_command(self, word, previous_is_pac):
379
399
  # if command is pop_up
380
400
  if word == '9420':
381
401
  self.buffer_dict.set_active('pop')
@@ -444,7 +464,10 @@ class SCCReader(BaseReader):
444
464
 
445
465
  # If command is not one of the aforementioned, add it to buffer
446
466
  else:
447
- self.buffer.interpret_command(word)
467
+ self.buffer.interpret_command(
468
+ command=word,
469
+ previous_is_pac=previous_is_pac
470
+ )
448
471
 
449
472
  def _translate_characters(self, word):
450
473
  # split word into the 2 bytes
@@ -1052,3 +1052,9 @@ MID_ROW_CODES = [
1052
1052
  "91a7", "91a8", "9129", "912a", "91ab", "912c", "91ad",
1053
1053
  "97ae", "972f", "91ae", "912f", "94a8"
1054
1054
  ]
1055
+
1056
+ BACKGROUND_COLOR_CODES = [
1057
+ "1020", "10a1", "10a2", "1023", "10a4", "1025", "1026",
1058
+ "10a7", "10a8", "1029", "102a", "10ab", "102c", "10ad",
1059
+ "10ae", "102f", "97ad"
1060
+ ]
@@ -8,8 +8,8 @@ from ..geometry import (
8
8
  )
9
9
  from .constants import (
10
10
  PAC_BYTES_TO_POSITIONING_MAP, COMMANDS, PAC_TAB_OFFSET_COMMANDS,
11
- MICROSECONDS_PER_CODEWORD, INCONVERTIBLE_TO_ASCII_EXTENDED_CHARS_ASSOCIATION,
12
- MID_ROW_CODES
11
+ MICROSECONDS_PER_CODEWORD, BACKGROUND_COLOR_CODES,
12
+ MID_ROW_CODES, EXTENDED_CHARS, SPECIAL_CHARS
13
13
  )
14
14
 
15
15
  PopOnCue = collections.namedtuple("PopOnCue", "buffer, start, end")
@@ -288,6 +288,8 @@ class InstructionNodeCreator:
288
288
  else:
289
289
  self._collection = collection
290
290
 
291
+ self.last_style = None
292
+
291
293
  self._position_tracer = position_tracker
292
294
 
293
295
  def is_empty(self):
@@ -335,23 +337,32 @@ class InstructionNodeCreator:
335
337
 
336
338
  node.add_chars(*chars)
337
339
 
338
- def interpret_command(self, command):
340
+ def interpret_command(self, command, previous_is_pac=None):
339
341
  """Given a command determines whether to turn italics on or off,
340
342
  or to set the positioning
341
343
 
342
344
  This is mostly used to convert from the legacy-style commands
343
345
 
344
346
  :type command: str
347
+ :type mode: pop or roll or paint
345
348
  """
346
349
  self._update_positioning(command)
347
350
 
348
351
  text = COMMANDS.get(command, '')
349
352
 
350
- if command in MID_ROW_CODES:
351
- for node in self._collection[::-1]:
352
- if node.is_text_node():
353
- node.text += ' '
354
- break
353
+ if command == "94a1":
354
+ self.handle_backspace("94a1")
355
+
356
+ if command in BACKGROUND_COLOR_CODES:
357
+ # Since these codes are optional, they must be preceded
358
+ # with the space character (20h),
359
+ # which will be deleted when the code is applied.
360
+ # ex: 2080 97ad 94a1
361
+ if (
362
+ self._collection[-1].is_text_node() and
363
+ self._collection[-1].text[-1].isspace()
364
+ ):
365
+ self._collection[-1].text = self._collection[-1].text[:-1]
355
366
 
356
367
  if 'italic' in text:
357
368
  if 'end' not in text:
@@ -359,6 +370,7 @@ class InstructionNodeCreator:
359
370
  _InstructionNode.create_italics_style(
360
371
  self._position_tracer.get_current_position())
361
372
  )
373
+ self.last_style = "italics on"
362
374
  else:
363
375
  self._collection.append(
364
376
  _InstructionNode.create_italics_style(
@@ -366,6 +378,19 @@ class InstructionNodeCreator:
366
378
  turn_on=False
367
379
  )
368
380
  )
381
+ self.last_style = "italics off"
382
+
383
+ # mid row code that is not first code on the line
384
+ # (previous node is not a break node)
385
+ # fixes OCTO-11022
386
+ if command in MID_ROW_CODES and not previous_is_pac:
387
+ if self.last_style == "italics off":
388
+ self.add_chars(' ')
389
+ else:
390
+ for node in self._collection[::-1]:
391
+ if node.is_text_node() and node.text:
392
+ node.text += ' '
393
+ break
369
394
 
370
395
  def _update_positioning(self, command):
371
396
  """Sets the positioning information to use for the next nodes
@@ -419,33 +444,32 @@ class InstructionNodeCreator:
419
444
 
420
445
  return instance
421
446
 
422
- def remove_ascii_duplicate(self, accented_character):
447
+ def handle_backspace(self, word):
423
448
  """
424
- Characters from the Extended Characters list are usually preceded by
425
- their ASCII substitute, in case the decoder is not able to display
426
- the special character.
427
-
428
- This is used to remove the substitute character in order to avoid
429
- displaying both.
430
-
431
- :type accented_character: str
449
+ Move cursor back one position and delete that character
432
450
  """
433
- is_text_node = (
434
- self._collection and
435
- self._collection[-1].is_text_node() and
436
- self._collection[-1].text
437
- )
438
- if is_text_node:
439
- try:
440
- ascii_char = [
441
- unicodedata.normalize('NFD', accented_character)
442
- .encode('ascii', 'strict').decode("utf-8")
443
- ]
444
- except (UnicodeEncodeError, UnicodeDecodeError):
445
- ascii_char = INCONVERTIBLE_TO_ASCII_EXTENDED_CHARS_ASSOCIATION.get(accented_character)
446
-
447
- if ascii_char and self._collection[-1].text[-1] in ascii_char:
448
- self._collection[-1].text = self._collection[-1].text[:-1]
451
+ node = self.get_previous_text_node()
452
+ # in case of no previous text nodes or
453
+ # if the backspace is required while no character
454
+ # do nothing
455
+ if node is None:
456
+ return
457
+ last_char = node.text[-1]
458
+ delete_previous_condition = (
459
+ (word in SPECIAL_CHARS and last_char not in SPECIAL_CHARS.values()) or
460
+ (word in EXTENDED_CHARS and last_char not in EXTENDED_CHARS.values()) or
461
+ word == "94a1"
462
+ )
463
+ # in case of special / extended char, perform backspace
464
+ # only if the previous character in not also special / extended
465
+ if delete_previous_condition:
466
+ node.text = node.text[:-1]
467
+
468
+ def get_previous_text_node(self):
469
+ for node in self._collection[::-1]:
470
+ if node.is_text_node() and node.text:
471
+ return node
472
+ return None
449
473
 
450
474
 
451
475
  def _get_layout_from_tuple(position_tuple):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycaption
3
- Version: 2.2.7
3
+ Version: 2.2.8
4
4
  Summary: Closed caption converter
5
5
  Author: Joe Norton
6
6
  Author-email: joey@nortoncrew.com
@@ -24,7 +24,7 @@ transcript_dependencies = [
24
24
 
25
25
  setup(
26
26
  name='pycaption',
27
- version='2.2.7',
27
+ version='2.2.8',
28
28
  description='Closed caption converter',
29
29
  long_description=open(README_PATH).read(),
30
30
  author='Joe Norton',
@@ -920,10 +920,10 @@ def sample_dfxp_from_scc_output():
920
920
  <region tts:displayAlign="before" tts:origin="40% 53%" tts:textAlign="left" xml:id="r5"/>
921
921
  <region tts:displayAlign="before" tts:origin="70% 17%" tts:textAlign="left" xml:id="r6"/>
922
922
  <region tts:displayAlign="before" tts:origin="20% 35%" tts:textAlign="left" xml:id="r7"/>
923
- <region tts:displayAlign="before" tts:origin="20% 83%" tts:textAlign="left" xml:id="r8"/>
923
+ <region tts:displayAlign="before" tts:origin="25% 83%" tts:textAlign="left" xml:id="r8"/>
924
924
  <region tts:displayAlign="before" tts:origin="70% 11%" tts:textAlign="left" xml:id="r9"/>
925
925
  <region tts:displayAlign="before" tts:origin="40% 41%" tts:textAlign="left" xml:id="r10"/>
926
- <region tts:displayAlign="before" tts:origin="20% 71%" tts:textAlign="left" xml:id="r11"/>
926
+ <region tts:displayAlign="before" tts:origin="25% 71%" tts:textAlign="left" xml:id="r11"/>
927
927
  </layout>
928
928
  </head>
929
929
  <body>
@@ -367,9 +367,11 @@ def sample_scc_duplicate_special_characters():
367
367
  return """\
368
368
  Scenarist_SCC V1.0
369
369
 
370
- 00:23:28;01 9420 91b0 91b0 9131 9131 9132 9132 91b3 91b3 9134 9134 91b5 91b5 91b6 91b6 9137 9137 9138 9138 91b9 91b9 91ba 91ba 913b 913b 91bc 91bc 913d 913d 913e 913e 91bf 91bf 942f
370
+ 00:23:28;01 9420 9420 91b0 91b0 9131 9131 9132 9132 91b3 91b3 9134 9134 91b5 91b5 91b6 91b6 9137 9137 9138 9138 91b9 91b9 91ba 91ba 913b 913b 91bc 91bc 913d 913d 913e 913e 91bf 91bf 942f
371
371
 
372
- 00:53:28;01 9420 91b0 9131 9132 91b3 9134 91b5 91b6 9137 9138 91b9 91ba 913b 91bc 913d 913e 91bf 942f
372
+ 00:33:28;01 9420 91b0 9131 9132 91b3 9134 91b5 91b6 9137 9138 91b9 91ba 913b 91bc 913d 913e 91bf 942f
373
+
374
+ 00:53:28;01 9420 91b0 9131 c1c1 9132 91b3 9134 91b5 91b6 9137 9138 91b9 91ba 913b 91bc c1c1 913d 913e 91bf 942f
373
375
 
374
376
  """
375
377
 
@@ -77,17 +77,15 @@ class TestSCCReader(ReaderTestingMixIn):
77
77
  ((40.0, UnitEnum.PERCENT), (53.0, UnitEnum.PERCENT)),
78
78
  ((70.0, UnitEnum.PERCENT), (17.0, UnitEnum.PERCENT)),
79
79
  ((20.0, UnitEnum.PERCENT), (35.0, UnitEnum.PERCENT)),
80
- ((20.0, UnitEnum.PERCENT), (83.0, UnitEnum.PERCENT)),
80
+ ((25.0, UnitEnum.PERCENT), (83.0, UnitEnum.PERCENT)),
81
81
  ((70.0, UnitEnum.PERCENT), (11.0, UnitEnum.PERCENT)),
82
82
  ((40.0, UnitEnum.PERCENT), (41.0, UnitEnum.PERCENT)),
83
- ((20.0, UnitEnum.PERCENT), (71.0, UnitEnum.PERCENT))
83
+ ((25.0, UnitEnum.PERCENT), (71.0, UnitEnum.PERCENT))
84
84
  ]
85
-
86
85
  actual_positioning = [
87
86
  caption_.layout_info.origin.serialized()
88
87
  for caption_ in captions.get_captions('en-US')
89
88
  ]
90
-
91
89
  assert expected_positioning == actual_positioning
92
90
 
93
91
  def test_tab_offset(self, sample_scc_tab_offset):
@@ -210,7 +208,7 @@ class TestSCCReader(ReaderTestingMixIn):
210
208
  'The I-10 Santa Monica Freeway ',
211
209
  'westbound is jammed,',
212
210
  'due to a three-car accident ',
213
- 'blocking lanes 1 and 2',
211
+ 'blocking lanes 1 and 2'
214
212
  ]
215
213
 
216
214
  caption_set = SCCReader().read(sample_scc_duplicate_tab_offset)
@@ -225,7 +223,11 @@ class TestSCCReader(ReaderTestingMixIn):
225
223
 
226
224
  def test_skip_duplicate_special_characters(
227
225
  self, sample_scc_duplicate_special_characters):
228
- expected_lines = ['®°½¿™¢£♪à èâêîôû', '®°½¿™¢£♪à èâêîôû']
226
+ expected_lines = [
227
+ '®°½¿™¢£♪à èâêîôû', # double commands so we skip one
228
+ '®°½¿™¢£♪à èâêîôû', # no double command, nothing skipped equal with above
229
+ '®°A½¿™¢£♪à èâêAîôû' # no skips but a couple of normal chars "c1c1" = AA
230
+ ]
229
231
 
230
232
  caption_set = SCCReader().read(sample_scc_duplicate_special_characters)
231
233
  actual_lines = [
@@ -234,7 +236,6 @@ class TestSCCReader(ReaderTestingMixIn):
234
236
  for node in cap_.nodes
235
237
  if node.type_ == CaptionNode.TEXT
236
238
  ]
237
-
238
239
  assert expected_lines == actual_lines
239
240
 
240
241
  def test_flashing_cue(self, sample_scc_flashing_cue):
@@ -273,21 +274,21 @@ class TestCoverageOnly:
273
274
  actual_texts = [cap_.nodes[0].content for cap_ in captions]
274
275
  expected_texts = [
275
276
  '>>> HI.',
276
- "I'M KEVIN CUNNING AND AT",
277
- "INVESTOR'S BANK WE BELIEVE IN",
278
- 'HELPING THE LOCAL NEIGHBORHOODS',
279
- 'AND IMPROVING THE LIVES OF ALL',
280
- 'WE SERVE.',
281
- '®°½',
282
- 'ABû',
283
- 'ÁÉÓ¡',
284
- "WHERE YOU'RE STANDING NOW,",
285
- "LOOKING OUT THERE, THAT'S AL",
286
- 'THE CROWD.',
287
- '>> IT WAS GOOD TO BE IN TH',
288
- "And restore Iowa's land, water",
289
- 'And wildlife.',
290
- '>> Bike Iowa, your source for',
277
+ "I'M KEVIN CUNNING AND AT",
278
+ "INVESTOR'S BANK WE BELIEVE IN",
279
+ 'HELPING THE LOCAL NEIGHBORHOODS',
280
+ 'AND IMPROVING THE LIVES OF ALL',
281
+ 'WE SERVE.',
282
+ '®°½',
283
+ '',
284
+ 'ÁÉÓ¡',
285
+ "WHERE YOU'RE STANDING NOW,",
286
+ "LOOKING OUT THERE, THAT'S AL",
287
+ 'THE CROWD.',
288
+ '>> IT WAS GOOD TO BE IN TH',
289
+ "And restore Iowa's land, water",
290
+ 'And wildlife.',
291
+ '>> Bike Iowa, your source for'
291
292
  ]
292
293
  assert expected_texts == actual_texts
293
294
 
@@ -383,6 +384,9 @@ class TestInterpretableNodeCreator:
383
384
  # 4. to get new opening italic nodes after changing position, if 3
384
385
  # happened
385
386
  # 5. to get a final italic closing node, if one is needed
387
+ # 9120 and 91ae are mid row codes and will add a space
388
+ # 9120 at the start of the following text node
389
+ # 91ae to the end of the previous text node
386
390
  node_creator.interpret_command('9470') # row 15, col 0
387
391
  node_creator.interpret_command('9120') # italics off
388
392
  node_creator.interpret_command('9120') # italics off
@@ -401,7 +405,7 @@ class TestInterpretableNodeCreator:
401
405
  node_creator.add_chars('b')
402
406
  node_creator.interpret_command('91ae') # italics ON again
403
407
  node_creator.add_chars('b')
404
- node_creator.interpret_command('9120') # italics OFF
408
+ node_creator.interpret_command('9120') # italics OFF adds space
405
409
  node_creator.interpret_command('9120') # italics OFF
406
410
 
407
411
  node_creator.interpret_command('1570') # row 6 col 0
@@ -420,32 +424,35 @@ class TestInterpretableNodeCreator:
420
424
  result = list(node_creator)
421
425
 
422
426
  assert result[0].is_text_node()
423
- assert result[1].requires_repositioning()
424
- assert result[2].is_italics_node()
425
- assert result[2].sets_italics_on()
427
+ assert result[1].is_text_node()
428
+ assert result[2].requires_repositioning()
426
429
 
427
- assert result[3].is_text_node()
430
+ assert result[3].is_italics_node()
431
+ assert result[3].sets_italics_on()
428
432
  assert result[4].is_text_node()
429
- assert result[5].is_text_node()
430
-
431
- assert result[6].is_italics_node()
432
- assert result[6].sets_italics_off()
433
+ assert result[5].sets_italics_off()
434
+ assert result[6].is_text_node()
433
435
 
434
- assert result[7].requires_repositioning()
435
- assert result[8].is_text_node()
436
+ assert result[7].is_text_node()
437
+ assert result[8].sets_italics_on()
436
438
 
437
- assert result[9].requires_repositioning()
438
- assert result[10].is_italics_node()
439
- assert result[10].sets_italics_on()
439
+ assert result[9].is_text_node()
440
+ assert result[10].is_text_node()
440
441
 
441
- assert result[11].is_text_node()
442
- assert result[12].is_explicit_break()
442
+ assert result[11].sets_italics_off()
443
+ assert result[12].is_text_node()
443
444
  assert result[13].is_text_node()
444
- assert result[14].is_explicit_break()
445
+ assert result[14].requires_repositioning()
445
446
  assert result[15].is_text_node()
446
447
 
447
- assert result[16].is_italics_node()
448
- assert result[16].sets_italics_off()
448
+ assert result[16].requires_repositioning()
449
+ assert result[17].sets_italics_on()
450
+ assert result[18].is_text_node()
451
+ assert result[19].is_explicit_break()
452
+ assert result[20].is_text_node()
453
+ assert result[21].is_explicit_break()
454
+ assert result[22].is_text_node()
455
+ assert result[23].sets_italics_off()
449
456
 
450
457
 
451
458
  class CaptionDummy:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes