commit 930ce7cdcbe96051dc2545ecbcd2ecb7f11c2715
parent 29211b1c1663048aed7a0c35102c8f35b678a489
Author: nibo <nibo@relim.de>
Date: Sat, 14 Sep 2024 16:18:14 +0200
out_pdf: break line if not fitting
For now this only applies to the title,
subtitle and sections name.
Diffstat:
3 files changed, 318 insertions(+), 105 deletions(-)
diff --git a/fontconfig.c b/fontconfig.c
@@ -5,7 +5,7 @@
#include <fontconfig/fontconfig.h>
#include "fontconfig.h"
-bool file_extension_is_ttc(const char *filepath)
+static bool file_extension_is_ttc(const char *filepath)
{
int mark = -1;
int i = 0;
diff --git a/out_pdf.c b/out_pdf.c
@@ -148,25 +148,6 @@ static struct Font **out_pdf_font_get_all(struct ChoSong **songs, struct Config
return fonts;
}
-static size_t out_pdf_text_find_fitting_length(const char *str, size_t len)
-{
- char tmp[512];
- strncpy((char *)&tmp, str, len);
- tmp[len] = 0;
-
- double width;
- width = pdfioContentTextMeasure(g_current_font_obj, tmp, g_current_font_size);
-
- while (width > LINE_LEN) {
- if (tmp[len] == ' ' || tmp[len] == '\t') {
- tmp[len] = 0;
- width = pdfioContentTextMeasure(g_current_font_obj, tmp, g_current_font_size);
- }
- len--;
- }
- return len;
-}
-
static char *out_pdf_filename_generate_from_songs(struct ChoSong **songs)
{
char *filename;
@@ -352,6 +333,39 @@ static bool out_pdf_font_set(pdfio_stream_t *stream, struct Font *font)
return true;
}
+static double text_line_set_lineheight(struct TextLine *line, enum SongFragmentType ftype)
+{
+ double biggest_font_size = 0.0;
+ int tli;
+ for (tli = 0; line->items[tli]; tli++) {
+ if (line->items[tli]->style->font->size > biggest_font_size) {
+ biggest_font_size = line->items[tli]->style->font->size;
+ }
+ }
+ switch (ftype) {
+ case SF_CHORD:
+ line->height = 2.0 + biggest_font_size;
+ break;
+ default:
+ line->height = 8.0 + biggest_font_size;
+ }
+ return line->height;
+}
+
+static struct TextLine *text_line_create(const char *str, struct Style *style)
+{
+ struct TextLine *line = malloc(sizeof(struct TextLine));
+ line->items = malloc(2 * sizeof(struct TextLineItem *));
+ line->items[0] = malloc(sizeof(struct TextLineItem));
+ line->items[0]->text = strdup(str);
+ line->items[0]->style = cho_style_duplicate(style);
+ line->items[0]->x = MARGIN_HORIZONTAL;
+ line->items[1] = NULL;
+ text_line_set_lineheight(line, SF_TEXT);
+ line->btype = BT_LINE;
+ return line;
+}
+
static double text_width(struct TextLineItem *item)
{
char *name = out_pdf_fnt_name_create(item->style->font);
@@ -361,14 +375,125 @@ static double text_width(struct TextLineItem *item)
return -1.0;
}
free(name);
- /* int i = 0;
- while (item->text[i] != 0) {
- printf("%02X ", item->text[i]);
+ return pdfioContentTextMeasure(font_obj, item->text, item->style->font->size);
+}
+
+static enum Bool text_fits(const char *str, struct Style *style)
+{
+ double width;
+ char *name = out_pdf_fnt_name_create(style->font);
+ pdfio_obj_t *font_obj = out_pdf_fnt_obj_get_by_name(name);
+ if (!font_obj) {
+ fprintf(stderr, "out_pdf_fnt_obj_get_by_name failed.\n");
+ return B_ERROR;
+ }
+ free(name);
+ width = pdfioContentTextMeasure(font_obj, str, style->font->size);
+ if (width > LINE_WIDTH) {
+ return B_FALSE;
+ }
+ return B_TRUE;
+}
+
+static int find_whitespace(const char *str, size_t start)
+{
+ int i;
+ for (i = start; i>=0; i--) {
+ if (str[i] == ' ' || str[i] == '\t') {
+ return i;
+ }
+ }
+ return -1;
+}
+
+static char *text_find_fitting(const char *str, struct Style *style)
+{
+ size_t len = strlen(str);
+ size_t start = len - 1;
+ char tmp[len+1];
+ strcpy((char *)&tmp, str);
+ enum Bool fits;
+ int i;
+ do {
+ i = find_whitespace((const char *)&tmp, start);
+ if (i == -1) {
+ fprintf(stderr, "ERR: Can't split text because no whitespace was found.\n");
+ return NULL;
+ }
+ tmp[i] = 0;
+ fits = text_fits((const char *)&tmp, style);
+ if (fits == B_ERROR) {
+ fprintf(stderr, "text_fits failed.\n");
+ return NULL;
+ }
+ start = i - 1;
+ } while (!fits);
+ return strdup((char *)&tmp);
+}
+
+static struct TextLine **text_split_by_whitespace(const char *str, struct Style *style)
+{
+ struct TextLine **lines = NULL;
+ struct TextLine *line;
+ int tl = 0;
+ char tmp[strlen(str)+1];
+ strcpy((char *)&tmp, str);
+ char *text = (char *)&tmp;
+ size_t fitting_len;
+ char *fitting;
+ enum Bool fits;
+ while (1) {
+ fits = text_fits(text, style);
+ if (fits == B_ERROR) {
+ fprintf(stderr, "text_fits failed.\n");
+ return NULL;
+ }
+ if (fits) {
+ if (text[0] == ' ' || text[0] == '\t') {
+ text++;
+ }
+ line = text_line_create(text, style);
+ lines = realloc(lines, (tl+2) * sizeof(struct TextLine *));
+ lines[tl] = line;
+ tl++;
+ lines[tl] = NULL;
+ break;
+ }
+ fitting = text_find_fitting(text, style);
+ if (!fitting) {
+ fprintf(stderr, "text_find_fitting failed.\n");
+ return NULL;
+ }
+ line = text_line_create(fitting, style);
+ lines = realloc(lines, (tl+1) * sizeof(struct TextLine *));
+ lines[tl] = line;
+ tl++;
+ fitting_len = strlen(fitting);
+ text += fitting_len;
+ free(fitting);
+ }
+ return lines;
+}
+
+static enum Bool text_line_fits(struct TextLine *line)
+{
+ double width;
+ int i = 0;
+ while (line->items[i] != NULL) {
i++;
}
- printf("\n"); */
- // printf("size: %.1f\n", item->style->font->size);
- return pdfioContentTextMeasure(font_obj, item->text, item->style->font->size);
+ if (i == 0) {
+ return B_TRUE;
+ }
+ width = text_width(line->items[i-1]);
+ if (width == EMPTY) {
+ fprintf(stderr, "text_width failed.\n");
+ return B_ERROR;
+ }
+ if (line->items[i-1]->x + width > LINE_WIDTH) {
+ return B_FALSE;
+ }
+ return B_TRUE;
}
static bool out_pdf_draw_line(pdfio_stream_t *stream, struct TextLineItem *item, double y, double width, enum LineLocation line_location)
@@ -586,25 +711,6 @@ static struct SpaceNeeded *needs_space(struct SpaceNeeded **spaces, int ly, int
return NULL;
}
-static double text_line_set_lineheight(struct TextLine *line, enum SongFragmentType ftype)
-{
- double biggest_font_size = 0.0;
- int tli;
- for (tli = 0; line->items[tli]; tli++) {
- if (line->items[tli]->style->font->size > biggest_font_size) {
- biggest_font_size = line->items[tli]->style->font->size;
- }
- }
- switch (ftype) {
- case SF_CHORD:
- line->height = 2.0 + biggest_font_size;
- break;
- default:
- line->height = 8.0 + biggest_font_size;
- }
- return line->height;
-}
-
static struct Text **text_create(struct ChoSong **songs, struct Config *config)
{
struct PrintableItem *printable_item;
@@ -616,67 +722,115 @@ static struct Text **text_create(struct ChoSong **songs, struct Config *config)
struct ChoLineItemAbove **text_above;
struct SpaceNeeded space;
struct SpaceNeeded **spaces = NULL;
+ struct TextLineMetadata **lines_metadata = NULL;
+ struct TextLine **text_lines = NULL;
+ int tlm = 0;
int sn = 0;
int t = 0;
int tl = 0;
int tli = 0;
int m;
struct Text **text = NULL;
+ enum Bool fits;
for (so = 0; songs[so]; so++, t++) {
text = realloc(text, (t+1) * sizeof(struct Text *));
text[t] = malloc(sizeof(struct Text));
text[t]->lines = NULL;
for (m = 0; songs[so]->metadata[m]; m++) {
if (strcmp(songs[so]->metadata[m]->name, "title") == 0) {
- text[t]->lines = realloc(text[t]->lines, (tl+1) * sizeof(struct TextLine *));
- text[t]->lines[tl] = malloc(sizeof(struct TextLine));
- text[t]->lines[tl]->items = malloc(2 * sizeof(struct TextLineItem *));
- text[t]->lines[tl]->items[0] = malloc(sizeof(struct TextLineItem));
- text[t]->lines[tl]->items[0]->text = strdup(songs[so]->metadata[m]->value);
- text[t]->lines[tl]->items[0]->style = cho_style_duplicate(songs[so]->metadata[m]->style);
- width = text_width(text[t]->lines[tl]->items[0]);
- if (width == EMPTY) {
- fprintf(stderr, "text_width failed.\n");
+ fits = text_fits(songs[so]->metadata[m]->value, songs[so]->metadata[m]->style);
+ if (fits == B_ERROR) {
+ fprintf(stderr, "text_fits failed.\n");
return NULL;
}
- text[t]->lines[tl]->items[0]->x = MARGIN_HORIZONTAL + (LINE_LEN - width) / 2;
- text[t]->lines[tl]->items[1] = NULL;
- text[t]->lines[tl]->btype = BT_LINE;
- y -= text_line_set_lineheight(text[t]->lines[tl], SF_TEXT);
- if (y < MARGIN_BOTTOM) {
- y = MEDIABOX_HEIGHT - MARGIN_TOP;
- text[t]->lines[tl]->btype = BT_PAGE;
+ if (fits) {
+ text[t]->lines = realloc(text[t]->lines, (tl+1) * sizeof(struct TextLine *));
+ text[t]->lines[tl] = malloc(sizeof(struct TextLine));
+ text[t]->lines[tl]->items = malloc(2 * sizeof(struct TextLineItem *));
+ text[t]->lines[tl]->items[0] = malloc(sizeof(struct TextLineItem));
+ text[t]->lines[tl]->items[0]->text = strdup(songs[so]->metadata[m]->value);
+ text[t]->lines[tl]->items[0]->style = cho_style_duplicate(songs[so]->metadata[m]->style);
+ width = text_width(text[t]->lines[tl]->items[0]);
+ if (width == EMPTY) {
+ fprintf(stderr, "text_width failed.\n");
+ return NULL;
+ }
+ text[t]->lines[tl]->items[0]->x = MARGIN_HORIZONTAL + (LINE_WIDTH - width) / 2;
+ text[t]->lines[tl]->items[1] = NULL;
+ text[t]->lines[tl]->btype = BT_LINE;
+ y -= text_line_set_lineheight(text[t]->lines[tl], SF_TEXT);
+ if (y < MARGIN_BOTTOM) {
+ y = MEDIABOX_HEIGHT - MARGIN_TOP;
+ text[t]->lines[tl]->btype = BT_PAGE;
+ }
+ tl++;
+ } else {
+ fprintf(stderr, "WARN: Title doesn't fit on one line.\n");
+ text_lines = text_split_by_whitespace(songs[so]->metadata[m]->value, songs[so]->metadata[m]->style);
+ for (int i = 0; text_lines[i]; i++) {
+ width = text_width(text_lines[i]->items[0]);
+ if (width == EMPTY) {
+ fprintf(stderr, "text_width failed.\n");
+ return NULL;
+ }
+ text_lines[i]->items[0]->x = MARGIN_HORIZONTAL + (LINE_WIDTH - width) / 2;
+ text[t]->lines = realloc(text[t]->lines, (tl+1) * sizeof(struct TextLine *));
+ text[t]->lines[tl] = text_lines[i];
+ tl++;
+ }
+ free(text_lines);
}
- tl++;
}
}
for (m = 0; songs[so]->metadata[m]; m++) {
if (strcmp(songs[so]->metadata[m]->name, "subtitle") == 0) {
- printable_item = config_printable_item_get(config->output->printable_items, "subtitle");
- if (!printable_item) {
- fprintf(stderr, "config_printable_item_get failed.\n");
- return NULL;
- }
- text[t]->lines = realloc(text[t]->lines, (tl+1) * sizeof(struct TextLine *));
- text[t]->lines[tl] = malloc(sizeof(struct TextLine));
- text[t]->lines[tl]->items = malloc(2 * sizeof(struct TextLineItem *));
- text[t]->lines[tl]->items[0] = malloc(sizeof(struct TextLineItem));
- text[t]->lines[tl]->items[0]->text = strdup(songs[so]->metadata[m]->value);
- text[t]->lines[tl]->items[0]->style = cho_style_duplicate(printable_item->style);
- width = text_width(text[t]->lines[tl]->items[0]);
- if (width == EMPTY) {
- fprintf(stderr, "text_width failed.\n");
+ fits = text_fits(songs[so]->metadata[m]->value, songs[so]->metadata[m]->style);
+ if (fits == B_ERROR) {
+ fprintf(stderr, "text_fits failed.\n");
return NULL;
}
- text[t]->lines[tl]->items[0]->x = MARGIN_HORIZONTAL + (LINE_LEN - width) / 2;
- text[t]->lines[tl]->items[1] = NULL;
- text[t]->lines[tl]->btype = BT_LINE;
- y -= text_line_set_lineheight(text[t]->lines[tl], SF_TEXT);
- if (y < MARGIN_BOTTOM) {
- y = MEDIABOX_HEIGHT - MARGIN_TOP;
- text[t]->lines[tl]->btype = BT_PAGE;
+ if (fits) {
+ printable_item = config_printable_item_get(config->output->printable_items, "subtitle");
+ if (!printable_item) {
+ fprintf(stderr, "config_printable_item_get failed.\n");
+ return NULL;
+ }
+ text[t]->lines = realloc(text[t]->lines, (tl+1) * sizeof(struct TextLine *));
+ text[t]->lines[tl] = malloc(sizeof(struct TextLine));
+ text[t]->lines[tl]->items = malloc(2 * sizeof(struct TextLineItem *));
+ text[t]->lines[tl]->items[0] = malloc(sizeof(struct TextLineItem));
+ text[t]->lines[tl]->items[0]->text = strdup(songs[so]->metadata[m]->value);
+ text[t]->lines[tl]->items[0]->style = cho_style_duplicate(printable_item->style);
+ width = text_width(text[t]->lines[tl]->items[0]);
+ if (width == EMPTY) {
+ fprintf(stderr, "text_width failed.\n");
+ return NULL;
+ }
+ text[t]->lines[tl]->items[0]->x = MARGIN_HORIZONTAL + (LINE_WIDTH - width) / 2;
+ text[t]->lines[tl]->items[1] = NULL;
+ text[t]->lines[tl]->btype = BT_LINE;
+ y -= text_line_set_lineheight(text[t]->lines[tl], SF_TEXT);
+ if (y < MARGIN_BOTTOM) {
+ y = MEDIABOX_HEIGHT - MARGIN_TOP;
+ text[t]->lines[tl]->btype = BT_PAGE;
+ }
+ tl++;
+ } else {
+ fprintf(stderr, "WARN: Subtitle doesn't fit on one line.\n");
+ text_lines = text_split_by_whitespace(songs[so]->metadata[m]->value, songs[so]->metadata[m]->style);
+ for (int i = 0; text_lines[i]; i++) {
+ width = text_width(text_lines[i]->items[0]);
+ if (width == EMPTY) {
+ fprintf(stderr, "text_width failed.\n");
+ return NULL;
+ }
+ text_lines[i]->items[0]->x = MARGIN_HORIZONTAL + (LINE_WIDTH - width) / 2;
+ text[t]->lines = realloc(text[t]->lines, (tl+1) * sizeof(struct TextLine *));
+ text[t]->lines[tl] = text_lines[i];
+ tl++;
+ }
+ free(text_lines);
}
- tl++;
}
}
text[t]->lines = realloc(text[t]->lines, (tl+1) * sizeof(struct TextLine *));
@@ -687,21 +841,44 @@ static struct Text **text_create(struct ChoSong **songs, struct Config *config)
tl++;
for (se = 0; songs[so]->sections[se]; se++) {
if (songs[so]->sections[se]->label) {
- text[t]->lines = realloc(text[t]->lines, (tl+1) * sizeof(struct TextLine *));
- text[t]->lines[tl] = malloc(sizeof(struct TextLine));
- text[t]->lines[tl]->items = malloc(2 * sizeof(struct TextLineItem *));
- text[t]->lines[tl]->items[0] = malloc(sizeof(struct TextLineItem));
- text[t]->lines[tl]->items[0]->text = strdup(songs[so]->sections[se]->label->name);
- text[t]->lines[tl]->items[0]->style = cho_style_duplicate(songs[so]->sections[se]->label->style);
- text[t]->lines[tl]->items[0]->x = MARGIN_HORIZONTAL;
- text[t]->lines[tl]->items[1] = NULL;
- text[t]->lines[tl]->btype = BT_LINE;
- y -= text_line_set_lineheight(text[t]->lines[tl], SF_TEXT);
- if (y < MARGIN_BOTTOM) {
- y = MEDIABOX_HEIGHT - MARGIN_TOP;
- text[t]->lines[tl]->btype = BT_PAGE;
+ fits = text_fits(songs[so]->sections[se]->label->name, songs[so]->sections[se]->label->style);
+ if (fits == B_ERROR) {
+ fprintf(stderr, "text_fits failed.\n");
+ return NULL;
+ }
+ if (fits) {
+ text[t]->lines = realloc(text[t]->lines, (tl+1) * sizeof(struct TextLine *));
+ text[t]->lines[tl] = malloc(sizeof(struct TextLine));
+ text[t]->lines[tl]->items = malloc(2 * sizeof(struct TextLineItem *));
+ text[t]->lines[tl]->items[0] = malloc(sizeof(struct TextLineItem));
+ text[t]->lines[tl]->items[0]->text = strdup(songs[so]->sections[se]->label->name);
+ text[t]->lines[tl]->items[0]->style = cho_style_duplicate(songs[so]->sections[se]->label->style);
+ text[t]->lines[tl]->items[0]->x = MARGIN_HORIZONTAL;
+ text[t]->lines[tl]->items[1] = NULL;
+ text[t]->lines[tl]->btype = BT_LINE;
+ y -= text_line_set_lineheight(text[t]->lines[tl], SF_TEXT);
+ if (y < MARGIN_BOTTOM) {
+ y = MEDIABOX_HEIGHT - MARGIN_TOP;
+ text[t]->lines[tl]->btype = BT_PAGE;
+ }
+ tl++;
+ } else {
+ fprintf(stderr, "WARN: Section name doesn't fit on one line.\n");
+ text_lines = text_split_by_whitespace(songs[so]->sections[se]->label->name, songs[so]->sections[se]->label->style);
+ for (int i = 0; text_lines[i]; i++) {
+ width = text_width(text_lines[i]->items[0]);
+ if (width == EMPTY) {
+ fprintf(stderr, "text_width failed.\n");
+ return NULL;
+ }
+ // text_lines[i]->items[0]->x = MARGIN_HORIZONTAL + (LINE_WIDTH - width) / 2;
+ text[t]->lines = realloc(text[t]->lines, (tl+1) * sizeof(struct TextLine *));
+ text[t]->lines[tl] = text_lines[i];
+ printf("text: '%s'\n", text_lines[i]->items[0]->text);
+ tl++;
+ }
+ free(text_lines);
}
- tl++;
}
lines = songs[so]->sections[se]->lines;
for (li = 0; lines[li]; li++, tl++) {
@@ -709,6 +886,12 @@ static struct Text **text_create(struct ChoSong **songs, struct Config *config)
int text_above_len = cho_text_above_count(text_above);
// TODO: The code inside the if statement seems to me too complicated, so simplify if possible
if (text_above_len > 0) {
+ lines_metadata = realloc(lines_metadata, (tlm+1) * sizeof(struct TextLineMetadata *));
+ lines_metadata[tlm] = malloc(sizeof(struct TextLineMetadata));
+ lines_metadata[tlm]->text_index = t;
+ lines_metadata[tlm]->text_line_index = tl;
+ lines_metadata[tlm]->is_lyrics = false;
+ tlm++;
text[t]->lines = realloc(text[t]->lines, (tl+1) * sizeof(struct TextLine *));
text[t]->lines[tl] = malloc(sizeof(struct TextLine));
text[t]->lines[tl]->items = NULL;
@@ -781,6 +964,14 @@ static struct Text **text_create(struct ChoSong **songs, struct Config *config)
tli++;
text[t]->lines[tl]->items = realloc(text[t]->lines[tl]->items, (tli+1) * sizeof(struct TextLineItem *));
text[t]->lines[tl]->items[tli] = NULL;
+ fits = text_line_fits(text[t]->lines[tl]);
+ if (fits == B_ERROR) {
+ fprintf(stderr, "text_line_fits failed.\n");
+ return NULL;
+ }
+ if (!fits) {
+ fprintf(stderr, "WARN: text line (chords/annotations) doesn't fit.\n");
+ }
text[t]->lines[tl]->btype = lines[li]->btype;
y -= text_line_set_lineheight(text[t]->lines[tl], SF_CHORD);
if (y < MARGIN_BOTTOM) {
@@ -792,10 +983,17 @@ static struct Text **text_create(struct ChoSong **songs, struct Config *config)
tli = 0;
tl++;
}
+ lines_metadata = realloc(lines_metadata, (tlm+1) * sizeof(struct TextLineMetadata *));
+ lines_metadata[tlm] = malloc(sizeof(struct TextLineMetadata));
+ lines_metadata[tlm]->text_index = t;
+ lines_metadata[tlm]->text_line_index = tl;
+ lines_metadata[tlm]->is_lyrics = true;
+ tlm++;
text[t]->lines = realloc(text[t]->lines, (tl+1) * sizeof(struct TextLine *));
text[t]->lines[tl] = malloc(sizeof(struct TextLine));
text[t]->lines[tl]->items = NULL;
for (ly = 0; lines[li]->lyrics[ly]; ly++) {
+ // out_pdf_text_find_fitting_length(lines[li]->lyrics[ly]);
int ii;
double last_text_line_item_width;
struct SpaceNeeded *sp;
@@ -845,6 +1043,14 @@ static struct Text **text_create(struct ChoSong **songs, struct Config *config)
}
text[t]->lines[tl]->items = realloc(text[t]->lines[tl]->items, (tli+1) * sizeof(struct TextLineItem *));
text[t]->lines[tl]->items[tli] = NULL;
+ fits = text_line_fits(text[t]->lines[tl]);
+ if (fits == B_ERROR) {
+ fprintf(stderr, "text_line_fits failed.\n");
+ return NULL;
+ }
+ if (!fits) {
+ fprintf(stderr, "WARN: text line (lyrics) doesn't fit.\n");
+ }
text[t]->lines[tl]->btype = lines[li]->btype;
y -= text_line_set_lineheight(text[t]->lines[tl], SF_TEXT);
if (y < MARGIN_BOTTOM) {
@@ -869,6 +1075,13 @@ static struct Text **text_create(struct ChoSong **songs, struct Config *config)
}
text = realloc(text, (t+1) * sizeof(struct Text *));
text[t] = NULL;
+ struct TextLineMetadata *me;
+ for (m = 0; m<tlm; m++) {
+ me = lines_metadata[m];
+ printf("%d %d %d\n", me->text_index, me->text_line_index, me->is_lyrics);
+ free(lines_metadata[m]);
+ }
+ free(lines_metadata);
return text;
}
diff --git a/out_pdf.h b/out_pdf.h
@@ -5,7 +5,7 @@
#define MARGIN_TOP 40.0
#define MARGIN_BOTTOM 180.0
#define MARGIN_HORIZONTAL 80.0
-#define LINE_LEN MEDIABOX_WIDTH - MARGIN_HORIZONTAL * 2
+#define LINE_WIDTH MEDIABOX_WIDTH - MARGIN_HORIZONTAL * 2
#define MIN_CHORD_GAP_WIDTH 5.0
#define SECTION_GAP_WIDTH 10.0
@@ -16,12 +16,6 @@ enum Bool {
B_TRUE
};
-enum Alignment {
- LEFT,
- CENTER,
- CONTINUE
-};
-
struct Fnt {
char *name;
pdfio_obj_t *font;
@@ -39,6 +33,12 @@ struct TextLineItem {
double x;
};
+struct TextLineMetadata {
+ int text_index;
+ int text_line_index;
+ bool is_lyrics;
+};
+
struct TextLine {
struct TextLineItem **items;
double height;