lorid

convert chordpro to pdf
git clone git://git.relim.de/lorid.git
Log | Files | Refs | README | LICENSE

commit d78118139fb627fe871f723fcc20703325e3ff47
parent fd7dabebe0ae11b609c96e3c2a25426663731f36
Author: nibo <nibo@relim.de>
Date:   Sat, 21 Dec 2024 12:20:31 +0100

Add option to render table of contents

Make lorid take more than one file as input.
A table of contents makes only sense if you
can provide many files.

Diffstat:
Mchordpro.c | 46++++++++++++++++++++++++++++++++++++++++++----
Mchordpro.h | 1+
Mconfig.c | 29++++++++++++++++++++++++++---
Mconfig.h | 6++++++
Mlorid.c | 56++++++++++++++++++++++++++++++++------------------------
Mout_pdf.c | 189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mout_pdf.h | 8++++++++
7 files changed, 281 insertions(+), 54 deletions(-)

diff --git a/chordpro.c b/chordpro.c @@ -33,7 +33,7 @@ static const char *state_enums[] = { "STATE_COMMENT" }; -struct StyleProperty default_style_properties[18] = { +struct StyleProperty default_style_properties[21] = { { SF_CHORD, SPT_FONT, { .font_name = NULL } }, { SF_CHORD, SPT_SIZE, { .font_size = EMPTY_DOUBLE } }, { SF_CHORD, SPT_COLOR, { .foreground_color = NULL } }, @@ -52,6 +52,9 @@ struct StyleProperty default_style_properties[18] = { { SF_TITLE, SPT_FONT, { .font_name = NULL } }, { SF_TITLE, SPT_SIZE, { .font_size = EMPTY_DOUBLE } }, { SF_TITLE, SPT_COLOR, { .foreground_color = NULL } }, + { SF_TOC, SPT_FONT, { .font_name = NULL } }, + { SF_TOC, SPT_SIZE, { .font_size = EMPTY_DOUBLE } }, + { SF_TOC, SPT_COLOR, { .foreground_color = NULL } }, }; static const char *chord_extensions_major[] = { @@ -879,6 +882,9 @@ cho_style_new_from_config(enum SongFragmentType ftype) case SF_SUBTITLE: style = config_output_style_get(g_config->output->styles, "subtitle"); return cho_style_copy(style->style); + case SF_TOC: + style = config_output_style_get(g_config->output->styles, "toc"); + return cho_style_copy(style->style); default: style = config_output_style_get(g_config->output->styles, "text"); return cho_style_copy(style->style); @@ -3130,6 +3136,28 @@ cho_song_count(struct ChoSong **songs) return i; } +const char * +cho_song_get_title(struct ChoSong *song) +{ + struct ChoMetadata **m; + m = song->metadata; + while (*m) { + if (!strcmp((*m)->name, "title")) { + return (*m)->value; + } + m++; + } + return NULL; +} + +int +cho_song_compare(const void *a, const void *b) +{ + struct ChoSong **aa = (struct ChoSong **)a; + struct ChoSong **bb = (struct ChoSong **)b; + return str_compare(cho_song_get_title(*aa), cho_song_get_title(*bb)); +} + static void cho_song_free(struct ChoSong *song) { @@ -3489,12 +3517,21 @@ cho_directive_parse(const char *name) directive->sprop = SPT_COLOR; directive->ftype = SF_TITLE; goto END; - } /* else if (!strcmp(name, "footerfont")) { - } else if (!strcmp(name, "footersize")) { - } else if (!strcmp(name, "footercolour")) { } else if (!strcmp(name, "tocfont")) { + directive->dtype = DT_FONT; + directive->sprop = SPT_FONT; + directive->ftype = SF_TOC; } else if (!strcmp(name, "tocsize")) { + directive->dtype = DT_FONT; + directive->sprop = SPT_SIZE; + directive->ftype = SF_TOC; } else if (!strcmp(name, "toccolour")) { + directive->dtype = DT_FONT; + directive->sprop = SPT_COLOR; + directive->ftype = SF_TOC; + } /* else if (!strcmp(name, "footerfont")) { + } else if (!strcmp(name, "footersize")) { + } else if (!strcmp(name, "footercolour")) { } */ if (!strcmp(name, "transpose")) { directive->dtype = DT_CHORD; @@ -4813,6 +4850,7 @@ cho_songs_parse(FILE *fp, const char *chordpro_filepath, struct Config *config) songs = erealloc(songs, (so+1) * sizeof(struct ChoSong *)); songs[so] = NULL; free(g_transpose_history); + g_transpose_history = NULL; for (e = 0; e<g_ia; e++) { cho_image_free(g_image_assets[e]); } diff --git a/chordpro.h b/chordpro.h @@ -322,6 +322,7 @@ struct ChoSong { struct ChoSong **cho_songs_parse(FILE *fp, const char *chordpro_filepath, struct Config *config); int cho_song_count(struct ChoSong **songs); +int cho_song_compare(const void *a, const void *b); void cho_songs_free(struct ChoSong **song); int cho_line_item_count(struct ChoLineItem **items); diff --git a/config.c b/config.c @@ -329,6 +329,9 @@ config_load_default(void) { struct Config *config = emalloc(sizeof(struct Config)); config->output = emalloc(sizeof(struct ConfigOutput)); + config->output->toc = emalloc(sizeof(struct ConfigToc)); + config->output->toc->show = false; + config->output->toc->title = strdup("Table Of Contents"); config->output->chorus = emalloc(sizeof(struct ConfigChorus)); config->output->chorus->label = strdup("Chorus"); config->output->chorus->quote = false; @@ -336,7 +339,7 @@ config_load_default(void) config->output->diagram->show = true; config->output->diagram->instrument = INS_GUITAR; config->output->system = NS_COMMON; - config->output->styles = emalloc(12 * sizeof(struct OutputStyle *)); + config->output->styles = emalloc(13 * sizeof(struct OutputStyle *)); config->output->styles[0] = config_output_style_new("title"); config->output->styles[0]->style->font->name = strdup("Inter"); config->output->styles[0]->style->font->weight = FW_BOLD; @@ -367,7 +370,10 @@ config_load_default(void) config->output->styles[10] = config_output_style_new("annotation"); config->output->styles[10]->style->font->name = strdup("Inter"); config->output->styles[10]->style->font->style = FS_ITALIC; - config->output->styles[11] = NULL; + config->output->styles[11] = config_output_style_new("toc"); + config->output->styles[11]->style->font->name = strdup("Inter"); + config->output->styles[11]->style->font->size = 12.0; + config->output->styles[12] = NULL; config->output->notes = config_notes_new_default(NS_COMMON); config->parser = emalloc(sizeof(struct ConfigParser)); config->parser->chords = emalloc(sizeof(struct ConfigChords)); @@ -392,6 +398,9 @@ config_print_default(void) config_notes_print_as_toml(config->output->system, config->output->notes); printf("[output]\n\n"); printf("system = \"%s\"\n\n", config_naming_system_to_config_string(config->output->system)); + printf("[output.toc]\n\n"); + printf("show = %s\n", config->output->toc->show ? "true" : "false"); + printf("title = \"%s\"\n\n", config->output->toc->title); printf("[output.chorus]\n\n"); printf("label = \"Chorus\"\n"); printf("quote = false\n\n"); @@ -622,7 +631,7 @@ config_load(const char *filepath) } toml_table_t *output = toml_table_table(table, "output"); if (output) { - toml_table_t *notes, *chorus, *diagram; + toml_table_t *notes, *chorus, *diagram, *toc; toml_value_t value; enum NamingSystem system; enum Instrument instrument; @@ -639,6 +648,18 @@ config_load(const char *filepath) config->output->chorus->quote = value.u.b; } } + toc = toml_table_table(output, "toc"); + if (toc) { + value = toml_table_bool(toc, "show"); + if (value.ok) { + config->output->toc->show = value.u.b; + } + value = toml_table_string(toc, "title"); + if (value.ok) { + free(config->output->toc->title); + config->output->toc->title = value.u.s; + } + } diagram = toml_table_table(output, "chord_diagram"); if (diagram) { value = toml_table_bool(diagram, "show"); @@ -756,6 +777,8 @@ config_free(struct Config *config) { struct OutputStyle **start_items = config->output->styles; struct Note **start_notes = config->output->notes; + free(config->output->toc->title); + free(config->output->toc); free(config->output->chorus->label); free(config->output->chorus); while (*config->output->styles) { diff --git a/config.h b/config.h @@ -61,7 +61,13 @@ struct ConfigChordDiagram { enum Instrument instrument; }; +struct ConfigToc { + bool show; + char *title; +}; + struct ConfigOutput { + struct ConfigToc *toc; struct ConfigChorus *chorus; struct ConfigChordDiagram *diagram; enum NamingSystem system; diff --git a/lorid.c b/lorid.c @@ -24,9 +24,10 @@ main(int argc, char *argv[]) const char *chordpro_filepath = NULL; char *config_filepath = NULL; char *output = NULL; - char *file_content = NULL; - char *mem = NULL; + struct ChoSong **all_songs = NULL; struct ChoSong **songs = NULL; + struct ChoSong **begin; + int s = 0; FILE *fp; while ((o = getopt_long(argc, argv, "pc:o:vh", long_options, &option_index)) != -1) { switch(o) { @@ -64,32 +65,41 @@ main(int argc, char *argv[]) return 1; } chordpro_filepath = argv[argc-1]; + all_songs = cho_songs_parse(fp, chordpro_filepath, config); + if (!all_songs) { + LOG_DEBUG("cho_songs_parse failed."); + return 1; + } + fclose(fp); } else { int file_count = argc - optind; int i; - size_t size; - fp = open_memstream(&mem, &size); - if (!fp) { - perror("open_memstream"); - return 1; - } for (i = argc-file_count; i<argc; i++) { - chordpro_filepath = argv[i]; - file_content = file_read(chordpro_filepath); - fprintf(fp, "%s", file_content); - if (i != argc-1) { - fprintf(fp, "{new_song}\n"); + fp = fopen(argv[i], "r"); + if (!fp) { + LOG_DEBUG("fopen failed."); + return 1; } - free(file_content); + songs = cho_songs_parse(fp, argv[i], config); + if (!songs) { + LOG_DEBUG("cho_songs_parse failed."); + return 1; + } + begin = songs; + fclose(fp); + while (*songs) { + all_songs = erealloc(all_songs, (s+1) * sizeof(struct ChoSong *)); + all_songs[s] = *songs; + s++; + songs++; + } + free(begin); } - chordpro_filepath = NULL; - } - songs = cho_songs_parse(fp, chordpro_filepath, config); - if (!songs) { - LOG_DEBUG("cho_songs_parse failed."); - return 1; + all_songs = erealloc(all_songs, (s+1) * sizeof(struct ChoSong *)); + all_songs[s] = NULL; } - char *pdf_filename = out_pdf_create(argc == optind+1 ? argv[argc-1] : NULL, output ? output : NULL, songs, config); + qsort(all_songs, cho_song_count(all_songs), sizeof(struct ChoSong *), cho_song_compare); + char *pdf_filename = out_pdf_create(chordpro_filepath, output, all_songs, config); if (!pdf_filename) { LOG_DEBUG("out_pdf_new failed."); return 1; @@ -97,9 +107,7 @@ main(int argc, char *argv[]) util_log(LOG_INFO, "Writing pdf to file: '%s'.", pdf_filename); free(pdf_filename); free(output); - free(mem); - cho_songs_free(songs); + cho_songs_free(all_songs); config_free(config); - fclose(fp); return 0; } diff --git a/out_pdf.c b/out_pdf.c @@ -963,6 +963,13 @@ pdf_text_free(struct PDFText *text) free(text); } +static void +toc_entry_free(struct TocEntry *entry) +{ + free(entry->title); + free(entry); +} + static struct PDFPage * pdf_page_new(void) { @@ -1010,6 +1017,40 @@ pdf_page_free(struct PDFPage *page) free(page); } +static struct PDFContent * +pdf_content_new(void) +{ + struct PDFContent *content = emalloc(sizeof(struct PDFContent)); + content->pages = NULL; + content->toc = NULL; + return content; +} + +static void +pdf_content_free(struct PDFContent *content) +{ + if (!content) { + return; + } + struct PDFPage **p; + struct TocEntry **toc; + p = content->pages; + while (*p) { + pdf_page_free(*p); + p++; + } + free(content->pages); + toc = content->toc; + if (toc) { + while (*toc) { + toc_entry_free(*toc); + toc++; + } + free(content->toc); + } + free(content); +} + static void spaces_free(struct SpaceNeeded **spaces) { @@ -1450,6 +1491,79 @@ pdf_texts_add_text( return true; } +/* static pdfio_dict_t * +annot_within_document_create(struct TocEntry *toc) +{ + pdfio_rect_t rect; + pdfio_dict_t *annot, *action; + rect.x1 = ctx->x; + rect.x2 = ctx->x + width; + rect.y1 = ctx->y - 2.0; + rect.y2 = ctx->y + style->font->size * 0.8; + annot = pdfioDictCreate(g_pdf_file); + if (!pdfioDictSetName(annot, "Subtype", "Link")) { + LOG_DEBUG("pdfioDictSetName failed."); + return false; + } + if (!pdfioDictSetRect(annot, "Rect", &rect)) { + LOG_DEBUG("pdfioDictSetRect failed."); + return false; + } + action = pdfioDictCreate(g_pdf_file); + if (!pdfioDictSetName(action, "S", "GoTo")) { + LOG_DEBUG("pdfioDictSetName failed."); + return false; + } + if (!pdfioDictSetName(action, "D", destination)) { + LOG_DEBUG("pdfioDictSetName failed."); + return false; + } + // if (!pdfioArrayAppendDict(ctx->content->pages[ctx->page]->annots, annot)) { + // LOG_DEBUG("pdfioArrayAppendDict failed."); + // return false; + // } + return true; +} */ + +static bool +pdf_toc_create( + struct PDFContent **toc_content, + struct PDFContent *pdf_content, + struct ChoSong **songs, + struct Config *config +) +{ + struct PDFContext ctx; + struct PDFText ***texts; + ctx.text = 0; + ctx.page = 0; + ctx.x = MARGIN_HORIZONTAL; + ctx.y = MEDIABOX_HEIGHT - MARGIN_TOP; + ctx.content = pdf_content_new(); + ctx.content->pages = emalloc(sizeof(struct PDFPage *)); + ctx.content->pages[ctx.page] = pdf_page_new(); + texts = &ctx.content->pages[ctx.page]->texts; + struct OutputStyle *toc_style = config_output_style_get(config->output->styles, "toc"); + struct TocEntry **toc; + toc = pdf_content->toc; + while (*toc) { + // annot_within_document_create(*toc); + if (!pdf_texts_add_text(&ctx, (*toc)->title, toc_style->style, A_LEFT)) { + LOG_DEBUG("pdf_texts_add_text failed."); + return false; + } + printf("%s\n", (*toc)->title); + toc++; + } + *texts = erealloc(*texts, (ctx.text+1) * sizeof(struct PDFText *)); + (*texts)[ctx.text] = NULL; + ctx.page++; + ctx.content->pages = erealloc(ctx.content->pages, (ctx.page+1) * sizeof(struct PDFPage *)); + ctx.content->pages[ctx.page] = NULL; + *toc_content = ctx.content; + return true; +} + static bool pdf_content_create( struct PDFContent **out, @@ -1458,7 +1572,6 @@ pdf_content_create( struct Obj **img_objs ) { - struct ChoSong **s; struct ChoMetadata **m; struct ChoSection **se; struct ChoLine **li; @@ -1478,24 +1591,25 @@ pdf_content_create( ctx.image = 0; ctx.diagram = 0; ctx.page = 0; + ctx.toc_entry = 0; ctx.spaces = NULL; - ctx.content = emalloc(sizeof(struct PDFContent)); + ctx.content = pdf_content_new(); ctx.content->pages = emalloc(sizeof(struct PDFPage *)); ctx.content->pages[ctx.page] = pdf_page_new(); - s = songs; texts = &ctx.content->pages[ctx.page]->texts; imgs = &ctx.content->pages[ctx.page]->images; diagrams = &ctx.content->pages[ctx.page]->diagrams; - while (*s) { + int s; + for (s = 0; songs[s]; s++) { if (config->output->diagram->show) { struct ChoChord **chords = NULL; - if (!out_pdf_get_chords(*s, &chords)) { + if (!out_pdf_get_chords(songs[s], &chords)) { LOG_DEBUG("out_pdf_get_chords failed."); return false; } if (chords) { qsort(chords, cho_chords_len(chords), sizeof(struct ChoChord *), cho_chord_compare); - dgrams = chord_diagrams_create(config, &chords, (*s)->diagrams); + dgrams = chord_diagrams_create(config, &chords, songs[s]->diagrams); dgrams_begin = dgrams; while (*dgrams) { // debug_chord_diagram_print(*dgrams); @@ -1508,9 +1622,15 @@ pdf_content_create( } cho_chords_free(chords); } - m = (*s)->metadata; + m = songs[s]->metadata; while (*m) { if (!strcmp((*m)->name, "title")) { + ctx.content->toc = erealloc(ctx.content->toc, (ctx.toc_entry+1) * sizeof(struct TocEntry *)); + ctx.content->toc[ctx.toc_entry] = emalloc(sizeof(struct TocEntry)); + ctx.content->toc[ctx.toc_entry]->title = strdup((*m)->value); + ctx.content->toc[ctx.toc_entry]->page_index = ctx.page; + ctx.content->toc[ctx.toc_entry]->page_y = ctx.y; + ctx.toc_entry++; if (!pdf_texts_add_text(&ctx, (*m)->value, (*m)->style, A_CENTER)) { LOG_DEBUG("pdf_texts_add_text failed."); return false; @@ -1518,7 +1638,7 @@ pdf_content_create( } m++; } - m = (*s)->metadata; + m = songs[s]->metadata; while (*m) { if (!strcmp((*m)->name, "subtitle")) { /* @@ -1540,7 +1660,7 @@ pdf_content_create( m++; } ctx.y -= 30.0; - se = (*s)->sections; + se = songs[s]->sections; while (*se) { if ((*se)->label) { if (!pdf_texts_add_text(&ctx, (*se)->label->text, (*se)->label->style, A_LEFT)) { @@ -1795,7 +1915,6 @@ pdf_content_create( ctx.y -= SECTION_GAP_WIDTH; se++; } - s++; } *texts = erealloc(*texts, (ctx.text+1) * sizeof(struct PDFText *)); (*texts)[ctx.text] = NULL; @@ -1806,24 +1925,36 @@ pdf_content_create( ctx.page++; ctx.content->pages = erealloc(ctx.content->pages, (ctx.page+1) * sizeof(struct PDFPage *)); ctx.content->pages[ctx.page] = NULL; + ctx.content->toc = erealloc(ctx.content->toc, (ctx.toc_entry+1) * sizeof(struct TocEntry *)); + ctx.content->toc[ctx.toc_entry] = NULL; *out = ctx.content; return true; } -static void -pdf_content_free(struct PDFContent *content) +static bool +pdf_toc_render(struct PDFContent *content, pdfio_file_t *file) { - if (!content) { - return; - } - struct PDFPage **p; - p = content->pages; - while (*p) { - pdf_page_free(*p); - p++; + struct PDFPage **pages; + struct PDFText **texts; + pdfio_stream_t *stream; + pages = content->pages; + int p; + for (p = 0; pages[p]; p++) { + texts = pages[p]->texts; + stream = out_pdf_page_create(file, NULL, pages[p]->annots); + while (*texts) { + if (!out_pdf_text_show(stream, *texts)) { + LOG_DEBUG("out_pdf_text_show failed."); + return false; + } + texts++; + } + if (!pdfioStreamClose(stream)) { + LOG_DEBUG("pdfioStreamClose failed."); + return false; + } } - free(content->pages); - free(content); + return true; } static bool @@ -1901,7 +2032,8 @@ out_pdf_create( struct Font **needed_fonts; struct Obj **img_objs = NULL; struct Obj *fnt; - struct PDFContent *pdf_content; + struct PDFContent *pdf_content = NULL; + struct PDFContent *toc_content = NULL; pdfio_rect_t media_box_a4 = { 0.0, 0.0, MEDIABOX_WIDTH, MEDIABOX_HEIGHT }; pdfio_rect_t crop_box = { 0.0, 0.0, MEDIABOX_WIDTH, MEDIABOX_HEIGHT }; int f = 0; @@ -1965,11 +2097,22 @@ out_pdf_create( LOG_DEBUG("pdf_content_create failed."); return NULL; } + if (config->output->toc->show) { + if (!pdf_toc_create(&toc_content, pdf_content, songs, config)) { + LOG_DEBUG("pdf_toc_create failed."); + return false; + } + if (!pdf_toc_render(toc_content, g_pdf_file)) { + LOG_DEBUG("pdf_toc_render failed."); + return false; + } + } if (!pdf_content_render(pdf_content, g_pdf_file)) { LOG_DEBUG("pdf_content_render failed."); return NULL; } objs_free(img_objs); + pdf_content_free(toc_content); pdf_content_free(pdf_content); if (!pdfioFileClose(g_pdf_file)) { LOG_DEBUG("pdfioFileClose failed."); diff --git a/out_pdf.h b/out_pdf.h @@ -36,6 +36,12 @@ struct CharPosition { int text_index; }; +struct TocEntry { + int page_index; + double page_y; + char *title; +}; + struct PDFImage { double x; double y; @@ -61,6 +67,7 @@ struct PDFPage { struct PDFContent { struct PDFPage **pages; + struct TocEntry **toc; }; struct PDFContext { @@ -71,6 +78,7 @@ struct PDFContext { int image; int diagram; int page; + int toc_entry; struct SpaceNeeded **spaces; double biggest_font_size; size_t consumed_lyrics;