Custom Bibles for EasyWorship

Keith Ng

If you are only interested in EasyWorship Bible downloads for CUV and CUVS:

  • CUV: Chinese Union Version (Traditional) - addon translation - Source
  • CUVS: Chinese Union Version (Simplified) - replaces official CUVS translation - Source

Some of you might have read my previous post on trying to decode the EasyWorship Bible format, in an effort to convert the existing Simplified Chinese Union Version translation to a Traditional CUV translation.

For those out of the loop, a quick recap:

  • EasyWorship (church presentation software) currently only supports the Simplified CUV Bible (CUVS), leaving out the Traditional CUV Bible (CUV) commonly used in regions like Hong Kong and Taiwan
  • Despite constant requests, the EW development team appears to have no interest in adding this translation
  • I attempted to reverse-engineer the EWB file format: allowing me to decode the existing CUVS translation, convert Chinese characters from simplified to traditional, then re-encode it as a new Bible

And I was successful…except that it turns out the existing CUVS translation was riddled with errors, where characters were mistakenly replaced with others. No, I’m not just talking about the one error addressed in the article (‘有’ becoming ‘冇’, ‘to have’ vs ‘to not have’) - turns out there were many more!

Given that the addressed error was spotted 8 years ago and still has not been fixed by EW (yikes!), the chance of the EW team making those (in my opinion, pretty major and urgent) corrections are pretty much slim to none, much less implementing new Bible translations as requested by their customers. To be fair, they have recently said their development efforts are focused on EW 8 - but still!

So, armed with a hex editor and existing EWB files, I attempted to dig a little deeper into their EWB format. Here’s what we know so far from the previous article:

  • EWB files are just an SQLite database
  • Verse metadata and the text itself are separately stored
  • Verses are stored as ZLib compressed data in a blob, per book
  • Every 8th byte in the ‘verse_info’ attribute of the ‘books’ table represents the Bible translation ID
  • Individual words are stored in a lookup table for the ‘search for passages by words or phrases’ functionality to work

To encode our own Bible translations, I needed to know exactly what the ‘book_info’ and ‘verse_info’ attributes stored. And as it turns out:

  • ‘book_info’: Number of chapters in the book, and number of verses in each chapter
  • ‘verse_info’: Length of the verse, position of where the verse starts in the text stream, verse number, chapter number, book number, translation ID

That is a very brief summary, but it is all documented in the code below - which, yes, allows you to encode your own Bibles for EasyWorship! All you need is an XML file of a Bible translation, which unfortunately might be the most challenging part, because there seems to be no singular standard for Bibles and scriptures.

For my script, I have settled on using Zefania XML - main reason being that the CUV and CUVS, the translations I was working with (and many other public domain translations) are readily available in that format (on SourceForge, and also here). You can learn more about Zefania XML on SourceForge and GitHub.

Of course, that is not to say the script cannot be modified for other scripture formats - if you feel comfortable, knock yourself out! It should hopefully be more straightforward now with assistance below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
# EasyWorship Bible Encoder
# Encodes Zefania Bible XML files into EWB format for EasyWorship
# Written by Keith Ng <[email protected]>, November 2024

# CONFIGURATION

# Unique ID of this Bible translation
# Valid values range from 1 to 127 (this value gets multiplied by 2 and stored in a single byte, hence max value of 127)
# Note that some IDs may already be used by official EW Bibles; using an existing ID will overwrite the official Bible with the same ID
bibleId = 127

# Complete name of this Bible translation which gets displayed in EW under the 'More Available' tab of the Scriptures section
bibleName = 'Chinese Union Version (Traditional)'

# Abbreviated name of this Bible translation
# This is what is commonly displayed in EW - both on the operator UI and live output
# IMPORTANT: Your XML file should be named as this value too (with XML file extension)
bibleNameAbbrev = 'CUV'

# Two-letter ISO 639 language code corresponding to the language of this Bible translation
# Appears to be used for limited purpose - only gets displayed in EW under the 'More Available' tab of the Scriptures section
# If you are overwriting an official Bible, this language code doesn't get displayed in place of the official one for some reason
bibleLangCode = 'ZH'

# Word spacing configuration
# Indicates whether words in the XML file are separated by spaces. This is important to correctly generate the lookup table for
# words in Bible passages, which allows you to search for a specific passage by words or phrases in it
# Check how passages in your XML file are formatted before adjusting this setting - there is no 'one size fits all' approach
# Generally, as an example:
# - Set to False for languages such as Chinese, Japanese or Korean where words are not naturally separated by spaces
# - Leave as True for languages where words are spaced (which is the majority of all languages)
bibleWordsSpaced = True

########################################################################################################################

"""
EWB FORMAT DOCUMENTATION

SQLite database consisting of four tables

- books (metadata for books)
- rowid: Chronological ID/number of book
- name: Name of the book
- abbrev_name: Standardised abbreviated name of the book - this value is the same for a given book across any translation
- alt_name: English name of the book, usually inserted for non-English translations to aid easily searching for passages
- book_info: Binary data storing number of chapters and number of verses in each chapter
- First byte always stores the number of chapters in the book. If there is only one chapter, this is set to 0x00
- Subsequent bytes store the number of verses in each chapter, chronologically (i.e. 2nd byte = no of verses in
chapter 1, 3rd byte for chapter 2 etc)
- verse_info: Binary data storing the verse-specific information for each verse in the book
- Each verse in the book is represented by 8 bytes
- First 4 bytes defines the length of the verse and a pointer for where that verse starts in the stream
- 1st byte: Length of uncompressed text in verse (if >255, overflows into next 3 bytes)
- 2nd-4th bytes: Pointer for starting position of verse in uncompressed stream (little endian)
- Last 4 bytes defines the verse number, chapter number, book number and Bible translation ID
- 1st byte: Verse number multiplied by 4 (if >255, overflows into next byte)
- 2nd byte: Chapter number multiplied by 4 (if >255, overflows into next byte)
- 3rd byte: Book number (same as rowid) multiplied by 4 (if >255, overflows into next byte)
- 4th byte: Bible translation ID multiplied by 2 (hence why Bible IDs have a max value of 127)

- header (metadata for Bible translation)
- rowid: Always value of 1
- id: Unique ID for the Bible translation (valid range 1-127)
- key: ID for Bible licensing, 0 = free
- name: Full name of Bible translation
- abbrev_name: Abbreviated name of Bible translation
- lang_code: Two letter ISO-639 language code
- plugin_version: Plugin version, assumed to be used for updates
- format_version: Format version, assumed to be used for updates
- copyright: Copyright notice in RTF
- license: License notice in RTF

- streams (text for each book)
- rowid: ID of the book (corresponding to books table)
- stream: ZLib compressed stream of the book's text
- Appears to use the best level of ZLib compression based on header
- Last 10 bytes stores the length of the book's text before ZLib compression
- First 4 bytes: unknown, possible header - always 51 4b 03 04 in hex
- Next 4 bytes: packed little-endian 4-byte integer - length of text before compression
- Last 2 bytes: unknown, possible footer - always 08 00 in hex

- words (lookup table for words)
- rowid: ID of the word
- word: Word that appears in the text
- verse_info: Binary data storing where the word appears in the text
- Each verse where the word appears is represented by 4 bytes
- 1st byte: Length of uncompressed text in verse (if >255, overflows into next 3 bytes)
- 2nd-4th bytes: Pointer for starting position of verse in uncompressed stream (little endian)
"""

########################################################################################################################

# Import required modules
import sqlite3
import zlib
import xml.etree.ElementTree as et
import re
import struct

# Create a new EWB SQLite database
conn = sqlite3.connect(f'{bibleNameAbbrev}.ewb')
cursor = conn.cursor()

# Create necessary tables
cursor.executescript('''
DROP TABLE IF EXISTS books;
DROP TABLE IF EXISTS header;
DROP TABLE IF EXISTS streams;
DROP TABLE IF EXISTS words;

CREATE TABLE books (rowid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, abbrev_name TEXT NOT NULL, alt_name TEXT NULL, book_info BLOB NULL, verse_info BLOB);
CREATE TABLE header (rowid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INT8 NOT NULL, key INT8 NOT NULL, name TEXT NOT NULL, abbrev_name TEXT NOT NULL, lang_code TEXT NOT NULL, plugin_version INT8 NOT NULL, format_version INT8 NOT NULL, copyright TEXT NULL, license TEXT NULL);
CREATE TABLE streams (rowid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, stream BLOB NOT NULL);
CREATE TABLE words (rowid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, word TEXT NOT NULL, verse_info INT32A NOT NULL);

DROP INDEX IF EXISTS idx_words_word;
CREATE INDEX idx_words_word ON words (word);
''')

# Populate header with ID, name and language
cursor.execute(
'INSERT INTO header (id, key, name, abbrev_name, lang_code, plugin_version, format_version) VALUES (?, 0, ?, ?, ?, 1, 1)',
(bibleId, bibleName, bibleNameAbbrev, bibleLangCode))

# Map common shortform names to full names for books
altBookNames = {
'ge': 'Genesis',
'gn': 'Genesis',
'gen': 'Genesis',
'gene': 'Genesis',
'genesis': 'Genesis',

'ex': 'Exodus',
'exo': 'Exodus',
'exod': 'Exodus',
'exodus': 'Exodus',

'leviticus': 'Leviticus',
'lev': 'Leviticus',
'le': 'Leviticus',
'lv': 'Leviticus',

'numbers': 'Numbers',
'num': 'Numbers',
'nu': 'Numbers',
'nm': 'Numbers',
'nb': 'Numbers',

'deuteronomy': 'Deuteronomy',
'deut': 'Deuteronomy',
'de': 'Deuteronomy',
'dt': 'Deuteronomy',

'joshua': 'Joshua',
'josh': 'Joshua',
'jos': 'Joshua',
'jsh': 'Joshua',

'judges': 'Judges',
'judg': 'Judges',
'jdg': 'Judges',
'jg': 'Judges',
'jdgs': 'Judges',

'ruth': 'Ruth',
'rth': 'Ruth',
'ru': 'Ruth',

'1samuel': '1 Samuel',
'1sam': '1 Samuel',
'1sm': '1 Samuel',
'1sa': '1 Samuel',
'1s': '1 Samuel',
'isam': '1 Samuel',
'1stsamuel': '1 Samuel',
'1stsam': '1 Samuel',
'firstsamuel': '1 Samuel',
'firstsam': '1 Samuel',

'2samuel': '2 Samuel',
'2sam': '2 Samuel',
'2sm': '2 Samuel',
'2sa': '2 Samuel',
'2s': '2 Samuel',
'iisam': '2 Samuel',
'2ndsamuel': '2 Samuel',
'2ndsam': '2 Samuel',
'secondsamuel': '2 Samuel',
'secondsam': '2 Samuel',

'1kings': '1 Kings',
'1kgs': '1 Kings',
'1ki': '1 Kings',
'1kin': '1 Kings',
'1k': '1 Kings',
'ikgs': '1 Kings',
'iki': '1 Kings',
'1stkings': '1 Kings',
'1stkgs': '1 Kings',
'firstkings': '1 Kings',
'firstkgs': '1 Kings',

'2kings': '2 Kings',
'2kgs': '2 Kings',
'2ki': '2 Kings',
'2kin': '2 Kings',
'2k': '2 Kings',
'iikgs': '2 Kings',
'iiki': '2 Kings',
'2ndkings': '2 Kings',
'2ndkgs': '2 Kings',
'secondkings': '2 Kings',
'secondkgs': '2 Kings',

'1chronicles': '1 Chronicles',
'1chron': '1 Chronicles',
'1chr': '1 Chronicles',
'1ch': '1 Chronicles',
'ichron': '1 Chronicles',
'ichr': '1 Chronicles',
'ich': '1 Chronicles',
'1stchronicles': '1 Chronicles',
'1stchron': '1 Chronicles',
'firstchronicles': '1 Chronicles',
'firstchron': '1 Chronicles',

'2chronicles': '2 Chronicles',
'2chron': '2 Chronicles',
'2chr': '2 Chronicles',
'2ch': '2 Chronicles',
'iichron': '2 Chronicles',
'iichr': '2 Chronicles',
'iich': '2 Chronicles',
'2ndchronicles': '2 Chronicles',
'2ndchron': '2 Chronicles',
'secondchronicles': '2 Chronicles',
'secondchron': '2 Chronicles',

'ezra': 'Ezra',
'ezr': 'Ezra',
'ez': 'Ezra',

'nehemiah': 'Nehemiah',
'neh': 'Nehemiah',
'ne': 'Nehemiah',

'esther': 'Esther',
'est': 'Esther',
'esth': 'Esther',
'es': 'Esther',

'job': 'Job',
'jb': 'Job',

'psalms': 'Psalm',
'ps': 'Psalm',
'psalm': 'Psalm',
'pslm': 'Psalm',
'psa': 'Psalm',
'psm': 'Psalm',
'pss': 'Psalm',

'proverbs': 'Proverbs',
'prov': 'Proverbs',
'pro': 'Proverbs',
'prv': 'Proverbs',
'pr': 'Proverbs',

'ecclesiastes': 'Ecclesiastes',
'eccles': 'Ecclesiastes',
'eccle': 'Ecclesiastes',
'ecc': 'Ecclesiastes',
'ec': 'Ecclesiastes',
'qoh': 'Ecclesiastes',

'songofsolomon': 'Song of Solomon',
'song': 'Song of Solomon',
'songofsongs': 'Song of Solomon',
'sos': 'Song of Solomon',
'so': 'Song of Solomon',
'canticleofcanticles': 'Song of Solomon',
'canticles': 'Song of Solomon',
'cant': 'Song of Solomon',

'isaiah': 'Isaiah',
'isa': 'Isaiah',
'is': 'Isaiah',

'jeremiah': 'Jeremiah',
'jer': 'Jeremiah',
'je': 'Jeremiah',
'jr': 'Jeremiah',

'lamentations': 'Lamentations',
'lam': 'Lamentations',
'la': 'Lamentations',

'ezekiel': 'Ezekiel',
'ezek': 'Ezekiel',
'eze': 'Ezekiel',
'ezk': 'Ezekiel',

'daniel': 'Daniel',
'dan': 'Daniel',
'da': 'Daniel',
'dn': 'Daniel',

'hosea': 'Hosea',
'hos': 'Hosea',
'ho': 'Hosea',

'joel': 'Joel',
'jl': 'Joel',

'amos': 'Amos',
'am': 'Amos',

'obadiah': 'Obadiah',
'obad': 'Obadiah',
'ob': 'Obadiah',

'jonah': 'Jonah',
'jnh': 'Jonah',
'jon': 'Jonah',

'micah': 'Micah',
'mic': 'Micah',
'mc': 'Micah',

'nahum': 'Nahum',
'nah': 'Nahum',
'na': 'Nahum',

'habakkuk': 'Habakkuk',
'hab': 'Habakkuk',
'hb': 'Habakkuk',

'zephaniah': 'Zephaniah',
'zeph': 'Zephaniah',
'zep': 'Zephaniah',
'zp': 'Zephaniah',

'haggai': 'Haggai',
'hag': 'Haggai',
'hg': 'Haggai',

'zechariah': 'Zechariah',
'zech': 'Zechariah',
'zec': 'Zechariah',
'zc': 'Zechariah',

'malachi': 'Malachi',
'mal': 'Malachi',
'ml': 'Malachi',

'matthew': 'Matthew',
'matt': 'Matthew',
'mt': 'Matthew',

'mark': 'Mark',
'mrk': 'Mark',
'mar': 'Mark',
'mk': 'Mark',
'mr': 'Mark',

'luke': 'Luke',
'luk': 'Luke',
'lk': 'Luke',

'john': 'John',
'joh': 'John',
'jhn': 'John',
'jn': 'John',

'acts': 'Acts',
'act': 'Acts',
'ac': 'Acts',

'romans': 'Romans',
'rom': 'Romans',
'ro': 'Romans',
'rm': 'Romans',

'1corinthians': '1 Corinthians',
'1cor': '1 Corinthians',
'1co': '1 Corinthians',
'icor': '1 Corinthians',
'ico': '1 Corinthians',
'icorinthians': '1 Corinthians',
'1stcorinthians': '1 Corinthians',
'firstcorinthians': '1 Corinthians',

'2corinthians': '2 Corinthians',
'2cor': '2 Corinthians',
'2co': '2 Corinthians',
'iicor': '2 Corinthians',
'iico': '2 Corinthians',
'iicorinthians': '2 Corinthians',
'2ndcorinthians': '2 Corinthians',
'secondcorinthians': '2 Corinthians',

'galatians': 'Galatians',
'gal': 'Galatians',
'ga': 'Galatians',

'ephesians': 'Ephesians',
'eph': 'Ephesians',
'ephes': 'Ephesians',

'philippians': 'Philippians',
'phil': 'Philippians',
'php': 'Philippians',
'pp': 'Philippians',

'colossians': 'Colossians',
'col': 'Colossians',
'co': 'Colossians',

'1thessalonians': '1 Thessalonians',
'1thess': '1 Thessalonians',
'1thes': '1 Thessalonians',
'1th': '1 Thessalonians',
'ithessalonians': '1 Thessalonians',
'ithess': '1 Thessalonians',
'ithes': '1 Thessalonians',
'ith': '1 Thessalonians',
'1stthessalonians': '1 Thessalonians',
'1stthess': '1 Thessalonians',
'firstthessalonians': '1 Thessalonians',
'firstthess': '1 Thessalonians',

'2thessalonians': '2 Thessalonians',
'2thess': '2 Thessalonians',
'2thes': '2 Thessalonians',
'2th': '2 Thessalonians',
'iithessalonians': '2 Thessalonians',
'iithess': '2 Thessalonians',
'iithes': '2 Thessalonians',
'iith': '2 Thessalonians',
'2ndthessalonians': '2 Thessalonians',
'2ndthess': '2 Thessalonians',
'secondthessalonians': '2 Thessalonians',
'secondthess': '2 Thessalonians',

'1timothy': '1 Timothy',
'1tim': '1 Timothy',
'1ti': '1 Timothy',
'itimothy': '1 Timothy',
'itim': '1 Timothy',
'iti': '1 Timothy',
'1sttimothy': '1 Timothy',
'1sttim': '1 Timothy',
'firsttimothy': '1 Timothy',
'firsttim': '1 Timothy',

'2timothy': '2 Timothy',
'2tim': '2 Timothy',
'2ti': '2 Timothy',
'iitimothy': '2 Timothy',
'iitim': '2 Timothy',
'iiti': '2 Timothy',
'2ndtimothy': '2 Timothy',
'2ndtim': '2 Timothy',
'secondtimothy': '2 Timothy',
'secondtim': '2 Timothy',

'titus': 'Titus',
'tit': 'Titus',
'ti': 'Titus',

'philemon': 'Philemon',
'philem': 'Philemon',
'phm': 'Philemon',
'pm': 'Philemon',

'hebrews': 'Hebrews',
'heb': 'Hebrews',

'james': 'James',
'jas': 'James',
'jm': 'James',

'1peter': '1 Peter',
'1pet': '1 Peter',
'1pe': '1 Peter',
'1pt': '1 Peter',
'1p': '1 Peter',
'ipet': '1 Peter',
'ipt': '1 Peter',
'ipe': '1 Peter',
'ipeter': '1 Peter',
'1stpeter': '1 Peter',
'firstpeter': '1 Peter',

'2peter': '2 Peter',
'2pet': '2 Peter',
'2pe': '2 Peter',
'2pt': '2 Peter',
'2p': '2 Peter',
'iipeter': '2 Peter',
'iipet': '2 Peter',
'iipt': '2 Peter',
'iipe': '2 Peter',
'2ndpeter': '2 Peter',
'secondpeter': '2 Peter',

'1john': '1 John',
'1jhn': '1 John',
'1jn': '1 John',
'1j': '1 John',
'1joh': '1 John',
'1jo': '1 John',
'ijohn': '1 John',
'ijhn': '1 John',
'ijoh': '1 John',
'ijn': '1 John',
'ijo': '1 John',
'1stjohn': '1 John',
'firstjohn': '1 John',

'2john': '2 John',
'2jhn': '2 John',
'2jn': '2 John',
'2j': '2 John',
'2joh': '2 John',
'2jo': '2 John',
'iijohn': '2 John',
'iijhn': '2 John',
'iijoh': '2 John',
'iijn': '2 John',
'iijo': '2 John',
'2ndjohn': '2 John',
'secondjohn': '2 John',

'3john': '3 John',
'3jhn': '3 John',
'3jn': '3 John',
'3j': '3 John',
'3joh': '3 John',
'3jo': '3 John',
'iiijohn': '3 John',
'iiijhn': '3 John',
'iiijoh': '3 John',
'iiijn': '3 John',
'iiijo': '3 John',
'3rdjohn': '3 John',
'thirdjohn': '3 John',

'jude': 'Jude',
'jud': 'Jude',
'jd': 'Jude',

'revelation': 'Revelation',
'rev': 'Revelation',
're': 'Revelation',
'therevelation': 'Revelation'
}

# Map full names to EW alternate names for books
abbrevBookNames = {
'Genesis': 'Gn',
'Exodus': 'Ex',
'Leviticus': 'Lv',
'Numbers': 'Nm',
'Deuteronomy': 'Dt',
'Joshua': 'Jos',
'Judges': 'Jgs',
'Ruth': 'Ru',
'1 Samuel': '1 Sm',
'2 Samuel': '2 Sm',
'1 Kings': '1 Kgs',
'2 Kings': '2 Kgs',
'1 Chronicles': '1 Chr',
'2 Chronicles': '2 Chr',
'Ezra': 'Ezr',
'Nehemiah': 'Neh',
'Esther': 'Est',
'Job': 'Jb',
'Psalm': 'Ps',
'Proverbs': 'Prv',
'Ecclesiastes': 'Eccl',
'Song of Solomon': 'Sg',
'Isaiah': 'Is',
'Jeremiah': 'Jer',
'Lamentations': 'Lam',
'Ezekiel': 'Ez',
'Daniel': 'Dn',
'Hosea': 'Hos',
'Joel': 'Jl',
'Amos': 'Am',
'Obadiah': 'Ob',
'Jonah': 'Jon',
'Micah': 'Mi',
'Nahum': 'Na',
'Habakkuk': 'Hb',
'Zephaniah': 'Zep',
'Haggai': 'Hg',
'Zechariah': 'Zec',
'Malachi': 'Mal',
'Matthew': 'Mt',
'Mark': 'Mk',
'Luke': 'Lk',
'John': 'Jn',
'Acts': 'Acts',
'Romans': 'Rom',
'1 Corinthians': '1 Cor',
'2 Corinthians': '2 Cor',
'Galatians': 'Gal',
'Ephesians': 'Eph',
'Philippians': 'Phil',
'Colossians': 'Col',
'1 Thessalonians': '1 Thes',
'2 Thessalonians': '2 Thes',
'1 Timothy': '1 Tm',
'2 Timothy': '2 Tm',
'Titus': 'Ti',
'Philemon': 'Phlm',
'Hebrews': 'Heb',
'James': 'Jas',
'1 Peter': '1 Pt',
'2 Peter': '2 Pt',
'1 John': '1 Jn',
'2 John': '2 Jn',
'3 John': '3 Jn',
'Jude': 'Jude',
'Revelation': 'Rv'
}


# Used to encode data for 'verse_info' attribute
def encode_verse_data(verse_length, verse_pointer):
verse_pointer *= 4

if verse_length >= 256:
verse_pointer += verse_length // 256
verse_length %= 256

return bytes([
verse_length,
verse_pointer & 0xFF,
(verse_pointer >> 8) & 0xFF,
(verse_pointer >> 16) & 0xFF
])


# Used to encode data for 'verse_info' attribute
def encode_verse_id(verse_no, chapter_no, book_no, translation_id):
verse_no *= 4
chapter_no *= 4
book_no *= 4
translation_id *= 2

if verse_no >= 256:
chapter_no += verse_no // 256
verse_no %= 256

if chapter_no >= 256:
book_no += chapter_no // 256
chapter_no %= 256

if book_no >= 256:
translation_id += book_no // 256
book_no %= 256

return bytes([
verse_no & 0xFF,
chapter_no & 0xFF,
book_no & 0xFF,
translation_id & 0xFF
])


# Dictionary to save words (which will populate the words lookup table)
words = {}

tree = et.parse(f'{bibleNameAbbrev}.xml')
root = tree.getroot()
for book in root.findall('BIBLEBOOK'):
# Map provided book shortform/standard name to EW alternate and abbreviated book names
standardName = book.get('bsname')
altName = altBookNames.get(standardName.lower(), standardName)
abbrevName = abbrevBookNames.get(altName, standardName)

# Create data for 'book_info' attribute
bookInfo = ''
chapters = book.findall('CHAPTER')
if len(chapters) > 1:
bookInfo = bytes([len(chapters)])
for chapter in chapters:
bookInfo += bytes([len(chapter.findall('VERS'))])
else:
bookInfo = b'\x00'

# Verses are stored in two tables:
# - 'streams' table contains the entire text of a book (the 'stream')
# - 'books' table contains the pointers that splits the stream into each individual verse

stream = '\0\0' # Stream needs two dummy characters at the start (EW seems to ignore the first two characters)
verseInfo = b'' # Every 8 bytes represent 1 verse
versePointer = 0

for chapter in chapters:
for verse in chapter.findall('VERS'):
verseLength = len(verse.text or '')
verseNumber = verse.get('vnumber')

if '-' in verseNumber:
# If verse number is a range, assign the verse text to the first verse and leave the subsequent verses blank
start, end = map(int, verseNumber.split('-'))
for vnum in range(start, end + 1):
encodedVerseId = encode_verse_id(vnum, int(chapter.get('cnumber')), int(book.get('bnumber')),
bibleId)
# Add encoded verse data and IDs
if vnum == start:
verseInfo += encode_verse_data(verseLength, versePointer) + encodedVerseId
else:
verseInfo += encode_verse_data(0, versePointer + verseLength) + encodedVerseId
# Overwrite variable with starting verse to use for words lookup table processing later
encodedVerseId = encode_verse_id(start, int(chapter.get('cnumber')), int(book.get('bnumber')), bibleId)
else:
encodedVerseId = encode_verse_id(int(verseNumber), int(chapter.get('cnumber')),
int(book.get('bnumber')), bibleId)
# Add encoded verse data and IDs
verseInfo += encode_verse_data(verseLength, versePointer) + encodedVerseId

# Add verse to stream
stream += verse.text or ''

if verse.text:
# Increment verse pointer
versePointer += verseLength

# Extract words from verse to populate 'words' table for in-verse searching
for word in re.findall(r'\S+', verse.text if bibleWordsSpaced else ' '.join(verse.text)):
currentWords = [word]
if bibleWordsSpaced:
# Split up words in apostrophes so that there are two: 1) base word, 2) the full word without any apostrophes
# This is also how EW seems to handle words with apostrophes in its official translations
apostrophes = ["'", "’", "‘", "ʼ", "′", "″", "‛", "❛", "❜", "`", "´", "˹", "˼", "˽", "ɿ"]
if any(char in word for char in apostrophes):
currentWords = []
for char in apostrophes:
if char in word:
currentWords.append(word.split(char)[0])
currentWords.append(word.replace(char, ''))

# Strip out non-alphanumeric characters and set pointers
for word in currentWords:
word = re.sub(r'[^\w\s]', '', word).lower().strip()
if not word:
continue

if not words.get(word):
words[word] = b''

words[word] += encodedVerseId

# Insert encoded data into books table
cursor.execute(
'INSERT INTO books (rowid, name, abbrev_name, alt_name, book_info, verse_info) VALUES (?, ?, ?, ?, ?, ?)',
(int(book.get('bnumber')), book.get('bname'), abbrevName, altName, bookInfo, verseInfo))

# Insert encoded book text into streams table
# Last 10 bytes store the length of the uncompressed book text
compressedStream = zlib.compress(stream.encode('utf-8'), level=9) + b'QK\x03\x04' + struct.pack('<I',
len(stream)) + b'\x08\x00'
cursor.execute('INSERT INTO streams (rowid, stream) VALUES (?, ?)', (int(book.get('bnumber')), compressedStream))

# Insert word pointers into words table
for word, verse_info in words.items():
cursor.execute('INSERT INTO words (word, verse_info) VALUES (?, ?)', (word, verse_info))

conn.commit()
conn.close()

It also turns out that many of the official EW Bible translations - and not just the CUVS - contain errors and probably require corrections (what a surprise!), so I have written a script that can decode EWB files into Zefania XML so you can easily make any adjustments, or just compare the EW-supplied version’s text with another source.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# EasyWorship Bible Decoder
# Decodes EWB format for EasyWorship into Zefania Bible XML files
# Written by Keith Ng <[email protected]>, November 2024

# Name of EWB file (without EWB file extension)
bibleNameAbbrev = 'CUV'

########################################################################################################################

# Import required modules
import sqlite3
import zlib
from lxml.etree import Element, SubElement, tostring
import datetime
from striprtf.striprtf import rtf_to_text
import os

# Connect to EWB SQLite database
conn = sqlite3.connect(f'{bibleNameAbbrev}.ewb')
cursor = conn.cursor()

# Fetch header information
cursor.execute('SELECT name, abbrev_name, lang_code, copyright FROM header')
header = cursor.fetchone()

# Start generating XML based on header info
root_el = Element('XMLBIBLE', biblename=header[0])
information = SubElement(root_el, 'INFORMATION')
SubElement(information, 'title').text = header[0]
SubElement(information, 'subject').text = 'Holy Bible'
SubElement(information, 'date').text = datetime.datetime.today().strftime('%Y-%m-%d')
SubElement(information, 'type').text = 'Bible'
SubElement(information, 'format').text = 'Zefania XML Bible Markup Language'
SubElement(information, 'identifier').text = header[1]
SubElement(information, 'language').text = header[2]
SubElement(information, 'rights').text = None if header[3] is None else rtf_to_text(header[3])


# Used to decode data for 'verse_info' attribute
def decode_verse_data(encoded_bytes):
verse_length, verse_pointer_1, verse_pointer_2, verse_pointer_3 = encoded_bytes

verse_pointer = (verse_pointer_3 << 16) | (verse_pointer_2 << 8) | verse_pointer_1

if verse_pointer % 4 != 0:
overflow = verse_pointer % 4
verse_pointer -= overflow
verse_length += overflow * 256

verse_pointer //= 4

return verse_length, verse_pointer


# Used to decode data for 'verse_info' attribute
def decode_verse_id(encoded_bytes):
verse_no, chapter_no, book_no, translation_id = encoded_bytes

if translation_id % 2 != 0:
overflow = translation_id % 2
translation_id -= overflow
book_no += overflow * 256

if book_no % 4 != 0:
overflow = book_no % 4
book_no -= overflow
chapter_no += overflow * 256

if chapter_no % 4 != 0:
overflow = chapter_no % 4
chapter_no -= overflow
verse_no += overflow * 256

verse_no //= 4
chapter_no //= 4
book_no //= 4
translation_id //= 2

return verse_no, chapter_no, book_no, translation_id


# Fetch books and corresponding stream
cursor.execute('SELECT books.rowid, name, abbrev_name, book_info, verse_info, stream FROM books JOIN streams ON books.rowid = streams.rowid ORDER BY books.rowid')

for book in cursor.fetchall():
# Create book element
book_el = SubElement(root_el, "BIBLEBOOK", bnumber=str(book[0]), bname=book[1], bsname=book[2])

book_info = book[3]

# Create chapter elements
chapters_num = int(book_info[0])
if chapters_num == 0:
# First byte represents how many chapters in the book, but if there is only one chapter it is set to 0x00
chapters_num = 1
chapters = [None] * chapters_num
for i in range(chapters_num):
chapters[i] = SubElement(book_el, 'CHAPTER', cnumber=str(i + 1))

# Fetch verse info and decompress book text
verse_info = book[4]
stream = zlib.decompress(book[5]).decode('utf-8')[2:]

# Loop through all verses - every 8 bytes represents one verse
for i in range(0, len(verse_info), 8):
verse = verse_info[i:i + 8]

# Decode data and IDs, fetch verse text based on pointers
verse_length, verse_pointer = decode_verse_data(verse[:4])
verse_no, chapter_no, book_no, translation_id = decode_verse_id(verse[-4:])
verse_text = stream[verse_pointer:verse_pointer + verse_length]

# Create verse element
SubElement(chapters[chapter_no - 1], 'VERS', vnumber=str(verse_no)).text = verse_text

# Write to XML file
with open(os.path.join(bibleNameAbbrev + '.xml'), 'wb') as file:
file.write(tostring(root_el, encoding='utf-8', pretty_print=True, xml_declaration=True))

conn.close()

Personally, I used the decoding script to check the text in several official EW Bibles we use against the versions provided by Bible Gateway and the YouVersion Bible. I did have to write scripts to download the text off these sites and convert them into Zefania XML - which for obvious reasons, I will not be releasing at this time.

It is interesting to note however that there were inconsistencies between the EW texts and the ones on Bible Gateway and YouVersion. I leaned towards the latter versions - although EW supposedly signs contracts with the publishers and obtains the translations directly from them, the fact that some of their encoded Bibles contain egregious mistakes cannot be overlooked.

A plea to the EasyWorship team - do better! You have developed a great piece of software, but your community engagement is letting it down. Some features and Bible translations have been requested by many customers for several years and there have been little to no progress updates. (Hint: official API is a good start)

And also, make the necessary corrections to your official Bible translations!

Comments
On this page
Custom Bibles for EasyWorship