Commit a05da910 authored by platypro's avatar platypro

Added basic PDF support. Many widgets have been changed.

parent bbd8f84e
......@@ -4,22 +4,11 @@ Quiz Grinder TODO
General
* [X] Command-line interface for backend
* [X] Static exercises
* [ ] Persistent users
Quiz export types
* [X] Network quiz
* [X] raw JSON export
* [ ] PDF export
* [ ] Exam export (Many of one quiz, with QR codes)
* [ ] Party quiz
Widgets
* [X] TextView
* [X] SVGView
* [X] NumberInput
* [X] TextInput
* [X] ChoiceInput
* [ ] Custom widgets
* [X] PDF export
Docs
* [ ] Building and deploying
......@@ -28,3 +17,7 @@ Docs
* [ ] Creating problems
* [ ] Web front-end
* [ ] Widget, Lua API, and Exercise reference
Future
* [ ] Exam export (Many of one quiz, with QR codes)
* [ ] Party quiz
......@@ -2,7 +2,9 @@ cmake_minimum_required(VERSION 3.0)
project(quizgrinder C)
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_C_FLAGS -Wall)
set(CMAKE_C_FLAGS "-Wall -O0")
option(QUIZGRINDER_BUILD_PDF "PDF Support" ON)
find_package(Threads)
find_package(PkgConfig)
......@@ -17,12 +19,6 @@ set(CMAKE_INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR} ${CMAKE_INSTALL_PREFIX}/lib
configure_file("src/paths.h.in" "${CMAKE_CURRENT_BINARY_DIR}/paths.h")
include_directories(
${CMAKE_CURRENT_BINARY_DIR}
${DEPS_INCLUDE}
src
)
set(SRCS
src/quizgrinder.c
src/exercise.c
......@@ -34,6 +30,15 @@ set(SRCS
${DEPS_SRC}
)
IF(QUIZGRINDER_BUILD_PDF)
pkg_search_module(CAIRO cairo)
pkg_search_module(RSVG librsvg-2.0)
pkg_search_module(PANGO pango)
pkg_search_module(PANGOCAIRO pangocairo)
set(SRCS ${SRCS} src/pdf.c)
add_definitions(-D_QUIZGRINDER_BUILD_PDF)
ENDIF()
add_executable(quizgrinder ${SRCS})
install(TARGETS quizgrinder DESTINATION ${CMAKE_INSTALL_FULL_BINDIR})
......@@ -48,3 +53,25 @@ target_link_libraries(quizgrinder
${DEPS_LIBS}
${CMAKE_THREAD_LIBS_INIT}
)
target_include_directories(quizgrinder PUBLIC
${CMAKE_CURRENT_BINARY_DIR}
${DEPS_INCLUDE}
src
)
if(QUIZGRINDER_BUILD_PDF)
target_link_libraries(quizgrinder
${CAIRO_LIBRARIES}
${RSVG_LIBRARIES}
${PANGO_LIBRARIES}
${PANGOCAIRO_LIBRARIES}
)
message(STATUS ${RSVG_INCLUDE_DIRS})
target_include_directories(quizgrinder PUBLIC
${CAIRO_INCLUDE_DIRS}
${RSVG_INCLUDE_DIRS}
${PANGO_INCLUDE_DIRS}
${PANGOCAIRO_INCLUDE_DIRS}
)
endif()
add_subdirectory(lua)
#libpmustache build settings
if(QUIZGRINDER_BUILD_PDF)
SET(PMUSTACHE_BUILD_GIO true)
endif()
add_subdirectory(libpmustache)
#WJElement build settings
......
Subproject commit 45493414a796fb57d7b23cebe128dd9b45c45198
Subproject commit 43aed4c0188aee04cdd78aad6d1cec6f5f8eb5e8
......@@ -17,4 +17,7 @@
#include <stdint.h>
#include <stdbool.h>
#define STR(a) #a
#define XSTR(a) STR(a)
#endif
......@@ -112,7 +112,7 @@ bool user_nextQuestion(PG_STATE* state, USER* u, WJWriter w)
} else WJWUInt32(KEYCOUNT(KEY_HINT), 0, w);
WJWString(KEY_CALCULATOR, u->questionAt->eref->calculator, true, w);
WJWString(KEY_NAME, u->questionAt->eref->name, true, w);
exercise_writeWidgets(state, u->questionAt, w);
exercise_writeJSONWidgets(state, u->questionAt, w);
return true;
}
......@@ -200,7 +200,7 @@ bool session_common_process(PG_STATE* state, WJElement request, WJWriter respons
u->hintAt = u->hintAt->next;
}
else
exercise_writeSolutions(state, u->questionAt, response);
exercise_writeJSONSolutions(state, u->questionAt, response);
}
}
else if(!strcmp(action, ACTION_QUESTION_CHECK))
......
......@@ -44,7 +44,7 @@ EXERCISE* mkExercise(PG_STATE* state)
return exer;
}
EXERTEMPLATE* exercise_mkTemplate(QUESTION* q)
EXERTEMPLATE* exercise_mkTemplate(PG_STATE* state, QUESTION* q, char* name)
{
//Add one to question template list
EXERTEMPLATE* prop = calloc(1, sizeof(EXERTEMPLATE));
......@@ -52,6 +52,44 @@ EXERTEMPLATE* exercise_mkTemplate(QUESTION* q)
prop->next = q->templates;
q->templates = prop;
TEMPLATECONTAINER* tc = state->exercise.templates;
while(tc)
{
if(!strcmp(tc->filename, name))
break;
tc = tc->next;
}
if(!tc)
{
tc = calloc(1, sizeof(TEMPLATECONTAINER));
if(!tc) return false;
size_t l = strlen(q->eref->basepath) + strlen(name) + 3;
char* filename = alloca(l);
snprintf(filename, l, "%s/t%s", q->eref->basepath, name);
tc->src = mustache_eatFile(filename);
if(!tc->src)
{
printf("Could not load template %s\n", filename);
free(tc);
return false;
}
size_t flen = strlen(name);
tc->filename = malloc(flen + 1);
if(!tc->filename) return false;
strcpy(tc->filename, name);
tc->template = mustache_mkIndex(tc->src, 0);
tc->next = state->exercise.templates;
state->exercise.templates = tc;
}
prop->builder.template = tc->template;
return prop;
}
......@@ -130,10 +168,11 @@ char* exercise_load(PG_STATE* state, char* filename)
if(!exer)
{
char* exerPath = malloc(strlen(state->path_exercise) + strlen(exerId) + 1);
size_t exerPath_len = strlen(state->path_exercise) + strlen(exerId) + 2;
char* exerPath = malloc(exerPath_len);
if(!exerPath) { return NULL; }
sprintf(exerPath, "%s/%s", state->path_exercise, exerId);
snprintf(exerPath, exerPath_len, "%s/%s", state->path_exercise, exerId);
char* file = exercise_loadResource(exerPath, "exercise.json");
FILE* f = fopen(file, "r");
......@@ -178,17 +217,28 @@ char* exercise_load(PG_STATE* state, char* filename)
else if(_IS(name, WJR_TYPE_ARRAY, KEYLIST(KEY_TEMPLATE)))
{
char* tobj;
void* baseContext;
while((tobj = WJRNext(name, 15, exer_rdr)))
{
EXERTEMPLATE* etemp = exercise_mkTemplate(&exer->staticQuestion);
EXERTEMPLATE* etemp = NULL;
char* telem;
char* srcName = NULL;
while((telem = WJRNext(tobj, 15, exer_rdr)))
{
if(_IS(telem, WJR_TYPE_STRING, KEY_PATH))
etemp->src = WJRStringLoad(NULL, exer_rdr);
srcName = WJRStringLoad(NULL, exer_rdr);
else if(_IS(telem, WJR_TYPE_OBJECT, KEY_SUBS))
etemp->subs = WJEOpenDocument(exer_rdr, telem, NULL, NULL);
baseContext = WJEOpenDocument(exer_rdr, telem, NULL, NULL);
}
if(srcName)
{
etemp = exercise_mkTemplate(state, &exer->staticQuestion, srcName);
etemp->builder.escape = NULL;
etemp->builder.provider = &WJEMustacheProvider;
}
if(etemp)
etemp->builder.baseContext = baseContext;
if(srcName) free(srcName);
}
}
else if(_IS(name, WJR_TYPE_ARRAY, KEYLIST(KEY_WIDGET)))
......@@ -426,47 +476,42 @@ size_t writeWJW(char *data, size_t length, void *userdata)
return (WJWStringN(NULL, data, length, FALSE, w) ? length : 0);
}
bool expandTemplate(struct PG_State* state, QUESTION* q, EXERTEMPLATE* temp, WJWriter w)
PROCESSEDSTRING exercise_processString(QUESTION* q, char* src)
{
TEMPLATECONTAINER* tc = state->exercise.templates;
while(tc)
{
if(!strcmp(tc->filename, temp->src))
break;
tc = tc->next;
}
if(!tc)
PROCESSEDSTRING result = {0};
result.string = src;
result.type = PSTYPE_STRING;
while(src)
{
tc = calloc(1, sizeof(TEMPLATECONTAINER));
if(!tc) return false;
size_t l = strlen(q->eref->basepath) + strlen(temp->src) + 3;
char* filename = alloca(l);
snprintf(filename, l, "%s/t%s", q->eref->basepath, temp->src);
tc->src = mustache_eatFile(filename);
if(!tc->src)
if(*src == '\\' || *src != '$')
return result;
else if(!strncmp(src + 1, KEY_TEMPLATE "<", strlen(KEY_TEMPLATE) + 1))
{
printf("Could not load template %s\n", filename);
free(tc);
return false;
uint32_t spid = strtoul(src + strlen(KEY_TEMPLATE) + 2, NULL, 0);
EXERTEMPLATE* temp = q->templates;
while(temp)
{
if(temp->id == spid)
{
result.template = &temp->builder;
result.type = PSTYPE_TEMPLATE;
return result;
}
temp = temp->next;
}
return result;
}
size_t flen = strlen(temp->src);
tc->filename = malloc(flen + 1);
if(!tc->filename) return false;
strcpy(tc->filename, temp->src);
tc->template = mustache_mkIndex(tc->src, 0);
tc->next = state->exercise.templates;
state->exercise.templates = tc;
else if(!strncmp(src + 1, KEY_SVG ":", strlen(KEY_SVG) + 1))
{
result.class = PSCLASS_SVG;
result.string += strlen(KEY_SVG) + 2;
src += strlen(KEY_SVG) + 2;
}
else return result;
}
PMUS_CONTEXT ctx = {.provider=WJEMustacheProvider, .write=writeWJW, .template=tc->template};
mustache_generate(&ctx, w, temp->subs);
return true;
return result;
}
bool writeWidget(PG_STATE* state, QUESTION* question, WJElement q, WJWriter w, char* name)
......@@ -483,24 +528,19 @@ bool writeWidget(PG_STATE* state, QUESTION* question, WJElement q, WJWriter w, c
if(child->type == WJR_TYPE_STRING)
{
char* str = WJEString(child, NULL, WJE_GET, NULL);
if(*str == '\\' || *str != '$')
WJEWriteDocument(child, w, child->name);
else if(!strncmp(str + 1, KEY_TEMPLATE ":", strlen(KEY_TEMPLATE)+1))
PROCESSEDSTRING proc = exercise_processString(question, str);
switch(proc.type)
{
uint32_t spid = strtoul(str + strlen(KEY_TEMPLATE) + 2, NULL, 0);
EXERTEMPLATE* temp = question->templates;
while(temp)
{
if(temp->id == spid)
{
WJWString(child->name, "", false, w);
WJWString(NULL,
expandTemplate(state, question, temp, w) ? "" : "ERROR", true, w);
break;
}
temp = temp->next;
}
case PSTYPE_STRING:
WJWString(child->name, proc.string, true, w);
break;
case PSTYPE_TEMPLATE:
WJWString(child->name, proc.class == PSCLASS_SVG ? "$svg:" : "", false, w);
mustache_generate(proc.template, writeWJW, w);
WJWString(NULL, "", true, w);
break;
default:
printf("Could not understand special property \"%s\"\n", str);
}
}
else if(child->type == WJR_TYPE_ARRAY || child->type == WJR_TYPE_OBJECT)
......@@ -515,7 +555,7 @@ bool writeWidget(PG_STATE* state, QUESTION* question, WJElement q, WJWriter w, c
return true;
}
bool exercise_writeWidgets(struct PG_State* state, QUESTION* question, WJWriter w)
bool exercise_writeJSONWidgets(struct PG_State* state, QUESTION* question, WJWriter w)
{
WJWOpenArray(KEYLIST(KEY_WIDGET), w);
......@@ -536,7 +576,7 @@ bool exercise_writeWidgets(struct PG_State* state, QUESTION* question, WJWriter
return true;
}
bool exercise_writeSolutions(struct PG_State* state, QUESTION* question, WJWriter w)
bool exercise_writeJSONSolutions(struct PG_State* state, QUESTION* question, WJWriter w)
{
WIDGET* wid = question->widgets;
WJWOpenArray(KEY_SOLUTION, w);
......@@ -558,9 +598,9 @@ bool exercise_writeSolutions(struct PG_State* state, QUESTION* question, WJWrite
bool widget_checkAnswer(WIDGET* w, char* ans, char* uans)
{
if(!ans || !uans) return false;
if(!strcmp(w->type, "textInput"))
if(!strcmp(w->type, WIDGET_TEXT_INPUT))
{
if(!WJEBool(w->options, "caps", WJE_GET, FALSE))
if(!WJEBool(w->options, KEY_TEXTINPUT_CAPS, WJE_GET, FALSE))
{
while (tolower(*ans) && (tolower(*ans) == tolower(*uans)))
ans++, uans++;
......@@ -568,18 +608,18 @@ bool widget_checkAnswer(WIDGET* w, char* ans, char* uans)
}
return !strcmp(ans, uans);
}
else if(!strcmp(w->type, "numberInput"))
else if(!strcmp(w->type, WIDGET_NUMBER_INPUT))
{
char* end;
double nuans = strtod(uans, &end);
if(*end) return false;
double nans = strtod( ans, &end);
if(*end) return false;
uint32_t mindp = WJEUInt32(w->options, "mindp", WJE_GET, FALSE);
uint32_t mindp = WJEUInt32(w->options, KEY_NUMBERINPUT_MINDP, WJE_GET, FALSE);
double exponent = pow(10, mindp);
return (round(nuans * exponent) == round(nans * exponent));
}
else if(!strcmp(w->type, "choiceInput"))
else if(!strcmp(w->type, WIDGET_CHOICE_INPUT))
{
// This has a specific format and length, just compare it.
return !strcmp(ans, uans);
......@@ -638,9 +678,7 @@ bool destroyQ_internal(QUESTION* quiz, bool freeWidgetFields)
while(temp)
{
EXERTEMPLATE* temp_next = temp->next;
WJECloseDocument(temp->subs);
if(freeWidgetFields)
free(temp->src);
WJECloseDocument(temp->builder.baseContext);
free(temp);
temp = temp_next;
}
......@@ -689,6 +727,7 @@ void exercise_cleanup(PG_STATE* state)
EXERCISE* exer_next = exer->next;
WJECloseDocument(exer->baseOptions);
WJECloseDocument(exer->staticStore);
free(exer->basepath);
free(exer->calculator);
free(exer->name);
free(exer->id);
......
......@@ -37,6 +37,13 @@ typedef enum StaticType {
#define STATTYPE_PURE_ "pure"
#define STATTYPE_STATIC_ "static"
#define WIDGET_TEXT_INPUT "textInput"
#define WIDGET_TEXT_VIEW "textView"
#define WIDGET_LINE_INPUT "lineInput"
#define WIDGET_NUMBER_INPUT "numberInput"
#define WIDGET_IMAGE_VIEW "imageView"
#define WIDGET_CHOICE_INPUT "choiceInput"
typedef struct Question
{
struct Exercise* eref;
......@@ -44,6 +51,7 @@ typedef struct Question
struct Widget* widget_last;
WJElement hints;
struct ExerTemplate* templates;
} QUESTION;
typedef struct Exercise
......@@ -61,6 +69,7 @@ typedef struct Exercise
STATICTYPE staticType;
char* id;
} EXERCISE;
typedef struct ExerciseRef
......@@ -85,8 +94,7 @@ typedef struct ExerTemplate
{
struct ExerTemplate* next;
uint32_t id;
char* src;
WJElement subs;
PMUS_BUILDER builder;
} EXERTEMPLATE;
typedef struct Widget
......@@ -141,14 +149,39 @@ typedef struct Quiz
uint16_t prime;
} QUIZ;
typedef enum ProcessedString_Type
{
PSTYPE_INVALID,
PSTYPE_TEMPLATE,
PSTYPE_STRING,
} PROCESSEDSTRING_TYPE;
typedef enum ProcessedString_Class
{
PSCLASS_NORMAL,
PSCLASS_SVG
} PROCESSEDSTRING_CLASS;
typedef struct ProcessedString
{
struct ProcessedString* child;
PROCESSEDSTRING_TYPE type;
PROCESSEDSTRING_CLASS class;
union {
PMUS_BUILDER* template;
char* string;
};
} PROCESSEDSTRING;
extern bool exercise_loadAll(struct PG_State* state);
extern char* exercise_loadOne(struct PG_State* state, char* set);
extern PROBLEMSET* exercise_getSet(struct PG_State* state, char* name);
extern GENMODE exercise_toGenMode(char* name);
extern EXERTEMPLATE* exercise_mkTemplate(QUESTION* q);
extern EXERTEMPLATE* exercise_mkTemplate(struct PG_State* state, QUESTION* q, char* name);
extern uint8_t exercise_checkAnswer(QUESTION* question, WJElement* solutions);
extern bool exercise_writeWidgets(struct PG_State* state, QUESTION* question, WJWriter w);
extern bool exercise_writeSolutions(struct PG_State* state, QUESTION* question, WJWriter w);
PROCESSEDSTRING exercise_processString(QUESTION* q, char* src);
extern bool exercise_writeJSONWidgets(struct PG_State* state, QUESTION* question, WJWriter w);
extern bool exercise_writeJSONSolutions(struct PG_State* state, QUESTION* question, WJWriter w);
extern void exercise_cleanup(struct PG_State* state);
extern QUIZ* exercise_mkQuiz(struct PG_State* state, PROBLEMSET* pset, GENMODE type, uint8_t numQuestions);
......
......@@ -36,6 +36,24 @@
#define KEY_TEMPLATE "template"
#define KEY_PATH "path"
#define KEY_SUBS "subs"
#define KEY_SVG "svg"
#define KEY_IMAGE "image"
// Widget keys
#define KEY_TEXTVIEW_TEXT "text"
#define KEY_TEXTVIEW_SHOWINSOLUTION "showInSolution"
#define KEY_TEXTINPUT_CAPS "caps"
#define KEY_TEXTINPUT_HINT "hint"
#define KEY_TEXTINPUT_LENGTH "length"
#define KEY_NUMBERINPUT_MINDP "mindp"
#define KEY_CHOICEINPUT_CHOICES "choices"
#define KEY_CHOICEINPUT_DISPLAY "display"
#define KEY_CHOICEINPUT_NUMSELECTIONS "numSelections"
#define KEY_IMAGEVIEW_SRC "src"
#define KEYLIST(k) k "s"
#define KEYCOUNT(k) k "Count"
......
/* Aeden McClain (c) 2019
* web: https://www.platypro.net
* email: [email protected]
* License info at bottom.
*
* This file is a part of libpmustache.
*/
#include "common.h"
#include "pdf.h"
#include <cairo/cairo.h>
#include <cairo/cairo-pdf.h>
#include <librsvg/rsvg.h>
#include <pango/pango.h>
#include <pango/pangocairo.h>
#include <math.h>
#include <stdarg.h>
#include "pmustache/template.h"
#include "pmustache/provider.wje.h"
#include "pmus-input-stream.h"
#include "quizgrinder.h"
#include "exercise.h"
#include "keys.h"
void pdf_set_xy(struct WidgetDrawCtx* ctx, int x, int y)
{
cairo_matrix_t newMatrix = ctx->matrix;
newMatrix.x0 += x;
newMatrix.y0 += y;
cairo_set_matrix(ctx->cairo, &newMatrix);
}
void pdf_translate_xy(struct WidgetDrawCtx* ctx, int x, int y)
{
cairo_matrix_translate(&ctx->matrix, x, y);
cairo_set_matrix(ctx->cairo, &ctx->matrix );
}
void pdf_apply_xy(struct WidgetDrawCtx* ctx, int x, int y)
{
ctx->matrix.x0 = x + ctx->m;
ctx->matrix.y0 = y + ctx->m;
cairo_set_matrix(ctx->cairo, &ctx->matrix);
}
void pdf_translate_reset(struct WidgetDrawCtx* ctx)
{
ctx->matrix.x0 = ctx->m;
ctx->matrix.y0 = ctx->m;
cairo_set_matrix(ctx->cairo, &ctx->matrix);
}
void pdf_pushy(struct WidgetDrawCtx* ctx, int y)
{ pdf_translate_xy(ctx, 0, y); }
double pdf_set_text(struct WidgetDrawCtx* ctx, char* text, int textLen, int width)
{
PangoRectangle extents;
pango_layout_set_text(ctx->textLayout, text, textLen);
pango_layout_set_width(ctx->textLayout, width * PANGO_SCALE);
pango_layout_get_extents(ctx->textLayout, NULL, &extents);
return (extents.height + extents.y) / PANGO_SCALE;
}
RsvgHandle* pdf_loadSvg(PROCESSEDSTRING* str)
{
GError* err = NULL;
GInputStream* stream;
if(str->type == PSTYPE_TEMPLATE)
stream = G_INPUT_STREAM(pmus_input_stream_open(str->template, &err));
else
stream = G_INPUT_STREAM(g_memory_input_stream_new_from_data(str->string, -1, NULL));
RsvgHandle* handle =
rsvg_handle_new_from_stream_sync (
stream,
NULL,
RSVG_HANDLE_FLAG_KEEP_IMAGE_DATA,
NULL,
&err);
g_object_unref(stream);
if(!handle) printf("Error in SVG: %s\n", err->message);
return handle;
}
void pdf_question_begin(struct WidgetDrawCtx* ctx)
{
char exerNumber[13];
ctx->surface = cairo_recording_surface_create(CAIRO_CONTENT_ALPHA, NULL);
ctx->cairo = cairo_create(ctx->surface);
pango_cairo_update_context(ctx->cairo, ctx->textContext);
pango_layout_context_changed(ctx->textLayout);
cairo_set_line_width(ctx->cairo, 1);
// Print Question Number and translate canvas to right
snprintf(exerNumber, 13, "%d.", ctx->exerciseAt);
pango_layout_set_text(ctx->textLayout, exerNumber, -1);
pango_cairo_show_layout(ctx->cairo, ctx->textLayout);
cairo_translate(ctx->cairo, PDF_QNUM_PADDING, 0);
cairo_get_matrix(ctx->cairo, &ctx->matrix );
}
void pdf_question_end(struct WidgetDrawCtx* ctx)
{
double questionHeight;
if(!ctx->surface) return;
cairo_recording_surface_ink_extents (
ctx->surface, NULL, NULL, NULL, &questionHeight);
cairo_set_source_surface(ctx->cairo_base, ctx->surface, 0, 0);
cairo_paint(ctx->cairo_base);
cairo_set_source_rgb(ctx->cairo_base, 0.0, 0.0, 0.0);
cairo_destroy(ctx->cairo);
ctx->cairo = NULL;
cairo_surface_destroy(ctx->surface);
ctx->surface = NULL;
cairo_translate(ctx->cairo_base, 0, questionHeight + PDF_QPADDING);
ctx->hLeft -= questionHeight + PDF_QPADDING;
if(questionHeight > ctx->hLeft)
{
cairo_show_page (ctx->cairo_base);
cairo_identity_matrix(ctx->cairo_base);
cairo_translate(ctx->cairo_base, ctx->m, ctx->m);
ctx->hLeft = ctx->h;
}
}
void pdf_title_draw(struct WidgetDrawCtx* ctx, char* text)
{
// PDF Title
pango_layout_set_alignment(ctx->textLayout, PANGO_ALIGN_CENTER);
pango_layout_set_attributes(ctx->textLayout, ctx->attr_title);
double pushDown = pdf_set_text(ctx, text, -1, ctx->w) + PDF_WPADDING_Y;
pango_cairo_show_layout(ctx->cairo_base, ctx->textLayout);
cairo_translate(ctx->cairo_base, 0, pushDown);
pango_layout_set_alignment(ctx->textLayout, PANGO_ALIGN_LEFT);
pango_layout_set_attributes(ctx->textLayout, NULL);
ctx->hLeft -= pushDown;
}
int pdf_draw_shortfield(struct WidgetDrawCtx* ctx,
double width,
char* hint, int hintLen,
char* note, int noteLen)
{
int ySize = PDF_SHORT_FIELD_PADDING;
pdf_set_xy(ctx, 0, ySize);
cairo_set_source_rgb(ctx->cairo, 0,0,0);
if(hint)
{
ySize += pdf_set_text(ctx, hint, hintLen, width - PDF_SHORT_FIELD_SIZE - 8);
pango_cairo_show_layout(ctx->cairo, ctx->textLayout);
}
else
{
width = (width / 2) + (PDF_SHORT_FIELD_SIZE / 2);
ySize += 3 * PDF_TEXT_HEIGHT;
}
pdf_set_xy(ctx, 0, 0);
// Draw answer area
double lineHeight = ySize;
cairo_move_to(ctx->cairo,
width - PDF_SHORT_FIELD_SIZE,
lineHeight);
cairo_line_to(ctx->cairo,
width,
lineHeight);
cairo_stroke(ctx->cairo);
if(note)
{
pango_layout_set_attributes(ctx->textLayout, ctx->attr_special);
pdf_set_xy(ctx, 0, ySize);
ySize += pdf_set_text(ctx, note, noteLen, width);
pango_cairo_show_layout(ctx->cairo, ctx->textLayout);
pango_layout_set_attributes(ctx->textLayout, NULL);
}
return (ySize);
}
void pdf_generate_solutions(struct WidgetDrawCtx* ctx,
struct SolutionBuffer* sol)
{
ctx->hLeft = ctx->h;
ctx->exerciseAt = 0;
pdf_title_draw(ctx, "Solutions");
while(sol)
{
struct SolutionBuffer* sol_next = sol->next;